Initial Combat Implementation
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
---
|
||||
tags:
|
||||
- design
|
||||
- abilities
|
||||
- data-driven
|
||||
- m3
|
||||
status: built
|
||||
updated: 2026-05-31
|
||||
permalink: gamevault/02-game-design/data-driven-abilities
|
||||
---
|
||||
|
||||
# M3 — Data-Driven Abilities & Modifiers (design)
|
||||
|
||||
> **Status: ✅ BUILT 2026-05-31** — runtime-validated on 6.4.7. Final architecture + build deviations in [[DR-004_M3_DataDriven_Abilities_Modifiers]]; session [[2026-05-31_M3_Data_Driven_Abilities]]. Build-time refinements vs this design: prefab refs went to a companion `AbilityPrefabElement` buffer (not the blob — blobs don't remap entity refs); recompute runs **every predicted tick** (not the dirty-flag option — rollback-correctness); `StatModifier.Target`/`Op` replicate as raw `byte`; `MaxHealth` single-sourced from the character SO; `PlayerAimSystem` left as-is. The design narrative below is preserved as the original intent.
|
||||
|
||||
> Goal: make the combat/ability system **scalable** by moving ability **and** character stats out of hard-baked components into **authored definitions**, with **runtime modifiers** (upgrades/buffs) mutating effective values.
|
||||
|
||||
## Why
|
||||
|
||||
M2 hard-codes combat values in baked components (`AbilityStats`, `Projectile` Speed/Damage/Range, `PlayerMoveStats`, `Health.Max`). Adding abilities or tuning balance means editing code/prefabs. For an ARPG with many abilities + an upgrade/modifier meta-game, definitions must be **data** a designer edits, and values must be **mutable at runtime** by upgrades/buffs. Locked by [[Pillars]]: server-authoritative + deterministic, so modifiers must be prediction-correct.
|
||||
|
||||
## Decisions (operator-chosen 2026-05-31)
|
||||
|
||||
- **Scope = pattern slice.** Establish the definition + modifier *pattern* and prove it by refactoring the **current projectile ability** + **1–2 sample abilities** onto it. Not building a full multi-archetype/loadout system yet (that comes later, on this foundation).
|
||||
- **Authoring = ScriptableObjects → baked to blob assets.** Designers edit SO assets in the inspector; a `Baker` converts them into DOTS-native immutable `BlobAssetReference` runtime data (Burst-fast, shared, zero per-instance cost).
|
||||
- **Modifiers = flat + percent stacks.** ARPG-standard additive (+X) and percentage (+X%) modifier stacks compute *effective* stats from the base definition; upgrades add/remove modifiers. **Server-authoritative + prediction-correct.**
|
||||
- **Stat scope = abilities + character stats.** One framework covers ability stats (damage/cooldown/range/projectile speed/auto-target) **and** character stats (max health, move speed, turn rate), so an upgrade can buff either.
|
||||
|
||||
## Architecture (proposed — DOTS-idiomatic)
|
||||
|
||||
**Authoring → runtime pipeline**
|
||||
1. `AbilityDefinition` / `CharacterStatsDefinition` **ScriptableObjects** (designer-facing; live under `Assets/_Project/Abilities/`).
|
||||
2. A baker bakes the SO set into a **singleton "definition database"**: a `BlobAssetReference<AbilityDatabaseBlob>` holding an array of ability/stat definitions indexed by a stable **`AbilityId`** (and `CharacterId`). Entity-prefab references (e.g. the projectile ghost) are resolved at bake into the blob; **managed/UI assets (icon, description, VFX/SFX prefabs) stay off the blob** (blobs are unmanaged) — looked up separately by id for presentation.
|
||||
3. Gameplay entities carry a light `AbilityRef { AbilityId }` / `CharacterStatsRef { CharacterId }` instead of inlined values; systems read the base values from the blob.
|
||||
|
||||
**Base → modifiers → effective stats**
|
||||
- `StatModifier` (buffer element): `{ StatTarget (enum: Damage, CooldownTicks, Range, ProjectileSpeed, MoveSpeed, MaxHealth, …), Op (Flat | PercentAdd | PercentMult), float Value, ModifierSource }`.
|
||||
- Per-entity `DynamicBuffer<StatModifier>` holds active modifiers (from upgrades, gear, buffs).
|
||||
- A deterministic `StatRecomputeSystem` computes **effective-stat components** (e.g. `EffectiveAbilityStats`, `EffectiveMoveStats`) from `base (blob) + modifiers` using the standard order: `effective = (base + Σ flat) × (1 + Σ percentAdd) × Π (1 + percentMult)`. Recompute on modifier-set change (dirty) rather than every tick.
|
||||
- Gameplay systems read the **effective** components: `AbilityFireSystem` uses effective damage/cooldown/range; `PlayerMoveSystem` uses effective move speed; health uses effective max.
|
||||
|
||||
**Netcode determinism** (the important constraint)
|
||||
- **Definitions** are static config — baked identically into both worlds' blobs; **not replicated**.
|
||||
- **Modifiers** affect server-authoritative damage, so the predicted client must compute the *same* effective stats: the active-modifier state is **replicated** (a ghost buffer of `StatModifier`, or derived deterministically from a replicated upgrade-level/loadout component). `StatRecomputeSystem` is a pure function → predicted + server results match. No wall-clock; timed buffs (a later extension) expire on `NetworkTick`.
|
||||
|
||||
## Proposed definition fields (refine at build time)
|
||||
|
||||
**AbilityDefinition:** `AbilityId`; `DisplayName` (FixedString); *[authoring/UI only]* `Description`, `Icon`; `Archetype` (enum — Projectile now; Hitscan/MeleeCone/AoE/Buff later); `Damage`; `DamageType`/`Element` (enum, for resistances later); `CooldownTicks`; `Range`; `ProjectileSpeed`; `AutoTargetRange`, `AutoTargetConeDegrees`; `ProjectilePrefab` (entity-prefab ref, baked); *[UI]* `MuzzleVfx`/`HitVfx`/`Sfx`; `Tags` (FixedList — for modifier targeting, e.g. "all Fire abilities +10%"). *Future:* `ResourceCost`, `CastTime`, `Charges`.
|
||||
|
||||
**CharacterStatsDefinition:** `CharacterId`; `MaxHealth`; `MoveSpeed`; `TurnRate`; (extensible).
|
||||
|
||||
## Refactor target (the pattern slice)
|
||||
|
||||
Move M2's hard-baked values onto the data model: `AbilityStats` + `Projectile`(Damage/Speed/Range) ← ability definition by id; `PlayerMoveStats` + `Health.Max` ← character-stats definition; gameplay reads route through the effective-stat components. Add 1–2 sample abilities (e.g. a faster low-damage shot + a slow heavy shot) purely as data to prove no code changes are needed per ability.
|
||||
|
||||
## Open questions (defer to build)
|
||||
|
||||
- Modifier replication: ghost buffer of active modifiers vs. replicate a compact upgrade/loadout state and re-derive modifiers locally (less bandwidth).
|
||||
- Recompute trigger: dirty-on-change vs every-tick (perf vs simplicity).
|
||||
- How many/which sample abilities; whether to include a basic upgrade source (pickup/level) to exercise modifiers, or stub modifiers via the debug hook.
|
||||
- UI/icon/description pipeline (managed lookup keyed by id).
|
||||
- Tag/element taxonomy (kept minimal until needed).
|
||||
|
||||
## Related
|
||||
[[DR-003_M2_Combat_Netcode_Architecture]] (the stats this refactors) · [[Systems_Index]] · [[Pillars]] (server-authoritative + deterministic).
|
||||
@@ -18,6 +18,18 @@ One design doc per gameplay system, linked here. Each should state: purpose, com
|
||||
- **Systems:** `PlayerMoveSystem`, `PlayerAimSystem` (`PredictedSimulationSystemGroup`, `.WithAll<Simulate>()`, deterministic — `SystemAPI.Time.DeltaTime` only); `PlayerInputGatherSystem` (client, `GhostInputSystemGroup`); `GoInGameClientSystem` (client) / `GoInGameServerSystem` (server — spawns the owner-predicted ghost, stamps `GhostOwner`, `LinkedEntityGroup` auto-despawn).
|
||||
- **Netcode shape:** player = **owner-predicted** ghost; client sends input only; server is authoritative. Status: **code-complete + EditMode-verified**; live runtime blocked by [[DR-002_Unity66_Alpha_Netcode_Transport]].
|
||||
|
||||
### M2 — Combat (predicted projectile, server damage) · [[2026-05-31_M2_Combat]]
|
||||
|
||||
- **Components** (`ProjectM.Simulation`): `Health` (`[GhostField]` Current; baked Max); `HitRadius`; `DamageEvent` (IBufferElementData); `AbilityStats` (auto-target range/cone, cooldown ticks — baked); `AbilityCooldown` (`[GhostField]` NextFireTick); `Projectile` (`[GhostField]` Direction + SpawnId; baked Speed/Damage/Range); `ProjectileSpawner` / `TrainingDummySpawner` (baked singletons); `TrainingDummyTag`. `PlayerInput` gains `Fire` (`InputEvent`).
|
||||
- **Systems:** `AbilityFireSystem` (predicted; `IsFirstTimeFullyPredictingTick`-gated predict-spawn; server branch applies `AutoTarget`); `ProjectileMoveSystem` (predicted); `ProjectileClassificationSystem` (client; predicted-spawn match by `SpawnId`; **non-Burst**); `ProjectileDamageSystem` (server; **swept** segment-vs-sphere hit); `HealthApplyDamageSystem` (server; DamageEvent → Health, dummy death-despawn); `TrainingDummySpawnSystem` (server; one-shot). Input: `PlayerInputGatherSystem` rewritten as managed `SystemBase` over the generated `ProjectMInput` action-map wrapper.
|
||||
- **Netcode shape:** projectile = **owner-predicted** ghost, client predict-spawns + classifies against server truth by `SpawnId=(ownerNetId<<16)|absoluteFireCount`; **auto-target & damage server-authoritative**; `Health.Current`/`Projectile.Direction` replicate. Status: **foundation built + runtime-validated** (server loop + replication); live keypress-fire pending an interactive test. Decisions: [[DR-003_M2_Combat_Netcode_Architecture]].
|
||||
|
||||
### M3 — Data-driven abilities & modifiers · [[2026-05-31_M3_Data_Driven_Abilities]]
|
||||
|
||||
- **Components** (`ProjectM.Simulation`): `AbilityDatabase` (singleton `BlobAssetReference<AbilityDatabaseBlob>`; `AbilityDefBlob`/`CharacterStatsBlob` keyed by `AbilityId`/`CharacterId` byte) + `AbilityPrefabElement` (companion entity-ref buffer for projectile prefabs); `AbilityRef` (`[GhostField]` id) / `CharacterStatsRef`; `StatModifier` (replicated `[GhostField]` buffer, `OwnerSendType.All`, raw-byte `StatTarget`/`ModOp`); `EffectiveAbilityStats` / `EffectiveCharacterStats` (derived, not replicated); `UpgradePickup` / `UpgradePickupSpawner`. `StatMath` (pure fold). **Removed** M2's `AbilityStats` / `PlayerMoveStats`.
|
||||
- **Systems:** `StatRecomputeSystem` (predicted, `[UpdateBefore]` Aim/Move; folds blob base + modifier buffer → `Effective*` **every tick** — rollback-correct); `AbilityFireSystem` rerouted (effective stats + prefab-by-id + snapshot-at-fire); `PlayerMoveSystem` → effective move; `UpgradePickupSpawnSystem` / `UpgradePickupSystem` (server; overlap-grant via `AppendToBuffer`); `DebugModifierInjectionSystem` (editor-only, server world); `HealthApplyDamageSystem` clamps to effective MaxHealth. Authoring: `AbilityDefinition`/`CharacterStatsDefinition` SOs + `AbilityDatabaseAuthoring` blob baker.
|
||||
- **Netcode shape:** definitions = baked config (not replicated, identical both worlds); modifiers = **replicated ghost buffer** on the player → both worlds recompute identical effective stats (prediction-correct, validated under tick-batching); pickup = **interpolated** server-authoritative ghost. Status: **built + runtime-validated** (EditMode 38/38). Decisions: [[DR-004_M3_DataDriven_Abilities_Modifiers]].
|
||||
|
||||
## Conventions
|
||||
|
||||
DOTS/ECS conventions live in repo `CLAUDE.md` and the `dots-dev` skill's `dots-conventions.md`. Don't duplicate volatile API details here — link to context7-derived notes instead.
|
||||
Reference in New Issue
Block a user