using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
using Theriapolis.Core;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Entities.Ai;
using Theriapolis.Core.Persistence;
using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;

namespace Theriapolis.Game.Screens;

/// <summary>
/// Phase 5 M5 turn-based combat overlay. Pushed by PlayScreen when an
/// encounter triggers; owns the live <see cref="Encounter"/>, drives input
/// on the player's turn, ticks NPC behaviors on theirs, and on victory
/// writes results back to the live actors and pops itself.
///
/// Player input (during player's turn):
///   WASD / arrows → move 1 tactical tile (5 ft. of movement budget)
///   SPACE          → attack closest hostile in reach
///   ENTER          → end turn
///
/// Save-anywhere works mid-combat: PlayScreen.CaptureBody calls
/// <see cref="SnapshotForSave"/>; on load, PlayScreen re-pushes this screen
/// with the rebuilt encounter via the rehydrate constructor.
/// </summary>
public sealed class CombatHUDScreen : IScreen
{
    private readonly Encounter        _encounter;
    private readonly ActorManager     _actors;
    private readonly Theriapolis.Core.Data.ContentResolver? _content;
    private readonly System.Action<EncounterEndResult> _onEnd;

    private Game1 _game = null!;
    private Desktop _desktop = null!;
    private Label?  _initLabel;
    private Label?  _logLabel;
    private Label?  _actionLabel;

    // NPC turn pacing — instant-resolve NPC turns frame-by-frame so the log scrolls.
    private float _npcTurnDelay;
    private const float NPC_TURN_SECONDS = 0.4f;

    // Edge-detect input.
    private bool _spaceWas, _enterWas, _wWas, _aWas, _sWas, _dWas;
    private bool _upWas, _downWas, _leftWas, _rightWas;
    private bool _rWas, _tWas;
    // Phase 6.5 M1 — class feature hotkeys: H = heal (Field Repair / Lay on
    // Paws), V = vocalize (Vocalization Dice).
    private bool _hWas, _vWas;
    // Phase 6.5 M3 — P = pheromone (Scent-Broker), O = oath (Covenant-Keeper).
    private bool _pWas, _oWas;

    public Encounter Encounter => _encounter;
    public bool      IsOver    => _encounter.IsOver;

    /// <summary>Build a fresh encounter from the supplied participants and push the HUD.</summary>
    public CombatHUDScreen(
        Encounter encounter,
        ActorManager actors,
        System.Action<EncounterEndResult> onEnd,
        Theriapolis.Core.Data.ContentResolver? content = null)
    {
        _encounter = encounter ?? throw new System.ArgumentNullException(nameof(encounter));
        _actors    = actors    ?? throw new System.ArgumentNullException(nameof(actors));
        _onEnd     = onEnd     ?? throw new System.ArgumentNullException(nameof(onEnd));
        _content   = content;
    }

    public void Initialize(Game1 game)
    {
        _game = game;
        BuildUI();
    }

    private void BuildUI()
    {
        var root = new VerticalStackPanel
        {
            Spacing             = 4,
            HorizontalAlignment = HorizontalAlignment.Stretch,
            VerticalAlignment   = VerticalAlignment.Bottom,
            Padding             = new Thickness(12, 8, 12, 8),
            Background          = new SolidBrush(new Color(0, 0, 0, 200)),
        };

        _initLabel = new Label { Text = "" };
        root.Widgets.Add(_initLabel);

        _logLabel = new Label { Text = "" };
        root.Widgets.Add(_logLabel);

        _actionLabel = new Label { Text = "WASD: move · SPACE: attack · ENTER: end turn" };
        root.Widgets.Add(_actionLabel);

        _desktop = new Desktop { Root = root };
        Refresh();
    }

