Compare commits

...

5 commits

Author SHA1 Message Date
e74dd17d0e chore: remove private deployment details 2026-08-21 15:47:57 +03:00
9b16359cb2 docs: warn that project is AI-assisted 2026-08-21 15:44:12 +03:00
ad2d9b229f feat: model cartridge set bonuses 2026-08-21 15:40:38 +03:00
bddc0817ad feat: real artwork, readable stats, and a build you can read at a glance
Six things, all found by using the deployed page.

**The icons were never images.** `everness.info/...` answers 200 with
`content-type: image/webp` for paths that do not exist, returning the SPA's
HTML shell, so all 72 downloaded "icons" were HTML documents named .webp and
every character silently rendered as initials. The assets live on
`api.everness.info/data/assets` instead, and the fetcher now verifies RIFF/WEBP
magic bytes and fails the build rather than warning. 95 files: 23 avatars at
256px, 23 portraits, 49 Arc icons.

**Stats read as the game writes them.** `prune-gamedata` harvests every
`{ id_stats, name, bShowPercent }` pair out of the mirror, so "CRIT Rate" is
the game's own wording rather than someone's typing, and values render as 12.5%
instead of 0.125. Ids the mirror never names are filled in by
`domain/statlabels.ts`, each with its reason and marked derived in the UI.
Two of everness's own percent flags are overridden: it marks `HealUp` and
`DamageUpNatureBase` flat while shipping them as fractions in exactly the range
of the bonuses it does mark as percentages, which printed "Healing Bonus 0.1".

**The build is legible without hovering.** Each piece carries its legend number
and colour, and the list beside the board states shape, type, level and every
stat outright; the cartridge sits in its own box below, since it occupies no
cells. Hover-only tooltips meant the information existed but could not be
scanned or compared. Shapes are drawn from the same cell lists the solver packs
with, so an icon cannot drift from its geometry.

**Owner groups have numbers, not hashes.** "group 0000000088" told the player
nothing; groups are now numbered stably and an unnamed one links to where it
gets named.

Also: import/export moved to its own Data tab with a library summary and the
to-do list, and the layout is denser throughout.

