Initial Combat Implementation

This commit is contained in:
Luis Gonzalez
2026-05-31 21:35:12 -07:00
parent 7fa77ce821
commit 1f647dd5e1
166 changed files with 93337 additions and 91 deletions
@@ -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** + **12 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 12 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).