@rydr/game-sdk 8.21.0 → 8.22.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 (44) hide show
  1. package/dist/client/PlatformClient.d.ts +11 -22
  2. package/dist/client/PlatformClient.d.ts.map +1 -1
  3. package/dist/client/PlatformClient.js +35 -0
  4. package/dist/client/PlatformClient.js.map +1 -1
  5. package/dist/host/PlatformHost.d.ts +4 -1
  6. package/dist/host/PlatformHost.d.ts.map +1 -1
  7. package/dist/host/PlatformHost.js +24 -0
  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/guards.d.ts.map +1 -1
  14. package/dist/protocol/guards.js +1 -0
  15. package/dist/protocol/guards.js.map +1 -1
  16. package/dist/protocol/messages.d.ts +6 -3
  17. package/dist/protocol/messages.d.ts.map +1 -1
  18. package/dist/protocol/version.d.ts +1 -1
  19. package/dist/protocol/version.js +1 -1
  20. package/dist/training/compile.d.ts +135 -0
  21. package/dist/training/compile.d.ts.map +1 -0
  22. package/dist/training/compile.js +367 -0
  23. package/dist/training/compile.js.map +1 -0
  24. package/dist/training/index.d.ts +6 -0
  25. package/dist/training/index.d.ts.map +1 -0
  26. package/dist/training/index.js +6 -0
  27. package/dist/training/index.js.map +1 -0
  28. package/dist/training/model.d.ts +252 -0
  29. package/dist/training/model.d.ts.map +1 -0
  30. package/dist/training/model.js +261 -0
  31. package/dist/training/model.js.map +1 -0
  32. package/dist/training/protocol.d.ts +60 -0
  33. package/dist/training/protocol.d.ts.map +1 -0
  34. package/dist/training/protocol.js +2 -0
  35. package/dist/training/protocol.js.map +1 -0
  36. package/dist/training/state.d.ts +201 -0
  37. package/dist/training/state.d.ts.map +1 -0
  38. package/dist/training/state.js +55 -0
  39. package/dist/training/state.js.map +1 -0
  40. package/dist/training/zones.d.ts +50 -0
  41. package/dist/training/zones.d.ts.map +1 -0
  42. package/dist/training/zones.js +57 -0
  43. package/dist/training/zones.js.map +1 -0
  44. package/package.json +1 -1
