Книга некроманта 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,137 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// "Кровь некроманта" (Necromancer's Blood) - dictated 2026-08-30. See items.xml
|
||||
/// (resourceNecromancerBlood) 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
|
||||
/// 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
|
||||
/// consumed" ingredient flag (confirmed - only `craft_tool="itemName"` exists for
|
||||
/// something adjacent, but it takes exactly ONE item name, not "any item of a category",
|
||||
/// and its actual runtime enforcement point wasn't confirmed by decompilation either -
|
||||
/// not risking an untested mechanism for this).
|
||||
/// 2. "При крафте нужно отнимать у персонажа 90% имеющегося ХП" - crafting this recipe
|
||||
/// costs 90% of the player's CURRENT health. Recipes have no cost hook beyond their
|
||||
/// ingredient list at all.
|
||||
///
|
||||
/// "любого ножа" (ANY knife) is detected via ItemClass.DisplayType == "meleeKnife" - decompiled
|
||||
/// Data/Config/items.xml directly: every real vanilla knife (meleeWpnBladeT0BoneKnife,
|
||||
/// meleeWpnBladeT1HuntingKnife, even meleeWpnBladeT3Machete) shares this exact DisplayType,
|
||||
/// which is how the game itself categorizes "the knife family" in its own UI - a single,
|
||||
/// reliable check instead of hand-maintaining a list of item names. necroWpnBladeNecroKnife
|
||||
/// (Extends meleeWpnBladeT0BoneKnife, never overrides DisplayType) is covered by the same
|
||||
/// check automatically.
|
||||
///
|
||||
/// PATCH POINTS - both on XUiC_RecipeStack, decompiled directly (not guessed):
|
||||
/// - SetRecipe(...) Prefix: the earliest confirmed point a "craft this recipe" click reaches
|
||||
/// (XUiC_CraftingQueue.AddRecipeToCraftAtIndex calls straight into this). Blocking here
|
||||
/// (return false) stops the craft from ever starting - recipe/recipeCount never get set,
|
||||
/// isCrafting never becomes true.
|
||||
/// - outputStack() Prefix+Postfix (via __state): outputStack() is where the output item is
|
||||
/// actually granted, once per queued unit - HP is only deducted when __result is true,
|
||||
/// i.e. the item genuinely was produced this call, not on a failed/blocked attempt.
|
||||
///
|
||||
/// CAVEAT - not glossed over: decompiling XUiC_RecipeStack/XUiC_CraftingQueue/XUiM_Recipes did
|
||||
/// NOT turn up the exact line that removes ingredients from the player's inventory (it happens
|
||||
/// somewhere upstream of SetRecipe, in whatever UI code handles the actual "Craft" button click
|
||||
/// - not found within reasonable search). XUiC_RecipeStack.HandleOnPress (the CANCEL button)
|
||||
/// refunds ingredients, which proves they're already gone by the time SetRecipe runs - so if
|
||||
/// SetRecipe's Prefix blocks a no-knife attempt, the jar may already be spent with nothing
|
||||
/// granted back. Blocking at the earliest CONFIRMED point was judged better than not blocking
|
||||
/// at all; a lost jar on a rare misclick is a minor rough edge, not a correctness bug. Revisit
|
||||
/// if this turns out to happen often in practice.
|
||||
/// </summary>
|
||||
public static class NecromancerBloodPatch
|
||||
{
|
||||
public const string BloodItemName = "resourceNecromancerBlood";
|
||||
public const float HealthCostFraction = 0.9f;
|
||||
|
||||
public static bool HasAnyKnife(EntityPlayerLocal player)
|
||||
{
|
||||
return ContainsKnife(player.inventory?.GetSlots()) || ContainsKnife(player.bag?.GetSlots());
|
||||
}
|
||||
|
||||
private static bool ContainsKnife(ItemStack[] slots)
|
||||
{
|
||||
if (slots == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (ItemStack stack in slots)
|
||||
{
|
||||
if (stack == null || stack.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (stack.itemValue?.ItemClass?.DisplayType == "meleeKnife")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiC_RecipeStack), "SetRecipe")]
|
||||
public static class Patch_XUiC_RecipeStack_SetRecipe_NecromancerBlood
|
||||
{
|
||||
public static bool Prefix(XUiC_RecipeStack __instance, Recipe _recipe, bool recipeModification)
|
||||
{
|
||||
// recipeModification covers the "clear this slot" calls (ClearQueue/RefreshQueue/
|
||||
// cancel) - never block those, only an actual attempt to start crafting our recipe.
|
||||
if (recipeModification || _recipe == null || _recipe.GetName() != NecromancerBloodPatch.BloodItemName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
EntityPlayerLocal player = __instance.xui?.playerUI?.entityPlayer;
|
||||
if (player == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!NecromancerBloodPatch.HasAnyKnife(player))
|
||||
{
|
||||
GameManager.ShowTooltip(player, "resourceNecromancerBloodNeedsKnife");
|
||||
Debug.Log("[NecromancerTome] NecromancerBloodPatch: blocked craft (no knife present) for owner=" + player.entityId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiC_RecipeStack), "outputStack")]
|
||||
public static class Patch_XUiC_RecipeStack_outputStack_NecromancerBlood
|
||||
{
|
||||
public static void Prefix(XUiC_RecipeStack __instance, out EntityPlayerLocal __state)
|
||||
{
|
||||
__state = null;
|
||||
if (__instance.recipe != null && __instance.recipe.GetName() == NecromancerBloodPatch.BloodItemName)
|
||||
{
|
||||
__state = __instance.xui?.playerUI?.entityPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Postfix(bool __result, EntityPlayerLocal __state)
|
||||
{
|
||||
if (!__result || __state == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Deliberately not clamped to leave the player at least 1 HP - the user asked for a
|
||||
// straight 90% cost, and a blood ritual that can genuinely kill you if you're already
|
||||
// badly hurt fits the theme. AddHealth is the same safe, non-combat HP-modification
|
||||
// API vanilla itself uses (decompiled EntityAlive.AddHealth) - not DamageEntity/
|
||||
// DamageResponse, since this isn't damage from a source, it's a direct self-cost.
|
||||
int amount = Mathf.RoundToInt(__state.Health * NecromancerBloodPatch.HealthCostFraction);
|
||||
if (amount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
__state.AddHealth(-amount);
|
||||
Debug.Log("[NecromancerTome] NecromancerBloodPatch: crafted blood, deducted " + amount + " HP from owner=" + __state.entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user