@rydr/game-sdk 8.21.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/client/PlatformClient.d.ts +65 -22
  2. package/dist/client/PlatformClient.d.ts.map +1 -1
  3. package/dist/client/PlatformClient.js +143 -13
  4. package/dist/client/PlatformClient.js.map +1 -1
  5. package/dist/host/PlatformHost.d.ts +25 -7
  6. package/dist/host/PlatformHost.d.ts.map +1 -1
  7. package/dist/host/PlatformHost.js +33 -2
  8. package/dist/host/PlatformHost.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/protocol/boards.d.ts +1 -13
  14. package/dist/protocol/boards.d.ts.map +1 -1
  15. package/dist/protocol/boards.js.map +1 -1
  16. package/dist/protocol/buttons.d.ts +1 -1
  17. package/dist/protocol/buttons.js +1 -1
  18. package/dist/protocol/guards.d.ts.map +1 -1
  19. package/dist/protocol/guards.js +2 -0
  20. package/dist/protocol/guards.js.map +1 -1
  21. package/dist/protocol/identity.d.ts +1 -1
  22. package/dist/protocol/index.d.ts +1 -0
  23. package/dist/protocol/index.d.ts.map +1 -1
  24. package/dist/protocol/index.js +1 -0
  25. package/dist/protocol/index.js.map +1 -1
  26. package/dist/protocol/messages.d.ts +30 -19
  27. package/dist/protocol/messages.d.ts.map +1 -1
  28. package/dist/protocol/stages.d.ts +102 -0
  29. package/dist/protocol/stages.d.ts.map +1 -0
  30. package/dist/protocol/stages.js +19 -0
  31. package/dist/protocol/stages.js.map +1 -0
  32. package/dist/protocol/version.d.ts +2 -2
  33. package/dist/protocol/version.d.ts.map +1 -1
  34. package/dist/protocol/version.js +46 -2
  35. package/dist/protocol/version.js.map +1 -1
  36. package/dist/three/character/kit.d.ts +1 -1
  37. package/dist/three/character/kit.js +1 -1
  38. package/dist/three/character/riders.d.ts +1 -1
  39. package/dist/three/character/riders.js +1 -1
  40. package/dist/three/character/skeleton.d.ts +1 -1
  41. package/dist/three/character/skeleton.js +1 -1
  42. package/dist/training/compile.d.ts +135 -0
  43. package/dist/training/compile.d.ts.map +1 -0
  44. package/dist/training/compile.js +367 -0
  45. package/dist/training/compile.js.map +1 -0
  46. package/dist/training/index.d.ts +6 -0
  47. package/dist/training/index.d.ts.map +1 -0
  48. package/dist/training/index.js +6 -0
  49. package/dist/training/index.js.map +1 -0
  50. package/dist/training/model.d.ts +252 -0
  51. package/dist/training/model.d.ts.map +1 -0
  52. package/dist/training/model.js +261 -0
  53. package/dist/training/model.js.map +1 -0
  54. package/dist/training/protocol.d.ts +60 -0
  55. package/dist/training/protocol.d.ts.map +1 -0
  56. package/dist/training/protocol.js +2 -0
  57. package/dist/training/protocol.js.map +1 -0
  58. package/dist/training/state.d.ts +201 -0
  59. package/dist/training/state.d.ts.map +1 -0
  60. package/dist/training/state.js +55 -0
  61. package/dist/training/state.js.map +1 -0
  62. package/dist/training/zones.d.ts +50 -0
  63. package/dist/training/zones.d.ts.map +1 -0
  64. package/dist/training/zones.js +57 -0
  65. package/dist/training/zones.js.map +1 -0
  66. package/dist/ui/README.md +2 -3
  67. package/dist/ui/option-menu.d.ts +1 -1
  68. package/dist/ui/option-menu.js +1 -1
  69. package/dist/ui/rider-rig.d.ts +1 -1
  70. package/dist/ui/rider-rig.js +2 -2
  71. package/package.json +1 -5
  72. package/dist/difficulty/index.d.ts +0 -141
  73. package/dist/difficulty/index.d.ts.map +0 -1
  74. package/dist/difficulty/index.js +0 -300
  75. package/dist/difficulty/index.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zones.d.ts","sourceRoot":"","sources":["../../src/training/zones.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,SAAS;IACxB,WAAW;IACX,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAQlC,CAAC;AAEX;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAOzD;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAGlE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAEpD"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Coggan's seven power zones — the single source of truth for zone boundaries
