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:
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ada099036105741448f2c567a6324e8f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Entities;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ProjectM.Authoring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bakes the Enemy-test GYM singletons: the <see cref="GymTag"/> mode switch + the <see cref="GymEnemyRoster"/>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<Entry> Enemies = new List<Entry>();
|
||||||
|
|
||||||
|
private class RosterBaker : Baker<GymRosterAuthoring>
|
||||||
|
{
|
||||||
|
public override void Bake(GymRosterAuthoring authoring)
|
||||||
|
{
|
||||||
|
var entity = GetEntity(TransformUsageFlags.None);
|
||||||
|
AddComponent<GymTag>(entity);
|
||||||
|
var buf = AddBuffer<GymEnemyRoster>(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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d210e424c43414a4e958efc44c025335
|
||||||
@@ -44,6 +44,9 @@ namespace ProjectM.Client
|
|||||||
public static void SetClass(byte classId) => Send(DebugOp.SetClass, classId);
|
public static void SetClass(byte classId) => Send(DebugOp.SetClass, classId);
|
||||||
public static void SetWarrior() => SetClass(ClassTraits.WarriorClass);
|
public static void SetWarrior() => SetClass(ClassTraits.WarriorClass);
|
||||||
public static void SetRanger() => SetClass(ClassTraits.RangerClass);
|
public static void SetRanger() => SetClass(ClassTraits.RangerClass);
|
||||||
|
/// <summary>GYM: spawn enemy KIND (a <see cref="ProjectM.Simulation.GymEnemyKind"/> byte) near the sender, count times.</summary>
|
||||||
|
public static void SpawnEnemy(byte kind, int count = 1) => Send(DebugOp.SpawnEnemy, kind, count);
|
||||||
|
|
||||||
|
|
||||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||||
static void ResetOnEnterPlayMode() => s_Pending.Clear();
|
static void ResetOnEnterPlayMode() => s_Pending.Clear();
|
||||||
|
|||||||
@@ -40,8 +40,14 @@ namespace ProjectM.Server
|
|||||||
// BEFORE the ECB exists — a per-request continue would already have marked the connection in-game.
|
// 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
|
// 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).
|
// in practice — the director spawns at subscene-stream, before any GoInGame round-trip).
|
||||||
if (!SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCatalog) || !metaCatalog.Value.IsCreated
|
// GYM (LANTERN A1): a fresh gym subscene has no CycleDirector, so the base meta-catalog guard would
|
||||||
|| !SystemAPI.TryGetSingletonBuffer<MetaTierState>(out var metaRecord, true))
|
// 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)
|
if (!_warnedMetaBlocked)
|
||||||
{
|
{
|
||||||
@@ -82,11 +88,22 @@ namespace ProjectM.Server
|
|||||||
// Expedition redesign: the server-only class anchor the meta systems key on (born-correct meta
|
// 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).
|
// seeding at Step 12a + per-class spend at Step 13 resolve the tier record through this).
|
||||||
ecb.AddComponent(player, new PlayerClass { ClassId = classId });
|
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
|
// 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
|
// 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
|
// 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
|
// 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).
|
// 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;
|
ref var metaPool = ref metaCatalog.Value.Value;
|
||||||
byte classBit = BoonMath.MaskFor(classId);
|
byte classBit = BoonMath.MaskFor(classId);
|
||||||
|
|||||||
@@ -208,6 +208,33 @@ namespace ProjectM.Server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
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);
|
ecb.DestroyEntity(reqEntity);
|
||||||
|
|||||||
@@ -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
|
/// 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.</summary>
|
/// player to the new class's max. Editor-only dev tool (class-switch); 0 / unknown -> Warrior.</summary>
|
||||||
public const byte SetClass = 13;
|
public const byte SetClass = 13;
|
||||||
|
|
||||||
|
/// <summary>GYM (editor): spawn <see cref="GymEnemyKind"/> ArgA near the sender, ArgB times (default 1). Reads the baked GymEnemyRoster.</summary>
|
||||||
|
public const byte SpawnEnemy = 14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Unity.Entities;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton tag baked into the Enemy-test GYM subscene (see the LANTERN roadmap A1 gym note). Marks the world
|
||||||
|
/// as the gym so <c>GoInGameServerSystem</c> 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.
|
||||||
|
/// </summary>
|
||||||
|
public struct GymTag : IComponentData { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baked gym enemy roster — one row per on-demand-spawnable enemy KIND (buffer index is arbitrary; match on
|
||||||
|
/// <see cref="Kind"/>). <c>DebugCommandReceiveSystem</c>'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 <see cref="AbilityPrefabElement"/> idiom). Server-read only.
|
||||||
|
/// </summary>
|
||||||
|
[InternalBufferCapacity(4)]
|
||||||
|
public struct GymEnemyRoster : IBufferElementData
|
||||||
|
{
|
||||||
|
/// <summary>Stable enemy-kind key (0 = Drowner, 1 = Grindylow; extend as the roster grows).</summary>
|
||||||
|
public byte Kind;
|
||||||
|
|
||||||
|
/// <summary>The baked enemy ghost prefab entity to instantiate for this kind.</summary>
|
||||||
|
public Entity Prefab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stable kind ids for <see cref="GymEnemyRoster.Kind"/> / the SpawnEnemy op's ArgA.</summary>
|
||||||
|
public static class GymEnemyKind
|
||||||
|
{
|
||||||
|
public const byte Drowner = 0;
|
||||||
|
public const byte Grindylow = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f39498494d08f66458393a87b395cd62
|
||||||
Reference in New Issue
Block a user