LANTERN P1: enemy-test gym tooling — on-demand spawn + GymTag clean-spawn

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:01:13 -07:00
parent b011a367e3
commit b92cc7196b
9 changed files with 148 additions and 2 deletions
@@ -208,6 +208,33 @@ namespace ProjectM.Server
}
}
break;
case DebugOp.SpawnEnemy:
// GYM: spawn a chosen enemy KIND (Drowner/Grindylow) from the baked roster near the sender.
if (sender != Entity.Null && SystemAPI.HasComponent<LocalTransform>(sender)
&& SystemAPI.TryGetSingletonEntity<GymTag>(out var gymEntity)
&& SystemAPI.HasBuffer<GymEnemyRoster>(gymEntity))
{
var roster = SystemAPI.GetBuffer<GymEnemyRoster>(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<LocalTransform>(sender).Position;
var bakedEnemyLt = state.EntityManager.GetComponentData<LocalTransform>(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);