@dgpholdings/greatoak-shared 1.1.35 → 1.1.36
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.
|
@@ -2,7 +2,8 @@ export type TRecord = {
|
|
|
2
2
|
isDone: boolean;
|
|
3
3
|
rpe?: string;
|
|
4
4
|
setNote?: string;
|
|
5
|
-
|
|
5
|
+
workDurationSecs?: number;
|
|
6
|
+
restDurationSecs?: number;
|
|
6
7
|
} & ({
|
|
7
8
|
type: "weight-reps";
|
|
8
9
|
kg: string;
|
|
@@ -41,7 +42,7 @@ export type TRecordConfig = {
|
|
|
41
42
|
enableRpe?: boolean;
|
|
42
43
|
enableSetNote?: boolean;
|
|
43
44
|
enableExerciseNote?: boolean;
|
|
44
|
-
|
|
45
|
+
enableTimeIntervalMode?: boolean;
|
|
45
46
|
};
|
|
46
47
|
export type TApiExerciseRecordUpdateReq = {
|
|
47
48
|
templateId: string;
|
|
@@ -3,7 +3,8 @@ type RefinedBase = {
|
|
|
3
3
|
isDone: boolean;
|
|
4
4
|
rpe?: number;
|
|
5
5
|
rir?: number;
|
|
6
|
-
|
|
6
|
+
workDurationSecs?: number;
|
|
7
|
+
restDurationSecs?: number;
|
|
7
8
|
setNote?: string;
|
|
8
9
|
};
|
|
9
10
|
export type TRefinedWeightRecord = RefinedBase & {
|
|
@@ -21,6 +22,12 @@ export type TRefinedBodyWeightRecord = RefinedBase & {
|
|
|
21
22
|
reps: number;
|
|
22
23
|
auxWeightKg?: number;
|
|
23
24
|
};
|
|
24
|
-
export type
|
|
25
|
+
export type TRefinedDistanceRecord = RefinedBase & {
|
|
26
|
+
type: "distance";
|
|
27
|
+
distanceKm: number;
|
|
28
|
+
durationSecs: string;
|
|
29
|
+
avgPaceSecsPerKm?: number;
|
|
30
|
+
};
|
|
31
|
+
export type TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord | TRefinedDistanceRecord;
|
|
25
32
|
export declare const refineRecordEntry: (entry: TRecord) => TRefinedRecord;
|
|
26
33
|
export {};
|
|
@@ -4,9 +4,11 @@ exports.refineRecordEntry = void 0;
|
|
|
4
4
|
const isDefined_utils_1 = require("./isDefined.utils");
|
|
5
5
|
const number_util_1 = require("./number.util");
|
|
6
6
|
const refineRecordEntry = (entry) => {
|
|
7
|
-
var _a, _b, _c;
|
|
8
|
-
const base = Object.assign(Object.assign(Object.assign({ isDone: entry.isDone }, ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rpe)) && { rpe: (0, number_util_1.toNumber)(entry.rpe) })), ((0, isDefined_utils_1.isDefinedNumber)(
|
|
9
|
-
|
|
7
|
+
var _a, _b, _c, _d, _e;
|
|
8
|
+
const base = Object.assign(Object.assign(Object.assign(Object.assign({ isDone: entry.isDone }, ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rpe)) && { rpe: (0, number_util_1.toNumber)(entry.rpe) })), ((0, isDefined_utils_1.isDefinedNumber)(entry.workDurationSecs) && {
|
|
9
|
+
workDurationSecs: entry.workDurationSecs,
|
|
10
|
+
})), ((0, isDefined_utils_1.isDefinedNumber)(entry.restDurationSecs) && {
|
|
11
|
+
restDurationSecs: entry.restDurationSecs,
|
|
10
12
|
})), ((0, isDefined_utils_1.isDefined)(entry.setNote) && { setNote: entry.setNote }));
|
|
11
13
|
if (entry.type === "weight-reps") {
|
|
12
14
|
return Object.assign(Object.assign({}, base), { type: "weight-reps", kg: (_a = (0, number_util_1.toNumber)(entry.kg)) !== null && _a !== void 0 ? _a : 0, rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
|
|
@@ -25,6 +27,15 @@ const refineRecordEntry = (entry) => {
|
|
|
25
27
|
auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
|
|
26
28
|
}));
|
|
27
29
|
}
|
|
30
|
+
if (entry.type === "distance") {
|
|
31
|
+
const distanceKm = (_d = (0, number_util_1.toNumber)(entry.distanceKm)) !== null && _d !== void 0 ? _d : 0;
|
|
32
|
+
const durationSecs = (_e = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _e !== void 0 ? _e : 0;
|
|
33
|
+
// Calculate pace if both distance and duration are provided
|
|
34
|
+
const avgPaceSecsPerKm = distanceKm > 0 && durationSecs > 0
|
|
35
|
+
? durationSecs / distanceKm
|
|
36
|
+
: undefined;
|
|
37
|
+
return Object.assign(Object.assign(Object.assign({}, base), { type: "distance", distanceKm, durationSecs: entry.durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)(avgPaceSecsPerKm) && { avgPaceSecsPerKm }));
|
|
38
|
+
}
|
|
28
39
|
throw new Error(`Unknown record type: ${entry.type}`);
|
|
29
40
|
};
|
|
30
41
|
exports.refineRecordEntry = refineRecordEntry;
|
|
@@ -17,16 +17,14 @@ type TScoreBreakdown = {
|
|
|
17
17
|
type TParams = {
|
|
18
18
|
record: TRefinedRecord;
|
|
19
19
|
userProfile?: TUserProfile;
|
|
20
|
-
|
|
20
|
+
avgRestDurationSecs?: number;
|
|
21
21
|
exerciseDefaultDifficultyMultiplier?: number;
|
|
22
|
+
isTimeIntervalModeEnabled?: boolean;
|
|
23
|
+
sessionSetCount?: number;
|
|
22
24
|
};
|
|
23
25
|
/**
|
|
24
26
|
* Compute the training stress score for a given record.
|
|
25
|
-
*
|
|
26
|
-
* @param record - A refined workout set record
|
|
27
|
-
* @param userProfile - Optional user demographics (age, gender, weight)
|
|
28
|
-
* @param avgRestInBetweenDurationSecs - Average rest between sets in session
|
|
29
|
-
* @returns An object with baseScore, applied boosts, penalties, and final score
|
|
27
|
+
* Uses improved load calculations with type-specific normalization and MET values for cardio.
|
|
30
28
|
*/
|
|
31
|
-
export declare const computeScoreFromRecord: ({ exerciseDefaultDifficultyMultiplier,
|
|
29
|
+
export declare const computeScoreFromRecord: ({ exerciseDefaultDifficultyMultiplier, avgRestDurationSecs, record, userProfile, isTimeIntervalModeEnabled, sessionSetCount, }: TParams) => TScoreBreakdown;
|
|
32
30
|
export {};
|
|
@@ -4,14 +4,10 @@ exports.computeScoreFromRecord = void 0;
|
|
|
4
4
|
const time_util_1 = require("./time.util");
|
|
5
5
|
/**
|
|
6
6
|
* Compute the training stress score for a given record.
|
|
7
|
-
*
|
|
8
|
-
* @param record - A refined workout set record
|
|
9
|
-
* @param userProfile - Optional user demographics (age, gender, weight)
|
|
10
|
-
* @param avgRestInBetweenDurationSecs - Average rest between sets in session
|
|
11
|
-
* @returns An object with baseScore, applied boosts, penalties, and final score
|
|
7
|
+
* Uses improved load calculations with type-specific normalization and MET values for cardio.
|
|
12
8
|
*/
|
|
13
|
-
const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1,
|
|
14
|
-
var _a, _b, _c, _d, _e;
|
|
9
|
+
const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRestDurationSecs, record, userProfile, isTimeIntervalModeEnabled = false, sessionSetCount = 1, }) => {
|
|
10
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
15
11
|
if (!record.isDone) {
|
|
16
12
|
return {
|
|
17
13
|
baseScore: 0,
|
|
@@ -20,85 +16,202 @@ const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRe
|
|
|
20
16
|
finalScore: 0,
|
|
21
17
|
};
|
|
22
18
|
}
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const userWeight = (_a = userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
|
|
20
|
+
const plus = [];
|
|
21
|
+
const minus = [];
|
|
22
|
+
// Step 1: Compute type-specific load with normalization
|
|
23
|
+
let rawLoad = 0;
|
|
24
|
+
let normalizedLoad = 0;
|
|
25
25
|
switch (record.type) {
|
|
26
|
-
case "weight-reps":
|
|
27
|
-
|
|
26
|
+
case "weight-reps": {
|
|
27
|
+
rawLoad = record.kg * record.reps;
|
|
28
|
+
normalizedLoad = rawLoad; // Baseline - no scaling needed
|
|
28
29
|
break;
|
|
30
|
+
}
|
|
29
31
|
case "duration": {
|
|
30
|
-
const durationSecs = (0, time_util_1.mmssToSecs)((
|
|
31
|
-
const auxWeight = (
|
|
32
|
-
|
|
32
|
+
const durationSecs = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
|
|
33
|
+
const auxWeight = (_c = record.auxWeightKg) !== null && _c !== void 0 ? _c : 0;
|
|
34
|
+
if (auxWeight > 0) {
|
|
35
|
+
// Weighted duration exercise (e.g., weighted plank)
|
|
36
|
+
rawLoad = auxWeight * durationSecs;
|
|
37
|
+
normalizedLoad = rawLoad * 0.8; // Slightly less intense than pure weight training
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Bodyweight duration exercise (e.g., plank, wall sit)
|
|
41
|
+
rawLoad = userWeight * (durationSecs / 60); // Per minute scaling
|
|
42
|
+
normalizedLoad = rawLoad * 1.2; // Duration exercises are metabolically demanding
|
|
43
|
+
}
|
|
33
44
|
break;
|
|
34
45
|
}
|
|
35
46
|
case "reps-only": {
|
|
36
|
-
const reps = (
|
|
37
|
-
const auxWeight = (
|
|
38
|
-
const
|
|
39
|
-
|
|
47
|
+
const reps = (_d = record.reps) !== null && _d !== void 0 ? _d : 0;
|
|
48
|
+
const auxWeight = (_e = record.auxWeightKg) !== null && _e !== void 0 ? _e : 0;
|
|
49
|
+
const effectiveWeight = auxWeight > 0 ? auxWeight : userWeight;
|
|
50
|
+
rawLoad = effectiveWeight * reps;
|
|
51
|
+
normalizedLoad = rawLoad * 0.7; // Bodyweight exercises slightly less intense than free weights
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "distance": {
|
|
55
|
+
const distance = (_f = record.distanceKm) !== null && _f !== void 0 ? _f : 0;
|
|
56
|
+
const durationSecs = (0, time_util_1.mmssToSecs)((_g = record.durationSecs) !== null && _g !== void 0 ? _g : "00:00");
|
|
57
|
+
if (distance > 0 && durationSecs > 0) {
|
|
58
|
+
// Calculate speed and estimate MET value
|
|
59
|
+
const speedKmh = (distance * 3600) / durationSecs;
|
|
60
|
+
// MET estimation based on speed (conservative estimates)
|
|
61
|
+
let metValue = 3; // Walking baseline
|
|
62
|
+
if (speedKmh > 4)
|
|
63
|
+
metValue = 4.5; // Brisk walking
|
|
64
|
+
if (speedKmh > 6)
|
|
65
|
+
metValue = 7; // Light jogging
|
|
66
|
+
if (speedKmh > 8)
|
|
67
|
+
metValue = 9; // Running
|
|
68
|
+
if (speedKmh > 12)
|
|
69
|
+
metValue = 12; // Fast running
|
|
70
|
+
if (speedKmh > 16)
|
|
71
|
+
metValue = 15; // Very fast running
|
|
72
|
+
// Calculate calories burned: MET × weight(kg) × hours
|
|
73
|
+
const caloriesBurned = metValue * userWeight * (durationSecs / 3600);
|
|
74
|
+
rawLoad = caloriesBurned;
|
|
75
|
+
normalizedLoad = caloriesBurned * 8; // Scale cardio to match resistance training
|
|
76
|
+
}
|
|
40
77
|
break;
|
|
41
78
|
}
|
|
42
79
|
}
|
|
43
|
-
// Step 2:
|
|
44
|
-
|
|
80
|
+
// Step 2: Base score using log scale with minimum threshold
|
|
81
|
+
const base = Math.log10(1 + Math.max(10, normalizedLoad)); // Minimum 10 to avoid very low scores
|
|
82
|
+
let workingScore = base;
|
|
83
|
+
// Step 3: Effort multiplier (RPE/RIR)
|
|
84
|
+
let effortFactor = 5; // Default moderate effort if no RPE/RIR provided
|
|
45
85
|
if (typeof record.rpe === "number") {
|
|
46
|
-
effortFactor = record.rpe;
|
|
86
|
+
effortFactor = Math.max(1, Math.min(10, record.rpe));
|
|
47
87
|
}
|
|
48
88
|
else if (typeof record.rir === "number") {
|
|
49
|
-
effortFactor = 10 - record.rir;
|
|
89
|
+
effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
|
|
50
90
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Step 4:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// Step 5:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
const effortMultiplier = 0.7 + effortFactor * 0.06; // Range: 0.76 to 1.3
|
|
92
|
+
workingScore *= effortMultiplier;
|
|
93
|
+
plus.push({
|
|
94
|
+
effortMultiplier: parseFloat((effortMultiplier - 1).toFixed(3)),
|
|
95
|
+
});
|
|
96
|
+
// Step 4: Volume/Fatigue factor (later sets are harder)
|
|
97
|
+
if (sessionSetCount > 1) {
|
|
98
|
+
const fatigueMultiplier = 1 + Math.min(0.3, (sessionSetCount - 1) * 0.05);
|
|
99
|
+
workingScore *= fatigueMultiplier;
|
|
100
|
+
plus.push({ fatigueBonus: parseFloat((fatigueMultiplier - 1).toFixed(3)) });
|
|
101
|
+
}
|
|
102
|
+
// Step 5: Interval training adjustments
|
|
103
|
+
if (isTimeIntervalModeEnabled) {
|
|
104
|
+
let intervalMultiplier = 1.08; // Base 8% boost for interval training
|
|
105
|
+
if (record.workDurationSecs && record.restDurationSecs) {
|
|
106
|
+
const restRatio = record.restDurationSecs / record.workDurationSecs;
|
|
107
|
+
// Optimal rest ratios vary by exercise type
|
|
108
|
+
let optimalRestRatio = 2.0; // Default
|
|
109
|
+
switch (record.type) {
|
|
110
|
+
case "weight-reps":
|
|
111
|
+
optimalRestRatio = 3.0; // Strength needs more rest
|
|
112
|
+
break;
|
|
113
|
+
case "distance":
|
|
114
|
+
case "duration":
|
|
115
|
+
optimalRestRatio = 1.0; // Cardio can handle shorter rest
|
|
116
|
+
break;
|
|
117
|
+
case "reps-only":
|
|
118
|
+
optimalRestRatio = 1.5; // Bodyweight middle ground
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
// Bonus for staying close to optimal ratio (within 50% deviation)
|
|
122
|
+
const deviation = Math.abs(restRatio - optimalRestRatio) / optimalRestRatio;
|
|
123
|
+
if (deviation <= 0.5) {
|
|
124
|
+
const ratioBonus = 0.05 * (1 - deviation * 2); // Up to 5% bonus
|
|
125
|
+
intervalMultiplier += ratioBonus;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
workingScore *= intervalMultiplier;
|
|
129
|
+
plus.push({
|
|
130
|
+
intervalTraining: parseFloat((intervalMultiplier - 1).toFixed(3)),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Step 6: User profile adjustments
|
|
134
|
+
let profileMultiplier = 1;
|
|
135
|
+
// Gender adjustment (women typically have less muscle mass)
|
|
136
|
+
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female") {
|
|
137
|
+
profileMultiplier *= 1.12;
|
|
138
|
+
}
|
|
139
|
+
// Age adjustment (strength declines with age)
|
|
140
|
+
if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) {
|
|
141
|
+
if (userProfile.age > 50)
|
|
142
|
+
profileMultiplier *= 1.1;
|
|
143
|
+
if (userProfile.age > 65)
|
|
144
|
+
profileMultiplier *= 1.15;
|
|
145
|
+
}
|
|
146
|
+
// Weight normalization (smaller people work harder relatively)
|
|
68
147
|
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
|
|
69
|
-
const
|
|
70
|
-
adjustment
|
|
148
|
+
const weightAdjustment = 70 / userProfile.weightKg;
|
|
149
|
+
// Cap adjustment to prevent extreme values
|
|
150
|
+
profileMultiplier *= Math.max(0.7, Math.min(1.4, weightAdjustment));
|
|
71
151
|
}
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
plus.push({
|
|
152
|
+
if (profileMultiplier !== 1) {
|
|
153
|
+
workingScore *= profileMultiplier;
|
|
154
|
+
plus.push({
|
|
155
|
+
profileAdjustment: parseFloat((profileMultiplier - 1).toFixed(3)),
|
|
156
|
+
});
|
|
75
157
|
}
|
|
76
|
-
// Step
|
|
77
|
-
if (typeof record.
|
|
78
|
-
typeof
|
|
79
|
-
const restUsed = record.
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
158
|
+
// Step 7: Rest efficiency penalty
|
|
159
|
+
if (typeof record.restDurationSecs === "number" &&
|
|
160
|
+
typeof avgRestDurationSecs === "number") {
|
|
161
|
+
const restUsed = record.restDurationSecs;
|
|
162
|
+
const baseGrace = 10; // Base grace period
|
|
163
|
+
const effortGrace = effortFactor * 3; // More grace for higher effort
|
|
164
|
+
const totalGrace = baseGrace + effortGrace;
|
|
165
|
+
const maxPenaltyThreshold = totalGrace + 60; // Cap penalty threshold
|
|
166
|
+
const excessRest = restUsed - avgRestDurationSecs;
|
|
167
|
+
if (excessRest > totalGrace) {
|
|
168
|
+
const penaltyExcess = Math.min(excessRest - totalGrace, maxPenaltyThreshold - totalGrace);
|
|
169
|
+
const penaltyRatio = penaltyExcess / (maxPenaltyThreshold - totalGrace);
|
|
170
|
+
const restPenalty = penaltyRatio * 0.15 * workingScore; // Up to 15% penalty
|
|
171
|
+
workingScore -= restPenalty;
|
|
172
|
+
minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
|
|
89
173
|
}
|
|
90
174
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
175
|
+
// Step 8: Work duration adherence (for timed exercises in interval mode)
|
|
176
|
+
if (isTimeIntervalModeEnabled && record.workDurationSecs) {
|
|
177
|
+
let actualDuration = 0;
|
|
178
|
+
if (record.type === "duration") {
|
|
179
|
+
actualDuration = (0, time_util_1.mmssToSecs)((_h = record.durationSecs) !== null && _h !== void 0 ? _h : "00:00");
|
|
180
|
+
}
|
|
181
|
+
else if (record.type === "distance") {
|
|
182
|
+
actualDuration = (0, time_util_1.mmssToSecs)((_j = record.durationSecs) !== null && _j !== void 0 ? _j : "00:00");
|
|
183
|
+
}
|
|
184
|
+
if (actualDuration > 0) {
|
|
185
|
+
const durationDiff = Math.abs(actualDuration - record.workDurationSecs);
|
|
186
|
+
const tolerance = record.workDurationSecs * 0.2; // 20% tolerance
|
|
187
|
+
if (durationDiff > tolerance) {
|
|
188
|
+
const excessPercent = (durationDiff - tolerance) / record.workDurationSecs;
|
|
189
|
+
const timingPenalty = Math.min(0.1 * workingScore, excessPercent * 0.08 * workingScore);
|
|
190
|
+
workingScore -= timingPenalty;
|
|
191
|
+
minus.push({
|
|
192
|
+
timingDeviationPenalty: parseFloat(timingPenalty.toFixed(3)),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Step 9: Exercise difficulty multiplier
|
|
198
|
+
workingScore *= exerciseDefaultDifficultyMultiplier;
|
|
199
|
+
if (exerciseDefaultDifficultyMultiplier !== 1) {
|
|
200
|
+
plus.push({
|
|
201
|
+
exerciseDifficulty: parseFloat((exerciseDefaultDifficultyMultiplier - 1).toFixed(3)),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// Step 10: Final score normalization and capping
|
|
205
|
+
let finalScore = Math.max(0, workingScore);
|
|
206
|
+
// Scale to a more intuitive range (roughly 0-100 for typical exercises)
|
|
207
|
+
finalScore *= 10;
|
|
208
|
+
// Cap extreme scores to prevent runaway values
|
|
209
|
+
finalScore = Math.min(200, finalScore);
|
|
97
210
|
return {
|
|
98
211
|
baseScore: parseFloat(base.toFixed(2)),
|
|
99
212
|
plus,
|
|
100
213
|
minus,
|
|
101
|
-
finalScore,
|
|
214
|
+
finalScore: parseFloat(finalScore.toFixed(1)),
|
|
102
215
|
};
|
|
103
216
|
};
|
|
104
217
|
exports.computeScoreFromRecord = computeScoreFromRecord;
|