From b92cc7196bfd563e4ea15c03abec16644a8ffe39 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Wed, 15 Jul 2026 00:01:13 -0700 Subject: [PATCH] =?UTF-8?q?LANTERN=20P1:=20enemy-test=20gym=20tooling=20?= =?UTF-8?q?=E2=80=94=20on-demand=20spawn=20+=20GymTag=20clean-spawn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GymTag + GymEnemyRoster (+ GymRosterAuthoring) bake the gym singletons. New DebugOp.SpawnEnemy (client SpawnEnemy wrapper + server receive case) spawns a chosen enemy KIND (Drowner/Grindylow) from the baked roster near the sender — replacing the wave-roster hack per the roadmap's Enemy-test GYM direction. GoInGameServerSystem takes a GymTag branch: bypass the meta-catalog spawn guard (a fresh gym has no CycleDirector) + seed a default Spark loadout on keys 1-4. Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/_Project/Scripts/Authoring/Debug.meta | 8 +++ .../Authoring/Debug/GymRosterAuthoring.cs | 49 +++++++++++++++++++ .../Debug/GymRosterAuthoring.cs.meta | 2 + .../Client/Debug/DebugCommandSendSystem.cs | 3 ++ .../Server/Connection/GoInGameServerSystem.cs | 21 +++++++- .../Server/Debug/DebugCommandReceiveSystem.cs | 27 ++++++++++ .../Simulation/Debug/DebugCommandRequest.cs | 3 ++ .../Scripts/Simulation/Debug/GymComponents.cs | 35 +++++++++++++ .../Simulation/Debug/GymComponents.cs.meta | 2 + 9 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Assets/_Project/Scripts/Authoring/Debug.meta create mode 100644 Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs create mode 100644 Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs.meta create mode 100644 Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs create mode 100644 Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs.meta diff --git a/Assets/_Project/Scripts/Authoring/Debug.meta b/Assets/_Project/Scripts/Authoring/Debug.meta new file mode 100644 index 000000000..5240a6a32 --- /dev/null +++ b/Assets/_Project/Scripts/Authoring/Debug.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ada099036105741448f2c567a6324e8f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs b/Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs new file mode 100644 index 000000000..7fdcdaeb2 --- /dev/null +++ b/Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using ProjectM.Simulation; +using Unity.Entities; +using UnityEngine; + +namespace ProjectM.Authoring +{ + /// + /// Bakes the Enemy-test GYM singletons: the mode switch + the + /// buffer of on-demand-spawnable enemy prefabs (Drowner / Grindylow — the ready skinned prefabs). Place ONE in + /// the gym subscene. Each prefab must be a baked ghost (interpolated ownerless enemy, EnemyAuthoring-based). The + /// SpawnEnemy dev op instantiates the row whose Kind matches the request. + /// + public class GymRosterAuthoring : MonoBehaviour + { + [System.Serializable] + public struct Entry + { + [Tooltip("Stable kind id (0 = Drowner, 1 = Grindylow).")] + public byte Kind; + public GameObject Prefab; + } + + [Tooltip("On-demand-spawnable enemy prefabs, one row per kind.")] + public List Enemies = new List(); + + private class RosterBaker : Baker + { + public override void Bake(GymRosterAuthoring authoring) + { + var entity = GetEntity(TransformUsageFlags.None); + AddComponent(entity); + var buf = AddBuffer(entity); + if (authoring.Enemies != null) + { + foreach (var e in authoring.Enemies) + { + if (e.Prefab == null) continue; + buf.Add(new GymEnemyRoster + { + Kind = e.Kind, + Prefab = GetEntity(e.Prefab, TransformUsageFlags.Dynamic), + }); + } + } + } + } + } +} diff --git a/Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs.meta new file mode 100644 index 000000000..a6a2e6bf5 --- /dev/null +++ b/Assets/_Project/Scripts/Authoring/Debug/GymRosterAuthoring.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d210e424c43414a4e958efc44c025335 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Client/Debug/DebugCommandSendSystem.cs b/Assets/_Project/Scripts/Client/Debug/DebugCommandSendSystem.cs index bf6fccf86..eb5ac20c9 100644 --- a/Assets/_Project/Scripts/Client/Debug/DebugCommandSendSystem.cs +++ b/Assets/_Project/Scripts/Client/Debug/DebugCommandSendSystem.cs @@ -44,6 +44,9 @@ namespace ProjectM.Client public static void SetClass(byte classId) => Send(DebugOp.SetClass, classId); public static void SetWarrior() => SetClass(ClassTraits.WarriorClass); public static void SetRanger() => SetClass(ClassTraits.RangerClass); + /// GYM: spawn enemy KIND (a byte) near the sender, count times. + public static void SpawnEnemy(byte kind, int count = 1) => Send(DebugOp.SpawnEnemy, kind, count); + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void ResetOnEnterPlayMode() => s_Pending.Clear(); diff --git a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs index 59dd939ea..a288816a5 100644 --- a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs +++ b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs @@ -40,8 +40,14 @@ namespace ProjectM.Server // BEFORE the ECB exists — a per-request continue would already have marked the connection in-game. // Nothing is consumed; RequireForUpdate re-passes and the request retries next tick (a ≤1-tick window // in practice — the director spawns at subscene-stream, before any GoInGame round-trip). - if (!SystemAPI.TryGetSingleton(out var metaCatalog) || !metaCatalog.Value.IsCreated - || !SystemAPI.TryGetSingletonBuffer(out var metaRecord, true)) + // GYM (LANTERN A1): a fresh gym subscene has no CycleDirector, so the base meta-catalog guard would + // block spawns forever. GymTag switches to the clean gym path (no meta seeding; a default Spark socket + // loadout below instead). The class seeds still apply (harmless: AbilityFireSystem reads sockets). + bool isGym = SystemAPI.HasSingleton(); + MetaUpgradeCatalog metaCatalog = default; + DynamicBuffer metaRecord = default; + if (!isGym && (!SystemAPI.TryGetSingleton(out metaCatalog) || !metaCatalog.Value.IsCreated + || !SystemAPI.TryGetSingletonBuffer(out metaRecord, true))) { if (!_warnedMetaBlocked) { @@ -82,11 +88,22 @@ namespace ProjectM.Server // Expedition redesign: the server-only class anchor the meta systems key on (born-correct meta // seeding at Step 12a + per-class spend at Step 13 resolve the tier record through this). ecb.AddComponent(player, new PlayerClass { ClassId = classId }); + if (isGym) + { + // GYM: default Spark socket loadout on keys 1-4 (AbilityFireSystem reads sockets, not AbilityRef). + // LightZone(9) is defined + dispatch-ready but off the default bar (socket it via a swap). + var gymSockets = ecb.SetBuffer(player); + gymSockets.Add(new AbilitySocket { SparkId = (byte)AbilityId.HookPull }); + gymSockets.Add(new AbilitySocket { SparkId = (byte)AbilityId.Blink }); + gymSockets.Add(new AbilitySocket { SparkId = (byte)AbilityId.DecoyWisp }); + gymSockets.Add(new AbilitySocket { SparkId = (byte)AbilityId.Vortex }); + } // Step 12a: born-correct PERMANENT meta seeding — replay this class's persisted tiers as // meta-band StatModifiers on the just-instantiated player (same ECB as Instantiate, the // ClassTraits idiom). Skip tier 0 / unknown ids (preserve-don't-crash); CLAMP a saved tier above a // rebalanced MaxTier (D-F5). Class gate via BoonMath.MaskFor (ClassId is the normalized // CharacterId 2/3 — a raw 1<(sender) + && SystemAPI.TryGetSingletonEntity(out var gymEntity) + && SystemAPI.HasBuffer(gymEntity)) + { + var roster = SystemAPI.GetBuffer(gymEntity); + Entity enemyPrefab = Entity.Null; + for (int r = 0; r < roster.Length; r++) + if (roster[r].Kind == (byte)cmd.ArgA) { enemyPrefab = roster[r].Prefab; break; } + if (enemyPrefab != Entity.Null) + { + var sPos = SystemAPI.GetComponent(sender).Position; + var bakedEnemyLt = state.EntityManager.GetComponentData(enemyPrefab); + int spawnCount = math.max(1, cmd.ArgB); + for (int k = 0; k < spawnCount; k++) + { + float ang = k * 0.7f; + float3 pos = sPos + new float3(math.cos(ang), 0f, math.sin(ang)) * 5f; + pos.y = sPos.y; + var enemy = ecb.Instantiate(enemyPrefab); + ecb.SetComponent(enemy, bakedEnemyLt.WithPosition(pos)); + ecb.AddComponent(enemy, new RegionTag { Region = RegionId.Base }); + } + } + } + break; } ecb.DestroyEntity(reqEntity); diff --git a/Assets/_Project/Scripts/Simulation/Debug/DebugCommandRequest.cs b/Assets/_Project/Scripts/Simulation/Debug/DebugCommandRequest.cs index e9f49e846..1f122e345 100644 --- a/Assets/_Project/Scripts/Simulation/Debug/DebugCommandRequest.cs +++ b/Assets/_Project/Scripts/Simulation/Debug/DebugCommandRequest.cs @@ -68,5 +68,8 @@ namespace ProjectM.Simulation /// Strips the old class trait seeds, re-seeds the new ones, swaps the Fire ability, and heals a living /// player to the new class's max. Editor-only dev tool (class-switch); 0 / unknown -> Warrior. public const byte SetClass = 13; + + /// GYM (editor): spawn ArgA near the sender, ArgB times (default 1). Reads the baked GymEnemyRoster. + public const byte SpawnEnemy = 14; } } diff --git a/Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs b/Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs new file mode 100644 index 000000000..a568adbb0 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs @@ -0,0 +1,35 @@ +using Unity.Entities; + +namespace ProjectM.Simulation +{ + /// + /// Singleton tag baked into the Enemy-test GYM subscene (see the LANTERN roadmap A1 gym note). Marks the world + /// as the gym so GoInGameServerSystem takes the CLEAN gym spawn path — bypassing the base/meta/cycle + /// seeding (the gym has no CycleDirector, so the meta-catalog spawn guard must not block it) and seeding a + /// default Spark socket loadout instead. Purely a mode switch; the gym never runs the base siege/economy. + /// + public struct GymTag : IComponentData { } + + /// + /// Baked gym enemy roster — one row per on-demand-spawnable enemy KIND (buffer index is arbitrary; match on + /// ). DebugCommandReceiveSystem's SpawnEnemy op instantiates the row whose Kind == + /// ArgA near the requesting player. Entity refs can't live in a blob, so this is a companion buffer on the + /// gym-roster entity (the idiom). Server-read only. + /// + [InternalBufferCapacity(4)] + public struct GymEnemyRoster : IBufferElementData + { + /// Stable enemy-kind key (0 = Drowner, 1 = Grindylow; extend as the roster grows). + public byte Kind; + + /// The baked enemy ghost prefab entity to instantiate for this kind. + public Entity Prefab; + } + + /// Stable kind ids for / the SpawnEnemy op's ArgA. + public static class GymEnemyKind + { + public const byte Drowner = 0; + public const byte Grindylow = 1; + } +} diff --git a/Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs.meta b/Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs.meta new file mode 100644 index 000000000..e817e5709 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Debug/GymComponents.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f39498494d08f66458393a87b395cd62 \ No newline at end of file