152 tests, tsc clean, both build targets clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:35:25 +03:00
71071cff94 chore: one-command static deploy
The homelab has no rsync, so the tree goes over as a tar stream, and the
destination is wiped first - a stale content-hashed chunk left behind would be
served forever, since /assets/* is cached as immutable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:43:28 +03:00
203 changed files with 2554 additions and 459 deletions

View file

@ -1,58 +1,63 @@
# nte-optimizer # nte-optimizer
Gear optimizer for *Neverness to Everness*. Static site, no server, no account. > [!CAUTION]
> **This repository is vibecoded.** Its implementation was written with substantial AI assistance and may contain plausible-looking mistakes, unsafe assumptions, or unreviewed edge cases. Treat it with care: inspect the code, verify outputs, and run the tests before relying on it.
It imports the JSON that [`nte-exporter`](../nte-exporter) writes during a normal Gear optimizer for *Neverness to Everness*. Static site, no server, no account, and no runtime connection to the game.
capture run, and recommends what to equip on whom — drawn onto each character's
console grid.
## Running it It imports a gear snapshot produced by `nte-exporter`, scores the available cartridges and modules, and draws recommended builds directly onto each character's console grid.
## Features
- Single-character optimization with Prydwen targets or custom targets and weights.
- Cartridge set and 2-piece/4-piece bonus selection.
- Arc stats, refinements, conditional-effect controls, and explicit notices for effects the model cannot score.
- Team optimization against one shared item pool without assigning an item twice.
- Console-grid layouts with module placement and enough item detail to find each piece in game.
- Local equipment tracking, Equip/Undo, and import refreshes from game truth.
- Predicted-versus-measured character sheet comparison.
- Hosted build plus a single-file offline build.
All account data stays in the browser. Nothing is uploaded by the app.
## Running locally
```sh ```sh
npm ci npm ci
npm run dev # development npm run dev
npm run build # dist/index.html — the normal build
npm run build:single # dist/nte.html — one file, works over file://
npm test
``` ```
`dist/` is **committed on purpose**. In three years, when `npm ci` fails on some Useful checks and builds:
transitive native module, `python3 -m http.server -d dist` will still work. The
source build is the convenience; the committed output is the artifact.
## Layout ```sh
npm test
npm run check:guides
npm run build # hosted build in dist/
npm run build:single # standalone dist/nte.html
```
| path | what lives there | The standalone build works over `file://`. Browser restrictions disable workers, IndexedDB, and bundled artwork there; the app falls back to inline solving and localStorage or memory.
## Model boundaries
This is a stat-target optimizer, not a damage simulator. It has no rotation or ability-multiplier model.
- Targets are floors. Published substat rankings determine their relative weights.
- Timed bonuses are reported but not scored without defensible uptime data.
- Shield strength, enemy resistance, ally-only buffs, and other unsupported quantities are marked unmodellable instead of mapped onto a different stat.
- Level-80 base-stat scaling remains unmeasured, so dependent sheet values are shown as unavailable.
- “Proven” means optimal for one packing and cartridge, not globally optimal across every internal beam-search choice.
## Project layout
| Path | Contents |
|---|---| |---|---|
| `data-src/` | raw everness mirrors, Arc classifications, guide data. Committed, not shipped. | | `data-src/` | Source classifications, cartridge bonuses, and guide data. |
| `tools/` | build-time scripts. Output goes to `src/generated/`, also committed. | | `tools/` | Data validation, generation, benchmarking, and build scripts. |
| `src/domain/` | **pure.** No DOM, no IndexedDB, no imports from `ui/`, `db/` or `state/`. | | `src/domain/` | Pure stat, board, cartridge, Arc, and scoring logic. |
| `src/solver/` | same rule. Chunked generators, so they run in a worker or inline unchanged. | | `src/solver/` | Worker-safe single-character and team solvers. |
| `src/db/` | schema, migrations, import, persistence adapters. | | `src/db/` | Import validation and local persistence. |
| `src/ui/` | React. Four tabs and a hash router. | | `src/ui/` | React interface. |
| `tests/` | Domain, solver, persistence, and integration checks. |
The `domain/` and `solver/` rule is what makes them worker-safe, Node-testable `src/domain/` and `src/solver/` stay free of DOM, storage, and UI imports so the same logic runs in tests, inline, or in a Web Worker.
and diffable against the Python they were ported from. Keep it.
## Two things that are easy to get wrong
**Ported rules were expensive to derive and break in ways that still look
plausible.** Before touching anything in `domain/`, read
`~/Apps/nte-research/WIREFORMAT.md` and the "facts worth not re-deriving"
section of `RESUME.md`. Builds are 6, 7 or 8 modules and never always 7; the
console trait is not always CRIT DMG or Type III; a transposed board renders
correctly on a symmetric grid.
**The model has measured gaps, and the app is built to show them rather than
hide them.** Set bonus values are unknown and contribute nothing. The base-stat
multiplier has never been read at level 80. A `proven` build is optimal for its
packing and cartridge, not globally. Every one of those is surfaced in the UI on
purpose — if you find yourself replacing one with a plausible number, don't.
## Degraded modes
Opening `dist/nte.html` from the filesystem gives the page an opaque origin, so
IndexedDB, workers, `fetch` and module scripts are all unavailable. The app
handles this behind two interfaces it never sees through — `PersistenceAdapter`
(IndexedDB → localStorage → memory) and `SolverHost` (worker → inline) — and
shows a banner when storage is not durable. Artwork is not carried in that build.

File diff suppressed because it is too large Load diff

View file

@ -1,127 +1,55 @@
{ {
"format": "nte-set-bonuses", "format": "nte-set-bonuses",
"format_version": 1, "format_version": 2,
"note": "Bonus VALUES are not in any capture and not on everness: items.json carries only the cartridge boxes. Each tier stays unknown until read in game or scraped, and an unknown tier contributes nothing rather than a guessed number.", "source": "Prydwen, read 2026-08-21; page updated 2026-04-26",
"sets": { "sets": {
"Crimson: Twin Butterflies": { "Crimson: Twin Butterflies": {
"2": { "2": {"description": "Incantation DMG +10%", "mode": "always", "stats": [{"stat": "DamageUpIncantationBase", "value": 0.1}], "why": "Unconditional wearer stat."},
"unknown": true, "4": {"description": "ATK +6% per nearby team Incantation DMG trigger, up to 6 stacks; each lasts 10s.", "mode": "stacks", "max_stacks": 6, "stats": [{"stat": "AtkUp", "value": 0.06}], "why": "Wearer ATK stacks to 6 and remains active off-field."}
"stats": [] },
}, "Devil's Blood: Curse": {
"4": { "2": {"description": "Psyche DMG +10%", "mode": "always", "stats": [{"stat": "DamageUpPsycheBase", "value": 0.1}], "why": "Unconditional wearer stat."},
"unknown": true, "4": {"description": "DMG +18%, increased to 36% against units affected by Nova or Stain.", "mode": "always", "stats": [{"stat": "DamageUpGeneralBase", "value": 0.18}], "why": "The unconditional 18% is modelled; the target-conditional increase is not assumed."}
"stats": [] },
} "Diabolos": {
}, "2": {"description": "Chaos DMG +10%", "mode": "always", "stats": [{"stat": "DamageUpChaosBase", "value": 0.1}], "why": "Unconditional wearer stat."},
"Devil's Blood: Curse": { "4": {"description": "Ignores 12% Chaos RES, increased to 24% for 20s after Nova or Scorch.", "mode": "unmodellable", "stats": [], "why": "Chaos RES is not DEF; mapping this to DefIgnore would model a different quantity."}
"2": { },
"unknown": true, "Fireflies and the Forest": {
"stats": [] "2": {"description": "Anima DMG +10%", "mode": "always", "stats": [{"stat": "DamageUpNatureBase", "value": 0.1}], "why": "Anima is named Nature in the stat ids."},
}, "4": {"description": "CRIT DMG +8% per nearby team Anima DMG trigger, up to 7 stacks; each lasts 10s.", "mode": "stacks", "max_stacks": 7, "stats": [{"stat": "CritDamageBase", "value": 0.08}], "why": "Wearer CRIT DMG stacks to 7 and remains active off-field."}
"4": { },
"unknown": true, "Kingdom's Guard": {
"stats": [] "2": {"description": "DEF +15%", "mode": "always", "stats": [{"stat": "DefUp", "value": 0.15}], "why": "Unconditional wearer stat."},
} "4": {"description": "Shields +20%", "mode": "unmodellable", "stats": [], "why": "Shield strength has no stat-vector slot."}
}, },
"Diabolos": { "Lost Radiance": {
"2": { "2": {"description": "Cosmos DMG +10%", "mode": "always", "stats": [{"stat": "DamageUpCosmosBase", "value": 0.1}], "why": "Unconditional wearer stat."},
"unknown": true, "4": {"description": "Ignores 25% DEF for 20s after Ultimate.", "mode": "duration", "stats": [{"stat": "DefIgnore", "value": 0.25}], "why": "Timed post-Ultimate stat; uptime needs rotation data, so it is reported but not scored."}
"stats": [] },
}, "Quiet Manor": {
"4": { "2": {"description": "Mental DMG +10%", "mode": "always", "stats": [{"stat": "DamageUpPsychicallyBase", "value": 0.1}], "why": "User-decided circumstantial mapping: DamageUpPsychicallyBase is the only non-element damage id, appears on 19 real items, and Fadia's guide pairs Mental DMG with Quiet Manor. Not sheet-confirmed; check in game."},
"unknown": true, "4": {"description": "Mental DMG +12% per Basic Attack, up to 3 stacks; each lasts 6s.", "mode": "stacks", "max_stacks": 3, "stats": [{"stat": "DamageUpPsychicallyBase", "value": 0.12}], "why": "Uses the same user-decided Mental DMG mapping as the 2-piece tier; stacks to 3."}
"stats": [] },
} "Shadow Creed": {
}, "2": {"description": "ATK +10%", "mode": "always", "stats": [{"stat": "AtkUp", "value": 0.1}], "why": "Unconditional wearer stat."},
"Fireflies and the Forest": { "4": {"description": "ATK +25% for 20s after Skill.", "mode": "duration", "stats": [{"stat": "AtkUp", "value": 0.25}], "why": "Timed post-Skill stat; uptime needs rotation data, so it is reported but not scored."}
"2": { },
"unknown": true, "Speedy Hedgehog": {
"stats": [] "2": {"description": "Charge Efficiency +12%", "mode": "always", "stats": [{"stat": "ChargeGetEfficiencyBase", "value": 0.12}], "why": "Unconditional wearer stat."},
}, "4": {"description": "All allies' ATK +15% for 20s after wearer uses Ultimate.", "mode": "unmodellable", "stats": [], "why": "Buff applies to allies, not the wearer being optimised."}
"4": { },
"unknown": true, "Street Boxer": {
"stats": [] "2": {"description": "Lakshana DMG +10%", "mode": "always", "stats": [{"stat": "DamageUpLakshanaBase", "value": 0.1}], "why": "Unconditional wearer stat."},
} "4": {"description": "CRIT Rate +14%.", "mode": "always", "stats": [{"stat": "CritBase", "value": 0.14}], "why": "Prydwen's reaction sentence repeats 14% from 14%; treated as a source typo, not a second value."}
}, },
"Kingdom's Guard": { "Thea's Night Tavern": {
"2": { "2": {"description": "HP +10%", "mode": "always", "stats": [{"stat": "HPMaxUp", "value": 0.1}], "why": "Unconditional wearer stat."},
"unknown": true, "4": {"description": "Healing Bonus +20%", "mode": "always", "stats": [{"stat": "HealUp", "value": 0.2}], "why": "Unconditional wearer stat."}
"stats": [] },
}, "Tiny Big Adventure": {
"4": { "2": {"description": "HP +10%", "mode": "always", "stats": [{"stat": "HPMaxUp", "value": 0.1}], "why": "Unconditional wearer stat."},
"unknown": true, "4": {"description": "Max HP +4% when taking damage, up to 10 stacks; each lasts 10s; Ultimate grants 10.", "mode": "stacks", "max_stacks": 10, "stats": [{"stat": "HPMaxUp", "value": 0.04}], "why": "Wearer Max HP stacks to 10; targeting the 4-piece tier uses its stated maximum."}
"stats": [] }
}
},
"Lost Radiance": {
"2": {
"unknown": true,
"stats": []
},
"4": {
"unknown": true,
"stats": []
}
},
"Quiet Manor": {
"2": {
"unknown": true,
"stats": []
},
"4": {
"unknown": true,
"stats": []
}
},
"Shadow Creed": {
"2": {
"unknown": true,
"stats": []
},
"4": {
"unknown": true,
"stats": []
}
},
"Speedy Hedgehog": {
"2": {
"unknown": true,
"stats": []
},
"4": {
"unknown": true,
"stats": []
}
},
"Street Boxer": {
"2": {
"unknown": true,
"stats": []
},
"4": {
"unknown": true,
"stats": []
}
},
"Thea's Night Tavern": {
"2": {
"unknown": true,
"stats": []
},
"4": {
"unknown": true,
"stats": []
}
},
"Tiny Big Adventure": {
"2": {
"unknown": true,
"stats": []
},
"4": {
"unknown": true,
"stats": []
}
} }
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Some files were not shown because too many files have changed in this diff Show more