LANTERN purge B2: delete EB-2 turret defense
TurretFireSystem, TurretAuthoring, Turret.prefab, Turret component, turret cap, B hotkey, HUD Charge chip + out-of-ammo cue, catalog entry, Tuning consts, tests (-7, 452 green). StructureType.Turret byte + ResourceId.Charge stay reserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,204 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!1 &2057690259992313649
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 4449724123933675757}
|
|
||||||
- component: {fileID: 3818890329194429404}
|
|
||||||
- component: {fileID: 6553709043537316589}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: Model
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!4 &4449724123933675757
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 2057690259992313649}
|
|
||||||
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: []
|
|
||||||
m_Father: {fileID: 3572766465862231365}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!33 &3818890329194429404
|
|
||||||
MeshFilter:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 2057690259992313649}
|
|
||||||
m_Mesh: {fileID: -8131118695690612189, guid: 472a8342ece31c14f9d53ff7119b7857, type: 3}
|
|
||||||
--- !u!23 &6553709043537316589
|
|
||||||
MeshRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 2057690259992313649}
|
|
||||||
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: 4cee7a5983b187943adc589b9cb1f084, type: 2}
|
|
||||||
- {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, 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: 9053853372340598254}
|
|
||||||
- component: {fileID: 6834786618115927220}
|
|
||||||
- component: {fileID: 1794795016809289889}
|
|
||||||
- component: {fileID: 9049467567705961987}
|
|
||||||
m_Layer: 9
|
|
||||||
m_Name: Turret
|
|
||||||
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: 4449724123933675757}
|
|
||||||
m_Father: {fileID: 0}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !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:
|
|
||||||
--- !u!114 &1794795016809289889
|
|
||||||
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: dfb1e0820cbf41b4bbbe99066dfadd40, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.TurretAuthoring
|
|
||||||
Range: 10
|
|
||||||
CooldownTicks: 30
|
|
||||||
Damage: 12
|
|
||||||
MaxHp: 120
|
|
||||||
--- !u!65 &9049467567705961987
|
|
||||||
BoxCollider:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3885353946372160549}
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_IncludeLayers:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
m_ExcludeLayers:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
m_LayerOverridePriority: 0
|
|
||||||
m_IsTrigger: 0
|
|
||||||
m_ProvidesContacts: 0
|
|
||||||
m_Enabled: 1
|
|
||||||
serializedVersion: 3
|
|
||||||
m_Size: {x: 0.8, y: 1.2, z: 0.8}
|
|
||||||
m_Center: {x: 0, y: 0.6, z: 0}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5459c9edea89bd94fa6f5043ae00eb40
|
|
||||||
PrefabImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -13,12 +13,6 @@ namespace ProjectM.Authoring
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class StructureCatalogAuthoring : MonoBehaviour
|
public class StructureCatalogAuthoring : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Tooltip("Turret structure ghost prefab (TurretAuthoring + GhostAuthoring).")]
|
|
||||||
public GameObject TurretPrefab;
|
|
||||||
|
|
||||||
[Tooltip("Ore cost to build a turret.")]
|
|
||||||
[Min(0)] public int TurretCostOre = 40; // DR-042 combat pass: was 10 (~1/3 node) -> 40 (~1.3 nodes); a real investment
|
|
||||||
|
|
||||||
[Tooltip("Wall structure ghost prefab (StructureAuthoring{Wall} + GhostAuthoring).")]
|
[Tooltip("Wall structure ghost prefab (StructureAuthoring{Wall} + GhostAuthoring).")]
|
||||||
public GameObject WallPrefab;
|
public GameObject WallPrefab;
|
||||||
|
|
||||||
@@ -39,16 +33,6 @@ namespace ProjectM.Authoring
|
|||||||
AddComponent<StructureCatalog>(entity);
|
AddComponent<StructureCatalog>(entity);
|
||||||
var buf = AddBuffer<StructureCatalogEntry>(entity);
|
var buf = AddBuffer<StructureCatalogEntry>(entity);
|
||||||
|
|
||||||
if (authoring.TurretPrefab != null)
|
|
||||||
{
|
|
||||||
buf.Add(new StructureCatalogEntry
|
|
||||||
{
|
|
||||||
Type = StructureType.Turret,
|
|
||||||
Prefab = GetEntity(authoring.TurretPrefab, TransformUsageFlags.Dynamic),
|
|
||||||
CostResourceId = ResourceId.Ore,
|
|
||||||
CostAmount = authoring.TurretCostOre,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (authoring.WallPrefab != null)
|
if (authoring.WallPrefab != null)
|
||||||
{
|
{
|
||||||
buf.Add(new StructureCatalogEntry
|
buf.Add(new StructureCatalogEntry
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
using ProjectM.Simulation;
|
|
||||||
using Unity.Entities;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace ProjectM.Authoring
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Authoring for the turret structure ghost prefab (duplicate UpgradePickup.prefab so the ownerless
|
|
||||||
/// interpolated GhostAuthoringComponent comes free). Bakes <see cref="PlacedStructure"/>{Type=Turret} +
|
|
||||||
/// <see cref="Turret"/> stats. BuildPlaceSystem stamps Cell + LastProcessedTick at placement.
|
|
||||||
/// </summary>
|
|
||||||
public class TurretAuthoring : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Min(1f)] public float Range = 10f;
|
|
||||||
[Min(1)] public int CooldownTicks = 30;
|
|
||||||
[Min(1f)] public float Damage = 12f;
|
|
||||||
[Min(1f)] public float MaxHp = 120f;
|
|
||||||
|
|
||||||
private class TurretBaker : Baker<TurretAuthoring>
|
|
||||||
{
|
|
||||||
public override void Bake(TurretAuthoring authoring)
|
|
||||||
{
|
|
||||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
|
||||||
this.AddPlacedStructure(entity, StructureType.Turret);
|
|
||||||
AddComponent(entity, new Turret
|
|
||||||
{
|
|
||||||
Range = authoring.Range,
|
|
||||||
CooldownTicks = authoring.CooldownTicks,
|
|
||||||
Damage = authoring.Damage,
|
|
||||||
});
|
|
||||||
// EB-1: structures are damageable + destructible (Husks push for them; HealthApplyDamageSystem
|
|
||||||
// destroys a Destructible at Health<=0). The DamageEvent buffer MUST exist on the archetype or an
|
|
||||||
// AI/turret strike crashes at ECB playback. NO HitRadius on purpose -> ProjectileDamageSystem (needs
|
|
||||||
// Health+HitRadius) ignores structures, so player shots never friendly-fire your own turret.
|
|
||||||
this.AddDamageable(entity, authoring.MaxHp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: dfb1e0820cbf41b4bbbe99066dfadd40
|
|
||||||
@@ -27,7 +27,6 @@ namespace ProjectM.Client
|
|||||||
// Dev hotkey fallback: key -> buildable.
|
// Dev hotkey fallback: key -> buildable.
|
||||||
static readonly (UnityEngine.InputSystem.Key Key, byte Type)[] s_BuildHotkeys =
|
static readonly (UnityEngine.InputSystem.Key Key, byte Type)[] s_BuildHotkeys =
|
||||||
{
|
{
|
||||||
(UnityEngine.InputSystem.Key.B, StructureType.Turret),
|
|
||||||
(UnityEngine.InputSystem.Key.V, StructureType.Wall),
|
(UnityEngine.InputSystem.Key.V, StructureType.Wall),
|
||||||
// LANTERN purge: the automation buildables (Fabricator/Harvester/Conveyor) are deleted; Pylon stays
|
// LANTERN purge: the automation buildables (Fabricator/Harvester/Conveyor) are deleted; Pylon stays
|
||||||
// hidden from the hotkey fallback to match the build palette. PlaceStructure execute_code statics remain.
|
// hidden from the hotkey fallback to match the build palette. PlaceStructure execute_code statics remain.
|
||||||
@@ -54,8 +53,6 @@ namespace ProjectM.Client
|
|||||||
public static void PlaceStructure(byte type, int cellX, int cellZ, byte direction = 0) =>
|
public static void PlaceStructure(byte type, int cellX, int cellZ, byte direction = 0) =>
|
||||||
s_PendingBuild.Enqueue(new PendingBuild { Type = type, CellX = cellX, CellZ = cellZ, Direction = direction });
|
s_PendingBuild.Enqueue(new PendingBuild { Type = type, CellX = cellX, CellZ = cellZ, Direction = direction });
|
||||||
|
|
||||||
/// <summary>EDITOR / execute_code hook: queue a turret placement at a specific cell.</summary>
|
|
||||||
public static void PlaceTurret(int cellX, int cellZ) => PlaceStructure(StructureType.Turret, cellX, cellZ);
|
|
||||||
|
|
||||||
/// <summary>EDITOR / execute_code hook: queue a wall placement at a specific cell.</summary>
|
/// <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);
|
public static void PlaceWall(int cellX, int cellZ) => PlaceStructure(StructureType.Wall, cellX, cellZ);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ namespace ProjectM.Client
|
|||||||
static readonly Color AetherCyan = new(0.30f, 0.85f, 1f);
|
static readonly Color AetherCyan = new(0.30f, 0.85f, 1f);
|
||||||
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
|
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
|
||||||
static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f);
|
static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f);
|
||||||
static readonly Color ChargeViolet = new(0.80f, 0.45f, 1f);
|
|
||||||
static readonly Color CoreRed = new(1f, 0.40f, 0.32f); // END-1 Engine Core integrity bar
|
static readonly Color CoreRed = new(1f, 0.40f, 0.32f); // END-1 Engine Core integrity bar
|
||||||
|
|
||||||
static readonly Color PanelDark = new(0.08f, 0.11f, 0.15f, 0.90f);
|
static readonly Color PanelDark = new(0.08f, 0.11f, 0.15f, 0.90f);
|
||||||
@@ -93,7 +92,7 @@ namespace ProjectM.Client
|
|||||||
readonly List<VisualElement> _pips = new();
|
readonly List<VisualElement> _pips = new();
|
||||||
|
|
||||||
// resources
|
// resources
|
||||||
Label _aetherNum, _oreNum, _bioNum, _chargeNum;
|
Label _aetherNum, _oreNum, _bioNum;
|
||||||
|
|
||||||
// build palette + hints
|
// build palette + hints
|
||||||
VisualElement _paletteRow, _hintBar, _buildDiscoveryChip;
|
VisualElement _paletteRow, _hintBar, _buildDiscoveryChip;
|
||||||
@@ -384,7 +383,7 @@ namespace ProjectM.Client
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Resources (feed palette affordability) ----
|
// ---- Resources (feed palette affordability) ----
|
||||||
int aether = 0, ore = 0, bio = 0, charge = 0;
|
int aether = 0, ore = 0, bio = 0;
|
||||||
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
|
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
|
||||||
{
|
{
|
||||||
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
|
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
|
||||||
@@ -394,25 +393,13 @@ namespace ProjectM.Client
|
|||||||
if (en.ItemId == ResourceId.Aether) aether = en.Count;
|
if (en.ItemId == ResourceId.Aether) aether = en.Count;
|
||||||
else if (en.ItemId == ResourceId.Ore) ore = en.Count;
|
else if (en.ItemId == ResourceId.Ore) ore = en.Count;
|
||||||
else if (en.ItemId == ResourceId.Biomass) bio = en.Count;
|
else if (en.ItemId == ResourceId.Biomass) bio = en.Count;
|
||||||
else if (en.ItemId == ResourceId.Charge) charge = en.Count;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_aetherNum.text = aether.ToString();
|
_aetherNum.text = aether.ToString();
|
||||||
_oreNum.text = ore.ToString();
|
_oreNum.text = ore.ToString();
|
||||||
_bioNum.text = bio.ToString();
|
_bioNum.text = bio.ToString();
|
||||||
_chargeNum.text = charge.ToString();
|
|
||||||
|
|
||||||
|
|
||||||
// DR-042 C6a: dim the Aether upgrade button when it isn't affordable (cost is a compile-time const).
|
|
||||||
// (Step 11: upgrade-button affordability tint retired with the button.)
|
|
||||||
// EB-2 quiet-turret cue (GLOBAL, not per-turret, so the deterministic Charge split never reads as one
|
|
||||||
// broken turret): a dry base during a siege tells the player to build a Fabricator.
|
|
||||||
if (siege && charge == 0 && !onExpedition)
|
|
||||||
{
|
|
||||||
_locationText.text = "TURRETS OUT OF AMMO - build a Fabricator (Ore -> ammo)";
|
|
||||||
_locationText.style.color = new Color(1f, 0.4f, 0.9f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Engine Core integrity (END-1): a red base-heart bar; an overrun stamps a transient pulse we flash ----
|
// ---- Engine Core integrity (END-1): a red base-heart bar; an overrun stamps a transient pulse we flash ----
|
||||||
if (SystemAPI.TryGetSingleton<CoreIntegrity>(out var core) && core.Max > 0)
|
if (SystemAPI.TryGetSingleton<CoreIntegrity>(out var core) && core.Max > 0)
|
||||||
{
|
{
|
||||||
@@ -1026,7 +1013,6 @@ namespace ProjectM.Client
|
|||||||
strip.Add(ResourceChip(theme != null ? theme.AetherIcon : null, AetherCyan, "0", out _aetherNum, 26, 20));
|
strip.Add(ResourceChip(theme != null ? theme.AetherIcon : null, AetherCyan, "0", out _aetherNum, 26, 20));
|
||||||
strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22));
|
strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22));
|
||||||
strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20));
|
strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20));
|
||||||
strip.Add(ResourceChip(null, ChargeViolet, "0", out _chargeNum, 26, 20)); // EB-2 turret ammo (flat violet, no icon)
|
|
||||||
// DR-042 C6a: the only Aether sink (ability-damage upgrade) gets a visible, clickable button (was U-key
|
// DR-042 C6a: the only Aether sink (ability-damage upgrade) gets a visible, clickable button (was U-key
|
||||||
// only). The Button element handles its own picking even though the HUD root Ignores clicks.
|
// only). The Button element handles its own picking even though the HUD root Ignores clicks.
|
||||||
// (Step 11: the Aether UPGRADE-DMG button was RETIRED with AbilityUpgradeRequest — the choice-of-3
|
// (Step 11: the Aether UPGRADE-DMG button was RETIRED with AbilityUpgradeRequest — the choice-of-3
|
||||||
@@ -1318,7 +1304,6 @@ namespace ProjectM.Client
|
|||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case StructureType.Turret: return "Turret";
|
|
||||||
case StructureType.Wall: return "Wall";
|
case StructureType.Wall: return "Wall";
|
||||||
case StructureType.Pylon: return "Pylon";
|
case StructureType.Pylon: return "Pylon";
|
||||||
default: return "?";
|
default: return "?";
|
||||||
|
|||||||
@@ -48,12 +48,10 @@ namespace ProjectM.Client
|
|||||||
public Sprite BioIcon;
|
public Sprite BioIcon;
|
||||||
|
|
||||||
[Header("Structure icons")]
|
[Header("Structure icons")]
|
||||||
public Sprite TurretIcon;
|
|
||||||
public Sprite WallIcon;
|
public Sprite WallIcon;
|
||||||
public Sprite PylonIcon;
|
public Sprite PylonIcon;
|
||||||
|
|
||||||
[Header("Build-ghost preview meshes (authored real-size, ground pivot — BuildSendSystem)")]
|
[Header("Build-ghost preview meshes (authored real-size, ground pivot — BuildSendSystem)")]
|
||||||
public Mesh TurretGhostMesh;
|
|
||||||
public Mesh WallGhostMesh;
|
public Mesh WallGhostMesh;
|
||||||
|
|
||||||
[Header("Build-mode control glyphs")]
|
[Header("Build-mode control glyphs")]
|
||||||
@@ -82,7 +80,6 @@ namespace ProjectM.Client
|
|||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case StructureType.Turret: return TurretIcon;
|
|
||||||
case StructureType.Wall: return WallIcon;
|
case StructureType.Wall: return WallIcon;
|
||||||
case StructureType.Pylon: return PylonIcon;
|
case StructureType.Pylon: return PylonIcon;
|
||||||
default: return null;
|
default: return null;
|
||||||
@@ -94,7 +91,6 @@ namespace ProjectM.Client
|
|||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case StructureType.Turret: return TurretGhostMesh;
|
|
||||||
case StructureType.Wall: return WallGhostMesh;
|
case StructureType.Wall: return WallGhostMesh;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,14 +47,10 @@ namespace ProjectM.Server
|
|||||||
var catalog = SystemAPI.GetBuffer<StructureCatalogEntry>(SystemAPI.GetSingletonEntity<StructureCatalog>());
|
var catalog = SystemAPI.GetBuffer<StructureCatalogEntry>(SystemAPI.GetSingletonEntity<StructureCatalog>());
|
||||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
||||||
|
|
||||||
// Derive occupancy from the live structure set (authoritative); also count turrets for the per-base cap.
|
// Derive occupancy from the live structure set (authoritative).
|
||||||
var occupied = new NativeHashSet<int2>(64, Allocator.Temp);
|
var occupied = new NativeHashSet<int2>(64, Allocator.Temp);
|
||||||
int turretCount = 0;
|
|
||||||
foreach (var ps in SystemAPI.Query<RefRO<PlacedStructure>>())
|
foreach (var ps in SystemAPI.Query<RefRO<PlacedStructure>>())
|
||||||
{
|
|
||||||
occupied.Add(ps.ValueRO.Cell);
|
occupied.Add(ps.ValueRO.Cell);
|
||||||
if (ps.ValueRO.Type == StructureType.Turret) turretCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||||
|
|
||||||
@@ -68,9 +64,7 @@ namespace ProjectM.Server
|
|||||||
for (int i = 0; i < catalog.Length; i++)
|
for (int i = 0; i < catalog.Length; i++)
|
||||||
if (catalog[i].Type == req.StructureType) { entryIdx = i; break; }
|
if (catalog[i].Type == req.StructureType) { entryIdx = i; break; }
|
||||||
|
|
||||||
// DR-042 combat pass: cap turrets per base (server-authoritative) so they can't be spammed.
|
if (entryIdx >= 0 && catalog[entryIdx].Prefab != Entity.Null
|
||||||
bool turretCapOk = req.StructureType != StructureType.Turret || turretCount < Tuning.TurretCap;
|
|
||||||
if (entryIdx >= 0 && catalog[entryIdx].Prefab != Entity.Null && turretCapOk
|
|
||||||
&& BuildPlacementMath.CanPlace(anchor, occupied, cell))
|
&& BuildPlacementMath.CanPlace(anchor, occupied, cell))
|
||||||
{
|
{
|
||||||
var entry = catalog[entryIdx];
|
var entry = catalog[entryIdx];
|
||||||
@@ -84,7 +78,6 @@ namespace ProjectM.Server
|
|||||||
// Commit IN-PLACE so a second same-tick request sees the spend + reservation.
|
// Commit IN-PLACE so a second same-tick request sees the spend + reservation.
|
||||||
StorageMath.Withdraw(ledger, entry.CostResourceId, entry.CostAmount);
|
StorageMath.Withdraw(ledger, entry.CostResourceId, entry.CostAmount);
|
||||||
occupied.Add(cell);
|
occupied.Add(cell);
|
||||||
if (req.StructureType == StructureType.Turret) turretCount++; // keep same-tick turret requests under the cap
|
|
||||||
|
|
||||||
var structure = ecb.Instantiate(entry.Prefab);
|
var structure = ecb.Instantiate(entry.Prefab);
|
||||||
var xform = m_TransformLookup[entry.Prefab];
|
var xform = m_TransformLookup[entry.Prefab];
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
using ProjectM.Simulation;
|
|
||||||
using Unity.Burst;
|
|
||||||
using Unity.Collections;
|
|
||||||
using Unity.Entities;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
using Unity.NetCode;
|
|
||||||
using Unity.Transforms;
|
|
||||||
|
|
||||||
namespace ProjectM.Server
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Server-only turret fire (hitscan) — EnemyAISystem reversed. Snapshots living Husks once (entity, planar
|
|
||||||
/// pos, region); each turret picks the nearest Husk in ITS region within Range and, on a NetworkTick
|
|
||||||
/// cooldown stored in <see cref="PlacedStructure.NextTick"/>, appends a direct <c>DamageEvent</c>
|
|
||||||
/// (SourceNetworkId=-1) to it. Reuses HealthApplyDamageSystem (already destroys EnemyTag at HP<=0) — no
|
|
||||||
/// projectile, no tunnelling, no friendly-fire. Plain server SimulationSystemGroup
|
|
||||||
/// <c>[UpdateAfter(PredictedSimulationSystemGroup)]</c> (the predicted group is OrderFirst → UpdateBefore is
|
|
||||||
/// ignored); the appended DamageEvent drains next tick (~16ms), consistent with EnemyAISystem. Self-gates:
|
|
||||||
/// Husks only exist during the Defend wave.
|
|
||||||
/// </summary>
|
|
||||||
[BurstCompile]
|
|
||||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
|
||||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
|
||||||
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
|
|
||||||
public partial struct TurretFireSystem : ISystem
|
|
||||||
{
|
|
||||||
[BurstCompile]
|
|
||||||
public void OnCreate(ref SystemState state)
|
|
||||||
{
|
|
||||||
state.RequireForUpdate<NetworkTime>();
|
|
||||||
state.RequireForUpdate<ResourceLedger>();
|
|
||||||
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Turret>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
[BurstCompile]
|
|
||||||
public void OnUpdate(ref SystemState state)
|
|
||||||
{
|
|
||||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
|
||||||
if (!serverTick.IsValid)
|
|
||||||
return;
|
|
||||||
uint now = serverTick.TickIndexForValidTick;
|
|
||||||
|
|
||||||
var huskEntities = new NativeList<Entity>(Allocator.Temp);
|
|
||||||
var huskPos = new NativeList<float2>(Allocator.Temp);
|
|
||||||
var huskRegion = new NativeList<byte>(Allocator.Temp);
|
|
||||||
foreach (var (xform, health, region, e) in
|
|
||||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>, RefRO<RegionTag>>()
|
|
||||||
.WithAll<EnemyTag>().WithNone<Dying>().WithEntityAccess()) // LIVING targets only (B3)
|
|
||||||
{
|
|
||||||
if (health.ValueRO.Current <= 0f)
|
|
||||||
continue;
|
|
||||||
huskEntities.Add(e);
|
|
||||||
huskPos.Add(xform.ValueRO.Position.xz);
|
|
||||||
huskRegion.Add(region.ValueRO.Region);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (huskEntities.Length == 0)
|
|
||||||
{
|
|
||||||
huskEntities.Dispose(); huskPos.Dispose(); huskRegion.Dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EB-2: resolve the shared ledger ONCE (NEVER GetSingleton<StorageEntry> — a 2nd StorageEntry buffer
|
|
||||||
// exists on the base container). Turrets withdraw Charge from it sequentially (a finite pool split in
|
|
||||||
// query order; later turrets soft-fail when it empties).
|
|
||||||
var ledgerEntity = SystemAPI.GetSingletonEntity<ResourceLedger>();
|
|
||||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
|
|
||||||
|
|
||||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
|
||||||
|
|
||||||
foreach (var (ps, turret, xform, region) in
|
|
||||||
SystemAPI.Query<RefRW<PlacedStructure>, RefRO<Turret>, RefRO<LocalTransform>, RefRO<RegionTag>>())
|
|
||||||
{
|
|
||||||
uint nextRaw = ps.ValueRO.NextTick;
|
|
||||||
if (nextRaw != 0)
|
|
||||||
{
|
|
||||||
var nextTick = new NetworkTick(nextRaw);
|
|
||||||
if (nextTick.IsValid && nextTick.IsNewerThan(serverTick))
|
|
||||||
continue; // still cooling down
|
|
||||||
}
|
|
||||||
|
|
||||||
float2 tp = xform.ValueRO.Position.xz;
|
|
||||||
byte treg = region.ValueRO.Region;
|
|
||||||
float rangeSq = turret.ValueRO.Range * turret.ValueRO.Range;
|
|
||||||
|
|
||||||
int best = -1;
|
|
||||||
float bestSq = float.MaxValue;
|
|
||||||
for (int i = 0; i < huskEntities.Length; i++)
|
|
||||||
{
|
|
||||||
if (huskRegion[i] != treg)
|
|
||||||
continue;
|
|
||||||
float sq = math.distancesq(huskPos[i], tp);
|
|
||||||
if (sq <= rangeSq && sq < bestSq)
|
|
||||||
{
|
|
||||||
bestSq = sq;
|
|
||||||
best = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (best >= 0)
|
|
||||||
{
|
|
||||||
// EB-2 felt spend: a shot costs Charge from the shared ledger. Gate BOTH the damage AND the
|
|
||||||
// cooldown advance on a SUCCESSFUL withdraw — out of Charge = SOFT-FAIL (no shot, no cooldown
|
|
||||||
// burn, so the turret fires the instant Charge returns). Refund a partial (cost>1 underflow).
|
|
||||||
int cost = math.max(1, Tuning.TurretChargeCostPerShot);
|
|
||||||
int got = StorageMath.Withdraw(ledger, ResourceId.Charge, cost);
|
|
||||||
if (got >= cost)
|
|
||||||
{
|
|
||||||
ecb.AppendToBuffer(huskEntities[best], new DamageEvent
|
|
||||||
{
|
|
||||||
Amount = turret.ValueRO.Damage,
|
|
||||||
SourceNetworkId = -1,
|
|
||||||
SourceTick = TickUtil.NonZero(now),
|
|
||||||
});
|
|
||||||
uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks);
|
|
||||||
ps.ValueRW.NextTick = TickUtil.NonZero(now + cd);
|
|
||||||
}
|
|
||||||
else if (got > 0)
|
|
||||||
{
|
|
||||||
StorageMath.Deposit(ledger, ResourceId.Charge, got); // never consume Charge without firing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ecb.Playback(state.EntityManager);
|
|
||||||
ecb.Dispose();
|
|
||||||
huskEntities.Dispose(); huskPos.Dispose(); huskRegion.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 53cc7669bd6cc5d4e8307b18732897bc
|
|
||||||
@@ -5,20 +5,19 @@ using Unity.NetCode;
|
|||||||
namespace ProjectM.Simulation
|
namespace ProjectM.Simulation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Structure type ids (a byte, not an enum, per the cross-assembly enum-in-Burst hazard). Turret is the
|
/// Structure type ids (a byte, not an enum, per the cross-assembly enum-in-Burst hazard). Ids 1-4 are
|
||||||
/// first concrete structure; Harvester/Fabricator/Conveyor are RESERVED now (free) for the M7 automation
|
/// RETIRED (EB-2 turret defense + M7 automation machines, deleted in the LANTERN purge) and stay reserved
|
||||||
/// pillar (production chains) so adding them later is purely additive.
|
/// so PlacedStructure.Type's [GhostField] serializer + old save bytes never re-mean.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class StructureType
|
public static class StructureType
|
||||||
{
|
{
|
||||||
public const byte None = 0;
|
public const byte None = 0;
|
||||||
|
// RETIRED ids — reserved, do not reuse:
|
||||||
public const byte Turret = 1;
|
public const byte Turret = 1;
|
||||||
// Reserved for M7 automation — do not reuse these codes:
|
|
||||||
public const byte Harvester = 2;
|
public const byte Harvester = 2;
|
||||||
public const byte Fabricator = 3;
|
public const byte Fabricator = 3;
|
||||||
public const byte Conveyor = 4;
|
public const byte Conveyor = 4;
|
||||||
// World-pass structural/cosmetic build-out (additive; these byte values keep PlacedStructure.Type's
|
// Live buildables:
|
||||||
// [GhostField] serializer identical — no re-bake of existing ghosts). Do NOT reuse 2-4 (M7 automation).
|
|
||||||
public const byte Wall = 5;
|
public const byte Wall = 5;
|
||||||
public const byte Pylon = 6;
|
public const byte Pylon = 6;
|
||||||
}
|
}
|
||||||
@@ -29,10 +28,8 @@ namespace ProjectM.Simulation
|
|||||||
/// (a cheap byte for client visual branching); <see cref="Cell"/> is server-only (clients derive it from
|
/// (a cheap byte for client visual branching); <see cref="Cell"/> is server-only (clients derive it from
|
||||||
/// the replicated LocalTransform via <see cref="BaseGridMath.WorldToCell"/>, so it stays off the wire).
|
/// the replicated LocalTransform via <see cref="BaseGridMath.WorldToCell"/>, so it stays off the wire).
|
||||||
/// <see cref="NextTick"/> / <see cref="LastProcessedTick"/> are server-only raw NetworkTick values
|
/// <see cref="NextTick"/> / <see cref="LastProcessedTick"/> are server-only raw NetworkTick values
|
||||||
/// (<see cref="TickUtil.NonZero"/>-guarded; 0 = inactive): the Turret reuses <see cref="NextTick"/> as its
|
/// (<see cref="TickUtil.NonZero"/>-guarded; 0 = inactive), kept for future timed structures (the turret
|
||||||
/// fire cooldown NOW (not dead weight), and M7 production reuses both as the next-production-tick +
|
/// cooldown + production catch-up that used them are retired — LANTERN purge).
|
||||||
/// deterministic offline catch-up linchpin (produced = floor((now - LastProcessedTick)/period)). These two
|
|
||||||
/// tick fields are the IDENTITY/TIMING that cannot be reconstructed retroactively, so they are baked now.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct PlacedStructure : IComponentData
|
public struct PlacedStructure : IComponentData
|
||||||
{
|
{
|
||||||
@@ -42,29 +39,16 @@ namespace ProjectM.Simulation
|
|||||||
/// <summary>Occupied grid cell (server-only; clients derive it from LocalTransform).</summary>
|
/// <summary>Occupied grid cell (server-only; clients derive it from LocalTransform).</summary>
|
||||||
public int2 Cell;
|
public int2 Cell;
|
||||||
|
|
||||||
/// <summary>Next action tick (server-only; turret cooldown now / next production tick in M7). 0 = inactive.</summary>
|
/// <summary>Next action tick (server-only). 0 = inactive.</summary>
|
||||||
public uint NextTick;
|
public uint NextTick;
|
||||||
|
|
||||||
/// <summary>Last tick this structure was processed (server-only; M7 offline-catch-up baseline). Stamped at spawn.</summary>
|
/// <summary>Last tick this structure was processed (server-only). Stamped at spawn.</summary>
|
||||||
public uint LastProcessedTick;
|
public uint LastProcessedTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A buildable defense turret (the first structure). Hitscan: <c>TurretFireSystem</c> applies a direct
|
|
||||||
/// <c>DamageEvent</c> to the nearest in-range living Husk on cooldown (reusing
|
|
||||||
/// <see cref="PlacedStructure.NextTick"/>) — reuses HealthApplyDamageSystem, no projectile/friendly-fire.
|
|
||||||
/// </summary>
|
|
||||||
public struct Turret : IComponentData
|
|
||||||
{
|
|
||||||
public float Range;
|
|
||||||
public int CooldownTicks;
|
|
||||||
public float Damage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One row of the build catalog: cost + prefab per structure type. Modeled on AbilityPrefabElement
|
/// One row of the build catalog: cost + prefab per structure type. Modeled on AbilityPrefabElement
|
||||||
/// (prefab baked via GetEntity, NEVER inside a blob — blobs don't remap entity refs). M7 adds a recipe
|
/// (prefab baked via GetEntity, NEVER inside a blob — blobs don't remap entity refs).
|
||||||
/// column to this element additively (the catalog is baked, not replicated → no format break).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct StructureCatalogEntry : IBufferElementData
|
public struct StructureCatalogEntry : IBufferElementData
|
||||||
{
|
{
|
||||||
@@ -85,3 +69,4 @@ namespace ProjectM.Simulation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public struct RuntimePlacedTag : IComponentData { }
|
public struct RuntimePlacedTag : IComponentData { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,18 +56,6 @@ namespace ProjectM.Simulation
|
|||||||
/// baked telegraph can't drift from the server windup.</summary>
|
/// baked telegraph can't drift from the server windup.</summary>
|
||||||
public const int ChargerWindupTicks = 30;
|
public const int ChargerWindupTicks = 30;
|
||||||
|
|
||||||
// ---- Base defense (EB-2) ----
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>EB-2: Charge (turret munition) consumed per turret shot, withdrawn from the global ledger. A
|
|
||||||
/// turret with 0 Charge SOFT-FAILS (no shot, no cooldown advance). A ledger-fed Fabricator mints Charge from
|
|
||||||
/// Ore. Operator feel-fork: keep generous so turrets stay fed while you keep mining.</summary>
|
|
||||||
public const int TurretChargeCostPerShot = 1;
|
|
||||||
|
|
||||||
/// <summary>Max turrets buildable per base (server-enforced in BuildPlaceSystem; the client preview goes red at
|
|
||||||
/// the cap). Turrets are a deliberate fortress investment, not spammable — paired with the 40-Ore build cost.</summary>
|
|
||||||
public const int TurretCap = 6;
|
|
||||||
|
|
||||||
// ---- Cold start (CycleDirectorSpawnSystem seeds the shared ledger on a NEW game) ----
|
// ---- Cold start (CycleDirectorSpawnSystem seeds the shared ledger on a NEW game) ----
|
||||||
|
|
||||||
/// <summary>DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save
|
/// <summary>DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save
|
||||||
|
|||||||
@@ -1,219 +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="TurretFireSystem"/> (hitscan defense turret).
|
|
||||||
/// A bare world is seeded with a <c>NetworkTime</c> singleton, a <see cref="ResourceLedger"/> Charge pool (the
|
|
||||||
/// turret's EB-2 ammo), a turret (PlacedStructure + Turret + RegionTag + LocalTransform) and Husks (Health +
|
|
||||||
/// EnemyTag + RegionTag + LocalTransform + a DamageEvent buffer). The system snapshots living Husks, fires at
|
|
||||||
/// the nearest in-range one in its OWN region on the <c>PlacedStructure.NextTick</c> cooldown — but ONLY when
|
|
||||||
/// it can withdraw <see cref="Tuning.TurretChargeCostPerShot"/> Charge from the ledger; out of Charge =
|
|
||||||
/// soft-fail (no shot, no cooldown burn, so it fires the instant Charge returns). These tests pin range
|
|
||||||
/// filtering, region gating, the wrap-safe cooldown, and the EB-2 Charge spend / soft-fail.
|
|
||||||
/// </summary>
|
|
||||||
public class TurretFireSystemTests
|
|
||||||
{
|
|
||||||
// Range/region/cooldown tests never need to run dry; the spend tests pass an explicit small pool.
|
|
||||||
const int AmpleCharge = 1_000_000;
|
|
||||||
|
|
||||||
static (World world, SimulationSystemGroup group, Entity ledger) MakeWorld(
|
|
||||||
string name, uint serverTick, int charge = AmpleCharge)
|
|
||||||
{
|
|
||||||
var world = new World(name);
|
|
||||||
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
||||||
group.AddSystemToUpdateList(world.GetOrCreateSystem<TurretFireSystem>());
|
|
||||||
group.SortSystems();
|
|
||||||
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
|
||||||
SetServerTick(world, serverTick);
|
|
||||||
var em = world.EntityManager;
|
|
||||||
var ledger = em.CreateEntity(typeof(ResourceLedger));
|
|
||||||
var buf = em.AddBuffer<StorageEntry>(ledger);
|
|
||||||
if (charge > 0)
|
|
||||||
buf.Add(new StorageEntry { ItemId = ResourceId.Charge, Count = charge });
|
|
||||||
return (world, group, ledger);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void SetServerTick(World world, uint tick)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
using var q = em.CreateEntityQuery(typeof(NetworkTime));
|
|
||||||
Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity();
|
|
||||||
em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) });
|
|
||||||
}
|
|
||||||
|
|
||||||
static Entity MakeTurret(EntityManager em, float3 pos, byte region, float range, int cooldown, float damage)
|
|
||||||
{
|
|
||||||
var e = em.CreateEntity();
|
|
||||||
em.AddComponentData(e, LocalTransform.FromPosition(pos));
|
|
||||||
em.AddComponentData(e, new RegionTag { Region = region });
|
|
||||||
em.AddComponentData(e, new PlacedStructure { Type = StructureType.Turret, NextTick = 0 });
|
|
||||||
em.AddComponentData(e, new Turret { Range = range, CooldownTicks = cooldown, Damage = damage });
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Entity MakeHusk(EntityManager em, float3 pos, byte region, float health)
|
|
||||||
{
|
|
||||||
var e = em.CreateEntity();
|
|
||||||
em.AddComponentData(e, LocalTransform.FromPosition(pos));
|
|
||||||
em.AddComponentData(e, new RegionTag { Region = region });
|
|
||||||
em.AddComponentData(e, new Health { Current = health, Max = health });
|
|
||||||
em.AddComponent<EnemyTag>(e);
|
|
||||||
em.AddBuffer<DamageEvent>(e);
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int LedgerCharge(EntityManager em, Entity ledger)
|
|
||||||
{
|
|
||||||
var buf = em.GetBuffer<StorageEntry>(ledger);
|
|
||||||
for (int i = 0; i < buf.Length; i++)
|
|
||||||
if (buf[i].ItemId == ResourceId.Charge) return buf[i].Count;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Turret_Fires_At_InRange_Husk_In_Its_Region()
|
|
||||||
{
|
|
||||||
var (world, group, _) = MakeWorld("TurretFireWorld", serverTick: 200);
|
|
||||||
using (world)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
var turret = MakeTurret(em, new float3(5, 1, 5), RegionId.Base, range: 20f, cooldown: 30, damage: 10f);
|
|
||||||
var husk = MakeHusk(em, new float3(10, 1, 10), RegionId.Base, health: 50f);
|
|
||||||
|
|
||||||
group.Update();
|
|
||||||
|
|
||||||
var dmg = em.GetBuffer<DamageEvent>(husk);
|
|
||||||
Assert.AreEqual(1, dmg.Length, "An in-range, same-region Husk takes exactly one turret hit.");
|
|
||||||
Assert.AreEqual(10f, dmg[0].Amount, 1e-4f, "DamageEvent carries the turret's damage.");
|
|
||||||
Assert.AreEqual(-1, dmg[0].SourceNetworkId, "Turret damage uses the -1 (world) source id.");
|
|
||||||
Assert.AreNotEqual(0u, em.GetComponentData<PlacedStructure>(turret).NextTick,
|
|
||||||
"The cooldown tick is stamped after firing.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Turret_Ignores_Husk_Out_Of_Range()
|
|
||||||
{
|
|
||||||
var (world, group, _) = MakeWorld("TurretRangeWorld", serverTick: 200);
|
|
||||||
using (world)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 5f, cooldown: 30, damage: 10f);
|
|
||||||
var husk = MakeHusk(em, new float3(100, 1, 0), RegionId.Base, health: 50f);
|
|
||||||
|
|
||||||
group.Update();
|
|
||||||
|
|
||||||
Assert.AreEqual(0, em.GetBuffer<DamageEvent>(husk).Length, "A Husk beyond Range takes no hit.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Turret_Ignores_Husk_In_A_Different_Region()
|
|
||||||
{
|
|
||||||
var (world, group, _) = MakeWorld("TurretRegionWorld", serverTick: 200);
|
|
||||||
using (world)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f);
|
|
||||||
var husk = MakeHusk(em, new float3(2, 1, 2), RegionId.Expedition, health: 50f);
|
|
||||||
|
|
||||||
group.Update();
|
|
||||||
|
|
||||||
Assert.AreEqual(0, em.GetBuffer<DamageEvent>(husk).Length,
|
|
||||||
"A Husk in a different region is never targeted (region gating).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Turret_Respects_Cooldown_Then_Fires_Again()
|
|
||||||
{
|
|
||||||
var (world, group, _) = MakeWorld("TurretCooldownWorld", serverTick: 200);
|
|
||||||
using (world)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f);
|
|
||||||
var husk = MakeHusk(em, new float3(3, 1, 0), RegionId.Base, health: 5000f);
|
|
||||||
|
|
||||||
group.Update(); // fires at tick 200, NextTick -> 230
|
|
||||||
Assert.AreEqual(1, em.GetBuffer<DamageEvent>(husk).Length, "Fires on the first ready tick.");
|
|
||||||
|
|
||||||
SetServerTick(world, 210);
|
|
||||||
group.Update(); // 210 < 230 -> still cooling down
|
|
||||||
Assert.AreEqual(1, em.GetBuffer<DamageEvent>(husk).Length, "No second shot while cooling down.");
|
|
||||||
|
|
||||||
SetServerTick(world, 240);
|
|
||||||
group.Update(); // 240 >= 230 -> ready again
|
|
||||||
Assert.AreEqual(2, em.GetBuffer<DamageEvent>(husk).Length, "Fires again once the cooldown elapses.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- EB-2: turret ammo (Charge) spend + soft-fail ----
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Turret_Out_Of_Charge_Soft_Fails_No_Shot_No_Cooldown_Burn()
|
|
||||||
{
|
|
||||||
var (world, group, ledger) = MakeWorld("TurretNoChargeWorld", serverTick: 200, charge: 0);
|
|
||||||
using (world)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
var turret = MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f);
|
|
||||||
var husk = MakeHusk(em, new float3(3, 1, 0), RegionId.Base, health: 50f);
|
|
||||||
|
|
||||||
group.Update();
|
|
||||||
|
|
||||||
Assert.AreEqual(0, em.GetBuffer<DamageEvent>(husk).Length,
|
|
||||||
"With an empty Charge pool the turret cannot fire (soft-fail).");
|
|
||||||
Assert.AreEqual(0u, em.GetComponentData<PlacedStructure>(turret).NextTick,
|
|
||||||
"A soft-failed shot burns no cooldown — the turret stays ready for the instant Charge returns.");
|
|
||||||
Assert.AreEqual(0, LedgerCharge(em, ledger), "No Charge is consumed when no shot is fired.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Turret_Consumes_One_Charge_Per_Shot()
|
|
||||||
{
|
|
||||||
var (world, group, ledger) = MakeWorld("TurretSpendWorld", serverTick: 200, charge: 5);
|
|
||||||
using (world)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f);
|
|
||||||
var husk = MakeHusk(em, new float3(3, 1, 0), RegionId.Base, health: 50f);
|
|
||||||
|
|
||||||
group.Update();
|
|
||||||
|
|
||||||
Assert.AreEqual(1, em.GetBuffer<DamageEvent>(husk).Length, "A funded turret fires.");
|
|
||||||
Assert.AreEqual(5 - Tuning.TurretChargeCostPerShot, LedgerCharge(em, ledger),
|
|
||||||
"Exactly TurretChargeCostPerShot Charge is withdrawn for the shot.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Two_Turrets_Share_A_Finite_Charge_Pool_Second_Soft_Fails()
|
|
||||||
{
|
|
||||||
// One Charge, two turrets, one Husk: the first turret (query/creation order) spends the last Charge and
|
|
||||||
// fires; the second withdraws 0 and soft-fails. The Husk takes exactly one hit and the pool empties.
|
|
||||||
var (world, group, ledger) = MakeWorld("TurretSharedPoolWorld", serverTick: 200, charge: 1);
|
|
||||||
using (world)
|
|
||||||
{
|
|
||||||
var em = world.EntityManager;
|
|
||||||
MakeTurret(em, new float3(-2, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f);
|
|
||||||
MakeTurret(em, new float3(2, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f);
|
|
||||||
var husk = MakeHusk(em, new float3(0, 1, 0), RegionId.Base, health: 500f);
|
|
||||||
|
|
||||||
group.Update();
|
|
||||||
|
|
||||||
Assert.AreEqual(1, em.GetBuffer<DamageEvent>(husk).Length,
|
|
||||||
"A single Charge funds exactly one of the two turrets this tick.");
|
|
||||||
Assert.AreEqual(0, LedgerCharge(em, ledger), "The shared Charge pool is drained to zero.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a86a8f9715eff4c45817718443589cdb
|
|
||||||
Reference in New Issue
Block a user