Continued
This commit is contained in:
Binary file not shown.
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5459c9edea89bd94fa6f5043ae00eb40
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97bd1bd2a09697f449a5580826a0355d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Entities;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ProjectM.Authoring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authoring for the baked <see cref="StructureCatalog"/> 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 <see cref="StructureCatalogEntry"/> buffer is already the data-driven shape. Place
|
||||||
|
/// once in the gameplay subscene.
|
||||||
|
/// </summary>
|
||||||
|
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<StructureCatalogAuthoring>
|
||||||
|
{
|
||||||
|
public override void Bake(StructureCatalogAuthoring authoring)
|
||||||
|
{
|
||||||
|
var entity = GetEntity(authoring, TransformUsageFlags.None);
|
||||||
|
AddComponent<StructureCatalog>(entity);
|
||||||
|
var buf = AddBuffer<StructureCatalogEntry>(entity);
|
||||||
|
|
||||||
|
if (authoring.TurretPrefab != null)
|
||||||
|
{
|
||||||
|
buf.Add(new StructureCatalogEntry
|
||||||
|
{
|
||||||
|
Type = StructureType.Turret,
|
||||||
|
Prefab = GetEntity(authoring.TurretPrefab, TransformUsageFlags.Dynamic),
|
||||||
|
CostResourceId = ResourceId.Ore,
|
||||||
|
CostAmount = authoring.TurretCostOre,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 40093ed42072f5a4889f5f62f510aa27
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Entities;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ProjectM.Authoring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authoring for the turret structure ghost prefab (duplicate UpgradePickup.prefab so the ownerless
|
||||||
|
/// interpolated GhostAuthoringComponent comes free). Bakes <see cref="PlacedStructure"/>{Type=Turret} +
|
||||||
|
/// <see cref="Turret"/> stats. BuildPlaceSystem stamps Cell + LastProcessedTick at placement.
|
||||||
|
/// </summary>
|
||||||
|
public class TurretAuthoring : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Min(1f)] public float Range = 10f;
|
||||||
|
[Min(1)] public int CooldownTicks = 30;
|
||||||
|
[Min(1f)] public float Damage = 12f;
|
||||||
|
|
||||||
|
private class TurretBaker : Baker<TurretAuthoring>
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dfb1e0820cbf41b4bbbe99066dfadd40
|
||||||
@@ -28,6 +28,7 @@ namespace ProjectM.Authoring
|
|||||||
});
|
});
|
||||||
AddComponent<ResourceLedger>(entity);
|
AddComponent<ResourceLedger>(entity);
|
||||||
AddBuffer<StorageEntry>(entity);
|
AddBuffer<StorageEntry>(entity);
|
||||||
|
AddComponent(entity, new GoalProgress { Charge = 0, Target = 10 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ef7d7956e34fb0d43a145bc4cfd425d0
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Unity.NetCode;
|
||||||
|
using Unity.Transforms;
|
||||||
|
|
||||||
|
namespace ProjectM.Client
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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<PendingBuild> s_PendingBuild =
|
||||||
|
new System.Collections.Generic.Queue<PendingBuild>();
|
||||||
|
static int s_PendingUpgrades = 0;
|
||||||
|
|
||||||
|
/// <summary>EDITOR / execute_code hook: queue a structure placement at a specific cell.</summary>
|
||||||
|
public static void PlaceStructure(byte type, int cellX, int cellZ) =>
|
||||||
|
s_PendingBuild.Enqueue(new PendingBuild { Type = type, CellX = cellX, CellZ = cellZ });
|
||||||
|
|
||||||
|
/// <summary>EDITOR / execute_code hook: queue a turret placement at a specific cell.</summary>
|
||||||
|
public static void PlaceTurret(int cellX, int cellZ) => PlaceStructure(StructureType.Turret, cellX, cellZ);
|
||||||
|
|
||||||
|
/// <summary>EDITOR / execute_code hook: queue an ability-damage upgrade.</summary>
|
||||||
|
public static void UpgradeAbility() => s_PendingUpgrades++;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected override void OnCreate()
|
||||||
|
{
|
||||||
|
RequireForUpdate<NetworkId>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
if (!SystemAPI.TryGetSingletonEntity<NetworkId>(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<BaseAnchor>(out var anchor))
|
||||||
|
return false;
|
||||||
|
foreach (var xform in SystemAPI.Query<RefRO<LocalTransform>>().WithAll<GhostOwnerIsLocal, PlayerTag>())
|
||||||
|
{
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 765356fd6c5e64c4e9ab588f99d3388f
|
||||||
@@ -27,6 +27,8 @@ namespace ProjectM.Client
|
|||||||
Text _phaseText;
|
Text _phaseText;
|
||||||
Text _resourceText;
|
Text _resourceText;
|
||||||
Text _locationText;
|
Text _locationText;
|
||||||
|
RectTransform _goalFill;
|
||||||
|
Text _goalText;
|
||||||
GameObject _respawnOverlay;
|
GameObject _respawnOverlay;
|
||||||
EntityQuery _huskQuery;
|
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);
|
_locationText.color = onExpedition ? new Color(1f, 0.8f, 0.4f) : new Color(0.6f, 0.85f, 1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_goalFill != null && SystemAPI.TryGetSingleton<GoalProgress>(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)
|
if (_resourceText != null)
|
||||||
{
|
{
|
||||||
string res = "";
|
string res = "";
|
||||||
@@ -211,6 +220,12 @@ namespace ProjectM.Client
|
|||||||
var lrt = _locationText.rectTransform;
|
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.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);
|
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).
|
// Downed / respawning overlay (full screen, toggled by Dead).
|
||||||
_respawnOverlay = new GameObject("RespawnOverlay", typeof(RectTransform));
|
_respawnOverlay = new GameObject("RespawnOverlay", typeof(RectTransform));
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 73829e22632af284d880736d6b0a371f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Burst;
|
||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Server
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Server-authoritative ability-damage upgrade (handles <see cref="AbilityUpgradeRequest"/> 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 <see cref="StatModifier"/> 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).
|
||||||
|
/// </summary>
|
||||||
|
[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<ResourceLedger>();
|
||||||
|
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||||
|
.WithAll<AbilityUpgradeRequest, ReceiveRpcCommandRequest>();
|
||||||
|
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnUpdate(ref SystemState state)
|
||||||
|
{
|
||||||
|
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
||||||
|
|
||||||
|
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
||||||
|
foreach (var (owner, entity) in
|
||||||
|
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, StatModifier>().WithEntityAccess())
|
||||||
|
playerByConn[owner.ValueRO.NetworkId] = entity;
|
||||||
|
|
||||||
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||||
|
|
||||||
|
foreach (var (receive, requestEntity) in
|
||||||
|
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>>().WithAll<AbilityUpgradeRequest>().WithEntityAccess())
|
||||||
|
{
|
||||||
|
var conn = receive.ValueRO.SourceConnection;
|
||||||
|
if (SystemAPI.HasComponent<NetworkId>(conn)
|
||||||
|
&& playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(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<StatModifier>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ff2ed6b5fa37a174aa7413f4d2f5d6b3
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Server-authoritative structure placement (handles <see cref="BuildPlaceRequest"/> RPCs). Derives
|
||||||
|
/// occupancy by scanning live <see cref="PlacedStructure"/> 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.
|
||||||
|
/// </summary>
|
||||||
|
[BurstCompile]
|
||||||
|
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||||
|
public partial struct BuildPlaceSystem : ISystem
|
||||||
|
{
|
||||||
|
ComponentLookup<LocalTransform> m_TransformLookup;
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnCreate(ref SystemState state)
|
||||||
|
{
|
||||||
|
m_TransformLookup = state.GetComponentLookup<LocalTransform>(isReadOnly: true);
|
||||||
|
state.RequireForUpdate<StructureCatalog>();
|
||||||
|
state.RequireForUpdate<BaseAnchor>();
|
||||||
|
state.RequireForUpdate<ResourceLedger>();
|
||||||
|
state.RequireForUpdate<NetworkTime>();
|
||||||
|
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||||
|
.WithAll<BuildPlaceRequest, ReceiveRpcCommandRequest>();
|
||||||
|
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnUpdate(ref SystemState state)
|
||||||
|
{
|
||||||
|
m_TransformLookup.Update(ref state);
|
||||||
|
uint now = SystemAPI.GetSingleton<NetworkTime>().ServerTick.TickIndexForValidTick;
|
||||||
|
var anchor = SystemAPI.GetSingleton<BaseAnchor>();
|
||||||
|
|
||||||
|
var catalog = SystemAPI.GetBuffer<StructureCatalogEntry>(SystemAPI.GetSingletonEntity<StructureCatalog>());
|
||||||
|
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
||||||
|
|
||||||
|
// Derive occupancy from the live structure set (authoritative source of truth).
|
||||||
|
var occupied = new NativeHashSet<int2>(64, Allocator.Temp);
|
||||||
|
foreach (var ps in SystemAPI.Query<RefRO<PlacedStructure>>())
|
||||||
|
occupied.Add(ps.ValueRO.Cell);
|
||||||
|
|
||||||
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||||
|
|
||||||
|
foreach (var (request, receive, requestEntity) in
|
||||||
|
SystemAPI.Query<RefRO<BuildPlaceRequest>, RefRO<ReceiveRpcCommandRequest>>().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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d1886c7056b315e42b7754f50c43c59e
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only turret fire (hitscan) — EnemyAISystem reversed. Snapshots living Husks once (entity, planar
|
||||||
|
/// pos, region); each turret picks the nearest Husk in ITS region within Range and, on a NetworkTick
|
||||||
|
/// cooldown stored in <see cref="PlacedStructure.NextTick"/>, appends a direct <c>DamageEvent</c>
|
||||||
|
/// (SourceNetworkId=-1) to it. Reuses HealthApplyDamageSystem (already destroys EnemyTag at HP<=0) — no
|
||||||
|
/// projectile, no tunnelling, no friendly-fire. Plain server SimulationSystemGroup
|
||||||
|
/// <c>[UpdateAfter(PredictedSimulationSystemGroup)]</c> (the predicted group is OrderFirst → UpdateBefore is
|
||||||
|
/// ignored); the appended DamageEvent drains next tick (~16ms), consistent with EnemyAISystem. Self-gates:
|
||||||
|
/// Husks only exist during the Defend wave.
|
||||||
|
/// </summary>
|
||||||
|
[BurstCompile]
|
||||||
|
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||||
|
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||||
|
[UpdateAfter(typeof(PredictedSimulationSystemGroup))]
|
||||||
|
public partial struct TurretFireSystem : ISystem
|
||||||
|
{
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnCreate(ref SystemState state)
|
||||||
|
{
|
||||||
|
state.RequireForUpdate<NetworkTime>();
|
||||||
|
state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly<Turret>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnUpdate(ref SystemState state)
|
||||||
|
{
|
||||||
|
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||||
|
if (!serverTick.IsValid)
|
||||||
|
return;
|
||||||
|
uint now = serverTick.TickIndexForValidTick;
|
||||||
|
|
||||||
|
var huskEntities = new NativeList<Entity>(Allocator.Temp);
|
||||||
|
var huskPos = new NativeList<float2>(Allocator.Temp);
|
||||||
|
var huskRegion = new NativeList<byte>(Allocator.Temp);
|
||||||
|
foreach (var (xform, health, region, e) in
|
||||||
|
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>, RefRO<RegionTag>>()
|
||||||
|
.WithAll<EnemyTag>().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<RefRW<PlacedStructure>, RefRO<Turret>, RefRO<LocalTransform>, RefRO<RegionTag>>())
|
||||||
|
{
|
||||||
|
uint nextRaw = ps.ValueRO.NextTick;
|
||||||
|
if (nextRaw != 0)
|
||||||
|
{
|
||||||
|
var nextTick = new NetworkTick(nextRaw);
|
||||||
|
if (nextTick.IsValid && nextTick.IsNewerThan(serverTick))
|
||||||
|
continue; // still cooling down
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 tp = xform.ValueRO.Position.xz;
|
||||||
|
byte treg = region.ValueRO.Region;
|
||||||
|
float rangeSq = turret.ValueRO.Range * turret.ValueRO.Range;
|
||||||
|
|
||||||
|
int best = -1;
|
||||||
|
float bestSq = float.MaxValue;
|
||||||
|
for (int i = 0; i < huskEntities.Length; i++)
|
||||||
|
{
|
||||||
|
if (huskRegion[i] != treg)
|
||||||
|
continue;
|
||||||
|
float sq = math.distancesq(huskPos[i], tp);
|
||||||
|
if (sq <= rangeSq && sq < bestSq)
|
||||||
|
{
|
||||||
|
bestSq = sq;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best >= 0)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 53cc7669bd6cc5d4e8307b18732897bc
|
||||||
@@ -71,6 +71,13 @@ namespace ProjectM.Server
|
|||||||
cycle.Phase = CyclePhase.Expedition;
|
cycle.Phase = CyclePhase.Expedition;
|
||||||
cycle.CycleNumber += 1;
|
cycle.CycleNumber += 1;
|
||||||
cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks);
|
cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks);
|
||||||
|
// Long-arc goal: one charge per completed cycle (single writer).
|
||||||
|
if (SystemAPI.HasComponent<GoalProgress>(cycleEntity))
|
||||||
|
{
|
||||||
|
var goal = SystemAPI.GetComponent<GoalProgress>(cycleEntity);
|
||||||
|
goal.Charge += 1;
|
||||||
|
SystemAPI.SetComponent(cycleEntity, goal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 53b186388519ddb458c72bb717085721
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="StatModifier"/> on the
|
||||||
|
/// player (replace-by-SourceId so the buffer stays bounded), which StatRecomputeSystem folds into
|
||||||
|
/// EffectiveAbilityStats.Damage on both worlds — no new replicated component.
|
||||||
|
/// </summary>
|
||||||
|
public struct AbilityUpgradeRequest : IRpcCommand { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1236d3751a5740741a4a10e0a653565f
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client -> server request to build a structure of <see cref="StructureType"/> at grid cell
|
||||||
|
/// (<see cref="CellX"/>, <see cref="CellZ"/>). 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.
|
||||||
|
/// </summary>
|
||||||
|
public struct BuildPlaceRequest : IRpcCommand
|
||||||
|
{
|
||||||
|
public byte StructureType;
|
||||||
|
public int CellX;
|
||||||
|
public int CellZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dbcc491dc3dd853459cd8cfad2458b17
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pure, deterministic build-placement helpers (unit-tested like <see cref="BaseGridMath"/> /
|
||||||
|
/// <c>StorageMath</c>). 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 <see cref="BaseAnchor"/>. The server passes a Temp <see cref="NativeHashSet{T}"/> of occupied
|
||||||
|
/// cells built by scanning live <see cref="PlacedStructure"/> ghosts.
|
||||||
|
/// </summary>
|
||||||
|
public static class BuildPlacementMath
|
||||||
|
{
|
||||||
|
/// <summary>True if <paramref name="cell"/> is occupied in the derived set.</summary>
|
||||||
|
public static bool IsOccupied(in NativeHashSet<int2> occupied, int2 cell) => occupied.Contains(cell);
|
||||||
|
|
||||||
|
/// <summary>Full server placement legality: the cell is in-plot (half-open, negative-safe) AND not occupied.</summary>
|
||||||
|
public static bool CanPlace(in BaseAnchor anchor, in NativeHashSet<int2> occupied, int2 cell)
|
||||||
|
{
|
||||||
|
return BaseGridMath.IsCellInPlot(anchor, cell) && !occupied.Contains(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 203dcbd4f9cc089408633b0bb6ccb2c1
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A built base structure occupying one grid cell. An ownerless INTERPOLATED ghost (RegionTag{Base},
|
||||||
|
/// world-owned, runtime-spawned by BuildPlaceSystem). <see cref="Type"/> is the only replicated field
|
||||||
|
/// (a cheap byte for client visual branching); <see cref="Cell"/> is server-only (clients derive it from
|
||||||
|
/// the replicated LocalTransform via <see cref="BaseGridMath.WorldToCell"/>, so it stays off the wire).
|
||||||
|
/// <see cref="NextTick"/> / <see cref="LastProcessedTick"/> are server-only raw NetworkTick values
|
||||||
|
/// (<see cref="TickUtil.NonZero"/>-guarded; 0 = inactive): the Turret reuses <see cref="NextTick"/> as its
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public struct PlacedStructure : IComponentData
|
||||||
|
{
|
||||||
|
/// <summary>Structure type (see <see cref="StructureType"/>); the only replicated field.</summary>
|
||||||
|
[GhostField] public byte Type;
|
||||||
|
|
||||||
|
/// <summary>Occupied grid cell (server-only; clients derive it from LocalTransform).</summary>
|
||||||
|
public int2 Cell;
|
||||||
|
|
||||||
|
/// <summary>Next action tick (server-only; turret cooldown now / next production tick in M7). 0 = inactive.</summary>
|
||||||
|
public uint NextTick;
|
||||||
|
|
||||||
|
/// <summary>Last tick this structure was processed (server-only; M7 offline-catch-up baseline). Stamped at spawn.</summary>
|
||||||
|
public uint LastProcessedTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A buildable defense turret (the first structure). Hitscan: <c>TurretFireSystem</c> applies a direct
|
||||||
|
/// <c>DamageEvent</c> to the nearest in-range living Husk on cooldown (reusing
|
||||||
|
/// <see cref="PlacedStructure.NextTick"/>) — reuses HealthApplyDamageSystem, no projectile/friendly-fire.
|
||||||
|
/// </summary>
|
||||||
|
public struct Turret : IComponentData
|
||||||
|
{
|
||||||
|
public float Range;
|
||||||
|
public int CooldownTicks;
|
||||||
|
public float Damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
public struct StructureCatalogEntry : IBufferElementData
|
||||||
|
{
|
||||||
|
public byte Type;
|
||||||
|
public Entity Prefab;
|
||||||
|
public byte CostResourceId;
|
||||||
|
public int CostAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tag on the baked singleton carrying the <see cref="StructureCatalogEntry"/> buffer (the build cost/prefab table).</summary>
|
||||||
|
public struct StructureCatalog : IComponentData { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 00d3379caf4807d4ebd97432848dd5d5
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Unity.Entities;
|
||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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: <c>CyclePhaseSystem</c> increments <see cref="Charge"/> on each completed
|
||||||
|
/// cycle (Build -> Expedition). The HUD observes it for a progress bar.
|
||||||
|
/// </summary>
|
||||||
|
public struct GoalProgress : IComponentData
|
||||||
|
{
|
||||||
|
/// <summary>Accumulated progress.</summary>
|
||||||
|
[GhostField] public int Charge;
|
||||||
|
|
||||||
|
/// <summary>Charge required to reach the goal.</summary>
|
||||||
|
[GhostField] public int Target;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e1f60b3396850074ca0e44b831b5980c
|
||||||
@@ -273,6 +273,52 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &409538537
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -1217,3 +1263,4 @@ SceneRoots:
|
|||||||
- {fileID: 17637047}
|
- {fileID: 17637047}
|
||||||
- {fileID: 1192434518}
|
- {fileID: 1192434518}
|
||||||
- {fileID: 236770154}
|
- {fileID: 236770154}
|
||||||
|
- {fileID: 380046995}
|
||||||
|
|||||||
@@ -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).
|
- **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.
|
- **`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.
|
- **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<int2>` (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=<sentinel>}` 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
|
## Bootstrap & worlds
|
||||||
|
|
||||||
|
|||||||
Vendored
+22
@@ -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
|
||||||
|
}
|
||||||
@@ -3,65 +3,109 @@ tags:
|
|||||||
- vision
|
- vision
|
||||||
- identity
|
- identity
|
||||||
status: draft
|
status: draft
|
||||||
updated: 2026-06-02
|
updated: 2026-06-03
|
||||||
permalink: gamevault/01-vision/identity
|
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
|
## 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
|
## 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.
|
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).
|
||||||
- **Hostile orange / red** → Husks and Blight corruption.
|
|
||||||
- **White-hot** → impacts, muzzle flashes, deaths.
|
|
||||||
|
|
||||||
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
|
## 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.
|
- **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. Wave/threat director over time.
|
- **Later:** ranged "spitters", armored "brutes", a "broodmaker" spawner, and eventually a corrupted-fabricator boss (a colony machine the Blight fully claimed).
|
||||||
|
|
||||||
## Automation flavor
|
## 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)
|
## How it maps to the locked pillars (no rework)
|
||||||
|
|
||||||
| Pillar ([[Pillars]]) | Fictional skin |
|
| Pillar ([[Pillars]]) | Fictional skin |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Action-ARPG combat (twin-stick) | Frontier operator gunplay vs. the Husk swarm |
|
| Action-ARPG combat (twin-stick) | A Sleeper channelling charged Aether vs. the Husk swarm |
|
||||||
| Co-op base power fantasy (V Rising feel) | The crew's outpost — built, defended, grown together |
|
| Co-op base power fantasy (V Rising feel) | A crew of Sleepers reawakening + defending one shared Awakening Engine |
|
||||||
| Automation as progression | Colony fabricators/drones/conveyors compounding output |
|
| Automation as progression | The colony's surviving Aether-machines compounding output while you raid |
|
||||||
| Persistent base + instanced expeditions | The safe **outpost anchor** (`BaseAnchor`) vs. the blighted **frontier** beyond |
|
| 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
|
## Related
|
||||||
|
|
||||||
- [[Pillars]] — locked mechanical decisions this fiction skins.
|
- [[Pillars]] — the 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.
|
- [[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.
|
||||||
|
|||||||
@@ -28,5 +28,5 @@ permalink: gamevault/01-vision/pillars
|
|||||||
|
|
||||||
## Related
|
## 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/`
|
- Decision records: `07_Sessions/_Decisions/`
|
||||||
@@ -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]] |
|
| **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 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]] |
|
| **— 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) | ⬜ |
|
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
|
||||||
|
|
||||||
Promote items from [[Backlog]] here when committed.
|
Promote items from [[Backlog]] here when committed.
|
||||||
@@ -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
|
(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.
|
`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.
|
See [[DR-013_M6_Aether_Cycle_Region_Split]] for the full architecture + validated evidence.
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|||||||
@@ -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<int2>` — 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=<sentinel>}` 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]].
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user