using NeonSprawl.Server.Game.Combat; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Npc; /// Maps GET /game/world/npc-runtime-snapshot (NEO-94). public static class NpcRuntimeSnapshotWorldApi { public static WebApplication MapNpcRuntimeSnapshotWorldApi(this WebApplication app) { app.MapGet( "/game/world/npc-runtime-snapshot", ( TimeProvider clock, INpcRuntimeStateStore runtimeStore, IThreatStateStore threatStore, INpcBehaviorDefinitionRegistry behaviorRegistry, IAbilityDefinitionRegistry abilityRegistry, ICombatEntityHealthStore combatHealthStore, IPositionStateStore positionStore, IPlayerCombatHealthStore playerHealthStore) => { var now = clock.GetUtcNow(); NpcRuntimeOperations.AdvanceAll( now, runtimeStore, threatStore, behaviorRegistry, abilityRegistry, combatHealthStore, positionStore, playerHealthStore); return Results.Json(BuildSnapshot(now, runtimeStore, threatStore, behaviorRegistry)); }); return app; } internal static NpcRuntimeSnapshotResponse BuildSnapshot( DateTimeOffset nowUtc, INpcRuntimeStateStore runtimeStore, IThreatStateStore threatStore, INpcBehaviorDefinitionRegistry behaviorRegistry) { var ids = PrototypeNpcRegistry.GetInstanceIdsInOrder(); var instances = new List(ids.Count); foreach (var npcInstanceId in ids) { if (!PrototypeNpcRegistry.TryGet(npcInstanceId, out var entry)) { throw new InvalidOperationException( $"Prototype NPC instance '{npcInstanceId}' is missing from {nameof(PrototypeNpcRegistry)}."); } if (!behaviorRegistry.TryGetDefinition(entry.BehaviorDefId, out var behavior)) { throw new InvalidOperationException( $"Behavior definition '{entry.BehaviorDefId}' for NPC '{npcInstanceId}' is missing from {nameof(INpcBehaviorDefinitionRegistry)}."); } if (!runtimeStore.TryGet(npcInstanceId, out var runtime)) { throw new InvalidOperationException( $"Prototype NPC runtime row '{npcInstanceId}' is registered but missing from {nameof(INpcRuntimeStateStore)}."); } string? aggroHolderPlayerId = null; if (threatStore.TryGet(npcInstanceId, out var threat)) { aggroHolderPlayerId = threat.AggroHolderPlayerId; } ActiveTelegraphJson? activeTelegraph = null; if (runtime.BehaviorState == NpcBehaviorState.TelegraphWindup && runtime.ActiveTelegraph is { } telegraph) { var elapsedSeconds = (nowUtc - telegraph.WindupStartedUtc).TotalSeconds; var remainingSeconds = Math.Max(0, behavior.TelegraphWindupSeconds - elapsedSeconds); activeTelegraph = new ActiveTelegraphJson { TelegraphId = telegraph.TelegraphId, NpcInstanceId = npcInstanceId, WindupRemainingSeconds = remainingSeconds, ArchetypeKind = behavior.ArchetypeKind, }; } instances.Add( new NpcInstanceRuntimeJson { NpcInstanceId = npcInstanceId, BehaviorDefId = entry.BehaviorDefId, State = NpcBehaviorStateWire.ToWireName(runtime.BehaviorState), AggroHolderPlayerId = aggroHolderPlayerId, ActiveTelegraph = activeTelegraph, }); } return new NpcRuntimeSnapshotResponse { ServerTimeUtc = nowUtc, NpcInstances = instances, }; } }