Книга некроманта 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
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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<int, Bag> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user