Книга некроманта 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,340 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Portal stones - BACKLOG.md item 6. See items.xml (thrownStonePortalBlue/
|
||||
/// thrownStonePortalBlack) for the item definitions.
|
||||
///
|
||||
/// REWRITTEN 2026-08-29 after the first version's core assumption turned out wrong, confirmed
|
||||
/// by the user testing it ("срабатывает мгновенно" - fires instantly, no 10s indicator). The
|
||||
/// original version used Class="Eat"/Delay="10" on the assumption that Delay was a HELD-hold
|
||||
/// duration (like the workbench's TakeDelay). Re-decompiling ItemActionEat more carefully
|
||||
/// shows that's wrong: ExecuteAction only runs once per click, ON RELEASE
|
||||
/// (`if (!_bReleased || ...) return;`), and for UseAnimation items the actual "eating in
|
||||
/// progress" duration comes from `AnimationDelayData.AnimationDelay[HoldType].RayCast` - a
|
||||
/// fixed-per-HoldType table that isn't exposed anywhere in Data/Config's XML at all (checked
|
||||
/// directly - no matches for "RayCast"/"AnimationDelay" in any vanilla XML), so `Delay` on a
|
||||
/// Class="Eat" item is really just a re-click COOLDOWN (how soon it can fire again), not a
|
||||
/// channel length. That's why it looked instant - the real channel was whatever HoldType 40's
|
||||
/// (the rock's) built-in eating-animation length happens to be, a couple seconds at most, not
|
||||
/// our intended 10.
|
||||
///
|
||||
/// Fix: stop trying to make Class="Eat" do a long channel at all. Instead, Prefix
|
||||
/// ItemActionEat.ExecuteAction itself (the exact click-release entry point, still using
|
||||
/// Class="Eat" in XML purely as "a clickable item action", nothing about its own timing is
|
||||
/// used any more) and, for our two stones, skip the original method entirely and open the
|
||||
/// game's own generic countdown-timer UI directly - XUiC_Timer.OpenTimer(xui, seconds,
|
||||
/// TimerEventData, ...), the exact same low-level primitive Block.TakeItemWithTimer itself
|
||||
/// calls for the workbench pickup timer (decompiled both to confirm - TakeItemWithTimer is
|
||||
/// just a block-flavored wrapper around this same generic UI system, nothing block-specific
|
||||
/// about the timer itself). This gives a REAL visible progress bar/percent-fill UI (confirmed
|
||||
/// via XUiC_Timer's own "percent"/"timeleft" bindings) for the full 10 seconds, and
|
||||
/// TimerEventData.CloseOnHit=true makes it cancel automatically if the player takes damage
|
||||
/// mid-channel (a real engine feature, not something built by hand) - matching "прерывается
|
||||
/// при получении урона" without any extra code. The actual teleport only runs from
|
||||
/// FullTimeFinishEvent, i.e. only if the timer runs all the way to completion.
|
||||
///
|
||||
/// Local-player-only, like every other UI-touching thing in this mod (SummonPatch.cs's
|
||||
/// tooltips, etc.) - the underlying XUi/LocalPlayerUI system this timer needs is a
|
||||
/// client-side-only concept, not something that makes sense for a remote player in this mod's
|
||||
/// existing (single-player-focused) design.
|
||||
///
|
||||
/// POWER-ATTACK CANCEL added 2026-08-29 (user request: "прервать кнопкой силовой атаки" - a
|
||||
/// zombie could jump the player mid-channel and they want an explicit escape, not just
|
||||
/// CloseOnHit's "already got hit" reaction). Confirmed a cancelled channel never teleports
|
||||
/// either way - closing the timer window early (Escape/CloseOnHit/this) fires
|
||||
/// TimerEventData.CloseEvent, not FullTimeFinishEvent, and TeleportToBedroll only ever runs
|
||||
/// from the latter (see OnChannelComplete below) - so "cancel = no teleport" was already true
|
||||
/// by construction, just needed a new way to trigger a cancel.
|
||||
/// vanilla's own TimerEventData.CancelWithActivateButton (already set true above) only checks
|
||||
/// PlayerActionsPermanent.Activate (decompiled XUiC_Timer.Update to confirm) - a small
|
||||
/// always-live action set TFP built specifically to stay readable during modal UI, which does
|
||||
/// NOT include Secondary/power-attack at all (checked its full field list). Rather than
|
||||
/// hijack Activate (the same key that STARTS the channel) or Cancel (Escape, not the button
|
||||
/// asked for), Patch_XUiC_Timer_Update_PortalStoneCancel below Postfixes XUiC_Timer.Update
|
||||
/// itself and polls PlayerActionsLocal.Secondary.WasPressed directly (the same underlying
|
||||
/// action already used elsewhere in this mod as "power attack", e.g. summon books' Action1)
|
||||
/// - NOT decompiled-confirmed whether this action still registers while the timer's modal
|
||||
/// window has input focus (SetControllable(false) fires on open, decompiled from XUiC_Timer,
|
||||
/// but that's a character/gameplay-layer flag, separate from the InControl input-polling
|
||||
/// layer PlayerAction reads from - the two are believed independent, not proven end-to-end).
|
||||
/// Test in-game; if power attack doesn't register while the bar is up, that gap is the first
|
||||
/// thing to dig into (possibly needs reading raw InControl device state instead of the
|
||||
/// semantic PlayerAction).
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(ItemActionEat), "ExecuteAction")]
|
||||
public static class Patch_ItemActionEat_ExecuteAction_PortalStones
|
||||
{
|
||||
public const string BlueStoneName = "thrownStonePortalBlue";
|
||||
public const string BlackStoneName = "thrownStonePortalBlack";
|
||||
public const float ChannelSeconds = 10f;
|
||||
|
||||
/// <summary>Sentinel stashed in TimerEventData.Data (an unused generic object field on
|
||||
/// vanilla's own class) purely so Patch_XUiC_Timer_Update_PortalStoneCancel below can
|
||||
/// tell "this is one of our portal-stone timers" apart from any other TimerEventData the
|
||||
/// engine or another mod might have open (e.g. a workstation pickup timer, BACKLOG.md
|
||||
/// item 9) - a reference-equality check on a private static object, nothing exposed or
|
||||
/// read by vanilla code.</summary>
|
||||
public static readonly object ChannelMarker = new object();
|
||||
|
||||
/// <summary>See buffs.xml - a marker/particle-carrier buff, added/removed directly by
|
||||
/// this file rather than by any buff-trigger vocabulary.</summary>
|
||||
public const string ChannelBuffName = "buffNecroPortalChannel";
|
||||
|
||||
public static bool Prefix(ItemActionData _actionData, bool _bReleased)
|
||||
{
|
||||
if (!_bReleased)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
string itemName = _actionData?.invData?.itemValue?.ItemClass?.Name;
|
||||
if (itemName != BlueStoneName && itemName != BlackStoneName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!(_actionData.invData.holdingEntity is EntityPlayerLocal player))
|
||||
{
|
||||
// Not the local player (e.g. an AI or remote entity somehow holding this) - let
|
||||
// vanilla Eat behavior run rather than silently doing nothing, same fallback
|
||||
// shape used elsewhere in this mod for the local-player-only simplification.
|
||||
return true;
|
||||
}
|
||||
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: channel started for " + itemName + ", owner=" + player.entityId);
|
||||
// Played directly here since ItemActionEat's own Sound_start handling is skipped
|
||||
// entirely along with the rest of its ExecuteAction (see class comment) - a plain
|
||||
// XML Sound_start property on this item would never fire otherwise.
|
||||
player.PlayOneShot("swoosh");
|
||||
// Black/smoke particle swirl for the duration of the channel (user request
|
||||
// 2026-08-29) - see buffs.xml's buffNecroPortalChannel + ParticlePatch.cs (generalized
|
||||
// to handle a player-targeted buff, not just the two zombie-facing ones it already
|
||||
// had). A plain marker buff, added/removed directly here rather than through any
|
||||
// buff-trigger vocabulary, since there's no "for as long as this XUiC_Timer is open"
|
||||
// trigger to hang it off - this IS that lifecycle.
|
||||
player.Buffs.AddBuff(ChannelBuffName);
|
||||
|
||||
TimerEventData timerData = new TimerEventData
|
||||
{
|
||||
CloseOnHit = true,
|
||||
CancelWithActivateButton = true,
|
||||
Data = ChannelMarker,
|
||||
};
|
||||
timerData.FullTimeFinishEvent += delegate
|
||||
{
|
||||
OnChannelComplete(player, itemName);
|
||||
};
|
||||
// CloseEvent fires when the timer window closes WITHOUT completing (cancelled by
|
||||
// damage/power-attack/Cancel) - confirmed by decompiling XUiC_Timer.OnClose/
|
||||
// timeReachedNull: timeReachedNull sets skipCloseEvent=true around the completion
|
||||
// path specifically so CloseEvent does NOT also fire on a successful finish, only on
|
||||
// every other way the window can close. FullTimeFinishEvent and CloseEvent are
|
||||
// therefore mutually exclusive per channel - exactly "however it ends" from the
|
||||
// class-level comment.
|
||||
timerData.CloseEvent += delegate
|
||||
{
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: channel cancelled for " + itemName + ", owner=" + player.entityId);
|
||||
player.Buffs.RemoveBuff(ChannelBuffName);
|
||||
};
|
||||
|
||||
string labelKey = (itemName == BlueStoneName) ? "thrownStonePortalBlueChanneling" : "thrownStonePortalBlackChanneling";
|
||||
LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player);
|
||||
XUiC_Timer.OpenTimer(playerUI.xui, ChannelSeconds, timerData, -1f, Localization.Get(labelKey));
|
||||
|
||||
// Skip ItemActionEat's own logic entirely for these two items - the click has been
|
||||
// fully handled by opening our own timer instead.
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void OnChannelComplete(EntityPlayerLocal player, string itemName)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: channel completed for " + itemName + ", owner=" + player.entityId);
|
||||
player.Buffs.RemoveBuff(ChannelBuffName);
|
||||
if (itemName == BlackStoneName)
|
||||
{
|
||||
ShowBlackPortalConfirmation(player);
|
||||
return;
|
||||
}
|
||||
TeleportToBedroll(player);
|
||||
}
|
||||
|
||||
/// <summary>Black portal confirmation + fullscreen video, user request 2026-08-30
|
||||
/// ("диалоговое окно... вы уверены... Если Да, то игра останавливается и проигрывается
|
||||
/// видео"). Real APIs, both decompiled directly:
|
||||
/// - XUiC_MessageBoxWindowGroup.ShowCustom(xui, title, text, icon, setupCallback, ...) -
|
||||
/// the same generic Yes/No popup vanilla itself uses (its own delete-item/disconnect
|
||||
/// confirmations, etc). ShowOkCancel/ShowConfirmCancel exist too but hardcode their
|
||||
/// button caption keys ("xuiOk"/"xuiCancel"/"btnConfirm") - ShowCustom's
|
||||
/// _setupCallback is the only variant that lets the two buttons be captioned
|
||||
/// "xuiYes"/"xuiNo" directly (both are real, already-localized vanilla keys, confirmed
|
||||
/// against Data/Config/Localization.csv), matching the user's literal "да/нет"
|
||||
/// wording. Buttons[0]/[2] (not [1]) is the same slot pairing ShowOkCancel/
|
||||
/// ShowConfirmCancel themselves use internally - Buttons[1] is left unused, same as
|
||||
/// vanilla's own 2-button dialogs.
|
||||
/// - GameManager.Instance.Pause(bool) - decompiled GameManager.updatePauseState: sets
|
||||
/// Time.timeScale=0 for real, but ONLY takes effect in singleplayer (an SP-only check
|
||||
/// baked into vanilla itself, not a limitation added by this mod) - a deliberate,
|
||||
/// documented no-op in multiplayer rather than something silently broken.
|
||||
/// - XUiC_VideoPlayer.PlayVideo(xui, VideoData, skippable, onFinished) - opens the same
|
||||
/// fullscreen "VideoPlayer" window vanilla's own TFP intro/menu-background videos use.
|
||||
/// Decompiled XUiV_Video confirms video playback isn't gated by Time.timeScale, so it
|
||||
/// keeps playing correctly while paused. skippable=true (Cancel key) so a broken/
|
||||
/// missing video file can't soft-lock the player - XUiV_Video.OnVideoErrorReceived
|
||||
/// already auto-closes on a bad file on its own, this is just a second, player-facing
|
||||
/// way out.
|
||||
///
|
||||
/// VIDEO FILE: per direct user instruction 2026-08-30 ("Пока файл видео замени
|
||||
/// заглушкой. Потом поставим нормальный"), Video/BlackPortal.webm is currently a COPY OF
|
||||
/// VANILLA'S OWN TFP_Intro.webm (from 7DaysToDie_Data/StreamingAssets/Video/), not real
|
||||
/// mod content - purely so the full dialog -> pause -> video -> unpause pipeline is
|
||||
/// genuinely testable end-to-end right now. Swap that one file for the real video later;
|
||||
/// nothing else needs to change (reuse the same filename, or update BlackPortalVideoPath
|
||||
/// below if the real file gets a different name).
|
||||
/// "@modfolder(NecromancerTome):..." is the exact mod-relative path syntax
|
||||
/// XUiV_Video.startVideo resolves via ModManager.TryPatchModPathString (decompiled to
|
||||
/// confirm - looks for "@modfolder(<mod name>):" and substitutes the mod's real
|
||||
/// install path; "NecromancerTome" here is this mod's own ModInfo.xml Name, not its
|
||||
/// DisplayName).</summary>
|
||||
public const string BlackPortalVideoPath = "@modfolder(NecromancerTome):Video/BlackPortal.webm";
|
||||
|
||||
public static void ShowBlackPortalConfirmation(EntityPlayerLocal player)
|
||||
{
|
||||
LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player);
|
||||
XUiC_MessageBoxWindowGroup.ShowCustom(
|
||||
playerUI.xui,
|
||||
Localization.Get("thrownStonePortalBlackConfirmTitle"),
|
||||
Localization.Get("thrownStonePortalBlackConfirmText"),
|
||||
"",
|
||||
delegate(XUiC_MessageBoxWindowGroup mb)
|
||||
{
|
||||
mb.Buttons[0].DefaultConfirm("xuiYes", delegate { ActivateBlackPortal(player); });
|
||||
mb.Buttons[2].DefaultCancel("xuiNo", null);
|
||||
},
|
||||
_openMainMenuOnClose: false,
|
||||
_modal: true,
|
||||
_buttonOnOutsideClick: -1,
|
||||
// Esc/outside-close counts as "No" - same convention ShowOkCancel/
|
||||
// ShowConfirmCancel themselves use for their own Cancel slot (index 2).
|
||||
_buttonOnExternalClose: 2);
|
||||
}
|
||||
|
||||
/// <summary>ЗАМЕНЕНО 2026-09-09: раньше отсюда сразу стартовало полноэкранное видео
|
||||
/// (BlackPortalVideoPath), теперь запускается финальная сцена из шести слайдов с текстом
|
||||
/// - FinalSlides.Begin. Причина в BACKLOG.md ("концовка серией диалоговых окон вместо
|
||||
/// видео"): видео не локализуется, а текст слайдов идёт обычной строкой через
|
||||
/// Localization.csv. Пауза и выход в главное меню никуда не делись - и то и другое
|
||||
/// теперь живёт внутри FinalSlides, а видео осталось финальным аккордом ПОСЛЕ выбора
|
||||
/// концовки на последнем слайде.
|
||||
///
|
||||
/// Всё, что описано в комментарии к BlackPortalVideoPath выше, по-прежнему верно и
|
||||
/// применяется - просто к двум новым файлам (FinalSlides.StayVideoPath /
|
||||
/// ReturnVideoPath) вместо одного. Сама константа BlackPortalVideoPath больше не
|
||||
/// используется и оставлена только как документация к разбору "@modfolder(...)" и
|
||||
/// XUiC_VideoPlayer.PlayVideo, на который FinalSlides ссылается.</summary>
|
||||
public static void ActivateBlackPortal(EntityPlayerLocal player)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: black portal confirmed by owner=" + player.entityId + ", handing over to FinalSlides");
|
||||
FinalSlides.Begin(player);
|
||||
}
|
||||
|
||||
/// <summary>Прежняя концовка "сразу видео, потом главное меню". Больше ниоткуда не
|
||||
/// вызывается (см. ActivateBlackPortal выше) - оставлена целиком, потому что весь разбор
|
||||
/// Pause/PlayVideo/Disconnect в её комментариях остаётся актуальным и на неё ссылается
|
||||
/// FinalSlides. Удалять при следующей уборке, если так и не понадобится.</summary>
|
||||
public static void PlayBlackPortalVideoLegacy(EntityPlayerLocal player)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: black portal confirmed by owner=" + player.entityId + ", pausing + playing video");
|
||||
GameManager.Instance.Pause(true);
|
||||
LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player);
|
||||
VideoData videoData = new VideoData { url = BlackPortalVideoPath };
|
||||
XUiC_VideoPlayer.PlayVideo(playerUI.xui, videoData, true, delegate(bool skipped)
|
||||
{
|
||||
// EXIT TO MAIN MENU after the video, user request 2026-08-30 ("После видео нужно
|
||||
// выходить из игры в главное меню") - fires whether the video played to the end
|
||||
// or was skipped (Cancel key / a bad file), same as any other "the video is over"
|
||||
// outcome. GameManager.Instance.Disconnect() is not a guess - it's the EXACT same
|
||||
// call the real in-game ESC menu's own "Exit to Main Menu" button uses
|
||||
// (decompiled XUiC_InGameMenuWindow.exitGame/BtnExit_OnPressed to confirm: it's a
|
||||
// thin wrapper straight to this method). Handles everything a clean exit needs by
|
||||
// itself - closes modal windows, un-pauses (calls Pause(false) internally, so no
|
||||
// separate unpause call needed here), saves/shuts down the local server, and
|
||||
// returns to XUiC_MainMenu - not reinventing any of that by hand. Replaces the
|
||||
// earlier "thrownStonePortalBlackNotBound" tooltip placeholder entirely: with a
|
||||
// real exit-to-menu ending, staying in-game and showing a tooltip no longer makes
|
||||
// sense (BACKLOG.md item 6's "destination not decided" placeholder is now this
|
||||
// exit itself, not a tooltip).
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: black portal video finished (skipped=" + skipped + "), exiting to main menu");
|
||||
GameManager.Instance.Disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>BedrollPos comes from EntityPlayer.PersistentPlayerData (decompiled - reads
|
||||
/// GameManager.Instance.persistentPlayers.GetPlayerDataFromEntityID(entityId)), the same
|
||||
/// field the game's own respawn-at-bedroll flow reads (PersistentPlayerData.BedrollPos /
|
||||
/// HasBedrollPos, confirmed by decompiling that class directly). +0.5 on x/z centers the
|
||||
/// block, +1 on y lifts the destination clear of the bedroll block itself - a reasonable
|
||||
/// guess at a safe landing offset, not a decompiled/confirmed "correct" one (the
|
||||
/// respawn-specific code that actually places a resurrected player likely does more
|
||||
/// ground-safety checking than this; worth revisiting if the stone ever drops the player
|
||||
/// inside a block). Teleport itself uses NetPackageTeleportPlayer, the exact same package
|
||||
/// ConsoleCmdTeleportsAbs.ExecuteTeleport (the real "teleportplayer" console command)
|
||||
/// uses - decompiled to confirm, not invented.</summary>
|
||||
public static void TeleportToBedroll(EntityPlayerLocal player)
|
||||
{
|
||||
PersistentPlayerData data = player.PersistentPlayerData;
|
||||
if (data == null || !data.HasBedrollPos)
|
||||
{
|
||||
GameManager.ShowTooltip(player, "thrownStonePortalBlueNoBedroll");
|
||||
Debug.LogWarning("[NecromancerTome] PortalStonePatch: owner=" + player.entityId + " has no bedroll set, can't teleport");
|
||||
return;
|
||||
}
|
||||
Vector3i bedrollPos = data.BedrollPos;
|
||||
Vector3 destination = new Vector3(bedrollPos.x + 0.5f, bedrollPos.y + 1f, bedrollPos.z + 0.5f);
|
||||
NetPackageTeleportPlayer package = NetPackageManager.GetPackage<NetPackageTeleportPlayer>().Setup(destination, null);
|
||||
package.ProcessPackage(GameManager.Instance.World, GameManager.Instance);
|
||||
player.PlayOneShot("spawnInStinger");
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: owner=" + player.entityId + " teleported to bedroll " + bedrollPos);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Lets the power-attack ("Secondary") input cancel an in-progress portal-stone
|
||||
/// channel - see the long comment on Patch_ItemActionEat_ExecuteAction_PortalStones above for
|
||||
/// the full reasoning. Separate patch class/target method (XUiC_Timer.Update, not
|
||||
/// ItemActionEat.ExecuteAction) since this has to run every frame WHILE the timer is open, not
|
||||
/// once at click time.
|
||||
///
|
||||
/// FIXED 2026-08-29 (user report: cancel didn't work at all) - the semantic
|
||||
/// PlayerActionsLocal.Secondary check alone (first version) apparently never registered while
|
||||
/// the timer's modal window has input focus, confirming the exact risk flagged when this was
|
||||
/// first written. Root cause not fully pinned down by decompilation (XUiC_Timer.OnOpen sets
|
||||
/// SetControllable(false) on the player, and nothing found ties that flag directly to
|
||||
/// PlayerAction's own InControl polling layer - the two are presumed independent but the
|
||||
/// actual suppression point wasn't located). Rather than keep guessing which exact system
|
||||
/// swallows it, added a SECOND, independent check straight to Unity's raw
|
||||
/// Input.GetMouseButtonDown(1) (right mouse button - confirmed as Secondary's real default
|
||||
/// KBM binding by decompiling PlayerActionsLocal.CreateActions) - raw Input polling reads
|
||||
/// hardware state directly, bypassing InControl/PlayerAction and whatever gates it, so this
|
||||
/// should fire regardless of modal-window suppression. Either check firing cancels the
|
||||
/// channel; keeping the semantic one too costs nothing and covers gamepad Secondary
|
||||
/// (LeftTrigger) if that one turns out to work. KBM-only fallback - if a gamepad player still
|
||||
/// can't cancel, that's the next gap to close (would need the equivalent raw axis read).</summary>
|
||||
[HarmonyPatch(typeof(XUiC_Timer), "Update")]
|
||||
public static class Patch_XUiC_Timer_Update_PortalStoneCancel
|
||||
{
|
||||
public static void Postfix(XUiC_Timer __instance)
|
||||
{
|
||||
if (__instance == null || __instance.eventData == null || __instance.eventData.Data != Patch_ItemActionEat_ExecuteAction_PortalStones.ChannelMarker)
|
||||
{
|
||||
return;
|
||||
}
|
||||
PlayerActionsLocal input = __instance.xui?.playerUI?.playerInput;
|
||||
bool cancelPressed = (input != null && input.Secondary.WasPressed) || Input.GetMouseButtonDown(1);
|
||||
if (cancelPressed)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] PortalStonePatch: channel cancelled via power attack");
|
||||
__instance.xui.playerUI.windowManager.Close(__instance.windowGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user