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.