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:
@@ -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<MetaUpgradeCatalog>(out var metaCatalog) || !metaCatalog.Value.IsCreated
|
||||
|| !SystemAPI.TryGetSingletonBuffer<MetaTierState>(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<GymTag>();
|
||||
MetaUpgradeCatalog metaCatalog = default;
|
||||
DynamicBuffer<MetaTierState> metaRecord = default;
|
||||
if (!isGym && (!SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out metaCatalog) || !metaCatalog.Value.IsCreated
|
||||
|| !SystemAPI.TryGetSingletonBuffer<MetaTierState>(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<AbilitySocket>(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<<ClassId would compute bits 2/3 and silently skip everything).
|
||||
if (!isGym)
|
||||
{
|
||||
ref var metaPool = ref metaCatalog.Value.Value;
|
||||
byte classBit = BoonMath.MaskFor(classId);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user