docs(plan): weapon OD rating implementation plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
10f8d135e5
commit
f92d158fd0
1 changed files with 387 additions and 0 deletions
387
docs/superpowers/plans/2026-07-15-weapon-od-rating.md
Normal file
387
docs/superpowers/plans/2026-07-15-weapon-od-rating.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# Weapon OD Rating Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Compute a Mag-Tools "OD" (over-damage vs best retail) rating for melee/missile/wand weapons at ingest, store it on `items.od_rating`, backfill existing rows, and expose `min_od`/`max_od` search filters + an OD column/detail row in the React inventory search.
|
||||
|
||||
**Architecture:** New `inventory-go/od.go` ports UtilityBelt's OD calc (`UtilityBelt/UtilityBelt/Tools/ItemInfo.cs` + `Lib/ItemInfoHelper/{WeaponMods,MiscCalcs,Dictionaries}.cs`, all in this workspace). `processItem` calls it and writes `od_rating` into the `items` map (the dynamic `buildInsert` in `ingest.go` picks it up with no other change). A backfill endpoint recomputes over stored `item_raw_data.original_json`. Search adds `min_od`/`max_od`/sort. Frontend adds one rating + one column + one detail row. Spec: `docs/superpowers/specs/2026-07-15-weapon-od-rating-design.md`.
|
||||
|
||||
**Tech Stack:** Go 1.25 (backend); React 19 + TS + Vite (frontend). Go has no local toolchain — tests run in a throwaway `golang:1.25-bookworm` container on `overlord.snakedesert.se`; the Dockerfile's `RUN go test ./...` gates the image build. Frontend build = `npm run build` locally.
|
||||
|
||||
**Working dir for git:** `C:/Users/erikn/source/repos/dereth-workspace/MosswartOverlord`
|
||||
|
||||
**Go test command (sync + run in container):**
|
||||
```bash
|
||||
cd "C:/Users/erikn/source/repos/dereth-workspace/MosswartOverlord" && \
|
||||
tar czf - go-services | ssh erik@overlord.snakedesert.se \
|
||||
"rm -rf /tmp/tdd-verify && mkdir -p /tmp/tdd-verify && tar xzf - -C /tmp/tdd-verify" && \
|
||||
ssh erik@overlord.snakedesert.se \
|
||||
"docker run --rm -v /tmp/tdd-verify/go-services/inventory-go:/src -w /src golang:1.25-bookworm \
|
||||
sh -c 'go mod tidy >/dev/null 2>&1; go test ./... -run <PATTERN> -v 2>&1'"
|
||||
```
|
||||
|
||||
**Key facts (verified against live data + source):**
|
||||
- Raw item passed to `processItem(raw)`: `raw["IntValues"]`, `raw["DoubleValues"]` are `map[string]any` (JSON number values are `float64`); `raw["Spells"]` / `raw["ActiveSpells"]` are arrays. Helpers `bag`, `ivI`, `dvF`, `toIntList` already exist in `process.go`.
|
||||
- Int keys: `159` EquipSkill, `353` Mastery, `160` WieldReqValue, `218103842` MaxDamage, `204` ElementalDmgBonus, `171` Tinks, `179` Imbued (nonzero ⇒ imbued), `131` Material, `47` WeaponType(masterid).
|
||||
- Double keys (in DoubleValues): `167772171` Variance, `167772174` DamageBonus, `152` ElementalDamageVsMonsters, `167772169` SalvageWorkmanship.
|
||||
- `object_class`: 1 melee, 9 missile, 31 wand/caster.
|
||||
- Best-values table + spell-effect dicts: transcribe from the workspace source files named above — DO NOT invent values.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: OD calculation core (`od.go`) — TDD
|
||||
|
||||
**Files:**
|
||||
- Create: `go-services/inventory-go/od.go`
|
||||
- Create: `go-services/inventory-go/od_test.go`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests.** Create `od_test.go`. Golden values are computed by hand from the formulas + the workspace table; verify each against the source before trusting.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func approx(a, b float64) bool { return math.Abs(a-b) < 0.01 }
|
||||
|
||||
// Wand: OD = (buffedElemVsMonsters - tableMaxElemVsMonsters) * 100.
|
||||
// Live "Frost Baton": skill 34 (war), wieldreq 385, ElemVsMonsters 1.40,
|
||||
// no spirit-drinker spells. Table max for (34,*,*,385) = 1.18 → OD = +22.
|
||||
func TestOD_Wand(t *testing.T) {
|
||||
raw := map[string]any{
|
||||
"ObjectClass": 31.0,
|
||||
"IntValues": map[string]any{"159": 34.0, "160": 385.0},
|
||||
"DoubleValues": map[string]any{"152": 1.40, "167772169": 8.0},
|
||||
"Spells": []any{}, "ActiveSpells": []any{},
|
||||
}
|
||||
od, ok := computeOD(raw)
|
||||
if !ok || !approx(od, 22.0) {
|
||||
t.Fatalf("wand OD = %v ok=%v, want +22", od, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// Non-loot (no workmanship) → no OD.
|
||||
func TestOD_NonLootNull(t *testing.T) {
|
||||
raw := map[string]any{
|
||||
"ObjectClass": 31.0,
|
||||
"IntValues": map[string]any{"159": 34.0, "160": 385.0},
|
||||
"DoubleValues": map[string]any{"152": 1.40},
|
||||
"Spells": []any{}, "ActiveSpells": []any{},
|
||||
}
|
||||
if _, ok := computeOD(raw); ok {
|
||||
t.Fatal("no-workmanship item must have no OD")
|
||||
}
|
||||
}
|
||||
|
||||
// Table lookup miss (unknown tier) → no OD.
|
||||
func TestOD_UnknownTierNull(t *testing.T) {
|
||||
raw := map[string]any{
|
||||
"ObjectClass": 31.0,
|
||||
"IntValues": map[string]any{"159": 34.0, "160": 999.0},
|
||||
"DoubleValues": map[string]any{"152": 1.40, "167772169": 8.0},
|
||||
"Spells": []any{}, "ActiveSpells": []any{},
|
||||
}
|
||||
if _, ok := computeOD(raw); ok {
|
||||
t.Fatal("unknown wieldreq tier must have no OD")
|
||||
}
|
||||
}
|
||||
|
||||
// Melee: OD = buffedMaxDmg - varianceTinks - tableMaxDmg,
|
||||
// varianceTinks = round(log(tableMaxVar/variance)/log(0.8), 2).
|
||||
// Heavy (44) sword (2) non-MS, wieldreq 430: tableMaxDmg=71, tableMaxVar=0.47.
|
||||
// A perfect retail-max item: maxDmg 71, variance 0.47 → varianceTinks=0 → OD 0.
|
||||
func TestOD_MeleeRetailMaxIsZero(t *testing.T) {
|
||||
raw := map[string]any{
|
||||
"ObjectClass": 1.0,
|
||||
"IntValues": map[string]any{"159": 44.0, "353": 2.0, "160": 430.0, "218103842": 71.0},
|
||||
"DoubleValues": map[string]any{"167772171": 0.47, "167772169": 10.0},
|
||||
"Spells": []any{}, "ActiveSpells": []any{},
|
||||
}
|
||||
od, ok := computeOD(raw)
|
||||
if !ok || !approx(od, 0.0) {
|
||||
t.Fatalf("melee retail-max OD = %v ok=%v, want 0", od, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// Melee with tighter variance (0.376 = one granite tink below 0.47):
|
||||
// varianceTinks = round(log(0.47/0.376)/log(0.8), 2) = round(1.00,2)=1 → OD = 71-1-71 = -1.
|
||||
func TestOD_MeleeOneTinkVariance(t *testing.T) {
|
||||
raw := map[string]any{
|
||||
"ObjectClass": 1.0,
|
||||
"IntValues": map[string]any{"159": 44.0, "353": 2.0, "160": 430.0, "218103842": 71.0},
|
||||
"DoubleValues": map[string]any{"167772171": 0.376, "167772169": 10.0},
|
||||
"Spells": []any{}, "ActiveSpells": []any{},
|
||||
}
|
||||
od, ok := computeOD(raw)
|
||||
if !ok || !approx(od, -1.0) {
|
||||
t.Fatalf("melee OD = %v ok=%v, want -1", od, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// Melee buffed: innate Legendary Blood Thirst (id 6089, +10 MaxDamage).
|
||||
// maxDmg 61 + 10 = 71, variance 0.47 → OD 0 (vs 71 table).
|
||||
func TestOD_MeleeBuffedByBloodThirst(t *testing.T) {
|
||||
raw := map[string]any{
|
||||
"ObjectClass": 1.0,
|
||||
"IntValues": map[string]any{"159": 44.0, "353": 2.0, "160": 430.0, "218103842": 61.0},
|
||||
"DoubleValues": map[string]any{"167772171": 0.47, "167772169": 10.0},
|
||||
"Spells": []any{6089.0}, "ActiveSpells": []any{},
|
||||
}
|
||||
od, ok := computeOD(raw)
|
||||
if !ok || !approx(od, 0.0) {
|
||||
t.Fatalf("melee buffed OD = %v ok=%v, want 0", od, ok)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests, verify they fail** with `undefined: computeOD` (pattern `TestOD`).
|
||||
|
||||
- [ ] **Step 3: Implement `od.go`.** Structure:
|
||||
- A `bestValue` struct `{maxDmg, maxVar, maxDmgMod, maxElemBonus, maxElemVsMon float64}` and a lookup `map[bestKey]bestValue` keyed by `bestKey{skill, mastery, multiStrike, wieldReq int}`, transcribed from `UtilityBelt/.../WeaponMods.cs` — EVERY row (heavy/light/finesse/two-handed/missile/wand regions). Column order in the source `Rows.Add` is `(Skill, Mastery, MultiStrike, WieldReq, MaxDmg, MaxVar, MaxDmgMod, MaxElementalDmgBonus, MaxElementalDmgVsMonsters)`; rows that omit trailing columns default them to 0. Melee rows use `(skill,mastery,ms,wieldreq,maxdmg,maxvar)`; missile rows use `...,maxDmgMod(col7)=0? no` — READ the source: missile rows are `(47, mastery, 0, wieldreq, 0,0, MaxDmgMod-col? )`. Actually missile rows fill `MaxElementalDmgBonus` (col 8) — e.g. `table.Rows.Add(47, 8, 0, 0, 0, 0, 110, 0)` means MaxDmg=0,MaxVar=0,MaxDmgMod=110,MaxElemBonus=0. Wand rows fill col 9 (MaxElementalDmgVsMonsters), e.g. `(34,0,0,290,0,0,0,0,1.03)`. Map each `Rows.Add(...)` positionally onto the struct fields; verify with a couple of spot values in a test comment.
|
||||
- Spell-effect maps transcribed from `UtilityBelt/.../Dictionaries.cs`:
|
||||
- `maxDamageSpellBonus map[int]int`: 1616:20, 2096:22, 5183:24, 4395:24, 2598:2, 2586:4, 4661:7, 6089:10, 3688:300.
|
||||
- `elemVsMonSpellBonus map[int]float64`: 3258:.06, 3259:.07, 5182:.08, 4414:.08, 3251:.01, 3250:.03, 4670:.05, 6098:.07.
|
||||
- `buffedMaxDamage(raw, iv)`: `ivI(iv,"218103842",0)` + Σ bonus over innate `raw["Spells"]` − Σ bonus over `raw["ActiveSpells"]`, using `maxDamageSpellBonus`.
|
||||
- `buffedElemVsMon(raw, dv)`: `dvF(dv,"152",0)` + Σ over Spells − Σ over ActiveSpells using `elemVsMonSpellBonus`.
|
||||
- MultiStrike detection: `iv[47] ∈ {160,166,486}` OR (`iv[47]==4` AND `iv[353]==11`). (If key 47 absent, ms=0.)
|
||||
- `computeOD(raw) (float64, bool)`:
|
||||
- `oc := rawI(raw,"ObjectClass",0)`; only 1/9/31 proceed, else `(0,false)`.
|
||||
- Gate: `dvF(dv,"167772169",0) > 0` else `(0,false)`.
|
||||
- Look up `bestValue` by `{skill=ivI(iv,"159",0), mastery=ivI(iv,"353",0), multiStrike, wieldReq=ivI(iv,"160",0)}`; miss → `(0,false)`.
|
||||
- Melee (oc==1): `variance := dvF(dv,"167772171",0)`; if variance<=0 → `(0,false)`; `varianceTinks := round(math.Log(bv.maxVar/variance)/math.Log(0.8), 2)`; `od := float64(buffedMaxDamage) - varianceTinks - bv.maxDmg`.
|
||||
- Missile (oc==9): `arrowMax` by mastery 8→40,9→53,10→42 (else 0); `tinks := ivI(iv,"171",0)`; `remaining := 10 - tinks; if tinks==0 { remaining = 9 } else if imbued==0 { remaining-- }; if remaining<0 {remaining=0}` where `imbued := ivI(iv,"179",0)`; `dmgMod := dvF(dv,"167772174",0)*100 - 100`; `bDmg := float64(buffedMaxDamage); if bDmg<=10 { bDmg+=24 }`; `bElem := float64(ivI(iv,"204",0))` (no elem spell buff needed for missile per UB); `maxMod := (bv.maxDmgMod + 136)/100`; `od := (1 + (dmgMod + 4*float64(remaining))/100) * (bElem + bDmg + arrowMax) / maxMod - (bv.maxElemBonus + 24 + arrowMax)`.
|
||||
- Wand (oc==31): `od := (buffedElemVsMon - bv.maxElemVsMon) * 100`.
|
||||
- Return `round(od,2), true`. Provide a local `round2(x float64) float64` = `math.Round(x*100)/100`.
|
||||
|
||||
- [ ] **Step 4: Run tests, verify PASS** (pattern `TestOD`), then whole package (`-run .`) green.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add go-services/inventory-go/od.go go-services/inventory-go/od_test.go
|
||||
git commit -m "feat(inventory-go): Mag-Tools OD weapon rating calculation (od.go)"
|
||||
```
|
||||
(append `Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>`)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Wire OD into ingest + schema
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-services/inventory-go/process.go` (in `processItem`, after the `items` map is built ~line 137, before combat)
|
||||
- Modify: `go-services/inventory-go/schema.go` (items table DDL)
|
||||
|
||||
- [ ] **Step 1: Set od_rating in processItem.** After the `items := map[string]any{...}` literal closes, add:
|
||||
```go
|
||||
if od, ok := computeOD(raw); ok {
|
||||
items["od_rating"] = od
|
||||
}
|
||||
```
|
||||
(When not ok, the key is absent → buildInsert omits it → column stays NULL. Correct.)
|
||||
|
||||
- [ ] **Step 2: Add the column to schema.go.** Find the `CREATE TABLE ... items (` DDL and add `od_rating DOUBLE PRECISION,` alongside the other nullable numeric columns. (Fresh installs only; prod uses SKIP_SCHEMA_INIT and gets the manual ALTER in Task 5.)
|
||||
|
||||
- [ ] **Step 3: Build/test** — full package green (`-run .`). No new test needed here (Task 1 covers the math; ingest wiring is exercised in Task 5 live).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add go-services/inventory-go/process.go go-services/inventory-go/schema.go
|
||||
git commit -m "feat(inventory-go): store od_rating at ingest + schema column"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Backfill endpoint
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-services/inventory-go/ingest.go` (add handler) and `go-services/inventory-go/main.go` (register route)
|
||||
|
||||
- [ ] **Step 1: Add handler in ingest.go:**
|
||||
```go
|
||||
// POST /admin/backfill-od — recompute od_rating for all weapon rows from stored
|
||||
// raw JSON. Internal-only (service is bound to loopback/compose). Idempotent.
|
||||
func (s *Server) handleBackfillOD(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT rd.item_id, rd.original_json FROM item_raw_data rd
|
||||
JOIN items i ON i.id = rd.item_id
|
||||
WHERE i.object_class IN (1,9,31)`)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
type upd struct {
|
||||
id int
|
||||
od float64
|
||||
}
|
||||
var updates []upd
|
||||
scanned := 0
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var oj map[string]any
|
||||
if err := rows.Scan(&id, &oj); err != nil {
|
||||
rows.Close()
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
scanned++
|
||||
if od, ok := computeOD(oj); ok {
|
||||
updates = append(updates, upd{id, od})
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
updated := 0
|
||||
for _, u := range updates {
|
||||
if _, err := s.pool.Exec(ctx, "UPDATE items SET od_rating=$1 WHERE id=$2", u.od, u.id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error(), "updated": updated})
|
||||
return
|
||||
}
|
||||
updated++
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"scanned": scanned, "updated": updated})
|
||||
}
|
||||
```
|
||||
Note: pgx scans a `jsonb` column into `map[string]any` directly; `computeOD` accepts exactly that shape (numbers as `float64`), same as ingest.
|
||||
|
||||
- [ ] **Step 2: Register in main.go** next to the other `mux.HandleFunc("POST /...")` lines:
|
||||
```go
|
||||
mux.HandleFunc("POST /admin/backfill-od", srv.handleBackfillOD)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build/test** green (`-run .`).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add go-services/inventory-go/ingest.go go-services/inventory-go/main.go
|
||||
git commit -m "feat(inventory-go): POST /admin/backfill-od recompute endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Search filter + sort + column
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-services/inventory-go/search.go`
|
||||
|
||||
- [ ] **Step 1: Expose od_rating in the CTE.** In `cteSelect`, add `i.od_rating,` to the selected columns (near `i.value, i.burden`). It flows into the row maps and out to JSON automatically (queryRowsAsMaps).
|
||||
|
||||
- [ ] **Step 2: Add the filters.** `geFilters` (search.go ~line 209) uses `strconv.ParseFloat` and `leFilters` (~line 230) uses int-only `qInt`. OD can be negative and fractional, so:
|
||||
- Add `min_od` to the `geFilters` slice: `{"min_od", "od_rating"}` (float path — correct).
|
||||
- Do NOT add `max_od` to `leFilters` (int-only would truncate negatives/decimals). Instead add a float block right after the existing `min_attack_bonus` block (~line 239), mirroring it:
|
||||
```go
|
||||
if v := q.Get("max_od"); v != "" {
|
||||
if n, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
conds = append(conds, "od_rating <= "+ab.add(n))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the sort key.** In `sortMapping`, add `"od": "od_rating",`.
|
||||
|
||||
- [ ] **Step 4: Build/test** green (`-run .`).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add go-services/inventory-go/search.go
|
||||
git commit -m "feat(inventory-go): min_od/max_od search filters + od sort"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Deploy backend + backfill + live verify
|
||||
|
||||
- [ ] **Step 1: Manual ALTER on the live inventory DB** (prod skips schema init):
|
||||
```bash
|
||||
ssh erik@overlord.snakedesert.se "docker exec inventory-db psql -U inventory_user -d inventory_db -c 'ALTER TABLE items ADD COLUMN IF NOT EXISTS od_rating double precision;'"
|
||||
```
|
||||
- [ ] **Step 2: Sync + build + recreate inventory-go** (test-gated build):
|
||||
```bash
|
||||
cd "C:/Users/erikn/source/repos/dereth-workspace/MosswartOverlord" && \
|
||||
tar czf - go-services | ssh erik@overlord.snakedesert.se "tar xzf - -C /home/erik/MosswartOverlord/"
|
||||
ssh erik@overlord.snakedesert.se 'cd /home/erik/MosswartOverlord && \
|
||||
export BUILD_VERSION="$(date -u +%Y.%-m.%-d.%H%M)-$(git rev-parse --short HEAD)" && \
|
||||
docker compose -f docker-compose.yml -f go-services/docker-compose.go.yml build --build-arg BUILD_VERSION=$BUILD_VERSION inventory-go && \
|
||||
docker compose -f docker-compose.yml -f go-services/docker-compose.go.yml -f go-services/docker-compose.cutover.yml up -d --no-deps inventory-go'
|
||||
```
|
||||
- [ ] **Step 3: Run the backfill:**
|
||||
```bash
|
||||
ssh erik@overlord.snakedesert.se "curl -s -X POST http://127.0.0.1:8772/admin/backfill-od"
|
||||
```
|
||||
Expected: `{"scanned":<n>,"updated":<m>}` with m > 0.
|
||||
- [ ] **Step 4: Verify the known Frost Baton (+22) and a sort:**
|
||||
```bash
|
||||
ssh erik@overlord.snakedesert.se "curl -s 'http://127.0.0.1:8772/search/items?include_all_characters=true&weapon_only=true&min_od=20&sort_by=od&sort_dir=desc&limit=10'" \
|
||||
| python3 -c "import json,sys; d=json.load(sys.stdin); print('total:',d['total_count']); [print(i['name'],'|',i.get('object_class_name'),'|',i.get('od_rating')) for i in d['items']]"
|
||||
```
|
||||
Expected: results include the Frost Baton with od_rating ≈ 22; all listed od_rating ≥ 20, descending.
|
||||
- [ ] **Step 5: Sanity — melee spot check** (pick a heavy sword the user knows in-game; compare od_rating sign to its Mag-Tools OD). Non-weapon search unaffected:
|
||||
```bash
|
||||
ssh erik@overlord.snakedesert.se "curl -s 'http://127.0.0.1:8772/search/items?include_all_characters=true&jewelry_only=true&limit=1' | python3 -c \"import json,sys;d=json.load(sys.stdin);print('jewelry total',d['total_count'])\""
|
||||
```
|
||||
- [ ] **Step 6: Commit** (no code change; this task is operational). If a git commit is desired for the version bump it happens via the frontend push later.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Frontend — filter + column + detail row
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/components/inventory/constants.ts`
|
||||
- Modify: `frontend/src/components/inventory/types.ts`
|
||||
- Modify: `frontend/src/components/inventory/DetailPanel.tsx`
|
||||
|
||||
- [ ] **Step 1: types.ts** — add to `InvItem`:
|
||||
```ts
|
||||
od_rating?: number | null;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: constants.ts** — add an OD rating filter and column. In `RATING_DEFS`, add as the first entry (so it appears with the common ratings):
|
||||
```ts
|
||||
{ param: 'min_od', label: 'Weapon OD', common: true },
|
||||
```
|
||||
In `COLUMNS`, add after `max_damage`:
|
||||
```ts
|
||||
{ key: 'od_rating', label: 'OD', sortKey: 'od', defaultVisible: false },
|
||||
```
|
||||
|
||||
- [ ] **Step 3: ResultsTable cell rendering.** In `ResultsTable.tsx` `cell()`, add a case so OD shows signed with 2 decimals and `—` for null:
|
||||
```ts
|
||||
case 'od_rating': {
|
||||
const v = item.od_rating;
|
||||
if (v == null) return '—';
|
||||
return v > 0 ? `+${v.toFixed(2)}` : v.toFixed(2);
|
||||
}
|
||||
```
|
||||
(Place it alongside the other explicit cases, before `default`.)
|
||||
|
||||
- [ ] **Step 4: DetailPanel.tsx** — add an OD row (only when present) after Max damage:
|
||||
```tsx
|
||||
{item.od_rating != null && <Row k="Weapon OD" v={item.od_rating > 0 ? `+${item.od_rating.toFixed(2)}` : item.od_rating.toFixed(2)} />}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Build green:** `cd "C:/Users/erikn/source/repos/dereth-workspace/MosswartOverlord/frontend" && npm run build`; delete `static/_build` after.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
```bash
|
||||
git add frontend/src/components/inventory/
|
||||
git commit -m "feat(frontend): weapon OD rating filter, column, and detail row"
|
||||
```
|
||||
|
||||
Note: `min_od` chip label comes free via `RATING_DEFS` in `ActiveChips` ("Weapon OD ≥ N"); the rating sidebar input is auto-generated from `RATING_DEFS`. No changes needed in FilterSidebar/ActiveChips.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Deploy frontend + final verify
|
||||
|
||||
- [ ] **Step 1:** `bash deploy-frontend.sh` from repo root.
|
||||
- [ ] **Step 2:** `git add static/ frontend/ && git commit -m "build(frontend): deploy weapon OD rating" && git push origin master`
|
||||
- [ ] **Step 3:** `ssh erik@overlord.snakedesert.se "cd /home/erik/MosswartOverlord && git pull --ff-only origin master"`
|
||||
- [ ] **Step 4: User verification** (login-gated, ask the user): open `/?view=inventory`, select Weapons, enter Weapon OD ≥ 10, confirm high-OD weapons appear; add the OD column via the picker; sort by OD; click a weapon and confirm the OD row in the detail panel; confirm a known good weapon's OD roughly matches its in-game Mag-Tools OD.
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes
|
||||
- Spec coverage: melee/missile/wand formulas (Task 1), ingest+schema (2), backfill (3), filters+sort (4), deploy+backfill (5), UI (6), deploy (7). All spec bullets covered.
|
||||
- The best-values table + spell dicts are transcribed from named in-repo source files, not invented — reviewer must diff against those files.
|
||||
- `min_od` is a float filter (negative values valid) — placed in the ParseFloat `geFilters` path, not the int `leFilters` path. `max_od` likewise should accept floats: if `leFilters` is int-only (`qInt`), add `max_od` via the float `min_attack_bonus`-style manual block instead. Implementer: check `leFilters`'s parse path and use a float path for `max_od`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue