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
+3
View File
@@ -0,0 +1,3 @@
[
"obsidian-local-rest-api"
]
@@ -0,0 +1,11 @@
{
"port": 27124,
"insecurePort": 27123,
"enableInsecureServer": false,
"apiKey": "5f7fd910de8063b1e2c9b8627fb89a06cd25ea4e345266d9a1e977b2c8fab91e",
"crypto": {
"cert": "-----BEGIN CERTIFICATE-----\r\nMIIDRTCCAi2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDExdPYnNp\r\nZGlhbiBMb2NhbCBSRVNUIEFQSTAeFw0yNjA2MDEwMzQzMTJaFw0yNzA2MDEwMzQz\r\nMTJaMCIxIDAeBgNVBAMTF09ic2lkaWFuIExvY2FsIFJFU1QgQVBJMIIBIjANBgkq\r\nhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmyKPpip2R3CZ5sMtChuZXkEnfg+fuD5f\r\nnzfbk1M5LbwSM/0VWIA/VBlnbeEQanl7iWJ24GpM3Cu8gm2U+lcXefcMOU10Qs7N\r\nZpvOuIsiafOuQuMJR2ebd6vra11aSvp75aHybTouvx7fM1JM4jHrni9+VS6rxm0N\r\nAfHwW1sWNHyJsLsmBd0cjFxleZdxDRYenhBRxbqos3QSYLTRKNUnmD4/O7ZhHiGC\r\n/uhR0ZvVtd5L+nHDrjqBOSBjhnVelmbYYyym2zXfGvef55skFzDj1w0fxAZvoGwY\r\nVXlghnf9k/uSo3RFZo6QgtDaTNwmE4NvysJgOEOWdOarkwXXWnXfIwIDAQABo4GF\r\nMIGCMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgLEMDsGA1UdJQQ0MDIG\r\nCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcD\r\nCDARBglghkgBhvhCAQEEBAMCAPcwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0B\r\nAQsFAAOCAQEAIfLcusgIMN4z/8giIoQsui/sHvSCK1oY+knkZfaPbN4ZKyI0liU9\r\nrJHJ9CLF2eRzEFTUTQ6er6fencAub4hmb26BO6ZVbp4LyEAj0W2TN2CHRdIEWLpE\r\nSMckFa+pIAcfzVj7pLXW8WCCUA4B8pDdg6h9drEkHNKSggyTIzuny7+KKpfoTRyi\r\n25PACobDXfzkQ/i2uoHjNHqVi2MNgWX+VO55zF9PxsuPNNjxfFyFd84iNw9xZ1UR\r\n63SjkV0/7weG1z8kmkNPAaDFQVR7toy7+bXZ6SFK+f8SB2PSg2krHxzfZRfTzgpx\r\naRM3ltg1JWAH/Iv16UMAegi3bu8fJl7GNg==\r\n-----END CERTIFICATE-----\r\n",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEAmyKPpip2R3CZ5sMtChuZXkEnfg+fuD5fnzfbk1M5LbwSM/0V\r\nWIA/VBlnbeEQanl7iWJ24GpM3Cu8gm2U+lcXefcMOU10Qs7NZpvOuIsiafOuQuMJ\r\nR2ebd6vra11aSvp75aHybTouvx7fM1JM4jHrni9+VS6rxm0NAfHwW1sWNHyJsLsm\r\nBd0cjFxleZdxDRYenhBRxbqos3QSYLTRKNUnmD4/O7ZhHiGC/uhR0ZvVtd5L+nHD\r\nrjqBOSBjhnVelmbYYyym2zXfGvef55skFzDj1w0fxAZvoGwYVXlghnf9k/uSo3RF\r\nZo6QgtDaTNwmE4NvysJgOEOWdOarkwXXWnXfIwIDAQABAoIBAAgNexH44f+55rfK\r\nMqnhgt6qWJDgvU/W3ieE5yzIsrOMnX3ZEtK4yzVRoWfKAoxGoAu04E8aHpyp/Ibh\r\nPQO9od0McNi1wvfjF+vOBclqzlQTCsrFv6DXFztEmJJ9beKSLkWnWDySX5aIZ5C9\r\nInsavbD7pLSzhnyJlXEf+gYmPmZmIOcENLTjLnRWYQv71hiEnIDNSlP19xRNe0eZ\r\nHpcahKyMFVZLm2FRs/dkygMKW80laaxd8bsq3PPA1Guzt50w0nGBH6K+x3N+ewmC\r\ndR2gSbH6OMHZ8XkkpqJlWKX9N182O5vhqy3YEplEq0T8MDhqWMLDZsH74OPzN/N0\r\nkbaZH5ECgYEA2OZBMZZ204dcBH78dqBlaEaGjbNRozFtyrgWnoYrZxuB+euHEwi0\r\nirqhl0i8yj1PhwmsGTbxNEFeqrFJvSsHoH4TQ7oJisawcrxmAbtxvPO7ZyZajVVZ\r\nAQwSg7uJrdsYDAXegOkytTkcFS2WgJkxhDJBc75bTo5zNx0KGYUaSfkCgYEAtxns\r\ntwsNfkJ2pcwhGvjCGBjFa8f+xaFUoNPWP0LY2wOurh930dB0lMOiLy3MSVV8Azv9\r\nSr8Y9S0o5vy+Ph0muFlHhz1JXL9sBWzYPxaGy6PNGjErGgRZUZqysVMxKaTsNwWC\r\nks48ouMSuHZx8bepi2dBImzUd24UusWP2SjAGPsCgYEAz0I2ycudgEO4ChPN6w0Z\r\n2aqKqJzRRb/VygBenNxYXQ5MBSSqzeVjn6z2/DjlLduoQsFbUjzN/8D3VORUMg8M\r\nGrBeeDktJQ73TKv6TW1wIb5FNSvRG3ySsA15I6fwx9C0CckR9NzhN4p660HErt20\r\nEz5yjMc5OLemIOP+4qPtmwECgYAH99TZUl3P9Mx9ApkeN10a91kAC8AGkbLBHPbh\r\n4eLWBR8A7NWmB9BK+QiBzRhqyJGRAndPXWmUodZ51t3gjhw1QY1fUUd0BdCJm+b9\r\nN9m92u7+CM27YB0S9Ax6swgcq9SrwE2iXd89p2wVIvJqdnemXWP0P5AvclbsmdRc\r\nuu1BKQKBgBPS78pPNrSO1+dBO2LCqujWA6zduv0uABODR0SwSJzUS9OSXoBE9oY8\r\n94OV1q0akOBLVrxdDYypQkOp8/iyS+8W34AQdLhYF2ILXHi8WAC5mymvTYUh2HH+\r\nBOeel4ED9pnBZd49krHlA/fXE7+JHCeOdduvNjfMlH81Ad2eTf1N\r\n-----END RSA PRIVATE KEY-----\r\n",
"publicKey": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmyKPpip2R3CZ5sMtChuZ\r\nXkEnfg+fuD5fnzfbk1M5LbwSM/0VWIA/VBlnbeEQanl7iWJ24GpM3Cu8gm2U+lcX\r\nefcMOU10Qs7NZpvOuIsiafOuQuMJR2ebd6vra11aSvp75aHybTouvx7fM1JM4jHr\r\nni9+VS6rxm0NAfHwW1sWNHyJsLsmBd0cjFxleZdxDRYenhBRxbqos3QSYLTRKNUn\r\nmD4/O7ZhHiGC/uhR0ZvVtd5L+nHDrjqBOSBjhnVelmbYYyym2zXfGvef55skFzDj\r\n1w0fxAZvoGwYVXlghnf9k/uSo3RFZo6QgtDaTNwmE4NvysJgOEOWdOarkwXXWnXf\r\nIwIDAQAB\r\n-----END PUBLIC KEY-----\r\n"
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
{
"id": "obsidian-local-rest-api",
"name": "Local REST API with MCP",
"version": "4.1.2",
"minAppVersion": "1.4.0",
"description": "A secure REST API and Model Context Protocol (MCP) server for your vault.",
"author": "Adam Coddington",
"authorUrl": "https://adamcoddington.net/",
"isDesktopOnly": true
}
@@ -0,0 +1,55 @@
/* Sets all the text color to red! */
div.obsidian-local-rest-api-settings div.api-key-display {
margin-bottom: 20px;
}
div.obsidian-local-rest-api-settings pre {
font-size: 0.8em;
padding: 10px 20px;
margin: 10px 25px;
background-color: var(--background-modifier-cover);
font-family: monospace;
user-select: all;
}
div.obsidian-local-rest-api-settings div.setting-item-control {
min-width: 50%;
}
div.obsidian-local-rest-api-settings textarea {
width: 100%;
}
div.obsidian-local-rest-api-settings div.certificate-expired {
padding: 10px 20px;
border: 2px solid #ff0000;
}
div.obsidian-local-rest-api-settings div.certificate-expiring-soon {
padding: 10px 20px;
border: 2px solid #ffff00;
}
div.obsidian-local-rest-api-settings div.certificate-regeneration-recommended {
padding: 10px 20px;
border: 2px solid #ffff00;
}
div.obsidian-local-rest-api-settings table.api-urls tr {
width: 100%;
}
div.obsidian-local-rest-api-settings table.api-urls th,
div.obsidian-local-rest-api-settings table.api-urls td {
padding: 5px 25px;
}
div.obsidian-local-rest-api-settings table.api-urls tr.disabled td.name,
div.obsidian-local-rest-api-settings table.api-urls tr.disabled td.url {
text-decoration: line-through;
}
div.obsidian-local-rest-api-settings p {
padding: 0px 15px;
}
@@ -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).
@@ -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.
+14 -3
View File
@@ -15,6 +15,17 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com
- [ ] **Re-validate the M1 play-tick on a stable Unity 6.x** — live runtime blocked on the 6.6 alpha ([[DR-002_Unity66_Alpha_Netcode_Transport]]); optionally reproduce with the `networked-cube` sample to file a bug.
- [ ] Replace template `SampleScene` with a dedicated bootstrap scene + gameplay subscene.
- [ ] Optional template cleanup: remove `com.unity.visualscripting`, `Assets/TutorialInfo/`, `Assets/Readme.asset` (delete each asset **with** its `.meta`).
- [ ] Decide **relay provider** (default Unity Relay) before M3.
- [ ] Decide home-base **grid 2D vs 3D** before M5.
- [ ] Decide **production replication** (predicted vs server-only) before M6.
- [ ] Decide **relay provider** (default Unity Relay) before M4 (co-op).
- [ ] Decide home-base **grid 2D vs 3D** before M6 (build/placement).
- [ ] Decide **production replication** (predicted vs server-only) before M7 (automation).
- [ ] **M2 follow-up — restart the editor to clear the corrupted Burst cache**, then confirm the console is clean on a warm play (no "not a known Burst entry point"). See [[2026-05-31_M2_Combat]] / [[DR-003_M2_Combat_Netcode_Architecture]].
- [ ] **M2 follow-up — live interactive fire test** (focused Play Mode: press Space / LMB / RT → predicted projectile + dummy HP drop). The server combat loop + replication are validated; the input→`AbilityFireSystem`→predicted-spawn→classification path is only validated structurally.
- [ ] **M2 follow-up — mouse-cursor aim for KBM** (needs a camera ground-ray rig); currently aim = gamepad right-stick + movement-heading fallback.
- [ ] **M2 follow-up — player death/respawn** (M2 only clamps player HP ≥ 0; dummies despawn on death).
- [ ] M2 polish — projectile/dummy visuals (primitive meshes/materials currently); optional predicted client-side auto-target if the soft server reconcile feels off.
- [ ] **M3 follow-up — UI/icon/description pipeline** for abilities (managed lookup keyed by `AbilityId`, off the blob). Deferred from M3 ([[2026-05-31_M3_Data_Driven_Abilities]]).
- [ ] **M3 follow-up — timed / removable modifiers** (expiry on `NetworkTick`, `ClearByType` via `StatModifier.SourceId`). M3 modifiers are permanent-once-granted.
- [ ] **M3 follow-up — multi-prefab abilities** (a per-ability *different* projectile ghost) needs `ProjectileClassificationSystem` generalized beyond the single shared prefab.
- [ ] **M3 follow-up — standalone-server debug modifier path** via `IRpcCommand` (current `DebugModifierInjectionSystem` is in-editor single-process only).
- [ ] **M3 follow-up — rate-limited turning** (`PlayerAimSystem` still snaps rotation; `EffectiveCharacterStats.TurnRate` is wired but unused).
- [ ] **M3 polish — pickup visuals** (primitive sphere/default material currently); pickup auto-grant feel (continuous overlap).
+6 -5
View File
@@ -12,10 +12,11 @@ permalink: gamevault/06-roadmap/milestones
|---|---|---|
| **M0 — Foundation** | DOTS + Netcode stack, asmdef split, bootstrap, smoke test green | ✅ Done 2026-05-29 — [[2026-05-29_Project_Setup]] |
| **M1 — Player slice** | Server-spawned owner-predicted player; twin-stick WASD + directional aim | ✅ Done 2026-05-31 — runtime-validated on Unity 6.4.7 (connect→spawn→owner-predicted ghost→replication; EditMode 3/3). The 6.6 failure was environment-specific, see [[DR-002_Unity66_Alpha_Netcode_Transport]] — [[2026-05-30_M1_Player_Slice]] |
| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | |
| **M3 — Co-op** | 24 players; client-hosted listen-server over Unity Relay | ⬜ |
| **M4 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | ⬜ |
| **M5 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
| **M6 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: input→fire→**predicted projectile**→**swept hit**→server damage→`Health` `[GhostField]` replicated server→client; movement + fire confirmed live; EditMode 22/22. Predicted-projectile + server auto-target + non-Burst classifier — [[DR-003_M2_Combat_Netcode_Architecture]], [[2026-05-31_M2_Combat]]. (Projectile ghost-map errors appear only under server tick-batching from running two editors at once — close the reference editor for clean netcode.) |
| **M3 — Data-driven abilities & modifiers** | Ability **and** character stats authored in ScriptableObjects, baked to DOTS **blob assets**; runtime **flat + % modifier** stacks (upgrades/buffs) → effective stats, server-authoritative + prediction-correct. Pattern slice: refactor the current projectile ability + 12 sample abilities onto the data model. | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: blob DB baked into both worlds; data-driven base + replicated `StatModifier` ghost buffer → **identical effective stats on server & owner-predicted client** (held under tick-batching); data-only ability swap; real pickup grant; EditMode 38/38. Blob DB + replicated modifier buffer + every-tick effective recompute — [[DR-004_M3_DataDriven_Abilities_Modifiers]], [[2026-05-31_M3_Data_Driven_Abilities]]. |
| **M4 — Co-op** | 24 players; client-hosted listen-server over Unity Relay | ⬜ |
| **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | ⬜ |
| **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
Promote items from [[Backlog]] here when committed.
@@ -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 + ~3040s 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** (24 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.
@@ -0,0 +1,35 @@
---
id: DR-003
title: M2 combat netcode architecture — predicted projectiles, server-authoritative auto-target & damage, swept hits
status: accepted
date: 2026-05-31
tags:
- decision
- netcode
- combat
- prediction
permalink: gamevault/07-sessions/decisions/dr-003-m2-combat-netcode-architecture
---
# DR-003 — M2 Combat Netcode Architecture
## Context
M2 builds the [[Milestones|combat foundation]]: directional ability fire + deterministic soft auto-target + server-authoritative health/damage, on Unity 6.4.7 / Netcode for Entities 1.13.2. Several architecture forks materially shape every future combat system, so they were settled up front (operator-approved) and validated against the official ECS samples + live `unity_reflect`.
## Decision
1. **Projectiles are owner-predicted ghosts with client predicted-spawn.** The firing client predict-spawns the projectile immediately and a custom classification system pairs it with the server's authoritative ghost by a `SpawnId = (ownerNetworkId << 16) | absoluteFireCount`. Pattern lifted verbatim from HelloNetcode `02_PredictedSpawning` (`ProcessFireCommandsSystem` + `GrenadeClassificationSystem`). Spawning is gated on `NetworkTime.IsFirstTimeFullyPredictingTick` so rollback never double-spawns; the absolute fire count is read from `InputBufferData<PlayerInput>.GetDataAtTick(ServerTick)` (the live `InputEvent.Count` is only the per-tick delta).
2. **Fire is a netcode `InputEvent`** on `PlayerInput` (`[GhostField]`). The client gather (`PlayerInputGatherSystem`, managed `SystemBase`) resets it each frame and calls `.Set()` on the press edge; netcode latches the monotone absolute count into the command buffer.
3. **Auto-target is server-authoritative, resolved at the fire tick.** The client predicts the shot along raw aim; the server runs the in-cone nearest-target search (`AutoTarget.Resolve`, deterministic, ties by index) and writes the assisted direction into the projectile's `[GhostField] Direction`, which reconciles the client's predicted projectile. Because the assist arc is soft (small), the reconciliation correction is minor.
4. **Damage is server-only.** `Health.Current` is a `[GhostField]` (replicated for display/reconcile); hits and destruction run only on the server (`ProjectileDamageSystem``DamageEvent` buffer → `HealthApplyDamageSystem`). Clients do not predict hits/destruction (matches the sample). No Unity Physics — hits are distance/cone math (Physics arrives at M4).
5. **Projectile hit detection is a swept segment-vs-sphere test**, not a point check (see Consequences).
## Consequences
- **Predicted-spawn classification cannot be `[BurstCompile]`d on 1.13.2.** Bursting the classifier trips a Burst *internal compiler error* (type-hash resolution failure) on the cross-assembly generic `SnapshotDataBufferComponentLookup.TryGetComponentDataFromSnapshotHistory<T>()`. `ProjectileClassificationSystem` is therefore a non-Burst `ISystem` (it only runs when ghost spawns are *received* — a cold path). Note: in 1.13.2 that method takes `ref DynamicBuffer<SnapshotDataBuffer>` (the official sample's by-value form is older).
- **Predicted projectile + server auto-target = a deliberate reconciliation curve.** The firing client briefly sees its projectile along raw aim until the server's auto-targeted `Direction` replicates. Acceptable for soft assist + co-op PvE. Upgrade path if it ever feels off: run auto-target predictively (needs deterministic, stable-id target selection on both worlds).
- **Point-distance hit tests tunnel** — a fast projectile (or any projectile while the server tick-batches under load) steps past a target's hit radius in a single tick. Caught at runtime (a speed-25 shot missed; speed-3 hit). Fixed with a swept test over each tick's travel segment (`[curPos - dir*speed*dt, curPos]`), `ProjectileDamageSystem` ordered `[UpdateAfter(ProjectileMoveSystem)]`, earliest-along-path target wins. Regression-tested in `ProjectileDamageSystemTests`.
- **Deferred** (revisit at the noted milestone/trigger): live interactive fire validation (press Fire in a focused editor — Input System ignores injected device input while the Game view is unfocused); predicted-spawn *client* feel; mouse-cursor aim for KBM (needs a camera ground-ray rig); player death/respawn (M2 clamps player HP ≥ 0, only dummies despawn).
Mirrors the server-authoritative + input-only-clients pillar from [[Pillars]]; extends the M1 predicted player slice ([[2026-05-30_M1_Player_Slice]]).
@@ -0,0 +1,40 @@
---
id: DR-004
title: M3 data-driven abilities & modifiers — blob definition DB, replicated StatModifier buffer, every-tick effective recompute
status: accepted
date: 2026-05-31
tags:
- decision
- netcode
- combat
- data-driven
- prediction
permalink: gamevault/07-sessions/decisions/dr-004-m3-data-driven-abilities-modifiers
---
# DR-004 — M3 Data-Driven Abilities & Modifiers
## Context
M2 hard-coded combat/character values into baked components (`AbilityStats`, `Projectile.Speed/Damage/Range`, `PlayerMoveStats`, `Health.Max`). M3 ([[Data_Driven_Abilities]]) moves those into **authored ScriptableObject definitions baked to DOTS blob assets**, with a runtime **flat + % modifier stack** producing effective stats — server-authoritative and prediction-correct. These forks shape the whole upgrade/buff meta-game, so they were settled up front (operator-approved at intake) and validated against context7 (Entities 6.4 / Netcode 1.13.2) + the official ECS samples, then runtime-confirmed. Extends [[DR-003_M2_Combat_Netcode_Architecture]].
## Decision
1. **Definitions → blob, not replicated.** `AbilityDefinition` / `CharacterStatsDefinition` ScriptableObjects → an `AbilityDatabaseAuthoring` baker builds a `BlobAssetReference<AbilityDatabaseBlob>` (`BlobArray` of ability/character entries, each keyed by a stable `AbilityId`/`CharacterId` byte + a `FixedString64Bytes` name) and adds an `AbilityDatabase` singleton. Baked in the gameplay subscene → streams identically into both worlds (config). Definitions are **never replicated**.
2. **Entity/prefab refs live OUTSIDE the blob.** Blob assets don't remap entity references. The per-ability projectile ghost prefab is resolved via `GetEntity` into a **companion `DynamicBuffer<AbilityPrefabElement>{ byte Id; Entity Prefab }`** on the database singleton (entity refs in components *are* patched). `AbilityFireSystem` resolves the prefab by the firing player's `AbilityRef.Id`.
3. **Modifiers replicate as a `[GhostField]` buffer.** `StatModifier : IBufferElementData` (`[GhostComponent(OwnerSendType = SendToOwnerType.All)]`, `[InternalBufferCapacity(8)]`) on the player ghost. **`Target`/`Op` replicate as raw `byte`** (mapped to the `StatTarget`/`ModOp` enums only in the pure math), to keep the generated serializer trivial and dodge the cross-assembly enum-codegen hazard that already de-Bursted the M2 classifier. Server is the only writer; the owner **must** receive it (`All`) or it mispredicts.
4. **Effective stats are derived, never replicated.** `EffectiveAbilityStats` / `EffectiveCharacterStats` (plain `IComponentData`) are computed by `StatRecomputeSystem` (Burst, `PredictedSimulationSystemGroup`, `[UpdateBefore]` Aim+Move so all consumers read fresh) folding blob base + the replicated modifier buffer via the pure `StatMath.Apply``effective = (base + Σflat) × (1 + ΣpercentAdd) × Π(1 + percentMult)`.
5. **Recompute EVERY predicted tick, no dirty flag.** Both inputs (blob base, replicated buffer) are restored on rollback; the `Effective*` components are **not** in the ghost snapshot, so a dirty-flag/change-filter would leave them stale across reprediction. Unconditional per-tick recompute over a tiny buffer is cheap and unconditionally prediction-correct.
6. **Projectile stats are snapshotted at fire.** `AbilityFireSystem` reads `EffectiveAbilityStats` and writes effective Damage/Speed/Range into the spawned `Projectile`, so the downstream move/damage systems are unchanged and predicted+server projectiles match (both folded the same replicated modifiers this tick).
7. **Upgrade source = both.** A real server-authoritative pickup (`UpgradePickup` interpolated ghost + subscene spawner; `UpgradePickupSystem` in `SimulationSystemGroup`, NOT predicted, overlap-grants a modifier via `ecb.AppendToBuffer` and despawns), plus an editor-only `DebugModifierInjectionSystem` running in the **server** world (static pokes → applied to the local player; a client-side append would be stomped by the next snapshot).
8. **`MaxHealth` single source = `CharacterStatsDefinition`.** `PlayerAuthoring` references the SO and seeds `Health.Current=Max` from it; `EffectiveCharacterStats.MaxHealth` (base+mods) is the runtime clamp ceiling in `HealthApplyDamageSystem` (no auto-heal). `PlayerAimSystem` left as-is (it snaps rotation and ignores turn rate; `TurnRate` kept in effective stats for future rate-limited turning).
## Consequences
- **Ghost-buffer-on-owner-predicted-ghost is the load-bearing surface.** Validated at runtime: a server-granted `+50 Damage` / `+50% MoveSpeed` produced **identical** `effDmg=70` / `effMove=9` on server **and** owner-predicted client — replication + recompute agree, no divergence. This held **even under server tick-batching** (the M2 stress condition), confirming the every-tick recompute choice.
- **`readonly` blob methods are a footgun.** A `readonly` struct method that calls a non-readonly member on a `BlobArray` field forces a defensive copy of the array → its relative-offset pointer breaks → the array reads empty. Caught by a unit test (lookups returned false for present ids); fix = drop `readonly`, always reach the blob through `ref blob.Value`.
- **Zero code per new ability.** Two sample abilities (FastLight, SlowHeavy) are pure SO data; swapping `AbilityRef.Id` (a `[GhostField]`) re-points the same fire code at different blob stats. Runtime-confirmed: cycling Primary→FastLight gave `dmg 20→8, spd 25→40, cd 12→5` on both worlds with no code path change.
- **Pickup is interpolated (server-authoritative), like the dummy.** Clients see/despawn it; the grant flows server → snapshot → owner. The debug hook is in-editor single-process only; a standalone-server debug path would need an `IRpcCommand` (deferred).
- **Deviations from the proposed design** (refined at build time): prefab refs → companion buffer (not the blob); recompute every-tick (not the doc's dirty-flag lean); raw-byte enum replication; `MaxHealth` sourced from the SO not a separate authoring number.
Mirrors the server-authoritative + deterministic pillar from [[Pillars]]; the modifier framework is the foundation the full ability/loadout/upgrade system builds on.