@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,367 @@
1
+ /**
2
+ * Compiling a workout: authored segments → a flat, time-addressable timeline.
3
+ *
4
+ * This is where percentages of FTP finally become watts, and it happens **once**,
5
+ * at the start of a ride (and again if the rider moves their difficulty dial
6
+ * mid-session). Everything downstream — the engine's clock, the chart, the ERG
7
+ * target — then addresses the timeline by *time* rather than re-walking the
8
+ * segment tree, which is what keeps a 3x13x30/15 workout (78 efforts, 157
9
+ * timeline entries) cheap to run at 60 fps.
10
+ *
11
+ * Ported from the legacy RYDR app, where this expansion had been duplicated in
12
+ * four places — the HUD, the game entry point, the image generator and the
13
+ * standalone HUD page — and they disagreed. One compiler, one answer.
14
+ */
15
+ import { zoneForPercent } from "./zones.js";
16
+ // ============================================================================
17
+ // Watts
18
+ // ============================================================================
19
+ /**
20
+ * A smart trainer's real control resolution is 1 W on paper but ~5 W in practice,
21
+ * and a target of "213 W" invites a precision the hardware doesn't have. Rounding
22
+ * here rather than at display time also means the number the rider reads is
23
+ * exactly the number sent to the trainer.
24
+ */
25
+ export function resolvePower(percentOfFtp, ftp) {
26
+ return Math.round(((percentOfFtp / 100) * ftp) / 5) * 5;
27
+ }
28
+ // ============================================================================
29
+ // Fast start
30
+ // ============================================================================
31
+ /**
32
+ * "Fast start" opens an effort above its target and settles back, so the rider
33
+ * reaches VO2max sooner in each rep — the Rønnestad protocol's key detail. The
34
+ * boost is paid back over the remainder, so the effort's *average* is unchanged
35
+ * and the workout's overall load is identical either way.
36
+ */
37
+ const FAST_START_MULTIPLIER = 1.2;
38
+ const FAST_START_MAX_S = 60;
39
+ /** The boost covers at most this fraction of the effort. */
40
+ const FAST_START_FRACTION = 1 / 3;
41
+ /**
42
+ * Open an effort above its target and pay the boost back over the remainder.
43
+ *
44
+ * Mutates the entry, and applies to **any** `work` entry — one fanned out of an
45
+ * interval set, or one placed by hand on the timeline editor. It used to live
46
+ * inside the interval-set branch, which meant flattening a Rønnestad session
47
+ * silently dropped the one detail the protocol is *for*.
48
+ *
49
+ * Deliberately not applied to a ramp: a ramp already declares the power shape
50
+ * its author wanted, and boosting its opening would fight that curve rather
51
+ * than serve it.
52
+ */
53
+ function applyFastStart(entry, target) {
54
+ if (entry.duration <= 0)
55
+ return;
56
+ const boostDuration = Math.min(entry.duration * FAST_START_FRACTION, FAST_START_MAX_S);
57
+ const boostPower = Math.round(target * FAST_START_MULTIPLIER);
58
+ const remainder = entry.duration - boostDuration;
59
+ if (remainder <= 0)
60
+ return;
61
+ entry.fastStartPower = boostPower;
62
+ entry.fastStartDuration = boostDuration;
63
+ // Pay the boost back so the effort still averages `target`.
64
+ entry.power = Math.round((target * entry.duration - boostPower * boostDuration) / remainder);
65
+ }
66
+ /** At or above this fraction of FTP, a non-rep segment counts as hard work. */
67
+ export const HARD_WORK_THRESHOLD = 0.75;
68
+ /**
69
+ * Expand authored segments into the timeline.
70
+ *
71
+ * A fixed zone becomes one `steady` entry and a ramp becomes one `steady` entry
72
+ * carrying its endpoints; an interval set fans out into a `work` and a `recovery`
73
+ * entry per rep, with a `rest` entry between sets.
74
+ */
75
+ export function compileWorkout(segments, ftp, options) {
76
+ const flat = [];
77
+ let t = 0;
78
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
79
+ const seg = segments[segmentIndex];
80
+ if (seg.type === "fixed") {
81
+ const power = resolvePower(seg.power, ftp);
82
+ const entry = {
83
+ // A block placed by hand says what it is; one authored as a plain steady
84
+ // block, and every workout written before the timeline editor existed,
85
+ // says nothing and is steady.
86
+ type: seg.phase ?? "steady",
87
+ duration: seg.duration,
88
+ power,
89
+ startTime: t,
90
+ endTime: t + seg.duration,
91
+ segmentIndex,
92
+ powerPercent: seg.power,
93
+ hardWork: power >= ftp * HARD_WORK_THRESHOLD,
94
+ };
95
+ if (entry.type === "work") {
96
+ entry.hardWork = true;
97
+ if (options?.fastStart)
98
+ applyFastStart(entry, power);
99
+ }
100
+ flat.push(entry);
101
+ t += seg.duration;
102
+ continue;
103
+ }
104
+ if (seg.type === "ramp") {
105
+ const rampStartPower = resolvePower(seg.startPower, ftp);
106
+ const rampEndPower = resolvePower(seg.endPower, ftp);
107
+ // `power` is the ramp's mean, so anything reading the field without
108
+ // understanding ramps (a summary, a coarse chart) still gets a fair number.
109
+ const power = Math.round((rampStartPower + rampEndPower) / 2);
110
+ flat.push({
111
+ type: seg.phase ?? "steady",
112
+ duration: seg.duration,
113
+ power,
114
+ startTime: t,
115
+ endTime: t + seg.duration,
116
+ segmentIndex,
117
+ rampStartPower,
118
+ rampEndPower,
119
+ powerPercent: (seg.startPower + seg.endPower) / 2,
120
+ hardWork: seg.phase === "work" || power >= ftp * HARD_WORK_THRESHOLD,
121
+ });
122
+ t += seg.duration;
123
+ continue;
124
+ }
125
+ const workPower = resolvePower(seg.workPower, ftp);
126
+ const recoveryPower = resolvePower(seg.recoveryPower, ftp);
127
+ const restPower = resolvePower(seg.restPower, ftp);
128
+ for (let set = 1; set <= seg.sets; set++) {
129
+ for (let rep = 1; rep <= seg.reps; rep++) {
130
+ const work = {
131
+ type: "work",
132
+ duration: seg.workDuration,
133
+ power: workPower,
134
+ startTime: t,
135
+ endTime: t + seg.workDuration,
136
+ segmentIndex,
137
+ set,
138
+ rep,
139
+ powerPercent: seg.workPower,
140
+ hardWork: true,
141
+ };
142
+ if (options?.fastStart)
143
+ applyFastStart(work, workPower);
144
+ flat.push(work);
145
+ t += seg.workDuration;
146
+ // A rep can have no recovery (a set of one long effort), and a zero-length
147
+ // entry would be an un-addressable hole in the timeline.
148
+ if (seg.recoveryDuration > 0) {
149
+ flat.push({
150
+ type: "recovery",
151
+ duration: seg.recoveryDuration,
152
+ power: recoveryPower,
153
+ startTime: t,
154
+ endTime: t + seg.recoveryDuration,
155
+ segmentIndex,
156
+ set,
157
+ rep,
158
+ powerPercent: seg.recoveryPower,
159
+ hardWork: recoveryPower >= ftp * HARD_WORK_THRESHOLD,
160
+ });
161
+ t += seg.recoveryDuration;
162
+ }
163
+ }
164
+ // Rest goes BETWEEN sets — never after the last one, which is the workout ending.
165
+ if (set < seg.sets && seg.restBetweenSets > 0) {
166
+ flat.push({
167
+ type: "rest",
168
+ duration: seg.restBetweenSets,
169
+ power: restPower,
170
+ startTime: t,
171
+ endTime: t + seg.restBetweenSets,
172
+ segmentIndex,
173
+ set,
174
+ powerPercent: seg.restPower,
175
+ hardWork: restPower >= ftp * HARD_WORK_THRESHOLD,
176
+ });
177
+ t += seg.restBetweenSets;
178
+ }
179
+ }
180
+ }
181
+ numberReps(flat);
182
+ return flat;
183
+ }
184
+ /**
185
+ * Give hand-placed efforts their set and rep numbers.
186
+ *
187
+ * An interval set numbers its own on the way out — it knows it is set 2 of 3.
188
+ * A block dragged onto the timeline knows only that it is an effort, so the
189
+ * numbering has to be read back off the sequence: each `work` entry is the next
190
+ * rep, and a `rest` entry closes the set it followed.
191
+ *
192
+ * That derivation is what keeps the rep wall and the interval countdown working
193
+ * on a flattened workout. It runs over the whole timeline in one pass and only
194
+ * ever *fills in* — an entry that already carries a number keeps it, and its
195
+ * numbers seed the counters, so a workout mixing an authored interval set with
196
+ * hand-placed blocks after it goes on counting from where the set left off
197
+ * rather than restarting at rep 1.
198
+ */
199
+ function numberReps(flat) {
200
+ let set = 1;
201
+ let rep = 0;
202
+ for (const entry of flat) {
203
+ if (entry.set !== undefined) {
204
+ // Authored by an interval set: adopt its numbering rather than imposing one.
205
+ set = entry.set;
206
+ rep = entry.rep ?? rep;
207
+ // A rest emitted by a set closes it, exactly as it does below.
208
+ if (entry.type === "rest") {
209
+ set += 1;
210
+ rep = 0;
211
+ }
212
+ continue;
213
+ }
214
+ if (entry.type === "work") {
215
+ rep += 1;
216
+ entry.set = set;
217
+ entry.rep = rep;
218
+ }
219
+ else if (entry.type === "recovery") {
220
+ // The recovery belongs to the effort it follows, so it takes that rep's
221
+ // number — and a recovery with no effort before it takes rep 1, which is
222
+ // the only honest answer for a workout that opens with one.
223
+ entry.set = set;
224
+ entry.rep = Math.max(1, rep);
225
+ }
226
+ else if (entry.type === "rest") {
227
+ entry.set = set;
228
+ set += 1;
229
+ rep = 0;
230
+ }
231
+ }
232
+ }
233
+ /** Total seconds of a compiled timeline. */
234
+ export function timelineDuration(timeline) {
235
+ return timeline.length === 0 ? 0 : timeline[timeline.length - 1].endTime;
236
+ }
237
+ /**
238
+ * The entry containing `timeSec`, or null past the end.
239
+ *
240
+ * Binary search rather than the legacy linear scan: this is called every frame,
241
+ * and a Rønnestad set alone compiles to 78 efforts.
242
+ */
243
+ export function segmentAtTime(timeline, timeSec) {
244
+ let lo = 0;
245
+ let hi = timeline.length - 1;
246
+ while (lo <= hi) {
247
+ const mid = (lo + hi) >> 1;
248
+ const seg = timeline[mid];
249
+ if (timeSec < seg.startTime)
250
+ hi = mid - 1;
251
+ else if (timeSec >= seg.endTime)
252
+ lo = mid + 1;
253
+ else
254
+ return seg;
255
+ }
256
+ return null;
257
+ }
258
+ /**
259
+ * The instantaneous target in watts at `timeSec` — the number that goes to the
260
+ * trainer and onto the screen.
261
+ *
262
+ * Unlike `FlatSegment.power` this resolves the two cases where a segment's target
263
+ * moves *within* it: a ramp interpolates between its endpoints, and a fast-start
264
+ * effort holds its boost before settling. Returns 0 past the end of the workout.
265
+ */
266
+ export function targetPowerAt(timeline, timeSec) {
267
+ const seg = segmentAtTime(timeline, timeSec);
268
+ if (!seg)
269
+ return 0;
270
+ return targetPowerIn(seg, timeSec);
271
+ }
272
+ /** The target within a known segment — the hot path, when the caller already has it. */
273
+ export function targetPowerIn(seg, timeSec) {
274
+ if (seg.rampStartPower !== undefined && seg.rampEndPower !== undefined) {
275
+ const progress = seg.duration > 0 ? (timeSec - seg.startTime) / seg.duration : 0;
276
+ const clamped = Math.min(1, Math.max(0, progress));
277
+ return Math.round(seg.rampStartPower + (seg.rampEndPower - seg.rampStartPower) * clamped);
278
+ }
279
+ if (seg.fastStartPower !== undefined &&
280
+ seg.fastStartDuration !== undefined &&
281
+ timeSec < seg.startTime + seg.fastStartDuration) {
282
+ return seg.fastStartPower;
283
+ }
284
+ return seg.power;
285
+ }
286
+ /**
287
+ * What the planned session will cost, computed from the target trace.
288
+ *
289
+ * Normalized power follows the real definition — sample at 1 Hz, take a 30-second
290
+ * rolling mean, then the fourth root of the mean fourth power — rather than the
291
+ * shortcut of averaging the targets. The distinction matters exactly where this
292
+ * app is aimed: a 3x13x30/15 at 130% FTP and a steady block at the same *average*
293
+ * are wildly different sessions, and only the rolling mean says so.
294
+ *
295
+ * These are of course the numbers for riding the workout *perfectly*. What the
296
+ * rider actually did is measured by the engine and reported on the results screen.
297
+ */
298
+ export function workoutMetrics(workout, ftp) {
299
+ const timeline = compileWorkout(workout.segments, ftp);
300
+ const duration = Math.round(timelineDuration(timeline));
301
+ const intervals = timeline.filter((seg) => seg.type === "work").length;
302
+ const empty = {
303
+ duration: 0,
304
+ intervals: 0,
305
+ averageIntensity: 0,
306
+ peakIntensity: 0,
307
+ normalizedPower: 0,
308
+ intensityFactor: 0,
309
+ tss: 0,
310
+ kilojoules: 0,
311
+ timeInZone: {},
312
+ };
313
+ if (duration <= 0 || ftp <= 0)
314
+ return { ...empty, intervals };
315
+ // 1 Hz target trace. A workout is minutes-to-hours long, so this is at most a
316
+ // few thousand numbers — cheaper than being clever about it.
317
+ const trace = new Float64Array(duration);
318
+ for (let t = 0; t < duration; t++)
319
+ trace[t] = targetPowerAt(timeline, t + 0.5);
320
+ let sum = 0;
321
+ let peak = 0;
322
+ for (let t = 0; t < duration; t++) {
323
+ sum += trace[t];
324
+ if (trace[t] > peak)
325
+ peak = trace[t];
326
+ }
327
+ // 30-second rolling mean, then the fourth-power mean of that.
328
+ const window = 30;
329
+ let rollingSum = 0;
330
+ let fourthSum = 0;
331
+ let counted = 0;
332
+ for (let t = 0; t < duration; t++) {
333
+ rollingSum += trace[t];
334
+ if (t >= window)
335
+ rollingSum -= trace[t - window];
336
+ // Skip the first partial window: it would divide a short sum by a full window
337
+ // and understate the opening. A workout shorter than the window has nothing
338
+ // to roll, so it falls back to its own mean.
339
+ if (t >= window - 1) {
340
+ const rolled = rollingSum / window;
341
+ fourthSum += rolled ** 4;
342
+ counted++;
343
+ }
344
+ }
345
+ const normalizedPower = counted > 0 ? Math.round((fourthSum / counted) ** 0.25) : Math.round(sum / duration);
346
+ const intensityFactor = normalizedPower / ftp;
347
+ // The canonical TSS formula: an hour at FTP is exactly 100.
348
+ const tss = (duration * normalizedPower * intensityFactor) / (ftp * 3600) * 100;
349
+ const timeInZone = {};
350
+ for (const seg of timeline) {
351
+ // Zone from the authored percentage, so a segment can't change zone as FTP moves.
352
+ const zone = zoneForPercent(seg.powerPercent).zone;
353
+ timeInZone[zone] = (timeInZone[zone] ?? 0) + seg.duration;
354
+ }
355
+ return {
356
+ duration,
357
+ intervals,
358
+ averageIntensity: sum / duration / ftp,
359
+ peakIntensity: peak / ftp,
360
+ normalizedPower,
361
+ intensityFactor,
362
+ tss: Math.round(tss),
363
+ kilojoules: Math.round(sum / 1000),
364
+ timeInZone,
365
+ };
366
+ }
367
+ //# sourceMappingURL=compile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/training/compile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export * from "./model.js";
2
+ export * from "./compile.js";
3
+ export * from "./zones.js";
4
+ export * from "./state.js";
5
+ export * from "./protocol.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/training/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
@@ -0,0 +1,6 @@
1
+ export * from "./model.js";
2
+ export * from "./compile.js";
3
+ export * from "./zones.js";
4
+ export * from "./state.js";
5
+ export * from "./protocol.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/training/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
@@ -0,0 +1,252 @@
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
+ /**
15
+ * What the session is *for*. Purely descriptive — it drives grouping and filtering
16
+ * in the library, never the execution. (The legacy app also used categories to
17
+ * decide which workouts could validate an FTP estimate; that belonged to its
18
+ * progression system and has no counterpart here.)
19
+ */
20
+ export type WorkoutCategory = "recovery" | "endurance" | "tempo" | "sweetSpot" | "threshold" | "aerobicPower" | "shortBursts" | "sprint";
21
+ export declare const CATEGORY_LABELS: Record<WorkoutCategory, string>;
22
+ /**
23
+ * Which power zone each category *reads* as, for tinting a pill or a badge.
24
+ *
25
+ * Purely presentational — a category is descriptive (see above) and never drives
26
+ * execution, so this is only ever asked for a colour. It exists so a category
27
+ * pill and the chart underneath it agree about what "Threshold" looks like; the
28
+ * colour itself still comes from `zoneByNumber` in `./zones`, which stays the
29
+ * one place a zone colour is written down.
30
+ *
31
+ * Two of the eight straddle a boundary and are pinned to the lower zone, which
32
+ * is the legacy app's choice and worth keeping: sweet spot lives between Z3 and
33
+ * Z4, short bursts between Z5 and Z6, and picking the higher zone in each case
34
+ * made them read as harder than the sessions actually are.
35
+ */
36
+ export declare const CATEGORY_ZONE: Record<WorkoutCategory, number>;
37
+ /** Display order for pickers and filters — easiest to hardest. */
38
+ export declare const CATEGORY_ORDER: readonly WorkoutCategory[];
39
+ /**
40
+ * What a segment *is* to the rider, which is not the same as which authored
41
+ * segment it came from: a steady 20-minute block and the work half of a 40/20
42
+ * are both "hold this", but only one of them is a rep being counted.
43
+ *
44
+ * Lives here rather than in `compile.ts` (which re-exports it) because a
45
+ * hand-placed block now declares its own — see {@link SegmentBase.phase}.
46
+ */
47
+ export type PhaseType = "work" | "recovery" | "rest" | "steady";
48
+ interface SegmentBase {
49
+ /** Unique within its workout. */
50
+ id: string;
51
+ /**
52
+ * What this block IS, for a block placed by hand on the timeline editor.
53
+ *
54
+ * An {@link IntervalSetSegment} says `3 sets of 13 efforts` and the compiler
55
+ * derives the phases; a block dragged onto the timeline has no set to belong
56
+ * to, so it has to say for itself whether it is an effort, a recovery, a rest
57
+ * between sets, or just a stretch of riding.
58
+ *
59
+ * That matters because everything downstream — the efforts count, the rep
60
+ * wall, the interval countdown, fast start, the mashup's effort matching —
61
+ * reads the *compiled* entry's phase, never the authored segment. Without
62
+ * this, flattening `3x13x30/15` into 78 blocks would silently turn a
63
+ * Rønnestad session into 39 minutes of undifferentiated "steady".
64
+ *
65
+ * Optional, and absent means `"steady"`, so every workout authored before the
66
+ * timeline editor existed compiles exactly as it did. Ignored on an
67
+ * {@link IntervalSetSegment}, which numbers its own phases.
68
+ */
69
+ phase?: PhaseType;
70
+ }
71
+ /** A steady block: one power held for one duration. Maps to ZWO `<SteadyState>`. */
72
+ export interface FixedZoneSegment extends SegmentBase {
73
+ type: "fixed";
74
+ /** Seconds. */
75
+ duration: number;
76
+ /** % of FTP — 75 means 75% FTP. */
77
+ power: number;
78
+ }
79
+ /**
80
+ * A repeating interval set, the workhorse of structured training.
81
+ *
82
+ * Reads as `sets × reps × work/recovery`, with an optional rest between sets:
83
+ * `3x13x30/15 + 3' rest` is three sets of thirteen 30-second efforts with 15
84
+ * seconds easy between them, resting three minutes between sets. Maps to ZWO
85
+ * `<IntervalsT>` plus a `<SteadyState>` per rest.
86
+ */
87
+ export interface IntervalSetSegment extends SegmentBase {
88
+ type: "intervals";
89
+ sets: number;
90
+ /** Efforts per set. */
91
+ reps: number;
92
+ /** Seconds of work per rep. */
93
+ workDuration: number;
94
+ /** % of FTP during the effort. */
95
+ workPower: number;
96
+ /** Seconds of recovery between reps. */
97
+ recoveryDuration: number;
98
+ /** % of FTP during the recovery. */
99
+ recoveryPower: number;
100
+ /** Seconds of rest between sets; 0 for none. */
101
+ restBetweenSets: number;
102
+ /** % of FTP during the rest. */
103
+ restPower: number;
104
+ }
105
+ /** A linear power ramp. Maps to ZWO `<Warmup>` / `<Cooldown>` / `<Ramp>`. */
106
+ export interface RampSegment extends SegmentBase {
107
+ type: "ramp";
108
+ /** Seconds. */
109
+ duration: number;
110
+ /** % of FTP at the start. */
111
+ startPower: number;
112
+ /** % of FTP at the end. */
113
+ endPower: number;
114
+ }
115
+ export type WorkoutSegment = FixedZoneSegment | IntervalSetSegment | RampSegment;
116
+ export type SegmentKind = WorkoutSegment["type"];
117
+ /**
118
+ * A coach's cue attached to a point in time, e.g. from a ZWO `<textevent>`.
119
+ * Preserved on import and shown during the ride.
120
+ */
121
+ export interface TextEvent {
122
+ /** Seconds from the start of the workout. */
123
+ timeOffset: number;
124
+ message: string;
125
+ }
126
+ /**
127
+ * One song placed on a workout's timeline.
128
+ *
129
+ * The workout's own clock, in seconds — the same axis the segments sit on, which
130
+ * is the entire point: a song placed at 12:00 is heard at 12:00 of the ride, so
131
+ * an author can put a drop on rep 1 and have it land there.
132
+ *
133
+ * Referenced by {@link MusicTrack.id} rather than by URL. The library is curated
134
+ * and mutable — a track can be re-trimmed, re-uploaded, retitled — and a lane
135
+ * that stored the URL would go on pointing at bytes that had been replaced.
136
+ * The cost is that a deleted track leaves a hole, which `normalizeLane` drops.
137
+ */
138
+ export interface PlacedTrack {
139
+ /** Unique within the lane. The same song may be placed more than once. */
140
+ id: string;
141
+ /** → {@link MusicTrack.id}. */
142
+ trackId: string;
143
+ /** Seconds from the start of the workout. */
144
+ startTime: number;
145
+ /**
146
+ * Where in the file to start, in seconds. Defaults to 0.
147
+ *
148
+ * Rarely needed: songs are cropped destructively in the music editor (see
149
+ * `src/music/trim.ts`), so a track in the library is usually already exactly
150
+ * the part that was wanted. This is for the last-mile nudge — dropping four
151
+ * bars of intro so the first beat lands on the block boundary — without
152
+ * re-cutting the file for every workout that uses it.
153
+ */
154
+ clipStartSec?: number;
155
+ /** Seconds to play. Defaults to the rest of the file after `clipStartSec`. */
156
+ duration?: number;
157
+ }
158
+ /**
159
+ * The songs a workout plays, and when.
160
+ *
161
+ * An object rather than a bare array so the lane can grow a property later (a
162
+ * per-workout crossfade, a ducking rule) without every stored document needing
163
+ * a migration.
164
+ *
165
+ * A workout with no `music` is not silent — it falls back to the rider's chosen
166
+ * style, shuffled on the wall clock (`src/music/playlist.ts`, WORK-10). This
167
+ * field is what "somebody scored this session by hand" looks like.
168
+ */
169
+ export interface WorkoutMusic {
170
+ tracks: PlacedTrack[];
171
+ }
172
+ /** Who can see a workout — mirrors the SDK data-store scope it's saved in. */
173
+ export type WorkoutVisibility = "preset" | "private" | "shared";
174
+ export interface Workout {
175
+ id: string;
176
+ name: string;
177
+ description: string;
178
+ segments: WorkoutSegment[];
179
+ category: WorkoutCategory;
180
+ /** Unix ms. */
181
+ createdAt: number;
182
+ /** Unix ms. */
183
+ updatedAt: number;
184
+ /** Cues carried over from an imported file. */
185
+ textEvents?: TextEvent[];
186
+ /**
187
+ * The songs this session was scored with, placed on its own clock.
188
+ *
189
+ * Absent for every workout that wasn't authored in the timeline editor, and
190
+ * absent is a perfectly good answer — see {@link WorkoutMusic}.
191
+ */
192
+ music?: WorkoutMusic;
193
+ }
194
+ /** A workout as it comes back from the store, with where it came from attached. */
195
+ export interface StoredWorkout extends Workout {
196
+ visibility: WorkoutVisibility;
197
+ /** Display name of the rider who wrote it — only meaningful for `shared`. */
198
+ authorName?: string;
199
+ /** True when the signed-in rider owns it, and so may edit or delete it. */
200
+ isMine: boolean;
201
+ }
202
+ export declare function generateSegmentId(): string;
203
+ export declare function generatePlacedTrackId(): string;
204
+ export declare function generateWorkoutId(): string;
205
+ export declare function createFixedZoneSegment(overrides?: Partial<Omit<FixedZoneSegment, "type" | "id">>): FixedZoneSegment;
206
+ export declare function createIntervalSetSegment(overrides?: Partial<Omit<IntervalSetSegment, "type" | "id">>): IntervalSetSegment;
207
+ export declare function createRampSegment(overrides?: Partial<Omit<RampSegment, "type" | "id">>): RampSegment;
208
+ export declare function createWorkout(overrides?: Partial<Omit<Workout, "id" | "createdAt" | "updatedAt">>): Workout;
209
+ /** A deep copy under a fresh id — for "duplicate", and for editing a preset. */
210
+ export declare function cloneWorkout(workout: Workout, name?: string): Workout;
211
+ /** Seconds a segment occupies on the timeline. */
212
+ export declare function segmentDuration(segment: WorkoutSegment): number;
213
+ /** Total seconds of a whole workout. */
214
+ export declare function workoutDuration(workout: Pick<Workout, "segments">): number;
215
+ /** How many work efforts the rider will actually do. */
216
+ export declare function workoutIntervalCount(workout: Pick<Workout, "segments">): number;
217
+ /**
218
+ * Explode an interval set into the individual blocks it stands for.
219
+ *
220
+ * The timeline editor places blocks one at a time, so a workout imported from
221
+ * ZWO — or authored in the card editor — has to be flattened before it can be
222
+ * dragged around. `3x13x30/15` becomes 78 blocks plus 2 rests.
223
+ *
224
+ * **This is deliberately lossy in exactly one way and lossless in every other.**
225
+ * Gone is the shorthand: nothing afterwards knows those 78 blocks were once
226
+ * three sets, so `describeSegment` stops saying `"3x13x30/15"` and ZWO export
227
+ * writes a run of `<SteadyState>` rather than an `<IntervalsT>`. Kept is
228
+ * everything the *ride* reads — each block carries its {@link PhaseType}, so the
229
+ * compiled timeline has the same phases, the same efforts count, the same
230
+ * `set`/`rep` numbering and the same fast start it had before. `compileWorkout`
231
+ * of a flattened workout equals `compileWorkout` of the original, which is
232
+ * asserted in `test/flatten.test.ts` and is the whole licence for doing this.
233
+ *
234
+ * Mirrors `compileWorkout`'s expansion step for step, including its two edge
235
+ * rules: a rep with no recovery emits no recovery block, and the rest goes
236
+ * BETWEEN sets and never after the last one.
237
+ */
238
+ export declare function flattenSegment(segment: WorkoutSegment): WorkoutSegment[];
239
+ /** Every segment flattened, in order. What the timeline editor opens a workout as. */
240
+ export declare function flattenSegments(segments: readonly WorkoutSegment[]): WorkoutSegment[];
241
+ /** Whether anything here would change shape under {@link flattenSegments}. */
242
+ export declare function hasCompositeSegments(workout: Pick<Workout, "segments">): boolean;
243
+ /** Whether somebody scored this session by hand — see {@link WorkoutMusic}. */
244
+ export declare function hasAuthoredMusic(workout: Pick<Workout, "music">): boolean;
245
+ /** `90` → `"1:30"`, `3725` → `"1:02:05"`. For clocks and countdowns. */
246
+ export declare function formatClock(totalSeconds: number): string;
247
+ /** `2700` → `"45min"`, `90` → `"1min 30s"`, `45` → `"45s"`. For labels, not clocks. */
248
+ export declare function formatDuration(totalSeconds: number): string;
249
+ /** The one-line shorthand a cyclist would actually say out loud. */
250
+ export declare function describeSegment(segment: WorkoutSegment): string;
251
+ export {};
252
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/training/model.ts"],"names":[],"mappings":"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"}