Add level-aware builder controls
This commit is contained in:
+42
-6
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user