@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.
- package/dist/client/PlatformClient.d.ts +65 -22
- package/dist/client/PlatformClient.d.ts.map +1 -1
- package/dist/client/PlatformClient.js +143 -13
- package/dist/client/PlatformClient.js.map +1 -1
- package/dist/host/PlatformHost.d.ts +25 -7
- package/dist/host/PlatformHost.d.ts.map +1 -1
- package/dist/host/PlatformHost.js +33 -2
- package/dist/host/PlatformHost.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/protocol/boards.d.ts +1 -13
- package/dist/protocol/boards.d.ts.map +1 -1
- package/dist/protocol/boards.js.map +1 -1
- package/dist/protocol/buttons.d.ts +1 -1
- package/dist/protocol/buttons.js +1 -1
- package/dist/protocol/guards.d.ts.map +1 -1
- package/dist/protocol/guards.js +2 -0
- package/dist/protocol/guards.js.map +1 -1
- package/dist/protocol/identity.d.ts +1 -1
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/index.js.map +1 -1
- package/dist/protocol/messages.d.ts +30 -19
- package/dist/protocol/messages.d.ts.map +1 -1
- package/dist/protocol/stages.d.ts +102 -0
- package/dist/protocol/stages.d.ts.map +1 -0
- package/dist/protocol/stages.js +19 -0
- package/dist/protocol/stages.js.map +1 -0
- package/dist/protocol/version.d.ts +2 -2
- package/dist/protocol/version.d.ts.map +1 -1
- package/dist/protocol/version.js +46 -2
- package/dist/protocol/version.js.map +1 -1
- package/dist/three/character/kit.d.ts +1 -1
- package/dist/three/character/kit.js +1 -1
- package/dist/three/character/riders.d.ts +1 -1
- package/dist/three/character/riders.js +1 -1
- package/dist/three/character/skeleton.d.ts +1 -1
- package/dist/three/character/skeleton.js +1 -1
- package/dist/training/compile.d.ts +135 -0
- package/dist/training/compile.d.ts.map +1 -0
- package/dist/training/compile.js +367 -0
- package/dist/training/compile.js.map +1 -0
- package/dist/training/index.d.ts +6 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/index.js +6 -0
- package/dist/training/index.js.map +1 -0
- package/dist/training/model.d.ts +252 -0
- package/dist/training/model.d.ts.map +1 -0
- package/dist/training/model.js +261 -0
- package/dist/training/model.js.map +1 -0
- package/dist/training/protocol.d.ts +60 -0
- package/dist/training/protocol.d.ts.map +1 -0
- package/dist/training/protocol.js +2 -0
- package/dist/training/protocol.js.map +1 -0
- package/dist/training/state.d.ts +201 -0
- package/dist/training/state.d.ts.map +1 -0
- package/dist/training/state.js +55 -0
- package/dist/training/state.js.map +1 -0
- package/dist/training/zones.d.ts +50 -0
- package/dist/training/zones.d.ts.map +1 -0
- package/dist/training/zones.js +57 -0
- package/dist/training/zones.js.map +1 -0
- package/dist/ui/README.md +2 -3
- package/dist/ui/option-menu.d.ts +1 -1
- package/dist/ui/option-menu.js +1 -1
- package/dist/ui/rider-rig.d.ts +1 -1
- package/dist/ui/rider-rig.js +2 -2
- package/package.json +1 -5
- package/dist/difficulty/index.d.ts +0 -141
- package/dist/difficulty/index.d.ts.map +0 -1
- package/dist/difficulty/index.js +0 -300
- package/dist/difficulty/index.js.map +0 -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 edits their FTP
|
|
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 @@
|
|
|
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 @@
|
|
|
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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"}
|