Files
necromants-tome-7d2d-3-2/HarmonySrc/SpatialVaultPatch.cs
T
Alex CubeandClaude Opus 5 e8f064f5ec Книга некроманта 1.0 — первая публичная версия
Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых
зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида
духов и сюжетный финал через Чёрный портал. Локализация на 13 языках.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:13:03 +03:00

169 lines
8.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// "Пространственный браслет" (Spatial Bracelet) - dictated 2026-08-30, implemented same
/// day. See items.xml (braceletSpatialVault) for the item - both Action0 and Action1 use
/// Class="Eat" purely as a click-catcher (same trick as every other Harmony-driven item this
/// mod already has), distinguished here by ItemActionData.indexInEntityOfAction (0/1), the
/// same field SummonPatch.cs already uses to tell a summon book's summon-click from its
/// recall-click.
///
/// POWER ATTACK (index 1) - personal storage, size scaling with Necromancy skill level:
/// - XUiC_BagStorageWindowGroup.Open(xui, entity, bag, lootContainer, title, ...) is the
/// REAL API EntityDrone.openStorageWindow() itself calls to show the drone's own cargo
/// window (decompiled EntityDrone directly to find this, not guessed) - reused directly
/// rather than reinventing a storage UI. LootContainer.GetLootContainer("roboticDrone")
/// is the same display/behavior template the drone's own window uses too - "как у дрона"
/// taken literally, not just as a vague size comparison.
/// - Slot count = Mathf.RoundToInt(necromancyLevel / 10f), per the user's own exact formula
/// ("1*скилл_некроманта/10 округлённый до целого") - read live from
/// player.Progression.GetProgressionValue("craftingNecroNecromancy").Level (decompiled
/// EntityAlive/Progression/ProgressionValue directly to confirm this exact call shape,
/// not guessed) - the SAME skill the Knife's own damage already scales with (capped at
/// level 5000, one level per zombie kill - see buffs.xml/progression.xml), so this grows
/// at the same pace as every other kill-count-tied payoff in this mod. Below level 10
/// this rounds to 0 - deliberately left as-is, not special-cased away, matching the
/// Knife's own "0 at 0 kills is a feature, not a bug" precedent - a tooltip explains it
/// instead of silently opening a useless empty window.
/// - PERSISTENCE - the one thing NOT fully solved here, flagged rather than silently
/// assumed: the Bag backing each player's vault lives in a plain in-memory
/// Dictionary&lt;int, Bag&gt; in this file (PlayerVaults below), keyed by entityId. This
/// is reliable for as long as the game process keeps running (survives death/respawn/
/// relogging within one play session, confirmed by how a static field behaves) but has
/// NOT been wired into any save/load system - closing the game entirely and reloading the
/// save later will NOT bring the vault's contents back (no persistence file, no hook into
/// PersistentPlayerData or a world-save event). Building real cross-session persistence
/// (a custom save file + ModEvents.GameSave/Load hooks, or piggybacking on an owned
/// world entity the way the summoned pets do - unconfirmed whether THOSE actually survive
/// a full restart either) is real, separate follow-up work, not attempted here. Treat
/// this like a session-scoped stash until that's built and confirmed - don't rely on it
/// across game restarts yet.
///
/// REGULAR ATTACK (index 0) - knock back + slow whatever zombie the crosshair is aimed at:
/// - Same raycast mechanism HarmonySrc/ThiefLoopPatch.cs already established for
/// braceletThiefLoop (GetLookRay + Physics.Raycast + RootTransformRefEntity.
/// FindEntityUpwards) - reused verbatim, just resolving to EntityZombie instead of
/// EntityLootContainer.
/// - Slow: zombie.Buffs.AddBuff("buffInjurySlow") - the exact same vanilla debuff already
/// reused elsewhere in this mod (the Dog's own bite, necroMeleeHandZombieDog).
/// - Knockback: DELIBERATELY a straight Entity.SetPosition "shove" (same API
/// PetFollowPatch.cs already uses to reposition pets), NOT a physics/ragdoll impulse.
/// Found real candidates for "proper" knockback while researching this
/// (EntityAlive.DoRagdoll(in DamageResponse), DamageResponse.ImpulseScale/HitDirection),
/// but fully reverse-engineering how a real DamageResponse gets built and fed into that
/// during normal combat - all its other fields (Source, Strength, Stun, ArmorSlot, etc.)
/// - would have taken real additional decompilation with no guarantee of getting all the
/// coordinate/enum conventions right on the first try. A direct position shove is cruder
/// (no animation, the zombie just appears further away) but uses an API this exact file's
/// own family already relies on successfully - chosen for certainty over polish. Revisit
/// with DoRagdoll if the teleport-shove feels too crude in testing.
/// </summary>
[HarmonyPatch(typeof(ItemActionEat), "ExecuteAction")]
public static class Patch_ItemActionEat_ExecuteAction_SpatialVault
{
public const string ItemName = "braceletSpatialVault";
public const string NecromancySkillName = "craftingNecroNecromancy";
public const float MaxRange = 50f;
public const float ShoveDistance = 6f;
/// <summary>See the class-level comment above for exactly what this does and doesn't
/// guarantee - session-scoped only, not yet saved/loaded across game restarts.</summary>
public static readonly Dictionary<int, Bag> PlayerVaults = new Dictionary<int, Bag>();
public static bool Prefix(ItemActionData _actionData, bool _bReleased)
{
if (!_bReleased)
{
return true;
}
string itemName = _actionData?.invData?.itemValue?.ItemClass?.Name;
if (itemName != ItemName)
{
return true;
}
if (!(_actionData.invData.holdingEntity is EntityPlayerLocal player))
{
return true;
}
if (_actionData.indexInEntityOfAction == 1)
{
OpenVault(player);
}
// else: regular attack (index 0) deliberately does nothing, per direct user request
// 2026-08-30 ("пусть тогда обычная атака у пространственного браслета не делает
// ничего") after the knockback+slow version didn't visibly do anything in testing -
// rather than debug ShoveZombieAtCrosshair blind (kept below, unused, in case this
// gets revisited), just absorb the click silently.
// Skip ItemActionEat's own logic entirely - the click has been fully handled here.
return false;
}
public static void OpenVault(EntityPlayerLocal player)
{
ProgressionValue progressionValue = player.Progression?.GetProgressionValue(NecromancySkillName);
int level = progressionValue != null ? progressionValue.Level : 0;
int slotCount = Mathf.RoundToInt(level / 10f);
if (slotCount <= 0)
{
GameManager.ShowTooltip(player, "braceletSpatialVaultTooWeak");
return;
}
if (!PlayerVaults.TryGetValue(player.entityId, out Bag bag))
{
bag = new Bag(slotCount);
PlayerVaults[player.entityId] = bag;
}
else if (bag.SlotCount < slotCount)
{
// Grow, never shrink - the skill level only ever goes up, so this only ever
// copies existing stacks into a bigger array, same shape
// EntityLootContainer.SetContent itself uses when it needs to resize a bag.
ItemStack[] oldSlots = bag.GetSlots();
ItemStack[] newSlots = ItemStack.CreateArray(slotCount);
Array.Copy(oldSlots, newSlots, oldSlots.Length);
bag.SetSlots(newSlots);
}
Debug.Log("[NecromancerTome] SpatialVaultPatch: owner=" + player.entityId + " opened vault, " + slotCount + " slots (Necromancy level " + level + ")");
LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player);
XUiC_BagStorageWindowGroup.Open(playerUI.xui, player, bag, LootContainer.GetLootContainer("roboticDrone"), Localization.Get("braceletSpatialVaultWindowTitle"));
}
public static void ShoveZombieAtCrosshair(EntityPlayerLocal player)
{
Ray ray = player.GetLookRay();
if (!Physics.Raycast(ray, out RaycastHit hit, MaxRange))
{
GameManager.ShowTooltip(player, "braceletSpatialVaultNoTarget");
return;
}
Transform entityTransform = RootTransformRefEntity.FindEntityUpwards(hit.collider.transform);
Entity entity = entityTransform != null ? entityTransform.GetComponent<Entity>() : null;
if (!(entity is EntityZombie zombie) || zombie.IsDead())
{
GameManager.ShowTooltip(player, "braceletSpatialVaultNoTarget");
return;
}
zombie.Buffs?.AddBuff("buffInjurySlow");
Vector3 shoveDir = zombie.position - player.position;
shoveDir.y = 0f;
shoveDir = shoveDir.sqrMagnitude > 0.01f ? shoveDir.normalized : player.transform.forward;
Vector3 destination = zombie.position + shoveDir * ShoveDistance + Vector3.up * 1f;
zombie.SetPosition(destination, true);
player.PlayOneShot("swoosh");
Debug.Log("[NecromancerTome] SpatialVaultPatch: owner=" + player.entityId + " shoved zombie " + zombie.entityId);
}
}
}