Further Tests & Progress
This commit is contained in:
@@ -52,6 +52,12 @@ Operator feedback after the first pass: make KBM aiming feel more natural / worl
|
||||
- **Skipped** (with reasons): constant-screen-size scaling (fixed-distance cam), aim line (redundant with the ring), KBM enemy-magnetism (would undermine the intentional gamepad-only assist).
|
||||
- Validated: EditMode **86/86**, console clean; runtime (focused) KBM ring active at the cursor ground point, OS cursor hidden, `TargetFacing` published, no exceptions.
|
||||
|
||||
## Refinement 2 — movement-based camera look-ahead (aim-drift fix, 2026-06-04)
|
||||
|
||||
Operator feedback: holding the mouse cursor NEAR the player, the aim/reticle "swims" without moving the mouse when the character turns or the camera pans — feels inaccurate. **Root cause:** the look-ahead in Refinement 1 led the framed point toward `PlayerFacing` (the aim). The KBM reticle/aim is the LIVE cursor screen-ray re-projected onto the ground each frame, so turning to face a near-cursor moved the camera (aim → facing → look-ahead → camera pan), and a stationary mouse then re-projected to a DIFFERENT ground point → the aim direction drifted. Worst near the player (short lever arm → high angular sensitivity). Research (gamedeveloper.com dual-stick controls; Relic Hunters Zero) confirmed that coupling the camera to the crosshair/aim causes exactly this and is conditioned-out in shipped games.
|
||||
|
||||
**Fix (supersedes the facing-based look-ahead in Refinement 1):** the camera look-ahead now leads toward **MOVEMENT** (`PlayerInput.Move`), not aim/facing. `PrototypeCameraTargetSystem` publishes `PrototypeCameraRig.TargetMoveDir`; the rig leads toward it. A stationary aim no longer pans the camera → the reticle is rock-stable while turning/aiming; the camera still anticipates where the player is MOVING. `AimLeadDistance` (default 2.5, tunable, 0 = off) is unchanged in magnitude — only its direction source changed. EditMode **127/127**, console clean. Live "feels accurate now" = operator (focused Game view).
|
||||
|
||||
## Open / deferred
|
||||
|
||||
- The **real mouse-cursor path + live device auto-switch** need a **focused** Game view — the unfocused editor can't inject mouse position / device actuation (validated the replication, math, gate, and reticle headlessly via `DebugInputInjectionSystem` + forced scheme). Operator focused click-test pending.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
id: DR-016
|
||||
title: Stage G combat gameplay slice — timed modifiers, knockback, attack telegraph (+ deferred Spitter/multi-prefab)
|
||||
status: accepted
|
||||
date: 2026-06-04
|
||||
tags:
|
||||
- decision
|
||||
- gameplay
|
||||
- combat
|
||||
- netcode
|
||||
- enemies
|
||||
- modifiers
|
||||
permalink: gamevault/07-sessions/decisions/dr-016-stage-g-combat-gameplay
|
||||
---
|
||||
|
||||
# DR-016 — Stage G combat gameplay slice
|
||||
|
||||
## Context
|
||||
|
||||
The 2026-06-04 polish pass ([[2026-06-04_Polish_Backlog_Pass]]) reached a "new gameplay" stage after polish stages A–F. The operator selected a slice: ranged **Spitter** enemy, **knockback + attack telegraph**, and **timed/removable modifiers + multi-prefab abilities** (small fold-ins — storage proximity-gate, pickup auto-grant, standalone-debug RPC — riding along). Each is netcode-touching, so a **4-agent adversarial design-review Workflow** (netcode/determinism · reuse · test-plan → synthesis) ran first and produced per-feature specs (re-bake?, determinism, files, EditMode tests). This DR records the architecture the review locked and what shipped.
|
||||
|
||||
## Decision
|
||||
|
||||
- **Timed/removable modifiers — a SEPARATE server-only `TimedModifier{SourceId,UntilTick}` buffer, NOT a field on the replicated `StatModifier`.** Adding ANY member (even non-`[GhostField]`) to a `[GhostField]` buffer element regenerates its serializer/stride/hash = an effective re-bake; the separate buffer keeps `StatModifier` byte-identical. `TimedModifierExpirySystem` (server, plain `SimulationSystemGroup`) removes the matching `StatModifier` by `SourceId` when due (wrap-safe `NetworkTick`); the shortened `[GhostField]` buffer auto-replicates and `StatRecomputeSystem` reverts the effective stat unchanged. `TimedModifierUtil.RemoveBySourceId` is the clear-by-type helper. **No re-bake.**
|
||||
- **Enemy knockback — server-only `KnockbackState{Dir,Speed,UntilTick}`, applied INSIDE `EnemyAISystem`.** Husk position already replicates (stock `LocalTransform` variant), so knockback needs NO new `[GhostField]` (no re-bake). The one desync trap is two writers of the Husk's `Position`; `EnemyAISystem` is the sole writer, so knockback is blended there (recoil REPLACES seek + suppresses the strike for the window). `ProjectileDamageSystem` stamps `KnockbackState` on hit (it has the projectile heading; backward-compatible via `TryGetSingleton<NetworkTime>` so existing tests pass). Tuned by `Tuning.KnockbackSpeed`(8, 0=off) / `KnockbackDurationTicks`(8). **No re-bake.**
|
||||
- **Husk attack telegraph — replicated `[GhostField] AttackWindup.WindUpUntilTick` on the Husk (a re-bake) + a 2-phase strike.** The client has NONE of the timing inputs (`EnemyStats` / `EnemyAttackCooldown` are server-only), so the wind-up signal MUST be replicated. A `uint` tick (not a `[GhostEnabledBit]`) so the client cue can ramp/countdown and survive a missed snapshot. `EnemyAISystem` restructured: when first in-range + cooldown-ready it commits `WindUpUntilTick = now + Tuning.AttackWindupTicks` and damages nothing; strikes when the tick elapses; cancels on leave-range; a knocked Husk doesn't wind up. Client cue = observer in `CombatFeedbackSystem` (warns on the wind-up-start edge). Tuned by `Tuning.AttackWindupTicks`(18 ≈ 0.3s, 0/1 = instant). **Re-bake** (done on a focused editor).
|
||||
- **Deferred (still selected, not yet built):** ranged **Spitter** (two NEW ghost prefabs — `EnemySpitter` + an interpolated `EnemyProjectile` — + spit-fire/move/damage-vs-players systems; the enemy projectile must NOT reuse the player's predicted projectile or `ProjectileDamageSystem`'s faction-blind target query; the plain-group sweep must use a stored `LastStep`, not `DeltaTime`, per the DR-013 dt-trap) and **multi-prefab abilities** (generalize the non-Burst `ProjectileClassificationSystem` to a ghost-type SET; core correctness — no owner-client double-spawn — is Play-only, `NetCodeTestWorld` being internal in 1.13.2). Small fold-ins (storage proximity-gate, pickup auto-grant, standalone-debug RPC) also pending.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Validated (6.4.7):** EditMode **86 → 127** (timed-mods +4, knockback +3, telegraph +2; the M6 system tests from Stage B are the rest). Console clean throughout. The telegraph re-bake was **Play-validated server==client** (4 Husks both worlds, 2 winding up, identical `maxWindTick`; no "not a known Burst entry point" spam; no errors). Knockback's existing-test backward-compat confirmed.
|
||||
- **Reusable rulings:** never mutate a replicated buffer-element layout (use a separate server-only tracking buffer); server-only state that drives an already-replicated field needs no re-bake; a replicated signal is mandatory when the client lacks the timing inputs; keep ONE writer of an interpolated ghost's transform; `replace_method` (MCP `script_apply_edits`) does NOT handle `struct` ISystems — use line/anchor edits.
|
||||
- **Tunables** let the operator dial or disable each gameplay change (knockback speed 0 = off; windup ticks 0/1 = instant/legacy).
|
||||
|
||||
See [[2026-06-04_Polish_Backlog_Pass]]. Builds on [[DR-009_GameFeel_Identity_FirstBlood]] (Husk/feel), [[DR-013_M6_Aether_Cycle_Region_Split]] (dt-trap, region gating), [[DR-004_M3_DataDriven_Abilities_Modifiers]] (StatModifier).
|
||||
Reference in New Issue
Block a user