Hygiene B2: dead-code removal

Delete (unreferenced in any scene, per operator decision):
- TrainingDummy* chain (authoring/spawner/system/components/prefab) + UpgradePickup* chain (authoring/spawner/systems/components/prefab); remove TrainingDummyTag from the 3 live combat queries (AbilityFire/Melee/HealthApplyDamage now key on EnemyTag) and repoint HealthApplyDamageSystemTests to EnemyTag.
- Dead RPC RegionTransitSystem + RegionTransitRequest (no sender, ungated) + its test.
- Heartbeat + HeartbeatSystem (no WorldSystemFilter, ran no-op every tick) + its test.
- BuildSendSystem dead conveyor/pylon dev hooks (s_ConveyorDir, [/] rotation, Place{Pylon,Harvester,Conveyor} statics).
- Deprecated CyclePhase alias consts (Expedition/Defend/Build/*Ticks); repoint WaveSystemTests to Siege/Calm.
- Unread FeelConfig fields (HitFlashDurationMs, RumbleHeavy) + dangling HudUi doc-comments + stale crefs.

451/451 EditMode tests pass. CLAUDE.md HeartbeatSystemTests example ref to be repointed in B7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 23:16:53 -07:00
parent b1041003f0
commit 589e712db3
53 changed files with 28 additions and 1229 deletions
@@ -1,147 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6681740481886397972
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3040602402374479956}
- component: {fileID: 9111625462373738953}
- component: {fileID: 6113974662432286332}
- component: {fileID: 5080357267889081991}
- component: {fileID: 3549493811897787778}
- component: {fileID: 1198698002335808414}
m_Layer: 0
m_Name: TrainingDummy
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3040602402374479956
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 2, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &9111625462373738953
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &6113974662432286332
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: f8df9cd33fb974460a903e35a6fce3c9, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 1
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!114 &5080357267889081991
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c32aebeb7bfbb464898dfee8e6e87e6c, type: 3}
m_Name:
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.TrainingDummyAuthoring
MaxHealth: 60
HitRadius: 0.8
--- !u!114 &3549493811897787778
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
--- !u!114 &1198698002335808414
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
HasOwner: 0
SupportAutoCommandTarget: 0
TrackInterpolationDelay: 0
GhostGroup: 0
UsePreSerialization: 0
UseSingleBaseline: 0
RollbackPredictedSpawnedGhostState: 0
RollbackPredictionOnStructuralChanges: 1
DefaultGhostMode: 0
SupportedGhostModes: 3
OptimizationMode: 0
Importance: 1
MaxSendRate: 0
prefabId:
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 9ee6032b77c444d07a1dc9dfbf5abd68
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,181 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &803404312977859260
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 544753127400808733}
- component: {fileID: 8071270979468917903}
- component: {fileID: 4362678855125273864}
m_Layer: 0
m_Name: Model
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &544753127400808733
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 803404312977859260}
serializedVersion: 2
m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0.4, z: 0}
m_LocalScale: {x: 1.3, y: 1.3, z: 1.3}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3572766465862231365}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &8071270979468917903
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 803404312977859260}
m_Mesh: {fileID: 4300000, guid: a52e2d101bf32694d9012060774e97fd, type: 3}
--- !u!23 &4362678855125273864
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 803404312977859260}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!1 &3885353946372160549
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3572766465862231365}
- component: {fileID: 6410754845414010269}
- component: {fileID: 9053853372340598254}
- component: {fileID: 6834786618115927220}
m_Layer: 0
m_Name: UpgradePickup
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3572766465862231365
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 544753127400808733}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &6410754845414010269
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 07e6d40378fcb43c5be706ef96cb4bb2, type: 3}
m_Name:
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.UpgradePickupAuthoring
Target: 0
Op: 0
Value: 10
HitRadius: 1.2
--- !u!114 &9053853372340598254
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
--- !u!114 &6834786618115927220
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
HasOwner: 0
SupportAutoCommandTarget: 1
TrackInterpolationDelay: 0
GhostGroup: 0
UsePreSerialization: 0
UseSingleBaseline: 0
RollbackPredictedSpawnedGhostState: 0
RollbackPredictionOnStructuralChanges: 1
DefaultGhostMode: 0
SupportedGhostModes: 3
OptimizationMode: 0
Importance: 1
MaxSendRate: 0
prefabId:
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: d7dd0078c7b1f4cf995ca3a4b1155569
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,41 +0,0 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the training-dummy enemy prefab. Bakes a stationary, damageable auto-target
/// candidate: <see cref="TrainingDummyTag"/> marks it for the ability auto-target cone,
/// <see cref="Health"/> and <see cref="HitRadius"/> make it a valid projectile hit target, and a
/// <see cref="DamageEvent"/> buffer receives server-authoritative hits. Dummies are NOT ghosts and
/// carry no <c>GhostOwner</c>, so projectiles never treat them as the firing owner.
/// <c>GetEntity(TransformUsageFlags.Dynamic)</c> ensures a runtime LocalTransform for hit tests
/// and spawn placement.
/// </summary>
public class TrainingDummyAuthoring : MonoBehaviour
{
[Min(0f), Tooltip("Starting and maximum health for the dummy.")]
public float MaxHealth = 60f;
[Min(0f), Tooltip("World-unit radius used by the projectile hit test.")]
public float HitRadius = 0.8f;
private class TrainingDummyBaker : Baker<TrainingDummyAuthoring>
{
public override void Bake(TrainingDummyAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
AddComponent<TrainingDummyTag>(entity);
AddComponent(entity, new Health
{
Current = authoring.MaxHealth,
Max = authoring.MaxHealth
});
AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
AddBuffer<DamageEvent>(entity);
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c32aebeb7bfbb464898dfee8e6e87e6c
@@ -1,48 +0,0 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="TrainingDummySpawner"/> singleton. Place this on a single
/// GameObject in the gameplay subscene; at runtime the server-only
/// <c>TrainingDummySpawnSystem</c> reads the singleton, instantiates <see cref="Count"/> dummies
/// laid out along +X from <see cref="Origin"/> at <see cref="Spacing"/> intervals, then destroys
/// the singleton so it fires exactly once. The entity itself carries no transform
/// (<c>TransformUsageFlags.None</c>); only the referenced <see cref="DummyPrefab"/> needs a
/// runtime-mutable LocalTransform (<c>TransformUsageFlags.Dynamic</c>).
/// </summary>
public class TrainingDummySpawnerAuthoring : MonoBehaviour
{
[Tooltip("Training dummy prefab to instantiate. Must carry TrainingDummyAuthoring.")]
public GameObject DummyPrefab;
[Min(0)]
[Tooltip("How many dummies to spawn.")]
public int Count = 3;
[Min(0f)]
[Tooltip("World-unit spacing between consecutive dummies along +X.")]
public float Spacing = 3f;
[Tooltip("World-space position of the first dummy.")]
public Vector3 Origin = new Vector3(0, 0, 8);
private class TrainingDummySpawnerBaker : Baker<TrainingDummySpawnerAuthoring>
{
public override void Bake(TrainingDummySpawnerAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new TrainingDummySpawner
{
Prefab = GetEntity(authoring.DummyPrefab, TransformUsageFlags.Dynamic),
Count = authoring.Count,
Spacing = authoring.Spacing,
Origin = authoring.Origin
});
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 51cc543c146d84239ba6dc219221df18
@@ -1,43 +0,0 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for an upgrade pickup ghost prefab: a world object that grants one stat modifier to the
/// first player that overlaps it (server-authoritative, applied by <c>UpgradePickupSystem</c>) and
/// then despawns. Bake the prefab as an interpolated ghost (add a GhostAuthoringComponent) so clients
/// see it appear and despawn. <c>GetEntity(TransformUsageFlags.Dynamic)</c> gives it a world transform.
/// </summary>
public class UpgradePickupAuthoring : MonoBehaviour
{
[Tooltip("Which stat the granted modifier targets.")]
public StatTarget Target = StatTarget.Damage;
[Tooltip("How the granted modifier combines.")]
public ModOp Op = ModOp.Flat;
[Tooltip("Modifier magnitude: flat amount, or fractional percent (0.1 = +10%).")]
public float Value = 10f;
[Tooltip("Overlap radius (world units) for the player pickup test.")]
[Min(0f)] public float HitRadius = 1f;
private class UpgradePickupBaker : Baker<UpgradePickupAuthoring>
{
public override void Bake(UpgradePickupAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
AddComponent(entity, new UpgradePickup
{
Target = (byte)authoring.Target,
Op = (byte)authoring.Op,
Value = authoring.Value,
SourceId = 0u,
});
AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 07e6d40378fcb43c5be706ef96cb4bb2
@@ -1,48 +0,0 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="UpgradePickupSpawner"/> singleton (mirrors
/// TrainingDummySpawnerAuthoring). Place on a single GameObject in the gameplay subscene; the
/// server-only <c>UpgradePickupSpawnSystem</c> reads it, spawns <see cref="Count"/> pickups along +X
/// from <see cref="Origin"/> at <see cref="Spacing"/> intervals, then destroys the singleton so it
/// fires exactly once. The entity carries no transform; only the prefab needs a runtime transform.
/// </summary>
public class UpgradePickupSpawnerAuthoring : MonoBehaviour
{
[Tooltip("Upgrade pickup prefab to instantiate. Must carry UpgradePickupAuthoring.")]
public GameObject PickupPrefab;
[Min(0)]
[Tooltip("How many pickups to spawn.")]
public int Count = 2;
[Min(0f)]
[Tooltip("World-unit spacing between consecutive pickups along +X.")]
public float Spacing = 3f;
[Tooltip("World-space position of the first pickup.")]
public Vector3 Origin = new Vector3(-4f, 0f, 6f);
private class UpgradePickupSpawnerBaker : Baker<UpgradePickupSpawnerAuthoring>
{
public override void Bake(UpgradePickupSpawnerAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new UpgradePickupSpawner
{
Prefab = authoring.PickupPrefab != null
? GetEntity(authoring.PickupPrefab, TransformUsageFlags.Dynamic)
: Entity.Null,
Count = authoring.Count,
Spacing = authoring.Spacing,
Origin = authoring.Origin,
});
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: e367eb55e2c2248f18be10d6c3c9ad67
@@ -16,14 +16,13 @@ namespace ProjectM.Client
/// <see cref="BuildPaletteState.Active"/>), so the place-click never also fires. Build mode is suspended while
/// the pause overlay is open, and the frame a palette button changes the selection never also places.
/// (2) keyboard hotkeys (fallback, suppressed in palette mode): B/V/F place at the local player's cell.
/// Editor-only statics (PlaceStructure / PlaceHarvester / ...) drive the same RPC path from execute_code for
/// Editor-only statics (PlaceStructure / PlaceTurret / ...) drive the same RPC path from execute_code for
/// headless validation. Managed SystemBase; UnityEngine.InputSystem types are fully qualified to avoid the
/// ProjectM.Simulation.PlayerInput name collision. The server re-validates legality + cost authoritatively.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial class BuildSendSystem : SystemBase
{
static byte s_ConveyorDir; // key-flow pending conveyor facing (0=+X,1=-X,2=+Z,3=-Z)
// Dev hotkey fallback: key -> buildable (Conveyor uses s_ConveyorDir for facing).
static readonly (UnityEngine.InputSystem.Key Key, byte Type)[] s_BuildHotkeys =
@@ -62,18 +61,9 @@ namespace ProjectM.Client
/// <summary>EDITOR / execute_code hook: queue a wall placement at a specific cell.</summary>
public static void PlaceWall(int cellX, int cellZ) => PlaceStructure(StructureType.Wall, cellX, cellZ);
/// <summary>EDITOR / execute_code hook: queue a pylon placement at a specific cell.</summary>
public static void PlacePylon(int cellX, int cellZ) => PlaceStructure(StructureType.Pylon, cellX, cellZ);
/// <summary>EDITOR / execute_code hook: queue a harvester placement at a specific cell.</summary>
public static void PlaceHarvester(int cellX, int cellZ) => PlaceStructure(StructureType.Harvester, cellX, cellZ);
/// <summary>EDITOR / execute_code hook: queue a fabricator placement at a specific cell.</summary>
public static void PlaceFabricator(int cellX, int cellZ) => PlaceStructure(StructureType.Fabricator, cellX, cellZ);
/// <summary>EDITOR / execute_code hook: queue a conveyor placement facing direction (0=+X,1=-X,2=+Z,3=-Z).</summary>
public static void PlaceConveyor(int cellX, int cellZ, byte direction) => PlaceStructure(StructureType.Conveyor, cellX, cellZ, direction);
#endif
protected override void OnCreate()
@@ -106,14 +96,9 @@ namespace ProjectM.Client
// Hotkey fallback (suppressed while the palette build mode is active).
if (keyboard != null && !BuildPaletteState.Active)
{
if (keyboard.leftBracketKey.wasPressedThisFrame)
s_ConveyorDir = (byte)((s_ConveyorDir + 3) % 4);
if (keyboard.rightBracketKey.wasPressedThisFrame)
s_ConveyorDir = (byte)((s_ConveyorDir + 1) % 4);
foreach (var (key, type) in s_BuildHotkeys)
if (keyboard[key].wasPressedThisFrame && TryGetLocalPlayerCell(out int2 cell))
SendBuild(connection, type, cell.x, cell.y, type == StructureType.Conveyor ? s_ConveyorDir : (byte)0);
SendBuild(connection, type, cell.x, cell.y, (byte)0);
}
#if UNITY_EDITOR
@@ -45,8 +45,6 @@ namespace ProjectM.Client
public static float HitStopRefDamage;
/// <summary>Tint for the (deferred) enemy material hit-flash — exposed now, wired in the ShaderGraph slice.</summary>
public static Color HitFlashColor;
/// <summary>Duration (ms) of the deferred enemy hit-flash.</summary>
public static float HitFlashDurationMs;
/// <summary>Master gate for the (deferred) true freeze-frame hit-stop. FALSE for v1 (camera-punch only).</summary>
public static bool HitStopFreezeEnabled;
@@ -130,8 +128,6 @@ namespace ProjectM.Client
public static float RumbleHit;
/// <summary>Rumble strength on a kill.</summary>
public static float RumbleKill;
/// <summary>Rumble strength on dash / local death (the heaviest).</summary>
public static float RumbleHeavy;
/// <summary>Seconds a rumble pulse lasts before it auto-stops.</summary>
public static float RumbleDurationSec;
@@ -190,7 +186,6 @@ namespace ProjectM.Client
HitStopMaxFrames = 2; // C4: a 2-frame (~33ms) camera hold reads as crunch, not a lag-y stutter
HitStopRefDamage = 30f;
HitFlashColor = new Color(1f, 0.85f, 0.55f, 1f);
HitFlashDurationMs = 80f;
HitStopFreezeEnabled = true; // C4: enable the finisher hit-stop hold (presentation-only camera freeze, bounded below)
// Feature 1/2 death
@@ -239,7 +234,6 @@ namespace ProjectM.Client
RumbleEnabled = true;
RumbleHit = 0.25f;
RumbleKill = 0.45f;
RumbleHeavy = 0.6f;
RumbleDurationSec = 0.12f;
// Deferred-items pass (2026-06)
@@ -146,8 +146,6 @@ namespace ProjectM.Client
return e;
}
/// <summary>True when the theme is loaded and a sprite is present (callers choose icon vs text fallback).</summary>
/// <summary>
/// An input-prompt chip: the Synty key/button glyph (left untinted to keep its printed face) when present,
@@ -170,7 +168,5 @@ namespace ProjectM.Client
return cap;
}
/// <summary>Apply a uniform 9-slice (horizontal / vertical source-texture px) to a skinned element.</summary>
}
}
@@ -13,7 +13,7 @@ namespace ProjectM.Server
/// this tick), subtracts the summed amount from <see cref="Health"/>, then clears the buffer so
/// each hit is applied exactly once. Entities that carry character stats (players) clamp to their
/// data-driven <see cref="EffectiveCharacterStats.MaxHealth"/> ceiling; others (training dummies)
/// clamp at zero. A dead <see cref="TrainingDummyTag"/> is destroyed; player death is deferred.
/// clamp at zero. A dead <see cref="EnemyTag"/> is destroyed; player death is deferred.
/// Health.Current is a <c>[GhostField]</c>, so the new value replicates to clients for display.
///
/// Runs server-only (<see cref="WorldSystemFilterFlags.ServerSimulation"/>) inside the prediction
@@ -156,7 +156,7 @@ namespace ProjectM.Server
if (SystemAPI.HasComponent<IsLunging>(entity)) SystemAPI.SetComponentEnabled<IsLunging>(entity, false);
}
}
else if (SystemAPI.HasComponent<TrainingDummyTag>(entity) || SystemAPI.HasComponent<EnemyTag>(entity) || SystemAPI.HasComponent<Destructible>(entity))
else if (SystemAPI.HasComponent<EnemyTag>(entity) || SystemAPI.HasComponent<Destructible>(entity))
ecb.DestroyEntity(entity);
}
}
@@ -1,52 +0,0 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-only, one-shot dummy spawner. On its first update it reads the baked
/// <see cref="TrainingDummySpawner"/> singleton and instantiates <c>Count</c> training-dummy
/// ghosts in a row, spaced <c>Spacing</c> world-units apart along +X starting at <c>Origin</c>.
/// It then destroys the spawner singleton entity so <c>RequireForUpdate<TrainingDummySpawner></c>
/// is no longer satisfied and the system stops running (idempotent: dummies are spawned exactly once).
/// Runs in the default <see cref="SimulationSystemGroup"/> (NOT the prediction loop) since spawning is
/// a non-predicted, server-authoritative event; the dummies replicate to clients as interpolated ghosts.
/// All structural changes are batched through an <see cref="EntityCommandBuffer"/>.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct TrainingDummySpawnSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<TrainingDummySpawner>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Grab both the singleton entity (to destroy when done) and its baked config.
var spawnerEntity = SystemAPI.GetSingletonEntity<TrainingDummySpawner>();
var spawner = SystemAPI.GetComponent<TrainingDummySpawner>(spawnerEntity);
var ecb = new EntityCommandBuffer(Allocator.Temp);
for (int i = 0; i < spawner.Count; i++)
{
var dummy = ecb.Instantiate(spawner.Prefab);
var position = spawner.Origin + new float3(i * spawner.Spacing, 0f, 0f);
ecb.SetComponent(dummy, LocalTransform.FromPosition(position));
}
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 7673acbc74f7a4bddbca0679122511ea
@@ -1,56 +0,0 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-only, one-shot upgrade-pickup spawner (mirrors TrainingDummySpawnSystem). On its first
/// update it reads the baked <see cref="UpgradePickupSpawner"/> singleton and instantiates
/// <c>Count</c> pickup ghosts in a row, spaced <c>Spacing</c> world-units apart along +X starting at
/// <c>Origin</c>, then destroys the singleton so the system idles (spawned exactly once). Runs in the
/// default <see cref="SimulationSystemGroup"/> (NOT the prediction loop); pickups replicate to clients
/// as interpolated ghosts. Structural changes are batched through an <see cref="EntityCommandBuffer"/>.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct UpgradePickupSpawnSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<UpgradePickupSpawner>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var spawnerEntity = SystemAPI.GetSingletonEntity<UpgradePickupSpawner>();
var spawner = SystemAPI.GetComponent<UpgradePickupSpawner>(spawnerEntity);
var ecb = new EntityCommandBuffer(Allocator.Temp);
if (spawner.Prefab != Entity.Null)
{
// Preserve the prefab's baked scale/rotation (FromPosition would reset Scale to 1).
var baseXform = SystemAPI.GetComponent<LocalTransform>(spawner.Prefab);
for (int i = 0; i < spawner.Count; i++)
{
var pickup = ecb.Instantiate(spawner.Prefab);
var position = spawner.Origin + new float3(i * spawner.Spacing, 0f, 0f);
var xform = baseXform;
xform.Position = position;
ecb.SetComponent(pickup, xform);
}
}
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 602544c80ca5649258a9d6ca9ad74f79
@@ -1,78 +0,0 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-authoritative upgrade pickup grant. When a player overlaps an <see cref="UpgradePickup"/>
/// (planar XZ distance within the pickup's <see cref="HitRadius"/>), appends the pickup's modifier to
/// the player's replicated <see cref="StatModifier"/> buffer (which replicates to the predicting
/// owner, so StatRecomputeSystem folds identical effective stats on both worlds) and destroys the
/// pickup. Runs in the default <see cref="SimulationSystemGroup"/> (NOT the prediction loop) since the
/// grant is a non-predicted server event. The buffer append + pickup destroy are batched through an
/// <see cref="EntityCommandBuffer"/> played back immediately — so a plain-world EditMode test needs no
/// separate ECB system.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct UpgradePickupSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<UpgradePickup>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Snapshot modifiable players (carrying the modifier buffer + a transform) once this tick.
var playerEntities = new NativeList<Entity>(Allocator.Temp);
var playerPositions = new NativeList<float3>(Allocator.Temp);
foreach (var (xform, e) in
SystemAPI.Query<RefRO<LocalTransform>>()
.WithAll<PlayerTag, StatModifier>()
.WithEntityAccess())
{
playerEntities.Add(e);
playerPositions.Add(xform.ValueRO.Position);
}
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (xform, radius, pickup, pickupEntity) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>, RefRO<UpgradePickup>>()
.WithEntityAccess())
{
float2 pp = new float2(xform.ValueRO.Position.x, xform.ValueRO.Position.z);
float r = radius.ValueRO.Value;
for (int i = 0; i < playerEntities.Length; i++)
{
float2 cp = new float2(playerPositions[i].x, playerPositions[i].z);
if (math.distancesq(pp, cp) > r * r)
continue;
ecb.AppendToBuffer(playerEntities[i], new StatModifier
{
Target = pickup.ValueRO.Target,
Op = pickup.ValueRO.Op,
Value = pickup.ValueRO.Value,
SourceId = pickup.ValueRO.SourceId,
});
ecb.DestroyEntity(pickupEntity);
break; // granted to the first overlapping player, then despawns
}
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
playerEntities.Dispose();
playerPositions.Dispose();
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c3d9ef25fbc464e52aa342b531d6f35e
@@ -1,71 +0,0 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-authoritative handler for <see cref="RegionTransitRequest"/> RPCs. Resolves the sender's player
/// (via the source connection's <see cref="NetworkId"/> -&gt; <see cref="GhostOwner"/>), flips its
/// <see cref="RegionTag"/> to the requested region, and teleports it to that region's origin
/// (<see cref="RegionMath.RegionOrigin"/>, centered on the base via <see cref="BaseGridMath.PlotCenter"/>).
/// Runs in the default server SimulationSystemGroup (NOT the prediction loop) so the transit applies once;
/// the next snapshot reconciles the owner-predicted client and <see cref="RegionRelevancySystem"/> re-scopes
/// which region's ghosts the connection receives. Mirrors the <see cref="StorageOpReceiveSystem"/> RPC shape.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct RegionTransitSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<BaseAnchor>();
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<RegionTransitRequest, ReceiveRpcCommandRequest>();
state.RequireForUpdate(state.GetEntityQuery(builder));
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var baseCenter = BaseGridMath.PlotCenter(SystemAPI.GetSingleton<BaseAnchor>());
// Map connection NetworkId -> player entity.
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
foreach (var (owner, entity) in
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, RegionTag>().WithEntityAccess())
{
playerByConn[owner.ValueRO.NetworkId] = entity;
}
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (request, receive, requestEntity) in
SystemAPI.Query<RefRO<RegionTransitRequest>, RefRO<ReceiveRpcCommandRequest>>().WithEntityAccess())
{
var connEntity = receive.ValueRO.SourceConnection;
if (SystemAPI.HasComponent<NetworkId>(connEntity))
{
int connId = SystemAPI.GetComponent<NetworkId>(connEntity).Value;
if (playerByConn.TryGetValue(connId, out var player))
{
byte target = request.ValueRO.TargetRegion;
SystemAPI.GetComponentRW<RegionTag>(player).ValueRW.Region = target;
SystemAPI.GetComponentRW<LocalTransform>(player).ValueRW.Position =
RegionMath.RegionOrigin(target, baseCenter);
}
}
ecb.DestroyEntity(requestEntity);
}
ecb.Playback(state.EntityManager);
playerByConn.Dispose();
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 44d6ce89f189d984c83cd213d86d4b02
@@ -80,7 +80,7 @@ namespace ProjectM.Simulation
if (isServer)
{
foreach (var (tx, th, te) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>>().WithAny<TrainingDummyTag, EnemyTag>().WithEntityAccess())
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>>().WithAll<EnemyTag>().WithEntityAccess())
{
if (th.ValueRO.Current <= 0f) continue; // B3: corpses are neither aim magnets nor cleave targets
candidatePositions.Add(tx.ValueRO.Position);
@@ -4,7 +4,7 @@ namespace ProjectM.Simulation
{
/// <summary>
/// EB-1 — opt-in marker for an entity that <see cref="ProjectM.Server.HealthApplyDamageSystem"/> DESTROYS when
/// its <see cref="Health"/> hits 0 (alongside <c>TrainingDummyTag</c>/<c>EnemyTag</c>). Baked ONLY on the
/// its <see cref="Health"/> hits 0 (alongside <c>EnemyTag</c>). Baked ONLY on the
/// player-built structure ghosts (Turret/Wall/Pylon) so "machines can die". DELIBERATELY a distinct tag rather
/// than gating on bare <see cref="PlacedStructure"/>: that identity is SHARED by the reserved M7 automation
/// machines (Harvester/Fabricator/Conveyor) whose teardown would silently drop in-flight conveyor cargo — the
@@ -7,7 +7,7 @@ namespace ProjectM.Simulation
/// Hit points for any damageable ghost (players, training dummies). Server-authoritative:
/// only server systems write <see cref="Current"/>; clients receive it via the [GhostField]
/// for display and prediction reconciliation. <see cref="Max"/> is baked identically on both
/// worlds and is not replicated. Added by PlayerBaker / TrainingDummyBaker.
/// worlds and is not replicated. Added by PlayerBaker.
/// </summary>
public struct Health : IComponentData
{
@@ -1,26 +0,0 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Baked singleton describing the training-dummy field to spawn at world start. Consumed once by
/// the server-only TrainingDummySpawnSystem, which instantiates <see cref="Count"/> dummies from
/// <see cref="Prefab"/> and then destroys this singleton so the spawn runs exactly once. Not
/// replicated — dummies are spawned authoritatively on the server and reach clients as ghosts.
/// </summary>
public struct TrainingDummySpawner : IComponentData
{
/// <summary>Baked entity prefab to instantiate for each dummy.</summary>
public Entity Prefab;
/// <summary>Number of dummies to spawn.</summary>
public int Count;
/// <summary>World-unit gap between consecutive dummies along the spawn line (X axis).</summary>
public float Spacing;
/// <summary>World-space position of the first dummy; subsequent dummies offset by <see cref="Spacing"/>.</summary>
public float3 Origin;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a8f27601fb20640718aaed2c48d1d016
@@ -1,11 +0,0 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Zero-size marker identifying a training-dummy enemy. Server auto-target collects
/// <see cref="TrainingDummyTag"/> entities as candidates, and the damage/death path destroys
/// a dummy when its <c>Health.Current</c> reaches zero. Added by TrainingDummyBaker.
/// </summary>
public struct TrainingDummyTag : IComponentData { }
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: ebe9114e40db748669d89ef861415bac
@@ -1,17 +0,0 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// A world pickup that, on player overlap, grants one StatModifier (server-authoritative) and is then
/// destroyed. Mirrors a single StatModifier payload (Target/Op as bytes). The pickup is an
/// interpolated ghost so clients see and despawn it; the grant is applied by UpgradePickupSystem.
/// </summary>
public struct UpgradePickup : IComponentData
{
public byte Target; // StatTarget
public byte Op; // ModOp
public float Value;
public uint SourceId;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a0d34f4d0b12241c7b0f0b1d002142f3
@@ -1,17 +0,0 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Singleton describing how many upgrade pickups to spawn and where (mirrors TrainingDummySpawner).
/// Consumed once by the server's UpgradePickupSpawnSystem.
/// </summary>
public struct UpgradePickupSpawner : IComponentData
{
public Entity Prefab;
public float3 Origin;
public int Count;
public float Spacing;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 059368b62be834614ae3a110a51d42da
@@ -1,13 +0,0 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Trivial unmanaged component used by the setup smoke test to prove the ECS
/// compile + source-gen + tick path works. Safe to delete once real gameplay exists.
/// </summary>
public struct Heartbeat : IComponentData
{
public int Tick;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a0347bd6d99db438f85ec1985b7f81e1
@@ -1,23 +0,0 @@
using Unity.Burst;
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Smoke-test system: increments every <see cref="Heartbeat"/> once per tick.
/// Default (no <c>[WorldSystemFilter]</c>) so it runs in the SimulationSystemGroup of
/// every world. Burst-compiled and unmanaged (<see cref="ISystem"/>) per DOTS convention.
/// </summary>
[BurstCompile]
public partial struct HeartbeatSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
foreach (var heartbeat in SystemAPI.Query<RefRW<Heartbeat>>())
{
heartbeat.ValueRW.Tick++;
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 78349f49733934f348acb488689fc9ec
@@ -174,7 +174,7 @@ namespace ProjectM.Simulation
var enemyPositions = new NativeList<float3>(Allocator.Temp);
foreach (var (xform, health, enemyEntity) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>>()
.WithAny<EnemyTag, TrainingDummyTag>()
.WithAll<EnemyTag>()
.WithEntityAccess())
{
if (health.ValueRO.Current <= 0f)
@@ -12,7 +12,6 @@ namespace ProjectM.Simulation
/// <para>
/// <b>Values that already live in a clear, public, semantically-named home (NOT duplicated here):</b>
/// <list type="bullet">
/// <item><see cref="CyclePhase.ExpeditionTicks"/> / <see cref="CyclePhase.BuildTicks"/> — cycle phase durations.</item>
/// <item><see cref="RegionMath.ExpeditionOffsetX"/> — base→expedition world-space offset.</item>
/// <item>Per-ability/character stats — authored in ScriptableObjects, baked to the AbilityDatabase blob (M3).</item>
/// </list>
@@ -39,22 +39,6 @@ namespace ProjectM.Simulation
/// <summary>The base is under assault by a Husk wave (event-triggered; ends when the wave is cleared).</summary>
public const byte Siege = 1;
// ---- Deprecated aliases (kept so HUD/audio/tests keep compiling through the cut-over; cleaned up later). ----
/// <summary>DEPRECATED alias of <see cref="Calm"/>.</summary>
public const byte Expedition = 0;
/// <summary>DEPRECATED alias of <see cref="Siege"/>.</summary>
public const byte Defend = 1;
/// <summary>DEPRECATED, unreachable — the timed Build phase is retired.</summary>
public const byte Build = 2;
/// <summary>DEPRECATED — the forced Expedition timer is retired (the loop is player-driven). Kept so existing crefs resolve.</summary>
public const uint ExpeditionTicks = 3600;
/// <summary>DEPRECATED — the forced Build timer is retired. Kept so existing crefs resolve.</summary>
public const uint BuildTicks = 1200;
}
/// <summary>
@@ -1,17 +0,0 @@
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Client -&gt; server request to move the sender's player between world regions (base &lt;-&gt; expedition).
/// A one-off action, so an RPC (not a per-tick predicted input), mirroring <see cref="StorageOpRequest"/>.
/// TargetRegion is a byte (see <see cref="RegionId"/>) to keep the generated serializer trivial. The
/// server teleports the sender's player to the region origin and flips its <see cref="RegionTag"/>, which
/// re-scopes GhostRelevancy so the client gains the target region's ghosts and drops the old region's.
/// </summary>
public struct RegionTransitRequest : IRpcCommand
{
/// <summary>Destination region id (see <see cref="RegionId"/>).</summary>
public byte TargetRegion;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 32c76b46284ed1845a412ca030de2499
@@ -14,7 +14,7 @@ namespace ProjectM.Tests
/// filter is ignored, so it still runs in this netcode-free world. The system plays its
/// EntityCommandBuffer back to state.EntityManager immediately (Temp allocator) per the build
/// contract, so no separate ECB system is required for destruction to take effect within a single
/// group update. Mirrors PlayerMoveSystemTests and HeartbeatSystemTests.
/// group update. Uses the plain-Entities world+tick pattern shared across the suite.
/// </summary>
public class HealthApplyDamageSystemTests
{
@@ -40,9 +40,9 @@ namespace ProjectM.Tests
using (world)
{
var em = world.EntityManager;
// TrainingDummyTag included so this entity is a valid death candidate too, but with
// EnemyTag included so this entity is a valid death candidate too, but with
// 50 HP and 35 total damage it survives, so it must NOT be destroyed here.
var entity = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(TrainingDummyTag));
var entity = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(EnemyTag));
em.SetComponentData(entity, new Health { Current = 50f, Max = 50f });
var dmg = em.GetBuffer<DamageEvent>(entity);
@@ -61,13 +61,13 @@ namespace ProjectM.Tests
}
[Test]
public void Lethal_Damage_Clamps_Health_To_Zero_And_Destroys_TrainingDummy()
public void Lethal_Damage_Clamps_Health_To_Zero_And_Destroys_Enemy()
{
var (world, group) = MakeWorld("HealthApplyDamageLethalDummyWorld");
var (world, group) = MakeWorld("HealthApplyDamageLethalEnemyWorld");
using (world)
{
var em = world.EntityManager;
var entity = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(TrainingDummyTag));
var entity = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(EnemyTag));
em.SetComponentData(entity, new Health { Current = 50f, Max = 50f });
var dmg = em.GetBuffer<DamageEvent>(entity);
@@ -75,22 +75,22 @@ namespace ProjectM.Tests
group.Update();
// The system clamps Current to a floor of 0 then destroys the dummy via an
// The system clamps Current to a floor of 0 then destroys the enemy via an
// immediately-played ECB, so the entity is gone within this single group update.
Assert.IsFalse(em.Exists(entity),
"A lethally-hit TrainingDummyTag entity must be destroyed by the system's ECB.");
"A lethally-hit EnemyTag entity must be destroyed by the system's ECB.");
using var remaining = em.CreateEntityQuery(typeof(TrainingDummyTag));
using var remaining = em.CreateEntityQuery(typeof(EnemyTag));
Assert.AreEqual(0, remaining.CalculateEntityCount(),
"No TrainingDummyTag entities should remain after a lethal hit.");
"No EnemyTag entities should remain after a lethal hit.");
}
}
[Test]
public void Lethal_Damage_On_NonDummy_Clamps_To_Zero_Without_Destroying()
public void Lethal_Damage_On_Untagged_Entity_Clamps_To_Zero_Without_Destroying()
{
// Player death is deferred in M2; only TrainingDummyTag entities are destroyed.
// A non-dummy (no TrainingDummyTag) at zero HP must be clamped to 0 and kept alive.
// Only death candidates (EnemyTag / Destructible) are destroyed. An untagged entity
// (e.g. a player, whose death is deferred) at zero HP must be clamped to 0 and kept alive.
var (world, group) = MakeWorld("HealthApplyDamageLethalPlayerWorld");
using (world)
{
@@ -104,7 +104,7 @@ namespace ProjectM.Tests
group.Update();
Assert.IsTrue(em.Exists(entity),
"A non-dummy (player) entity must survive lethal damage in M2; death is deferred.");
"An untagged (player) entity must survive lethal damage; death is deferred.");
Assert.AreEqual(0f, em.GetComponentData<Health>(entity).Current, 1e-4f,
"Health.Current must be clamped to 0 (never negative).");
Assert.AreEqual(0, em.GetBuffer<DamageEvent>(entity).Length,
@@ -120,7 +120,7 @@ namespace ProjectM.Tests
using (world)
{
var em = world.EntityManager;
var entity = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(TrainingDummyTag));
var entity = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(EnemyTag));
em.SetComponentData(entity, new Health { Current = 42f, Max = 60f });
group.Update();
@@ -193,6 +193,5 @@ namespace ProjectM.Tests
Assert.AreEqual(65f, em.GetComponentData<Health>(e).Current, 1e-4f, "No stats ceiling -> 100-35 = 65.");
}
}
}
}
@@ -1,40 +0,0 @@
using NUnit.Framework;
using Unity.Entities;
using ProjectM.Simulation;
namespace ProjectM.Tests
{
/// <summary>
/// Setup smoke test: proves the DOTS toolchain compiles, source-generates, and ticks.
/// Boots a bare ECS world, registers <see cref="HeartbeatSystem"/> in the
/// SimulationSystemGroup, and asserts the component advances once per group update.
/// (Netcode client/server world boot is covered separately by the §3 Play Mode check.)
/// </summary>
public class HeartbeatSystemTests
{
[Test]
public void Heartbeat_Advances_Once_Per_SimulationGroup_Tick()
{
using var world = new World("HeartbeatTestWorld");
var simulationGroup = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
var heartbeatSystem = world.GetOrCreateSystem<HeartbeatSystem>();
simulationGroup.AddSystemToUpdateList(heartbeatSystem);
simulationGroup.SortSystems();
var entityManager = world.EntityManager;
var entity = entityManager.CreateEntity(typeof(Heartbeat));
Assert.AreEqual(0, entityManager.GetComponentData<Heartbeat>(entity).Tick,
"Heartbeat should start at 0.");
const int ticks = 8;
for (int i = 0; i < ticks; i++)
{
simulationGroup.Update();
}
Assert.AreEqual(ticks, entityManager.GetComponentData<Heartbeat>(entity).Tick,
"Heartbeat.Tick should advance exactly once per SimulationSystemGroup update.");
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 74e3326af426947cfaff903aa94ed9f0
@@ -9,7 +9,7 @@ namespace ProjectM.Tests
{
/// <summary>
/// Plain-Entities EditMode tests for the server-only <see cref="InventoryDepositSystem"/> — the RPC that
/// moves a player's PERSONAL inventory into the shared ledger. Mirrors RegionTransitSystemTests' seeding:
/// moves a player's PERSONAL inventory into the shared ledger. Mirrors the RPC-receive tests' seeding:
/// a ResourceLedger singleton, a mock connection (NetworkId), a player (GhostOwner + InventorySlot +
/// PlayerTag), and an InventoryDepositRequest + ReceiveRpcCommandRequest. Pins: a specific-item deposit
/// moves the clamped amount; ItemId 0 deposits everything and empties the bag; an unresolvable connection
@@ -1,106 +0,0 @@
using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Core;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Tests
{
/// <summary>
/// Plain-Entities EditMode tests for the server-only <see cref="RegionTransitSystem"/> — the RPC region-transit
/// handler. A bare world is seeded with a BaseAnchor, a mock connection entity (NetworkId), a player
/// (GhostOwner + RegionTag + LocalTransform + PlayerTag) and a RegionTransitRequest + ReceiveRpcCommandRequest
/// whose SourceConnection points at the connection. Pins: a request from a resolvable connection flips the
/// player's region + teleports it to the region origin; an unresolvable connection transits nobody; the request
/// is consumed either way.
/// </summary>
public class RegionTransitSystemTests
{
static (World world, SimulationSystemGroup group) MakeWorld(string name)
{
var world = new World(name);
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<RegionTransitSystem>());
group.SortSystems();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
var em = world.EntityManager;
var anchor = em.CreateEntity(typeof(BaseAnchor));
em.SetComponentData(anchor, new BaseAnchor
{
AnchorPos = new float3(5, 0, 5),
GridOrigin = new float3(0, 0, 0),
CellSize = 2f,
GridDims = new int2(5, 5),
});
return (world, group);
}
static Entity MakeConnection(EntityManager em, int networkId)
{
var e = em.CreateEntity();
em.AddComponentData(e, new NetworkId { Value = networkId });
return e;
}
static Entity MakePlayer(EntityManager em, int networkId, byte region, float3 pos)
{
var e = em.CreateEntity();
em.AddComponentData(e, new GhostOwner { NetworkId = networkId });
em.AddComponentData(e, new RegionTag { Region = region });
em.AddComponentData(e, LocalTransform.FromPosition(pos));
em.AddComponent<PlayerTag>(e);
return e;
}
static void MakeTransitRequest(EntityManager em, byte targetRegion, Entity sourceConnection)
{
var e = em.CreateEntity();
em.AddComponentData(e, new RegionTransitRequest { TargetRegion = targetRegion });
em.AddComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = sourceConnection });
}
[Test]
public void Request_From_Known_Connection_Transits_Its_Player()
{
var (world, group) = MakeWorld("RegionTransitOk");
using (world)
{
var em = world.EntityManager;
var conn = MakeConnection(em, networkId: 1);
var player = MakePlayer(em, networkId: 1, region: RegionId.Base, pos: new float3(5, 1, 5));
MakeTransitRequest(em, RegionId.Expedition, conn);
group.Update();
Assert.AreEqual(RegionId.Expedition, em.GetComponentData<RegionTag>(player).Region,
"The sender's player flips to the requested region.");
Assert.AreEqual(1005f, em.GetComponentData<LocalTransform>(player).Position.x, 1e-2f,
"The player teleports to the expedition region origin (base center + 1000 on X).");
using var reqQ = em.CreateEntityQuery(typeof(RegionTransitRequest));
Assert.AreEqual(0, reqQ.CalculateEntityCount(), "The handled request is destroyed.");
}
}
[Test]
public void Request_From_Unresolvable_Connection_Transits_Nobody()
{
var (world, group) = MakeWorld("RegionTransitUnknown");
using (world)
{
var em = world.EntityManager;
var player = MakePlayer(em, networkId: 1, region: RegionId.Base, pos: new float3(5, 1, 5));
MakeTransitRequest(em, RegionId.Expedition, Entity.Null); // no NetworkId on Entity.Null
group.Update();
Assert.AreEqual(RegionId.Base, em.GetComponentData<RegionTag>(player).Region,
"A request whose connection can't be resolved transits nobody.");
using var reqQ = em.CreateEntityQuery(typeof(RegionTransitRequest));
Assert.AreEqual(0, reqQ.CalculateEntityCount(), "The request is still consumed.");
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 727cdc6da6f5b8842ba5b311702e5224
@@ -1,73 +0,0 @@
using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Tests
{
/// <summary>
/// Plain-Entities test for <see cref="UpgradePickupSystem"/> (server grant). A player overlapping a
/// pickup gets the pickup's modifier appended to its StatModifier buffer and the pickup is destroyed;
/// a player out of range leaves the buffer empty and the pickup alive. The system's ECB plays back
/// immediately (server pattern), so no separate ECB system is needed.
/// </summary>
public class UpgradePickupSystemTests
{
static (World world, Entity player, Entity pickup) MakeWorld(float3 playerPos, float3 pickupPos, float radius)
{
var world = new World("UpgradePickupTestWorld");
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<UpgradePickupSystem>());
group.SortSystems();
var em = world.EntityManager;
var player = em.CreateEntity(typeof(PlayerTag), typeof(StatModifier), typeof(LocalTransform));
em.SetComponentData(player, LocalTransform.FromPosition(playerPos));
var pickup = em.CreateEntity(typeof(UpgradePickup), typeof(HitRadius), typeof(LocalTransform));
em.SetComponentData(pickup, LocalTransform.FromPosition(pickupPos));
em.SetComponentData(pickup, new HitRadius { Value = radius });
em.SetComponentData(pickup, new UpgradePickup
{
Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 25f, SourceId = 7u
});
return (world, player, pickup);
}
[Test]
public void Overlap_Grants_Modifier_And_Destroys_Pickup()
{
var (world, player, pickup) = MakeWorld(float3.zero, new float3(0.5f, 0f, 0f), 1f);
try
{
world.GetExistingSystemManaged<SimulationSystemGroup>().Update();
var buf = world.EntityManager.GetBuffer<StatModifier>(player);
Assert.AreEqual(1, buf.Length);
Assert.AreEqual((byte)StatTarget.Damage, buf[0].Target);
Assert.AreEqual((byte)ModOp.Flat, buf[0].Op);
Assert.AreEqual(25f, buf[0].Value, 1e-4f);
Assert.AreEqual(7u, buf[0].SourceId);
Assert.IsFalse(world.EntityManager.Exists(pickup), "Pickup should be destroyed after grant.");
}
finally { world.Dispose(); }
}
[Test]
public void Out_Of_Range_Leaves_Buffer_Empty_And_Pickup_Alive()
{
var (world, player, pickup) = MakeWorld(float3.zero, new float3(10f, 0f, 0f), 1f);
try
{
world.GetExistingSystemManaged<SimulationSystemGroup>().Update();
var buf = world.EntityManager.GetBuffer<StatModifier>(player);
Assert.AreEqual(0, buf.Length);
Assert.IsTrue(world.EntityManager.Exists(pickup), "Out-of-range pickup should remain.");
}
finally { world.Dispose(); }
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 34f9fd7580c614586b6a5c68a0e6393c
@@ -68,7 +68,7 @@ namespace ProjectM.Tests
[Test]
public void Due_Lull_Starts_Wave_With_Escalating_Count()
{
var (world, group) = MakeWorld("WaveLullStart", serverTick: 100, cyclePhase: CyclePhase.Defend);
var (world, group) = MakeWorld("WaveLullStart", serverTick: 100, cyclePhase: CyclePhase.Siege);
using (world)
{
var em = world.EntityManager;
@@ -88,7 +88,7 @@ namespace ProjectM.Tests
[Test]
public void Spawning_Emits_One_Husk_And_Decrements_Remaining()
{
var (world, group) = MakeWorld("WaveSpawnOne", serverTick: 100, cyclePhase: CyclePhase.Defend);
var (world, group) = MakeWorld("WaveSpawnOne", serverTick: 100, cyclePhase: CyclePhase.Siege);
using (world)
{
var em = world.EntityManager;
@@ -107,7 +107,7 @@ namespace ProjectM.Tests
[Test]
public void Director_Is_Gated_Off_Outside_Defend()
{
var (world, group) = MakeWorld("WaveGated", serverTick: 100, cyclePhase: CyclePhase.Expedition);
var (world, group) = MakeWorld("WaveGated", serverTick: 100, cyclePhase: CyclePhase.Calm);
using (world)
{
var em = world.EntityManager;
@@ -125,7 +125,7 @@ namespace ProjectM.Tests
[Test]
public void Fully_Spawned_Cleared_Wave_Returns_To_Lull()
{
var (world, group) = MakeWorld("WaveCleared", serverTick: 100, cyclePhase: CyclePhase.Defend);
var (world, group) = MakeWorld("WaveCleared", serverTick: 100, cyclePhase: CyclePhase.Siege);
using (world)
{
var em = world.EntityManager;