Add level-aware builder controls

This commit is contained in:
JB
2026-07-25 20:52:08 -04:00
parent 7f9a75021d
commit 0425fede8e
13 changed files with 654 additions and 26 deletions
+42 -6
View File
@@ -62,6 +62,11 @@ from the orange links reserved for SHD navigation and legal resources. Support
and legal links form a compact horizontal rail that may wrap at intermediate
widths and becomes a two-column touch-friendly grid on narrow mobile screens.
The footer repeats the complete SHD brand lockup used by the global header:
double-ring SHD mark, white `SHD`, orange `.build`, and the restrained
`LOADOUT NETWORK` descriptor. Do not substitute a plain text wordmark or color
the complete product name orange.
Contact notifications use the mandatory ISAC visual identity because they are
operational inbound messages. The notification carries a concise preview and a
specific action to an authorization-checked full record. Full messages never
@@ -75,6 +80,38 @@ contact columns stack, destination segments become full-width 44-pixel rows,
attestations remain readable, and reply/recovery actions occupy the available
width.
### Builder range controls
Configurable numeric rolls use the shared range-value control: a descriptive
label and current minimum/maximum above a synchronized range slider and direct
number input. The slider supports fast exploration while the number field
supports exact entry; both clamp to the currently calculated bounds. Focus uses
the SHD action border, active slider feedback uses the standard pressed
brightness treatment, and number fields retain the global select-all-on-focus
behavior.
When an equipment level changes, preserve the configured roll's relative
position within its previous range instead of resetting it arbitrarily. Mark
unverified portions of a level curve with both `ESTIMATED` text and the
High-end yellow caution treatment. Controls stack the slider and direct input
at narrow mobile widths and must never create horizontal page overflow.
Equipment level uses this same synchronized control rather than a standalone
number field. Changing level remaps base and rolled values to the equivalent
relative position in the newly calculated range.
Gear and weapons always expose exactly two required rolled-Attribute positions.
Render both positions as persistent numbered rows; do not offer add or remove
actions. Already selected Attributes are disabled in the other position.
Use compact segmented radio controls for the small finite weapon-tier set.
Use the shared builder radio grid for visually identified Brand Sets and weapon
talents: transparent icons lead each square engineered option, text remains
visible, and the selected state uses the orange action edge plus an explicit
radio state. Talent grids become searchable when their available pool is large.
On narrow viewports option grids reduce to two columns and then one column so
labels and 44-pixel-or-larger targets remain usable.
## Tokens
The canonical tokens live in `src/app.css` under `:root`.
@@ -987,12 +1024,11 @@ the live totals immediately. Trigger-dependent talents remain explicitly
effects enter combined totals. This textual distinction must remain clear
without relying on color.
Gear level is builder-instance state, never part of an ISAC Gear definition.
Each equipped piece stores its own level from 1–50 in the local or persisted
build. Until an authoritative level-scaling curve is represented in game data,
the workbench must state that level scaling is neutral rather than silently
inventing a formula; base Health and Armor remain constrained by their
canonical ranges.
Gear and weapon level are builder-instance state, never part of an ISAC item
definition. Each equipped item stores its own level from 1–50 in the local or
persisted build. Range endpoints are calculated from the documented level
curve and stored reference level; provisional portions of that curve must be
identified in text as estimated.
### Registry relationship icon rails
+220
View File
@@ -0,0 +1,220 @@
# Weapon and gear stat level scaling
This document records the empirically observed rules for adjusting weapon and
gear stat ranges by item level. The rules apply independently to both endpoints
of a configured range.
These formulas are based on in-game observations rather than published game
mechanics. Preserve source observations when adding new samples and revise the
constants if a larger data set disproves them.
## Numeric representation and display
Stat ranges must retain their unrounded decimal values. The integer shown by the
game is consistent with truncation:
```text
displayedValue = floor(rawValue)
```
Do not use `ceil`, and do not repeatedly level an already displayed integer.
Both approaches accumulate errors. A level-1 value displayed as `0` can still
have a positive fractional raw value that becomes visible at a higher level.
For example, a standard gear item displayed `0 ENG` at level 1 and `2 ENG` at
level 42. With a level-42 multiplier of `3.01`, a raw base near `0.7` explains
both values:
```text
floor(0.7) = 0
floor(0.7 × 3.01) = 2
```
When only displayed integers are available, a prediction can be off by one.
That is expected information loss, not evidence for rounding upward.
## Shared progression model
The stable portion of both progressions is linear, not compounded:
```text
rawStat(level) = hiddenBase × multiplier(level)
```
The multiplier increases by `0.05` per level. Equivalently, each level adds a
fixed `5%` of the hidden base:
```text
rawStat(level + 1) - rawStat(level) = hiddenBase × 0.05
```
Never calculate a new level as `previousDisplayedValue × 1.05`; that describes
compound growth and does not match the observations.
For a range `[minimum, maximum]`, calculate each endpoint separately:
```text
scaledMinimum = hiddenMinimum × multiplier(level)
scaledMaximum = hiddenMaximum × multiplier(level)
```
Only truncate when producing a game-matching display value.
## Gear
The observed stable gear multiplier is:
```text
gearMultiplier(level) = 0.91 + 0.05 × level
```
This rule is validated for levels 10 through 50 across Standard, Specialized,
and High-end samples. Quality changes the hidden stat ranges; the observed
late-level multiplier does not change with quality.
Given hidden level-independent endpoints:
```text
rawMinimum(level) = hiddenMinimum × gearMultiplier(level)
rawMaximum(level) = hiddenMaximum × gearMultiplier(level)
```
Given an unrounded range recorded at a known reference level:
```text
targetRaw =
referenceRaw
× gearMultiplier(targetLevel)
/ gearMultiplier(referenceLevel)
```
### Gear example
At level 42:
```text
gearMultiplier(42) = 0.91 + 0.05 × 42 = 3.01
```
A Standard item with approximate hidden bases of `615.0 HP`, `107.4 AR`,
`10.0 FP`, `11.4 TGH`, and `0.7 ENG` produces:
```text
HP = floor(615.0 × 3.01) = 1851
AR = floor(107.4 × 3.01) = 323
FP = floor(10.0 × 3.01) = 30
TGH = floor(11.4 × 3.01) = 34
ENG = floor(0.7 × 3.01) = 2
```
### Early gear levels
Levels 1 through 9 show a quality-dependent transition and do not follow the
stable multiplier directly. Do not extrapolate that transition from a single
level-1-to-level-2 change. Until the early curves are sampled more completely,
store authoritative ranges for those levels or mark calculated results as
estimates.
## Weapons
Weapon observations from levels 40 through 50 show the same fixed `0.05`
multiplier increase per level, but a different offset:
```text
weaponMultiplier(level) ≈ 1.14 + 0.05 × level
```
This gives approximately:
```text
weaponMultiplier(40) = 3.14
weaponMultiplier(50) = 3.64
```
and predicts a level-40-to-level-50 ratio of:
```text
3.64 / 3.14 ≈ 1.15924
```
Two observed weapons produced ratios of approximately `1.15960` and `1.15936`.
The small difference is consistent with hidden fractional bases and truncated
display values.
Given an unrounded weapon range at a known reference level:
```text
targetRaw =
referenceRaw
× weaponMultiplier(targetLevel)
/ weaponMultiplier(referenceLevel)
```
The `1.14` offset is provisional and is currently validated only for the
level-40-to-level-50 interval. Preserve enough decimal precision to revise it
without migrating rounded data.
### Weapon example
A weapon changed from `753 DMG` at level 40 to `873 DMG` at level 50:
```text
753 × (3.64 / 3.14) ≈ 872.95
floor(872.95) = 872
```
The displayed-source calculation misses by one because `753` is already
truncated. Applying the ratio to the unrounded source value produces the
observed `873`. This is why calculations must use stored decimal endpoints.
The observed changes over those ten levels were linear:
```text
DMG: 753 → 873 (12.0 raw points per level)
FP: 44 → 51 (about 0.7 per level)
TGH: 15 → 18 (about 0.3 per level)
ENG: 45 → 52 (about 0.7 per level)
```
### Early weapon levels
Do not apply the provisional late-level weapon offset to level 1. An Exotic
sample's displayed level-1 values and level-50 values fit a `2.91` endpoint
ratio for most stats, which is incompatible with simply extending the
level-40-to-level-50 offset backward. Weapons therefore also appear to have an
early transition or another quality-dependent input.
Until more intermediate samples are available:
- use the weapon formula only within the validated level 40–50 interval;
- do not derive a hidden base directly from a displayed level-1 value;
- prefer authoritative low-level ranges where exact results are required.
## Implementation requirements
1. Store range endpoints as decimals and retain their reference level.
2. Scale minimum and maximum independently using raw values.
3. Calculate directly from the reference value; never chain per-level rounded
results.
4. Truncate only at the presentation boundary when matching the game display.
5. Clamp supported levels to `1–50`.
6. Treat gear levels below 10 and weapon levels below 40 as provisional until
their curves are verified.
7. Add observed intermediate values as regression fixtures before changing a
multiplier or rounding rule.
## Current regression observations
Useful known outputs include:
- Specialized gear at level 27: `1610 HP`, `280 AR`, `29 FP`, `23 TGH`,
`10 ENG`.
- Standard gear at level 42: `1851 HP`, `323 AR`, `30 FP`, `34 TGH`, `2 ENG`.
- High-end gear: level 40 `2682 HP`, `464 AR`, `47 FP`, `16 TGH`, `36 ENG`;
level 41 `2728 HP`, `472 AR`, `48 FP`, `16 TGH`, `37 ENG`; level 50
`3143 HP`, `544 AR`, `55 FP`, `19 TGH`, `43 ENG`.
- Weapon sample: level 40 `1886 DMG`, `120 FP`, `123 TGH`, `31 ENG`; level 50
`2187 DMG`, `139 FP`, `142 TGH`, `36 ENG`.
- Weapon sample: level 40 `753 DMG`, `44 FP`, `15 TGH`, `45 ENG`; level 50
`873 DMG`, `51 FP`, `18 TGH`, `52 ENG`.
+36
View File
@@ -20,6 +20,12 @@ This project is intentionally being built in vertical slices.
Authorization must be checked on the server for every mutation. Hiding an admin link is not authorization.
Weapon and gear range calculations must preserve unrounded decimal endpoints
and follow the empirically validated progression rules in
[`STAT_LEVEL_SCALING.md`](STAT_LEVEL_SCALING.md). In particular, leveling is
linear rather than compounded and game-matching integer values are truncated
only at the presentation boundary.
Local credentials are stored as Argon2id PHC strings with a unique random salt
per password. Current work factors follow OWASP's 19 MiB / 2 iteration /
single-lane minimum profile and can be increased over time; successful login
@@ -84,6 +90,36 @@ This guard prevents accidental merging or overwriting of contributor data.
Stop the application before backup or restore so the embedded database file is
not locked by another process.
## Low-downtime development migrations
The development database is a shared working data source: contributors may be
entering ISAC records while application development continues. Database changes
therefore use a prepared migration window rather than taking the server down at
the beginning of implementation.
Before the window, finish the code and idempotent migration, batch related
schema changes, identify verification queries, and run `npm run check` plus
`npm run build` while the server remains available. Prefer additive,
backward-compatible tables and safely defaulted or nullable columns. Coordinate
with known contributors so the stop does not interrupt an unsaved submission.
The offline window is deliberately narrow:
1. Inspect running processes and stop only this project's development server.
2. Run and verify `npm run db:backup`.
3. Run the already-prepared migration.
4. Verify expected schema state, record counts, and critical entities.
5. Restart the development server immediately.
6. Confirm a basic page or health check, then perform extended browser and
regression testing with the service online.
Code editing, builds, formatting, documentation review, and noncritical UI
testing do not belong inside this window. Destructive, data-rewriting, or
long-running migrations require a separately communicated maintenance window
and a verified recovery plan. Every migration handoff reports the backup path,
migration outcome, successful server restart, critical checks, and the
approximate offline interval when available.
# ISAC audit and compensating reversals
Every successful ISAC mutation appends an event to `audit_log` through the