using Content.Server.Actions; using Content.Server.DoAfter; using Content.Server.Polymorph.Components; using Content.Server.Polymorph.Systems; using Content.Shared._Sunrise.Kitsune; using Content.Shared._Sunrise.SpriteColor; using Content.Shared._Sunrise.TTS; using Content.Shared.Actions; using Content.Shared.Actions.Components; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Content.Shared.Humanoid; using Content.Shared.Polymorph; using Content.Shared.Popups; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Prototypes; namespace Content.Server._Sunrise.Kitsune; public sealed class KitsuneTransformSystem : EntitySystem { private const float TransformDurationSeconds = 240f; // 4 minutes private const float TransformDoAfterDurationSeconds = 3f; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly DoAfterSystem _doAfter = default!; [Dependency] private readonly PolymorphSystem _polymorph = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly DamageableSystem _damage = default!; [Dependency] private readonly SpriteColorSystem _spriteColor = default!; private ISawmill _sawmill = default!; // Dictionary to track when each transformed entity should auto-revert private Dictionary _transformDurations = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnKitsuneTransform); SubscribeLocalEvent(OnKitsuneTransformDoAfter); SubscribeLocalEvent(OnKitsuneRevert); SubscribeLocalEvent(OnKitsuneRevertDoAfter); } public override void Update(float frameTime) { base.Update(frameTime); // Check for expired transforms var expired = new List(); var toUpdate = new Dictionary(); foreach (var (uid, timeLeft) in _transformDurations) { var newTimeLeft = timeLeft - frameTime; if (newTimeLeft <= 0) expired.Add(uid); else toUpdate[uid] = newTimeLeft; } // Update the timers _transformDurations = toUpdate; // Auto-revert expired transforms foreach (var uid in expired) { if (TryComp(uid, out var component) && TryComp(uid, out var morphComp)) { _polymorph.Revert((uid, morphComp)); _popup.PopupEntity(Loc.GetString("kitsune-transform-expired"), uid, uid, PopupType.MediumCaution); } } } private void OnKitsuneTransform(EntityUid uid, KitsuneTransformComponent component, KitsuneTransformActionEvent args) { args.Handled = true; if (TryComp(uid, out _)) { _popup.PopupEntity(Loc.GetString("kitsune-transform-already-transformed"), uid, uid, PopupType.MediumCaution); return; } // Start the do-after var doAfterArgs = new DoAfterArgs(EntityManager, uid, TimeSpan.FromSeconds(TransformDoAfterDurationSeconds), new KitsuneTransformDoAfterEvent(), uid) { BreakOnMove = true, BreakOnDamage = true, MovementThreshold = 1f, NeedHand = false, }; if (_doAfter.TryStartDoAfter(doAfterArgs)) { _popup.PopupEntity(Loc.GetString("kitsune-transform-starting"), uid, uid, PopupType.MediumCaution); _audio.PlayPvs(new SoundPathSpecifier("/Audio/_Sunrise/BloodCult/butcher.ogg"), uid); } } private void OnKitsuneTransformDoAfter(EntityUid uid, KitsuneTransformComponent component, ref KitsuneTransformDoAfterEvent args) { if (args.Cancelled) return; // Transform into fox if (!_prototypeManager.TryIndex(new ProtoId("KitsuneTransform"), out var prototype)) { _popup.PopupEntity(Loc.GetString("kitsune-transform-failed"), uid, uid, PopupType.MediumCaution); _sawmill.Warning($"Kitsune transform failed: could not find 'KitsuneTransform' polymorph prototype"); return; } // Apply 9 slash damage to self var damage = new DamageSpecifier() { DamageDict = new Dictionary { { "Slash", FixedPoint2.New(9) } } }; _damage.TryChangeDamage(uid, damage); // Store the original entity reference before polymorph component.StashedHumanoid = uid; // Set transform duration timer _transformDurations[uid] = TransformDurationSeconds; // Perform polymorph var newUid = _polymorph.PolymorphEntity(uid, prototype) ?? throw new ArgumentNullException("_polymorph.PolymorphEntity(uid, prototype)"); // Transfer TTS voice to the fox form from the original humanoid's voice if (TryComp(uid, out var originalTts)) { if (TryComp(newUid, out var foxTts)) { foxTts.VoicePrototypeId = originalTts.VoicePrototypeId; } } // Apply the humanoid's hair color to the colored fur layer if (TryComp(uid, out var humanoidAppearance)) { // Use CachedHairColor if available, otherwise fallback to SkinColor var hairColor = humanoidAppearance.CachedHairColor ?? humanoidAppearance.EyeColor; _spriteColor.SetStateColor(newUid, "nine-tail_fox_gray_color", hairColor); } _popup.PopupEntity(Loc.GetString("kitsune-transform-success"), newUid, newUid, PopupType.MediumCaution); _audio.PlayPvs(new SoundPathSpecifier("/Audio/_Sunrise/BloodCult/enter_blood.ogg"), newUid); } private void OnKitsuneRevert(EntityUid uid, KitsuneTransformComponent component, KitsuneRevertActionEvent args) { args.Handled = true; if (!TryComp(uid, out _)) { _popup.PopupEntity(Loc.GetString("kitsune-revert-not-transformed"), uid, uid, PopupType.MediumCaution); return; } // Start the do-after for revert var doAfterArgs = new DoAfterArgs(EntityManager, uid, TimeSpan.FromSeconds(3), new KitsuneRevertDoAfterEvent(), uid) { BreakOnMove = true, BreakOnDamage = true, MovementThreshold = 1f, NeedHand = false, }; if (_doAfter.TryStartDoAfter(doAfterArgs)) { _popup.PopupEntity(Loc.GetString("kitsune-revert-starting"), uid, uid, PopupType.MediumCaution); } } private void OnKitsuneRevertDoAfter(EntityUid uid, KitsuneTransformComponent component, ref KitsuneRevertDoAfterEvent args) { if (args.Cancelled) return; // Clear the duration timer _transformDurations.Remove(uid); // Revert the polymorph if (!TryComp(uid, out var morphComp)) return; _polymorph.Revert((uid, morphComp)); _popup.PopupEntity(Loc.GetString("kitsune-revert-success"), uid, uid, PopupType.MediumCaution); } }