3
+ * and colours in this app.
4
+ *
5
+ * These are a *coaching* convention, not a platform scale, which is why they live
6
+ * here and not in `@rydr/game-sdk`. (The platform's own ladder — the 1→50 rider
7
+ * rider's own FTP — is a different thing entirely and arrives on the scoped
8
+ * identity as `ftp`; never mix the two.)
9
+ *
10
+ * Nothing outside this file should hardcode a zone colour: a chart, a badge and a
11
+ * live readout showing the same effort in three different colours is exactly the
12
+ * bug this module exists to prevent.
13
+ */
14
+ export const POWER_ZONES = [
15
+ { zone: 1, name: "Active Recovery", shortName: "Z1", minFtp: 0, maxFtp: 0.55, color: "#94a3b8", colorLight: "rgba(148,163,184,0.25)" },
16
+ { zone: 2, name: "Endurance", shortName: "Z2", minFtp: 0.55, maxFtp: 0.75, color: "#3b82f6", colorLight: "rgba(59,130,246,0.25)" },
17
+ { zone: 3, name: "Tempo", shortName: "Z3", minFtp: 0.75, maxFtp: 0.9, color: "#22c55e", colorLight: "rgba(34,197,94,0.25)" },
18
+ { zone: 4, name: "Threshold", shortName: "Z4", minFtp: 0.9, maxFtp: 1.05, color: "#ffba0a", colorLight: "rgba(255,186,10,0.25)" },
19
+ { zone: 5, name: "VO2max", shortName: "Z5", minFtp: 1.05, maxFtp: 1.2, color: "#f97316", colorLight: "rgba(249,115,22,0.25)" },
20
+ { zone: 6, name: "Anaerobic", shortName: "Z6", minFtp: 1.2, maxFtp: 1.5, color: "#ef4444", colorLight: "rgba(239,68,68,0.25)" },
21
+ { zone: 7, name: "Neuromuscular", shortName: "Z7", minFtp: 1.5, maxFtp: Infinity, color: "#a855f7", colorLight: "rgba(168,85,247,0.25)" },
22
+ ];
23
+ /**
24
+ * The zone for a power expressed as a **percentage of FTP** (120 = 120% FTP).
25
+ *
26
+ * Prefer this over {@link zoneForWatts} wherever the authored percentage is still
27
+ * in hand — a segment's colour then can't drift when the rider's FTP changes, and
28
+ * two segments authored at the same percentage always draw identically.
29
+ */
30
+ export function zoneForPercent(percent) {
31
+ if (!(percent > 0))
32
+ return POWER_ZONES[0];
33
+ const ratio = percent / 100;
34
+ for (let i = POWER_ZONES.length - 1; i >= 0; i--) {
35
+ if (ratio >= POWER_ZONES[i].minFtp)
36
+ return POWER_ZONES[i];
37
+ }
38
+ return POWER_ZONES[0];
39
+ }
40
+ /** The zone for a live power reading in watts against an FTP in watts. */
41
+ export function zoneForWatts(watts, ftp) {
42
+ if (!(watts > 0) || !(ftp > 0))
43
+ return POWER_ZONES[0];
44
+ return zoneForPercent((watts / ftp) * 100);
45
+ }
46
+ /**
47
+ * The zone with a given number, 1–7.
48
+ *
49
+ * For the places that already hold a zone *number* — a time-in-zone tally, a
50
+ * legend row — and need its colour or name back. Without this they end up
51
+ * re-listing the colours locally, which is how a chart and its own legend come to
52
+ * disagree.
53
+ */
54
+ export function zoneByNumber(zone) {
55
+ return POWER_ZONES[Math.min(POWER_ZONES.length, Math.max(1, Math.round(zone))) - 1];
56
+ }
57
+ //# sourceMappingURL=zones.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zones.js","sourceRoot":"","sources":["../../src/training/zones.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmBH,MAAM,CAAC,MAAM,WAAW,GAAyB;IAC/C,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAK,MAAM,EAAE,IAAI,EAAM,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,wBAAwB,EAAE;IAC7I,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAQ,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAM,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,uBAAuB,EAAE;IAC5I,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAY,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,sBAAsB,EAAE;IAC3I,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAQ,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAG,MAAM,EAAE,IAAI,EAAM,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,uBAAuB,EAAE;IAC5I,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAW,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,uBAAuB,EAAE;IAC5I,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAQ,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAG,MAAM,EAAE,GAAG,EAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,sBAAsB,EAAE;IAC3I,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,eAAe,EAAI,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,uBAAuB,EAAE;CACpI,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;QAAE,OAAO,WAAW,CAAC,CAAC,CAAE,CAAC;IAC3C,MAAM,KAAK,GAAG,OAAO,GAAG,GAAG,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACjD,IAAI,KAAK,IAAI,WAAW,CAAC,CAAC,CAAE,CAAC,MAAM;YAAE,OAAO,WAAW,CAAC,CAAC,CAAE,CAAC;IAC9D,CAAC;IACD,OAAO,WAAW,CAAC,CAAC,CAAE,CAAC;AACzB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,GAAW;IACrD,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAAE,OAAO,WAAW,CAAC,CAAC,CAAE,CAAC;IACvD,OAAO,cAAc,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;AACvF,CAAC"}
package/dist/ui/README.md CHANGED
@@ -133,7 +133,7 @@ handlers go quiet, `isDown`/`stick` read resting, and anything held is released
133
133
  **It also does NOT touch the trainer.** The activity state is left exactly as your game set it, so a
134
134
  rider who opens this mid-run keeps the resistance they were riding. This menu is an overlay over LIVE
135
135
  gameplay, not a screen they navigated to — they're still on the bike with their legs turning, and
136
- easing under them (then snapping back on Resume) would change the difficulty of the run they're
136
+ easing under them (then snapping back on Resume) would change the intensity of the run they're
137
137
  deciding whether to restart. If your game does want easing while it's up, call
138
138
  `session.setActivity("menu")` in `onOpen` and `"playing"` in `onClose` — each game decides.
139
139
 
@@ -495,8 +495,7 @@ The scale runs `0 → 1.5 × ftp`, with the rider's threshold marked at two thir
495
495
  above it for a sprint. Pass `ftp` (from `session.identity.ftp`) and never hard-code a raw-watt scale —
496
496
  the same rule as `createPowerRace`. The effort colour is a **continuous ramp** over %FTP — grey-blue →
