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:
@@ -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"/> -> <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 -> server request to move the sender's player between world regions (base <-> 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;
|
||||
|
||||
Reference in New Issue
Block a user