Счёт убийств Некромантии переехал из XML в код
Чинит баг: скилл не засчитывал часть убийств зомби. Причин было две, и обе
закрываются одним ходом.
ПРИЧИНА 1: ОДИН КЛАСС - НЕ ВСЕ ЗОМБИ. Счёт висел append-ом на zombieTemplateMale.
Человекоподобные покрыты (effect_group у entity_class наследуется через extends, и
все шаблоны зомби сходятся к Male), но пять зомби-ЗВЕРЕЙ наследуют животную ветку
и до него не доходят вовсе:
animalZombieBear extends animalBear
animalZombieBoar extends animalBoar
animalZombieDog extends animalWolf
animalZombieVulture extends animalTemplateHostile
animalZombieVultureRadiated extends animalZombieVulture
Убийство зомбопса, зомбомедведя, зомбокабана и зомбоворона не считалось никак.
Вороны попадаются постоянно - это и была бОльшая часть "не всегда засчитывает".
ПРИЧИНА 2: target="other" - БУКВАЛЬНЫЙ УБИЙЦА. Триггер onOtherKilledSelf с
требованием EntityTagCompare tags="player" начислял только за убийство своей
рукой. Робомолот, кровотечение и питомцы требование не проходят, хотя опыт игрок
за них получает: ваниль определяет получателя не по убийце, а по DamageSource - в
EntityAlive.AwardKillXPServer, где для этого есть обращение к BuffClass (DoT) и
отдельный флаг bTrapKillXP (ловушки).
Требование было ОДНО на оба эффекта, поэтому недосчитывался и
necroZombieKillsCVar, а это урон Ножа некроманта (items.xml: Damage = CVar / 10).
Баг тихо занижал ещё и нож.
РЕШЕНИЕ: Postfix на EntityPlayer.AddKillXP. Этот метод вызывается ровно из одного
места во всей сборке - из AwardKillXPServer, то есть уже ПОСЛЕ того, как ваниль
разобрала DamageSource и решила, чей это фраг (проверено сканированием IL).
Мы не повторяем её логику и не угадываем владельца турели или автора
кровотечения - забираем готовый ответ. Наш счёт совпадает с опытом на экране по
построению, включая случаи, о которых мы не подумали.
ФИЛЬТР ПО ТЕГУ zombie, А НЕ ПО КЛАССУ. Проверено по данным: zombieBiker,
zombieArlene, zombieBoe, zombieSpider несут "entity,zombie,...", зомби-звери -
"entity,animal,zombie,zombieAnimal,...". Тег есть у всех. Работает это благодаря
тому, что Tags у entity_class НЕ наследуется через extends: каждый реально
спавнящийся зомби выписывает теги сам, а безтеговые шаблоны не спавнятся. Тег
переживёт и новых зомби из патчей игры, и чужие моды.
Повышение уровня воспроизводит MinEventActionAddProgressionLevel.Execute шаг в
шаг по его IL: GetProgressionValue, Level+1, кламп по MaxLevel, для крафтового
скилла AddCraftingSkillNotification и HandleCheckCrafting, затем
bProgressionStatsChanged и bPlayerStatsChanged под !isEntityRemote.
HandleCheckCrafting легко выбросить и дорого потерять - без него рецепты рискуют
не заметить, что открылись. Уведомление с _bAddOnlyIfNotExisting=true, чтобы в
орду не всплывал тост на каждый труп.
Config/entityclasses.xml: append снят ЦЕЛИКОМ, на его месте комментарий, почему
возвращать нельзя - XML-триггер рядом с патчем засчитает убийство своей рукой
ДВАЖДЫ. Это единственная ловушка переезда.
Сборка: 0 ошибок (4 прежних MSB3277). Проверено рефлексией по собранной DLL:
атрибут нацелен верно, перегрузка AddKillXP ровно одна, имя параметра killedEntity
совпадает с ванильным (Harmony инжектит по имени - опечатка дала бы молчаливо
неработающий патч), PatchAll подхватывает файл сам.
НЕ ПРОВЕРЕНО В ИГРЕ. Отдельно: питомцы НЕ гарантированы - патч следует решению
ванили, а не принимает его. Если ваниль не зачисляет владельцу убийство
питомцем, не зачислит и он; это отдельная работа, а не ошибка здесь.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e890999391
commit
229b436420
+25
-13
@@ -1,17 +1,29 @@
|
|||||||
<config>
|
<config>
|
||||||
<!-- Step 1: lifetime zombie kill counter.
|
<!-- СЧЁТ УБИЙСТВ ПЕРЕЕХАЛ В КОД 2026-09-16. Здесь СОЗНАТЕЛЬНО ничего нет, и вернуть это
|
||||||
zombieTemplateMale is the root template every zombie entity_class extends
|
обратно нельзя - см. HarmonySrc/NecromancyKillCreditPatch.cs.
|
||||||
(directly, or indirectly via zombieTemplateShort), so patching it here covers
|
|
||||||
every zombie variant in the game without listing them individually. -->
|
Тут стоял append на zombieTemplateMale с двумя onOtherKilledSelf-эффектами
|
||||||
<append xpath="/entity_classes/entity_class[@name='zombieTemplateMale']">
|
(ModifyCVar necroZombieKillsCVar и AddProgressionLevel craftingNecroNecromancy) под общим
|
||||||
<effect_group>
|
требованием EntityTagCompare target="other" tags="player". Он был сломан дважды:
|
||||||
<requirement name="EntityTagCompare" target="other" tags="player"/>
|
|
||||||
<triggered_effect trigger="onOtherKilledSelf" action="ModifyCVar" target="other" cvar="necroZombieKillsCVar" operation="add" value="1"/>
|
1. Один класс - не все зомби. Пять зомби-ЗВЕРЕЙ наследуют животную ветку и до
|
||||||
<!-- "Некромантия" skill: +1 level per zombie kill, capped by its own max_level.
|
zombieTemplateMale не доходят вовсе (animalZombieBear extends animalBear,
|
||||||
See progression.xml for why this drives the skill instead of reading books. -->
|
animalZombieBoar extends animalBoar, animalZombieDog extends animalWolf,
|
||||||
<triggered_effect trigger="onOtherKilledSelf" action="AddProgressionLevel" target="other" progression_name="craftingNecroNecromancy" level="1"/>
|
animalZombieVulture extends animalTemplateHostile, animalZombieVultureRadiated).
|
||||||
</effect_group>
|
Убийство зомбопса, зомбомедведя, зомбокабана и зомбоворона не считалось никак.
|
||||||
</append>
|
2. target="other" - это БУКВАЛЬНЫЙ убийца, а не тот, кому ваниль зачла убийство.
|
||||||
|
Робомолот, кровотечение и питомцы требование tags="player" не проходят, хотя опыт
|
||||||
|
игрок за них получает: ваниль определяет получателя по DamageSource, в
|
||||||
|
EntityAlive.AwardKillXPServer.
|
||||||
|
|
||||||
|
Требование было одно на оба эффекта, поэтому вместе со скиллом недосчитывался и
|
||||||
|
necroZombieKillsCVar - то есть занижался урон Ножа некроманта (items.xml: Damage =
|
||||||
|
necroZombieKillsCVar / 10).
|
||||||
|
|
||||||
|
ОБА эффекта теперь делает Postfix на EntityPlayer.AddKillXP - единственной точке, где
|
||||||
|
ваниль уже решила, чей это фраг. ЕСЛИ ВЕРНУТЬ ЭТОТ append НА МЕСТО, убийство своей рукой
|
||||||
|
будет засчитано ДВАЖДЫ: и здесь, и в патче. Ровно это и проверять, если уровень вдруг
|
||||||
|
начнёт расти по два за труп. -->
|
||||||
|
|
||||||
<!-- "Зомбособака" (Zombie Dog pet): BACKLOG.md item 3. Extends the vanilla hostile
|
<!-- "Зомбособака" (Zombie Dog pet): BACKLOG.md item 3. Extends the vanilla hostile
|
||||||
animalZombieDog (same prefab/physics/sounds - a real zombie dog model, not a reskinned
|
animalZombieDog (same prefab/physics/sounds - a real zombie dog model, not a reskinned
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
using HarmonyLib;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NecromancerTome
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Счёт убийств для скилла "Некромантия" (user report 2026-09-16: "Почему-то наш скилл
|
||||||
|
/// некроманта не всегда засчитывает убийство зомби... если робомолот убьёт зомбака, то игрок
|
||||||
|
/// получает за это опыт. Если зомби умрёт от кровотечения, которое навесил игрок, то игрок
|
||||||
|
/// получит опыт. У нас скилл некроманта в этих случаях не прибавляется. Это баг.").
|
||||||
|
///
|
||||||
|
/// WHAT WAS BROKEN, AND IT WAS TWO SEPARATE THINGS.
|
||||||
|
///
|
||||||
|
/// Until this patch the whole count was four lines of XML appended to ONE entity class in
|
||||||
|
/// Config/entityclasses.xml:
|
||||||
|
///
|
||||||
|
/// <append xpath="/entity_classes/entity_class[@name='zombieTemplateMale']">
|
||||||
|
/// <requirement name="EntityTagCompare" target="other" tags="player"/>
|
||||||
|
/// <triggered_effect trigger="onOtherKilledSelf" action="ModifyCVar" target="other" .../>
|
||||||
|
/// <triggered_effect trigger="onOtherKilledSelf" action="AddProgressionLevel" target="other" .../>
|
||||||
|
///
|
||||||
|
/// 1. ONE CLASS IS NOT EVERY ZOMBIE. Humanoids were fine - effect_group DOES inherit through
|
||||||
|
/// extends on entity_class (unlike items.xml, see progression.xml's header), and every
|
||||||
|
/// zombie template chains back to zombieTemplateMale. But the five zombie ANIMALS inherit
|
||||||
|
/// the animal branch and never reach it:
|
||||||
|
/// animalZombieBear extends animalBear, animalZombieBoar extends animalBoar,
|
||||||
|
/// animalZombieDog extends animalWolf, animalZombieVulture extends animalTemplateHostile,
|
||||||
|
/// animalZombieVultureRadiated extends animalZombieVulture
|
||||||
|
/// Killing a zombie dog, bear, boar or vulture counted for nothing at all. Zombie vultures
|
||||||
|
/// are everywhere on roads, which is most of what "не всегда засчитывает" was.
|
||||||
|
///
|
||||||
|
/// 2. target="other" IS THE LITERAL KILLER, NOT THE PLAYER WHO EARNED IT. With
|
||||||
|
/// trigger="onOtherKilledSelf" plus a requirement that "other" be tagged player, anything
|
||||||
|
/// that kills on the player's behalf fails the requirement: a robotic sledge (the turret is
|
||||||
|
/// "other"), a bleed the player applied (no direct killer at the moment of death), a summoned
|
||||||
|
/// pet (the pet is "other"). Vanilla still awards XP in all of these because it does NOT use
|
||||||
|
/// the literal killer - it resolves the crediting player from the DamageSource, in
|
||||||
|
/// EntityAlive.AwardKillXPServer(DamageSource, EntityAlive), whose body reads BuffClass
|
||||||
|
/// (DoT damage) and a dedicated bTrapKillXP flag (trap kills) before calling AddKillXP.
|
||||||
|
///
|
||||||
|
/// BOTH EFFECTS SHARED ONE REQUIREMENT, so every missed kill also failed to raise
|
||||||
|
/// necroZombieKillsCVar - and that CVar is the Necromancer's Knife's damage (items.xml: "Damage
|
||||||
|
/// = necroZombieKillsCVar / 10", recomputed continuously in buffs.xml). The bug was quietly
|
||||||
|
/// underpowering the knife too, which is why the fix keeps both effects together.
|
||||||
|
///
|
||||||
|
/// WHY THIS HOOK AND NOT A WIDER XML PATCH. Adding the five animal classes by XML would have
|
||||||
|
/// fixed cause 1 and left cause 2 untouched. EntityPlayer.AddKillXP is the single point where
|
||||||
|
/// vanilla has ALREADY decided which player gets the kill - it is called from exactly one place
|
||||||
|
/// in the whole assembly, AwardKillXPServer, after all the DamageSource resolution is done.
|
||||||
|
/// Hooking it means our count agrees with the XP number the player sees on screen by
|
||||||
|
/// construction, for every case vanilla handles, including ones nobody has thought of yet.
|
||||||
|
/// Verified by metadata scan: AwardKillXPServer is the only caller of AddKillXP.
|
||||||
|
///
|
||||||
|
/// THE XML TRIGGERS ARE GONE, NOT LEFT ALONGSIDE. Config/entityclasses.xml no longer carries
|
||||||
|
/// the effect_group - if it stayed, a kill by the player's own hand would satisfy both it and
|
||||||
|
/// this patch and count TWICE. That was the one trap of moving the count into code, and it is
|
||||||
|
/// the first thing to check if levels ever start rising two at a time.
|
||||||
|
///
|
||||||
|
/// PETS ARE NOT GUARANTEED BY THIS PATCH. The user also asked that summoned creatures count.
|
||||||
|
/// They will count if and only if vanilla itself credits the owner for a pet kill - this patch
|
||||||
|
/// follows vanilla's decision, it does not make it. Whether it does is NOT verified and is the
|
||||||
|
/// specific thing to watch for in game; if pets turn out not to be credited, that is a separate
|
||||||
|
/// piece of work (giving the pet's DamageSource an owner), not a bug in this file.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyPatch(typeof(EntityPlayer), "AddKillXP")]
|
||||||
|
public static class Patch_EntityPlayer_AddKillXP_NecromancyCount
|
||||||
|
{
|
||||||
|
public const string NecromancySkillName = "craftingNecroNecromancy";
|
||||||
|
public const string KillsCVarName = "necroZombieKillsCVar";
|
||||||
|
|
||||||
|
/// <summary>The tag every zombie carries, humanoid and animal alike. Checked against the
|
||||||
|
/// real data rather than assumed: zombieBiker/zombieArlene/zombieBoe/zombieSpider all
|
||||||
|
/// declare "entity,zombie,..." and the five zombie animals declare
|
||||||
|
/// "entity,animal,zombie,zombieAnimal,...". Note that entity Tags do NOT inherit through
|
||||||
|
/// extends (entityclasses.xml says so in a comment right on the property), which is exactly
|
||||||
|
/// why this works: every concrete, spawnable zombie spells its own tags out, and the bare
|
||||||
|
/// templates that do not are never spawned.
|
||||||
|
///
|
||||||
|
/// A tag test also ages better than the class list it replaces: any zombie added by a
|
||||||
|
/// future game version or another mod counts the moment it calls itself a zombie.</summary>
|
||||||
|
private static readonly FastTags<TagGroup.Global> ZombieTag =
|
||||||
|
FastTags<TagGroup.Global>.Parse("zombie");
|
||||||
|
|
||||||
|
public static void Postfix(EntityPlayer __instance, EntityAlive killedEntity)
|
||||||
|
{
|
||||||
|
if (__instance == null || killedEntity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!killedEntity.HasAnyTags(ZombieTag))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddKillsCVar(__instance);
|
||||||
|
AddNecromancyLevel(__instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>necroZombieKillsCVar += 1 - the same thing the removed ModifyCVar action did,
|
||||||
|
/// and the reason it is here rather than left in XML is that it shared the broken
|
||||||
|
/// requirement with the progression effect. GetCVar/SetCVar are public on EntityAlive and
|
||||||
|
/// are the same storage the buffs.xml formula reads.</summary>
|
||||||
|
private static void AddKillsCVar(EntityPlayer _player)
|
||||||
|
{
|
||||||
|
_player.SetCVar(KillsCVarName, _player.GetCVar(KillsCVarName) + 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>+1 level of Necromancy, replicating MinEventActionAddProgressionLevel.Execute
|
||||||
|
/// step for step rather than inventing a shorter version of it - its IL was read for this:
|
||||||
|
/// GetProgressionValue, Level + amount, clamp to ProgressionClass.MaxLevel, then (for a
|
||||||
|
/// crafting skill) the level-up toast and HandleCheckCrafting, then the two dirty flags.
|
||||||
|
///
|
||||||
|
/// HandleCheckCrafting is the part that would be easy to drop and expensive to miss: it is
|
||||||
|
/// what the game calls on a crafting-skill level change, and skipping it risks recipes not
|
||||||
|
/// noticing they became available. Both it and AddCraftingSkillNotification are public.
|
||||||
|
///
|
||||||
|
/// The clamp matters for a different reason than it looks: max_level is 5000, and without
|
||||||
|
/// the clamp Level would keep climbing past it forever, because nothing else limits it.</summary>
|
||||||
|
private static void AddNecromancyLevel(EntityPlayer _player)
|
||||||
|
{
|
||||||
|
Progression progression = _player.Progression;
|
||||||
|
if (progression == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressionValue pv = progression.GetProgressionValue(NecromancySkillName);
|
||||||
|
if (pv == null || pv.ProgressionClass == null)
|
||||||
|
{
|
||||||
|
// Not a crash, and not silent either: this means the skill did not load, which is a
|
||||||
|
// config problem worth seeing once in the log rather than a reason to throw inside
|
||||||
|
// a kill handler.
|
||||||
|
Debug.LogWarning("[NecromancerTome] NecromancyKillCredit: progression '" +
|
||||||
|
NecromancySkillName + "' not found - kill not counted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oldLevel = pv.Level;
|
||||||
|
int maxLevel = pv.ProgressionClass.MaxLevel;
|
||||||
|
int newLevel = oldLevel + 1;
|
||||||
|
if (newLevel > maxLevel)
|
||||||
|
{
|
||||||
|
newLevel = maxLevel;
|
||||||
|
}
|
||||||
|
if (newLevel == oldLevel)
|
||||||
|
{
|
||||||
|
// Already at 5000. The CVar above still went up on purpose - the knife's damage is
|
||||||
|
// not capped by the skill's max_level, and the player who is past the cap should
|
||||||
|
// keep getting stronger knives.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pv.Level = newLevel;
|
||||||
|
|
||||||
|
EntityPlayerLocal local = _player as EntityPlayerLocal;
|
||||||
|
if (pv.ProgressionClass.IsCrafting && local != null)
|
||||||
|
{
|
||||||
|
// true = add the notification only if one is not already up, so a horde night does
|
||||||
|
// not stack a fresh toast per corpse.
|
||||||
|
local.PlayerUI?.xui?.CollectedItemList?.AddCraftingSkillNotification(pv, true);
|
||||||
|
pv.ProgressionClass.HandleCheckCrafting(local, oldLevel, newLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// isEntityRemote guards these in vanilla too: a remote player's stats are the server's
|
||||||
|
// business, and marking them dirty here would be claiming an authority we do not have.
|
||||||
|
if (!_player.isEntityRemote)
|
||||||
|
{
|
||||||
|
progression.bProgressionStatsChanged = true;
|
||||||
|
_player.bPlayerStatsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user