497
497
  blue → violet → pink → red, interpolated rather than banded, so it never jumps mid-surge — and is **local to this
498
- component**: they are deliberately not `@rydr/game-sdk/difficulty`'s ramp, which colours the rider's
499
- chosen *calibration level* and is a different quantity that happens to be measured in watts too.
498
+ component**, not a shared ramp.
500
499
 
501
500
  Feed it **raw `power`**. It keeps both numbers: the readout shows the true instantaneous value while
502
501
  the bar follows an internal EMA so it climbs smoothly between the trainer's sparse (~1–4 Hz) updates.
@@ -45,7 +45,7 @@
45
45
  * game left it — a rider who opens it mid-run keeps the resistance they were riding. That is
46
46
  * deliberate: this menu is a transient overlay over LIVE gameplay, not a screen the rider navigated
47
47
  * to, and they are still on the bike with their legs turning. Dropping to the eased menu resistance
48
- * and snapping back on `Resume` would change the difficulty of the very run they are deciding
48
+ * and snapping back on `Resume` would change the intensity of the very run they are deciding
49
49
  * whether to restart. (Contrast the PLATFORM overlay, which is the shell's own and does ease.)
50
50
  * A game that genuinely wants easing here can still do it itself in `onOpen`/`onClose` —
51
51
  * `session.setActivity("menu")` / `("playing")` — which is more flexible than an option anyway.
@@ -45,7 +45,7 @@
45
45
  * game left it — a rider who opens it mid-run keeps the resistance they were riding. That is
46
46
  * deliberate: this menu is a transient overlay over LIVE gameplay, not a screen the rider navigated
47
47
  * to, and they are still on the bike with their legs turning. Dropping to the eased menu resistance
48
- * and snapping back on `Resume` would change the difficulty of the very run they are deciding
48
+ * and snapping back on `Resume` would change the intensity of the very run they are deciding
49
49
  * whether to restart. (Contrast the PLATFORM overlay, which is the shell's own and does ease.)
50
50
  * A game that genuinely wants easing here can still do it itself in `onOpen`/`onClose` —
51
51
  * `session.setActivity("menu")` / `("playing")` — which is more flexible than an option anyway.
@@ -62,7 +62,7 @@
62
62
  * 350 W rider therefore see the same gauge do the same thing for the same effort — never hard-code a
63
63
  * raw-watt scale (the same rule as {@link createPowerRace}). The effort colour is a continuous ramp
64
64
  * over %FTP — grey-blue, blue, violet, pink, red, with no band boundaries to jump at — and is
65
- * are **local to this component**: they are deliberately NOT `@rydr/game-sdk/difficulty`'s ramp, which
65
+ * are **local to this component**: they are deliberately NOT a shared ramp, which
66
66
  * colours the rider's *chosen calibration level* and would be a different quantity wearing the same
67
67
  * paint.
68
68
  *
@@ -62,7 +62,7 @@
62
62
  * 350 W rider therefore see the same gauge do the same thing for the same effort — never hard-code a
63
63
  * raw-watt scale (the same rule as {@link createPowerRace}). The effort colour is a continuous ramp
64
64
  * over %FTP — grey-blue, blue, violet, pink, red, with no band boundaries to jump at — and is
65
- * are **local to this component**: they are deliberately NOT `@rydr/game-sdk/difficulty`'s ramp, which
65
+ * are **local to this component**: they are deliberately NOT a shared ramp, which
66
66
  * colours the rider's *chosen calibration level* and would be a different quantity wearing the same
67
67
  * paint.
68
68
  *
@@ -293,7 +293,7 @@ const FLY_GEAR = 2.1;
293
293
  const STREAK_FULL_RPM = 95;
294
294
  /**
295
295
  * %FTP effort colours — LOCAL to this component. See the module header on why this is not
296
- * `@rydr/game-sdk/difficulty`'s ramp.
296
+ * this component's own ramp.
297
297
  *
298
298
  * A CONTINUOUS ramp, not a table of zones. It was six discrete bands (recovery, endurance, tempo,
299
299
  * threshold, VO2, anaerobic) and every boundary was a jump: the bar changed colour in one frame while
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rydr/game-sdk",
3
- "version": "8.21.0",
3
+ "version": "9.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/bdefrenne/rydr-game-sdk.git"
@@ -44,10 +44,6 @@
44
44
  "types": "./dist/overlay/index.d.ts",
45
45
  "import": "./dist/overlay/index.js"
46
46
  },
47
- "./difficulty": {
48
- "types": "./dist/difficulty/index.d.ts",
49
- "import": "./dist/difficulty/index.js"
50
- },
51
47
  "./sounds": {
52
48
  "types": "./dist/sounds/index.d.ts",
53
49
  "import": "./dist/sounds/index.js"
@@ -1,141 +0,0 @@
1
- /**
2
- * The **FTP-difficulty ladder** — levels 1 → 50, and the watts each one means.
3
- *
4
- * This is the rider's chosen calibration: the number every game's `%FTP` demands are a percentage
5
- * of. It is **not** a measurement of anyone's fitness, and it is not the rank of a score.
6
- *
7
- * ## Why this lives in the SDK
8
- *
9
- * A leaderboard row carries watts — `BoardEntry.ftpDifficulty` in `src/protocol/boards.ts` — and
10
- * nothing else. Any game that draws a rider's badge next to their score therefore has to turn those
11
- * watts into a level and a colour, and a game cannot import the shell's copy across the iframe
12
- * boundary. Before this module existed the only way through that was to hand-port the ladder into
13
- * each game, which is exactly what happened: guitar hero shipped a private copy that kept rendering
14
- * a retired 36-rung metal ladder long after the shell had moved on, so one rider's `ftpDifficulty`
15
- * produced a bronze "3" in-game and a ramp-coloured "26" in the chrome. **The interpretation of a
16
- * protocol field belongs to the protocol.** The shell imports this module too — there is one ladder,
17
- * published once, and both sides read it.
18
- *
19
- * Pure by design: no `lit`, no `three`, no DOM. {@link visualForLevel} returns CSS strings, which any
20
- * renderer can use — a Lit component's `style`, an `innerHTML` template, a canvas fill.
21
- *
22
- * ## The scale
23
- *
24
- * **Watts are what get stored; the level is derived here.** Every persisted copy of the dial (the
25
- * shell's `profile.ftpDifficulty`, a leaderboard entry's `ftp_difficulty`) holds watts, and every
26
- * client computes the level from it through {@link levelForWatts}. That's what lets this scale be
27
- * redrawn — floor, band widths, tier count — without migrating or reinterpreting a single stored row.
28
- * The cost, accepted: redrawing it re-labels history, so a shift in the levels reported by analytics
29
- * can mean the ladder moved rather than the riders.
30
- *
31
- * ```
32
- * watts = level ≤ 9 ? 50 + 5 × (level − 1) // 50, 55, 60 … 90
33
- * : level × 10 // 100, 110 … 500
34
- * ```
35
- *
36
- * `level × 10 = watts` holds exactly from 100 W up, where essentially every rider sits — that
37
- * relationship is the point of the number, since it makes "22" readable as "~220 W" with no lookup.
38
- * Below 100 W the steps are finer because a flat 10 W step there is a **20%** jump where at 400 W
39
- * it's 2.5%. The floor of 50 W is the researched floor for a deconditioned adult; the ceiling of
40
- * 500 W is beyond any realistic rider on purpose, so nobody is ever pinned at either end.
41
- *
42
- * **There is deliberately no watts-per-press constant** (see {@link stepWatts}): the watts a − / +
43
- * press moves are whatever the neighbouring rung is — 5 W inside levels 1 → 9, 10 W from level 10
44
- * up, and 10 W across the 9 → 10 boundary. Any single constant is wrong in one of those bands by
45
- * construction, which is the bug that convention replaced.
46
- *
47
- * The rationale for the whole design, including the colour ramp below, is written up in the platform
48
- * repo: `rydr-platform/docs/run-level-and-leaderboards.md`.
49
- */
50
- /** Weakest level. The ladder is **1-based** — there is no level 0. */
51
- export declare const LEVEL_MIN = 1;
52
- /** Strongest level. */
53
- export declare const LEVEL_MAX = 50;
54
- /** Watts of the fine band's first rung, i.e. of {@link LEVEL_MIN}. */
55
- export declare const FTP_MIN = 50;
56
- /** Watts at {@link LEVEL_MAX}. A real ceiling, not a bucket: nobody is meant to reach it. */
57
- export declare const FTP_MAX = 500;
58
- /** Levels per tier badge. Ten tiers × five levels covers 1 → 50 exactly. */
59
- export declare const LEVELS_PER_TIER = 5;
60
- /** Number of tier badges. */
61
- export declare const TIER_COUNT: number;
62
- /** One rung of the ladder. */
63
- export interface FtpLevel {
64
- /** The level itself, 1 (weakest) → 50 (strongest). This is the number riders see. */
65
- level: number;
66
- /** What the level means in watts — the value that actually gets stored. */
67
- watts: number;
68
- /**
69
- * Band index, 0 → 9 — five levels each. Purely structural: it exists so a ladder picker can
70
- * group 50 rungs into scannable rows, and it carries **no meaning of its own**. The metal and gem
71
- * names it used to have (Iron → Diamond) were dropped with the tier colours, because a named tier
72
- * reads as a club to belong to and the level is a dial the rider sets, not a rank they earned.
73
- * Label a band with {@link rangeLabelForTier}, never with a name.
74
- */
75
- tier: number;
76
- }
77
- /** The pill's visual styling for a level. */
78
- export interface LevelVisual {
79
- /** CSS `background` for the pill — a translucent two-stop gradient of the level's colour. */
80
- fill: string;
81
- /** Keyline colour. Held at a higher alpha than the fill so the hue stays crisp. */
82
- accent: string;
83
- /** Shadow/glow colour under the pill. */
84
- glow: string;
85
- /** Colour for the numeral — the level's own hue, brightened until it's legible on the fill. */
86
- ink: string;
87
- }
88
- /** Clamp any number to a whole level inside the ladder. */
89
- export declare function clampLevel(level: number): number;
90
- /** The watts a level means. Clamps, so it's safe to call with arithmetic results. */
91
- export declare function wattsForLevel(level: number): number;
92
- /**
93
- * The level a watts value sits at — the **nearest** rung, so any number (a legacy profile, the
94
- * weight-relative result of an onboarding questionnaire, one day a measured estimate) has a level.
95
- * Ties round up, which matters only for values landing exactly between two rungs.
96
- *
97
- * Each band's formula is inverted, then whichever candidate rung is genuinely closer wins. Trusting
98
- * one formula by a `watts < 100` branch is wrong **at the seam**, because the gap from level 9 (90 W)
99
- * to level 10 (100 W) is 10 W while the low-band formula divides by 5: it read 98 W as level 11 and
100
- * snapped it up to 110 W — a 12% jump — and 93 W as level 10. Only 93, 94, 98 and 99 W were affected,
101
- * which is exactly why it took a brute-force sweep against {@link FTP_LEVELS} to notice.
102
- */
103
- export declare function levelForWatts(watts: number): number;
104
- /** Snap arbitrary watts onto the ladder — `wattsForLevel(levelForWatts(w))`. Idempotent. */
105
- export declare function snapWatts(watts: number): number;
106
- /**
107
- * Move one level from wherever `watts` is, and return the new **watts**.
108
- *
109
- * This is the only correct way to step the dial: it derives the level, moves it by one, and reads
110
- * that level's watts back. Off-ladder input snaps to its nearest rung on the first press, then
111
- * steps from there. At either end of the ladder it returns the end.
112
- */
113
- export declare function stepWatts(watts: number, direction: 1 | -1): number;
114
- /** The tier badge index (0 → 9) a level belongs to. */
115
- export declare function tierForLevel(level: number): number;
116
- /**
117
- * A level's ramp colour as one flat hex.
118
- *
119
- * {@link visualForLevel} returns a translucent CSS gradient, which is right for a pill and useless
120
- * to anything that needs a single value — a gradient stop, a chart series, a canvas fill. Callers
121
- * wanting to draw the ramp itself (e.g. a results-screen level track) sample this per level.
122
- */
123
- export declare function colourForLevel(level: number): string;
124
- /** The pill styling for a level — its position on the ramp. */
125
- export declare function visualForLevel(level: number): LevelVisual;
126
- /** Every rung, weakest → strongest (level 1 → 50). */
127
- export declare const FTP_LEVELS: readonly FtpLevel[];
128
- /** The rung a watts value sits on — the whole {@link FtpLevel}, for UI that needs its parts. */
129
- export declare function levelAt(watts: number): FtpLevel;
130
- /** Watts label for a level — "255 W". */
131
- export declare function wattsLabel(level: number): string;
132
- /**
133
- * Level-range label for a band — "26-30". This is what a band gets called now that the metal names
134
- * are gone: a band is a group of five rungs on a dial, so its honest label is which rungs it spans.
135
- */
136
- export declare function levelRangeLabelForTier(tier: number): string;
137
- /** Watts-range label for a whole tier badge — "270-310 W". */
138
- export declare function rangeLabelForTier(tier: number): string;
139
- /** The levels belonging to a tier badge, weakest → strongest. */
140
- export declare function levelsInTier(tier: number): readonly FtpLevel[];
141
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/difficulty/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,sEAAsE;AACtE,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,uBAAuB;AACvB,eAAO,MAAM,SAAS,KAAK,CAAC;AAG5B,sEAAsE;AACtE,eAAO,MAAM,OAAO,KAAK,CAAC;AAG1B,6FAA6F;AAC7F,eAAO,MAAM,OAAO,MAAM,CAAC;AAE3B,4EAA4E;AAC5E,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,6BAA6B;AAC7B,eAAO,MAAM,UAAU,QAA8B,CAAC;AAEtD,8BAA8B;AAC9B,MAAM,WAAW,QAAQ;IACvB,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IAC1B,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,+FAA+F;IAC/F,GAAG,EAAE,MAAM,CAAC;CACb;AAID,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED,4FAA4F;AAC5F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAElE;AAsLD,uDAAuD;AACvD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CASzD;AAID,sDAAsD;AACtD,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAGxC,CAAC;AAEH,gGAAgG;AAChG,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAE/C;AAED,yCAAyC;AACzC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,QAAQ,EAAE,CAE9D"}
@@ -1,300 +0,0 @@
1
- /**
2
- * The **FTP-difficulty ladder** — levels 1 → 50, and the watts each one means.
3
- *
4
- * This is the rider's chosen calibration: the number every game's `%FTP` demands are a percentage
5
- * of. It is **not** a measurement of anyone's fitness, and it is not the rank of a score.
6
- *
7
- * ## Why this lives in the SDK
8
- *
9
- * A leaderboard row carries watts — `BoardEntry.ftpDifficulty` in `src/protocol/boards.ts` — and
10
- * nothing else. Any game that draws a rider's badge next to their score therefore has to turn those
11
- * watts into a level and a colour, and a game cannot import the shell's copy across the iframe
12
- * boundary. Before this module existed the only way through that was to hand-port the ladder into
13
- * each game, which is exactly what happened: guitar hero shipped a private copy that kept rendering
14
- * a retired 36-rung metal ladder long after the shell had moved on, so one rider's `ftpDifficulty`
15
- * produced a bronze "3" in-game and a ramp-coloured "26" in the chrome. **The interpretation of a
16
- * protocol field belongs to the protocol.** The shell imports this module too — there is one ladder,
17
- * published once, and both sides read it.
18
- *
19
- * Pure by design: no `lit`, no `three`, no DOM. {@link visualForLevel} returns CSS strings, which any
20
- * renderer can use — a Lit component's `style`, an `innerHTML` template, a canvas fill.
21
- *
22
- * ## The scale
23
- *
24
- * **Watts are what get stored; the level is derived here.** Every persisted copy of the dial (the
25
- * shell's `profile.ftpDifficulty`, a leaderboard entry's `ftp_difficulty`) holds watts, and every
26
- * client computes the level from it through {@link levelForWatts}. That's what lets this scale be
27
- * redrawn — floor, band widths, tier count — without migrating or reinterpreting a single stored row.
28
- * The cost, accepted: redrawing it re-labels history, so a shift in the levels reported by analytics
29
- * can mean the ladder moved rather than the riders.
30
- *
31
- * ```
32
- * watts = level ≤ 9 ? 50 + 5 × (level − 1) // 50, 55, 60 … 90
33
- * : level × 10 // 100, 110 … 500
34
- * ```
35
- *
36
- * `level × 10 = watts` holds exactly from 100 W up, where essentially every rider sits — that
37
- * relationship is the point of the number, since it makes "22" readable as "~220 W" with no lookup.
38
- * Below 100 W the steps are finer because a flat 10 W step there is a **20%** jump where at 400 W
39
- * it's 2.5%. The floor of 50 W is the researched floor for a deconditioned adult; the ceiling of
40
- * 500 W is beyond any realistic rider on purpose, so nobody is ever pinned at either end.
41
- *
42
- * **There is deliberately no watts-per-press constant** (see {@link stepWatts}): the watts a − / +
43
- * press moves are whatever the neighbouring rung is — 5 W inside levels 1 → 9, 10 W from level 10
44
- * up, and 10 W across the 9 → 10 boundary. Any single constant is wrong in one of those bands by
45
- * construction, which is the bug that convention replaced.
46
- *
47
- * The rationale for the whole design, including the colour ramp below, is written up in the platform
48
- * repo: `rydr-platform/docs/run-level-and-leaderboards.md`.
49
- */
50
- /** Weakest level. The ladder is **1-based** — there is no level 0. */
51
- export const LEVEL_MIN = 1;
52
- /** Strongest level. */
53
- export const LEVEL_MAX = 50;
54
- /** Last level of the fine 5 W band. */
55
- const LOW_BAND_MAX = 9;
56
- /** Watts of the fine band's first rung, i.e. of {@link LEVEL_MIN}. */
57
- export const FTP_MIN = 50;
58
- /** Watts per rung inside the fine band (levels 1 → 9 only — never use this as *the* step). */
59
- const LOW_BAND_STEP = 5;
60
- /** Watts at {@link LEVEL_MAX}. A real ceiling, not a bucket: nobody is meant to reach it. */
61
- export const FTP_MAX = 500;
62
- /** Levels per tier badge. Ten tiers × five levels covers 1 → 50 exactly. */
63
- export const LEVELS_PER_TIER = 5;
64
- /** Number of tier badges. */
65
- export const TIER_COUNT = LEVEL_MAX / LEVELS_PER_TIER; // 10
66
- // ── the scale ─────────────────────────────────────────────────────────────────────
67
- /** Clamp any number to a whole level inside the ladder. */
68
- export function clampLevel(level) {
69
- return Math.max(LEVEL_MIN, Math.min(LEVEL_MAX, Math.round(level)));
70
- }
71
- /** The watts a level means. Clamps, so it's safe to call with arithmetic results. */
72
- export function wattsForLevel(level) {
73
- const lvl = clampLevel(level);
74
- return lvl <= LOW_BAND_MAX ? FTP_MIN + LOW_BAND_STEP * (lvl - LEVEL_MIN) : lvl * 10;
75
- }
76
- /**
77
- * The level a watts value sits at — the **nearest** rung, so any number (a legacy profile, the
78
- * weight-relative result of an onboarding questionnaire, one day a measured estimate) has a level.
79
- * Ties round up, which matters only for values landing exactly between two rungs.
80
- *
81
- * Each band's formula is inverted, then whichever candidate rung is genuinely closer wins. Trusting
82
- * one formula by a `watts < 100` branch is wrong **at the seam**, because the gap from level 9 (90 W)
83
- * to level 10 (100 W) is 10 W while the low-band formula divides by 5: it read 98 W as level 11 and
84
- * snapped it up to 110 W — a 12% jump — and 93 W as level 10. Only 93, 94, 98 and 99 W were affected,
85
- * which is exactly why it took a brute-force sweep against {@link FTP_LEVELS} to notice.
86
- */
87
- export function levelForWatts(watts) {
88
- if (!Number.isFinite(watts))
89
- return LEVEL_MIN;
90
- if (watts <= FTP_MIN)
91
- return LEVEL_MIN;
92
- if (watts >= FTP_MAX)
93
- return LEVEL_MAX;
94
- const low = clampLevel(LEVEL_MIN + (watts - FTP_MIN) / LOW_BAND_STEP);
95
- const high = clampLevel(watts / 10);
96
- // `<` not `<=`, so a value exactly between two rungs keeps the documented round-up.
97
- return Math.abs(wattsForLevel(high) - watts) < Math.abs(wattsForLevel(low) - watts) ? high : low;
98
- }
99
- /** Snap arbitrary watts onto the ladder — `wattsForLevel(levelForWatts(w))`. Idempotent. */
100
- export function snapWatts(watts) {
101
- return wattsForLevel(levelForWatts(watts));
102
- }
103
- /**
104
- * Move one level from wherever `watts` is, and return the new **watts**.
105
- *
106
- * This is the only correct way to step the dial: it derives the level, moves it by one, and reads
107
- * that level's watts back. Off-ladder input snaps to its nearest rung on the first press, then
108
- * steps from there. At either end of the ladder it returns the end.
109
- */
110
- export function stepWatts(watts, direction) {
111
- return wattsForLevel(levelForWatts(watts) + direction);
112
- }
113
- // ── colour ────────────────────────────────────────────────────────────────────────
114
- const hex2rgb = (h) => {
115
- const s = h.replace("#", "");
116
- return [0, 2, 4].map((i) => parseInt(s.slice(i, i + 2), 16));
117
- };
118
- const to2 = (x) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, "0");
119
- const rgb2hex = (r, g, b) => `#${to2(r)}${to2(g)}${to2(b)}`;
120
- const mix = (h1, h2, t) => {
121
- const a = hex2rgb(h1);
122
- const b = hex2rgb(h2);
123
- return rgb2hex(a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t);
124
- };
125
- const rgba = (hex, alpha) => `rgba(${hex2rgb(hex).join(",")},${alpha})`;
126
- const hsl = (h, s, l) => {
127
- const sat = Math.min(100, s) / 100;
128
- const lig = l / 100;
129
- const k = (n) => (n + h / 30) % 12;
130
- const a = sat * Math.min(lig, 1 - lig);
131
- const f = (n) => lig - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
132
- return rgb2hex(255 * f(0), 255 * f(8), 255 * f(4));
133
- };
134
- const hslA = (h, s, l, alpha) => `hsl(${h.toFixed(1)} ${Math.min(100, s).toFixed(1)}% ${l.toFixed(1)}% / ${alpha})`;
135
- const lerp = (a, b, t) => a + (b - a) * t;
136
- /**
137
- * Relative luminance, the sRGB formula WCAG contrast is built on. Needed because the answer swings
138
- * hard with hue: a gold and a blue at the same nominal lightness are nowhere near each other behind
139
- * white text, so every threshold below is measured rather than guessed.
140
- */
141
- const relLum = (hex) => {
142
- const lin = (u) => u <= 0.03928 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4);
143
- const [r, g, b] = hex2rgb(hex);
144
- return 0.2126 * lin(r / 255) + 0.7152 * lin(g / 255) + 0.0722 * lin(b / 255);
145
- };
146
- const contrast = (a, b) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
147
- /**
148
- * Positions are explicit because even spacing is what made level 26 ugly. Interpolating green
149
- * (142°) → gold (45°) passes through ~90°, and 90° deepened enough for white text is army-green
150
- * olive. Gold itself was never the problem and can't be avoided anyway: every green-to-red path
151
- * crosses it, and going the other way round the wheel revisits blue. What's fixable is how long
152
- * the ramp lingers in the crossing, so green is warmed to 152° (further from chartreuse), gold to
153
- * 42° at 96% saturation (deepens to a rich gold, not mustard), and the green→gold segment is
154
- * squeezed to 0.027 of the path — positioned so the whole traverse falls in the GAP between
155
- * level 26 (p 0.456) and level 27 (p 0.500). Nothing lands in the lime zone.
156
- *
157
- * The cost, stated honestly: that pair is a visible hue jump, the one hard edge in an otherwise
158
- * continuous ramp — and a hard edge is the "club boundary" the metals were dropped to avoid. It
159
- * buys the ugliest colour out of the most populated band. Widen this segment if the edge ever
160
- * reads worse than the olive did.
161
- */
162
- const HUE_STOPS = [
163
- { p: 0.0, h: 217, s: 91 }, // blue
164
- { p: 0.465, h: 152, s: 74 }, // green
165
- { p: 0.492, h: 42, s: 96 }, // gold
166
- { p: 0.74, h: 22, s: 95 }, // orange
167
- { p: 1.0, h: 0, s: 84 }, // red
168
- ];
169
- /** First and last level the hue path moves across; outside this it's clamped. */
170
- const HUE_LO = 10;
171
- const HUE_HI = 44;
172
- /** Lightness floor and ceiling. The floor keeps level 1 separable from a dark container. */
173
- const L_LO = 34;
174
- const L_HI = 64;
175
- const S_SCALE_LO = 0.5;
176
- const S_SCALE_HI = 1.0;
177
- /** Fill alpha. The pill is glass, so it reads as chrome rather than a sticker on top of it. */
178
- const FILL_ALPHA = 0.4;
179
- /** Max luminance a fill may have. Above this, white-on-fill stops being comfortable. */
180
- const LUM_MAX = 0.25;
181
- /** Contrast the coloured numeral must clear against its own composited fill. */
182
- const INK_MIN_CONTRAST = 4.2;
183
- /** Nominal dark container behind a pill — what a translucent fill composites over. */
184
- const CONTAINER = "#111c2e";
185
- const smoothstep = (t) => t * t * (3 - 2 * t);
186
- /** Hue-path position for a level: eased inside the window, clamped outside it. */
187
- function huePos(level) {
188
- const t = (clampLevel(level) - HUE_LO) / (HUE_HI - HUE_LO);
189
- return smoothstep(Math.max(0, Math.min(1, t)));
190
- }
191
- /**
192
- * Hue + saturation at a position. Hue decreases monotonically 217° → 0° with no wraparound, so
193
- * plain interpolation between adjacent stops walks the ramp in order and never takes the long way
194
- * round the wheel.
195
- */
196
- function hueAt(p) {
197
- const x = Math.max(0, Math.min(1, p));
198
- let i = 0;
199
- while (i < HUE_STOPS.length - 2 && x > HUE_STOPS[i + 1].p)
200
- i++;
201
- const a = HUE_STOPS[i];
202
- const b = HUE_STOPS[i + 1];
203
- const t = (x - a.p) / (b.p - a.p);
204
- return { h: lerp(a.h, b.h, t), s: lerp(a.s, b.s, t) };
205
- }
206
- /** The level's own colour, before any legibility adjustment. */
207
- function rampColour(level) {
208
- const lvl = clampLevel(level);
209
- const { h, s } = hueAt(huePos(lvl));
210
- const t = (lvl - LEVEL_MIN) / (LEVEL_MAX - LEVEL_MIN);
211
- const q = 2 * t - smoothstep(t); // complement of the hue curve
212
- return { h, s: Math.min(100, s * lerp(S_SCALE_LO, S_SCALE_HI, q)), l: lerp(L_LO, L_HI, q) };
213
- }
214
- /**
215
- * Fill lightness, solved PER HUE rather than by a flat cap — the cap that makes gold safe would
216
- * needlessly darken blue into slate. Walks down from the level's own lightness until the fill is
217
- * dark enough to sit under white/coloured text.
218
- */
219
- function fillLightness(c) {
220
- let l = c.l;
221
- while (l > 18 && relLum(hsl(c.h, c.s, l)) > LUM_MAX)
222
- l -= 1;
223
- return l;
224
- }
225
- /**
226
- * The numeral's colour: the level's own hue, brightened until it clears
227
- * {@link INK_MIN_CONTRAST} against the **composited** fill — the fill blended over the container
228
- * at {@link FILL_ALPHA}, not the fill's own colour. That distinction is the whole thing: at 40%
229
- * glass most of what sits behind the glyph is container, so measuring against the raw fill would
230
- * let the ink come out too dark. Exact mirror of {@link fillLightness}: that walks a fill down
231
- * until text is safe, this walks the text up until the hue is safe.
232
- *
233
- * Saturation is capped at 88% so bright ink doesn't vibrate against its own fill.
234
- */
235
- function inkColour(c) {
236
- const bg = relLum(mix(hsl(c.h, c.s, fillLightness(c)), CONTAINER, FILL_ALPHA));
237
- const s = Math.min(c.s, 88);
238
- let l = Math.min(92, c.l + 14);
239
- while (l < 92 && contrast(relLum(hsl(c.h, s, l)), bg) < INK_MIN_CONTRAST)
240
- l += 1;
241
- return hsl(c.h, s, l);
242
- }
243
- /** The tier badge index (0 → 9) a level belongs to. */
244
- export function tierForLevel(level) {
245
- return Math.floor((clampLevel(level) - LEVEL_MIN) / LEVELS_PER_TIER);
246
- }
247
- /**
248
- * A level's ramp colour as one flat hex.
249
- *
250
- * {@link visualForLevel} returns a translucent CSS gradient, which is right for a pill and useless
251
- * to anything that needs a single value — a gradient stop, a chart series, a canvas fill. Callers
252
- * wanting to draw the ramp itself (e.g. a results-screen level track) sample this per level.
253
- */
254
- export function colourForLevel(level) {
255
- const c = rampColour(level);
256
- return hsl(c.h, c.s, c.l);
257
- }
258
- /** The pill styling for a level — its position on the ramp. */
259
- export function visualForLevel(level) {
260
- const c = rampColour(level);
261
- const lf = fillLightness(c);
262
- return {
263
- fill: `linear-gradient(140deg,${hslA(c.h, c.s, lf, FILL_ALPHA)},${hslA(c.h, c.s, Math.max(12, lf - 11), FILL_ALPHA)})`,
264
- accent: hslA(c.h, c.s, Math.min(72, c.l + 6), Math.min(1, FILL_ALPHA + 0.18)),
265
- glow: rgba(hsl(c.h, c.s, c.l), 0.4 * FILL_ALPHA),
266
- ink: inkColour(c),
267
- };
268
- }
269
- // ── the ladder, materialised ──────────────────────────────────────────────────────
270
- /** Every rung, weakest → strongest (level 1 → 50). */
271
- export const FTP_LEVELS = Array.from({ length: LEVEL_MAX }, (_, i) => {
272
- const level = i + LEVEL_MIN;
273
- return { level, watts: wattsForLevel(level), tier: tierForLevel(level) };
274
- });
275
- /** The rung a watts value sits on — the whole {@link FtpLevel}, for UI that needs its parts. */
276
- export function levelAt(watts) {
277
- return FTP_LEVELS[levelForWatts(watts) - LEVEL_MIN];
278
- }
279
- /** Watts label for a level — "255 W". */
280
- export function wattsLabel(level) {
281
- return `${wattsForLevel(level)} W`;
282
- }
283
- /**
284
- * Level-range label for a band — "26-30". This is what a band gets called now that the metal names
285
- * are gone: a band is a group of five rungs on a dial, so its honest label is which rungs it spans.
286
- */
287
- export function levelRangeLabelForTier(tier) {
288
- const first = tier * LEVELS_PER_TIER + LEVEL_MIN;
289
- return `${first}-${first + LEVELS_PER_TIER - 1}`;
290
- }
291
- /** Watts-range label for a whole tier badge — "270-310 W". */
292
- export function rangeLabelForTier(tier) {
293
- const first = tier * LEVELS_PER_TIER + LEVEL_MIN;
294
- return `${wattsForLevel(first)}-${wattsForLevel(first + LEVELS_PER_TIER - 1)} W`;
295
- }
296
- /** The levels belonging to a tier badge, weakest → strongest. */
297
- export function levelsInTier(tier) {
298
- return FTP_LEVELS.filter((l) => l.tier === tier);
299
- }
300
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/difficulty/index.ts"],"names":[],"mappings":"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"}