Docs: Phase 1.7 boon-overhaul build spec + DR-047 + session log + roadmap tick + CLAUDE.md 7-type-query gotcha
Boon overhaul CODE-COMPLETE (mechanic-changers): design forks locked, pre-code review (11 findings folded), post-impl review (incomplete on a session limit — raised findings self-verified, 2 real bugs fixed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: 2026-07-12 — Phase 1.7 Boon Overhaul (mechanic-changers)
|
||||
date: 2026-07-12
|
||||
tags: [session, coop-hades, phase-1.7, boons, netcode, dots]
|
||||
permalink: gamevault/07-sessions/2026-07-12-phase17-boon-overhaul
|
||||
---
|
||||
|
||||
# Phase 1.7 — Boon Overhaul
|
||||
|
||||
**Goal:** turn boons from flat-stat rows (the operator's "boring" verdict) into **mechanic-changers** so two runs feel
|
||||
different. The full `/dots-dev` spine ran (ground → forks → ctx7 → pre-code review → plan → serial impl → verify →
|
||||
post-impl review → docs). Build Spec: [[Boon_Overhaul_Build_Spec]]. Decision record: [[DR-047_Phase17_Boon_Overhaul_Mechanic_Changers]].
|
||||
|
||||
## Forks locked (operator, this session)
|
||||
1. **Ability-swap boons DEFERRED to Phase 3** (content-thin — only Ranger has alt abilities; the Warrior cone has none;
|
||||
couples to the client's `AbilityRef→class` derivation). Phase 1.7 = behavior-boons only.
|
||||
2. **~8 mechanic-changers + ~4 stat boons** (keep a few "safe" econ/pacing picks).
|
||||
3. **Synergy = tags for dedup + dominated-offer protection + light build-bias** (NO synergy bonuses in v1).
|
||||
|
||||
## What shipped
|
||||
**Architecture (netcode-correct, review-hardened):**
|
||||
- **`BoonEffects`** — new per-player `[GhostComponent(SendToOwnerType.SendToOwner)]` with `[GhostField] byte Pierce,Fork,Chain,Flags`.
|
||||
Baked INERT on the player (the `BoonOffer` idiom → **one deliberate player-ghost re-bake**); mutated on pick (non-structural);
|
||||
zeroed on the Returning edge. Rollback-correct via the GhostFields (owner is the sole predictor).
|
||||
- **`ProjectileEffectState`** — SERVER-ONLY (NOT a GhostField), baked inert on the projectile prefab → keeps the `Projectile`
|
||||
ghost hash FROZEN (the C1 correction; a field on `Projectile` itself WOULD change its StableTypeHash→serializer hash).
|
||||
Seeded server-side at spawn; a `FixedList64Bytes<Entity>` re-hit set excluded DURING target selection.
|
||||
- **`DashTrailState`** — server-only per-player Blade-Dash dedup keyed to `DashState.StartTick` (no reliable clear edge).
|
||||
- **`BoonDefBlob` +`Kind`/`EffectKind`/`Family`** discriminators (config blob, not wire); the 12-row table redefined.
|
||||
- **`TimedModifierUtil.Upsert`** (refresh-never-stack) + a `TimedModifier`-buffer `RemoveBySourceId` overload.
|
||||
- **`Tuning.FrenzySourceId = 0x00B0FFFF`** — pinned to the TOP of the boon band (the bottom-up pick counter can't reach it;
|
||||
the Returning range-strip still clears it free).
|
||||
|
||||
**Combat hooks:** pierce/chain in `ProjectileDamageSystem` (survive-and-continue, one destroy/tick preserved); fork = extra
|
||||
predicted projectiles at a spread in `AbilityFireSystem` (unique packed spawnId); knockback→pull negates `Dir` at the stamp
|
||||
(new `KnockbackUtil.Stamp(..., bool pull)`); Blade-Dash = new server `DashTrailDamageSystem`; Detonating Finisher inline in
|
||||
`MeleeComboSystem` (reads BoonEffects in the player loop, stashes IsFinisher/Detonate/Pull on `PendingCleave`); Siphon+Frenzy
|
||||
= new server `KillRewardSystem` off a `Dying.KillerNetId`+`Rewarded` latch stamped in `HealthApplyDamageSystem`.
|
||||
|
||||
**Offers:** `BoonMath.PickBoons` gains the owner's `BoonEffects` — dedup (skip an owned non-stacking flag), dominated-offer
|
||||
protection (no two same-`Family` in a deal → kills the "-15% vs -25% cd" bug), light ×1.5 build-bias. Integer-hash only.
|
||||
|
||||
**Table (12):** 1 Piercing·2 Split·3 Ricochet (Ranger, projectile) · 4 Detonating Finisher (Warrior) · 5 Blade Dash · 6 Gravity
|
||||
Pull · 7 Siphon · 8 Frenzy (both) · 9 Executioner · 10 Titan's Vigor · 11 Fleet Foot · 12 Berserker's Pace (both, stat).
|
||||
|
||||
## Reviews
|
||||
- **Pre-code (`wf_c4bdd60d-7b2`, 27 agents / 0 errors):** 23 raised → **11 confirmed**, all folded (C1 separate projectile
|
||||
component; C2 SendToOwner not All; C3–5 Frenzy top-of-band + Upsert + TimedModifier Returning-strip; C6 hit-set-excluded-in-
|
||||
selection; C7 dash dedup on StartTick; C8 inline finisher; C9 killer-capture-before-clear; C10 owned-state determinism doc).
|
||||
- **Post-impl (`wf_d2a0a673-af5`) — INCOMPLETE:** **4 verify agents died on a session limit** (the masquerade hazard — checked
|
||||
the failures list). Self-verified the raised findings against the code and fixed **2 real bugs the Build Spec §5 open-risks
|
||||
predicted** + 1 nit: (a) **Fork uncapped** → `shots = 1 + min(Fork,8)` (forkIndex is 4 spawnId bits, wrapped at 16); (b)
|
||||
**Blade-Dash hit-set overflow re-damaged** an unrecordable enemy every tick → break once `Hit` full; (c) the Slice-2 comment
|
||||
was displaced onto the BoonEffects lookup line (the `apply_text_edits` swallow gotcha) → relabelled.
|
||||
|
||||
## Verification
|
||||
- **L1** console clean vs baseline (only benign Server-Tick-Batching warnings), no Burst ICE.
|
||||
- **L2** **488/488 EditMode** (from 474; +13 new: pierce/chain, TimedModifierUtil Upsert, KillReward ×4, DashTrail ×2, offer
|
||||
dedup/owned-flag, a predicted-chain cycle guard) + **live Play smoke: `BoonEffects` server==owner-client** (set server
|
||||
Pierce3/Fork2/Chain1/Flags16 → replicated to the owning client), client connected (new GhostField didn't break the handshake
|
||||
→ ghost hash consistent), world-creation exception-free (no ordering cycle).
|
||||
- **L3** N/A — behavioral slice, no new visual assets; the boon effects in-combat are the operator's playtest.
|
||||
|
||||
## Wire/bake churn
|
||||
`BoonEffects` = ONE player-ghost re-bake (front-loaded; both worlds bake identically → handshake OK). `ProjectileEffectState`/
|
||||
`DashTrailState`/`Dying`/`BoonDefBlob` = no ghost-hash change. `StatModifier` + RPC collection UNCHANGED.
|
||||
|
||||
## Next-session intent
|
||||
**Phase 1.7 boon overhaul is CODE-COMPLETE + tests green + Play-smoke-verified.** NEXT = the operator's **feel/balance
|
||||
playtest** (the gate: "two consecutive runs feel different from boons alone") — tune knobs: Fork cap (8), DashTrail
|
||||
radius/damage (1.6/12), Frenzy duration/mult (240t/-0.30), Siphon heal (8), finisher detonation radius (3.5). Then **Phase 2 —
|
||||
Hub-ification** (mothball sieges, win moves fully expedition-side, base=hub; adversarial design-review first). Order stays
|
||||
1.7 → 2 → 3 → 4/5 parallel. Also OPEN (deferred): ability-swap boons (Phase 3), a client build-display HUD for active boons.
|
||||
|
||||
Related: [[Iteration_2026-07_CoopHades]] · [[DR-047_Phase17_Boon_Overhaul_Mechanic_Changers]] · [[Boon_Overhaul_Build_Spec]] · [[DR-044_Expedition_Redesign_Shipped_Demo_Polish]]
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: DR-047 — Phase 1.7 Boon Overhaul (mechanic-changers)
|
||||
date: 2026-07-12
|
||||
tags: [decision, coop-hades, phase-1.7, boons, netcode]
|
||||
permalink: gamevault/decisions/dr-047-phase17-boon-overhaul
|
||||
---
|
||||
|
||||
# DR-047 — Phase 1.7 Boon Overhaul: mechanic-changers
|
||||
|
||||
**Status:** shipped (code-complete, 488 EditMode green + Play smoke server==client). Supersedes the flat-stat-only boon
|
||||
table from [[DR-044_Expedition_Redesign_Shipped_Demo_Polish]]. Full build detail: [[Boon_Overhaul_Build_Spec]]; session:
|
||||
[[2026-07-12_Phase17_Boon_Overhaul]].
|
||||
|
||||
## Decision
|
||||
Boons become **8 mechanic-changers + 4 flat-stat** (operator forks: ability-swap DEFERRED to Phase 3; synergy = tags for
|
||||
dedup/dominated-protection/light-bias, NO synergy bonuses). Mechanic-changers ride a NEW per-player replicated
|
||||
`BoonEffects` component — NOT the flat `StatModifier` pipeline, which cannot express behavior changes.
|
||||
|
||||
## Invariants (these outlive the current table/tuning)
|
||||
1. **`BoonEffects` = `[GhostComponent(SendToOwnerType.SendToOwner)]` + `[GhostField] byte Pierce,Fork,Chain,Flags`,
|
||||
baked INERT on the player, zeroed on the Returning edge.** Rollback-correctness comes from the GhostFields, NOT the
|
||||
send type (owner is the sole predictor; needed so the owner predict-spawns the right Fork/Pierce projectiles).
|
||||
A pick MUTATES it (non-structural, baked-present) — never AddComponent.
|
||||
2. **Per-projectile pierce/chain state lives on a SEPARATE server-only `ProjectileEffectState`, never on `Projectile`.**
|
||||
Adding ANY field to the ghost `Projectile` changes its StableTypeHash→serializer hash = a projectile ghost re-bake;
|
||||
a separate non-ghost component keeps it frozen (the `KnockbackState` precedent).
|
||||
3. **Pierce/chain re-hit guard = a per-projectile hit-set excluded DURING target selection** (not post-filtered), and
|
||||
**exactly ONE `ecb.DestroyEntity` per projectile per tick** across the hit / survive+range / no-hit branches.
|
||||
4. **Fork = extra predicted projectiles at a spread**, each with a UNIQUE deterministic spawnId
|
||||
`(netId<<16)|((fireCount&0xFFF)<<4)|(forkIndex&0xF)`. **forkIndex is 4 bits → Fork MUST be capped ≤15** (shipped cap 8);
|
||||
an uncapped fork wraps forkIndex → spawnId collision → classification mis-match. (Open risk: fireCount is 12 bits →
|
||||
wraps at 4096 shots/run.)
|
||||
5. **On-kill boons (Siphon/Frenzy) live in a SEPARATE `KillRewardSystem`, not in `HealthApplyDamageSystem`** — healing the
|
||||
killer needs RW `Health` which would alias that system's `RefRW<Health>` victim query. The kill edge stamps
|
||||
`Dying.KillerNetId` (last player-sourced hit, captured BEFORE `dmg.Clear()`) + a `Rewarded` idempotency latch.
|
||||
6. **A run-scoped timed buff (Frenzy) is `Upsert`-ed (refresh, never stack) on a FIXED SourceId pinned to the TOP of the
|
||||
boon band** (`BoonSourceIdBase+Span-1`, unreachable by the bottom-up pick counter) and stripped from BOTH the
|
||||
`StatModifier` AND `TimedModifier` buffers on Returning (the range-strip only covers StatModifier).
|
||||
7. **`BoonMath.PickBoons` takes the owner's `BoonEffects`** for dedup (skip owned non-stacking flags) + dominated-offer
|
||||
protection (no two same-`Family` in a deal) + light ×1.5 build-bias — integer-hash only, drawn ONCE per RoomEpoch (the
|
||||
latch), so feeding owned-state stays deterministic per (seed,room,player,owned-at-draw).
|
||||
8. **Projectile boons (pierce/fork/chain) are Ranger-only (ClassMask=2)** — the Warrior's Fire is a cone, not a projectile.
|
||||
9. **`SystemAPI.Query` caps at 7 type args** → reading `BoonEffects` in the already-7-wide `AbilityFireSystem`/`MeleeComboSystem`
|
||||
player loops goes through a `ComponentLookup`, not an 8th query type.
|
||||
|
||||
## Deferred (open)
|
||||
Ability-swap boons (→ Phase 3, with a `PlayerClass` GhostField if the client class-derivation coupling matters); a
|
||||
client build-display HUD for active boons; DashTrail as a swept segment (v1 uses a per-tick radius — tunnel-safe at the
|
||||
current dash step); fireCount 12-bit spawnId budget.
|
||||
|
||||
## Process note
|
||||
Post-impl review (`wf_d2a0a673-af5`) had **4 verify agents die on a session limit** — an INCOMPLETE review masquerading as
|
||||
near-clean. The raised-but-unverified findings were self-verified against the code; two were real bugs (fork cap, dash
|
||||
overflow) the Build Spec §5 open-risks had predicted. Reinforces [[the failures-list check]] + the value of writing
|
||||
open-risks into the spec so a truncated review still has a checklist.
|
||||
Reference in New Issue
Block a user