Кряк боли при создании Крови некроманта
Крафт крови и так снимает 90% текущего ХП, но делал это молча. Теперь на том же
месте играет штатный звук боли игрока - тот, что слышно при накалывании на кол
или на колючую проволоку.
ИМЯ КЛИПА СПРАШИВАЕТСЯ У СУЩНОСТИ, А НЕ ВПИСАНО СТРОКОЙ. Звук боли зависит от
пола персонажа (Data/Config/entityclasses.xml):
playerMale SoundHurt="player1painlg" SoundHurtSmall="player1painsm"
playerFemale SoundHurt="player2painlg" SoundHurtSmall="player2painsm"
playerFemale extends playerMale и переопределяет обе строки, так что
захардкоженный "player1painlg" выдал бы женскому персонажу мужской кряк. Вместо
этого зовутся GetSoundHurt() и GetSoundHurtSmall() - публичные методы
EntityAlive, каждый из которых целиком есть чтение поля (проверено по IL:
"ldarg.0; ldfld soundHurt|soundHurtSmall; ret", токены разрешены через
Module.ResolveField).
ПОРОГ ВЗЯТ У САМОЙ ИГРЫ. EntityPlayer переопределяет
GetSoundHurt(DamageSource, int) - тот самый метод, через который проходят колья
и колючая проволока. Его IL разобран целиком, и правило такое: урон СТРОГО
больше 15 -> большой кряк (soundHurt), 15 и меньше -> малый (soundHurtSmall),
плюс фолбэк на большой, если малый не объявлен. Воспроизведено один в один,
включая строгое "больше" и фолбэк; константа названа PainSoundBigDamage и несёт
этот разбор в комментарии.
Сам GetSoundHurt(DamageSource, int) не вызывается: ему нужен DamageSource, а
здесь урона от источника нет - ХП снимается через AddHealth, осознанно (см.
комментарий на месте вызова). Правило выбора - три строки, поэтому оно повторено,
а не подделано синтетическим DamageSource.
PLAYONESHOT ВЫЗВАН РОВНО КАК В ВАНИЛИ. Единственный вызывающий -
EntityAlive.OnUpdateEntity, IL 348: там PlayOneShot получает
sound_in_head:false, serverSignalOnly:false, isUnique:false, _animEvent:null,
volumeScale:1f. Значения по умолчанию у Entity.PlayOneShot - те же самые
(сверено через RawDefaultValue), так что вызов одним аргументом поведенчески
идентичен ванильному: кряк идёт ОТ персонажа, а не "в голове". Оттуда же взят
null-guard - ваниль пропускает вызов при пустом имени клипа, а не отдаёт его
PlayOneShot.
Вызов стоит ПОСЛЕ AddHealth(-amount), а не до: крафт, который свалился бы выше,
не должен издавать звук, которому игрок не найдёт объяснения. При цене 90% это
практически всегда большой кряк - малый включится только на остатке около 17 ХП,
там же, где его сделала бы малым ваниль.
ЗАОДНО: устаревшая шапка NecromancerBloodPatch.cs. Комментарий утверждал, что
кровь живёт в item_modifiers.xml, "not in items.xml, since 2026-09-15". Откат
15.09 это отменил, но тогда правились только комментарии в items.xml, а .cs
пропустили. Переписано: кровь в items.xml, и рядом коротко, почему её оттуда
нельзя двигать.
Сборка: 0 ошибок (4 прежних MSB3277 про версии System.Runtime, к правке
отношения не имеют). Скомпилированный PlayPainSound проверен рефлексией по
готовой DLL - вызовы идут в ожидаемом порядке.
НЕ ПРОВЕРЕНО В ИГРЕ. Проверять обоими полами персонажа - ради этого имя клипа и
спрашивается у сущности.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5a512260cc
commit
e890999391
@@ -4,13 +4,20 @@ using UnityEngine;
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// "Кровь некроманта" (Necromancer's Blood) - dictated 2026-08-30. See item_modifiers.xml
|
||||
/// (resourceNecromancerBlood - it lives THERE, not in items.xml, since 2026-09-15: it had to
|
||||
/// become an ItemClassModifier to be installable in the Spatial Bracelet, and that class is
|
||||
/// only created from <item_modifier>. Nothing here changed - the lookup is by name, and both
|
||||
/// files register into the same ItemClass.nameToItem) for the item, recipes.xml for the base
|
||||
/// recipe (an empty jar,
|
||||
/// like any other resource conversion). Two rules the user asked for have NO vanilla XML
|
||||
/// "Кровь некроманта" (Necromancer's Blood) - dictated 2026-08-30. See items.xml
|
||||
/// (resourceNecromancerBlood) for the item and recipes.xml for the base recipe (an empty jar,
|
||||
/// like any other resource conversion).
|
||||
///
|
||||
/// IT LIVES IN items.xml, AND THAT IS NOT AN ACCIDENT. On 2026-09-15 it was moved into
|
||||
/// item_modifiers.xml so it could be installed in the Spatial Bracelet, and that move
|
||||
/// DESTROYED a character in a save: ItemValue.Read/Write gate the modification block on
|
||||
/// !(itemClass is ItemClassModifier), so the item's CLASS decides the byte layout of every
|
||||
/// stack of it in the save, and an existing save read one byte off from the first blood stack
|
||||
/// onward. It was rolled back the same day, the bracelet's charge became a brand-new item
|
||||
/// (resourceBloodSphere, and later resourceBloodStone) instead, and the rule stands: an item
|
||||
/// that could already be in someone's inventory must not change class in either direction.
|
||||
/// The full account is in BACKLOG.md; the earlier wording of this comment claimed the modifier
|
||||
/// home as current and outlived the code by a day. Two rules the user asked for have NO vanilla XML
|
||||
/// equivalent at all, so both are enforced here instead:
|
||||
/// 1. "нужна... наличие любого ножа" - a knife must be present (in the toolbelt or
|
||||
/// backpack) to craft this, but is NOT consumed. recipes.xml has no "required but not
|
||||
@@ -54,6 +61,73 @@ namespace NecromancerTome
|
||||
public const string BloodItemName = "resourceNecromancerBlood";
|
||||
public const float HealthCostFraction = 0.9f;
|
||||
|
||||
/// <summary>Damage above which the player's BIG pain grunt is used instead of the small
|
||||
/// one. Not invented: it is vanilla's own threshold, read out of
|
||||
/// EntityPlayer.GetSoundHurt(DamageSource, int) - the override that every spike and every
|
||||
/// strand of barbed wire goes through. Its IL is
|
||||
///
|
||||
/// ldarg.2 // _damageStrength
|
||||
/// ldc.i4.s 15
|
||||
/// bgt.s -> GetSoundHurt() // strictly MORE than 15 -> soundHurt (…painlg)
|
||||
/// call GetSoundHurtSmall() // 15 or less -> soundHurtSmall (…painsm)
|
||||
///
|
||||
/// so the comparison is strictly greater-than, and 15 exactly still counts as small. The
|
||||
/// same method has an earlier branch for damage type 16 that returns GetSoundDrownPain();
|
||||
/// that one is the drowning case and has nothing to do with us.</summary>
|
||||
public const int PainSoundBigDamage = 15;
|
||||
|
||||
/// <summary>Plays the player's own pain grunt, picking the big or the small one by the
|
||||
/// same rule vanilla uses for spikes and barbed wire (user request 2026-09-16: "в игре
|
||||
/// есть звук боли (когда персонаж напарывается на колья или на колючую проволоку). Пусть
|
||||
/// этот звук воспроизводится при создании крови некроманта").
|
||||
///
|
||||
/// WHY THE SOUND NAME IS ASKED FOR AND NOT SPELLED OUT. The clip differs by gender -
|
||||
/// playerMale carries SoundHurt="player1painlg"/SoundHurtSmall="player1painsm" and
|
||||
/// playerFemale overrides both to player2pain* (Data/Config/entityclasses.xml). Hardcoding
|
||||
/// "player1painlg" would have given every female character a male grunt. GetSoundHurt() and
|
||||
/// GetSoundHurtSmall() are public on EntityAlive and are plain field reads (verified: each
|
||||
/// one's whole body is "ldarg.0; ldfld soundHurt|soundHurtSmall; ret"), so they return
|
||||
/// whatever this entity's own class declared and cost nothing.
|
||||
///
|
||||
/// WHY NOT GetSoundHurt(DamageSource, int), which would pick for us: it needs a
|
||||
/// DamageSource, and this is not damage from a source - the HP here is spent by AddHealth,
|
||||
/// deliberately (see the comment at the call site). Its selection rule is three lines, so
|
||||
/// it is reproduced instead of faked with a synthetic DamageSource.
|
||||
///
|
||||
/// The null guard is vanilla's too: EntityAlive.OnUpdateEntity stores the result and skips
|
||||
/// the call on null (brfalse right after the stloc) rather than handing PlayOneShot a null
|
||||
/// clip name. The fallback to the big grunt covers an entity that declares SoundHurt but
|
||||
/// not SoundHurtSmall - again exactly what vanilla's override does when
|
||||
/// GetSoundHurtSmall() comes back empty.
|
||||
///
|
||||
/// PlayOneShot(name) with no further arguments is byte-for-byte what vanilla passes here:
|
||||
/// its optional parameters default to sound_in_head:false, serverSignalOnly:false,
|
||||
/// isUnique:false, _animEvent:null, volumeScale:1f, and OnUpdateEntity's own call pushes
|
||||
/// exactly those five constants. So the grunt comes out of the character, not "in the
|
||||
/// head", same as being spiked.</summary>
|
||||
public static void PlayPainSound(EntityPlayerLocal _player, int _damage)
|
||||
{
|
||||
if (_player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string clip = _damage > PainSoundBigDamage
|
||||
? _player.GetSoundHurt()
|
||||
: _player.GetSoundHurtSmall();
|
||||
|
||||
if (string.IsNullOrEmpty(clip))
|
||||
{
|
||||
clip = _player.GetSoundHurt();
|
||||
}
|
||||
if (string.IsNullOrEmpty(clip))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_player.PlayOneShot(clip);
|
||||
}
|
||||
|
||||
public static bool HasAnyKnife(EntityPlayerLocal player)
|
||||
{
|
||||
return ContainsKnife(player.inventory?.GetSlots()) || ContainsKnife(player.bag?.GetSlots());
|
||||
@@ -135,6 +209,13 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
__state.AddHealth(-amount);
|
||||
|
||||
// The grunt goes AFTER the HP is actually gone, so a craft that somehow bailed out
|
||||
// above never makes a sound the player cannot account for. At the 90% cost this is the
|
||||
// big pain clip in every normal case (amount > 15 unless the player is already down to
|
||||
// about 17 HP), and drops to the small one exactly where vanilla would drop it too.
|
||||
NecromancerBloodPatch.PlayPainSound(__state, amount);
|
||||
|
||||
Debug.Log("[NecromancerTome] NecromancerBloodPatch: crafted blood, deducted " + amount + " HP from owner=" + __state.entityId);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user