Vault Re-Alignment

This commit is contained in:
2026-06-09 23:26:20 -07:00
parent a7405c3f38
commit da522efe7a
63 changed files with 119048 additions and 15 deletions
@@ -62,6 +62,20 @@ namespace ProjectM.Client
if (GUILayout.Button("Go Expedition")) DebugCommandSendSystem.Teleport(RegionId.Expedition);
GUILayout.EndHorizontal();
GUILayout.Space(6);
GUILayout.Label("- Telemetry (MC-0) -");
if (DevTelemetryReadout.HasData)
{
var t = DevTelemetryReadout.Latest;
GUILayout.Label($"tick {t.LastSampleTick} husks {t.LiveEnemyCount}");
GUILayout.Label($"dash neg {t.DashIFrameNegatedHits} / wasted {t.DashesWasted}");
GUILayout.Label($"whiff open {t.ChargerWhiffWindowsOpened} / punish {t.ChargerWhiffPunishesLanded}");
}
else
{
GUILayout.Label("(waiting for server telemetry...)");
}
GUILayout.EndArea();
}
@@ -0,0 +1,78 @@
#if UNITY_EDITOR
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
namespace ProjectM.Client
{
/// <summary>
/// MC-0 — EDITOR-ONLY client receiver for the periodic <see cref="ProjectM.Simulation.DebugTelemetryReport"/>.
/// Drains the snapshot into <see cref="DevTelemetryReadout"/> (a plain static) so the IMGUI <c>DebugOverlay</c>
/// reads NO ECS state directly (the job-safety rule for presentation). Plain client
/// <see cref="SimulationSystemGroup"/>; non-Burst (touches a managed static).
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct DevTelemetryReceiveSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<ProjectM.Simulation.DebugTelemetryReport, ReceiveRpcCommandRequest>();
state.RequireForUpdate(state.GetEntityQuery(builder));
}
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (report, reqEntity) in
SystemAPI.Query<RefRO<ProjectM.Simulation.DebugTelemetryReport>>()
.WithAll<ReceiveRpcCommandRequest>().WithEntityAccess())
{
var r = report.ValueRO;
DevTelemetryReadout.Latest = new DevTelemetryReadout.Snapshot
{
DashIFrameNegatedHits = r.DashIFrameNegatedHits,
DashesWasted = r.DashesWasted,
ChargerWhiffWindowsOpened = r.ChargerWhiffWindowsOpened,
ChargerWhiffPunishesLanded = r.ChargerWhiffPunishesLanded,
LiveEnemyCount = r.LiveEnemyCount,
LastSampleTick = r.LastSampleTick,
};
DevTelemetryReadout.HasData = true;
ecb.DestroyEntity(reqEntity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
/// <summary>
/// MC-0 — static bridge from the ECS telemetry receiver to the IMGUI <c>DebugOverlay</c> (so the overlay reads
/// a plain struct, never ECS state). Reset on play-enter so a fast-enter-playmode reload can't show stale data.
/// </summary>
public static class DevTelemetryReadout
{
public struct Snapshot
{
public uint DashIFrameNegatedHits;
public uint DashesWasted;
public uint ChargerWhiffWindowsOpened;
public uint ChargerWhiffPunishesLanded;
public uint LiveEnemyCount;
public uint LastSampleTick;
}
public static Snapshot Latest;
public static bool HasData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void Reset()
{
Latest = default;
HasData = false;
}
}
}
#endif
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 685c34765eac3434aadf08c13fce1aa5
@@ -77,6 +77,7 @@ namespace ProjectM.Client
var gamepad = UnityEngine.InputSystem.Gamepad.current;
var mouse = UnityEngine.InputSystem.Mouse.current;
var keyboard = UnityEngine.InputSystem.Keyboard.current;
bool dashPressed = ((keyboard != null && keyboard.leftShiftKey.wasPressedThisFrame) || (gamepad != null && gamepad.buttonEast.wasPressedThisFrame)) && !BuildPaletteState.Active;
float2 rightStick = float2.zero;
bool gamepadActive = false;
@@ -102,7 +103,7 @@ namespace ProjectM.Client
kbmActive =
keyboard.wKey.isPressed || keyboard.aKey.isPressed || keyboard.sKey.isPressed || keyboard.dKey.isPressed ||
keyboard.upArrowKey.isPressed || keyboard.downArrowKey.isPressed ||
keyboard.leftArrowKey.isPressed || keyboard.rightArrowKey.isPressed || keyboard.spaceKey.isPressed;
keyboard.leftArrowKey.isPressed || keyboard.rightArrowKey.isPressed || keyboard.spaceKey.isPressed || keyboard.leftShiftKey.isPressed;
}
if (gamepadActive && kbmActive)
@@ -160,6 +161,9 @@ namespace ProjectM.Client
input.ValueRW.Fire = default;
if (firePressed)
input.ValueRW.Fire.Set();
input.ValueRW.Dash = default;
if (dashPressed)
input.ValueRW.Dash.Set();
}
}
@@ -52,14 +52,18 @@ namespace ProjectM.Client
ParticleSystem _hitFx;
ParticleSystem _deathFx;
ParticleSystem _muzzleFx;
ParticleSystem _dashFx;
AudioClip _hitClip;
AudioClip _deathClip;
AudioClip _fireClip;
AudioClip _telegraphClip;
AudioClip _dashClip;
Entity _localPlayer = Entity.Null;
uint _lastLocalFireTick;
bool _fireTickInit;
uint _lastLocalDashTick;
bool _dashTickInit;
const int NumberPoolSize = 32;
const int MaxActiveVfx = 40; // bound one-shot VFX GameObject churn under sustained combat
@@ -72,6 +76,7 @@ namespace ProjectM.Client
_deathClip = MakeClip("husk_death", 320f, 50f, 0.34f, 0.55f, noise: false);
_fireClip = MakeClip("fire", 880f, 1500f, 0.07f, 0.30f, noise: false);
_telegraphClip = MakeClip("telegraph", 680f, 1020f, 0.12f, 0.35f, noise: false);
_dashClip = MakeClip("dash", 950f, 240f, 0.12f, 0.50f, noise: false);
}
protected override void OnStartRunning()
@@ -83,6 +88,7 @@ namespace ProjectM.Client
_hitFx = MakeBurst("HitSparks", mat, new Color(3f, 2.2f, 0.6f), 0.13f, 7f, 0.32f, 256);
_deathFx = MakeBurst("DeathBurst", mat, new Color(3.2f, 0.7f, 0.25f), 0.22f, 9f, 0.55f, 512);
_muzzleFx = MakeBurst("Muzzle", mat, new Color(0.6f, 2.4f, 3.2f), 0.12f, 5f, 0.20f, 128);
_dashFx = MakeBurst("DashWhoosh", mat, new Color(0.7f, 2.6f, 3.0f), 0.16f, 4f, 0.30f, 256);
for (int i = 0; i < NumberPoolSize; i++)
_numbers.Add(CreateNumber());
@@ -104,6 +110,8 @@ namespace ProjectM.Client
EntityManager.CompleteDependencyBeforeRO<Health>();
EntityManager.CompleteDependencyBeforeRO<LocalTransform>();
EntityManager.CompleteDependencyBeforeRO<AttackWindup>();
EntityManager.CompleteDependencyBeforeRO<DashState>();
EntityManager.CompleteDependencyBeforeRO<DashCooldown>();
// Resolve the local player (for hit colouring + fire feedback).
_localPlayer = Entity.Null;
@@ -115,6 +123,17 @@ namespace ProjectM.Client
localPos = xf.ValueRO.Position;
}
// Client-derived dash window of the LOCAL player (DashSystem runs in the client prediction loop
// too): drives the i-frame shimmer + the hit-feedback suppression below. Observe-only.
bool localIFrameActive = false;
if (_localPlayer != Entity.Null && EntityManager.HasComponent<DashState>(_localPlayer)
&& SystemAPI.TryGetSingleton<NetworkTime>(out var dashNetTime) && dashNetTime.ServerTick.IsValid)
{
var localDash = EntityManager.GetComponentData<DashState>(_localPlayer);
localIFrameActive = localDash.IFrameUntilTick != 0u
&& new NetworkTick(localDash.IFrameUntilTick).IsNewerThan(dashNetTime.ServerTick);
}
// Edge-detect Health on every damageable ghost (players + Husks).
_seen.Clear();
foreach (var (health, xf, entity) in
@@ -136,7 +155,9 @@ namespace ProjectM.Client
PlayClip(_telegraphClip, (Vector3)p, 0.5f);
}
if (cur < prev.Hp - 0.001f)
// Local hit feedback is SUPPRESSED while the local i-frame window is active: the server
// negates the hit; any transient Health dip is reconciliation flicker, not a real hit.
if (cur < prev.Hp - 0.001f && !(isLocalPlayer && localIFrameActive && FeelConfig.DashHitSuppress))
{
SpawnNumber(prev.Hp - cur, (Vector3)p, isLocalPlayer, cam);
Burst(_hitFx, cfg != null ? cfg.Hit : null, (Vector3)p + Vector3.up * 0.8f, FeelConfig.HitBurstCount);
@@ -201,6 +222,26 @@ namespace ProjectM.Client
_fireTickInit = true;
}
// Local-player dash feedback (MC-1): DashCooldown.NextTick advances exactly once per dash
// (replicated [GhostField], predicted both sides; raw uint edge like the muzzle flash — cosmetic
// only). Whoosh + afterimage burst + camera punch on start, shimmer trail while i-frames last.
if (_localPlayer != Entity.Null && EntityManager.HasComponent<DashCooldown>(_localPlayer))
{
uint nextDash = EntityManager.GetComponentData<DashCooldown>(_localPlayer).NextTick;
if (_dashTickInit && nextDash != 0 && nextDash != _lastLocalDashTick)
{
EmitAt(_dashFx, (Vector3)localPos + Vector3.up * 0.6f, FeelConfig.DashBurstCount);
PlayClip(_dashClip, (Vector3)localPos, FeelConfig.DashSfxVolume);
PrototypeCameraRig.AddShake(FeelConfig.DashShake);
PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick, FeelConfig.HitStopDurationMs);
}
_lastLocalDashTick = nextDash;
_dashTickInit = true;
if (localIFrameActive) // i-frame shimmer trail while the local window is active
EmitAt(_dashFx, (Vector3)localPos + Vector3.up * 0.7f, FeelConfig.DashShimmerPerFrame);
}
UpdateProjectileTrails(cfg);
PruneVfx();
AnimateNumbers(dt, cam);
@@ -74,6 +74,21 @@ namespace ProjectM.Client
/// <summary>Tether line width (world units).</summary>
public static float LockOnLineWidth;
// ---- Feature 5 (MC-1): dash juice ----
/// <summary>Camera shake on the local player's dash start.</summary>
public static float DashShake;
/// <summary>Transient FOV punch (degrees) on dash start — the "lurch" read (camera punch, never Time.timeScale).</summary>
public static float DashFovKick;
/// <summary>Afterimage/whoosh particle burst count at dash start.</summary>
public static int DashBurstCount;
/// <summary>Dash whoosh SFX volume.</summary>
public static float DashSfxVolume;
/// <summary>Particles emitted per frame while the local i-frame window is active (the shimmer trail).</summary>
public static int DashShimmerPerFrame;
/// <summary>Suppress local hit-feedback during the local i-frame window (masks the prediction-reconciliation
/// Health flicker on a clean dodge — the documented acceptable-not-a-bug interaction).</summary>
public static bool DashHitSuppress;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
public static void ResetDefaults()
{
@@ -108,6 +123,14 @@ namespace ProjectM.Client
LockOnArcDegrees = 60f;
LockOnLineColor = new Color(0.55f, 0.9f, 1f, 0.35f);
LockOnLineWidth = 0.05f;
// Feature 5 dash (MC-1)
DashShake = 0.18f;
DashFovKick = 1.2f;
DashBurstCount = 14;
DashSfxVolume = 0.55f;
DashShimmerPerFrame = 2;
DashHitSuppress = true;
}
}
}