Deferred feel items: true enemy body hit-flash, co-op remote swing arcs, near-impact strike beep
Clears the three follow-ups deferred by the combat-overhaul pass (c3b53cef2) + the
DR-041 "needs its own ShaderGraph slice" note. All client-only, observe-only presentation
(PresentationSystemGroup; no sim mutation, no [GhostField], no server work).
- Item 1: EnemyHitFlashSystem flashes the ACTUAL enemy body by driving the stock
Entities-Graphics URPMaterialPropertyBaseColor override on the Rukhanka render-entity
LEG children (root has no MaterialMeshInfo) -- NO ShaderGraph edit, no new component type.
Lerp white->BodyFlashColor on a Health-decrease edge, decay back to white. Verified on
screen (the AnimatedLitShader honors the per-instance _BaseColor override).
- Item 2: per-remote-player slash-arc pool in CombatFeedbackSystem, edge-detected from the
replicated MeleeCombo on interpolated teammates (.WithDisabled<GhostOwnerIsLocal>());
BuildSlashMesh -> BuildSlashInto(mesh,...) refactor; local player keeps _slashMr.
- Item 3: once-per-windup near-impact strike beep folded into the danger-cone loop, gated
to a resolved local player.
- 9 new FeelConfig knobs (+ ResetDefaults).
390/390 EditMode, clean compile, zero Play exceptions. 3-lens adversarial review
(wf_8a998c6c-af9) -- no critical/major; fixed 4 minors: spurious beep at base origin before
the local player resolves, frozen tint if BodyFlashEnabled toggles off mid-flash, render-child
capture with no recovery, OnDestroy GO symmetry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,21 @@ namespace ProjectM.Client
|
||||
Material _barBgMat, _barFillMat;
|
||||
// Telegraph scale-pulse (Slice 1, Feature C): per-enemy windup-onset time, folded into the danger cone.
|
||||
readonly Dictionary<Entity, float> _pulseStart = new();
|
||||
// Near-impact strike beep (deferred-items pass): entity -> the WindUpUntilTick it last beeped for (once/windup).
|
||||
readonly Dictionary<Entity, uint> _strikeBeeped = new();
|
||||
|
||||
// Remote teammates' melee cleave arcs (deferred-items pass, co-op): one pooled slash renderer per remote
|
||||
// player, edge-detected from the replicated MeleeCombo.SwingStartTick (the local player keeps _slashMr).
|
||||
class RemoteSlash
|
||||
{
|
||||
public GameObject Go; public Mesh Mesh; public MeshRenderer Mr; public Material Mat;
|
||||
public float Age, Life, Range, Half; public int SweepSign; public Color Tint;
|
||||
public bool Active; public uint LastSwingTick; public bool Init;
|
||||
}
|
||||
readonly Dictionary<Entity, RemoteSlash> _remoteSlashes = new();
|
||||
readonly HashSet<Entity> _remoteSeen = new();
|
||||
readonly List<Entity> _remoteStale = new();
|
||||
|
||||
|
||||
AudioClip _hitClip;
|
||||
AudioClip _deathClip;
|
||||
@@ -152,6 +167,14 @@ namespace ProjectM.Client
|
||||
if (kv.Value != null) { var mf = kv.Value.GetComponent<MeshFilter>(); if (mf != null && mf.sharedMesh != null) Object.Destroy(mf.sharedMesh); }
|
||||
foreach (var kv in _healthBars)
|
||||
if (kv.Value.CanvasGo != null) Object.Destroy(kv.Value.CanvasGo);
|
||||
foreach (var kv in _remoteSlashes)
|
||||
{
|
||||
if (kv.Value.Mesh != null) Object.Destroy(kv.Value.Mesh);
|
||||
if (kv.Value.Mat != null) Object.Destroy(kv.Value.Mat);
|
||||
if (kv.Value.Go != null) Object.Destroy(kv.Value.Go);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
@@ -395,7 +418,8 @@ namespace ProjectM.Client
|
||||
PruneVfx();
|
||||
AnimateNumbers(dt, cam);
|
||||
UpdateSlash(dt);
|
||||
UpdateEnemyDanger();
|
||||
UpdateEnemyDanger(localPos);
|
||||
UpdateRemoteSwings(dt);
|
||||
UpdateHealthBars(dt, cam, localPos);
|
||||
}
|
||||
|
||||
@@ -691,7 +715,7 @@ namespace ProjectM.Client
|
||||
|
||||
// Rebuild the crescent (inner->outer arc) for the LIVE cone half-angle + range, in local +Z-forward space.
|
||||
// `reveal` (0..1) sweeps the arc open from one edge (sweepSign) toward the other so the cleave reads directional.
|
||||
void BuildSlashMesh(float range, float halfAngle, float reveal, int sweepSign)
|
||||
void BuildSlashInto(Mesh mesh, float range, float halfAngle, float reveal, int sweepSign)
|
||||
{
|
||||
const int seg = 16;
|
||||
float r1 = Mathf.Max(0.4f, range);
|
||||
@@ -721,12 +745,12 @@ namespace ProjectM.Client
|
||||
tris[i * 6 + 0] = b; tris[i * 6 + 1] = b + 1; tris[i * 6 + 2] = b + 2;
|
||||
tris[i * 6 + 3] = b + 1; tris[i * 6 + 4] = b + 3; tris[i * 6 + 5] = b + 2;
|
||||
}
|
||||
_slashMesh.Clear();
|
||||
_slashMesh.vertices = verts;
|
||||
_slashMesh.colors = cols;
|
||||
_slashMesh.uv = uvs;
|
||||
_slashMesh.triangles = tris;
|
||||
_slashMesh.RecalculateBounds();
|
||||
mesh.Clear();
|
||||
mesh.vertices = verts;
|
||||
mesh.colors = cols;
|
||||
mesh.uv = uvs;
|
||||
mesh.triangles = tris;
|
||||
mesh.RecalculateBounds();
|
||||
}
|
||||
|
||||
// Trigger a cone-shaped slash matching the LIVE melee range + half-angle, oriented along facing. The arc IS
|
||||
@@ -738,7 +762,7 @@ namespace ProjectM.Client
|
||||
bool finisher = step >= comboLen;
|
||||
_slashRange = range; _slashHalf = halfAngle;
|
||||
_slashSweepSign = (step % 2 == 0) ? -1 : 1; // alternate L->R / R->L per swing -> reads as alternating strikes
|
||||
BuildSlashMesh(range, halfAngle, 0f, _slashSweepSign); // start closed; UpdateSlash sweeps it open
|
||||
BuildSlashInto(_slashMesh, range, halfAngle, 0f, _slashSweepSign); // start closed; UpdateSlash sweeps it open
|
||||
Vector3 f = math.lengthsq(facing) > 1e-6f ? new Vector3(facing.x, 0f, facing.y).normalized : Vector3.forward;
|
||||
var tr = _slashMr.transform;
|
||||
tr.position = pos + Vector3.up * 0.12f;
|
||||
@@ -766,15 +790,105 @@ namespace ProjectM.Client
|
||||
// MC-4 clarity: SWEEP the crescent open across the arc over the first ~60% of life (reads as a blade
|
||||
// travelling through the cleave), then hold + fade — instead of popping the whole cone at once.
|
||||
float reveal = Mathf.Clamp01(u / 0.6f);
|
||||
BuildSlashMesh(_slashRange, _slashHalf, reveal, _slashSweepSign);
|
||||
BuildSlashInto(_slashMesh, _slashRange, _slashHalf, reveal, _slashSweepSign);
|
||||
var c = _slashTint; c.a = u < 0.6f ? 1f : 1f - (u - 0.6f) / 0.4f; _slashMat.color = c;
|
||||
_slashMr.transform.localScale = Vector3.one * (1f + u * 0.10f);
|
||||
}
|
||||
|
||||
// Remote teammates' melee cleave arcs (deferred-items pass, co-op readability): the local player's swing
|
||||
// renders via _slashMr; here each REMOTE player (interpolated, GhostOwnerIsLocal DISABLED) gets a pooled
|
||||
// slash arc edge-detected from its replicated MeleeCombo.SwingStartTick + PlayerFacing. Observe-only client
|
||||
// presentation; no sim, no new [GhostField]. Anchored to the moving teammate while it sweeps open + fades.
|
||||
void UpdateRemoteSwings(float dt)
|
||||
{
|
||||
if (!FeelConfig.RemoteSwingEnabled || _fxRoot == null) return;
|
||||
int comboLen = SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg) ? (int)math.clamp((int)tcfg.MeleeComboLength, 1, 3) : 3;
|
||||
float baseRange = tcfg.MeleeRange > 0f ? tcfg.MeleeRange : 2.6f;
|
||||
float baseHalf = tcfg.MeleeConeHalfAngleRad > 0f ? tcfg.MeleeConeHalfAngleRad : 0.9f;
|
||||
float finisherMult = tcfg.MeleeFinisherMult > 0f ? tcfg.MeleeFinisherMult : 1.8f;
|
||||
|
||||
_remoteSeen.Clear();
|
||||
foreach (var (xf, facing, mc, entity) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<PlayerFacing>, RefRO<MeleeCombo>>()
|
||||
.WithAll<PlayerTag>().WithDisabled<GhostOwnerIsLocal>().WithEntityAccess())
|
||||
{
|
||||
_remoteSeen.Add(entity);
|
||||
if (!_remoteSlashes.TryGetValue(entity, out var rs)) { rs = CreateRemoteSlash(); _remoteSlashes[entity] = rs; }
|
||||
|
||||
uint swing = mc.ValueRO.SwingStartTick;
|
||||
if (rs.Init && swing != 0 && swing != rs.LastSwingTick)
|
||||
{
|
||||
int step = math.max(1, (int)mc.ValueRO.Step);
|
||||
bool finisher = step >= comboLen;
|
||||
rs.Range = finisher ? baseRange * finisherMult : baseRange;
|
||||
rs.Half = baseHalf;
|
||||
rs.SweepSign = (step % 2 == 0) ? -1 : 1;
|
||||
rs.Tint = FeelConfig.RemoteSlashColor * (finisher ? 1.5f : 1f);
|
||||
rs.Life = finisher ? 0.26f : 0.18f;
|
||||
rs.Age = 0f;
|
||||
rs.Active = true;
|
||||
BuildSlashInto(rs.Mesh, rs.Range, rs.Half, 0f, rs.SweepSign);
|
||||
rs.Mat.color = rs.Tint;
|
||||
rs.Mr.enabled = true;
|
||||
}
|
||||
rs.LastSwingTick = swing;
|
||||
rs.Init = true;
|
||||
|
||||
if (rs.Active)
|
||||
{
|
||||
rs.Age += dt;
|
||||
float u = rs.Age / Mathf.Max(1e-4f, rs.Life);
|
||||
if (u >= 1f) { rs.Active = false; rs.Mr.enabled = false; }
|
||||
else
|
||||
{
|
||||
float2 fdir = facing.ValueRO.Direction;
|
||||
Vector3 f = math.lengthsq(fdir) > 1e-6f ? new Vector3(fdir.x, 0f, fdir.y).normalized : Vector3.forward;
|
||||
var tr = rs.Mr.transform;
|
||||
tr.position = (Vector3)xf.ValueRO.Position + Vector3.up * 0.12f;
|
||||
tr.rotation = Quaternion.LookRotation(f, Vector3.up);
|
||||
float reveal = Mathf.Clamp01(u / 0.6f);
|
||||
BuildSlashInto(rs.Mesh, rs.Range, rs.Half, reveal, rs.SweepSign);
|
||||
var c = rs.Tint; c.a = u < 0.6f ? 1f : 1f - (u - 0.6f) / 0.4f; rs.Mat.color = c;
|
||||
tr.localScale = Vector3.one * (1f + u * 0.10f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_remoteSlashes.Count != _remoteSeen.Count)
|
||||
{
|
||||
_remoteStale.Clear();
|
||||
foreach (var kv in _remoteSlashes) if (!_remoteSeen.Contains(kv.Key)) _remoteStale.Add(kv.Key);
|
||||
for (int i = 0; i < _remoteStale.Count; i++)
|
||||
{
|
||||
var rs = _remoteSlashes[_remoteStale[i]];
|
||||
if (rs.Mesh != null) Object.Destroy(rs.Mesh);
|
||||
if (rs.Mat != null) Object.Destroy(rs.Mat);
|
||||
if (rs.Go != null) Object.Destroy(rs.Go);
|
||||
_remoteSlashes.Remove(_remoteStale[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteSlash CreateRemoteSlash()
|
||||
{
|
||||
var go = new GameObject("RemoteSlashArc");
|
||||
go.transform.SetParent(_fxRoot, false);
|
||||
var mesh = new Mesh { name = "RemoteSlashArc" };
|
||||
go.AddComponent<MeshFilter>().sharedMesh = mesh;
|
||||
var mr = go.AddComponent<MeshRenderer>();
|
||||
var mat = MakeParticleMaterial();
|
||||
mat.name = "RemoteSlashArc";
|
||||
mr.sharedMaterial = mat;
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
mr.receiveShadows = false;
|
||||
mr.enabled = false;
|
||||
return new RemoteSlash { Go = go, Mesh = mesh, Mr = mr, Mat = mat, Active = false, Init = false };
|
||||
}
|
||||
|
||||
// Enemy attack TELEGRAPH (MC-4 clarity): while an enemy's AttackWindup counts down, paint a red ground danger
|
||||
// cone in its facing out to its reach, brightening + scaling as the strike nears -> the player reads WHERE +
|
||||
// WHEN to dodge. Client-only, observe-only; one pooled mesh per winding-up enemy, pruned each frame.
|
||||
void UpdateEnemyDanger()
|
||||
void UpdateEnemyDanger(float3 localPos)
|
||||
{
|
||||
if (_fxRoot == null || _dangerMat == null) return;
|
||||
Unity.NetCode.NetworkTick serverTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt) ? nt.ServerTick : default;
|
||||
@@ -804,6 +918,20 @@ namespace ProjectM.Client
|
||||
// any windup length (fixes the Charger plateauing early under the old hard-coded 22).
|
||||
float windupDur = math.max(1f, tele.ValueRO.WindupTicks);
|
||||
intensity = math.saturate(1f - remaining / windupDur);
|
||||
|
||||
// Near-impact strike beep (deferred-items pass): a "dodge NOW" cue once per windup, gated to
|
||||
// enemies near the local player (the danger cone already proves it's winding up to strike).
|
||||
if (FeelConfig.StrikeBeepEnabled && _localPlayer != Entity.Null && remaining <= FeelConfig.StrikeBeepLeadTicks
|
||||
&& (!_strikeBeeped.TryGetValue(entity, out var beepedUntil) || beepedUntil != until))
|
||||
{
|
||||
float3 ep = xf.ValueRO.Position;
|
||||
if (math.distancesq(ep, localPos) <= FeelConfig.StrikeBeepMaxDistSq)
|
||||
{
|
||||
PlayClip(_strikeBeepClip, (Vector3)ep, FeelConfig.StrikeBeepVolume);
|
||||
_strikeBeeped[entity] = until;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Feature C: a short anticipation scale-pulse folded into the client-owned cone (never the ghost).
|
||||
@@ -861,6 +989,8 @@ namespace ProjectM.Client
|
||||
if (g != null) { var mf = g.GetComponent<MeshFilter>(); if (mf != null && mf.sharedMesh != null) Object.Destroy(mf.sharedMesh); Object.Destroy(g); }
|
||||
_dangerZones.Remove(_dangerStale[i]);
|
||||
_pulseStart.Remove(_dangerStale[i]);
|
||||
_strikeBeeped.Remove(_dangerStale[i]);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Collections.Generic;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Rendering; // URPMaterialPropertyBaseColor, MaterialMeshInfo (Entities Graphics)
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-only TRUE BODY hit-flash for enemies (the focused follow-up DR-041 deferred as "its own ShaderGraph
|
||||
/// slice"). Enemies render via Rukhanka GPU deformation: the ghost ROOT holds the gameplay components
|
||||
/// (<see cref="Health"/>, <see cref="EnemyTag"/>) while the visible meshes are LinkedEntityGroup CHILD render
|
||||
/// entities (each with a <see cref="MaterialMeshInfo"/> + the AnimatedLitShader material, whose <c>_BaseColor</c>
|
||||
/// is white). <see cref="CombatFeedbackSystem"/>'s colored particle puff is the asset-free stand-in; THIS system
|
||||
/// flashes the actual body by driving the built-in Entities-Graphics per-instance override
|
||||
/// <see cref="URPMaterialPropertyBaseColor"/> (a registered <c>[MaterialProperty("_BaseColor")]</c>) on those
|
||||
/// render children: on an enemy <see cref="Health"/>-decrease edge it lerps <c>_BaseColor</c> toward
|
||||
/// <see cref="FeelConfig.BodyFlashColor"/> and decays back to white. No new component type, NO ShaderGraph edit,
|
||||
/// no server work, no <c>[GhostField]</c> — observe-only client presentation, so it is rollback-irrelevant.
|
||||
/// The render children gain <see cref="URPMaterialPropertyBaseColor"/> lazily (added once per enemy via ECB);
|
||||
/// white is the baked rest value (every enemy uses the same AnimatedLitShader/Synty-atlas convention), so
|
||||
/// <c>Flash==0</c> restores the untouched look and the override is never visible at rest.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
[UpdateInGroup(typeof(PresentationSystemGroup))]
|
||||
public partial class EnemyHitFlashSystem : SystemBase
|
||||
{
|
||||
class FlashEntry
|
||||
{
|
||||
public readonly List<Entity> RenderKids = new();
|
||||
public float LastHp;
|
||||
public float Flash; // 1 on a fresh hit, decays to 0
|
||||
public bool Settled; // wrote the final white frame after a flash ended (skips per-frame writes at rest)
|
||||
}
|
||||
|
||||
readonly Dictionary<Entity, FlashEntry> _tracked = new();
|
||||
readonly HashSet<Entity> _seen = new();
|
||||
readonly List<Entity> _stale = new();
|
||||
|
||||
static readonly float4 White = new float4(1f, 1f, 1f, 1f);
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if (!FeelConfig.BodyFlashEnabled) { RestoreAllToRest(); return; } // settle any mid-flash body back to white before bailing
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
EntityManager.CompleteDependencyBeforeRO<Health>();
|
||||
|
||||
// Pass 1: discover enemies, ensure each is tracked + its render children carry the override component.
|
||||
_seen.Clear();
|
||||
var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
|
||||
foreach (var (health, entity) in
|
||||
SystemAPI.Query<RefRO<Health>>().WithAll<EnemyTag, LinkedEntityGroup>().WithEntityAccess())
|
||||
{
|
||||
_seen.Add(entity);
|
||||
if (_tracked.ContainsKey(entity)) continue;
|
||||
|
||||
var entry = new FlashEntry { LastHp = health.ValueRO.Current, Flash = 0f, Settled = true };
|
||||
var leg = EntityManager.GetBuffer<LinkedEntityGroup>(entity);
|
||||
for (int i = 0; i < leg.Length; i++)
|
||||
{
|
||||
var c = leg[i].Value;
|
||||
if (!EntityManager.Exists(c) || !EntityManager.HasComponent<MaterialMeshInfo>(c)) continue;
|
||||
entry.RenderKids.Add(c);
|
||||
if (!EntityManager.HasComponent<URPMaterialPropertyBaseColor>(c))
|
||||
ecb.AddComponent(c, new URPMaterialPropertyBaseColor { Value = White });
|
||||
}
|
||||
// Render children can lag ghost instantiation a frame; only finalize once we actually found them (else retry next frame).
|
||||
if (entry.RenderKids.Count > 0) _tracked[entity] = entry;
|
||||
}
|
||||
ecb.Playback(EntityManager);
|
||||
ecb.Dispose();
|
||||
|
||||
// Pass 2: edge-detect Health, drive + decay the flash, write _BaseColor to the render children.
|
||||
var bc = FeelConfig.BodyFlashColor;
|
||||
float4 peak = new float4(bc.r, bc.g, bc.b, bc.a);
|
||||
float decay = dt / math.max(0.01f, FeelConfig.BodyFlashDurationSec);
|
||||
foreach (var kv in _tracked)
|
||||
{
|
||||
var entity = kv.Key;
|
||||
var entry = kv.Value;
|
||||
if (!_seen.Contains(entity)) continue; // despawned -> pruned below
|
||||
|
||||
float cur = EntityManager.GetComponentData<Health>(entity).Current;
|
||||
if (cur < entry.LastHp - 0.001f) { entry.Flash = 1f; entry.Settled = false; }
|
||||
entry.LastHp = cur;
|
||||
|
||||
if (entry.Flash <= 0f)
|
||||
{
|
||||
if (!entry.Settled) { WriteColor(entry, White); entry.Settled = true; } // settle to baked white once
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Flash = math.max(0f, entry.Flash - decay);
|
||||
WriteColor(entry, math.lerp(White, peak, entry.Flash));
|
||||
}
|
||||
|
||||
// Prune despawned enemies (their render children die with them; just drop the managed entry).
|
||||
if (_tracked.Count != _seen.Count)
|
||||
{
|
||||
_stale.Clear();
|
||||
foreach (var kv in _tracked) if (!_seen.Contains(kv.Key)) _stale.Add(kv.Key);
|
||||
for (int i = 0; i < _stale.Count; i++) _tracked.Remove(_stale[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Settle every tracked enemy's body back to its baked white rest color and stop tracking — invoked when
|
||||
// BodyFlashEnabled is toggled OFF mid-flash so no body is left frozen at an overdriven tint (re-tracked on re-enable).
|
||||
void RestoreAllToRest()
|
||||
{
|
||||
foreach (var kv in _tracked) WriteColor(kv.Value, White);
|
||||
_tracked.Clear();
|
||||
}
|
||||
|
||||
void WriteColor(FlashEntry entry, float4 col)
|
||||
{
|
||||
for (int i = 0; i < entry.RenderKids.Count; i++)
|
||||
{
|
||||
var c = entry.RenderKids[i];
|
||||
if (EntityManager.Exists(c) && EntityManager.HasComponent<URPMaterialPropertyBaseColor>(c))
|
||||
EntityManager.SetComponentData(c, new URPMaterialPropertyBaseColor { Value = col });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03cbda5f0bedbe14fbf680d92db148fe
|
||||
@@ -135,6 +135,27 @@ namespace ProjectM.Client
|
||||
/// <summary>Seconds a rumble pulse lasts before it auto-stops.</summary>
|
||||
public static float RumbleDurationSec;
|
||||
|
||||
// ---- Deferred-items pass (2026-06): true body hit-flash, remote co-op swings, near-impact strike beep ----
|
||||
/// <summary>Master gate for the enemy material BODY hit-flash (drives URPMaterialPropertyBaseColor on render children).</summary>
|
||||
public static bool BodyFlashEnabled;
|
||||
/// <summary>Peak _BaseColor the enemy body flashes to on a hit (HDR; lerps from the baked white base and decays back).</summary>
|
||||
public static Color BodyFlashColor;
|
||||
/// <summary>Seconds the body flash decays from peak back to the baked white base.</summary>
|
||||
public static float BodyFlashDurationSec;
|
||||
/// <summary>Master gate for rendering REMOTE teammates' melee cleave arcs (co-op readability).</summary>
|
||||
public static bool RemoteSwingEnabled;
|
||||
/// <summary>Tint of a remote teammate's slash arc (cooler/friendlier than the local warm arc).</summary>
|
||||
public static Color RemoteSlashColor;
|
||||
/// <summary>Master gate for the near-impact \"dodge NOW\" strike beep on a winding-up enemy.</summary>
|
||||
public static bool StrikeBeepEnabled;
|
||||
/// <summary>Volume of the near-impact strike beep.</summary>
|
||||
public static float StrikeBeepVolume;
|
||||
/// <summary>Ticks before the strike lands that the beep fires (the dodge-reaction lead).</summary>
|
||||
public static int StrikeBeepLeadTicks;
|
||||
/// <summary>Squared world-distance from the local player beyond which the strike beep is suppressed (avoids a distant cacophony).</summary>
|
||||
public static float StrikeBeepMaxDistSq;
|
||||
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
public static void ResetDefaults()
|
||||
{
|
||||
@@ -201,6 +222,18 @@ namespace ProjectM.Client
|
||||
RumbleKill = 0.45f;
|
||||
RumbleHeavy = 0.6f;
|
||||
RumbleDurationSec = 0.12f;
|
||||
|
||||
// Deferred-items pass (2026-06)
|
||||
BodyFlashEnabled = true;
|
||||
BodyFlashColor = new Color(3.2f, 2.8f, 2.2f, 1f); // hot near-white overdrive (multiplies the Synty atlas base map)
|
||||
BodyFlashDurationSec = 0.16f;
|
||||
RemoteSwingEnabled = true;
|
||||
RemoteSlashColor = new Color(1.4f, 2.2f, 2.8f, 1f); // cool teammate arc
|
||||
StrikeBeepEnabled = true;
|
||||
StrikeBeepVolume = 0.40f;
|
||||
StrikeBeepLeadTicks = 8;
|
||||
StrikeBeepMaxDistSq = 225f; // 15 m
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user