diff --git a/Assets/Screenshots/m6_base_turrets.png b/Assets/Screenshots/m6_base_turrets.png
new file mode 100644
index 000000000..37a718281
--- /dev/null
+++ b/Assets/Screenshots/m6_base_turrets.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99fab6007f3631ae8dded915e6223f4d5737e8c8b5c4873588101719c19f4182
+size 226379
diff --git a/Assets/Screenshots/m6_base_turrets.png.meta b/Assets/Screenshots/m6_base_turrets.png.meta
new file mode 100644
index 000000000..85b49c0d8
--- /dev/null
+++ b/Assets/Screenshots/m6_base_turrets.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: dd4de11cfd2910947b35819172869126
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Turret.prefab b/Assets/_Project/Prefabs/Turret.prefab
new file mode 100644
index 000000000..c70177ba4
--- /dev/null
+++ b/Assets/_Project/Prefabs/Turret.prefab
@@ -0,0 +1,148 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !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: 3909651526955663392}
+ - component: {fileID: 3320445911748035220}
+ - component: {fileID: 9053853372340598254}
+ - component: {fileID: 6834786618115927220}
+ - component: {fileID: 1794795016809289889}
+ m_Layer: 0
+ 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: 2.5, y: 2.5, z: 2.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &3909651526955663392
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Mesh: {fileID: 4300000, guid: abc00000000010690097314383055197, type: 3}
+--- !u!23 &3320445911748035220
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ 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: 175e5bfb2ad02704caa3d1753aad499d, 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 &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
diff --git a/Assets/_Project/Prefabs/Turret.prefab.meta b/Assets/_Project/Prefabs/Turret.prefab.meta
new file mode 100644
index 000000000..3e8e9f559
--- /dev/null
+++ b/Assets/_Project/Prefabs/Turret.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 5459c9edea89bd94fa6f5043ae00eb40
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/Building.meta b/Assets/_Project/Scripts/Authoring/Building.meta
new file mode 100644
index 000000000..eb31be0a1
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Building.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 97bd1bd2a09697f449a5580826a0355d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs
new file mode 100644
index 000000000..7c735852f
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs
@@ -0,0 +1,44 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for the baked singleton (the build cost/prefab table). For the
+ /// M6 foundation there is one buildable type (Turret), so the entry is flat fields — only the prefab
+ /// object-ref + cost amount need inspector wiring; the type + cost-resource are byte consts in the baker
+ /// (enum-via-MCP is unreliable, and bytes dodge the cross-assembly enum-in-Burst hazard). M7 generalizes to
+ /// an array; the runtime buffer is already the data-driven shape. Place
+ /// once in the gameplay subscene.
+ ///
+ 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 = 10;
+
+ private class StructureCatalogBaker : Baker
+ {
+ public override void Bake(StructureCatalogAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.None);
+ AddComponent(entity);
+ var buf = AddBuffer(entity);
+
+ if (authoring.TurretPrefab != null)
+ {
+ buf.Add(new StructureCatalogEntry
+ {
+ Type = StructureType.Turret,
+ Prefab = GetEntity(authoring.TurretPrefab, TransformUsageFlags.Dynamic),
+ CostResourceId = ResourceId.Ore,
+ CostAmount = authoring.TurretCostOre,
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs.meta
new file mode 100644
index 000000000..400430a9a
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Building/StructureCatalogAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 40093ed42072f5a4889f5f62f510aa27
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs
new file mode 100644
index 000000000..4d752cc20
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs
@@ -0,0 +1,39 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for the turret structure ghost prefab (duplicate UpgradePickup.prefab so the ownerless
+ /// interpolated GhostAuthoringComponent comes free). Bakes {Type=Turret} +
+ /// stats. BuildPlaceSystem stamps Cell + LastProcessedTick at placement.
+ ///
+ public class TurretAuthoring : MonoBehaviour
+ {
+ [Min(1f)] public float Range = 10f;
+ [Min(1)] public int CooldownTicks = 30;
+ [Min(1f)] public float Damage = 12f;
+
+ private class TurretBaker : Baker
+ {
+ public override void Bake(TurretAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
+ AddComponent(entity, new PlacedStructure
+ {
+ Type = StructureType.Turret,
+ Cell = default,
+ NextTick = 0u,
+ LastProcessedTick = 0u,
+ });
+ AddComponent(entity, new Turret
+ {
+ Range = authoring.Range,
+ CooldownTicks = authoring.CooldownTicks,
+ Damage = authoring.Damage,
+ });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs.meta
new file mode 100644
index 000000000..468df5a7b
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Building/TurretAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: dfb1e0820cbf41b4bbbe99066dfadd40
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
index e4a5c18a8..970587349 100644
--- a/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
+++ b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
@@ -28,6 +28,7 @@ namespace ProjectM.Authoring
});
AddComponent(entity);
AddBuffer(entity);
+ AddComponent(entity, new GoalProgress { Charge = 0, Target = 10 });
}
}
}
diff --git a/Assets/_Project/Scripts/Client/Building.meta b/Assets/_Project/Scripts/Client/Building.meta
new file mode 100644
index 000000000..8c9df44f7
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Building.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ef7d7956e34fb0d43a145bc4cfd425d0
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs
new file mode 100644
index 000000000..fc0c758f9
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs
@@ -0,0 +1,97 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.NetCode;
+using Unity.Transforms;
+
+namespace ProjectM.Client
+{
+ ///
+ /// Client-only sender for build + upgrade RPCs. Keyboard: B builds a Turret at the local player's current
+ /// grid cell; U upgrades ability damage. Editor-only statics (PlaceStructure / PlaceTurret / UpgradeAbility)
+ /// drive the same path from execute_code for headless validation (a one-shot key can't be injected on an
+ /// unfocused editor — the StorageOpSendSystem idiom). Managed SystemBase (reads the Input System);
+ /// UnityEngine.InputSystem is fully qualified to avoid the ProjectM.Simulation.PlayerInput name collision.
+ /// The server re-validates legality + cost authoritatively; this only sends a hint.
+ ///
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ public partial class BuildSendSystem : SystemBase
+ {
+#if UNITY_EDITOR
+ struct PendingBuild { public byte Type; public int CellX; public int CellZ; }
+ static readonly System.Collections.Generic.Queue s_PendingBuild =
+ new System.Collections.Generic.Queue();
+ static int s_PendingUpgrades = 0;
+
+ /// EDITOR / execute_code hook: queue a structure placement at a specific cell.
+ public static void PlaceStructure(byte type, int cellX, int cellZ) =>
+ s_PendingBuild.Enqueue(new PendingBuild { Type = type, CellX = cellX, CellZ = cellZ });
+
+ /// EDITOR / execute_code hook: queue a turret placement at a specific cell.
+ public static void PlaceTurret(int cellX, int cellZ) => PlaceStructure(StructureType.Turret, cellX, cellZ);
+
+ /// EDITOR / execute_code hook: queue an ability-damage upgrade.
+ public static void UpgradeAbility() => s_PendingUpgrades++;
+#endif
+
+ protected override void OnCreate()
+ {
+ RequireForUpdate();
+ }
+
+ protected override void OnUpdate()
+ {
+ if (!SystemAPI.TryGetSingletonEntity(out var connection))
+ return;
+
+ var keyboard = UnityEngine.InputSystem.Keyboard.current;
+ if (keyboard != null)
+ {
+ if (keyboard.bKey.wasPressedThisFrame && TryGetLocalPlayerCell(out int2 cell))
+ SendBuild(connection, StructureType.Turret, cell.x, cell.y);
+ if (keyboard.uKey.wasPressedThisFrame)
+ SendUpgrade(connection);
+ }
+
+#if UNITY_EDITOR
+ while (s_PendingBuild.Count > 0)
+ {
+ var b = s_PendingBuild.Dequeue();
+ SendBuild(connection, b.Type, b.CellX, b.CellZ);
+ }
+ while (s_PendingUpgrades > 0)
+ {
+ s_PendingUpgrades--;
+ SendUpgrade(connection);
+ }
+#endif
+ }
+
+ bool TryGetLocalPlayerCell(out int2 cell)
+ {
+ cell = default;
+ if (!SystemAPI.TryGetSingleton(out var anchor))
+ return false;
+ foreach (var xform in SystemAPI.Query>().WithAll())
+ {
+ cell = BaseGridMath.WorldToCell(anchor, xform.ValueRO.Position);
+ return true;
+ }
+ return false;
+ }
+
+ void SendBuild(Entity connection, byte type, int cellX, int cellZ)
+ {
+ var e = EntityManager.CreateEntity();
+ EntityManager.AddComponentData(e, new BuildPlaceRequest { StructureType = type, CellX = cellX, CellZ = cellZ });
+ EntityManager.AddComponentData(e, new SendRpcCommandRequest { TargetConnection = connection });
+ }
+
+ void SendUpgrade(Entity connection)
+ {
+ var e = EntityManager.CreateEntity();
+ EntityManager.AddComponentData(e, new AbilityUpgradeRequest());
+ EntityManager.AddComponentData(e, new SendRpcCommandRequest { TargetConnection = connection });
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs.meta b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs.meta
new file mode 100644
index 000000000..2f4de9bf2
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 765356fd6c5e64c4e9ab588f99d3388f
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
index 1d0fe552f..f09e5a203 100644
--- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
+++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
@@ -27,6 +27,8 @@ namespace ProjectM.Client
Text _phaseText;
Text _resourceText;
Text _locationText;
+ RectTransform _goalFill;
+ Text _goalText;
GameObject _respawnOverlay;
EntityQuery _huskQuery;
@@ -81,6 +83,13 @@ namespace ProjectM.Client
_locationText.color = onExpedition ? new Color(1f, 0.8f, 0.4f) : new Color(0.6f, 0.85f, 1f);
}
+ if (_goalFill != null && SystemAPI.TryGetSingleton(out var goal))
+ {
+ float gfrac = goal.Target > 0 ? Mathf.Clamp01(goal.Charge / (float)goal.Target) : 0f;
+ SetFill(_goalFill, gfrac);
+ if (_goalText != null) _goalText.text = "GOAL " + goal.Charge + " / " + goal.Target;
+ }
+
if (_resourceText != null)
{
string res = "";
@@ -211,6 +220,12 @@ namespace ProjectM.Client
var lrt = _locationText.rectTransform;
lrt.anchorMin = new Vector2(0.5f, 1f); lrt.anchorMax = new Vector2(0.5f, 1f); lrt.pivot = new Vector2(0.5f, 1f);
lrt.anchoredPosition = new Vector2(0, -96); lrt.sizeDelta = new Vector2(760, 36);
+ // Goal progress bar (top-center, below the location line).
+ var goalBg = MakeBar("GoalBg", _canvas.transform, new Color(0f, 0f, 0f, 0.55f), Vector2.zero, new Vector2(360, 16));
+ goalBg.anchorMin = new Vector2(0.5f, 1f); goalBg.anchorMax = new Vector2(0.5f, 1f); goalBg.pivot = new Vector2(0.5f, 1f);
+ goalBg.anchoredPosition = new Vector2(0, -126); goalBg.sizeDelta = new Vector2(360, 16);
+ _goalFill = MakeFill("GoalFill", goalBg, new Color(0.8f, 0.6f, 1f));
+ _goalText = MakeText("GoalText", goalBg, "GOAL 0 / 10", 15, TextAnchor.MiddleCenter, Color.white, font);
// Downed / respawning overlay (full screen, toggled by Dead).
_respawnOverlay = new GameObject("RespawnOverlay", typeof(RectTransform));
diff --git a/Assets/_Project/Scripts/Server/Building.meta b/Assets/_Project/Scripts/Server/Building.meta
new file mode 100644
index 000000000..9487de558
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Building.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 73829e22632af284d880736d6b0a371f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs b/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs
new file mode 100644
index 000000000..542fff40a
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs
@@ -0,0 +1,92 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-authoritative ability-damage upgrade (handles RPCs). Resolves
+ /// the sender's player (SourceConnection -> NetworkId -> GhostOwner) and, if the global ledger affords the
+ /// Aether cost, withdraws it IN-PLACE and grows a single damage on the player
+ /// (replace-by-SourceId so the [InternalBufferCapacity(8)] buffer stays bounded — repeated upgrades grow one
+ /// row's percent rather than appending). StatRecomputeSystem folds it into EffectiveAbilityStats.Damage on
+ /// both worlds. Plain server SimulationSystemGroup (not predicted → applied once).
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ public partial struct AbilityUpgradeSystem : ISystem
+ {
+ const uint UpgradeSourceId = 0x00A0E711u; // distinct sentinel so the upgrade modifier is found + grown
+ const float TierStep = 0.25f; // +25% damage per tier
+ const int CostAmount = 20; // Aether per tier
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity());
+
+ var playerByConn = new NativeHashMap(8, Allocator.Temp);
+ foreach (var (owner, entity) in
+ SystemAPI.Query>().WithAll().WithEntityAccess())
+ playerByConn[owner.ValueRO.NetworkId] = entity;
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ foreach (var (receive, requestEntity) in
+ SystemAPI.Query>().WithAll().WithEntityAccess())
+ {
+ var conn = receive.ValueRO.SourceConnection;
+ if (SystemAPI.HasComponent(conn)
+ && playerByConn.TryGetValue(SystemAPI.GetComponent(conn).Value, out var player))
+ {
+ int have = 0;
+ for (int i = 0; i < ledger.Length; i++)
+ if (ledger[i].ItemId == ResourceId.Aether) { have = ledger[i].Count; break; }
+
+ if (have >= CostAmount)
+ {
+ StorageMath.Withdraw(ledger, ResourceId.Aether, CostAmount);
+
+ var mods = SystemAPI.GetBuffer(player);
+ bool grown = false;
+ for (int i = 0; i < mods.Length; i++)
+ {
+ if (mods[i].SourceId == UpgradeSourceId && mods[i].Target == (byte)StatTarget.Damage)
+ {
+ var m = mods[i];
+ m.Value += TierStep;
+ mods[i] = m;
+ grown = true;
+ break;
+ }
+ }
+ if (!grown)
+ mods.Add(new StatModifier
+ {
+ Target = (byte)StatTarget.Damage,
+ Op = (byte)ModOp.PercentAdd,
+ Value = TierStep,
+ SourceId = UpgradeSourceId,
+ });
+ }
+ }
+
+ ecb.DestroyEntity(requestEntity);
+ }
+
+ ecb.Playback(state.EntityManager);
+ playerByConn.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs.meta b/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs.meta
new file mode 100644
index 000000000..90855d758
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ff2ed6b5fa37a174aa7413f4d2f5d6b3
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs
new file mode 100644
index 000000000..e59264bd7
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs
@@ -0,0 +1,104 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.NetCode;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-authoritative structure placement (handles RPCs). Derives
+ /// occupancy by scanning live ghosts into a Temp NativeHashSet (structures
+ /// are the source of truth — no cached buffer on the immutable BaseAnchor). For each request it validates
+ /// catalog/legality/occupancy/cost, and on success commits IN-PLACE (StorageMath.Withdraw on the global
+ /// ledger + reserve the cell in the set) so two same-tick requests for one cell can't both pass — the
+ /// StorageOpReceiveSystem in-place idiom — then instantiates the catalog prefab at the cell center
+ /// (RegionTag{Base}, world-owned, NextTick=0, LastProcessedTick stamped). Plain server SimulationSystemGroup
+ /// (not predicted → applied once). Rejects invalid requests silently.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ public partial struct BuildPlaceSystem : ISystem
+ {
+ ComponentLookup m_TransformLookup;
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ m_TransformLookup = state.GetComponentLookup(isReadOnly: true);
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ m_TransformLookup.Update(ref state);
+ uint now = SystemAPI.GetSingleton().ServerTick.TickIndexForValidTick;
+ var anchor = SystemAPI.GetSingleton();
+
+ var catalog = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity());
+ var ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity());
+
+ // Derive occupancy from the live structure set (authoritative source of truth).
+ var occupied = new NativeHashSet(64, Allocator.Temp);
+ foreach (var ps in SystemAPI.Query>())
+ occupied.Add(ps.ValueRO.Cell);
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ foreach (var (request, receive, requestEntity) in
+ SystemAPI.Query, RefRO>().WithEntityAccess())
+ {
+ var req = request.ValueRO;
+ int2 cell = new int2(req.CellX, req.CellZ);
+
+ int entryIdx = -1;
+ for (int i = 0; i < catalog.Length; i++)
+ if (catalog[i].Type == req.StructureType) { entryIdx = i; break; }
+
+ if (entryIdx >= 0 && catalog[entryIdx].Prefab != Entity.Null
+ && BuildPlacementMath.CanPlace(anchor, occupied, cell))
+ {
+ var entry = catalog[entryIdx];
+
+ int have = 0;
+ for (int i = 0; i < ledger.Length; i++)
+ if (ledger[i].ItemId == entry.CostResourceId) { have = ledger[i].Count; break; }
+
+ if (have >= entry.CostAmount)
+ {
+ // Commit IN-PLACE so a second same-tick request sees the spend + reservation.
+ StorageMath.Withdraw(ledger, entry.CostResourceId, entry.CostAmount);
+ occupied.Add(cell);
+
+ var structure = ecb.Instantiate(entry.Prefab);
+ var xform = m_TransformLookup[entry.Prefab];
+ xform.Position = BaseGridMath.CellToWorld(anchor, cell); // preserve baked Scale
+ ecb.SetComponent(structure, xform);
+ ecb.SetComponent(structure, new PlacedStructure
+ {
+ Type = req.StructureType,
+ Cell = cell,
+ NextTick = 0u,
+ LastProcessedTick = TickUtil.NonZero(now),
+ });
+ ecb.AddComponent(structure, new RegionTag { Region = RegionId.Base });
+ }
+ }
+
+ ecb.DestroyEntity(requestEntity);
+ }
+
+ ecb.Playback(state.EntityManager);
+ occupied.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs.meta b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs.meta
new file mode 100644
index 000000000..24c101de7
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Building/BuildPlaceSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d1886c7056b315e42b7754f50c43c59e
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs
new file mode 100644
index 000000000..d44da6fc1
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs
@@ -0,0 +1,110 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.NetCode;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// 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 , appends a direct DamageEvent
+ /// (SourceNetworkId=-1) to it. Reuses HealthApplyDamageSystem (already destroys EnemyTag at HP<=0) — no
+ /// projectile, no tunnelling, no friendly-fire. Plain server SimulationSystemGroup
+ /// [UpdateAfter(PredictedSimulationSystemGroup)] (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.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateAfter(typeof(PredictedSimulationSystemGroup))]
+ public partial struct TurretFireSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly()));
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var serverTick = SystemAPI.GetSingleton().ServerTick;
+ if (!serverTick.IsValid)
+ return;
+ uint now = serverTick.TickIndexForValidTick;
+
+ var huskEntities = new NativeList(Allocator.Temp);
+ var huskPos = new NativeList(Allocator.Temp);
+ var huskRegion = new NativeList(Allocator.Temp);
+ foreach (var (xform, health, region, e) in
+ SystemAPI.Query, RefRO, RefRO>()
+ .WithAll().WithEntityAccess())
+ {
+ 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;
+ }
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ foreach (var (ps, turret, xform, region) in
+ SystemAPI.Query, RefRO, RefRO, RefRO>())
+ {
+ 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)
+ {
+ ecb.AppendToBuffer(huskEntities[best], new DamageEvent
+ {
+ Amount = turret.ValueRO.Damage,
+ SourceNetworkId = -1,
+ });
+ uint cd = (uint)math.max(1, turret.ValueRO.CooldownTicks);
+ ps.ValueRW.NextTick = TickUtil.NonZero(now + cd);
+ }
+ }
+
+ ecb.Playback(state.EntityManager);
+ ecb.Dispose();
+ huskEntities.Dispose(); huskPos.Dispose(); huskRegion.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs.meta b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs.meta
new file mode 100644
index 000000000..88b909ef1
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Building/TurretFireSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 53cc7669bd6cc5d4e8307b18732897bc
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs b/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs
index daf93473c..50204923d 100644
--- a/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs
+++ b/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs
@@ -71,6 +71,13 @@ namespace ProjectM.Server
cycle.Phase = CyclePhase.Expedition;
cycle.CycleNumber += 1;
cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks);
+ // Long-arc goal: one charge per completed cycle (single writer).
+ if (SystemAPI.HasComponent(cycleEntity))
+ {
+ var goal = SystemAPI.GetComponent(cycleEntity);
+ goal.Charge += 1;
+ SystemAPI.SetComponent(cycleEntity, goal);
+ }
}
break;
}
diff --git a/Assets/_Project/Scripts/Simulation/Building.meta b/Assets/_Project/Scripts/Simulation/Building.meta
new file mode 100644
index 000000000..153d11a06
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 53b186388519ddb458c72bb717085721
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Simulation/Building/AbilityUpgradeRequest.cs b/Assets/_Project/Scripts/Simulation/Building/AbilityUpgradeRequest.cs
new file mode 100644
index 000000000..1f31ca966
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/AbilityUpgradeRequest.cs
@@ -0,0 +1,12 @@
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Client -> server request to upgrade the sender's ability damage one tier, spending Aether from the
+ /// shared ledger. A one-off RPC. The server grows a single damage on the
+ /// player (replace-by-SourceId so the buffer stays bounded), which StatRecomputeSystem folds into
+ /// EffectiveAbilityStats.Damage on both worlds — no new replicated component.
+ ///
+ public struct AbilityUpgradeRequest : IRpcCommand { }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Building/AbilityUpgradeRequest.cs.meta b/Assets/_Project/Scripts/Simulation/Building/AbilityUpgradeRequest.cs.meta
new file mode 100644
index 000000000..c46c2812c
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/AbilityUpgradeRequest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1236d3751a5740741a4a10e0a653565f
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Building/BuildPlaceRequest.cs b/Assets/_Project/Scripts/Simulation/Building/BuildPlaceRequest.cs
new file mode 100644
index 000000000..f327d4e2f
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/BuildPlaceRequest.cs
@@ -0,0 +1,18 @@
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Client -> server request to build a structure of at grid cell
+ /// (, ). A one-off action, so an RPC (mirrors StorageOpRequest /
+ /// RegionTransitRequest). StructureType is a byte; the cell is two int scalars (NOT an int2) to stay
+ /// within the project's scalar-only RPC payload precedent (avoids first-of-its-kind composite-math-in-RPC
+ /// codegen risk on Netcode 1.13.2). The server re-validates legality + cost authoritatively.
+ ///
+ public struct BuildPlaceRequest : IRpcCommand
+ {
+ public byte StructureType;
+ public int CellX;
+ public int CellZ;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Building/BuildPlaceRequest.cs.meta b/Assets/_Project/Scripts/Simulation/Building/BuildPlaceRequest.cs.meta
new file mode 100644
index 000000000..865c3be23
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/BuildPlaceRequest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: dbcc491dc3dd853459cd8cfad2458b17
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Building/BuildPlacementMath.cs b/Assets/_Project/Scripts/Simulation/Building/BuildPlacementMath.cs
new file mode 100644
index 000000000..91ae68735
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/BuildPlacementMath.cs
@@ -0,0 +1,24 @@
+using Unity.Collections;
+using Unity.Mathematics;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Pure, deterministic build-placement helpers (unit-tested like /
+ /// StorageMath). Occupancy is DERIVED from the live structure set each placement (the structure
+ /// ghosts are the source of truth — restart- and replay-order-safe), never cached on the immutable
+ /// baked . The server passes a Temp of occupied
+ /// cells built by scanning live ghosts.
+ ///
+ public static class BuildPlacementMath
+ {
+ /// True if is occupied in the derived set.
+ public static bool IsOccupied(in NativeHashSet occupied, int2 cell) => occupied.Contains(cell);
+
+ /// Full server placement legality: the cell is in-plot (half-open, negative-safe) AND not occupied.
+ public static bool CanPlace(in BaseAnchor anchor, in NativeHashSet occupied, int2 cell)
+ {
+ return BaseGridMath.IsCellInPlot(anchor, cell) && !occupied.Contains(cell);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Building/BuildPlacementMath.cs.meta b/Assets/_Project/Scripts/Simulation/Building/BuildPlacementMath.cs.meta
new file mode 100644
index 000000000..88f61feba
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/BuildPlacementMath.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 203dcbd4f9cc089408633b0bb6ccb2c1
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs
new file mode 100644
index 000000000..f674df8ab
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs
@@ -0,0 +1,75 @@
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Structure type ids (a byte, not an enum, per the cross-assembly enum-in-Burst hazard). Turret is the
+ /// first concrete structure; Harvester/Fabricator/Conveyor are RESERVED now (free) for the M7 automation
+ /// pillar (production chains) so adding them later is purely additive.
+ ///
+ public static class StructureType
+ {
+ public const byte None = 0;
+ public const byte Turret = 1;
+ // Reserved for M7 automation — do not reuse these codes:
+ public const byte Harvester = 2;
+ public const byte Fabricator = 3;
+ public const byte Conveyor = 4;
+ }
+
+ ///
+ /// A built base structure occupying one grid cell. An ownerless INTERPOLATED ghost (RegionTag{Base},
+ /// world-owned, runtime-spawned by BuildPlaceSystem). is the only replicated field
+ /// (a cheap byte for client visual branching); is server-only (clients derive it from
+ /// the replicated LocalTransform via , so it stays off the wire).
+ /// / are server-only raw NetworkTick values
+ /// (-guarded; 0 = inactive): the Turret reuses as its
+ /// fire cooldown NOW (not dead weight), and M7 production reuses both as the next-production-tick +
+ /// 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.
+ ///
+ public struct PlacedStructure : IComponentData
+ {
+ /// Structure type (see ); the only replicated field.
+ [GhostField] public byte Type;
+
+ /// Occupied grid cell (server-only; clients derive it from LocalTransform).
+ public int2 Cell;
+
+ /// Next action tick (server-only; turret cooldown now / next production tick in M7). 0 = inactive.
+ public uint NextTick;
+
+ /// Last tick this structure was processed (server-only; M7 offline-catch-up baseline). Stamped at spawn.
+ public uint LastProcessedTick;
+ }
+
+ ///
+ /// A buildable defense turret (the first structure). Hitscan: TurretFireSystem applies a direct
+ /// DamageEvent to the nearest in-range living Husk on cooldown (reusing
+ /// ) — reuses HealthApplyDamageSystem, no projectile/friendly-fire.
+ ///
+ public struct Turret : IComponentData
+ {
+ public float Range;
+ public int CooldownTicks;
+ public float Damage;
+ }
+
+ ///
+ /// 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
+ /// column to this element additively (the catalog is baked, not replicated → no format break).
+ ///
+ public struct StructureCatalogEntry : IBufferElementData
+ {
+ public byte Type;
+ public Entity Prefab;
+ public byte CostResourceId;
+ public int CostAmount;
+ }
+
+ /// Tag on the baked singleton carrying the buffer (the build cost/prefab table).
+ public struct StructureCatalog : IComponentData { }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs.meta b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs.meta
new file mode 100644
index 000000000..0c8db296b
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Building/StructureComponents.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 00d3379caf4807d4ebd97432848dd5d5
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/World/GoalProgress.cs b/Assets/_Project/Scripts/Simulation/World/GoalProgress.cs
new file mode 100644
index 000000000..19db3452f
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/GoalProgress.cs
@@ -0,0 +1,20 @@
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Long-arc progress toward the goal ("reach THEM"). Lives on the GLOBAL CycleDirector ghost (relevant in
+ /// every region, alongside CycleState + the resource ledger), so it is visible to all players regardless
+ /// of region. SINGLE writer: CyclePhaseSystem increments on each completed
+ /// cycle (Build -> Expedition). The HUD observes it for a progress bar.
+ ///
+ public struct GoalProgress : IComponentData
+ {
+ /// Accumulated progress.
+ [GhostField] public int Charge;
+
+ /// Charge required to reach the goal.
+ [GhostField] public int Target;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/World/GoalProgress.cs.meta b/Assets/_Project/Scripts/Simulation/World/GoalProgress.cs.meta
new file mode 100644
index 000000000..26c98439b
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/GoalProgress.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e1f60b3396850074ca0e44b831b5980c
\ No newline at end of file
diff --git a/Assets/_Project/Subscenes/Gameplay.unity b/Assets/_Project/Subscenes/Gameplay.unity
index 9bb12cc68..8255f51d9 100644
--- a/Assets/_Project/Subscenes/Gameplay.unity
+++ b/Assets/_Project/Subscenes/Gameplay.unity
@@ -273,6 +273,52 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &380046993
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 380046995}
+ - component: {fileID: 380046994}
+ m_Layer: 0
+ m_Name: StructureCatalog
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &380046994
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 380046993}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 40093ed42072f5a4889f5f62f510aa27, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.StructureCatalogAuthoring
+ TurretPrefab: {fileID: 3885353946372160549, guid: 5459c9edea89bd94fa6f5043ae00eb40, type: 3}
+ TurretCostOre: 10
+--- !u!4 &380046995
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 380046993}
+ 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: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &409538537
GameObject:
m_ObjectHideFlags: 0
@@ -1217,3 +1263,4 @@ SceneRoots:
- {fileID: 17637047}
- {fileID: 1192434518}
- {fileID: 236770154}
+ - {fileID: 380046995}
diff --git a/CLAUDE.md b/CLAUDE.md
index 5e1613a1c..d67d998ed 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -139,6 +139,10 @@ The M6 core-loop slice (Expedition→Defend→Build) + the base/expedition world
- **Run an adversarial design-review Workflow (3 critics: netcode/relevancy, determinism/prediction, reuse/scope → synthesize) BEFORE coding a netcode-heavy slice** — for M6 Stage 2 it caught every one of the above pre-implementation (relevancy trap, singleton collision, dt-trap, double-destroy, lazy-create hazard).
- **`manage_gameobject create` `component_properties` SILENTLY DROPS enum + Vector3 fields** (it set object-refs and simple scalars, but baked authoring enums/`Vector3` stayed at their C# defaults — two gates baked identical, one worked only by coincidence). **Always set those via a follow-up `manage_components set_property` (with a `properties` dict) and VERIFY through the `mcpforunity://scene/gameobject/{id}/component/{Type}` resource** (or, for a ghost, by reading the baked component in `execute_code` after Play). Same caveat applies to `manage_prefabs modify_contents` `component_properties`. Per-renderer color via `manage_material set_renderer_color` defaults to a runtime **PropertyBlock that does NOT persist into Play** — create a material asset (`manage_material create`) and `assign_material_to_renderer`, or use a prefab-stage assign, for colors that survive a domain reload.
- **Walk-in region gates (M6 visibility pass):** a baked `ExpeditionGate{FromRegion,ToRegion,Radius,ArrivalPos}` entity (visible primitive, collider stripped so you pass through) + a server `ExpeditionGateSystem` (plain group, `[UpdateAfter(CyclePhaseSystem)]`) proximity-transits a player whose `RegionTag` matches `FromRegion` (flip RegionTag + teleport to `ArrivalPos`, offset from the destination gate so no re-trigger). Returning to base mid-Expedition expires the cycle timer → Defend ("timer cap + early return"). The expedition is a *place* = cosmetic ground/pillars in **SampleScene** at the +1000 offset (classic URP, like SyntyWorld), not the DOTS subscene; gameplay nodes/gates are the baked subscene entities.
+- **Build/automation foundation (M6 Stage 3, the M7 contract):** generic `PlacedStructure{[GhostField] byte Type; int2 Cell (server-only); uint NextTick; uint LastProcessedTick}` on an ownerless interpolated ghost (`RegionTag{Base}`, world-owned, runtime-spawned). **Bake the two tick fields NOW** — the turret reuses `NextTick` as its fire cooldown, and they are the deterministic-offline-catch-up linchpin M7 needs and that can't be reconstructed retroactively. Only `Type` replicates (client derives `Cell` from `LocalTransform` via `BaseGridMath.WorldToCell`). Data-driven `StructureCatalog` buffer (`{byte Type; Entity Prefab; byte CostResourceId; int CostAmount}`, modeled on `AbilityPrefabElement`); M7 adds a recipe column additively. **Occupancy is DERIVED** by scanning live structure ghosts into a Temp `NativeHashSet` (structures are the source of truth — restart/replay-safe), NEVER a mutable buffer on the immutable baked `BaseAnchor`.
+- **Co-op placement atomicity:** `BuildPlaceSystem` commits the `StorageMath.Withdraw` + cell-reservation **IN-PLACE inside the RPC foreach** (only the `Instantiate` goes through the ECB) — the `StorageOpReceiveSystem` idiom — so two same-tick `BuildPlaceRequest`s for one cell can't both pass (validated: → exactly one structure + one withdraw). RPC carries `int CellX/CellZ` scalars, not `int2` (scalar-only RPC precedent).
+- **Buildable turret = hitscan = reversed `EnemyAISystem`:** snapshot living Husks, nearest-in-same-region-within-Range, on the `NextTick` cooldown append a direct `DamageEvent{Damage, SourceNetworkId=-1}` → reuses `HealthApplyDamageSystem` (despawns at HP≤0). NO projectile → no tunnelling, no friendly-fire/team model. Plain server group `[UpdateAfter(PredictedSimulationSystemGroup)]`.
+- **Resource-gated ability tiers reuse `StatModifier` — no new replicated component.** `AbilityUpgradeSystem` spends Aether and grows ONE `StatModifier{Target=Damage, Op=PercentAdd, SourceId=}` on the player (**replace-by-SourceId** so the `[InternalBufferCapacity(8)]` buffer stays bounded — repeated upgrades grow one row, not append); `StatRecomputeSystem` folds it into `EffectiveAbilityStats.Damage` on both worlds (the `UpgradePickup` path). `GoalProgress{[GhostField] int Charge, Target}` lives on the global CycleDirector ghost, single-writer in `CyclePhaseSystem`. **Disk-persistence writer is deferred to post-M7** (in-session-only state, per DR-008); freeze the save schema + bake the structure tick fields now so it's additive. See [[DR-014_M6_Build_Structures_Automation_Foundation]].
## Bootstrap & worlds
diff --git a/Docs/Vault/.obsidian/graph.json b/Docs/Vault/.obsidian/graph.json
new file mode 100644
index 000000000..a4617d854
--- /dev/null
+++ b/Docs/Vault/.obsidian/graph.json
@@ -0,0 +1,22 @@
+{
+ "collapse-filter": true,
+ "search": "",
+ "showTags": false,
+ "showAttachments": false,
+ "hideUnresolved": false,
+ "showOrphans": true,
+ "collapse-color-groups": true,
+ "colorGroups": [],
+ "collapse-display": true,
+ "showArrow": false,
+ "textFadeMultiplier": 0,
+ "nodeSizeMultiplier": 1,
+ "lineSizeMultiplier": 1,
+ "collapse-forces": true,
+ "centerStrength": 0.518713248970312,
+ "repelStrength": 10,
+ "linkStrength": 1,
+ "linkDistance": 250,
+ "scale": 1.0639049569280792,
+ "close": false
+}
\ No newline at end of file
diff --git a/Docs/Vault/01_Vision/Identity.md b/Docs/Vault/01_Vision/Identity.md
index 5dc6ffa58..19fb9cdf6 100644
--- a/Docs/Vault/01_Vision/Identity.md
+++ b/Docs/Vault/01_Vision/Identity.md
@@ -3,65 +3,109 @@ tags:
- vision
- identity
status: draft
-updated: 2026-06-02
+updated: 2026-06-03
permalink: gamevault/01-vision/identity
---
-# Game Identity — Frontier colony (sci-fi)
+# Game Identity — The Awakening Engine (Aether-colony sci-fi)
-> The fiction that turns the [[Pillars|mechanical pillars]] into a *place*. Decided 2026-06-02 to bridge the "tech-demo → game" gap. It is a **skin over the locked pillars — no mechanical rework**: every pillar already built (M1–M5) keeps its systems and gains a fictional role below.
+> The fiction that turns the [[Pillars|mechanical pillars]] into a *place*. Originally the "frontier colony" skin (2026-06-02, [[DR-009_GameFeel_Identity_FirstBlood]]); **evolved 2026-06-03** to absorb the magic + sci-fi premise (amnesiac Sleeper, a guiding voice, a goal called THEM) into one coherent world — recorded in [[DR-014_M6_Build_Structures_Automation_Foundation|the M6 build slice]] and the fiction-adoption decision. It remains a **skin over the locked pillars — no mechanical rework**: every M1–M6 system keeps its code and gains a fictional role. The frontier colony is *recontextualised, not replaced*.
## One-sentence pitch
-> **A 2–4 player co-op action-RPG about holding a frontier outpost on a hostile world: twin-stick combat against the Husk swarm, a shared base you build and automate, and production chains that keep running while you fight.**
+> **A 2–4 player co-op action-RPG about memory-wiped Sleepers who wake in a fallen Aether-colony and — urged on by a voice that is their own future self — harvest the corrupting Aether that powers everything to recharge their bodies, minds, and shared base, holding the line against the Husk swarm cycle by cycle as they push toward THEM.**
## Setting
-An off-world **colony foothold** — a small crew anchoring a survivable outpost on a frontier world that does not want them there. A spreading corruption — **the Blight** — turns drones, machinery, and fauna into aggressive **Husks** that probe and swarm the perimeter. The outpost is the safe anchor you grow and defend; beyond it lies the blighted frontier you push into for resources.
+An off-world **colony foothold gone dark**, found long after the lights went out. A crew came here to tap **AETHER** — a single luminous substance that is at once raw magical energy *and* the only fuel the colony's machines ever ran on — bled up from something buried beneath the regolith. They built a grounded near-future **industrial outpost** around it (fabricators, conveyors, assembler-drones, turrets, a perimeter, a hibernation bay) that ran on Aether the way ours runs on electricity. The strike worked — then the Aether began rewriting what it touched: uncontained, it soured the drones, the machinery, and the local fauna into the orange-lit **Husks** that now probe and swarm the perimeter, and it scrambled the crew, who went under in their pods.
-## Player fantasy
+You wake feeble and memory-blank inside a cracked hibernation-pod at the heart of the base. Beyond the outpost's safe anchor lies the **blighted frontier** — a procedurally shifting field where Aether wells up through glowing nodes — reachable only through the colony's Aether **gates**. The world reads as grounded industrial sci-fi where **Aether is the only fantastical thing**: every machine, weapon, and structure is a mundane engineered object running on it.
-A **frontier operator**: mobile, lethal twin-stick gunplay; you and your friends raise an outpost, wire up automation so the colony *compounds* even while you're out fighting, and push the perimeter back against the Husks. Setup is rewarded over grind (per the automation pillar).
+## Aether — the one substance (the magic/tech unifier)
-## Tone
+There is **no separate "magic system"** — Aether is the single thing that makes magic and tech read as one. It has **two states**, and the whole art palette now literally encodes them at a glance:
-Grounded near-future **industrial sci-fi** with an edge of cosmic/biotech corruption. Not cute, not horror — **tense frontier survival with a power-fantasy payoff**. Reference feel: *Helldivers* frontier grit × *Factorio* logistics satisfaction × *V Rising* base-growth loop × *Diablo / PoE* combat readability.
+- **Cyan/teal = Aether claimed and ordered** — energy, your abilities, owned structures, the Engine, the crew.
+- **Orange-red = Aether gone wild** — the **Blight**, the **Husks**, corrupted nodes.
+- **White-hot = the conversion moment** — impacts, channeling flares (where one becomes the other).
-## Art north-star
+This maps **1:1** onto every built system: your **abilities are charged Aether** (and tiering them is your operator-frame learning to channel more of it); the **Husks are corrupted Aether** animating the colony's own dead; your **turret burns Aether** to put the corruption back down. Resources: **AETHER** (the universal charge — powers/tiers abilities + charges structures), **ORE** (inert Aether-bearing colony alloy you mine to build), **BIOMASS** (Aether bound into living tissue — the Blight-saturated feedstock the colony's bio-fabricators refine; the reserved automation input). The **Blight is Aether without a will to shape it** — that single idea ties the threat to the economy instead of leaving it unexplained ambient menace.
-**Dark, readable, high-contrast.** A desaturated industrial world (metal, regolith, dim ambient) so that **bright emissive gameplay elements pop and bloom**:
+## The Echo, amnesia & THEM (the spine)
-- **Cyan / teal** → the player crew, friendly energy, owned structures.
-- **Hostile orange / red** → Husks and Blight corruption.
-- **White-hot** → impacts, muzzle flashes, deaths.
+You wake amnesiac inside the **Awakening Engine** — the home base, reframed as a cracked, half-charged hibernation/restoration pod — because Aether overexposure scrambled you when the colony fell. The pod now **reassembles you from the Aether you feed it: memory and ability return together as the charge rises** (this is *why* you start feeble and *why* the loop makes you whole — capability and comprehension climb on one curve).
-Silhouette-first readability (ARPG legibility from the top-down frame), bloom on emissives, vignette to focus the frame, ACES tonemapping. **Primitives + emissive + post-processing now**; stylized low-poly hard-surface (crew/structures) and corrupted biomech (Husks) later. The palette is the identity even before real models.
+A voice speaks in your ear — **the Echo** — and it is, ambiguously, an imprint of *your own future self* in the Aether (which the colony used for communion before it darkened), reaching back along the substance because it has already lived this and believes the only way out is forward. **It is deliberately not fully trusted**: reassuring and pushy by turns, urging you toward **THEM** "by any means." Its motives are held a half-step unresolved — is it saving you, or using your hands? — and that ambiguity is the dramatic engine (locked: *ambiguous*, with room for a payoff beat at the goal).
+
+**THEM keeps three live readings** (locked: *kept open*, arbitrated by a later narrative milestone), all co-op-coherent: THEM = the **Wellspring** (the buried source of the Aether the colony woke — the literal goal: charge the Engine enough to open a stable deep gate to it); THEM = the **lost crew** still under their pods; or THEM = the **Echo itself**, a self that wants to be made whole. Because every charge restores a fragment of memory alongside power, the reveal is *metered by the same charge you spend to grow*.
+
+## The Aether Cycle, in-fiction
+
+The loop ([[DR-013_M6_Aether_Cycle_Region_Split]]) is the Engine's recharge rhythm — **zero mechanical change**, three diegetic beats:
+
+- **Sortie (Expedition):** step through a **Wake-Gate** into the **Blightfield** and harvest Aether/Ore/Biomass from glowing nodes. Drawing that much raw Aether is *loud* — a beacon the Blight senses — so the soft incursion timer is "attention spikes": harvest still thins the local wild concentration, but the longer you channel the more Husks converge, and you return through a **Recall-Gate** before the swarm follows you home.
+- **Siege (Defend):** the Husks you provoked mount a wave on the anchor; you and your built turrets (burning Aether to put the corruption down) hold the perimeter.
+- **Forge (Build/Charge):** in the lull you spend the one shared ledger at the Engine — placing/tiering colony structures (Ore) and charging your abilities (Aether) from a single economy, with the surviving Aether-machines harvesting/fabricating while you rest.
+
+**Why it repeats + escalates:** the Engine recharges in stages, each completed cycle banks one increment toward THEM (the **"ENGINE CHARGE → THEM"** goal meter), but one cycle's Aether is never enough — the Blight regrows between charges, so the frontier is re-raided, each pass reaching deeper into denser Blight that draws bigger waves. The escalation *is* the Blight waking exactly as you do (mechanically the escalating `WaveSystem`). The two regions are "inside the Engine's Aether-bubble" vs "out in the wild Blight"; the gates are the only seam. It terminates when the meter fills and the Engine opens the deep gate to the Wellspring.
## The enemy — Husks
-Corrupted rogue drones/lifeforms driven to swarm the colony. They are the **threat that makes combat *combat*** and gives the base a reason to exist.
+Corrupted rogue drones / machinery / fauna — **the colony's own dead, animated by wild Aether** — driven to swarm the charge banked in the base. They are the threat that makes combat *combat* and gives the base a reason to exist.
-- **First Husk (this slice):** a melee charger — seeks the nearest operator, closes, strikes. Server-authoritative interpolated ghost.
-- **Later:** ranged "spitters", armored "brutes", a "broodmaker" spawner, and eventually a corrupted-fabricator boss. Wave/threat director over time.
+- **First Husk (this slice):** a melee charger — seeks the nearest operator, closes, strikes. Server-authoritative interpolated ghost; escalating `WaveSystem` threat director.
+- **Later:** ranged "spitters", armored "brutes", a "broodmaker" spawner, and eventually a corrupted-fabricator boss (a colony machine the Blight fully claimed).
## Automation flavor
-Colony **fabricators, conveyors, assembler drones, and power** — the self-running production chains of the locked automation pillar, fictionalized as the outpost's industry that keeps growing while the crew fights and expeditions.
+Colony **fabricators, conveyors, assembler-drones, and power** — the self-running production chains of the locked automation pillar — are the outpost's **surviving Aether-industry you reboot**. The reserved Harvester / Fabricator / Conveyor structure codes ([[DR-014_M6_Build_Structures_Automation_Foundation]]) are that industry; their fiction comes for free (Aether-powered machines that compound the colony's output while the crew fights and raids).
+
+## Art north-star (kept — now meaningful)
+
+**Dark, readable, high-contrast** (unchanged from [[DR-009_GameFeel_Identity_FirstBlood]] at the pixel level — only *re-labelled* as Aether-state): a desaturated industrial world so bright emissive gameplay elements pop and bloom; ACES tonemapping (URP HDR color grading), vignette, silhouette-first ARPG legibility; the **cyan = ordered / orange-red = wild Blight / white-hot = conversion** palette now *encodes contained-vs-wild Aether*. The Synty colony world (cosmetic SampleScene) + GabrielAguiar VFX are integrated.
+
+**Additive as art lands (none required to adopt this identity):** the **Awakening Engine** as the cyan-cored hero set-piece of the base whose charge visibly brightens with the goal meter (a diegetic readout); an **Aether-veining** motif (cyan capillaries on tamed machines, the *same* veins in angry orange across Husks/corrupted terrain — one visual language for the two states); stronger cyan portal shimmer on the gates; cyan/orange node glow; optional memory-fragment motes that flash a voice line on harvest.
+
+## Audio / UI identity
+
+Industrial synth, weighty impacts, a low hostile drone for Husk proximity. **Additive:** the **Echo** — an intimate, close-mic, slightly Aether-modulated version of the player's *own* voice for guiding lines, loop-transition prompts, and the THEM mystery (each Sleeper has their own Echo in co-op — locked: *per-Sleeper*); a faint Aether **hum** that rises near nodes/the Engine, soured under the Blight (same source, corrupted). UI stays the clean diegetic **colony-terminal** HUD, re-themed as the **Engine's diagnostic panel** (health, ability charge, Husk threat, cycle phase/countdown, location/gate-hint, and the goal bar labelled **"ENGINE CHARGE → THEM"** with a "memory restored %" tie-in). Voice acting + memory-beat writing is **net-new content** — playable with subtitle/text lines first.
+
+## The lexicon
+
+| Thing | Name | Code anchor |
+|---|---|---|
+| Player role | **Sleeper** (operator-class: *Revenant*) | the player ghost |
+| The guiding voice | **the Echo** (your Echo) | client presentation (no sim surface) |
+| The goal / "THEM" | **the Wellspring** (the deep gate to THEM) | `GoalProgress` → `"ENGINE CHARGE → THEM"` |
+| The base | **the Awakening Engine** (the *Anchor*) | `BaseAnchor` + the grid + shared ledger |
+| Expedition field | **the Blightfield** (the *Wild*) | the Expedition region |
+| The gates | **Aether Gates** — *Wake-Gate* (out) / *Recall-Gate* (back) | `ExpeditionGate` |
+| The loop | **the Aether Cycle** — *Sortie / Siege / Forge* | `CycleState` phases |
## How it maps to the locked pillars (no rework)
| Pillar ([[Pillars]]) | Fictional skin |
|---|---|
-| Action-ARPG combat (twin-stick) | Frontier operator gunplay vs. the Husk swarm |
-| Co-op base power fantasy (V Rising feel) | The crew's outpost — built, defended, grown together |
-| Automation as progression | Colony fabricators/drones/conveyors compounding output |
-| Persistent base + instanced expeditions | The safe **outpost anchor** (`BaseAnchor`) vs. the blighted **frontier** beyond |
+| Action-ARPG combat (twin-stick) | A Sleeper channelling charged Aether vs. the Husk swarm |
+| Co-op base power fantasy (V Rising feel) | A crew of Sleepers reawakening + defending one shared Awakening Engine |
+| Automation as progression | The colony's surviving Aether-machines compounding output while you raid |
+| Persistent base + instanced expeditions | The Engine's safe Aether-bubble (`BaseAnchor`) vs. the blighted **Wild** beyond |
-## Audio / UI identity (direction)
+## Locked narrative choices (2026-06-03)
-Industrial synth, weighty impacts, a low hostile drone for Husk proximity. UI reads as a **clean diegetic colony-terminal** — functional HUD (health, ability charge, threat count), neon-on-dark.
+- **The Echo is ambiguous** — never fully trusted; "by any means"; needs a payoff beat at the goal.
+- **THEM stays open** — three live readings (Wellspring / lost crew / the Echo), arbitrated later.
+- **Aether-as-corruption is atmospheric only** — no stat penalty for upgrading (honours the skill-expression pillar); a literal corruption/risk meter is a possible *future net-new mechanic*, not part of this identity.
+- **Per-Sleeper Echo** in co-op — each player's own future-self.
+
+## Open / deferred (content scope, not mechanics)
+
+- The **narrative payoff** — the goal meter currently fills (`Target=10`) with no authored ending; staged **memory beats tied to charge milestones** + the Echo's voice lines are net-new *content/writing* (playable as subtitles first), tracked for a later narrative milestone — NOT a systems change.
+- A literal **corruption/risk meter**, **memory-gated abilities**, or a real **ending payoff** are net-new design for a future milestone if ever pursued.
+- The recharging pod stays a **persistent anchor** (death = respawn), never a roguelike run-reset (locked persistent-base pillar / [[DR-013_M6_Aether_Cycle_Region_Split]]).
## Related
-- [[Pillars]] — locked mechanical decisions this fiction skins.
-- Established in the "First Blood" game-feel slice (2026-06-02) — see the session log + decision record for the presentation/enemy architecture that first expresses this identity.
+- [[Pillars]] — the locked mechanical decisions this fiction skins.
+- [[DR-009_GameFeel_Identity_FirstBlood]] — the original frontier-colony identity this evolves.
+- [[DR-013_M6_Aether_Cycle_Region_Split]] / [[DR-014_M6_Build_Structures_Automation_Foundation]] — the M6 systems this fiction reads onto.
diff --git a/Docs/Vault/01_Vision/Pillars.md b/Docs/Vault/01_Vision/Pillars.md
index 01540733b..928414355 100644
--- a/Docs/Vault/01_Vision/Pillars.md
+++ b/Docs/Vault/01_Vision/Pillars.md
@@ -28,5 +28,5 @@ permalink: gamevault/01-vision/pillars
## Related
-- [[Identity]] — the **fiction** that skins these pillars (sci-fi frontier colony), decided 2026-06-02. No mechanical rework; adds setting, tone, art north-star, and the Husk enemy.
+- [[Identity]] — the **fiction** that skins these pillars: **The Awakening Engine** (Aether-colony sci-fi), evolved 2026-06-03 ([[DR-015_The_Awakening_Engine_Fiction_Adoption]]) from the original frontier-colony skin ([[DR-009_GameFeel_Identity_FirstBlood]], 2026-06-02). Skin-only — no mechanical rework; **Aether** is the one substance unifying magic + tech (cyan = ordered / orange = wild Blight), with an amnesiac-**Sleeper** / **Echo** (the guiding voice) / **THEM** spine.
- Decision records: `07_Sessions/_Decisions/`
\ No newline at end of file
diff --git a/Docs/Vault/06_Roadmap/Milestones.md b/Docs/Vault/06_Roadmap/Milestones.md
index 13009ff8d..47615493c 100644
--- a/Docs/Vault/06_Roadmap/Milestones.md
+++ b/Docs/Vault/06_Roadmap/Milestones.md
@@ -19,7 +19,7 @@ permalink: gamevault/06-roadmap/milestones
| **M5.5 — Game feel & identity** | Bridge "tech-demo → game": the **Husk** enemy (server AI, interpolated ghost), player death/respawn, combat juice (damage numbers/VFX/SFX/camera shake), a core HUD, and a sci-fi look pass — under the new fiction ([[Identity]], sci-fi frontier colony) | ✅ Done 2026-06-02 — runtime-validated on 6.4.7: Husks spawn(6)+replicate+chase+strike; death→respawn loop; HUD (health/cooldown/threat/downed); emissive dark-sci-fi look. EditMode **74/74**. ctx7-verified APIs. **Deepened same day:** auto-target on Husks, replicated respawn-invulnerability, and a `WaveSystem` threat director (escalating waves of 3 Husk variants — Grunt/Swarmer/Brute) replacing the flat sustain — runtime-validated (wave 1→2 escalation 4→6, distinct maxHP 30/15/80). [[DR-009_GameFeel_Identity_FirstBlood]], [[2026-06-02_GameFeel_Identity]], [[2026-06-02_GameFeel_Deepening]] |
| **— 2026-06-03 Visual & controls polish —** | Non-milestone polish layered on M5.5 (no mechanical rework): HDRP→URP art import + reusable converter; a cohesive **Synty** sci-fi colony world (cosmetic SampleScene GameObjects) + **GabrielAguiar** combat VFX; **KBM mouse-cursor aim + gamepad aim** with last-actuation device auto-switch (rides the existing `PlayerInput.Aim` ghost field). | ✅ Done 2026-06-03 — [[DR-010_Art_Import_URP_Conversion_Visual_Upgrade]], [[DR-011_Synty_World_VFX_Integration]], [[DR-012_Aim_Controls_Cursor_Gamepad]] |
| **— 2026-06-03 Pre-M6 cleanup —** | Loose-ends pass before M6: vault roadmap reconcile, Unity-template + orphaned-material removal, rate-limited turning, console/runtime health gate. | ✅ Done 2026-06-03 — [[2026-06-03_Pre_M6_Cleanup]] |
-| **M6 — The Aether Cycle (core loop)** | Reframed from "grid build placement" into the first vertical slice of the **core game loop**: Expedition (gather) → Defend (wave) → Build/Charge (spend), persistent base + procedural sorties, escalating toward a goal. Build placement is now Stage 3 of this milestone. | 🚧 In progress 2026-06-03 — **Stages 0–2 done + runtime-validated** on 6.4.7: **base/expedition split via coordinate-region + `GhostRelevancy`** (player transit despawns/re-grants the other region's ghosts; server==client); a **server phase-director** (Expedition→Defend→Build→Expedition auto-cycle, cycle 1→2, Husk `WaveSystem` only in Defend, escalation 4→6); and **resources + harvest** — a **global CycleDirector ghost** carrying the replicated `CycleState` + a shared resource **ledger** (relevant in every region, unlike the base storage), a procedural **expedition field** (8 resource-node ghosts seeded per cycle, region-scoped), and a tunnel-safe **harvest** sweep depositing into the ledger; client **HUD** shows phase + resource counts. Supersedes DR-008's "split requires streaming" framing. **Stages 3–4 (build placement/turret/ability-tiers, persistence/goal) = continuation.** — [[DR-013_M6_Aether_Cycle_Region_Split]], [[2026-06-03_M6_Aether_Cycle_CoreLoop]] |
+| **M6 — The Aether Cycle (core loop)** | Reframed from "grid build placement" into the first vertical slice of the **core game loop**: Expedition (gather) → Defend (wave) → Build/Charge (spend), persistent base + procedural sorties, escalating toward a goal. Build placement is now Stage 3 of this milestone. | 🚧 In progress 2026-06-03 — **Stages 0–4 done + runtime-validated** on 6.4.7 (M6 core loop systems complete): **base/expedition split via coordinate-region + `GhostRelevancy`** (player transit despawns/re-grants the other region's ghosts; server==client); a **server phase-director** (Expedition→Defend→Build→Expedition auto-cycle, cycle 1→2, Husk `WaveSystem` only in Defend, escalation 4→6); and **resources + harvest** — a **global CycleDirector ghost** carrying the replicated `CycleState` + a shared resource **ledger** (relevant in every region, unlike the base storage), a procedural **expedition field** (8 resource-node ghosts seeded per cycle, region-scoped), and a tunnel-safe **harvest** sweep depositing into the ledger; client **HUD** shows phase + resource counts. Supersedes DR-008's "split requires streaming" framing. **Stage 3** (generic automation-ready **structure model** + data-driven catalog + grid **build-placement** RPC with co-op-atomic commit + a hitscan **turret** that auto-defends + **ability tiers** via a bounded StatModifier) and **Stage 4 goal meter** are **done + validated** (turret placed/Ore-deducted/replicated; two same-tick requests → one build; turrets killed the wave; ability damage 20→30 bounded; goal increments per cycle). Disk-persistence **writer deferred to post-M7** (M7-additive surface — tick fields + frozen schema — baked now); the structure model is the M7 production-chain foundation. Playable walk-in-gate loop with build/spend, visible in the HUD. — [[DR-014_M6_Build_Structures_Automation_Foundation]] — [[DR-013_M6_Aether_Cycle_Region_Split]], [[2026-06-03_M6_Aether_Cycle_CoreLoop]] |
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
Promote items from [[Backlog]] here when committed.
\ No newline at end of file
diff --git a/Docs/Vault/07_Sessions/2026/2026-06-03_M6_Aether_Cycle_CoreLoop.md b/Docs/Vault/07_Sessions/2026/2026-06-03_M6_Aether_Cycle_CoreLoop.md
index 2f14139c7..5d921bcc0 100644
--- a/Docs/Vault/07_Sessions/2026/2026-06-03_M6_Aether_Cycle_CoreLoop.md
+++ b/Docs/Vault/07_Sessions/2026/2026-06-03_M6_Aether_Cycle_CoreLoop.md
@@ -76,6 +76,33 @@ the return gate comes back to base AND starts Defend early; HUD reads phase/coun
(both gates baked with authoring defaults — the BaseGate worked only by coincidence); fixed with
`manage_components set_property` + verified via the `mcpforunity://scene/gameobject/{id}/component/...` resource.
+**Stage 3 + 4 — build/structures (automation-ready) + turret + ability tiers + goal (DONE + validated).**
+Adversarially design-reviewed first (3 critics incl. a dedicated M7-automation-readiness lens; caught the co-op
+double-spend, the BaseAnchor-occupancy-cache contradiction, the persistence/offline-catch-up entanglement).
+Built a GENERIC structure foundation per the operator's automation-forward directive: `PlacedStructure`
+([GhostField] Type byte + server-only Cell/NextTick/LastProcessedTick — the tick fields are the M7 offline-
+catch-up linchpin, baked now), a data-driven `StructureCatalog` buffer, occupancy DERIVED from live ghosts,
+`BuildPlaceRequest` RPC with in-place ledger+occupancy commit (co-op-atomic), a hitscan `Turret` (reversed
+`EnemyAISystem` → direct `DamageEvent`), ability tiers via a growing `StatModifier` (replace-by-SourceId), a
+`GoalProgress` meter on the CycleDirector ghost, and HUD goal bar. **Validated headless + screenshots:** turret
+placed at cell (10,10) (Ore 50→40, replicated); two same-tick requests for one cell → one structure + one
+withdraw; built turrets auto-killed all Husks; ability damage 20→25→30 with the buffer bounded to one row;
+goal increments per cycle (HUD "GOAL n/10"). **Persistence disk-writer DEFERRED to post-M7** (schema frozen +
+tick fields baked so it's additive). Full architecture + the M7 contract: [[DR-014_M6_Build_Structures_Automation_Foundation]].
+
+**Fiction reconciliation — "The Awakening Engine" (DONE).** A judge-panel workflow (3 framings: Aether-as-Blight /
+Reawakened-Operator / unreliable-Voice → synthesis) reconciled the magic-tech/amnesia/voice/THEM premise with the
+locked frontier-colony identity into ONE coherent, skin-only world (no mechanical rework). Core: **Aether is the
+single substance** (cyan = claimed/ordered, orange-red = wild **Blight**, white-hot = conversion) powering every
+machine/ability/structure and, uncontained, animating the colony's dead into **Husks** — it maps 1:1 onto the
+built economy + loop. The base = the **Awakening Engine** (recharge pod restoring memory+power from deposited
+Aether); the voice = **the Echo** (your future-self imprint); the goal = **the Wellspring** ("ENGINE CHARGE →
+THEM"); phases re-named **Sortie/Siege/Forge**; field = **the Blightfield**; gates = **Wake-/Recall-Gate**.
+Operator locked four forks: Echo **ambiguous**, THEM **kept open** (3 readings), corruption **atmospheric-only**,
+**per-Sleeper** Echo. Rewrote [[Identity]], recorded [[DR-015_The_Awakening_Engine_Fiction_Adoption]], updated
+[[Pillars]] — resolves the DR-013 fiction-reconciliation open item. Narrative payoff (memory beats + Echo VO,
+subtitle-first) is net-new content for a later milestone, not a systems change.
+
See [[DR-013_M6_Aether_Cycle_Region_Split]] for the full architecture + validated evidence.
## Decisions
diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-014_M6_Build_Structures_Automation_Foundation.md b/Docs/Vault/07_Sessions/_Decisions/DR-014_M6_Build_Structures_Automation_Foundation.md
new file mode 100644
index 000000000..a91dc1ec4
--- /dev/null
+++ b/Docs/Vault/07_Sessions/_Decisions/DR-014_M6_Build_Structures_Automation_Foundation.md
@@ -0,0 +1,69 @@
+---
+id: DR-014
+title: M6 Stage 3/4 — generic build/structure foundation (automation-ready) + turret + ability tiers + goal; persistence deferred
+status: accepted
+date: 2026-06-03
+tags:
+- decision
+- netcode
+- building
+- automation
+- structures
+- m6
+- m7
+permalink: gamevault/07-sessions/decisions/dr-014-m6-build-structures-automation-foundation
+---
+
+# DR-014 — M6 Stage 3/4: build/structure foundation (automation-ready) + turret + ability tiers + goal
+
+## Context
+
+M6 Stages 0–2 + the visibility pass delivered the playable core loop (region split, cycle phases, resources +
+harvest, walk-in gates, HUD). Stage 3 is the **"spend" half** — the original M6 "build placement" — plus a
+buildable defense (turret) and a resource sink for ability power; Stage 4 is the **goal meter** + (originally)
+disk persistence. The operator directive: *"design these next systems with the scope that eventually we are
+going to have some basic automation aspects in this"* — so the STRUCTURE model is the foundation the locked
+**automation pillar** ([[Pillars]], M7 production chains) builds on, and must extend to
+harvesters/fabricators/conveyors/recipes/tick-production/offline-catch-up **without a rework**. The operator
+will make game-design decisions *after* the technical foundation is solid.
+
+A 3-critic + synthesis design-review workflow (netcode / determinism / **M7-automation-readiness**) pressure-
+tested the design pre-code and caught real issues (the co-op placement double-spend, the BaseAnchor-occupancy-
+cache contradiction, the ability-modifier path, the persistence/offline-catch-up entanglement). See
+[[2026-06-03_M6_Aether_Cycle_CoreLoop]].
+
+## Decision
+
+1. **Generic structure model (the automation foundation).** `PlacedStructure { [GhostField] byte Type; int2 Cell; uint NextTick; uint LastProcessedTick }` on an ownerless interpolated ghost (`RegionTag{Base}`, world-owned, runtime-spawned). Only **Type** replicates (a cheap byte for client visual branching); **Cell** is server-only (clients derive it from the replicated `LocalTransform` via `BaseGridMath.WorldToCell`). **`NextTick`/`LastProcessedTick`** are server-only raw `NetworkTick` values (`TickUtil.NonZero`-guarded) — the turret reuses `NextTick` as its fire cooldown NOW, and they are the **M7 linchpin**: the next-production-tick + the deterministic offline-catch-up baseline (`produced = floor((now − LastProcessedTick)/period)`). These two tick fields are baked now because they are the timing identity that **cannot be reconstructed retroactively**. `StructureType` byte consts `{None=0, Turret=1}` with **Harvester=2/Fabricator=3/Conveyor=4 reserved** (free).
+2. **Data-driven catalog.** A baked `StructureCatalog` singleton carrying a `StructureCatalogEntry { byte Type; Entity Prefab; byte CostResourceId; int CostAmount }` buffer (modeled on `AbilityPrefabElement` — prefab via `GetEntity`, never a blob). M7 adds a recipe column to the element additively (baked, not replicated → no format break). Authoring is flat fields for the single turret entry now (the MCP `component_properties` enum/array set is unreliable — see gotcha); the runtime buffer is already the extensible shape.
+3. **Occupancy DERIVED, not cached.** `BuildPlaceSystem` (server-only, runs on a placement RPC) scans live `PlacedStructure` ghosts into a Temp `NativeHashSet` — structures are the source of truth (restart-/replay-order-safe). NOT a mutable buffer on the immutable baked `BaseAnchor`. Pure `BuildPlacementMath.CanPlace(anchor, occupied, cell)` (= `IsCellInPlot` && !occupied), unit-test-ready.
+4. **Placement = `BuildPlaceRequest` RPC `{ byte StructureType; int CellX; int CellZ }`** (scalar cells, NOT `int2`, per the scalar-only RPC precedent). `BuildPlaceSystem` validates catalog→legality→occupancy→cost and **commits IN-PLACE** (`StorageMath.Withdraw` on the global ledger + reserve the cell in the set) so two same-tick requests for one cell can't both pass — the `StorageOpReceiveSystem` in-place idiom; only the `Instantiate` goes through the ECB. Plain server `SimulationSystemGroup` (not predicted).
+5. **Turret = hitscan (reversed `EnemyAISystem`).** `Turret { float Range; int CooldownTicks; float Damage }`; `TurretFireSystem` snapshots living Husks, picks the nearest in its region within Range, and on a `NextTick` cooldown appends a direct `DamageEvent{Damage, SourceNetworkId=-1}` — reuses `HealthApplyDamageSystem` (already despawns at HP≤0), **no projectile/tunnelling/friendly-fire**. Self-gates (Husks exist only in Defend).
+6. **Ability tiers via `StatModifier` (no new replicated component).** `AbilityUpgradeRequest` RPC → `AbilityUpgradeSystem` spends fixed Aether and grows a single `StatModifier{Target=Damage, Op=PercentAdd, SourceId=}` on the player (**replace-by-SourceId** so the `[InternalBufferCapacity(8)]` buffer stays bounded). `StatRecomputeSystem` folds it into `EffectiveAbilityStats.Damage` on both worlds — same path as `UpgradePickup`.
+7. **Goal meter shipped; persistence WRITER deferred.** `GoalProgress { [GhostField] int Charge; int Target }` on the global CycleDirector ghost, **single-writer** in `CyclePhaseSystem` (increment per completed cycle); HUD shows a bar. The disk-persistence writer is **deferred to a post-M7 slice** (deepest M7 entanglement; in-session-only state, consistent with DR-008's deferral) — but the M7-additive surface is locked NOW: the tick fields (§1), the data-driven catalog (§2), occupancy-derive (§3), and a **frozen save schema** `{ cycleNumber, savedServerTick, ledger[] absolute, structures[]{type,cell,nextTick,lastProcessedTick, RESERVED io-rows slot}, abilityModifiers[] }` so M7 needs no format version bump.
+
+## Consequences
+
+- **Validated at runtime on 6.4.7 (single in-editor client), headless via `execute_code` + screenshots:**
+ - **Build:** turret placed at cell (10,10) → world (−5.5,1,−5.5) (correct snap, baked Scale preserved), Ore 50→40, replicated to the client.
+ - **Co-op atomicity:** two same-tick `BuildPlaceRequest` for one cell → exactly **one** structure + **one** withdraw (Ore 40→30) — the named double-spend blocker is fixed.
+ - **Turret defense:** built turrets auto-acquired + **killed all spawned Husks** (hitscan `DamageEvent` → death → despawn); both turrets' `NextTick` cooldowns advanced independently.
+ - **Ability tiers:** damage 20→25→**30** across two upgrades; the `StatModifier` buffer stayed at **one** row (replace-by-SourceId grew 0.25→0.5); Aether 50→30→10.
+ - **Goal:** `GoalProgress.Charge` increments per cycle; HUD reads **"GOAL n / 10"** from the replicated value.
+ - All systems visible in the HUD (phase/countdown/cycle/resources/location/gate-hint/goal/husks/health). Console clean of code/Burst/RPC errors.
+- **No new asmdefs.** New code under `…/Building/` (Simulation/Server/Client) + `…/World/GoalProgress.cs`; reuses `EnemyAISystem`/`EnemyAIMath` (turret), `StorageMath`+global ledger (cost), `BaseGridMath` (grid), `StatModifier`/`StatRecomputeSystem` (tiers), the runtime-ghost + duplicate-`UpgradePickup` prefab recipe, the byte-RPC pattern.
+- **This is the M7 contract.** Production chains add: a recipe column on `StructureCatalogEntry`; per-structure `StorageEntry` I/O buffers on harvester/fabricator prefabs only; a `StructureProductionSystem` ticking recipes off `NextTick` + computing offline catch-up off `LastProcessedTick`; conveyor adjacency/topology — all **additive** (new `StructureType` codes + components), no migration of the turret ghost or the frozen schema.
+
+## Open / deferred (mostly M7)
+
+- **Disk-persistence writer + `BaseRestoreSystem`** — post-M7 slice. Restore replays structures through a charge-free placement path (extract from `BuildPlaceSystem` then), restores the ledger as an ABSOLUTE set (no re-`Withdraw`), and **rebases ticks** (saved ticks are relative to the old `ServerTick` origin — store as deltas-from-`savedServerTick`).
+- **Per-structure I/O buffers, recipe field, `StructureProductionSystem`, conveyor/harvester/fabricator + adjacency graph** — M7 (type-codes reserved; tick fields baked).
+- **Ghost-relevancy ceiling** — the unspawned/transiting "sees-everything" fallback in `RegionRelevancySystem` + its O(ghosts×connections)/tick scan become load-bearing at M7 structure counts; redesign deferred (document the N×M ceiling).
+- **Client build UX** — currently `B` builds a turret at the local player's cell + an editor static; a mouse-cell-preview build mode (reusing `AimMath`/`AimReticleSystem`) is a polish pass.
+- **Visuals** — turret reuses the pickup glow material; per-type structure + per-type resource-node colors are a polish pass (operator's game-design decisions come after the foundation).
+
+## Build gotcha recorded this session
+
+- **`manage_gameobject create` `component_properties` (and `manage_prefabs modify_contents` `component_properties`) SILENTLY DROP enum + Vector3 + nested-array fields** (object-refs + simple scalars apply; baked authoring enums/arrays stay at C# defaults). Set those via a follow-up `manage_components set_property` (with a `properties` dict for scalars / `value` for one) and VERIFY via the `mcpforunity://scene/gameobject/{id}/component/{Type}` resource. For authoring with enum/array config, prefer **flat scalar fields + byte consts in the baker** over inspector enums/arrays. (First hit on the M6 gates; re-confirmed on the structure catalog.)
+
+Builds on [[DR-013_M6_Aether_Cycle_Region_Split]] (the cycle/ledger/CycleDirector + region split this extends), reuses the patterns from [[DR-008_M5_HomeBase_BaseLayer_Storage]] (grid, RPC, runtime ghost) / [[DR-009_GameFeel_Identity_FirstBlood]] (enemy AI, DamageEvent) / [[DR-004_M3_DataDriven_Abilities_Modifiers]] (StatModifier). Serves the automation pillar in [[Pillars]].
diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-015_The_Awakening_Engine_Fiction_Adoption.md b/Docs/Vault/07_Sessions/_Decisions/DR-015_The_Awakening_Engine_Fiction_Adoption.md
new file mode 100644
index 000000000..d2d441a41
--- /dev/null
+++ b/Docs/Vault/07_Sessions/_Decisions/DR-015_The_Awakening_Engine_Fiction_Adoption.md
@@ -0,0 +1,58 @@
+---
+id: DR-015
+title: The Awakening Engine — fiction reconciliation (Aether unifier + Sleeper/Echo/THEM spine), skin-only
+status: accepted
+date: 2026-06-03
+tags:
+- decision
+- identity
+- narrative
+- fiction
+- vision
+- m6
+permalink: gamevault/07-sessions/decisions/dr-015-the-awakening-engine-fiction-adoption
+---
+
+# DR-015 — The Awakening Engine: fiction reconciliation (skin-only)
+
+## Context
+
+The M6 design pass introduced a new narrative direction (amnesiac protagonist; a voice in your ear; reach
+"THEM by any means"; a magic + sci-fi world of "magic-powered machines"; harvest magical energy to build/charge;
+procedural expeditions; periodic base defense; the "Awakening Engine" lean) that sat alongside — and partly in
+tension with — the **locked frontier-colony identity** ([[Identity]] / [[DR-009_GameFeel_Identity_FirstBlood]]:
+industrial sci-fi outpost, the Blight, Husks, cyan/orange palette, automation). [[DR-013_M6_Aether_Cycle_Region_Split]]
+explicitly deferred the **fiction reconciliation** pending operator sign-off. By the time of this decision the
+codebase had real fiction-bearing systems built + validated (Aether/Ore/Biomass resources, the Aether Cycle
+loop, the base↔expedition region split, walk-in gates, Husk waves, buildable turrets, ability tiers, a goal
+meter toward THEM) — so the fiction had to reconcile into **one coherent world that reads onto everything built**
+without contradicting the locked **mechanical** pillars ([[Pillars]]).
+
+A judge-panel design workflow generated three reconciliation framings (Aether-as-Blight / Reawakened-Operator /
+unreliable-Voice) and synthesised the strongest, honouring the operator's chosen "Awakening Engine" lean. The
+operator then locked four narrative forks. See [[2026-06-03_M6_Aether_Cycle_CoreLoop]].
+
+## Decision
+
+**Adopt "The Awakening Engine" as the game's fiction — a pure SKIN-ONLY reconciliation (no mechanical rework; DR-009/013/014 code untouched).** The full identity is in [[Identity]]. The reconciliation rests on five moves:
+
+1. **Aether is the single unifying substance — the only fantastical thing.** Raw magical energy the colony's mundane sci-fi machines were built to burn (abilities, turrets, gates, fabricators, the pod all run on it like electricity). It has **two states the existing palette already encodes**: cyan = Aether claimed/ordered (you, owned structures, the Engine); orange-red = Aether gone wild (the **Blight**, the **Husks**, corrupted nodes); white-hot = the conversion. This maps 1:1 onto the built economy: **Aether** (id 1) charges/tiers abilities + structures, **Ore** (id 2) builds, **Biomass** (id 3) is the automation feedstock. The palette is **re-labelled, not repainted** — no art change to adopt.
+2. **The Blight is uncontained Aether overcharge** animating the colony's own dead drones/machinery/fauna into Husks — tying the threat to the resource economy instead of leaving it unexplained.
+3. **The base = the Awakening Engine** (a hibernation/restoration pod): deposited Aether restores the Sleeper's memory **and** power together (why you start feeble and the loop makes you whole — capability and comprehension on one curve), powers the colony, and banks goal charge. The single shared ledger is save-state + XP + fuel at once. (`BaseAnchor` ⇒ "the Anchor".)
+4. **The spine = a Sleeper, the Echo, and THEM.** You wake amnesiac (Aether-scrambled); the **Echo** is an imprint of your own future self in the Aether, guiding you toward THEM. The Aether Cycle reads as the Engine's recharge rhythm (phases **Sortie / Siege / Forge**); the incursion timer = harvesting is "loud" (a beacon — *local concentration drops, global attention spikes*, so intuition never fights the systems); the regions = the Engine's Aether-bubble vs **the Blightfield**; gates = **Wake-Gate / Recall-Gate**; the goal meter = **"ENGINE CHARGE → THEM"** → the **Wellspring**.
+5. **Four narrative forks locked by the operator:** (a) **the Echo is AMBIGUOUS** (never fully trusted; needs a payoff beat at the goal); (b) **THEM stays OPEN** — three live readings (Wellspring / lost crew / the Echo), arbitrated by a later milestone; (c) **Aether-as-corruption is ATMOSPHERIC ONLY** — no stat penalty for upgrading (honours the skill-expression pillar); a literal corruption meter is possible future net-new design, not part of this; (d) **PER-SLEEPER Echo** in co-op (each player's own future-self).
+
+## Consequences
+
+- **Resolves the [[DR-013_M6_Aether_Cycle_Region_Split]] "fiction reconciliation — operator sign-off" open item.** [[Identity]] is rewritten as "The Awakening Engine (Aether-colony sci-fi)"; the frontier colony is recontextualised, not replaced. The lexicon (Sleeper / the Echo / the Wellspring / the Awakening Engine / the Blightfield / Aether Gates / the Aether Cycle's Sortie-Siege-Forge) is the canonical naming, each anchored to a code component.
+- **Reads onto every built system with NO code change** (verified in the synthesis coherence pass): resources ⇄ ability-tier StatModifier path + structure costs + reserved M7 automation codes; cycle ⇄ `CyclePhaseSystem`/`CycleState`; regions ⇄ the GhostRelevancy split; gates ⇄ `ExpeditionGate`; goal ⇄ `GoalProgress`; turret ⇄ the hitscan defense; Husks ⇄ `WaveSystem`; base ⇄ `BaseAnchor`/grid/ledger; palette ⇄ the DR-009 emissive look.
+- **Violates no locked pillar.** The voice / memory beats / THEM reveal are **client presentation + content**, never the predicted sim (observe-only, like the existing HUD/VFX). The recharging pod stays a **persistent anchor** (death = respawn), not a roguelike reset.
+- **What this adoption is NOT:** any *mechanic* the fiction implies — a literal corruption/risk meter, memory-gated abilities, an authored ending payoff — is **net-new design/content for a later milestone**. Adopting the identity is copy + light-asset only. The goal meter currently fills (`Target=10`) with no authored payoff; **staged memory beats + the Echo's voice lines (playable as subtitles first) are the net-new narrative content** tracked for a future milestone.
+
+## Open / deferred
+
+- **Narrative payoff + the Echo's voice/memory-beat content** — a later narrative milestone (writing + light/optional VO; subtitle-first). The three THEM readings are arbitrated then.
+- **Art additive (none required now):** the Awakening Engine hero set-piece + charge readout, the cyan/orange Aether-veining motif, gate portal shimmer, node glow, memory-fragment motes, the Echo voice + Aether hum.
+- **Pillars.md** [[Identity]] note updated to point at the Awakening-Engine framing.
+
+Evolves [[DR-009_GameFeel_Identity_FirstBlood]] (the frontier-colony identity); skins the locked [[Pillars]]; reads onto [[DR-013_M6_Aether_Cycle_Region_Split]] + [[DR-014_M6_Build_Structures_Automation_Foundation]]. Skin-only — no system in M1–M6 changes.