    public void Update(GameTime gt)
    {
        if (_encounter.IsOver) { Resolve(); return; }

        var actor = _encounter.CurrentActor;
        if (actor.IsDown)
        {
            // Phase 5 M6: player death-save loop. Roll once at the start of
            // the player's turn while at 0 HP, then end turn. NPC combatants
            // skip this and go straight to EndTurn (they're removed from
            // initiative since IsAlive is false).
            if (actor.SourceCharacter is not null && actor.DeathSaves is not null)
            {
                var outcome = actor.DeathSaves.Roll(_encounter, actor);
                if (outcome == Theriapolis.Core.Rules.Combat.DeathSaveOutcome.Dead)
                {
                    PushDefeated(actor.Name + " fell to a final-blow death save.");
                    return;
                }
            }
            _encounter.EndTurn();
            Refresh();
            return;
        }

        // Find this combatant's live actor (by id) so we can dispatch behavior or read input.
        var liveActor = FindLiveActor(actor.Id);
        if (liveActor is NpcActor npc)
        {
            _npcTurnDelay += (float)gt.ElapsedGameTime.TotalSeconds;
            if (_npcTurnDelay < NPC_TURN_SECONDS) return;
            _npcTurnDelay = 0f;
            var ctx = new AiContext(_encounter);
            BehaviorRegistry.For(npc.BehaviorId).TakeTurn(actor, ctx);
            _encounter.EndTurn();
            Refresh();
            return;
        }

        // Player turn — wait for input.
        DrivePlayerTurn(gt);
        Refresh();
    }

    private void DrivePlayerTurn(GameTime gt)
    {
        var ks = Keyboard.GetState();
        bool space = JustPressed(ks, Keys.Space, ref _spaceWas);
        bool enter = JustPressed(ks, Keys.Enter, ref _enterWas);
        bool w     = JustPressed(ks, Keys.W,     ref _wWas);
        bool a     = JustPressed(ks, Keys.A,     ref _aWas);
        bool s     = JustPressed(ks, Keys.S,     ref _sWas);
        bool d     = JustPressed(ks, Keys.D,     ref _dWas);
        bool up    = JustPressed(ks, Keys.Up,    ref _upWas);
        bool down  = JustPressed(ks, Keys.Down,  ref _downWas);
        bool left  = JustPressed(ks, Keys.Left,  ref _leftWas);
        bool right = JustPressed(ks, Keys.Right, ref _rightWas);

        int dx = 0, dy = 0;
        if (w || up)    dy = -1;
        if (s || down)  dy = +1;
        if (a || left)  dx = -1;
        if (d || right) dx = +1;
        if (dx != 0 || dy != 0) TryMovePlayer(dx, dy);
        if (space)               TryAttack();
        if (enter)               { _encounter.EndTurn(); Refresh(); }

        // Phase 5 M6: class-feature toggles.
        bool r = JustPressed(ks, Keys.R, ref _rWas);
        bool t = JustPressed(ks, Keys.T, ref _tWas);
        if (r) TryToggleRage();
        if (t) TryToggleSentinelStance();

        // Phase 6.5 M1: heal + vocalize hotkeys. H prefers Lay on Paws when
        // available (Covenant-Keeper); falls through to Field Repair
        // (Claw-Wright). V grants a Vocalization Die to the closest ally.
        bool h = JustPressed(ks, Keys.H, ref _hWas);
        bool v = JustPressed(ks, Keys.V, ref _vWas);
        if (h) TryHealAction();
        if (v) TryVocalize();

        // Phase 6.5 M3: pheromone + oath hotkeys. P emits a Fear pheromone
        // (the most universally useful default; future UI can let the
        // player pick the type). O declares an oath against the closest
        // hostile.
        bool p = JustPressed(ks, Keys.P, ref _pWas);
        bool o = JustPressed(ks, Keys.O, ref _oWas);
        if (p) TryEmitPheromone();
        if (o) TryDeclareOath();
    }