@@ -0,0 +1,261 @@
1
+ /**
2
+ * The workout data model.
3
+ *
4
+ * A workout is an ordered list of **segments**, and every power in it is a
5
+ * percentage of FTP — never watts. That is the whole reason a workout written by
6
+ * one rider is rideable by another: 88% FTP is sweet spot for a 180 W rider and
7
+ * for a 340 W rider, where "160 W" is a different session for each of them.
8
+ * Watts appear exactly once, at the last moment, in `compile.ts`.
9
+ *
10
+ * Three segment kinds cover the structured-training vocabulary:
11
+ * a steady block, a repeating interval set, and a linear ramp. Ported from the
12
+ * legacy RYDR app (`src/types/workout.ts`), which had earned this shape.
13
+ */
14
+ export const CATEGORY_LABELS = {
15
+ recovery: "Recovery",
16
+ endurance: "Endurance",
17
+ tempo: "Tempo",
18
+ sweetSpot: "Sweet Spot",
19
+ threshold: "Threshold",
20
+ aerobicPower: "Aerobic Power",
21
+ shortBursts: "Short Bursts",
22
+ sprint: "Sprint",
23
+ };
24
+ /**
25
+ * Which power zone each category *reads* as, for tinting a pill or a badge.
26
+ *
27
+ * Purely presentational — a category is descriptive (see above) and never drives
28
+ * execution, so this is only ever asked for a colour. It exists so a category
29
+ * pill and the chart underneath it agree about what "Threshold" looks like; the
30
+ * colour itself still comes from `zoneByNumber` in `./zones`, which stays the
31
+ * one place a zone colour is written down.
32
+ *
33
+ * Two of the eight straddle a boundary and are pinned to the lower zone, which
34
+ * is the legacy app's choice and worth keeping: sweet spot lives between Z3 and
35
+ * Z4, short bursts between Z5 and Z6, and picking the higher zone in each case
36
+ * made them read as harder than the sessions actually are.
37
+ */
38
+ export const CATEGORY_ZONE = {
39
+ recovery: 1,
40
+ endurance: 2,
41
+ tempo: 3,
42
+ sweetSpot: 3,
43
+ threshold: 4,
44
+ aerobicPower: 5,
45
+ shortBursts: 5,
46
+ sprint: 7,
47
+ };
48
+ /** Display order for pickers and filters — easiest to hardest. */
49
+ export const CATEGORY_ORDER = [
50
+ "recovery",
51
+ "endurance",
52
+ "tempo",
53
+ "sweetSpot",
54
+ "threshold",
55
+ "aerobicPower",
56
+ "shortBursts",
57
+ "sprint",
58
+ ];
59
+ // ============================================================================
60
+ // Factories
61
+ // ============================================================================
62
+ function uid(prefix) {
63
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
64
+ }
65
+ export function generateSegmentId() {
66
+ return uid("seg");
67
+ }
68
+ export function generatePlacedTrackId() {
69
+ return uid("cue");
70
+ }
71
+ export function generateWorkoutId() {
72
+ return uid("wk");
73
+ }
74
+ export function createFixedZoneSegment(overrides) {
75
+ return {
76
+ id: generateSegmentId(),
77
+ type: "fixed",
78
+ duration: 300,
79
+ power: 75,
80
+ ...overrides,
81
+ };
82
+ }
83
+ export function createIntervalSetSegment(overrides) {
84
+ return {
85
+ id: generateSegmentId(),
86
+ type: "intervals",
87
+ sets: 3,
88
+ reps: 10,
89
+ workDuration: 30,
90
+ workPower: 120,
91
+ recoveryDuration: 15,
92
+ recoveryPower: 50,
93
+ restBetweenSets: 120,
94
+ restPower: 50,
95
+ ...overrides,
96
+ };
97
+ }
98
+ export function createRampSegment(overrides) {
99
+ return {
100
+ id: generateSegmentId(),
101
+ type: "ramp",
102
+ duration: 300,
103
+ startPower: 50,
104
+ endPower: 100,
105
+ ...overrides,
106
+ };
107
+ }
108
+ export function createWorkout(overrides) {
109
+ const now = Date.now();
110
+ return {
111
+ id: generateWorkoutId(),
112
+ name: "",
113
+ description: "",
114
+ category: "threshold",
115
+ segments: [],
116
+ createdAt: now,
117
+ updatedAt: now,
118
+ ...overrides,
119
+ };
120
+ }
121
+ /** A deep copy under a fresh id — for "duplicate", and for editing a preset. */
122
+ export function cloneWorkout(workout, name) {
123
+ const now = Date.now();
124
+ return {
125
+ ...structuredClone(workout),
126
+ id: generateWorkoutId(),
127
+ name: name ?? `${workout.name} (copy)`,
128
+ segments: workout.segments.map((seg) => ({ ...seg, id: generateSegmentId() })),
129
+ createdAt: now,
130
+ updatedAt: now,
131
+ };
132
+ }
133
+ // ============================================================================
134
+ // Durations
135
+ // ============================================================================
136
+ /** Seconds a segment occupies on the timeline. */
137
+ export function segmentDuration(segment) {
138
+ if (segment.type === "fixed" || segment.type === "ramp")
139
+ return segment.duration;
140
+ // An interval set is (work + recovery) per rep, times reps, times sets, plus
141
+ // the rests BETWEEN sets — there is no rest after the final one.
142
+ const perRep = segment.workDuration + segment.recoveryDuration;
143
+ const perSet = perRep * segment.reps;
144
+ const rests = segment.restBetweenSets * Math.max(0, segment.sets - 1);
145
+ return perSet * segment.sets + rests;
146
+ }
147
+ /** Total seconds of a whole workout. */
148
+ export function workoutDuration(workout) {
149
+ return workout.segments.reduce((total, seg) => total + segmentDuration(seg), 0);
150
+ }
151
+ /** How many work efforts the rider will actually do. */
152
+ export function workoutIntervalCount(workout) {
153
+ return workout.segments.reduce((total, seg) => (seg.type === "intervals" ? total + seg.sets * seg.reps : total), 0);
154
+ }
155
+ // ============================================================================
156
+ // Flattening
157
+ // ============================================================================
158
+ /**
159
+ * Explode an interval set into the individual blocks it stands for.
160
+ *
161
+ * The timeline editor places blocks one at a time, so a workout imported from
162
+ * ZWO — or authored in the card editor — has to be flattened before it can be
163
+ * dragged around. `3x13x30/15` becomes 78 blocks plus 2 rests.
164
+ *
165
+ * **This is deliberately lossy in exactly one way and lossless in every other.**
166
+ * Gone is the shorthand: nothing afterwards knows those 78 blocks were once
167
+ * three sets, so `describeSegment` stops saying `"3x13x30/15"` and ZWO export
168
+ * writes a run of `<SteadyState>` rather than an `<IntervalsT>`. Kept is
169
+ * everything the *ride* reads — each block carries its {@link PhaseType}, so the
170
+ * compiled timeline has the same phases, the same efforts count, the same
171
+ * `set`/`rep` numbering and the same fast start it had before. `compileWorkout`
172
+ * of a flattened workout equals `compileWorkout` of the original, which is
173
+ * asserted in `test/flatten.test.ts` and is the whole licence for doing this.
174
+ *
175
+ * Mirrors `compileWorkout`'s expansion step for step, including its two edge
176
+ * rules: a rep with no recovery emits no recovery block, and the rest goes
177
+ * BETWEEN sets and never after the last one.
178
+ */
179
+ export function flattenSegment(segment) {
180
+ if (segment.type !== "intervals")
181
+ return [segment];
182
+ const out = [];
183
+ for (let set = 1; set <= segment.sets; set++) {
184
+ for (let rep = 1; rep <= segment.reps; rep++) {
185
+ out.push(createFixedZoneSegment({
186
+ duration: segment.workDuration,
187
+ power: segment.workPower,
188
+ phase: "work",
189
+ }));
190
+ // A rep can have no recovery (a set of one long effort), and a zero-length
191
+ // block would be an un-addressable hole in the timeline.
192
+ if (segment.recoveryDuration > 0) {
193
+ out.push(createFixedZoneSegment({
194
+ duration: segment.recoveryDuration,
195
+ power: segment.recoveryPower,
196
+ phase: "recovery",
197
+ }));
198
+ }
199
+ }
200
+ if (set < segment.sets && segment.restBetweenSets > 0) {
201
+ out.push(createFixedZoneSegment({
202
+ duration: segment.restBetweenSets,
203
+ power: segment.restPower,
204
+ phase: "rest",
205
+ }));
206
+ }
207
+ }
208
+ return out;
209
+ }
210
+ /** Every segment flattened, in order. What the timeline editor opens a workout as. */
211
+ export function flattenSegments(segments) {
212
+ return segments.flatMap(flattenSegment);
213
+ }
214
+ /** Whether anything here would change shape under {@link flattenSegments}. */
215
+ export function hasCompositeSegments(workout) {
216
+ return workout.segments.some((seg) => seg.type === "intervals");
217
+ }
218
+ /** Whether somebody scored this session by hand — see {@link WorkoutMusic}. */
219
+ export function hasAuthoredMusic(workout) {
220
+ return (workout.music?.tracks.length ?? 0) > 0;
221
+ }
222
+ // ============================================================================
223
+ // Formatting
224
+ // ============================================================================
225
+ /** `90` → `"1:30"`, `3725` → `"1:02:05"`. For clocks and countdowns. */
226
+ export function formatClock(totalSeconds) {
227
+ const s = Math.max(0, Math.round(totalSeconds));
228
+ const hours = Math.floor(s / 3600);
229
+ const minutes = Math.floor((s % 3600) / 60);
230
+ const seconds = s % 60;
231
+ const mm = hours > 0 ? String(minutes).padStart(2, "0") : String(minutes);
232
+ return hours > 0
233
+ ? `${hours}:${mm}:${String(seconds).padStart(2, "0")}`
234
+ : `${mm}:${String(seconds).padStart(2, "0")}`;
235
+ }
236
+ /** `2700` → `"45min"`, `90` → `"1min 30s"`, `45` → `"45s"`. For labels, not clocks. */
237
+ export function formatDuration(totalSeconds) {
238
+ const s = Math.max(0, Math.round(totalSeconds));
239
+ const minutes = Math.floor(s / 60);
240
+ const seconds = s % 60;
241
+ if (minutes === 0)
242
+ return `${seconds}s`;
243
+ if (seconds === 0)
244
+ return `${minutes}min`;
245
+ return `${minutes}min ${seconds}s`;
246
+ }
247
+ /** The one-line shorthand a cyclist would actually say out loud. */
248
+ export function describeSegment(segment) {
249
+ if (segment.type === "fixed") {
250
+ return `${formatDuration(segment.duration)} @ ${segment.power}% FTP`;
251
+ }
252
+ if (segment.type === "ramp") {
253
+ return `${formatDuration(segment.duration)} ${segment.startPower}→${segment.endPower}% FTP`;
254
+ }
255
+ const { sets, reps, workDuration, recoveryDuration, restBetweenSets, workPower } = segment;
256
+ // "3x13x30/15" — sets × reps × work/recovery, the standard notation.
257
+ const core = sets > 1 ? `${sets}x${reps}x` : `${reps}x`;
258
+ const rest = restBetweenSets > 0 && sets > 1 ? ` + ${Math.round(restBetweenSets / 60)}' rest` : "";
259
+ return `${core}${workDuration}/${recoveryDuration} @ ${workPower}% FTP${rest}`;
260
+ }
261
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/training/model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /** Training is platform-owned; games consume snapshots and actual hardware watts. */
2
+ import type { EngineState } from "./state.js";
3
+ import type { Workout } from "./model.js";
4
+ import type { FlatSegment } from "./compile.js";
5
+ /** Trainer preference, independent of game rules and power normalization. */
6
+ export type EffortMode = "steady" | "dynamic";
7
+ /** Complete immutable plan, sent on attachment or revision, never every tick. */
8
+ export interface WorkoutPlan {
9
+ id: string;
10
+ revision: number;
11
+ workout: Workout;
12
+ timeline: readonly FlatSegment[];
13
+ }
14
+ /** Live authoritative position. Seconds use the platform's single workout clock. */
15
+ export type WorkoutProgress = Omit<EngineState, "workout" | "timeline">;
16
+ /** Atomic state: a null workout means no structured training is attached. */
17
+ export interface TrainingSnapshot {
18
+ effort: EffortMode;
19
+ baseTargetWatts: number;
20
+ ergActive: boolean;
21
+ workout: {
22
+ plan: WorkoutPlan;
23
+ progress: WorkoutProgress;
24
+ } | null;
25
+ }
26
+ /** Transport omits unchanged plans; revision guards reject stale progress. */
27
+ export interface TrainingUpdate {
28
+ effort: EffortMode;
29
+ baseTargetWatts: number;
30
+ ergActive: boolean;
31
+ workout: {
32
+ id: string;
33
+ revision: number;
34
+ plan?: WorkoutPlan;
35
+ progress: WorkoutProgress;
36
+ } | null;
37
+ }
38
+ /** Read-only workout state plus a game's chosen effort preference. */
39
+ export interface TrainingSession {
40
+ readonly supported: boolean;
41
+ readonly current: TrainingSnapshot | null;
42
+ subscribe(listener: (state: TrainingSnapshot) => void): () => void;
43
+ setEffort(mode: EffortMode): void;
44
+ }
45
+ export interface TrainingSource {
46
+ current(): TrainingSnapshot;
47
+ subscribe(listener: (state: TrainingSnapshot) => void): () => void;
48
+ setEffort(mode: EffortMode): void;
49
+ }
50
+ export interface TrainingStateMessage {
51
+ rydr: true;
52
+ type: "rydr/training.state";
53
+ state: TrainingUpdate;
54
+ }
55
+ export interface TrainingEffortMessage {
56
+ rydr: true;
57
+ type: "rydr/training.effort";
58
+ mode: EffortMode;
59
+ }
60
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/training/protocol.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC9C,iFAAiF;AACjF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;CAClC;AACD,oFAAoF;AACpF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC;AACxE,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,eAAe,CAAA;KAAE,GAAG,IAAI,CAAC;CAClE;AACD,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,eAAe,CAAA;KAAE,GAAG,IAAI,CAAC;CACjG;AACD,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnE,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;CACnC;AACD,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,gBAAgB,CAAC;IAC5B,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnE,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;CACnC;AACD,MAAM,WAAW,oBAAoB;IAAG,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE;AACxG,MAAM,WAAW,qBAAqB;IAAG,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,sBAAsB,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/training/protocol.ts"],"names":[],"mappings":""}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The engine's vocabulary: its phases, the numbers it measures, and the events
3
+ * it emits. Deliberately free of anything to do with rendering — see `engine.ts`
4
+ * on why the engine has no idea what's drawing it.
5
+ */
6
+ import type { FlatSegment } from "./compile.js";
7
+ import type { Workout } from "./model.js";
8
+ /**
9
+ * Where the ride is in its lifecycle.
10
+ *
11
+ * - `ready` — compiled and waiting for the rider's first pedal stroke.
12
+ * - `active` — running.
13
+ * - `paused` — stopped, by choice or because the rider stopped pedalling.
14
+ * - `cooldown` — the timeline is finished, the rider keeps spinning easy. Not
15
+ * the end: they decide when to end the ride, so a cool-down is never cut off
16
+ * by a clock.
17
+ * - `complete` — finished and scored. Terminal.
18
+ */
19
+ export type EnginePhase = "ready" | "active" | "paused" | "cooldown" | "complete";
20
+ /** Why the ride is paused. `stopped` is the engine's own doing, not the rider's. */
21
+ export type PauseReason = "rider" | "stopped";
22
+ /** What the rider actually did in one effort, against what was asked. */
23
+ export interface EffortResult {
24
+ /** Index into the compiled timeline. */
25
+ index: number;
26
+ set: number;
27
+ rep: number;
28
+ /** What was asked, watts. */
29
+ targetPower: number;
30
+ /** What was produced, watts. */
31
+ averagePower: number;
32
+ peakPower: number;
33
+ /** Seconds actually ridden — less than planned if skipped, more if extended. */
34
+ duration: number;
35
+ /** Share of the effort spent inside the target band, 0–100. */
36
+ timeInZonePercent: number;
37
+ /** How it went. See `classifyEffort`. */
38
+ quality: EffortQuality;
39
+ /** bpm at the start of the effort, 0 without a monitor. */
40
+ startHeartRate: number;
41
+ /** bpm at the end, 0 without a monitor. */
42
+ endHeartRate: number;
43
+ /** True when the rider jumped past this effort rather than riding it. */
44
+ skipped: boolean;
45
+ }
46
+ /**
47
+ * How an effort went, relative to the target band.
48
+ *
49
+ * Four levels rather than pass/fail because "just under" and "nowhere near" are
50
+ * different sessions, and a rider deciding whether to add a set needs to know
51
+ * which one they're having.
52
+ */
53
+ export type EffortQuality = "excellent" | "onTarget" | "close" | "missed";
54
+ /** Power and heart rate over one timeline entry, for the results breakdown. */
55
+ export interface SegmentStats {
56
+ index: number;
57
+ averagePower: number;
58
+ peakPower: number;
59
+ averageHeartRate: number;
60
+ peakHeartRate: number;
61
+ sampleCount: number;
62
+ }
63
+ /** A stretch of timeline the rider jumped over, so the chart can show the gap. */
64
+ export interface SkipZone {
65
+ from: number;
66
+ to: number;
67
+ }
68
+ /** An FTP change made mid-ride, and where on the timeline it happened. */
69
+ export interface FtpChange {
70
+ ftp: number;
71
+ atPosition: number;
72
+ }
73
+ /**
74
+ * Everything a renderer needs, as one immutable snapshot per frame.
75
+ *
76
+ * A single object rather than a set of getters so a renderer can't read half of
77
+ * one frame and half of the next, and so a future 3D view gets exactly the same
78
+ * information as the chart with no new engine API.
79
+ */
80
+ export interface EngineState {
81
+ phase: EnginePhase;
82
+ pauseReason: PauseReason | null;
83
+ /** Position on the workout timeline, seconds. This drives the chart cursor. */
84
+ position: number;
85
+ /** Wall-clock seconds since the ride began — excludes skipped time. */
86
+ elapsed: number;
87
+ /** Total seconds of the compiled timeline. */
88
+ totalDuration: number;
89
+ /** Seconds left in the current entry. */
90
+ segmentRemaining: number;
91
+ /** Seconds left in the whole workout. */
92
+ totalRemaining: number;
93
+ /** What the rider should be doing right now, watts. Correct in every phase. */
94
+ targetPower: number;
95
+ /** Whether the boost portion of a fast-start effort is live. */
96
+ inFastStart: boolean;
97
+ segment: FlatSegment | null;
98
+ /** The next entry, so a renderer can warn what's coming. */
99
+ nextSegment: FlatSegment | null;
100
+ timeline: readonly FlatSegment[];
101
+ /** 1-based, 0 outside an interval set. */
102
+ set: number;
103
+ /** 1-based, 0 outside an interval set. */
104
+ rep: number;
105
+ /** Total efforts in the workout, for "rep 12 of 39". */
106
+ totalReps: number;
107
+ /** Mean power so far in the current entry, watts. */
108
+ segmentAveragePower: number;
109
+ /** Share of the current entry spent in the target band, 0–100. */
110
+ segmentTimeInZone: number;
111
+ results: readonly EffortResult[];
112
+ segmentStats: readonly SegmentStats[];
113
+ skipZones: readonly SkipZone[];
114
+ /** Whether the game currently holds the trainer in ERG. */
115
+ ergActive: boolean;
116
+ /** Whether the trainer could do ERG at all. */
117
+ ergAvailable: boolean;
118
+ /** The FTP the timeline is compiled against — the rider can change it mid-ride. */
119
+ ftp: number;
120
+ ftpHistory: readonly FtpChange[];
121
+ /** Seconds spent in cool-down, once the timeline has finished. */
122
+ cooldownElapsed: number;
123
+ workout: Workout;
124
+ }
125
+ /** The finished ride, as the results screen and the run record see it. */
126
+ export interface RideSummary {
127
+ workout: Workout;
128
+ ftp: number;
129
+ /** Seconds actually ridden. */
130
+ elapsed: number;
131
+ results: readonly EffortResult[];
132
+ segmentStats: readonly SegmentStats[];
133
+ /** Mean power across the ride, watts. */
134
+ averagePower: number;
135
+ /** Normalized power of what was ridden, watts. */
136
+ normalizedPower: number;
137
+ intensityFactor: number;
138
+ tss: number;
139
+ kilojoules: number;
140
+ averageHeartRate: number;
141
+ peakHeartRate: number;
142
+ peakPower: number;
143
+ averageCadence: number;
144
+ /** Share of the whole ride spent inside the target band, 0–100. */
145
+ compliancePercent: number;
146
+ /** How many of the planned efforts were actually ridden. */
147
+ effortsCompleted: number;
148
+ effortsPlanned: number;
149
+ }
150
+ /** One effort finished — for a cue, a chart mark, a readout. */
151
+ export interface EffortCompleteEvent {
152
+ result: EffortResult;
153
+ /** Last effort of its set. */
154
+ lastInSet: boolean;
155
+ /** Last effort of the whole workout. */
156
+ lastInWorkout: boolean;
157
+ }
158
+ /**
159
+ * What the engine announces. A renderer subscribes to what it cares about and
160
+ * reads {@link EngineState} for everything else.
161
+ */
162
+ export interface EngineEvents {
163
+ /** Every frame. The bread and butter of any renderer. */
164
+ tick: EngineState;
165
+ /** The phase changed — worth a screen change, not just a repaint. */
166
+ phase: EngineState;
167
+ /** The timeline moved to a new entry. */
168
+ segment: EngineState;
169
+ effort: EffortCompleteEvent;
170
+ /** The timeline ran out; the rider is spinning down and hasn't ended yet. */
171
+ finished: EngineState;
172
+ /** The rider ended the ride. Terminal — the summary is final. */
173
+ complete: RideSummary;
174
+ }
175
+ export type EngineEventName = keyof EngineEvents;
176
+ /**
177
+ * How far off target still counts as on target: 5% of the target, but never
178
+ * tighter than 15 W.
179
+ *
180
+ * The floor is what makes this usable. At a 100 W recovery target a pure 5% band
181
+ * is ±5 W, which no human holds and no trainer reports that precisely, so a rider
182
+ * spinning perfectly well would be scored as missing. Carried over from the
183
+ * legacy app, where it was tuned against real rides.
184
+ */
185
+ export declare function zoneHalfWidth(targetPower: number): number;
186
+ export declare function zoneBounds(targetPower: number): {
187
+ lower: number;
188
+ upper: number;
189
+ };
190
+ export declare function isInZone(power: number, targetPower: number): boolean;
191
+ /**
192
+ * Grade an effort by its average power.
193
+ *
194
+ * Over the band is `excellent` rather than an error: this app prescribes a target
195
+ * but doesn't police it, and a rider who went harder did more work, not the wrong
196
+ * work. `close` reaches one further band-width below, which is the difference
197
+ * between fading at the end of a rep and abandoning it.
198
+ */
199
+ export declare function classifyEffort(averagePower: number, targetPower: number): EffortQuality;
200
+ export declare const QUALITY_LABELS: Record<EffortQuality, string>;
201
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/training/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The engine's vocabulary: its phases, the numbers it measures, and the events
3
+ * it emits. Deliberately free of anything to do with rendering — see `engine.ts`
4
+ * on why the engine has no idea what's drawing it.
5
+ */
6
+ // ============================================================================
7
+ // The target band
8
+ // ============================================================================
9
+ /**
10
+ * How far off target still counts as on target: 5% of the target, but never
11
+ * tighter than 15 W.
12
+ *
13
+ * The floor is what makes this usable. At a 100 W recovery target a pure 5% band
14
+ * is ±5 W, which no human holds and no trainer reports that precisely, so a rider
15
+ * spinning perfectly well would be scored as missing. Carried over from the
16
+ * legacy app, where it was tuned against real rides.
17
+ */
18
+ export function zoneHalfWidth(targetPower) {
19
+ return Math.max(targetPower * 0.05, 15);
20
+ }
21
+ export function zoneBounds(targetPower) {
22
+ const half = zoneHalfWidth(targetPower);
23
+ return { lower: Math.round(targetPower - half), upper: Math.round(targetPower + half) };
24
+ }
25
+ export function isInZone(power, targetPower) {
26
+ const { lower, upper } = zoneBounds(targetPower);
27
+ return power >= lower && power <= upper;
28
+ }
29
+ /**
30
+ * Grade an effort by its average power.
31
+ *
32
+ * Over the band is `excellent` rather than an error: this app prescribes a target
33
+ * but doesn't police it, and a rider who went harder did more work, not the wrong
34
+ * work. `close` reaches one further band-width below, which is the difference
35
+ * between fading at the end of a rep and abandoning it.
36
+ */
37
+ export function classifyEffort(averagePower, targetPower) {
38
+ if (targetPower <= 0)
39
+ return "onTarget";
40
+ const { lower, upper } = zoneBounds(targetPower);
41
+ if (averagePower > upper)
42
+ return "excellent";
43
+ if (averagePower >= lower)
44
+ return "onTarget";
45
+ if (averagePower >= targetPower - zoneHalfWidth(targetPower) * 2)
46
+ return "close";
47
+ return "missed";
48
+ }
49
+ export const QUALITY_LABELS = {
50
+ excellent: "Over target",
51
+ onTarget: "On target",
52
+ close: "Just under",
53
+ missed: "Missed",
54
+ };
55
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/training/state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmNH,+EAA+E;AAC/E,kBAAkB;AAClB,+EAA+E;AAE/E;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,WAAmB;IAC/C,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,WAAmB;IAC5C,MAAM,IAAI,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;IACxC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,WAAmB;IACzD,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACjD,OAAO,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,CAAC;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,YAAoB,EAAE,WAAmB;IACtE,IAAI,WAAW,IAAI,CAAC;QAAE,OAAO,UAAU,CAAC;IACxC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACjD,IAAI,YAAY,GAAG,KAAK;QAAE,OAAO,WAAW,CAAC;IAC7C,IAAI,YAAY,IAAI,KAAK;QAAE,OAAO,UAAU,CAAC;IAC7C,IAAI,YAAY,IAAI,WAAW,GAAG,aAAa,CAAC,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IACjF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAkC;IAC3D,SAAS,EAAE,aAAa;IACxB,QAAQ,EAAE,WAAW;IACrB,KAAK,EAAE,YAAY;IACnB,MAAM,EAAE,QAAQ;CACjB,CAAC"}
@@ -0,0 +1,50 @@
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
+ * difficulty level — is a different thing entirely and comes from
8
+ * `@rydr/game-sdk/difficulty`; 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 interface PowerZone {
15
+ /** 1–7. */
16
+ zone: number;
17
+ /** "Threshold". */
18
+ name: string;
19
+ /** "Z4". */
20
+ shortName: string;
21
+ /** Lower bound as a fraction of FTP, inclusive. 0 for Z1. */
22
+ minFtp: number;
23
+ /** Upper bound as a fraction of FTP, exclusive. Infinity for Z7. */
24
+ maxFtp: number;
25
+ /** Solid colour — strokes, text, badges. */
26
+ color: string;
27
+ /** Translucent variant — fills and backgrounds. */
28
+ colorLight: string;
29
+ }
30
+ export declare const POWER_ZONES: readonly PowerZone[];
31
+ /**
32
+ * The zone for a power expressed as a **percentage of FTP** (120 = 120% FTP).
33
+ *
34
+ * Prefer this over {@link zoneForWatts} wherever the authored percentage is still
35
+ * in hand — a segment's colour then can't drift when the rider's FTP changes, and
36
+ * two segments authored at the same percentage always draw identically.
37
+ */
38
+ export declare function zoneForPercent(percent: number): PowerZone;
39
+ /** The zone for a live power reading in watts against an FTP in watts. */
40
+ export declare function zoneForWatts(watts: number, ftp: number): PowerZone;
41
+ /**
42
+ * The zone with a given number, 1–7.
43
+ *
44
+ * For the places that already hold a zone *number* — a time-in-zone tally, a
45
+ * legend row — and need its colour or name back. Without this they end up
46
+ * re-listing the colours locally, which is how a chart and its own legend come to
47
+ * disagree.
48
+ */
49
+ export declare function zoneByNumber(zone: number): PowerZone;
50
+ //# sourceMappingURL=zones.d.ts.map