Run Re-Do
This commit is contained in:
@@ -38,13 +38,14 @@ namespace ProjectM.Client
|
||||
UnityEngine.Camera _camera; // cursor -> ground re-raycast for click-to-place (resolved lazily)
|
||||
GameObject _ghost; // translucent ground preview cube
|
||||
Material _ghostMat;
|
||||
MeshFilter _ghostMf; // ghost mesh swaps to the selected structure's preview mesh (HudTheme)
|
||||
MeshRenderer _ghostMr;
|
||||
Mesh _cubeMesh; // procedural fallback (the original preview cube)
|
||||
byte _ghostType = 255; // last-applied preview type (255 = unset)
|
||||
byte _lastSelected; // skip placing on the frame a palette click changes the selection
|
||||
|
||||
// DR-042 C6a: the ability-upgrade send is RUNTIME (the HUD Aether button calls UpgradeAbility); only the
|
||||
// execute_code PLACE statics stay editor-gated. Mirrors EquipSendSystem's unconditional queue + drain.
|
||||
static int s_PendingUpgrades = 0;
|
||||
/// <summary>Runtime hook (HUD Aether button) + execute_code: queue an ability-damage upgrade.</summary>
|
||||
public static void UpgradeAbility() => s_PendingUpgrades++;
|
||||
// (Step 11: UpgradeAbility/s_PendingUpgrades RETIRED with the AbilityUpgradeRequest wire — in-run boons +
|
||||
// the base meta-shop replaced the Aether damage upgrade.)des++;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
struct PendingBuild { public byte Type; public int CellX; public int CellZ; public byte Direction; }
|
||||
@@ -113,17 +114,8 @@ namespace ProjectM.Client
|
||||
foreach (var (key, type) in s_BuildHotkeys)
|
||||
if (keyboard[key].wasPressedThisFrame && TryGetLocalPlayerCell(out int2 cell))
|
||||
SendBuild(connection, type, cell.x, cell.y, type == StructureType.Conveyor ? s_ConveyorDir : (byte)0);
|
||||
if (keyboard.uKey.wasPressedThisFrame)
|
||||
SendUpgrade(connection);
|
||||
}
|
||||
|
||||
// DR-042 C6a: the ability-upgrade drain runs at RUNTIME (the HUD Aether button enqueues via UpgradeAbility);
|
||||
// only the execute_code PLACE drain stays editor-gated.
|
||||
while (s_PendingUpgrades > 0)
|
||||
{
|
||||
s_PendingUpgrades--;
|
||||
SendUpgrade(connection);
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
while (s_PendingBuild.Count > 0)
|
||||
{
|
||||
@@ -185,7 +177,7 @@ namespace ProjectM.Client
|
||||
byte reason = BuildPreviewMath.Evaluate(anchor, targetCell, occupied, LedgerOre(), cost);
|
||||
bool valid = reason == BuildPreviewMath.Valid;
|
||||
|
||||
ShowGhost(BaseGridMath.CellToWorld(anchor, targetCell), anchor.CellSize, valid);
|
||||
ShowGhost(BaseGridMath.CellToWorld(anchor, targetCell), anchor.CellSize, valid, sel);;
|
||||
|
||||
// Place on a left-click (valid, not the selecting click).
|
||||
if (valid && !justSelected && mouse.leftButton.wasPressedThisFrame)
|
||||
@@ -208,16 +200,44 @@ namespace ProjectM.Client
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
// ---- Ground ghost preview (procedural translucent cube, like AimReticleSystem's reticle) ----
|
||||
void ShowGhost(float3 center, float cellSize, bool valid)
|
||||
// ---- Ground ghost preview: the selected structure's REAL mesh (HudTheme, build-safe serialized refs)
|
||||
// tinted translucent green/red; falls back to the original procedural cube when no mesh is authored. ----
|
||||
void ShowGhost(float3 center, float cellSize, bool valid, byte type)
|
||||
{
|
||||
EnsureGhost();
|
||||
_ghost.transform.position = (Vector3)center + Vector3.up * 0.5f;
|
||||
_ghost.transform.localScale = new Vector3(cellSize * 0.9f, 1f, cellSize * 0.9f);
|
||||
ApplyGhostMesh(type);
|
||||
if (_ghostMf.sharedMesh == _cubeMesh)
|
||||
{
|
||||
_ghost.transform.position = (Vector3)center + Vector3.up * 0.5f;
|
||||
_ghost.transform.localScale = new Vector3(cellSize * 0.9f, 1f, cellSize * 0.9f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// preview meshes are authored real-size with a ground pivot (SM_Turret_01 / SM_Wall_01 / SM_Fabricator_01)
|
||||
_ghost.transform.position = (Vector3)center;
|
||||
_ghost.transform.localScale = Vector3.one;
|
||||
}
|
||||
_ghostMat.color = valid ? new Color(0.3f, 1f, 0.45f, 0.35f) : new Color(1f, 0.32f, 0.26f, 0.35f);
|
||||
if (!_ghost.activeSelf) _ghost.SetActive(true);
|
||||
}
|
||||
|
||||
// Swap the ghost's mesh when the palette selection changes (255 = unset sentinel; no selection is 0 -> cube).
|
||||
void ApplyGhostMesh(byte type)
|
||||
{
|
||||
if (type == _ghostType) return;
|
||||
_ghostType = type;
|
||||
var theme = HudTheme.Get();
|
||||
Mesh mesh = theme != null ? theme.StructureGhostMesh(type) : null;
|
||||
if (mesh == null) mesh = _cubeMesh;
|
||||
_ghostMf.sharedMesh = mesh;
|
||||
if (_ghostMr.sharedMaterials.Length != mesh.subMeshCount)
|
||||
{
|
||||
var mats = new Material[mesh.subMeshCount]; // one translucent mat per submesh so the whole preview tints
|
||||
for (int i = 0; i < mats.Length; i++) mats[i] = _ghostMat;
|
||||
_ghostMr.sharedMaterials = mats;
|
||||
}
|
||||
}
|
||||
|
||||
void HideGhost()
|
||||
{
|
||||
if (_ghost != null && _ghost.activeSelf) _ghost.SetActive(false);
|
||||
@@ -233,10 +253,13 @@ namespace ProjectM.Client
|
||||
_ghost.name = "~BuildGhost";
|
||||
var col = _ghost.GetComponent<Collider>();
|
||||
if (col != null) Object.Destroy(col);
|
||||
var mr = _ghost.GetComponent<MeshRenderer>();
|
||||
mr.sharedMaterial = _ghostMat;
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
mr.receiveShadows = false;
|
||||
_ghostMf = _ghost.GetComponent<MeshFilter>();
|
||||
_cubeMesh = _ghostMf.sharedMesh;
|
||||
_ghostType = 255;
|
||||
_ghostMr = _ghost.GetComponent<MeshRenderer>();
|
||||
_ghostMr.sharedMaterial = _ghostMat;
|
||||
_ghostMr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
_ghostMr.receiveShadows = false;
|
||||
_ghost.SetActive(false);
|
||||
}
|
||||
|
||||
@@ -270,12 +293,6 @@ namespace ProjectM.Client
|
||||
EntityManager.AddComponentData(e, new BuildPlaceRequest { StructureType = type, CellX = cellX, CellZ = cellZ, Direction = direction });
|
||||
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 });
|
||||
}
|
||||
// (Step 11: the Aether ability-upgrade sender was RETIRED with AbilityUpgradeRequest — boons replaced it.)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side boon-pick sender: a static enqueue (the Step-14 3-card modal / execute_code) drained into
|
||||
/// <see cref="BoonPickRequest"/> RPCs. Carries only the option INDEX — the server resolves it against the
|
||||
/// sender's own authoritative <c>BoonOffer</c> and validates lifecycle/pending, so a stale or forged pick is
|
||||
/// simply dropped. Statics reset on play-enter (the stale-bridge hazard).
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class BoonSendSystem : SystemBase
|
||||
{
|
||||
static int s_Pending;
|
||||
static byte s_PendingIndex;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void ResetStatics()
|
||||
{
|
||||
s_Pending = 0;
|
||||
s_PendingIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Queue a boon pick (0/1/2). The HUD card click + execute_code drive this.</summary>
|
||||
public static void PickBoon(byte optionIndex)
|
||||
{
|
||||
s_PendingIndex = optionIndex;
|
||||
s_Pending++;
|
||||
}
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
while (s_Pending > 0)
|
||||
{
|
||||
s_Pending--;
|
||||
var req = EntityManager.CreateEntity(typeof(BoonPickRequest), typeof(SendRpcCommandRequest));
|
||||
EntityManager.SetComponentData(req, new BoonPickRequest { Index = s_PendingIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7e90bc7230614845817b81f0f7b70f0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60aece22e94371c468102dbf35c3fe47
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side meta-purchase sender: a static enqueue (the Step-14 base meta-shop panel / execute_code) drained
|
||||
/// into <see cref="MetaSpendRequest"/> RPCs. Carries only the upgrade ID — the tier is server-computed and the
|
||||
/// server re-validates everything (Staging gate, class mask, MaxTier, prereq, Aether affordability), so a stale
|
||||
/// or forged request is simply dropped. A real QUEUE (unlike the single-slot boon pick) — two rapid clicks on
|
||||
/// two different shop rows must both arrive. Statics reset on play-enter (the stale-bridge hazard).
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class MetaSpendSendSystem : SystemBase
|
||||
{
|
||||
static readonly Queue<byte> s_Queue = new();
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void ResetStatics() => s_Queue.Clear();
|
||||
|
||||
/// <summary>Queue a permanent-upgrade purchase by catalog id. The shop row click + execute_code drive this.</summary>
|
||||
public static void RequestPurchase(byte upgradeId) => s_Queue.Enqueue(upgradeId);
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
while (s_Queue.Count > 0)
|
||||
{
|
||||
var req = EntityManager.CreateEntity(typeof(MetaSpendRequest), typeof(SendRpcCommandRequest));
|
||||
EntityManager.SetComponentData(req, new MetaSpendRequest { UpgradeId = s_Queue.Dequeue() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f3197b4ede6dea41a6c4c93ffa51b42
|
||||
@@ -102,9 +102,9 @@ namespace ProjectM.Client
|
||||
case Mine: return attack + " — Attack the glowing Ore to mine it";
|
||||
case Build: return build + " — open Build, then place a Turret by your Core";
|
||||
case Fabricator: return "Build a Fabricator — turrets need Charge (Ore → ammo)";
|
||||
case Gate: return "Reach the Expedition Gate — clearing it charges the Engine";
|
||||
case Clear: return "Clear the zone — defeat every enemy";
|
||||
case Return: return "Return through the gate — bank your clear (+1 Engine charge)";
|
||||
case Gate: return "Press T to READY UP — when everyone is ready, the run launches";
|
||||
case Clear: return "Clear each room — pick a boon, choose your path, reach the boss";
|
||||
case Return: return "Fell the boss — you return home and the Engine charges (+1)";;
|
||||
case Defend: return "Defend the Core! — hold the line through the siege";
|
||||
case Done: return "You've got it. Clear expeditions to fill the Engine and win.";
|
||||
default: return "";
|
||||
|
||||
@@ -215,13 +215,9 @@ namespace ProjectM.Client
|
||||
}
|
||||
return found;
|
||||
}
|
||||
// base gate (go) lives in the base region; expedition gate (return) lives past the region split.
|
||||
bool wantBase = kind == OnboardingStepMath.PointerBaseGate;
|
||||
foreach (var lt in SystemAPI.Query<RefRO<LocalTransform>>().WithAll<ExpeditionGate>())
|
||||
{
|
||||
var p = lt.ValueRO.Position;
|
||||
if ((p.x < ExpeditionRegionXMin) == wantBase) { target = p; return true; }
|
||||
}
|
||||
// Step 11: the walk-in ExpeditionGate was RETIRED (the ready-check launches runs), so gate pointers
|
||||
// have no world target — hide the arrow; the step text still teaches. Step 14's HUD pass re-points
|
||||
// onboarding at the READY panel instead.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,20 @@ namespace ProjectM.Client
|
||||
// END-2: terminal win/loss banner (observes the replicated RunOutcome; latched server-side).
|
||||
VisualElement _runBanner;
|
||||
Label _runBannerText, _runBannerSub;
|
||||
// DR-042 C6a: the Aether ability-upgrade button (was U-key only) + its live affordability tint.
|
||||
Button _upgradeBtn;
|
||||
// Step 14 (expedition redesign): the choice-of-3 boon modal + the route-choice panel. Both are
|
||||
// observe-only readers of replicated state (BoonOffer via GhostOwnerIsLocal; RunInfo RouteOpt*); clicks
|
||||
// enqueue through the client send-systems' statics. Built lazily on first show.
|
||||
VisualElement _boonModal, _boonCardRow;
|
||||
VisualElement _routePanel, _routeBtnRow;
|
||||
Label _routeTitle;
|
||||
byte _boonShownFor; // last (Option0^Option1^Option2 ^ room) signature the modal was built for
|
||||
bool _boonModalBuilt, _routePanelBuilt;
|
||||
// Step 14 (meta shop): Staging-only permanent-upgrade shop (replicated MetaTierState + ledger Aether;
|
||||
// row clicks enqueue MetaSpendSendSystem.RequestPurchase — the server re-validates everything).
|
||||
VisualElement _metaPanel, _metaRowsHost;
|
||||
Label _metaShopTitle;
|
||||
bool _metaShopBuilt;
|
||||
int _metaShownFor; // last (class, tiers, aether) signature the shop rows were built for
|
||||
|
||||
|
||||
readonly List<VisualElement> _pips = new();
|
||||
@@ -171,41 +183,97 @@ namespace ProjectM.Client
|
||||
_cycleText.text = "";
|
||||
}
|
||||
|
||||
// ---- Location + gate hint (banner sub-line) ----
|
||||
var cam = Camera.main;
|
||||
// ---- Location line (banner sub-line) — Step 14: driven by the replicated RunInfo lifecycle FSM ----
|
||||
// (the old camera-X + walk-in-gate copy died with the gate; siege/final overrides below still win).
|
||||
var cam = Camera.main; // camera-X region signal still feeds downstream panels (atmosphere/threat)
|
||||
bool onExpedition = cam != null && cam.transform.position.x > ExpeditionRegionXMin;
|
||||
_locationText.text = onExpedition
|
||||
? "ON EXPEDITION - carve the frontier, then return"
|
||||
: finalSiege
|
||||
? "FINAL SIEGE - hold the Engine, this is the last stand"
|
||||
: siege
|
||||
? "DEFEND THE BASE - hold the line"
|
||||
: "MINE THE CRYSTALS - any attack harvests Ore, then BUILD";
|
||||
_locationText.style.color = onExpedition ? new Color(1f, 0.8f, 0.4f)
|
||||
: finalSiege ? new Color(1f, 0.3f, 0.25f)
|
||||
: siege ? new Color(1f, 0.55f, 0.4f) : new Color(0.6f, 0.95f, 0.7f);
|
||||
// DR-042 C7 (gate prompt) + C7b (objective readout): the expedition is the win-driver, so signpost it.
|
||||
// Reads the REPLICATED ExpeditionObjective summary (cross-region safe). Lower priority than the siege /
|
||||
// cold-turret / overrun overrides below, which still win.
|
||||
if (SystemAPI.TryGetSingleton<ExpeditionObjective>(out var obj))
|
||||
bool haveRun = SystemAPI.TryGetSingleton<RunInfo>(out var runInfo);
|
||||
SystemAPI.TryGetSingleton<ExpeditionObjective>(out var obj);
|
||||
if (haveRun && !siege && !finalSiege)
|
||||
{
|
||||
if (onExpedition)
|
||||
switch (runInfo.Lifecycle)
|
||||
{
|
||||
if (obj.State == ExpeditionObjectiveState.Cleared)
|
||||
{ _locationText.text = "ZONE CLEARED - return to base to claim"; _locationText.style.color = new Color(0.5f, 1f, 0.6f); }
|
||||
else if (obj.State == ExpeditionObjectiveState.Active)
|
||||
{ _locationText.text = "CLEAR THE ZONE - " + obj.Remaining + " enemies remaining"; _locationText.style.color = new Color(1f, 0.8f, 0.4f); }
|
||||
}
|
||||
else if (!siege)
|
||||
{
|
||||
if (obj.State == ExpeditionObjectiveState.Cleared)
|
||||
{ _locationText.text = "EXPEDITION CLEARED - return to claim your reward"; _locationText.style.color = new Color(0.5f, 1f, 0.6f); }
|
||||
else if (obj.State == ExpeditionObjectiveState.Active)
|
||||
{ _locationText.text = "EXPEDITION IN PROGRESS - " + obj.Remaining + " enemies remaining"; _locationText.style.color = new Color(1f, 0.8f, 0.4f); }
|
||||
else
|
||||
{ _locationText.text = "GO TO THE EXPEDITION GATE - clear a sortie to advance the Engine"; _locationText.style.color = new Color(0.55f, 0.85f, 1f); }
|
||||
case RunLifecycle.Staging:
|
||||
{
|
||||
// Client counts the party's replicated ready flags (send-to-all) for the N/M readout.
|
||||
int total = 0, readyCount = 0;
|
||||
foreach (var pr in SystemAPI.Query<RefRO<PlayerReady>>().WithAll<PlayerTag>())
|
||||
{
|
||||
total++;
|
||||
if (pr.ValueRO.Value != 0) readyCount++;
|
||||
}
|
||||
_locationText.text = "READY UP [T] - " + readyCount + "/" + Mathf.Max(total, 1)
|
||||
+ " ready - launch a run to advance the Engine";
|
||||
_locationText.style.color = new Color(0.55f, 0.85f, 1f);
|
||||
break;
|
||||
}
|
||||
case RunLifecycle.Launching:
|
||||
{
|
||||
uint nowTick = SystemAPI.TryGetSingleton<NetworkTime>(out var ntime) && ntime.ServerTick.IsValid
|
||||
? ntime.ServerTick.TickIndexForValidTick : 0u;
|
||||
int secs = runInfo.LaunchTick != 0 && nowTick != 0
|
||||
? Mathf.Max(0, (int)((runInfo.LaunchTick - nowTick) / 60u) + 1) : 0;
|
||||
_locationText.text = "LAUNCHING IN " + secs + " - un-ready [T] to abort";
|
||||
_locationText.style.color = new Color(1f, 0.9f, 0.4f);
|
||||
break;
|
||||
}
|
||||
case RunLifecycle.InRoom:
|
||||
{
|
||||
string room = "ROOM " + (runInfo.CurrentRoom + 1) + "/" + runInfo.RoomCount
|
||||
+ " " + RoomTypeLabel(runInfo.CurrentRoomType);
|
||||
_locationText.text = obj.State == ExpeditionObjectiveState.Active
|
||||
? room + " - " + obj.Remaining + " enemies remaining"
|
||||
: room + " - clear it to advance";
|
||||
_locationText.style.color = new Color(1f, 0.8f, 0.4f);
|
||||
break;
|
||||
}
|
||||
case RunLifecycle.RoomReward:
|
||||
_locationText.text = "ROOM CLEARED - choose your boon";
|
||||
_locationText.style.color = new Color(0.5f, 1f, 0.6f);
|
||||
break;
|
||||
case RunLifecycle.RouteSelect:
|
||||
_locationText.text = "CHOOSE YOUR PATH";
|
||||
_locationText.style.color = new Color(0.55f, 0.85f, 1f);
|
||||
break;
|
||||
case RunLifecycle.Returning:
|
||||
_locationText.text = "RETURNING HOME...";
|
||||
_locationText.style.color = new Color(0.7f, 0.9f, 1f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!haveRun)
|
||||
{
|
||||
_locationText.text = finalSiege
|
||||
? "FINAL SIEGE - hold the Engine, this is the last stand"
|
||||
: siege ? "DEFEND THE BASE - hold the line"
|
||||
: "MINE THE CRYSTALS - any attack harvests Ore, then BUILD";
|
||||
_locationText.style.color = finalSiege ? new Color(1f, 0.3f, 0.25f)
|
||||
: siege ? new Color(1f, 0.55f, 0.4f) : new Color(0.6f, 0.95f, 0.7f);
|
||||
}
|
||||
else
|
||||
{
|
||||
_locationText.text = finalSiege
|
||||
? "FINAL SIEGE - hold the Engine, this is the last stand"
|
||||
: "DEFEND THE BASE - hold the line";
|
||||
_locationText.style.color = finalSiege ? new Color(1f, 0.3f, 0.25f) : new Color(1f, 0.55f, 0.4f);
|
||||
}
|
||||
|
||||
|
||||
// ---- Step 14: the choice-of-3 boon modal + the route-choice panel (observe replicated state; the
|
||||
// card/button clicks enqueue through the client send-systems' statics) ----
|
||||
BoonOffer localOffer = default;
|
||||
bool hasOffer = false;
|
||||
foreach (var off in SystemAPI.Query<RefRO<BoonOffer>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
||||
{
|
||||
localOffer = off.ValueRO;
|
||||
hasOffer = true;
|
||||
break;
|
||||
}
|
||||
BlobAssetReference<BoonCatalogBlob> boonPool = default;
|
||||
if (SystemAPI.TryGetSingleton<BoonCatalog>(out var bcat))
|
||||
boonPool = bcat.Value;
|
||||
UpdateBoonModal(localOffer, hasOffer && localOffer.Pending == 1, boonPool);
|
||||
UpdateRoutePanel(haveRun ? runInfo : default);
|
||||
|
||||
// ---- Goal (hex-pip meter, or a continuous bar for large targets) ----
|
||||
if (SystemAPI.TryGetSingleton<GoalProgress>(out var goal))
|
||||
@@ -255,9 +323,31 @@ namespace ProjectM.Client
|
||||
_oreNum.text = ore.ToString();
|
||||
_bioNum.text = bio.ToString();
|
||||
_chargeNum.text = charge.ToString();
|
||||
|
||||
// ---- Step 14 (meta shop): Staging-only permanent-upgrade shop for the LOCAL class. Class derives from
|
||||
// the replicated AbilityRef (tracks the dev class-switch; PlayerClass is server-only); tiers from the
|
||||
// replicated MetaTierState record on the director ghost; Aether from the ledger read above. ----
|
||||
byte localClass = ClassTraits.WarriorClass;
|
||||
bool haveLocalPlayer = false;
|
||||
foreach (var ar in SystemAPI.Query<RefRO<AbilityRef>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
||||
{
|
||||
localClass = ClassTraits.ClassForAbility(ar.ValueRO.Id);
|
||||
haveLocalPlayer = true;
|
||||
break;
|
||||
}
|
||||
bool metaShow = false;
|
||||
BlobAssetReference<MetaUpgradeCatalogBlob> metaPool = default;
|
||||
DynamicBuffer<MetaTierState> metaRecord = default;
|
||||
if (haveRun && runInfo.Lifecycle == RunLifecycle.Staging && haveLocalPlayer && !siege
|
||||
&& SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat) && metaCat.Value.IsCreated
|
||||
&& SystemAPI.TryGetSingletonBuffer<MetaTierState>(out metaRecord, true))
|
||||
{
|
||||
metaPool = metaCat.Value;
|
||||
metaShow = true;
|
||||
}
|
||||
UpdateMetaShop(metaShow, localClass, aether, metaPool, metaRecord);
|
||||
// DR-042 C6a: dim the Aether upgrade button when it isn't affordable (cost is a compile-time const).
|
||||
if (_upgradeBtn != null)
|
||||
_upgradeBtn.style.opacity = aether >= Tuning.AbilityUpgradeCostAmount ? 1f : 0.5f;
|
||||
// (Step 11: upgrade-button affordability tint retired with the button.)
|
||||
// EB-2 quiet-turret cue (GLOBAL, not per-turret, so the deterministic Charge split never reads as one
|
||||
// broken turret): a dry base during a siege tells the player to build a Fabricator.
|
||||
if (siege && charge == 0 && !onExpedition)
|
||||
@@ -848,9 +938,8 @@ namespace ProjectM.Client
|
||||
strip.Add(ResourceChip(null, ChargeViolet, "0", out _chargeNum, 26, 20)); // EB-2 turret ammo (flat violet, no icon)
|
||||
// DR-042 C6a: the only Aether sink (ability-damage upgrade) gets a visible, clickable button (was U-key
|
||||
// only). The Button element handles its own picking even though the HUD root Ignores clicks.
|
||||
_upgradeBtn = MenuUi.Button("UPGRADE DMG (" + Tuning.AbilityUpgradeCostAmount + " AETHER)", BuildSendSystem.UpgradeAbility);
|
||||
_upgradeBtn.style.marginLeft = 18;
|
||||
strip.Add(_upgradeBtn);
|
||||
// (Step 11: the Aether UPGRADE-DMG button was RETIRED with AbilityUpgradeRequest — the choice-of-3
|
||||
// boon modal + the base meta-shop (Step 14) replace it.)
|
||||
|
||||
root.Add(strip);
|
||||
}
|
||||
@@ -1138,5 +1227,243 @@ namespace ProjectM.Client
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Step 14: boon modal + route panel (lazy-built overlays; clicks -> client send statics) ====
|
||||
|
||||
byte _routeShownFor; // last (room ^ options) signature the route buttons were built for
|
||||
|
||||
static string RoomTypeLabel(byte roomType) => roomType == RoomTypeId.Boss ? "[BOSS]"
|
||||
: roomType == RoomTypeId.Elite ? "[ELITE]"
|
||||
: roomType == RoomTypeId.Reward ? "[REWARD]" : "[COMBAT]";
|
||||
|
||||
void UpdateBoonModal(BoonOffer offer, bool show, BlobAssetReference<BoonCatalogBlob> pool)
|
||||
{
|
||||
if (!show || !pool.IsCreated)
|
||||
{
|
||||
if (_boonModal != null) _boonModal.style.display = DisplayStyle.None;
|
||||
_boonShownFor = 0;
|
||||
return;
|
||||
}
|
||||
var root = _doc != null ? _doc.rootVisualElement : null;
|
||||
if (root == null) return;
|
||||
if (!_boonModalBuilt)
|
||||
{
|
||||
BuildBoonModal(root);
|
||||
_boonModalBuilt = true;
|
||||
}
|
||||
|
||||
// Rebuild the three cards only when the offer actually changes (a new room's deal).
|
||||
byte sig = (byte)(offer.Option0 ^ (offer.Option1 * 3) ^ (offer.Option2 * 7));
|
||||
if (sig == 0) sig = 1;
|
||||
if (_boonShownFor != sig)
|
||||
{
|
||||
_boonCardRow.Clear();
|
||||
ref var defs = ref pool.Value;
|
||||
for (byte k = 0; k < 3; k++)
|
||||
{
|
||||
byte id = k == 2 ? offer.Option2 : k == 1 ? offer.Option1 : offer.Option0;
|
||||
int idx = BoonMath.FindDef(ref defs, id);
|
||||
string title = idx >= 0 ? defs.Defs[idx].Name.ToString() : ("BOON " + id);
|
||||
string desc = idx >= 0 ? defs.Defs[idx].Desc.ToString() : "";
|
||||
byte pick = k; // capture a COPY into the closure, never the loop variable
|
||||
var card = MenuUi.Button(title + "\n" + desc, () => BoonSendSystem.PickBoon(pick));
|
||||
card.style.width = 200;
|
||||
card.style.height = 96;
|
||||
card.style.marginLeft = 8;
|
||||
card.style.marginRight = 8;
|
||||
card.style.whiteSpace = WhiteSpace.Normal;
|
||||
_boonCardRow.Add(card);
|
||||
}
|
||||
_boonShownFor = sig;
|
||||
}
|
||||
_boonModal.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
void BuildBoonModal(VisualElement root)
|
||||
{
|
||||
_boonModal = new VisualElement { pickingMode = PickingMode.Ignore };
|
||||
_boonModal.style.position = Position.Absolute;
|
||||
_boonModal.style.left = 0; _boonModal.style.right = 0;
|
||||
_boonModal.style.top = 0; _boonModal.style.bottom = 0;
|
||||
_boonModal.style.alignItems = Align.Center;
|
||||
_boonModal.style.justifyContent = Justify.Center;
|
||||
_boonModal.style.display = DisplayStyle.None;
|
||||
|
||||
var box = new VisualElement();
|
||||
box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.96f);
|
||||
box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
|
||||
box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
|
||||
box.style.paddingLeft = 18; box.style.paddingRight = 18;
|
||||
box.style.paddingTop = 14; box.style.paddingBottom = 16;
|
||||
box.style.alignItems = Align.Center;
|
||||
|
||||
var title = new Label("ROOM CLEARED — CHOOSE A BOON");
|
||||
title.style.color = new Color(0.6f, 1f, 0.7f);
|
||||
title.style.fontSize = 18;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.marginBottom = 12;
|
||||
box.Add(title);
|
||||
|
||||
_boonCardRow = new VisualElement();
|
||||
_boonCardRow.style.flexDirection = FlexDirection.Row;
|
||||
box.Add(_boonCardRow);
|
||||
|
||||
_boonModal.Add(box);
|
||||
root.Add(_boonModal);
|
||||
}
|
||||
|
||||
void UpdateRoutePanel(RunInfo runInfo)
|
||||
{
|
||||
// Keyed on the LIFECYCLE (never RouteOptionCount alone — the review's D-F6 criterion).
|
||||
bool show = runInfo.Lifecycle == RunLifecycle.RouteSelect && runInfo.RouteOptionCount > 0;
|
||||
if (!show)
|
||||
{
|
||||
if (_routePanel != null) _routePanel.style.display = DisplayStyle.None;
|
||||
_routeShownFor = 0;
|
||||
return;
|
||||
}
|
||||
var root = _doc != null ? _doc.rootVisualElement : null;
|
||||
if (root == null) return;
|
||||
if (!_routePanelBuilt)
|
||||
{
|
||||
BuildRoutePanel(root);
|
||||
_routePanelBuilt = true;
|
||||
}
|
||||
|
||||
byte sig = (byte)((runInfo.CurrentRoom + 1) ^ (runInfo.RouteOpt0Type * 3)
|
||||
^ (runInfo.RouteOpt1Type * 5) ^ (runInfo.RouteOpt2Type * 7) ^ runInfo.RouteOptionCount);
|
||||
if (sig == 0) sig = 1;
|
||||
if (_routeShownFor != sig)
|
||||
{
|
||||
_routeBtnRow.Clear();
|
||||
for (byte k = 0; k < runInfo.RouteOptionCount && k < 3; k++)
|
||||
{
|
||||
byte type = k == 2 ? runInfo.RouteOpt2Type : k == 1 ? runInfo.RouteOpt1Type : runInfo.RouteOpt0Type;
|
||||
byte pick = k; // closure copy
|
||||
var b = MenuUi.Button("→ " + RoomTypeLabel(type), () => RouteSendSystem.PickRoute(pick));
|
||||
b.style.marginLeft = 6;
|
||||
b.style.marginRight = 6;
|
||||
_routeBtnRow.Add(b);
|
||||
}
|
||||
_routeTitle.text = "CHOOSE YOUR PATH — room " + (runInfo.CurrentRoom + 2) + "/" + runInfo.RoomCount;
|
||||
_routeShownFor = sig;
|
||||
}
|
||||
_routePanel.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
void BuildRoutePanel(VisualElement root)
|
||||
{
|
||||
_routePanel = new VisualElement { pickingMode = PickingMode.Ignore };
|
||||
_routePanel.style.position = Position.Absolute;
|
||||
_routePanel.style.left = 0; _routePanel.style.right = 0;
|
||||
_routePanel.style.bottom = 120;
|
||||
_routePanel.style.alignItems = Align.Center;
|
||||
_routePanel.style.display = DisplayStyle.None;
|
||||
|
||||
var box = new VisualElement();
|
||||
box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.94f);
|
||||
box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
|
||||
box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
|
||||
box.style.paddingLeft = 16; box.style.paddingRight = 16;
|
||||
box.style.paddingTop = 10; box.style.paddingBottom = 12;
|
||||
box.style.alignItems = Align.Center;
|
||||
|
||||
_routeTitle = new Label("CHOOSE YOUR PATH");
|
||||
_routeTitle.style.color = new Color(0.55f, 0.85f, 1f);
|
||||
_routeTitle.style.fontSize = 15;
|
||||
_routeTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_routeTitle.style.marginBottom = 8;
|
||||
box.Add(_routeTitle);
|
||||
|
||||
_routeBtnRow = new VisualElement();
|
||||
_routeBtnRow.style.flexDirection = FlexDirection.Row;
|
||||
box.Add(_routeBtnRow);
|
||||
|
||||
_routePanel.Add(box);
|
||||
root.Add(_routePanel);
|
||||
}
|
||||
|
||||
void UpdateMetaShop(bool show, byte classId, int aether,
|
||||
BlobAssetReference<MetaUpgradeCatalogBlob> pool, DynamicBuffer<MetaTierState> record)
|
||||
{
|
||||
if (!show)
|
||||
{
|
||||
if (_metaPanel != null) _metaPanel.style.display = DisplayStyle.None;
|
||||
_metaShownFor = 0;
|
||||
return;
|
||||
}
|
||||
var root = _doc != null ? _doc.rootVisualElement : null;
|
||||
if (root == null) return;
|
||||
if (!_metaShopBuilt)
|
||||
{
|
||||
BuildMetaShop(root);
|
||||
_metaShopBuilt = true;
|
||||
}
|
||||
|
||||
// Rebuild the rows only when class / owned tiers / affordability actually change (Staging-only, <=8 rows).
|
||||
int sig = classId * 131 ^ aether * 31;
|
||||
for (int i = 0; i < record.Length; i++)
|
||||
sig ^= (record[i].ClassId * 7 + record[i].UpgradeId * 13 + record[i].Tier) * (i + 3);
|
||||
if (sig == 0) sig = 1;
|
||||
if (_metaShownFor != sig)
|
||||
{
|
||||
_metaRowsHost.Clear();
|
||||
_metaShopTitle.text = (classId == ClassTraits.RangerClass ? "RANGER" : "WARRIOR")
|
||||
+ " PERMANENT UPGRADES - AETHER " + aether;
|
||||
ref var defs = ref pool.Value;
|
||||
byte classBit = BoonMath.MaskFor(classId);
|
||||
for (int d = 0; d < defs.Defs.Length; d++)
|
||||
{
|
||||
if ((defs.Defs[d].ClassMask & classBit) == 0) continue;
|
||||
byte id = defs.Defs[d].Id;
|
||||
byte owned = MetaMath.TierOf(record, classId, id);
|
||||
bool maxed = owned >= defs.Defs[d].MaxTier;
|
||||
int cost = MetaMath.CostForTier(in defs.Defs[d], owned);
|
||||
string label = defs.Defs[d].Name.ToString() + " [" + owned + "/" + defs.Defs[d].MaxTier + "]"
|
||||
+ (maxed ? " MAXED" : " - " + cost + " Aether")
|
||||
+ "\n" + defs.Defs[d].Desc.ToString();
|
||||
byte buyId = id; // closure copy, never the loop variable
|
||||
var row = MenuUi.Button(label, () => MetaSpendSendSystem.RequestPurchase(buyId));
|
||||
row.style.width = 290;
|
||||
row.style.marginBottom = 4;
|
||||
row.style.whiteSpace = WhiteSpace.Normal;
|
||||
row.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
row.SetEnabled(!maxed && aether >= cost); // honest UI; the server re-validates everything anyway
|
||||
_metaRowsHost.Add(row);
|
||||
}
|
||||
_metaShownFor = sig;
|
||||
}
|
||||
_metaPanel.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
void BuildMetaShop(VisualElement root)
|
||||
{
|
||||
_metaPanel = new VisualElement { pickingMode = PickingMode.Ignore };
|
||||
_metaPanel.style.position = Position.Absolute;
|
||||
_metaPanel.style.right = 12;
|
||||
_metaPanel.style.top = Length.Percent(22);
|
||||
_metaPanel.style.alignItems = Align.FlexEnd;
|
||||
_metaPanel.style.display = DisplayStyle.None;
|
||||
|
||||
var box = new VisualElement();
|
||||
box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.92f);
|
||||
box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
|
||||
box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
|
||||
box.style.paddingLeft = 12; box.style.paddingRight = 12;
|
||||
box.style.paddingTop = 10; box.style.paddingBottom = 10;
|
||||
|
||||
_metaShopTitle = new Label("PERMANENT UPGRADES");
|
||||
_metaShopTitle.style.color = AetherCyan;
|
||||
_metaShopTitle.style.fontSize = 14;
|
||||
_metaShopTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_metaShopTitle.style.marginBottom = 8;
|
||||
box.Add(_metaShopTitle);
|
||||
|
||||
_metaRowsHost = new VisualElement();
|
||||
box.Add(_metaRowsHost);
|
||||
|
||||
_metaPanel.Add(box);
|
||||
root.Add(_metaPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,30 @@ namespace ProjectM.Client
|
||||
Color expFog = cfg != null ? cfg.ExpeditionFogColor : new Color(0.851f, 0.549f, 0.227f, 1f);
|
||||
float expDen = cfg != null ? cfg.ExpeditionFogDensity : 0.010f;
|
||||
Color expAmb = cfg != null ? cfg.ExpeditionAmbientSky : new Color(0.910f, 0.769f, 0.604f, 1f);
|
||||
// Step 14 (expedition redesign): the EXPEDITION-side palette keys on the replicated per-room biome
|
||||
// (RunInfo.CurrentBiome) so every room re-themes — the camera-X blend still owns the base↔run fade.
|
||||
// Palettes are code consts (cosmetic; per-biome config knobs can layer on later without churn).
|
||||
if (SystemAPI.TryGetSingleton<ProjectM.Simulation.RunInfo>(out var runInfo)
|
||||
&& runInfo.Lifecycle != ProjectM.Simulation.RunLifecycle.Staging)
|
||||
{
|
||||
switch (runInfo.CurrentBiome)
|
||||
{
|
||||
case ProjectM.Simulation.RoomBiomeId.Meadow:
|
||||
expFog = new Color(0.62f, 0.82f, 0.62f, 1f); expDen = 0.008f;
|
||||
expAmb = new Color(0.68f, 0.88f, 0.70f, 1f);
|
||||
break;
|
||||
case ProjectM.Simulation.RoomBiomeId.Cavern:
|
||||
expFog = new Color(0.42f, 0.48f, 0.60f, 1f); expDen = 0.014f;
|
||||
expAmb = new Color(0.50f, 0.56f, 0.72f, 1f);
|
||||
break;
|
||||
case ProjectM.Simulation.RoomBiomeId.Blight:
|
||||
expFog = new Color(0.55f, 0.42f, 0.62f, 1f); expDen = 0.016f;
|
||||
expAmb = new Color(0.62f, 0.50f, 0.70f, 1f);
|
||||
break;
|
||||
// Arid keeps the config/default orange above.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float x = _cam.transform.position.x;
|
||||
float t = Mathf.Clamp01((x - (boundary - half)) / (2f * half));
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace ProjectM.Client
|
||||
Body(c, "Turret (40 Ore) — auto-fires at enemies. Needs Charge as ammo.");
|
||||
Body(c, "Fabricator (30 Ore) — converts Ore → Charge so turrets keep firing.");
|
||||
Body(c, "Wall (Biomass) — a cheap barrier that blocks enemies.");
|
||||
Body(c, "Aether — spend it on UPGRADE DMG to boost your damage.");
|
||||
Body(c, "Aether — rare; fuels PERMANENT class upgrades at the base between runs.");
|
||||
Body(c, "Open Build with Tab (Y), pick a piece, click a green tile to place it.");
|
||||
break;
|
||||
case 3: // Threats
|
||||
|
||||
@@ -55,6 +55,11 @@ namespace ProjectM.Client
|
||||
public Sprite FabricatorIcon;
|
||||
public Sprite ConveyorIcon;
|
||||
|
||||
[Header("Build-ghost preview meshes (authored real-size, ground pivot — BuildSendSystem)")]
|
||||
public Mesh TurretGhostMesh;
|
||||
public Mesh WallGhostMesh;
|
||||
public Mesh FabricatorGhostMesh;
|
||||
|
||||
[Header("Build-mode control glyphs")]
|
||||
public Sprite KbmPlace; // LMB
|
||||
public Sprite KbmCancel; // RMB
|
||||
@@ -91,6 +96,18 @@ namespace ProjectM.Client
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Placement-ghost preview mesh for a <see cref="StructureType"/> byte (null → the cube fallback).</summary>
|
||||
public Mesh StructureGhostMesh(byte type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case StructureType.Turret: return TurretGhostMesh;
|
||||
case StructureType.Wall: return WallGhostMesh;
|
||||
case StructureType.Fabricator: return FabricatorGhostMesh;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- cached SDF font definitions (one FontAsset per font, built once, reset per play session) ----
|
||||
static FontAsset _displayFa, _bodyFa, _bodyLightFa;
|
||||
static bool _displayTried, _bodyTried, _bodyLightTried;
|
||||
|
||||
@@ -120,7 +120,16 @@ namespace ProjectM.Client
|
||||
if (data == null) return;
|
||||
var em = server.EntityManager;
|
||||
var e = em.CreateEntity();
|
||||
em.AddComponentData(e, new PendingSave { GoalCharge = data.GoalCharge, GoalTarget = data.GoalTarget, CoreCurrent = data.CoreCurrent, RunOutcome = (byte)data.RunOutcome, HasData = 1 });
|
||||
// v5->v6 migration (operator-approved): an old save's Charge counted boss-cleared runs (DR-042), so a
|
||||
// missing RunsCompleted floors to it — the HUD never shows "Charge 3/4" beside "Runs completed: 0".
|
||||
int runsCompleted = data.RunsCompleted > data.GoalCharge ? data.RunsCompleted : data.GoalCharge;
|
||||
em.AddComponentData(e, new PendingSave { GoalCharge = data.GoalCharge, GoalTarget = data.GoalTarget, CoreCurrent = data.CoreCurrent, RunOutcome = (byte)data.RunOutcome, RunsCompleted = runsCompleted, MaxDepthReached = data.MaxDepthReached, HasData = 1 });
|
||||
// v6: stage the meta tiers UNCONDITIONALLY (empty OK — the Bursted spawn system GetBuffers it in the
|
||||
// HasData block; a conditional buffer would throw on any v<=5 Continue). Rows verbatim, no clamping.
|
||||
var mbuf = em.AddBuffer<PendingMetaRow>(e);
|
||||
if (data.MetaUpgrades != null)
|
||||
foreach (var mrow in data.MetaUpgrades)
|
||||
mbuf.Add(new PendingMetaRow { ClassId = mrow.ClassId, UpgradeId = mrow.UpgradeId, Tier = mrow.Tier });
|
||||
var buf = em.AddBuffer<PendingSaveLedgerRow>(e);
|
||||
if (data.Ledger != null)
|
||||
foreach (var row in data.Ledger)
|
||||
@@ -167,6 +176,9 @@ namespace ProjectM.Client
|
||||
var st = tq.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (st.IsValid) nowTick = st.TickIndexForValidTick;
|
||||
}
|
||||
// v6: the permanent-meta slice via the ONE shared collector — omitting it HERE (the most common
|
||||
// exit path) would silently WIPE all meta progression on quit (the meta review's top blocker).
|
||||
MetaSaveScan.Collect(em, dir, out var metaRows, out var runsCompleted, out var maxDepth);
|
||||
SaveStructureScan.Collect(em, nowTick, out var structures, out var structureIo);
|
||||
|
||||
SaveService.Save(new SaveData
|
||||
@@ -174,6 +186,9 @@ namespace ProjectM.Client
|
||||
GoalCharge = goal.Charge,
|
||||
GoalTarget = goal.Target,
|
||||
CoreCurrent = core.Current,
|
||||
RunsCompleted = runsCompleted,
|
||||
MaxDepthReached = maxDepth,
|
||||
MetaUpgrades = metaRows,
|
||||
RunOutcome = outcome.Value,
|
||||
|
||||
Ledger = rows,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23831c9502abad541a3b2a3dc65502c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,59 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side ready-toggle sender: a static enqueue (HUD button at Step 14 / the T dev key / execute_code)
|
||||
/// drained into <see cref="ReadyToggleRequest"/> RPC entities — the BuildSendSystem queue+drain idiom. The local
|
||||
/// bool tracks only the toggle DIRECTION; the server-replicated <see cref="PlayerReady"/> is the truth the HUD
|
||||
/// renders. Statics reset on play-enter (statics survive fast-enter-playmode reloads — the stale-bridge hazard).
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class ReadySendSystem : SystemBase
|
||||
{
|
||||
static int s_Pending; // queued explicit sets
|
||||
static byte s_PendingValue;
|
||||
static bool s_LocalReady; // last requested state (toggle direction only, not authority)
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void ResetStatics()
|
||||
{
|
||||
s_Pending = 0;
|
||||
s_PendingValue = 0;
|
||||
s_LocalReady = false;
|
||||
}
|
||||
|
||||
/// <summary>Queue an explicit ready set (HUD button / execute_code).</summary>
|
||||
public static void SetReady(bool ready)
|
||||
{
|
||||
s_PendingValue = (byte)(ready ? 1 : 0);
|
||||
s_Pending++;
|
||||
s_LocalReady = ready;
|
||||
}
|
||||
|
||||
/// <summary>Queue a toggle of the last requested state (the T dev key; HUD replaces this at Step 14).</summary>
|
||||
public static void ToggleReady() => SetReady(!s_LocalReady);
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
||||
if (keyboard != null && keyboard.tKey.wasPressedThisFrame && !PauseMenuController.Open)
|
||||
ToggleReady();
|
||||
|
||||
while (s_Pending > 0)
|
||||
{
|
||||
s_Pending--;
|
||||
var req = EntityManager.CreateEntity(typeof(ReadyToggleRequest), typeof(SendRpcCommandRequest));
|
||||
EntityManager.SetComponentData(req, new ReadyToggleRequest { Ready = s_PendingValue });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7047cb8f6861ba8498f33698c9948dc8
|
||||
@@ -0,0 +1,60 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side route-pick sender: a static enqueue (the Step-14 map panel's option buttons / execute_code)
|
||||
/// drained into <see cref="RouteSelectRequest"/> RPCs. The request is stamped from the CLIENT's replicated
|
||||
/// <see cref="RunInfo"/>: <c>ForRunEpoch = (int)RunSeed</c> (the re-meaned run-identity token — the server-only
|
||||
/// RunEpoch is not client-knowable) and <c>ForLayer = CurrentRoom</c> VERBATIM (during a gate that is still the
|
||||
/// just-cleared layer — never +1). The server re-validates everything; a stale/possessed pick is simply dropped.
|
||||
/// Statics reset on play-enter (the stale-bridge hazard).
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class RouteSendSystem : SystemBase
|
||||
{
|
||||
static int s_Pending;
|
||||
static byte s_PendingIndex;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void ResetStatics()
|
||||
{
|
||||
s_Pending = 0;
|
||||
s_PendingIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Queue a route pick (0..RouteOptionCount-1). HUD map panel + execute_code drive this.</summary>
|
||||
public static void PickRoute(byte optionIndex)
|
||||
{
|
||||
s_PendingIndex = optionIndex;
|
||||
s_Pending++;
|
||||
}
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
RequireForUpdate<RunInfo>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if (s_Pending == 0)
|
||||
return;
|
||||
var runInfo = SystemAPI.GetSingleton<RunInfo>();
|
||||
while (s_Pending > 0)
|
||||
{
|
||||
s_Pending--;
|
||||
var req = EntityManager.CreateEntity(typeof(RouteSelectRequest), typeof(SendRpcCommandRequest));
|
||||
EntityManager.SetComponentData(req, new RouteSelectRequest
|
||||
{
|
||||
OptionIndex = s_PendingIndex,
|
||||
ForRunEpoch = (int)runInfo.RunSeed, // the re-meaned replicated run token
|
||||
ForLayer = runInfo.CurrentRoom, // the gate's un-incremented cleared layer
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c22921cccedc3564b86d12491d88488e
|
||||
Reference in New Issue
Block a user