    /// <summary>
    /// Phase 6.5 M3 — Scent-Broker Pheromone Craft hotkey. Bonus action;
    /// emits a Fear pheromone in 10-ft radius. Hostiles in range CON-save
    /// or get Frightened. Future UI iteration can offer a type picker.
    /// </summary>
    private void TryEmitPheromone()
    {
        var actor = _encounter.CurrentActor;
        var c = actor.SourceCharacter;
        if (c is null || c.ClassDef.Id != "scent_broker") return;
        if (c.Level < 2)
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name}: Pheromone Craft unlocks at level 2.");
            return;
        }
        if (c.PheromoneUsesRemaining <= 0)
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name}: no Pheromone Craft uses remaining.");
            return;
        }
        if (Theriapolis.Core.Rules.Combat.FeatureProcessor.TryEmitPheromone(
                _encounter, actor, Theriapolis.Core.Rules.Combat.PheromoneType.Fear))
            _encounter.CurrentTurn.ConsumeBonusAction();
    }

    /// <summary>
    /// Phase 6.5 M3 — Covenant-Keeper Covenant's Authority hotkey. Bonus
    /// action; declares an oath against the closest hostile, inflicting
    /// -2 to attack rolls vs. the Covenant-Keeper for 10 rounds.
    /// </summary>
    private void TryDeclareOath()
    {
        var actor = _encounter.CurrentActor;
        var c = actor.SourceCharacter;
        if (c is null || c.ClassDef.Id != "covenant_keeper") return;
        if (c.Level < 2)
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name}: Covenant's Authority unlocks at level 2.");
            return;
        }
        var aiCtx = new Theriapolis.Core.Entities.Ai.AiContext(_encounter);
        var target = aiCtx.FindClosestHostile(actor);
        if (target is null)
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name}: no hostile target in sight.");
            return;
        }
        if (Theriapolis.Core.Rules.Combat.FeatureProcessor.TryDeclareOath(
                _encounter, actor, target))
            _encounter.CurrentTurn.ConsumeBonusAction();
    }

    /// <summary>
    /// Phase 6.5 M1 — heal hotkey. Auto-targets the most-damaged friendly
    /// (self or ally). Uses Lay on Paws (Covenant-Keeper) when there's pool
    /// remaining, else falls through to Field Repair (Claw-Wright).
    /// Consumes the action and a bonus action where appropriate. No-op for
    /// non-healer classes.
    /// </summary>
    private void TryHealAction()
    {
        if (!_encounter.CurrentTurn.ActionAvailable) return;
        var actor = _encounter.CurrentActor;
        var c = actor.SourceCharacter;
        if (c is null) return;
        var aiCtx = new Theriapolis.Core.Entities.Ai.AiContext(_encounter);
        var target = aiCtx.FindMostDamagedFriendly(actor) ?? actor;

        bool acted = false;
        if (c.ClassDef.Id == "covenant_keeper" && c.LayOnPawsPoolRemaining > 0)
        {
            // Spend up to 5 (or whatever's needed to top up the target) per
            // press — keeps the no-target-picker UX quick to use repeatedly.
            int request = System.Math.Max(1, target.MaxHp - target.CurrentHp);
            if (request > 5) request = 5;
            acted = Theriapolis.Core.Rules.Combat.FeatureProcessor.TryLayOnPaws(_encounter, actor, target, request);
        }
        else if (c.ClassDef.Id == "claw_wright" && c.FieldRepairUsesRemaining > 0)
        {
            acted = Theriapolis.Core.Rules.Combat.FeatureProcessor.TryFieldRepair(_encounter, actor, target);
        }
        else
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name} has no heal action available.");
        }
        if (acted) _encounter.CurrentTurn.ConsumeAction();
    }

    /// <summary>
    /// Phase 6.5 M1 — Vocalization Dice. Bonus action for Muzzle-Speakers;
    /// auto-targets the closest ally (excludes self). No-op when no ally is
    /// in combat (the typical M1 case — the player is alone).
    /// </summary>
    private void TryVocalize()
    {
        var actor = _encounter.CurrentActor;
        var c = actor.SourceCharacter;
        if (c is null || c.ClassDef.Id != "muzzle_speaker") return;
        if (c.VocalizationDiceRemaining <= 0)
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name}: no Vocalization Dice remaining.");
            return;
        }
        var aiCtx = new Theriapolis.Core.Entities.Ai.AiContext(_encounter);
        var ally = aiCtx.FindClosestAlly(actor);
        if (ally is null)
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name}: no ally in range to inspire.");
            return;
        }
        if (Theriapolis.Core.Rules.Combat.FeatureProcessor.TryGrantVocalizationDie(_encounter, actor, ally))
            _encounter.CurrentTurn.ConsumeBonusAction();
    }

    private void TryToggleRage()
    {
        var actor = _encounter.CurrentActor;
        if (actor.RageActive)
        {
            actor.RageActive = false;
            _encounter.AppendLog(CombatLogEntry.Kind.Note, $"{actor.Name} ends the rage.");
            return;
        }
        if (!FeatureProcessor.TryActivateRage(_encounter, actor))
            _encounter.AppendLog(CombatLogEntry.Kind.Note, $"{actor.Name} can't enter rage right now.");
        _encounter.CurrentTurn.ConsumeBonusAction();
    }

    private void TryToggleSentinelStance()
    {
        var actor = _encounter.CurrentActor;
        FeatureProcessor.ToggleSentinelStance(_encounter, actor);
        _encounter.CurrentTurn.ConsumeBonusAction();
    }

    private void TryMovePlayer(int dx, int dy)
    {
        if (_encounter.CurrentTurn.RemainingMovementFt < 5) return;
        var actor = _encounter.CurrentActor;
        var newPos = new Vec2((int)actor.Position.X + dx, (int)actor.Position.Y + dy);
        actor.Position = newPos;
        _encounter.AppendLog(CombatLogEntry.Kind.Move,
            $"{actor.Name} moves to ({(int)newPos.X},{(int)newPos.Y}).");
        _encounter.CurrentTurn.ConsumeMovement(5);
    }

    private void TryAttack()
    {
        if (!_encounter.CurrentTurn.ActionAvailable) return;
        var actor   = _encounter.CurrentActor;
        var ctx     = new AiContext(_encounter);
        var target  = ctx.FindClosestHostile(actor);
        if (target is null) return;
        var attack  = actor.AttackOptions[0];
        if (!ReachAndCover.IsInReach(actor, target, attack))
        {
            _encounter.AppendLog(CombatLogEntry.Kind.Note,
                $"{actor.Name}: target out of reach.");
            return;
        }
        Resolver.AttemptAttack(_encounter, actor, target, attack);
        _encounter.CurrentTurn.ConsumeAction();
    }

    private static bool JustPressed(KeyboardState ks, Keys k, ref bool was)
    {
        bool now = ks.IsKeyDown(k);
        bool jp  = now && !was;
        was = now;
        return jp;
    }

    private void Refresh()
    {
        if (_initLabel is not null)
        {
            var sb = new System.Text.StringBuilder();
            sb.Append($"R{_encounter.RoundNumber} ");
            for (int i = 0; i < _encounter.InitiativeOrder.Count; i++)
            {
                var c = _encounter.Participants[_encounter.InitiativeOrder[i]];
                if (i == _encounter.CurrentTurnIndex) sb.Append("→");
                sb.Append($"[{c.Name} {c.CurrentHp}/{c.MaxHp}] ");
            }
            _initLabel.Text = sb.ToString();
        }
        if (_logLabel is not null)
        {
            int start = System.Math.Max(0, _encounter.Log.Count - 6);
            var sb = new System.Text.StringBuilder();
            for (int i = start; i < _encounter.Log.Count; i++)
            {
                var e = _encounter.Log[i];
                sb.AppendLine(e.Message);
            }
            _logLabel.Text = sb.ToString();
        }
        if (_actionLabel is not null)
        {
            var actor = _encounter.IsOver ? null : _encounter.CurrentActor;
            if (actor is null)
                _actionLabel.Text = "Encounter ended.";
            else if (FindLiveActor(actor.Id) is NpcActor)
                _actionLabel.Text = $"{actor.Name}'s turn (NPC) …";
            else
            {
                string featureHints = "";
                var c = actor.SourceCharacter;
                if (c is not null)
                {
                    if (c.ClassDef.Id == "feral")
                        featureHints += actor.RageActive
                            ? $"  [Raging — R to end]"
                            : $"  [R: Rage ({c.RageUsesRemaining} left)]";
                    if (c.ClassDef.Id == "bulwark")
                        featureHints += actor.SentinelStanceActive
                            ? "  [Stance — T to leave]"
                            : "  [T: Sentinel Stance]";
                    // Phase 6.5 M1 hotkey hints.
                    if (c.ClassDef.Id == "covenant_keeper" && c.LayOnPawsPoolRemaining > 0)
                        featureHints += $"  [H: Lay on Paws ({c.LayOnPawsPoolRemaining} HP)]";
                    if (c.ClassDef.Id == "claw_wright" && c.FieldRepairUsesRemaining > 0)
                        featureHints += $"  [H: Field Repair ({c.FieldRepairUsesRemaining})]";
                    if (c.ClassDef.Id == "muzzle_speaker" && c.VocalizationDiceRemaining > 0)
                        featureHints += $"  [V: Vocalize ({c.VocalizationDiceRemaining})]";
                    // Phase 6.5 M3 hotkey hints.
                    if (c.ClassDef.Id == "scent_broker" && c.Level >= 2 && c.PheromoneUsesRemaining > 0)
                        featureHints += $"  [P: Pheromone ({c.PheromoneUsesRemaining})]";
                    if (c.ClassDef.Id == "covenant_keeper" && c.Level >= 2 && c.CovenantAuthorityUsesRemaining > 0)
                        featureHints += $"  [O: Oath ({c.CovenantAuthorityUsesRemaining})]";
                }
                _actionLabel.Text = $"{actor.Name}'s turn — WASD: move ({_encounter.CurrentTurn.RemainingMovementFt}ft left) · SPACE: attack · ENTER: end turn{featureHints}";
            }
        }
    }

    private Actor? FindLiveActor(int id)
    {
        foreach (var a in _actors.All)
            if (a.Id == id) return a;
        return null;
    }

    private void Resolve()
    {
        // Write combatant state back to live actors and remove dead NPCs.
        // Phase 5 M6: roll loot per killed NPC and auto-pickup into player
        // inventory. Loot RNG is a sub-stream of the encounter seed so save+load
        // round-trips produce identical drops.
        var killedByChunk = new Dictionary<Theriapolis.Core.Tactical.ChunkCoord, List<int>>();
        var pickedUp     = new List<(string Name, int Qty)>();
        var lootRng      = _content is null
            ? null
            : new Theriapolis.Core.Util.SeededRng(_encounter.EncounterSeed ^ Theriapolis.Core.C.RNG_LOOT);
        Theriapolis.Core.Items.Inventory? playerInv = null;
        int xpEarned = 0;     // Phase 6.5 M0 — sum of killed-NPC XpAward values; awarded to player below.

        foreach (var c in _encounter.Participants)
        {
            var live = FindLiveActor(c.Id);
            if (live is NpcActor npc)
            {
                npc.CurrentHp = c.CurrentHp;
                npc.Position  = c.Position;
                if (c.IsDown)
                {
                    if (npc.SourceChunk is { } chunk && npc.SourceSpawnIndex is int idx)
                    {
                        if (!killedByChunk.TryGetValue(chunk, out var list))
                            killedByChunk[chunk] = list = new List<int>();
                        list.Add(idx);
                    }
                    // Auto-pickup loot into player inventory. Residents
                    // (Phase 6 M1) don't have a loot table — only Phase 5
                    // hostiles do.
                    if (lootRng is not null && _content is not null && npc.Template is not null)
                    {
                        var drops = Theriapolis.Core.Loot.LootRoller.Roll(
                            npc.Template.LootTable, _content.LootTables, _content.Items, lootRng);
                        foreach (var d in drops)
                        {
                            playerInv ??= _actors.Player?.Character?.Inventory;
                            if (playerInv is null) break;
                            playerInv.Add(d.Def, d.Qty);
                            pickedUp.Add((d.Def.Name, d.Qty));
                        }
                    }
                    // Phase 6.5 M0 — award XP for the kill. Templates' XpAward
                    // was loaded since Phase 5 but never consumed; this is
                    // the wiring.
                    if (npc.Template is not null && npc.Template.XpAward > 0)
                        xpEarned += npc.Template.XpAward;
                    _actors.RemoveActor(npc.Id);
                }
            }
            else if (live is PlayerActor pa && pa.Character is not null)
            {
                pa.Character.CurrentHp = c.CurrentHp;
                pa.Position            = c.Position;
                pa.Character.Conditions.Clear();
                foreach (var cond in c.Conditions) pa.Character.Conditions.Add(cond);
            }
        }

        if (pickedUp.Count > 0)
        {
            string lootLine = string.Join(", ", pickedUp.Select(p => p.Qty > 1 ? $"{p.Name} ×{p.Qty}" : p.Name));
            _encounter.AppendLog(CombatLogEntry.Kind.Note, "Picked up: " + lootLine);
        }

        // Phase 6.5 M0 — award accumulated combat XP to the player.
        if (xpEarned > 0)
        {
            var pcChar = _actors.Player?.Character;
            if (pcChar is not null)
            {
                pcChar.Xp += xpEarned;
                _encounter.AppendLog(CombatLogEntry.Kind.Note, $"+{xpEarned} XP.");
                if (Theriapolis.Core.Rules.Character.LevelUpFlow.CanLevelUp(pcChar))
                    _encounter.AppendLog(CombatLogEntry.Kind.Note, "Level up available — open the pause menu.");
            }
        }

        var result = new EncounterEndResult
        {
            Killed         = killedByChunk,
            PlayerSurvived = _encounter.Participants.Any(c =>
                c.Allegiance == Theriapolis.Core.Rules.Character.Allegiance.Player && !c.IsDown),
        };
        _onEnd(result);
        _game.Screens.Pop();
    }

    /// <summary>
    /// Snapshot the encounter for save-anywhere. PlayScreen.CaptureBody
    /// calls this and stores the result in <c>SaveBody.ActiveEncounter</c>.
    /// </summary>
    private void PushDefeated(string cause)
    {
        // Pop ourselves so the play-screen sits underneath; then push the
        // DefeatedScreen which the player can dismiss to return to title.
        _onEnd(new EncounterEndResult { PlayerSurvived = false });
        _game.Screens.Pop();
        _game.Screens.Push(new DefeatedScreen(cause));
    }

    public EncounterState SnapshotForSave()
    {
        var snaps = new CombatantSnapshot[_encounter.Participants.Count];
        for (int i = 0; i < _encounter.Participants.Count; i++)
        {
            var c = _encounter.Participants[i];
            var snap = new CombatantSnapshot
            {
                Id          = c.Id,
                Name        = c.Name,
                IsPlayer    = c.SourceCharacter is not null,
                CurrentHp   = c.CurrentHp,
                PositionX   = c.Position.X,
                PositionY   = c.Position.Y,
                Conditions  = c.Conditions.Select(x => (byte)x).ToArray(),
            };
            if (c.SourceTemplate is not null)
            {
                snap.NpcTemplateId = c.SourceTemplate.Id;
                var live = FindLiveActor(c.Id);
                if (live is NpcActor npc)
                {
                    snap.NpcChunkX     = npc.SourceChunk?.X;
                    snap.NpcChunkY     = npc.SourceChunk?.Y;
                    snap.NpcSpawnIndex = npc.SourceSpawnIndex;
                }
            }
            snaps[i] = snap;
        }
        var initOrder = new int[_encounter.InitiativeOrder.Count];
        for (int i = 0; i < initOrder.Length; i++) initOrder[i] = _encounter.InitiativeOrder[i];
        return new EncounterState
        {
            EncounterId      = _encounter.EncounterId,
            RollCount        = _encounter.RollCount,
            CurrentTurnIndex = _encounter.CurrentTurnIndex,
            RoundNumber      = _encounter.RoundNumber,
            InitiativeOrder  = initOrder,
            Combatants       = snaps,
        };
    }

    public void Draw(GameTime gt, SpriteBatch sb)
    {
        // Don't clear — let the play-screen's last frame stay visible underneath.
        _desktop.Render();
    }

    public void Deactivate() { }
    public void Reactivate() { }
}

/// <summary>
/// Reported back to PlayScreen when an encounter wraps so it can update
/// the chunk roster delta + decide whether to push the death screen.
/// </summary>
public sealed class EncounterEndResult
{
    public Dictionary<Theriapolis.Core.Tactical.ChunkCoord, List<int>> Killed { get; init; }
        = new();
    public bool PlayerSurvived { get; init; } = true;
}
