Initial Combat Implementation
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
---
|
||||
date: 2026-05-31
|
||||
type: session
|
||||
tags: [session, dots, netcode, m2, combat]
|
||||
permalink: gamevault/07-sessions/2026/2026-05-31-m2-combat
|
||||
---
|
||||
|
||||
# Session 2026-05-31 — M2 Combat Foundation
|
||||
|
||||
## Goal
|
||||
|
||||
Build **M2 — Combat**: directional ability fire (predicted projectile), deterministic soft auto-target, server-authoritative health/damage, and a training dummy to shoot — plus migrate input to the new Unity Input System action map. Strict, foundation-grade pass.
|
||||
|
||||
## Done
|
||||
|
||||
### Architecture locked — see [[DR-003_M2_Combat_Netcode_Architecture]]
|
||||
Predicted projectile ghosts (client predict-spawn + classification by `SpawnId`); server-authoritative auto-target at the fire tick (reconciled via `[GhostField] Projectile.Direction`); server-only damage (`Health.Current` `[GhostField]`); swept hit detection. Fire is a netcode `InputEvent`.
|
||||
|
||||
### Built — 26 files, compiles clean, runtime-validated on 6.4.7
|
||||
- **Input migration:** added `Aim` (Vector2) + `Fire` (Button) to `Project M Input.inputactions`; retargeted the generated wrapper into the `ProjectM.Client` asmdef (`wrapperCodePath` → `Scripts/Client/Input/ProjectMInput.cs`) so client systems can reference it; rewrote `PlayerInputGatherSystem` as a managed `SystemBase` reading the wrapper (`Fire.Set()` on press edge).
|
||||
- **Simulation:** `Health`, `HitRadius`, `DamageEvent` (buffer), `AbilityStats`, `AbilityCooldown` (`[GhostField]`), `Projectile` (`[GhostField]` Direction+SpawnId), `ProjectileSpawner`, `TrainingDummyTag`, `TrainingDummySpawner`; systems `AutoTarget` (static), `AbilityFireSystem` (predicted, `IsFirstTimeFullyPredictingTick`-gated, server-branch auto-target), `ProjectileMoveSystem` (predicted).
|
||||
- **Client:** `ProjectileClassificationSystem` (predicted-spawn classifier; **non-Burst** — see DR-003).
|
||||
- **Server:** `ProjectileDamageSystem` (swept hit), `HealthApplyDamageSystem`, `TrainingDummySpawnSystem` (spawns 3 dummies, self-disables).
|
||||
- **Authoring/assets:** `Projectile.prefab` (OwnerPredicted ghost), `TrainingDummy.prefab` (Interpolated ghost), `PlayerAuthoring` bakes Health/AbilityStats/AbilityCooldown/DamageEvent; both spawners wired into `Gameplay.unity`; `Application.runInBackground` enabled (M1 follow-up).
|
||||
- **Tests:** `AutoTargetTests`, `ProjectileMoveSystemTests`, `HealthApplyDamageSystemTests`, `ProjectileDamageSystemTests` (incl. tunnelling regression) — **EditMode 22/22 green**.
|
||||
|
||||
### Runtime validation (Play Mode, in-editor, `execute_code` inspection)
|
||||
- connect → server spawns 3 dummies (HP 60) → **replicated to client** (both worlds show 3 dummies); player ghost spawns with Health 100 / AbilityStats / AbilityCooldown.
|
||||
- Server-injected projectile → `ProjectileMoveSystem` → `ProjectileDamageSystem` (swept hit) → `DamageEvent` → `HealthApplyDamageSystem` → **dummy HP 60→40**, and the drop **replicated server→client** via the `Health` `[GhostField]`.
|
||||
- Bug caught + fixed at runtime: a fast (speed-25) projectile tunnelled the point hit-check; the swept fix made it hit. Slow (speed-3) hit before the fix. Regression test added.
|
||||
|
||||
### Method
|
||||
Orchestrated authoring + 3-lens adversarial review of all 25 logic files via a background workflow against a frozen build contract; applied in compile-gated clusters; every volatile Netcode 1.13.2 API verified via `unity_reflect`/context7 + the official ECS samples before coding.
|
||||
|
||||
## Decisions
|
||||
- [[DR-003_M2_Combat_Netcode_Architecture]] — predicted projectiles, server auto-target/damage, swept hits, non-Burst classifier.
|
||||
|
||||
## Open / deferred
|
||||
- **ENVIRONMENT (action needed): restart the Unity editor.** A Burst *internal compiler error* (from the first compile, since fixed by de-Bursting the classifier) corrupted the Burst incremental cache → every newly-added `[BurstCompile]` entry point (the 5 combat systems + the generated Health/AbilityCooldown/Projectile ghost serializers) logs "not a known Burst entry point" and runs managed-fallback (slow → server tick-batching + ~30–40s play-enter). Code is correct (clean compile + 22/22 tests + runtime damage/replication all work). A **fresh editor launch** (or `Library/BurstCache` wipe while closed) should clear it; re-confirm the console is clean on the next warm play.
|
||||
- **Live interactive fire not yet validated:** the Input System ignores injected device input while the Game view is unfocused, so the input→`AbilityFireSystem`→predicted-spawn→classification path was validated *structurally* (compiles, instantiates, mirrors the verified sample) but not by a real keypress. **Operator test:** focus the editor in Play Mode, press Space / left-click / RT → expect your predicted projectile + dummy HP drop.
|
||||
- Deferred (revisit at trigger): mouse-cursor aim for KBM (needs camera ground-ray rig); player death/respawn (currently HP clamps ≥0, dummies despawn); predicted auto-target if the soft server reconcile ever feels off; projectile/dummy visual polish (currently primitive meshes).
|
||||
|
||||
## Addendum (same session) — input-validation tooling + prototype visuals
|
||||
|
||||
**Headless input interaction (for validation).** The Unity Input System ignores device input while the Game view is unfocused, which blocked headless fire/move validation. Three options were weighed; focus-switching CLI was rejected (fragile, intrusive). Enabled/built:
|
||||
- `InputSettings.editorInputBehaviorInPlayMode = AllDeviceInputAlwaysGoesToGameView` (+ `runInBackground`) so injected/real input reaches the unfocused game.
|
||||
- `DebugInputInjectionSystem` (`#if UNITY_EDITOR`, `ProjectM.Client`): runs after the real gather; static pokes (`Fire()`, `SetMove`, `SetAim`, `Stop()`) drive the local player's `PlayerInput`, exercising the authentic command→prediction pipeline (not a shortcut). Cross-platform (pure C#). **Validated: `SetMove(2,0)` drove the player to x≈101 on server, replicated to client** — proving the full input→command→server→sim→replication path (and the tool) works for continuous input.
|
||||
|
||||
**Finding — one-shot fire vs tick-batching.** Driving the `Fire` `InputEvent` via the hook did **not** advance `AbilityCooldown.NextFireTick` (AbilityFireSystem never fired), while the server was **tick-batching** (the Burst-cache degradation). Continuous values (Move) survive batching; one-shot events are exactly what Unity's tick-batch warning says gets lost. **Action:** validate fire on a *healthy* (post-editor-restart) editor — re-run the hook (now upgraded to hold Fire across ~10 frames for reliable propagation) or press the key in a focused Game view. If it still fails when healthy, switch AbilityFireSystem's `input.Fire.IsSet` gate to the buffered `.Count` read the HelloNetcode sample uses.
|
||||
|
||||
**Prototype visuals.** `PrototypeCameraRig` (MonoBehaviour on Main Camera, `ProjectM.Client`): player-following, fully tunable (pitch/yaw/distance/FOV/ortho), default **mid 3/4 ~45° perspective** (V Rising / D4 feel) — operator-chosen. Bright URP-Lit materials: player = cyan, dummies = red, projectiles = yellow, ground = dark grey; added a ground plane to `SampleScene`. Screenshot confirms framing + colors: `Assets/Screenshots/M2_prototype_view.png`.
|
||||
|
||||
**Post-restart verification (2026-05-31).** Operator restarted Unity (Burst cache cleared — the "not a known Burst entry point" flood is gone) and relaunched the Local Reference editor. **Fire now validated end-to-end**: via `DebugInputInjectionSystem.Fire()`, `AbilityFireSystem` fired (`NextFireTick` advanced) and the dummy went **60→20 HP on both server and client** — input→fire→predicted-projectile→swept-hit→damage→replication all confirmed live. The earlier fire failure **was** the Burst-degraded tick-batching dropping the one-shot `InputEvent`. **Projectile ghost-map errors** the operator saw are **not a code bug** — they appear only under **server tick-batching** (predicted-spawn reconciliation races); a clean single fire produces zero ghost errors. Root cause of the persistent batching: **two Unity editors running at once** (Project M + Local Reference) starving Project M's server — close the reference editor (or keep Project M focused) for clean netcode. **Fixed:** `PrototypeCameraRig` startup job-safety bug (it queried `EntityManager` from LateUpdate during subscene load) — now an ECS `PrototypeCameraTargetSystem` publishes the player position to the camera. **M2 marked ✅ Done.**
|
||||
|
||||
## Next
|
||||
**M3 — Data-driven abilities & modifiers** (new, slotted before co-op): ability + character stats in ScriptableObjects → baked blob assets, runtime flat/% modifier stacks → effective stats — design in [[Data_Driven_Abilities]]. Then **M4 — Co-op** over Unity Relay per [[Milestones]].
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
date: 2026-05-31
|
||||
type: session
|
||||
tags: [session, dots, netcode, m3, abilities, data-driven, modifiers]
|
||||
permalink: gamevault/07-sessions/2026/2026-05-31-m3-data-driven-abilities
|
||||
---
|
||||
|
||||
# Session 2026-05-31 — M3 Data-Driven Abilities & Modifiers
|
||||
|
||||
## Goal
|
||||
|
||||
Build **M3 — Data-driven abilities & modifiers**: move M2's hard-baked combat/character values into authored ScriptableObjects baked to **blob assets**, add a runtime **flat + % `StatModifier`** stack producing effective stats (server-authoritative + prediction-correct), and prove it by refactoring the projectile ability + 2 sample abilities, exercised by a debug hook **and** a real pickup. Design: [[Data_Driven_Abilities]]; architecture locked in [[DR-004_M3_DataDriven_Abilities_Modifiers]].
|
||||
|
||||
## Intake decisions (operator)
|
||||
|
||||
Modifiers replicate as a **ghost buffer of `StatModifier`** (both worlds recompute identically); upgrade source = **both** (debug hook + real pickup); **2 extra** sample abilities (fast-light + slow-heavy). Smaller technical defaults decided in-plan (every-tick recompute, raw-byte enum replication, single projectile prefab, `PlayerAimSystem` unchanged, `MaxHealth` single-source via SO, permanent modifiers).
|
||||
|
||||
## Done — 22 files, compiles clean, EditMode 38/38, runtime-validated on 6.4.7
|
||||
|
||||
### Built (see [[DR-004_M3_DataDriven_Abilities_Modifiers]])
|
||||
- **Simulation:** `StatIds` (`AbilityId`/`CharacterId`/`StatTarget`/`ModOp` enums); `StatModifier` (replicated `[GhostField]` buffer, `OwnerSendType.All`, raw-byte Target/Op); `EffectiveAbilityStats` / `EffectiveCharacterStats`; `AbilityRef` (`[GhostField]` id) / `CharacterStatsRef`; `AbilityDatabaseBlob` (+ `AbilityDefBlob`/`CharacterStatsBlob`, `TryGet*`); `AbilityDatabase` singleton + `AbilityPrefabElement` companion buffer; `StatMath` (pure fold); `StatRecomputeSystem` (predicted, every-tick); `UpgradePickup` + `UpgradePickupSpawner`. Rerouted `AbilityFireSystem` (effective stats + prefab-by-id + snapshot-at-fire), `PlayerMoveSystem` (effective move). **Deleted** `AbilityStats`, `PlayerMoveStats`.
|
||||
- **Authoring:** `AbilityDefinition` / `CharacterStatsDefinition` ScriptableObjects; `AbilityDatabaseAuthoring` (blob baker, `DependsOn`); `UpgradePickupAuthoring` / `UpgradePickupSpawnerAuthoring`; `PlayerAuthoring` now references the SOs and bakes refs/effective/modifier-buffer/Health-from-SO.
|
||||
- **Server:** `UpgradePickupSpawnSystem` (one-shot), `UpgradePickupSystem` (overlap → `AppendToBuffer` + despawn), `DebugModifierInjectionSystem` (`#if UNITY_EDITOR`, server world); `HealthApplyDamageSystem` clamps to effective MaxHealth.
|
||||
- **Assets:** 4 SO definitions (Primary/FastLight/SlowHeavy + Default character); `UpgradePickup.prefab` (interpolated ghost); `Player.prefab` re-wired; `AbilityDatabase` + `UpgradePickupSpawner` GameObjects added to `Gameplay.unity`.
|
||||
- **Tests:** `StatMathTests`, `AbilityDatabaseBlobTests`, `StatRecomputeSystemTests`, `UpgradePickupSystemTests` (+ migrated `PlayerMoveSystemTests`) — **EditMode 38/38 green** (16 new + 22 existing).
|
||||
|
||||
### Runtime validation (Play Mode, in-editor, `execute_code` inspection)
|
||||
- **Blob baked into both worlds** (`db=1` each); player spawns with data-driven base effective stats on server **and** client: `move=6, maxHp=100, dmg=20, spd=25, cd=12`; 2 pickups spawned + replicated.
|
||||
- **Modifier replication + prediction-correct recompute (the key claim):** server-granted `+50 Damage(Flat)` and `+50% MoveSpeed(PercentAdd)` → **identical** `effDmg=70`, `effMove=9` and matching modifier buffers on **server and owner-predicted client**. Held **even under tick-batching**.
|
||||
- **Data-only ability swap:** `CycleAbility` Primary→FastLight → `dmg 20→8, spd 25→40, cd 12→5` on both worlds, `AbilityRef.Id` GhostField replicated — zero code per ability. `ClearModifiers` reverted to base.
|
||||
- **Real pickup grant:** drove the player over a pickup → modifier granted server-side + pickup despawned (2→1) + replicated; `+10 Damage` folded to `effDmg=18` on both worlds.
|
||||
- Console: only the expected server-tick-batching warning (in-editor, unfocused, heavy first bake); **no Burst ICE, no ghost-serialization or prediction-divergence errors**.
|
||||
|
||||
### Notable fix caught by tests
|
||||
- **`readonly` blob lookup methods read the array as empty** — a `readonly` struct method calling `BlobArray`'s non-readonly indexer forces a defensive copy that breaks the relative-offset pointer. Dropped `readonly`; reach the blob via `ref blob.Value`. (Caught by `AbilityDatabaseBlobTests` before any runtime use.)
|
||||
|
||||
### Method
|
||||
context7-led research (blob baking, `[GhostField]` buffers, `OwnerSendType`) → Plan-agent design → plan-gated → compile-checkpointed clusters (A data types → B pure tests → C recompute+reroute+deletes atomic → D authoring → E server → F assets/scene → G runtime). `read_console` after every write; `Write` (not delete+recreate) for the GUID-referenced `PlayerAuthoring`; `execute_code` for `List<SO>` wiring (component_properties can't set list/ref fields).
|
||||
|
||||
## Decisions
|
||||
- [[DR-004_M3_DataDriven_Abilities_Modifiers]] — blob definition DB + companion prefab buffer, replicated `StatModifier` buffer, every-tick effective recompute, snapshot-at-fire, server-world debug hook.
|
||||
|
||||
## Open / deferred
|
||||
- **UI/icon/description pipeline** — managed lookup keyed by id, not built (deferred).
|
||||
- **Multi-prefab abilities** — M3 reuses one projectile ghost prefab (different stats snapshotted at fire); a per-ability *different* projectile ghost would need `ProjectileClassificationSystem` generalized.
|
||||
- **Timed/removable modifiers** — M3 modifiers are permanent-once-granted; `StatModifier.SourceId` reserved for future `ClearByType`/expiry-on-`NetworkTick`.
|
||||
- **Standalone-server debug** — the modifier hook is in-editor single-process only; promote to an `IRpcCommand` if remote-determinism testing is needed.
|
||||
- **Rate-limited turning** — `PlayerAimSystem` still snaps rotation; `EffectiveCharacterStats.TurnRate` is wired but unused.
|
||||
|
||||
## Next
|
||||
**M4 — Co-op** (2–4 players, client-hosted listen-server over Unity Relay) per [[Milestones]]. The modifier framework is the foundation for the upgrade/loadout meta-game on top of this slice.
|
||||
Reference in New Issue
Block a user