Melee feel overhaul: SwordCombat anims at heavy cadence, salvage axe, damage-at-contact + buffer + dash law (guidelines forks 1-6)
07-18/19/20 arc: LightCombo01/HeavyCombo01 clips onto Swing1-3/Slam at ~natural speed (26-tick window, 30-tick recover, DPS-held damage retune), Menacing01 combat idle (InCombat), SM_Wep_Axe_Large_01 rigid-skinned to Hand_R at x1.25, LANTERN FX retone + blade-smear ribbon. Forks 1-6 per Combat_Attack_Feel_Guidelines (design review wf_000bc247): cleave resolves at the CONTACT tick (MeleeCleavePending schedule-and-consume, knob 31, 0=legacy; connect cues moved to contact), MeleeRange 2.2 + reach-only finisher mult 1.25 (knob 30), BufferedAttackTick GhostField input buffer (knob 29, unlock-edge validity), dash refused pre-contact (lookup-based). Tests: MeleeComboTests pin legacy knobs; +3 fork tests (414 green); live server proof: HP drop exactly at swing+16 under tick batching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,9 @@ namespace ProjectM.Client
|
||||
bool _slashActive;
|
||||
float _slashRange, _slashHalf; // live cone geometry re-sampled each frame for the per-frame sweep rebuild
|
||||
int _slashSweepSign = 1; // alternate sweep direction per combo step (reads as alternating strikes)
|
||||
Mesh _smearMesh; MeshRenderer _smearMr; Material _smearMat; // 07-20 G2.3: blade-smear ribbon (leading-edge band at blade height)
|
||||
uint _pendingConnectTick; // 07-20 G2.1 (review C14): the local swing's CONTACT tick; connect cues fire THEN (0 = none)
|
||||
int _pendingConnectStep;
|
||||
double _lastHoldTime; // C4: last hit-stop hold time (throttle so a horde wipe doesn't stutter)
|
||||
|
||||
// Remote teammates' melee cleave arcs (deferred-items pass, co-op): one pooled slash renderer per remote
|
||||
@@ -150,6 +153,8 @@ namespace ProjectM.Client
|
||||
Object.Destroy(_fxRoot.gameObject);
|
||||
if (_slashMesh != null) Object.Destroy(_slashMesh);
|
||||
if (_slashMat != null) Object.Destroy(_slashMat);
|
||||
if (_smearMesh != null) Object.Destroy(_smearMesh);
|
||||
if (_smearMat != null) Object.Destroy(_smearMat);
|
||||
|
||||
foreach (var kv in _remoteSlashes)
|
||||
{
|
||||
@@ -441,45 +446,44 @@ namespace ProjectM.Client
|
||||
PrototypeCameraRig.AddShake(0.04f * step);
|
||||
int comboLen = SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg) ? (int)math.clamp((int)tcfg.MeleeComboLength, 1, 3) : 3;
|
||||
bool finisher = step >= comboLen;
|
||||
float slashRange = tcfg.MeleeRange > 0f ? tcfg.MeleeRange : 2.6f;
|
||||
float slashRange = tcfg.MeleeRange > 0f ? tcfg.MeleeRange : 2.2f;
|
||||
float slashHalf = tcfg.MeleeConeHalfAngleRad > 0f ? tcfg.MeleeConeHalfAngleRad : 0.9f;
|
||||
// Slice-2 deferred reach fix: the SERVER folds per-player StatModifiers into melee range
|
||||
// (class seed +0.8, boons, meta tiers) — the arc must sweep the REAL reach, not the base.
|
||||
// (class seed +0.8, boons, meta tiers) -- the arc must sweep the REAL reach, not the base.
|
||||
if (EntityManager.HasBuffer<StatModifier>(_localPlayer))
|
||||
slashRange = math.max(0f, StatMath.Apply(slashRange, StatTarget.MeleeRange,
|
||||
EntityManager.GetBuffer<StatModifier>(_localPlayer, true)));
|
||||
if (finisher) slashRange *= tcfg.MeleeFinisherMult > 0f ? tcfg.MeleeFinisherMult : 1.8f;
|
||||
// MC-4 connect-vs-whiff: client-side cone overlap over the cached enemy snapshot gives an IMMEDIATE
|
||||
// "you bit" read (the authoritative server damage spark/number arrives a few ticks later).
|
||||
bool connected = false; Vector3 nearestHit = (Vector3)localPos; float ndist = float.MaxValue;
|
||||
float cosHalf = Mathf.Cos(slashHalf);
|
||||
float2 fdir = new float2(face.x, face.z);
|
||||
foreach (var kv in _cache)
|
||||
if (finisher) slashRange *= tcfg.MeleeFinisherRangeMult > 0f ? tcfg.MeleeFinisherRangeMult : 1.25f; // 07-20 G2.2: REACH-only finisher mult
|
||||
// 07-20 G2.4 (review C13): the sweep-reveal (60% of life) completes AT the LIVE contact tick, so
|
||||
// the drawn edge and the damage moment stay one object under knob tuning.
|
||||
uint contactTicks = MeleeTiming.ContactTicks((byte)step, math.max(0f, tcfg.MeleeContactTicks));
|
||||
float arcLife = Mathf.Max(finisher ? 0.5f : 0.34f, (contactTicks / 60f) / 0.6f);
|
||||
TriggerSlash((Vector3)localPos, new float2(face.x, face.z), slashRange, slashHalf, step, comboLen, false, arcLife);
|
||||
// 07-20 G2.1/G5 (review C14): the CONNECT package (bite burst/thunk/kick/rumble + the finisher
|
||||
// hold) fires at the CONTACT tick -- not here at the swing edge, where damage doesn't exist yet.
|
||||
_pendingConnectStep = step;
|
||||
if (contactTicks == 0u)
|
||||
{
|
||||
if (!kv.Value.IsEnemy) continue;
|
||||
if (MeleeConeMath.InCone(localPos, fdir, slashRange, cosHalf, kv.Value.Pos))
|
||||
{
|
||||
float d2 = math.distancesq(localPos, kv.Value.Pos);
|
||||
if (d2 < ndist) { ndist = d2; nearestHit = (Vector3)kv.Value.Pos; connected = true; }
|
||||
}
|
||||
_pendingConnectTick = 0u;
|
||||
EvaluateMeleeConnect(localPos, step, comboLen); // knob 0 = legacy same-tick connect
|
||||
}
|
||||
TriggerSlash((Vector3)localPos, new float2(face.x, face.z), slashRange, slashHalf, step, comboLen, connected); // sweeps + ramps + brightens on connect
|
||||
if (connected)
|
||||
else
|
||||
{
|
||||
Burst(_hitFx, cfg != null ? cfg.Hit : null, nearestHit + Vector3.up * 0.7f, FeelConfig.HitBurstCount);
|
||||
PlayClip(_meleeConnectClip, nearestHit, FeelConfig.MeleeConnectVolume);
|
||||
PrototypeCameraRig.PunchFov(FeelConfig.MeleeConnectFovKick, FeelConfig.HitStopDurationMs);
|
||||
if (FeelConfig.RumbleEnabled && AimPresentation.Scheme == 1)
|
||||
RumbleUtil.Pulse(FeelConfig.RumbleHit * 0.6f, FeelConfig.RumbleHit, FeelConfig.RumbleDurationSec);
|
||||
_pendingConnectTick = TickUtil.NonZero(mc.SwingStartTick + contactTicks);
|
||||
}
|
||||
if (finisher)
|
||||
{
|
||||
PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick * 0.6f, FeelConfig.HitStopDurationMs);
|
||||
TryHold(); // C4: a beat of crunch on the combo finisher (the deliberate payoff hit)
|
||||
}
|
||||
}
|
||||
_lastLocalSwingTick = mc.SwingStartTick;
|
||||
_swingTickInit = true;
|
||||
|
||||
// 07-20: fire the deferred connect package when the blade lands (contact tick reached; wrap-safe).
|
||||
if (_pendingConnectTick != 0u
|
||||
&& SystemAPI.TryGetSingleton<NetworkTime>(out var meleeNt) && meleeNt.ServerTick.IsValid
|
||||
&& !new NetworkTick(_pendingConnectTick).IsNewerThan(meleeNt.ServerTick))
|
||||
{
|
||||
int cLen = SystemAPI.TryGetSingleton<TuningConfig>(out var ct2) ? (int)math.clamp((int)ct2.MeleeComboLength, 1, 3) : 3;
|
||||
EvaluateMeleeConnect(localPos, _pendingConnectStep, cLen);
|
||||
_pendingConnectTick = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -799,30 +803,51 @@ namespace ProjectM.Client
|
||||
_slashMr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
_slashMr.receiveShadows = false;
|
||||
_slashMr.enabled = false;
|
||||
|
||||
// 07-20 G2.3: the blade-smear ribbon -- a narrow band at blade height tracking the arc's leading edge
|
||||
// (child of the arc GO so the transform follows for free). Geometry via BuildSlashInto's angular-window
|
||||
// mode (review C18: one shared builder, no drift from arc retones).
|
||||
var smearGo = new GameObject("MeleeSmearRibbon");
|
||||
smearGo.transform.SetParent(go.transform, false);
|
||||
_smearMesh = new Mesh { name = "MeleeSmearRibbon" };
|
||||
smearGo.AddComponent<MeshFilter>().sharedMesh = _smearMesh;
|
||||
_smearMr = smearGo.AddComponent<MeshRenderer>();
|
||||
_smearMat = MakeParticleMaterial();
|
||||
_smearMat.name = "MeleeSmearRibbon";
|
||||
_smearMr.sharedMaterial = _smearMat;
|
||||
_smearMr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
_smearMr.receiveShadows = false;
|
||||
_smearMr.enabled = false;
|
||||
}
|
||||
|
||||
// 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 BuildSlashInto(Mesh mesh, float range, float halfAngle, float reveal, int sweepSign)
|
||||
// 07-20 G2.3 extension: innerFrac/y/angularWindowRad let the SAME builder cut the blade-smear ribbon
|
||||
// (a narrow band trailing the leading edge at blade height) -- defaults reproduce the classic ground arc.
|
||||
void BuildSlashInto(Mesh mesh, float range, float halfAngle, float reveal, int sweepSign,
|
||||
float innerFrac = 0.45f, float y = 0f, float angularWindowRad = 0f)
|
||||
{
|
||||
const int seg = 16;
|
||||
float r1 = Mathf.Max(0.4f, range);
|
||||
float r0 = r1 * 0.45f;
|
||||
float r0 = r1 * innerFrac;
|
||||
float aStart = sweepSign >= 0 ? -halfAngle : halfAngle; // trailing edge
|
||||
float aFull = sweepSign >= 0 ? halfAngle : -halfAngle; // far edge
|
||||
float aEnd = Mathf.Lerp(aStart, aFull, Mathf.Clamp01(reveal)); // current leading edge of the sweep
|
||||
float aBase = aStart;
|
||||
if (angularWindowRad > 0f) // smear mode: only the trailing band behind the leading edge
|
||||
aBase = sweepSign >= 0 ? Mathf.Max(aStart, aEnd - angularWindowRad) : Mathf.Min(aStart, aEnd + angularWindowRad);
|
||||
var verts = new Vector3[(seg + 1) * 2];
|
||||
var cols = new Color[(seg + 1) * 2];
|
||||
var uvs = new Vector2[(seg + 1) * 2];
|
||||
var tris = new int[seg * 6];
|
||||
for (int i = 0; i <= seg; i++)
|
||||
{
|
||||
float a = Mathf.Lerp(aStart, aEnd, i / (float)seg);
|
||||
float a = Mathf.Lerp(aBase, aEnd, i / (float)seg);
|
||||
float sx = Mathf.Sin(a), cz = Mathf.Cos(a);
|
||||
verts[i * 2] = new Vector3(sx * r0, 0f, cz * r0);
|
||||
verts[i * 2 + 1] = new Vector3(sx * r1, 0f, cz * r1);
|
||||
verts[i * 2] = new Vector3(sx * r0, y, cz * r0);
|
||||
verts[i * 2 + 1] = new Vector3(sx * r1, y, cz * r1);
|
||||
float lead = i / (float)seg; // 0 trailing -> 1 leading edge (brightest at the travelling blade)
|
||||
cols[i * 2] = new Color(1f, 1f, 1f, 0.55f * (0.2f + 0.8f * lead)); // inner, brightest at the leading edge
|
||||
cols[i * 2] = new Color(1f, 1f, 1f, 0.35f * (0.2f + 0.8f * lead)); // 07-19 retone: a wake, not a laser // inner, brightest at the leading edge
|
||||
cols[i * 2 + 1] = new Color(1f, 1f, 1f, 0f); // outer rim fades out
|
||||
uvs[i * 2] = new Vector2(0.5f, 0.5f);
|
||||
uvs[i * 2 + 1] = new Vector2(0.5f, 0.5f);
|
||||
@@ -845,17 +870,68 @@ namespace ProjectM.Client
|
||||
// the range telegraph (MC-4 clarity) AND now SWEEPS across + ramps per combo step so the swing reads as a
|
||||
// directional, escalating cleave rather than a static flash.
|
||||
// C4: fire a brief presentation-only hit-stop hold, throttled (never Time.timeScale; the sim keeps ticking).
|
||||
void TryHold()
|
||||
void TryHold(int frames = 0) // 07-20 G5 (review C17): ONE hold path, per-verb frames (0 = the light default)
|
||||
{
|
||||
if (!FeelConfig.HitStopFreezeEnabled) return;
|
||||
double now = SystemAPI.Time.ElapsedTime;
|
||||
if (now - _lastHoldTime < 0.22) return;
|
||||
_lastHoldTime = now;
|
||||
PrototypeCameraRig.Hold(FeelConfig.HitStopMaxFrames);
|
||||
PrototypeCameraRig.Hold(frames > 0 ? frames : FeelConfig.HitStopMaxFrames);
|
||||
}
|
||||
|
||||
// 07-20 G2.1/G5 (review C14): the melee CONNECT package, fired when the blade actually LANDS. Recomputes
|
||||
// aim + reach LIVE at the contact tick (the cast-turn kept steering since the swing edge) over the cached
|
||||
// enemy snapshot; brightens the still-sweeping arc on a bite; the finisher hold is the G5 heavy beat.
|
||||
void EvaluateMeleeConnect(float3 localPos, int step, int comboLen)
|
||||
{
|
||||
if (_localPlayer == Entity.Null || !EntityManager.Exists(_localPlayer)) return;
|
||||
var cfg = VFXConfig.Instance;
|
||||
bool finisher = step >= comboLen;
|
||||
float range = 2.2f, half = 0.9f, finRange = 1.25f;
|
||||
if (SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg))
|
||||
{
|
||||
if (tcfg.MeleeRange > 0f) range = tcfg.MeleeRange;
|
||||
if (tcfg.MeleeConeHalfAngleRad > 0f) half = tcfg.MeleeConeHalfAngleRad;
|
||||
if (tcfg.MeleeFinisherRangeMult > 0f) finRange = tcfg.MeleeFinisherRangeMult;
|
||||
}
|
||||
if (EntityManager.HasBuffer<StatModifier>(_localPlayer))
|
||||
range = math.max(0f, StatMath.Apply(range, StatTarget.MeleeRange,
|
||||
EntityManager.GetBuffer<StatModifier>(_localPlayer, true)));
|
||||
if (finisher) range *= finRange;
|
||||
float2 fdir = new float2(0f, 1f);
|
||||
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer) && EntityManager.HasComponent<PlayerInput>(_localPlayer))
|
||||
fdir = FacingMath.ResolveAim(
|
||||
EntityManager.GetComponentData<PlayerInput>(_localPlayer).Aim,
|
||||
EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction);
|
||||
bool connected = false; Vector3 nearestHit = (Vector3)localPos; float ndist = float.MaxValue;
|
||||
float cosHalf = Mathf.Cos(half);
|
||||
foreach (var kv in _cache)
|
||||
{
|
||||
if (!kv.Value.IsEnemy) continue;
|
||||
if (MeleeConeMath.InCone(localPos, fdir, range, cosHalf, kv.Value.Pos))
|
||||
{
|
||||
float d2 = math.distancesq(localPos, kv.Value.Pos);
|
||||
if (d2 < ndist) { ndist = d2; nearestHit = (Vector3)kv.Value.Pos; connected = true; }
|
||||
}
|
||||
}
|
||||
if (connected)
|
||||
{
|
||||
Burst(_hitFx, cfg != null ? cfg.Hit : null, nearestHit + Vector3.up * 0.7f, FeelConfig.HitBurstCount);
|
||||
PlayClip(_meleeConnectClip, nearestHit, FeelConfig.MeleeConnectVolume);
|
||||
PrototypeCameraRig.PunchFov(FeelConfig.MeleeConnectFovKick, FeelConfig.HitStopDurationMs);
|
||||
if (FeelConfig.RumbleEnabled && AimPresentation.Scheme == 1)
|
||||
RumbleUtil.Pulse(FeelConfig.RumbleHit * 0.6f, FeelConfig.RumbleHit, FeelConfig.RumbleDurationSec);
|
||||
if (_slashActive) _slashTint *= 1.4f; // the bite brighten, now AT the bite (UpdateSlash re-reads the tint)
|
||||
}
|
||||
if (finisher)
|
||||
{
|
||||
PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick * 0.6f, FeelConfig.HitStopDurationMs);
|
||||
TryHold(FeelConfig.FinisherHoldFrames); // G5: the heavy payoff beat, at contact
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int step, int comboLen, bool connected)
|
||||
void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int step, int comboLen, bool connected, float lifeOverride = 0f)
|
||||
{
|
||||
if (_slashMr == null || _slashMat == null) return;
|
||||
bool finisher = step >= comboLen;
|
||||
@@ -870,14 +946,18 @@ void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int
|
||||
// Per-step ramp so the chain visibly builds to the finisher (the steps were byte-identical before).
|
||||
float t = comboLen > 1 ? math.saturate((step - 1) / (float)(comboLen - 1)) : 1f;
|
||||
_slashTint = finisher
|
||||
? new Color(3.4f, 2.4f, 0.7f) // finisher: warm HDR flash
|
||||
: Color.Lerp(new Color(1.2f, 1.9f, 2.6f), new Color(1.9f, 2.8f, 3.4f), t); // cool, brighter per step
|
||||
if (connected) _slashTint *= 1.6f; // brighter arc on a confirmed bite (the immediate "you hit" read)
|
||||
_slashLife = finisher ? 0.28f : Mathf.Lerp(0.15f, 0.20f, t);
|
||||
? new Color(1.7f, 1.25f, 0.5f) // finisher: lamp-warm accent (07-19 retone)
|
||||
: Color.Lerp(new Color(0.5f, 0.9f, 1.05f), new Color(0.8f, 1.3f, 1.4f), t); // dim bioluminescent teal, brighter per step
|
||||
if (connected) _slashTint *= 1.4f; // brighter arc on a confirmed bite (the immediate "you hit" read)
|
||||
_slashTint *= Mathf.Max(0f, FeelConfig.MeleeArcIntensity); // 07-19 LANTERN retone: live dimmer knob
|
||||
_slashLife = lifeOverride > 0f ? lifeOverride : (finisher ? 0.50f : Mathf.Lerp(0.30f, 0.38f, t)); // 07-20: melee passes a knob-derived life (reveal ends AT contact)
|
||||
_slashAge = 0f;
|
||||
_slashActive = true;
|
||||
_slashMat.color = _slashTint;
|
||||
_slashMr.enabled = true;
|
||||
// 07-19 vibe: a few slow bubbles shed along the cleave path (the murk answers the swing).
|
||||
if (_bubbleFx != null && FeelConfig.MeleeArcBubbles > 0)
|
||||
EmitColored(_bubbleFx, pos + f * (range * 0.55f) + Vector3.up * 0.35f, FeelConfig.MeleeArcBubbles, new Color(0.75f, 0.95f, 1f, 0.8f));
|
||||
}
|
||||
|
||||
void UpdateSlash(float dt)
|
||||
@@ -885,13 +965,26 @@ void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int
|
||||
if (!_slashActive || _slashMr == null) return;
|
||||
_slashAge += dt;
|
||||
float u = _slashAge / Mathf.Max(1e-4f, _slashLife);
|
||||
if (u >= 1f) { _slashActive = false; _slashMr.enabled = false; return; }
|
||||
if (u >= 1f) { _slashActive = false; _slashMr.enabled = false; if (_smearMr != null) _smearMr.enabled = false; return; }
|
||||
// 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);
|
||||
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);
|
||||
_slashMr.transform.localScale = Vector3.one * (1f + u * 0.06f); // 07-19: subtler drift
|
||||
|
||||
// 07-20 G2.3: the blade-smear ribbon rides the arc's leading edge at blade height while the sweep runs
|
||||
// (the reach trick: the swept edge reads FROM THE WEAPON, not as a floor decal alone).
|
||||
bool smearShow = _smearMr != null && u < 0.75f;
|
||||
if (_smearMr != null) _smearMr.enabled = smearShow;
|
||||
if (smearShow)
|
||||
{
|
||||
BuildSlashInto(_smearMesh, _slashRange, _slashHalf, reveal, _slashSweepSign,
|
||||
innerFrac: 0.7f, y: 0.95f, angularWindowRad: 0.25f); // thin band = streak, not a pane
|
||||
var sc = _slashTint * 0.8f;
|
||||
sc.a = (u < 0.6f ? 0.7f : 0.7f * (1f - (u - 0.6f) / 0.15f));
|
||||
_smearMat.color = sc;
|
||||
}
|
||||
}
|
||||
|
||||
// Remote teammates' melee cleave arcs (deferred-items pass, co-op readability): the local player's swing
|
||||
@@ -907,7 +1000,8 @@ void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int
|
||||
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;
|
||||
float finisherMult = tcfg.MeleeFinisherRangeMult > 0f ? tcfg.MeleeFinisherRangeMult : 1.25f; // 07-20 G2.2: REACH-only finisher mult
|
||||
float remoteContactKnob = math.max(0f, tcfg.MeleeContactTicks); // 07-20 G2.4: remote sweeps track contact too
|
||||
|
||||
_remoteSeen.Clear();
|
||||
foreach (var (xf, facing, mc, entity) in
|
||||
@@ -930,7 +1024,8 @@ void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int
|
||||
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.Life = Mathf.Max(finisher ? 0.50f : 0.34f,
|
||||
(MeleeTiming.ContactTicks((byte)step, remoteContactKnob) / 60f) / 0.6f); // 07-20: reveal ends AT contact (knob-aware, review C13)
|
||||
rs.Age = 0f;
|
||||
rs.Active = true;
|
||||
BuildSlashInto(rs.Mesh, rs.Range, rs.Half, 0f, rs.SweepSign);
|
||||
@@ -955,7 +1050,7 @@ void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int
|
||||
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);
|
||||
tr.localScale = Vector3.one * (1f + u * 0.06f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user