@aneuhold/core-ts-db-lib 4.1.12 → 4.1.13
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/CHANGELOG.md +21 -0
- package/lib/browser.d.ts +11 -8
- package/lib/browser.d.ts.map +1 -1
- package/lib/browser.js +6 -4
- package/lib/browser.js.map +1 -1
- package/lib/browser.ts +23 -9
- package/lib/ctos/workout/WorkoutMuscleGroupVolumeCTO.d.ts +14 -0
- package/lib/ctos/workout/WorkoutMuscleGroupVolumeCTO.d.ts.map +1 -1
- package/lib/ctos/workout/WorkoutMuscleGroupVolumeCTO.ts +18 -0
- package/lib/documents/workout/WorkoutSet.d.ts +8 -0
- package/lib/documents/workout/WorkoutSet.d.ts.map +1 -1
- package/lib/documents/workout/WorkoutSet.ts +9 -0
- package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts +40 -0
- package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts.map +1 -1
- package/lib/services/workout/Exercise/WorkoutExerciseService.js +134 -1
- package/lib/services/workout/Exercise/WorkoutExerciseService.js.map +1 -1
- package/lib/services/workout/Exercise/WorkoutExerciseService.ts +204 -1
- package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts +15 -0
- package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts.map +1 -1
- package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js +18 -1
- package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js.map +1 -1
- package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.ts +19 -1
- package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts +26 -4
- package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts.map +1 -1
- package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js +51 -4
- package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js.map +1 -1
- package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.ts +58 -3
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts +45 -1
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts.map +1 -1
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js +201 -11
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js.map +1 -1
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.ts +285 -9
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.d.ts +33 -0
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.d.ts.map +1 -0
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.js +24 -0
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.js.map +1 -0
- package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.ts +36 -0
- package/lib/services/workout/Session/WorkoutSessionService.d.ts.map +1 -1
- package/lib/services/workout/Session/WorkoutSessionService.js +1 -11
- package/lib/services/workout/Session/WorkoutSessionService.js.map +1 -1
- package/lib/services/workout/Session/WorkoutSessionService.ts +1 -17
- package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts +14 -2
- package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts.map +1 -1
- package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js +17 -3
- package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js.map +1 -1
- package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.ts +28 -3
- package/lib/services/workout/Set/WorkoutSetService.d.ts +17 -5
- package/lib/services/workout/Set/WorkoutSetService.d.ts.map +1 -1
- package/lib/services/workout/Set/WorkoutSetService.js +83 -16
- package/lib/services/workout/Set/WorkoutSetService.js.map +1 -1
- package/lib/services/workout/Set/WorkoutSetService.ts +107 -24
- package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts +72 -2
- package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts.map +1 -1
- package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js +202 -15
- package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js.map +1 -1
- package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.ts +268 -18
- package/package.json +1 -1
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { UUID } from 'crypto';
|
|
2
2
|
import type { WorkoutExerciseCTO } from '../../../../ctos/workout/WorkoutExerciseCTO.js';
|
|
3
|
+
import type {
|
|
4
|
+
WorkoutMuscleGroupVolumeCTO,
|
|
5
|
+
WorkoutVolumeLandmarkEstimate
|
|
6
|
+
} from '../../../../ctos/workout/WorkoutMuscleGroupVolumeCTO.js';
|
|
3
7
|
import type { WorkoutSessionExercise } from '../../../../documents/workout/WorkoutSessionExercise.js';
|
|
4
8
|
import type WorkoutMesocyclePlanContext from '../../Mesocycle/WorkoutMesocyclePlanContext.js';
|
|
5
9
|
import WorkoutSessionExerciseService from '../../SessionExercise/WorkoutSessionExerciseService.js';
|
|
10
|
+
import WorkoutSFRService from '../SFR/WorkoutSFRService.js';
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* A service for handling volume planning operations across microcycles.
|
|
@@ -24,6 +29,39 @@ export default class WorkoutVolumePlanningService {
|
|
|
24
29
|
private static readonly MAX_SETS_PER_EXERCISE = 8;
|
|
25
30
|
private static readonly MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION = 10;
|
|
26
31
|
|
|
32
|
+
/** Minimum average RSM required for a mesocycle to count toward MEV estimation. */
|
|
33
|
+
private static readonly MEV_RSM_THRESHOLD = 4;
|
|
34
|
+
|
|
35
|
+
/** Default estimated MEV when no qualifying mesocycle history exists. */
|
|
36
|
+
private static readonly DEFAULT_MEV = 2;
|
|
37
|
+
|
|
38
|
+
/** Minimum average performance score (or recovery presence) to count a mesocycle toward MRV estimation. */
|
|
39
|
+
private static readonly MRV_PERFORMANCE_THRESHOLD = 2.5;
|
|
40
|
+
|
|
41
|
+
/** Extra sets added above the historical peak when no stressed mesocycles exist, to estimate MRV. */
|
|
42
|
+
private static readonly MRV_HEADROOM = 2;
|
|
43
|
+
|
|
44
|
+
/** Default estimated MRV when no mesocycle history exists at all. */
|
|
45
|
+
private static readonly DEFAULT_MRV = 8;
|
|
46
|
+
|
|
47
|
+
/** RSM bracket upper bound for "below MEV" proximity (0 to this value inclusive). */
|
|
48
|
+
private static readonly MEV_PROXIMITY_BELOW_THRESHOLD = 3;
|
|
49
|
+
|
|
50
|
+
/** RSM bracket upper bound for "at MEV" proximity (above BELOW threshold up to this value inclusive). */
|
|
51
|
+
private static readonly MEV_PROXIMITY_AT_THRESHOLD = 6;
|
|
52
|
+
|
|
53
|
+
/** Recommended set adjustment when volume is below MEV. */
|
|
54
|
+
private static readonly MEV_BELOW_SET_ADJUSTMENT = 3;
|
|
55
|
+
|
|
56
|
+
/** Recommended set adjustment when volume is above MEV. */
|
|
57
|
+
private static readonly MEV_ABOVE_SET_ADJUSTMENT = -2;
|
|
58
|
+
|
|
59
|
+
/** Maximum sets that can be added to a single exercise in one progression step. */
|
|
60
|
+
private static readonly MAX_SET_ADDITION_PER_EXERCISE = 2;
|
|
61
|
+
|
|
62
|
+
/** Maximum total sets to distribute across a muscle group in one progression step. */
|
|
63
|
+
private static readonly MAX_TOTAL_SET_ADDITIONS = 3;
|
|
64
|
+
|
|
27
65
|
/**
|
|
28
66
|
* Calculates the set plan for an entire microcycle.
|
|
29
67
|
*/
|
|
@@ -56,9 +94,185 @@ export default class WorkoutVolumePlanningService {
|
|
|
56
94
|
}
|
|
57
95
|
});
|
|
58
96
|
|
|
97
|
+
// Apply MEV proximity adjustment when generating the second microcycle (index 1)
|
|
98
|
+
// after the first microcycle is complete with RSM data. Only applies when volume
|
|
99
|
+
// data (volumeCTOs) was provided to the context.
|
|
100
|
+
if (
|
|
101
|
+
microcycleIndex === 1 &&
|
|
102
|
+
!isDeloadMicrocycle &&
|
|
103
|
+
context.muscleGroupToVolumeLandmarkMap.size > 0
|
|
104
|
+
) {
|
|
105
|
+
this.applyMevProximityAdjustments(context, exerciseIdToSetCount);
|
|
106
|
+
}
|
|
107
|
+
|
|
59
108
|
return { exerciseIdToSetCount, recoveryExerciseIds };
|
|
60
109
|
}
|
|
61
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Estimates MEV, MRV, and MAV for a muscle group based on historical data
|
|
113
|
+
* across completed mesocycles.
|
|
114
|
+
*
|
|
115
|
+
* @param volumeCTO The WorkoutMuscleGroupVolumeCTO containing mesocycle
|
|
116
|
+
* history for this muscle group.
|
|
117
|
+
*/
|
|
118
|
+
static estimateVolumeLandmarks(
|
|
119
|
+
volumeCTO: WorkoutMuscleGroupVolumeCTO
|
|
120
|
+
): WorkoutVolumeLandmarkEstimate {
|
|
121
|
+
const { mesocycleHistory } = volumeCTO;
|
|
122
|
+
|
|
123
|
+
// Estimated MEV
|
|
124
|
+
let estimatedMev: number;
|
|
125
|
+
const effectiveMesocycles = mesocycleHistory.filter(
|
|
126
|
+
(m) => m.avgRsm !== null && m.avgRsm >= this.MEV_RSM_THRESHOLD
|
|
127
|
+
);
|
|
128
|
+
if (effectiveMesocycles.length > 0) {
|
|
129
|
+
estimatedMev =
|
|
130
|
+
effectiveMesocycles.reduce((sum, m) => sum + m.startingSetCount, 0) /
|
|
131
|
+
effectiveMesocycles.length;
|
|
132
|
+
estimatedMev = Math.round(estimatedMev);
|
|
133
|
+
} else if (mesocycleHistory.length > 0) {
|
|
134
|
+
estimatedMev = Math.min(...mesocycleHistory.map((m) => m.startingSetCount));
|
|
135
|
+
} else {
|
|
136
|
+
estimatedMev = this.DEFAULT_MEV;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Estimated MRV
|
|
140
|
+
let estimatedMrv: number;
|
|
141
|
+
const stressedMesocycles = mesocycleHistory.filter(
|
|
142
|
+
(m) =>
|
|
143
|
+
(m.avgPerformanceScore !== null &&
|
|
144
|
+
m.avgPerformanceScore >= this.MRV_PERFORMANCE_THRESHOLD) ||
|
|
145
|
+
m.recoverySessionCount > 0
|
|
146
|
+
);
|
|
147
|
+
if (stressedMesocycles.length > 0) {
|
|
148
|
+
estimatedMrv =
|
|
149
|
+
stressedMesocycles.reduce((sum, m) => sum + m.peakSetCount, 0) / stressedMesocycles.length;
|
|
150
|
+
estimatedMrv = Math.round(estimatedMrv);
|
|
151
|
+
} else if (mesocycleHistory.length > 0) {
|
|
152
|
+
estimatedMrv = Math.max(...mesocycleHistory.map((m) => m.peakSetCount)) + this.MRV_HEADROOM;
|
|
153
|
+
} else {
|
|
154
|
+
estimatedMrv = this.DEFAULT_MRV;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Hard cap MRV at 10 (MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION)
|
|
158
|
+
estimatedMrv = Math.min(estimatedMrv, this.MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION);
|
|
159
|
+
|
|
160
|
+
// Ensure MRV > MEV
|
|
161
|
+
if (estimatedMrv <= estimatedMev) {
|
|
162
|
+
estimatedMrv = estimatedMev + 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const estimatedMav = Math.ceil((estimatedMev + estimatedMrv) / 2);
|
|
166
|
+
|
|
167
|
+
return { estimatedMev, estimatedMrv, estimatedMav, mesocycleCount: mesocycleHistory.length };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Evaluates MEV (Minimum Effective Volume) proximity for a muscle group based on
|
|
172
|
+
* RSM scores from the first microcycle. Called when generating the second microcycle to adjust
|
|
173
|
+
* the volume baseline.
|
|
174
|
+
*
|
|
175
|
+
* Returns `null` when the first microcycle is incomplete or has no RSM data for the muscle group.
|
|
176
|
+
*
|
|
177
|
+
* @param context The mesocycle planning context containing microcycle/session/exercise data.
|
|
178
|
+
* @param muscleGroupId The muscle group to evaluate.
|
|
179
|
+
*/
|
|
180
|
+
static evaluateMevProximity(
|
|
181
|
+
context: WorkoutMesocyclePlanContext,
|
|
182
|
+
muscleGroupId: UUID
|
|
183
|
+
): {
|
|
184
|
+
/** 'below' = RSM 0-3, 'at' = RSM 4-6, 'above' = RSM 7-9 */
|
|
185
|
+
proximity: 'below' | 'at' | 'above';
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Recommended total set adjustment for this muscle group.
|
|
189
|
+
* Positive = add sets, negative = remove sets, 0 = no change.
|
|
190
|
+
* Range: -2 to +3
|
|
191
|
+
*/
|
|
192
|
+
recommendedSetAdjustment: number;
|
|
193
|
+
|
|
194
|
+
/** The average RSM across session exercises targeting this muscle group. */
|
|
195
|
+
averageRsm: number;
|
|
196
|
+
} | null {
|
|
197
|
+
const firstMicrocycle = context.microcyclesInOrder[0];
|
|
198
|
+
if (!firstMicrocycle) return null;
|
|
199
|
+
|
|
200
|
+
// Check the first microcycle is complete
|
|
201
|
+
const lastSessionId = firstMicrocycle.sessionOrder[firstMicrocycle.sessionOrder.length - 1];
|
|
202
|
+
if (!context.sessionMap.get(lastSessionId)?.complete) return null;
|
|
203
|
+
|
|
204
|
+
// Collect RSM totals from session exercises that target this muscle group
|
|
205
|
+
const rsmTotals: number[] = [];
|
|
206
|
+
for (const sessionId of firstMicrocycle.sessionOrder) {
|
|
207
|
+
const session = context.sessionMap.get(sessionId);
|
|
208
|
+
if (!session) continue;
|
|
209
|
+
for (const seId of session.sessionExerciseOrder) {
|
|
210
|
+
const se = context.sessionExerciseMap.get(seId);
|
|
211
|
+
if (!se) continue;
|
|
212
|
+
const exercise = context.exerciseMap.get(se.workoutExerciseId);
|
|
213
|
+
if (!exercise?.primaryMuscleGroups.includes(muscleGroupId)) continue;
|
|
214
|
+
|
|
215
|
+
const rsmTotal = WorkoutSFRService.getRsmTotal(se.rsm);
|
|
216
|
+
if (rsmTotal !== null) {
|
|
217
|
+
rsmTotals.push(rsmTotal);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (rsmTotals.length === 0) return null;
|
|
223
|
+
|
|
224
|
+
const averageRsm = rsmTotals.reduce((sum, val) => sum + val, 0) / rsmTotals.length;
|
|
225
|
+
const bracket = Math.floor(averageRsm);
|
|
226
|
+
|
|
227
|
+
if (bracket <= this.MEV_PROXIMITY_BELOW_THRESHOLD) {
|
|
228
|
+
return {
|
|
229
|
+
proximity: 'below',
|
|
230
|
+
recommendedSetAdjustment: this.MEV_BELOW_SET_ADJUSTMENT,
|
|
231
|
+
averageRsm
|
|
232
|
+
};
|
|
233
|
+
} else if (bracket <= this.MEV_PROXIMITY_AT_THRESHOLD) {
|
|
234
|
+
return { proximity: 'at', recommendedSetAdjustment: 0, averageRsm };
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
proximity: 'above',
|
|
238
|
+
recommendedSetAdjustment: this.MEV_ABOVE_SET_ADJUSTMENT,
|
|
239
|
+
averageRsm
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Applies MEV proximity adjustments based on RSM data from the first microcycle.
|
|
245
|
+
* Adjusts set counts per muscle group when the first microcycle indicates volume
|
|
246
|
+
* was below or above MEV.
|
|
247
|
+
*/
|
|
248
|
+
private static applyMevProximityAdjustments(
|
|
249
|
+
context: WorkoutMesocyclePlanContext,
|
|
250
|
+
exerciseIdToSetCount: Map<UUID, number>
|
|
251
|
+
): void {
|
|
252
|
+
if (!context.muscleGroupToExerciseCTOsMap) return;
|
|
253
|
+
|
|
254
|
+
for (const [muscleGroupId, muscleGroupExerciseCTOs] of context.muscleGroupToExerciseCTOsMap) {
|
|
255
|
+
const mevResult = this.evaluateMevProximity(context, muscleGroupId);
|
|
256
|
+
if (!mevResult || mevResult.recommendedSetAdjustment === 0) continue;
|
|
257
|
+
|
|
258
|
+
// Distribute the adjustment evenly across exercises in this muscle group
|
|
259
|
+
const exerciseCount = muscleGroupExerciseCTOs.length;
|
|
260
|
+
const adjustmentPerExercise = Math.floor(mevResult.recommendedSetAdjustment / exerciseCount);
|
|
261
|
+
const adjustmentRemainder = mevResult.recommendedSetAdjustment % exerciseCount;
|
|
262
|
+
|
|
263
|
+
muscleGroupExerciseCTOs.forEach((cto, index) => {
|
|
264
|
+
const currentSets = exerciseIdToSetCount.get(cto._id) ?? 2;
|
|
265
|
+
const extra =
|
|
266
|
+
index < Math.abs(adjustmentRemainder) ? Math.sign(mevResult.recommendedSetAdjustment) : 0;
|
|
267
|
+
const newSets = Math.max(
|
|
268
|
+
1,
|
|
269
|
+
Math.min(currentSets + adjustmentPerExercise + extra, this.MAX_SETS_PER_EXERCISE)
|
|
270
|
+
);
|
|
271
|
+
exerciseIdToSetCount.set(cto._id, newSets);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
62
276
|
/**
|
|
63
277
|
* Calculates the set count for each exercise in a particular muscle group for this microcycle.
|
|
64
278
|
*
|
|
@@ -75,13 +289,16 @@ export default class WorkoutVolumePlanningService {
|
|
|
75
289
|
const recoveryExerciseIds = new Set<UUID>();
|
|
76
290
|
const sessionIndexToExerciseIds = new Map<number, UUID[]>();
|
|
77
291
|
|
|
292
|
+
const { progressionInterval } = context;
|
|
293
|
+
|
|
78
294
|
// 1. Calculate baselines for all exercises in muscle group
|
|
79
295
|
muscleGroupExerciseCTOs.forEach((cto, index) => {
|
|
80
296
|
const baseline = this.calculateBaselineSetCount(
|
|
81
297
|
microcycleIndex,
|
|
82
298
|
muscleGroupExerciseCTOs.length,
|
|
83
299
|
index,
|
|
84
|
-
isDeloadMicrocycle
|
|
300
|
+
isDeloadMicrocycle,
|
|
301
|
+
progressionInterval
|
|
85
302
|
);
|
|
86
303
|
exerciseIdToSetCount.set(cto._id, Math.min(baseline, this.MAX_SETS_PER_EXERCISE));
|
|
87
304
|
|
|
@@ -150,13 +367,26 @@ export default class WorkoutVolumePlanningService {
|
|
|
150
367
|
// For deload microcycles, halve the historical count (minimum 1 set). We don't use baseline
|
|
151
368
|
// because the user may have adjusted over the mesocycle in a way that the baseline is actually
|
|
152
369
|
// a higher set count than what would be calculated by halving the previous microcycle's sets.
|
|
370
|
+
//
|
|
371
|
+
// For exercises returning from recovery, use the estimated MAV from volume landmarks
|
|
372
|
+
// when available, instead of the pre-recovery historical count.
|
|
373
|
+
const primaryMuscleGroupId = muscleGroupExerciseCTOs[0]?.primaryMuscleGroups[0];
|
|
374
|
+
const volumeLandmark = primaryMuscleGroupId
|
|
375
|
+
? context.muscleGroupToVolumeLandmarkMap.get(primaryMuscleGroupId)
|
|
376
|
+
: undefined;
|
|
377
|
+
|
|
153
378
|
muscleGroupExerciseCTOs.forEach((cto) => {
|
|
154
379
|
const previousSessionExercise = exerciseIdToPrevSessionExercise.get(cto._id);
|
|
155
380
|
if (previousSessionExercise) {
|
|
156
|
-
let setCount
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
381
|
+
let setCount: number;
|
|
382
|
+
|
|
383
|
+
if (exercisesThatWerePreviouslyInRecovery.has(cto._id) && volumeLandmark) {
|
|
384
|
+
// Exercise is returning from recovery — resume at the estimated MAV
|
|
385
|
+
setCount = Math.min(volumeLandmark.estimatedMav, this.MAX_SETS_PER_EXERCISE);
|
|
386
|
+
} else {
|
|
387
|
+
setCount = Math.min(previousSessionExercise.setOrder.length, this.MAX_SETS_PER_EXERCISE);
|
|
388
|
+
}
|
|
389
|
+
|
|
160
390
|
if (isDeloadMicrocycle) {
|
|
161
391
|
setCount = Math.max(1, Math.floor(setCount / 2));
|
|
162
392
|
}
|
|
@@ -164,8 +394,8 @@ export default class WorkoutVolumePlanningService {
|
|
|
164
394
|
}
|
|
165
395
|
});
|
|
166
396
|
|
|
167
|
-
// Deload microcycles
|
|
168
|
-
if (isDeloadMicrocycle) {
|
|
397
|
+
// Deload microcycles and zero-progression cycles (Resensitization) skip SFR-based set additions
|
|
398
|
+
if (isDeloadMicrocycle || progressionInterval === 0) {
|
|
169
399
|
return { exerciseIdToSetCount, recoveryExerciseIds };
|
|
170
400
|
}
|
|
171
401
|
|
|
@@ -225,7 +455,7 @@ export default class WorkoutVolumePlanningService {
|
|
|
225
455
|
const previousSetCount = previousSessionExercise.setOrder.length;
|
|
226
456
|
const recoverySets = Math.max(1, Math.floor(previousSetCount / 2));
|
|
227
457
|
exerciseIdToSetCount.set(cto._id, recoverySets);
|
|
228
|
-
} else if (recommendation
|
|
458
|
+
} else if (recommendation !== null && recommendation >= 0) {
|
|
229
459
|
totalSetsToAdd += recommendation;
|
|
230
460
|
|
|
231
461
|
// Consider as candidate if session is not already capped
|
|
@@ -289,8 +519,12 @@ export default class WorkoutVolumePlanningService {
|
|
|
289
519
|
WorkoutVolumePlanningService.MAX_SETS_PER_EXERCISE - currentSets;
|
|
290
520
|
const maxDueToSessionLimit =
|
|
291
521
|
WorkoutVolumePlanningService.MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION - sessionTotal;
|
|
292
|
-
|
|
293
|
-
|
|
522
|
+
const maxAddable = Math.min(
|
|
523
|
+
setsToAdd,
|
|
524
|
+
maxDueToExerciseLimit,
|
|
525
|
+
maxDueToSessionLimit,
|
|
526
|
+
WorkoutVolumePlanningService.MAX_SET_ADDITION_PER_EXERCISE
|
|
527
|
+
);
|
|
294
528
|
|
|
295
529
|
if (maxAddable > 0) {
|
|
296
530
|
exerciseIdToSetCount.set(exerciseId, currentSets + maxAddable);
|
|
@@ -298,8 +532,10 @@ export default class WorkoutVolumePlanningService {
|
|
|
298
532
|
return maxAddable;
|
|
299
533
|
}
|
|
300
534
|
|
|
301
|
-
|
|
302
|
-
|
|
535
|
+
let setsRemaining =
|
|
536
|
+
totalSetsToAdd >= WorkoutVolumePlanningService.MAX_TOTAL_SET_ADDITIONS
|
|
537
|
+
? WorkoutVolumePlanningService.MAX_TOTAL_SET_ADDITIONS
|
|
538
|
+
: totalSetsToAdd;
|
|
303
539
|
for (const candidate of candidates) {
|
|
304
540
|
const added = addSetsToExercise(candidate.exerciseId, setsRemaining);
|
|
305
541
|
setsRemaining -= added;
|
|
@@ -316,14 +552,26 @@ export default class WorkoutVolumePlanningService {
|
|
|
316
552
|
* for the entire microcycle, regardless of which session those exercises are in.
|
|
317
553
|
*
|
|
318
554
|
* Baseline: 2 sets per exercise in the muscle group.
|
|
319
|
-
* Progression: add 1
|
|
320
|
-
*
|
|
555
|
+
* Progression: add 1 set per muscle group every `progressionInterval` microcycles.
|
|
556
|
+
* - MuscleGain (interval 1): every microcycle
|
|
557
|
+
* - Cut (interval 2): every other microcycle
|
|
558
|
+
* - Resensitization (interval 0): no progression (flat 2 sets per exercise)
|
|
559
|
+
*
|
|
560
|
+
* @param microcycleIndex The index of the current microcycle.
|
|
561
|
+
* @param totalExercisesInMuscleGroupForMicrocycle Total exercises in the muscle group for
|
|
562
|
+
* this microcycle.
|
|
563
|
+
* @param exerciseIndexInMuscleGroupForMicrocycle Index of the current exercise within the
|
|
564
|
+
* muscle group.
|
|
565
|
+
* @param isDeloadMicrocycle Whether this is a deload microcycle.
|
|
566
|
+
* @param progressionInterval Number of microcycles between each set addition. 0 means no
|
|
567
|
+
* progression.
|
|
321
568
|
*/
|
|
322
569
|
private static calculateBaselineSetCount(
|
|
323
570
|
microcycleIndex: number,
|
|
324
571
|
totalExercisesInMuscleGroupForMicrocycle: number,
|
|
325
572
|
exerciseIndexInMuscleGroupForMicrocycle: number,
|
|
326
|
-
isDeloadMicrocycle: boolean
|
|
573
|
+
isDeloadMicrocycle: boolean,
|
|
574
|
+
progressionInterval: number = 1
|
|
327
575
|
): number {
|
|
328
576
|
// Deload microcycle: half the sets from the previous microcycle, minimum 1 set.
|
|
329
577
|
if (isDeloadMicrocycle) {
|
|
@@ -331,14 +579,16 @@ export default class WorkoutVolumePlanningService {
|
|
|
331
579
|
microcycleIndex - 1,
|
|
332
580
|
totalExercisesInMuscleGroupForMicrocycle,
|
|
333
581
|
exerciseIndexInMuscleGroupForMicrocycle,
|
|
334
|
-
false
|
|
582
|
+
false,
|
|
583
|
+
progressionInterval
|
|
335
584
|
);
|
|
336
585
|
return Math.max(1, Math.floor(baselineSets / 2));
|
|
337
586
|
}
|
|
338
587
|
|
|
339
588
|
// Total sets to distribute for this muscle group in this microcycle.
|
|
340
|
-
|
|
341
|
-
|
|
589
|
+
const progressionSets =
|
|
590
|
+
progressionInterval === 0 ? 0 : Math.ceil(microcycleIndex / progressionInterval);
|
|
591
|
+
const totalSets = 2 * totalExercisesInMuscleGroupForMicrocycle + progressionSets;
|
|
342
592
|
|
|
343
593
|
// Distribute sets evenly, with earlier exercises getting extra sets from the remainder.
|
|
344
594
|
const baseSetsPerExercise = Math.floor(totalSets / totalExercisesInMuscleGroupForMicrocycle);
|
package/package.json
CHANGED