@dgpholdings/greatoak-shared 1.1.36 → 1.1.37

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.
@@ -1,4 +1,5 @@
1
- import { TRecord, TRecordBodyWeight, TRecordDuration, TRecordWeight } from "../types/TApiExerciseRecord";
1
+ import { TRecord, TRecordBodyWeight, TRecordDistance, TRecordDuration, TRecordWeight } from "../types/TApiExerciseRecord";
2
2
  export declare const isRecordWeightTypeGuard: (param: TRecord[]) => param is TRecordWeight[];
3
3
  export declare const isRecordDurationTypeGuard: (param: TRecord[]) => param is TRecordDuration[];
4
4
  export declare const isRecordBodyWeightTypeGuard: (param: TRecord[]) => param is TRecordBodyWeight[];
5
+ export declare const isRecordDistanceTypeGuard: (param: TRecord[]) => param is TRecordDistance[];
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isRecordBodyWeightTypeGuard = exports.isRecordDurationTypeGuard = exports.isRecordWeightTypeGuard = void 0;
3
+ exports.isRecordDistanceTypeGuard = exports.isRecordBodyWeightTypeGuard = exports.isRecordDurationTypeGuard = exports.isRecordWeightTypeGuard = void 0;
4
4
  const isRecordWeightTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "weight-reps"; };
5
5
  exports.isRecordWeightTypeGuard = isRecordWeightTypeGuard;
6
6
  const isRecordDurationTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "duration"; };
7
7
  exports.isRecordDurationTypeGuard = isRecordDurationTypeGuard;
8
8
  const isRecordBodyWeightTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "reps-only"; };
9
9
  exports.isRecordBodyWeightTypeGuard = isRecordBodyWeightTypeGuard;
10
+ const isRecordDistanceTypeGuard = (param) => param[0].type === "distance";
11
+ exports.isRecordDistanceTypeGuard = isRecordDistanceTypeGuard;
@@ -78,6 +78,7 @@ export type TUser = {
78
78
  billingPlanType: "monthly" | "annually" | "free";
79
79
  billingPlanVersion?: string;
80
80
  billingPlanGeoCountryCode: string;
81
+ fitnessLevel?: number;
81
82
  paymentMethod?: "credit_card" | "paypal" | "apple_pay" | "upi";
82
83
  billingPlanNextDueDate?: string;
83
84
  devices: {
@@ -91,6 +92,7 @@ export type TUser = {
91
92
  dob?: Date;
92
93
  weightKg?: number;
93
94
  heightCm?: number;
95
+ bodyFatPercentage?: number;
94
96
  };
95
97
  export type TUserSignInData = Pick<TUser, "email" | "isVerified" | "appLanguage" | "billingPlanType" | "billingPlanVersion" | "paymentMethod" | "authMethod" | "dob" | "weightKg" | "gender"> & {
96
98
  createdAt: Date;
@@ -39,7 +39,6 @@ export type TExercise = {
39
39
  secondaryMuscles: TBodyPartKeys;
40
40
  trainingTypes: TTrainingType[];
41
41
  avgSetDurationInSecs?: number;
42
- difficultyScoreMultiplier: number;
43
42
  difficultyLevel: number;
44
43
  hypertrophyLevel: number;
45
44
  strengthGainLevel: number;
@@ -47,6 +46,27 @@ export type TExercise = {
47
46
  calorieBurnLevel: number;
48
47
  stabilityLevel: number;
49
48
  enduranceLevel: number;
49
+ metabolicData: {
50
+ baseMET: number;
51
+ metRange: [number, number];
52
+ compoundMultiplier: number;
53
+ muscleGroupFactor: number;
54
+ intensityScaling: "linear" | "exponential" | "plateau";
55
+ epocFactor: number;
56
+ weightFactors?: {
57
+ lightWeight: number;
58
+ moderateWeight: number;
59
+ heavyWeight: number;
60
+ };
61
+ durationFactors?: {
62
+ shortDuration: number;
63
+ mediumDuration: number;
64
+ longDuration: number;
65
+ };
66
+ paceFactors?: {
67
+ [key: string]: number;
68
+ };
69
+ };
50
70
  youtubeVideoUrl: string[];
51
71
  modelVideoUrl: string;
52
72
  thumbnailUrl: string;
@@ -1,9 +1,12 @@
1
+ import { TGender } from "./TApiAuth";
1
2
  export type TUserMetric = {
2
3
  dob: Date;
3
4
  weightKg?: number;
5
+ heightCm?: number;
4
6
  appLanguage: string;
5
7
  emailAddress?: string;
6
- gender: "male" | "female" | "unmentioned";
8
+ gender: TGender;
9
+ fitnessLevel?: 1 | 2 | 3 | 4 | 5;
7
10
  createdAt: Date;
8
11
  updatedAt?: Date;
9
12
  };
@@ -4,4 +4,7 @@ export { countryToCurrencyCode } from "./billing.utils";
4
4
  export { computeScoreFromRecord } from "./scoring.utils";
5
5
  export { isDefined, isDefinedNumber } from "./isDefined.utils";
6
6
  export { refineRecordEntry } from "./record.utils";
7
+ export { generateWorkoutSummaryText, calculateWorkoutSummary, } from "./workoutSummary.util";
7
8
  export type { TRefinedRecord, TRefinedBodyWeightRecord, TRefinedDurationRecord, TRefinedWeightRecord, } from "./record.utils";
9
+ export type { TUserProfile, TScoreBreakdown } from "./scoring.utils";
10
+ export type { TWorkoutSummary, TExerciseSummary, TSetSummary, TMuscleRecoveryMap, } from "./workoutSummary.util";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.refineRecordEntry = exports.isDefinedNumber = exports.isDefined = exports.computeScoreFromRecord = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
3
+ exports.calculateWorkoutSummary = exports.generateWorkoutSummaryText = exports.refineRecordEntry = exports.isDefinedNumber = exports.isDefined = exports.computeScoreFromRecord = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
4
4
  var number_util_1 = require("./number.util");
5
5
  Object.defineProperty(exports, "toNumber", { enumerable: true, get: function () { return number_util_1.toNumber; } });
6
6
  var time_util_1 = require("./time.util");
@@ -16,3 +16,6 @@ Object.defineProperty(exports, "isDefined", { enumerable: true, get: function ()
16
16
  Object.defineProperty(exports, "isDefinedNumber", { enumerable: true, get: function () { return isDefined_utils_1.isDefinedNumber; } });
17
17
  var record_utils_1 = require("./record.utils");
18
18
  Object.defineProperty(exports, "refineRecordEntry", { enumerable: true, get: function () { return record_utils_1.refineRecordEntry; } });
19
+ var workoutSummary_util_1 = require("./workoutSummary.util");
20
+ Object.defineProperty(exports, "generateWorkoutSummaryText", { enumerable: true, get: function () { return workoutSummary_util_1.generateWorkoutSummaryText; } });
21
+ Object.defineProperty(exports, "calculateWorkoutSummary", { enumerable: true, get: function () { return workoutSummary_util_1.calculateWorkoutSummary; } });
@@ -1,10 +1,15 @@
1
1
  import { TRefinedRecord } from "./record.utils";
2
- type TUserProfile = {
2
+ import { TExercise, TGender, TUserMetric } from "../types";
3
+ export type TUserProfile = {
3
4
  age?: number;
4
- gender?: "male" | "female" | "non-binary" | "unspecified";
5
+ gender?: TGender;
5
6
  weightKg?: number;
7
+ heightCm?: number;
8
+ bodyFatPercentage?: number;
9
+ fitnessLevel?: TUserMetric["fitnessLevel"];
10
+ fitnessGoal?: "strength" | "hypertrophy" | "endurance" | "general";
6
11
  };
7
- type TScoreBreakdown = {
12
+ export type TScoreBreakdown = {
8
13
  baseScore: number;
9
14
  plus: {
10
15
  [reason: string]: number;
@@ -13,18 +18,19 @@ type TScoreBreakdown = {
13
18
  [reason: string]: number;
14
19
  }[];
15
20
  finalScore: number;
21
+ caloriesBurned: number;
22
+ metValue: number;
23
+ epocCalories: number;
16
24
  };
17
25
  type TParams = {
18
26
  record: TRefinedRecord;
27
+ exercise: TExercise;
19
28
  userProfile?: TUserProfile;
20
29
  avgRestDurationSecs?: number;
21
- exerciseDefaultDifficultyMultiplier?: number;
22
30
  isTimeIntervalModeEnabled?: boolean;
23
- sessionSetCount?: number;
24
31
  };
25
32
  /**
26
- * Compute the training stress score for a given record.
27
- * Uses improved load calculations with type-specific normalization and MET values for cardio.
33
+ * Enhanced training stress score computation using metabolic data
28
34
  */
29
- export declare const computeScoreFromRecord: ({ exerciseDefaultDifficultyMultiplier, avgRestDurationSecs, record, userProfile, isTimeIntervalModeEnabled, sessionSetCount, }: TParams) => TScoreBreakdown;
35
+ export declare const computeScoreFromRecord: ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled, }: TParams) => TScoreBreakdown;
30
36
  export {};
@@ -3,151 +3,309 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeScoreFromRecord = void 0;
4
4
  const time_util_1 = require("./time.util");
5
5
  /**
6
- * Compute the training stress score for a given record.
7
- * Uses improved load calculations with type-specific normalization and MET values for cardio.
6
+ * Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
8
7
  */
9
- const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRestDurationSecs, record, userProfile, isTimeIntervalModeEnabled = false, sessionSetCount = 1, }) => {
10
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
11
- if (!record.isDone) {
12
- return {
13
- baseScore: 0,
14
- plus: [],
15
- minus: [],
16
- finalScore: 0,
17
- };
8
+ const calculateBMR = (userProfile) => {
9
+ var _a, _b, _c;
10
+ const weight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
11
+ const height = (_b = userProfile.heightCm) !== null && _b !== void 0 ? _b : 170;
12
+ const age = (_c = userProfile.age) !== null && _c !== void 0 ? _c : 30;
13
+ let bmr;
14
+ if (userProfile.gender === "female") {
15
+ bmr = 10 * weight + 6.25 * height - 5 * age - 161;
18
16
  }
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;
17
+ else {
18
+ bmr = 10 * weight + 6.25 * height - 5 * age + 5;
19
+ }
20
+ // Adjust for body composition if available
21
+ if (userProfile.bodyFatPercentage) {
22
+ const leanBodyMass = weight * (1 - userProfile.bodyFatPercentage / 100);
23
+ const lbmAdjustment = leanBodyMass * 0.56;
24
+ bmr += lbmAdjustment;
25
+ }
26
+ return Math.max(1200, bmr);
27
+ };
28
+ /**
29
+ * Calculate precise MET value using exercise metabolic data
30
+ */
31
+ const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
32
+ var _a, _b, _c, _d, _e;
33
+ const { metabolicData } = exercise;
34
+ const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
35
+ const fitnessLevel = (_b = userProfile.fitnessLevel) !== null && _b !== void 0 ? _b : 3;
36
+ // Fitness level efficiency: trained individuals are more efficient
37
+ const efficiencyFactor = 1 - (fitnessLevel - 1) * 0.04; // 4% more efficient per level
38
+ let calculatedMET = metabolicData.baseMET;
39
+ // Apply compound movement multiplier
40
+ calculatedMET *= metabolicData.compoundMultiplier;
41
+ // Apply muscle group factor
42
+ calculatedMET *= metabolicData.muscleGroupFactor / 2; // Normalize to reasonable range
43
+ // Record type specific adjustments
25
44
  switch (record.type) {
26
45
  case "weight-reps": {
27
- rawLoad = record.kg * record.reps;
28
- normalizedLoad = rawLoad; // Baseline - no scaling needed
46
+ if (metabolicData.weightFactors) {
47
+ const weightRatio = record.kg / userWeight;
48
+ let weightMultiplier = metabolicData.weightFactors.moderateWeight;
49
+ if (weightRatio < 0.5) {
50
+ weightMultiplier = metabolicData.weightFactors.lightWeight;
51
+ }
52
+ else if (weightRatio > 1.0) {
53
+ weightMultiplier = metabolicData.weightFactors.heavyWeight;
54
+ }
55
+ calculatedMET *= weightMultiplier;
56
+ }
57
+ // Apply rep-based intensity scaling
58
+ const repAdjustment = getRepIntensityAdjustment(record.reps, metabolicData.intensityScaling);
59
+ calculatedMET *= repAdjustment;
29
60
  break;
30
61
  }
31
- case "duration": {
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
62
+ case "reps-only": {
63
+ // High rep bodyweight exercises get metabolic bonus
64
+ const repAdjustment = getRepIntensityAdjustment(record.reps, metabolicData.intensityScaling);
65
+ calculatedMET *= repAdjustment;
66
+ // Bodyweight exercises scale with user weight efficiency
67
+ if (userWeight > 80) {
68
+ calculatedMET *= 1.1; // Heavier people work harder
38
69
  }
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
70
+ else if (userWeight < 60) {
71
+ calculatedMET *= 0.95; // Lighter people have slight advantage
43
72
  }
44
73
  break;
45
74
  }
46
- case "reps-only": {
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
75
+ case "duration": {
76
+ const durationSecs = (0, time_util_1.mmssToSecs)((_c = record.durationSecs) !== null && _c !== void 0 ? _c : "00:00");
77
+ if (metabolicData.durationFactors) {
78
+ let durationMultiplier = metabolicData.durationFactors.mediumDuration;
79
+ if (durationSecs < 30) {
80
+ durationMultiplier = metabolicData.durationFactors.shortDuration;
81
+ }
82
+ else if (durationSecs > 120) {
83
+ durationMultiplier = metabolicData.durationFactors.longDuration;
84
+ }
85
+ calculatedMET *= durationMultiplier;
86
+ }
52
87
  break;
53
88
  }
54
89
  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
90
+ const distance = (_d = record.distanceKm) !== null && _d !== void 0 ? _d : 0;
91
+ const durationSecs = (0, time_util_1.mmssToSecs)((_e = record.durationSecs) !== null && _e !== void 0 ? _e : "00:00");
92
+ if (distance > 0 && durationSecs > 0 && metabolicData.paceFactors) {
59
93
  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
94
+ // Find closest pace factor
95
+ let closestMET = metabolicData.baseMET;
96
+ let closestSpeed = Infinity;
97
+ const paceKeys = Object.keys(metabolicData.paceFactors);
98
+ for (let i = 0; i < paceKeys.length; i++) {
99
+ const paceStr = paceKeys[i];
100
+ const metVal = metabolicData.paceFactors[paceStr];
101
+ const pace = parseFloat(paceStr);
102
+ if (Math.abs(speedKmh - pace) < Math.abs(speedKmh - closestSpeed)) {
103
+ closestSpeed = pace;
104
+ closestMET = metVal;
105
+ }
106
+ }
107
+ calculatedMET = closestMET;
76
108
  }
77
109
  break;
78
110
  }
79
111
  }
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
112
+ // Apply effort factor (RPE/RIR influence on MET)
113
+ const effortMultiplier = 0.7 + (effortFactor / 10) * 0.5; // 0.7 to 1.2x
114
+ calculatedMET *= effortMultiplier;
115
+ // Apply interval training bonus
116
+ if (record.workDurationSecs && record.restDurationSecs) {
117
+ const workToRestRatio = record.workDurationSecs / record.restDurationSecs;
118
+ const intervalBonus = 1 + Math.min(0.3, workToRestRatio * 0.08); // Up to 30% bonus
119
+ calculatedMET *= intervalBonus;
120
+ }
121
+ // Apply fitness level efficiency
122
+ calculatedMET *= efficiencyFactor;
123
+ // Ensure MET stays within exercise's defined range
124
+ const [minMET, maxMET] = metabolicData.metRange;
125
+ calculatedMET = Math.max(minMET, Math.min(maxMET, calculatedMET));
126
+ return parseFloat(calculatedMET.toFixed(1));
127
+ };
128
+ /**
129
+ * Get rep-based intensity adjustment
130
+ */
131
+ const getRepIntensityAdjustment = (reps, scalingType) => {
132
+ const baseReps = 8; // Reference point
133
+ const repDiff = reps - baseReps;
134
+ switch (scalingType) {
135
+ case "linear":
136
+ return Math.max(0.7, 1 + repDiff * 0.02); // 2% per rep difference
137
+ case "exponential":
138
+ return Math.max(0.7, Math.pow(1.04, repDiff)); // Exponential growth
139
+ case "plateau":
140
+ // Plateaus at high reps but grows quickly initially
141
+ if (reps > 20)
142
+ return 1.3;
143
+ return Math.max(0.7, 1 + repDiff * 0.04);
144
+ default:
145
+ return 1.0;
146
+ }
147
+ };
148
+ /**
149
+ * Calculate exercise duration in minutes
150
+ */
151
+ const getExerciseDurationMinutes = (record, exercise) => {
152
+ var _a, _b;
153
+ let durationSecs = 0;
154
+ switch (record.type) {
155
+ case "weight-reps":
156
+ case "reps-only": {
157
+ if (exercise.avgSetDurationInSecs) {
158
+ durationSecs = exercise.avgSetDurationInSecs;
159
+ }
160
+ else {
161
+ // Estimate based on reps and exercise complexity
162
+ const reps = record.type === "weight-reps" ? record.reps : record.reps;
163
+ const baseTimePerRep = exercise.difficultyLevel > 7 ? 3.5 : 2.5;
164
+ durationSecs = reps * baseTimePerRep;
165
+ }
166
+ break;
167
+ }
168
+ case "duration": {
169
+ durationSecs = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
170
+ break;
171
+ }
172
+ case "distance": {
173
+ durationSecs = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
174
+ break;
175
+ }
176
+ }
177
+ // Add rest time if available (but not for distance exercises)
178
+ if (record.type !== "distance" && record.restDurationSecs) {
179
+ durationSecs += record.restDurationSecs;
180
+ }
181
+ return Math.max(0.5, durationSecs / 60);
182
+ };
183
+ /**
184
+ * Calculate comprehensive calorie burn including EPOC
185
+ */
186
+ const calculateCalorieBurn = (record, exercise, metValue, durationMinutes, userProfile) => {
187
+ var _a;
188
+ const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
189
+ const bmr = calculateBMR(userProfile);
190
+ const bmrPerMinute = bmr / (24 * 60);
191
+ // Exercise calories: MET × weight(kg) × duration(hours)
192
+ const exerciseCalories = metValue * userWeight * (durationMinutes / 60);
193
+ // EPOC (Excess Post-Exercise Oxygen Consumption) calories
194
+ const epocCalories = exerciseCalories * exercise.metabolicData.epocFactor;
195
+ // BMR calories during exercise time
196
+ const bmrCaloriesDuringExercise = bmrPerMinute * durationMinutes;
197
+ // Total calories = exercise + EPOC + BMR during exercise
198
+ const totalCalories = exerciseCalories + epocCalories + bmrCaloriesDuringExercise;
199
+ return {
200
+ exerciseCalories: parseFloat(exerciseCalories.toFixed(1)),
201
+ epocCalories: parseFloat(epocCalories.toFixed(1)),
202
+ totalCalories: parseFloat(totalCalories.toFixed(1)),
203
+ };
204
+ };
205
+ /**
206
+ * Enhanced training stress score computation using metabolic data
207
+ */
208
+ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled = false, }) => {
209
+ var _a, _b;
210
+ if (!record.isDone) {
211
+ return {
212
+ baseScore: 0,
213
+ plus: [],
214
+ minus: [],
215
+ finalScore: 0,
216
+ caloriesBurned: 0,
217
+ metValue: 0,
218
+ epocCalories: 0,
219
+ };
220
+ }
221
+ const plus = [];
222
+ const minus = [];
223
+ // Calculate effort factor from RPE/RIR
224
+ let effortFactor = 5; // Default moderate effort
85
225
  if (typeof record.rpe === "number") {
86
226
  effortFactor = Math.max(1, Math.min(10, record.rpe));
87
227
  }
88
228
  else if (typeof record.rir === "number") {
89
229
  effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
90
230
  }
91
- const effortMultiplier = 0.7 + effortFactor * 0.06; // Range: 0.76 to 1.3
92
- workingScore *= effortMultiplier;
231
+ // Calculate precise MET value using exercise metabolic data
232
+ const metValue = calculateMETValue(record, exercise, effortFactor, userProfile || {});
233
+ // Calculate exercise duration
234
+ const durationMinutes = getExerciseDurationMinutes(record, exercise);
235
+ // Calculate comprehensive calorie burn
236
+ const calorieData = calculateCalorieBurn(record, exercise, metValue, durationMinutes, userProfile || {});
237
+ // Base score from calorie burn (more accurate than arbitrary load calculations)
238
+ const calorieBasedLoad = calorieData.exerciseCalories + calorieData.epocCalories * 2; // EPOC counts double
239
+ const base = Math.log10(1 + Math.max(5, calorieBasedLoad));
240
+ let workingScore = base;
241
+ // Exercise difficulty bonus (from database)
242
+ const difficultyMultiplier = 0.8 + (exercise.difficultyLevel / 10) * 0.4; // 0.8 to 1.2x
243
+ workingScore *= difficultyMultiplier;
93
244
  plus.push({
94
- effortMultiplier: parseFloat((effortMultiplier - 1).toFixed(3)),
245
+ exerciseDifficulty: parseFloat((difficultyMultiplier - 1).toFixed(3)),
95
246
  });
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)) });
247
+ // Goal alignment bonus
248
+ if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.fitnessGoal) {
249
+ let goalMultiplier = 1;
250
+ switch (userProfile.fitnessGoal) {
251
+ case "strength":
252
+ goalMultiplier = 1 + (exercise.strengthGainLevel / 10) * 0.12;
253
+ break;
254
+ case "hypertrophy":
255
+ goalMultiplier = 1 + (exercise.hypertrophyLevel / 10) * 0.12;
256
+ break;
257
+ case "endurance":
258
+ goalMultiplier = 1 + (exercise.enduranceLevel / 10) * 0.12;
259
+ break;
260
+ case "general":
261
+ const avgLevel = (exercise.strengthGainLevel +
262
+ exercise.hypertrophyLevel +
263
+ exercise.enduranceLevel +
264
+ exercise.flexibilityLevel) /
265
+ 4;
266
+ goalMultiplier = 1 + (avgLevel / 10) * 0.08;
267
+ break;
268
+ }
269
+ if (goalMultiplier > 1) {
270
+ workingScore *= goalMultiplier;
271
+ plus.push({ goalAlignment: parseFloat((goalMultiplier - 1).toFixed(3)) });
272
+ }
101
273
  }
102
- // Step 5: Interval training adjustments
274
+ // Stability and complexity bonus
275
+ const stabilityBonus = 1 + (exercise.stabilityLevel / 10) * 0.06;
276
+ workingScore *= stabilityBonus;
277
+ plus.push({ stabilityDemand: parseFloat((stabilityBonus - 1).toFixed(3)) });
278
+ // High calorie burn efficiency bonus
279
+ if (calorieData.exerciseCalories > 50) {
280
+ // High calorie burn exercises
281
+ const efficiencyBonus = 1.05;
282
+ workingScore *= efficiencyBonus;
283
+ plus.push({
284
+ highCalorieBurn: parseFloat((efficiencyBonus - 1).toFixed(3)),
285
+ });
286
+ }
287
+ // Interval training bonus (already calculated in MET, but add scoring bonus)
103
288
  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;
289
+ const intervalScoreBonus = 1.08;
290
+ workingScore *= intervalScoreBonus;
129
291
  plus.push({
130
- intervalTraining: parseFloat((intervalMultiplier - 1).toFixed(3)),
292
+ intervalTraining: parseFloat((intervalScoreBonus - 1).toFixed(3)),
131
293
  });
132
294
  }
133
- // Step 6: User profile adjustments
295
+ // User profile adjustments
134
296
  let profileMultiplier = 1;
135
- // Gender adjustment (women typically have less muscle mass)
136
297
  if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female") {
137
- profileMultiplier *= 1.12;
298
+ profileMultiplier *= 1.08; // Reduced from 1.12 since metabolic calculations are more accurate
138
299
  }
139
- // Age adjustment (strength declines with age)
140
300
  if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) {
141
301
  if (userProfile.age > 50)
142
- profileMultiplier *= 1.1;
302
+ profileMultiplier *= 1.06;
143
303
  if (userProfile.age > 65)
144
- profileMultiplier *= 1.15;
304
+ profileMultiplier *= 1.1;
145
305
  }
146
- // Weight normalization (smaller people work harder relatively)
147
306
  if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
148
307
  const weightAdjustment = 70 / userProfile.weightKg;
149
- // Cap adjustment to prevent extreme values
150
- profileMultiplier *= Math.max(0.7, Math.min(1.4, weightAdjustment));
308
+ profileMultiplier *= Math.max(0.85, Math.min(1.25, weightAdjustment));
151
309
  }
152
310
  if (profileMultiplier !== 1) {
153
311
  workingScore *= profileMultiplier;
@@ -155,38 +313,39 @@ const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRe
155
313
  profileAdjustment: parseFloat((profileMultiplier - 1).toFixed(3)),
156
314
  });
157
315
  }
158
- // Step 7: Rest efficiency penalty
316
+ // Enhanced rest efficiency penalty
159
317
  if (typeof record.restDurationSecs === "number" &&
160
318
  typeof avgRestDurationSecs === "number") {
161
319
  const restUsed = record.restDurationSecs;
162
- const baseGrace = 10; // Base grace period
163
- const effortGrace = effortFactor * 3; // More grace for higher effort
320
+ const baseGrace = 8 + exercise.difficultyLevel * 1.5; // More grace for complex exercises
321
+ const effortGrace = effortFactor * 2.5;
164
322
  const totalGrace = baseGrace + effortGrace;
165
- const maxPenaltyThreshold = totalGrace + 60; // Cap penalty threshold
323
+ const maxPenaltyThreshold = totalGrace + 45;
166
324
  const excessRest = restUsed - avgRestDurationSecs;
167
325
  if (excessRest > totalGrace) {
168
326
  const penaltyExcess = Math.min(excessRest - totalGrace, maxPenaltyThreshold - totalGrace);
169
327
  const penaltyRatio = penaltyExcess / (maxPenaltyThreshold - totalGrace);
170
- const restPenalty = penaltyRatio * 0.15 * workingScore; // Up to 15% penalty
328
+ const restPenalty = penaltyRatio * 0.12 * workingScore; // Reduced penalty
171
329
  workingScore -= restPenalty;
172
330
  minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
173
331
  }
174
332
  }
175
- // Step 8: Work duration adherence (for timed exercises in interval mode)
333
+ // Work duration adherence for interval training
176
334
  if (isTimeIntervalModeEnabled && record.workDurationSecs) {
177
335
  let actualDuration = 0;
178
336
  if (record.type === "duration") {
179
- actualDuration = (0, time_util_1.mmssToSecs)((_h = record.durationSecs) !== null && _h !== void 0 ? _h : "00:00");
337
+ actualDuration = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
180
338
  }
181
339
  else if (record.type === "distance") {
182
- actualDuration = (0, time_util_1.mmssToSecs)((_j = record.durationSecs) !== null && _j !== void 0 ? _j : "00:00");
340
+ actualDuration = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
183
341
  }
184
342
  if (actualDuration > 0) {
185
- const durationDiff = Math.abs(actualDuration - record.workDurationSecs);
186
- const tolerance = record.workDurationSecs * 0.2; // 20% tolerance
343
+ const expectedDuration = exercise.avgSetDurationInSecs || record.workDurationSecs;
344
+ const durationDiff = Math.abs(actualDuration - expectedDuration);
345
+ const tolerance = expectedDuration * 0.25; // 25% tolerance
187
346
  if (durationDiff > tolerance) {
188
- const excessPercent = (durationDiff - tolerance) / record.workDurationSecs;
189
- const timingPenalty = Math.min(0.1 * workingScore, excessPercent * 0.08 * workingScore);
347
+ const excessPercent = (durationDiff - tolerance) / expectedDuration;
348
+ const timingPenalty = Math.min(0.08 * workingScore, excessPercent * 0.05 * workingScore);
190
349
  workingScore -= timingPenalty;
191
350
  minus.push({
192
351
  timingDeviationPenalty: parseFloat(timingPenalty.toFixed(3)),
@@ -194,24 +353,18 @@ const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRe
194
353
  }
195
354
  }
196
355
  }
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
356
+ // Final score normalization
205
357
  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);
358
+ finalScore *= 12; // Scale to intuitive range (0-120+ for typical exercises)
359
+ finalScore = Math.min(200, finalScore); // Cap extreme scores
210
360
  return {
211
361
  baseScore: parseFloat(base.toFixed(2)),
212
362
  plus,
213
363
  minus,
214
364
  finalScore: parseFloat(finalScore.toFixed(1)),
365
+ caloriesBurned: calorieData.totalCalories,
366
+ metValue,
367
+ epocCalories: calorieData.epocCalories,
215
368
  };
216
369
  };
217
370
  exports.computeScoreFromRecord = computeScoreFromRecord;
@@ -0,0 +1,87 @@
1
+ import { TRefinedRecord } from "./record.utils";
2
+ import { TExercise, TBodyPartKeys } from "../types";
3
+ import { TScoreBreakdown, TUserProfile } from "./scoring.utils";
4
+ /**
5
+ * EXERCISE SCORING SYSTEM DOCUMENTATION
6
+ * =====================================
7
+ *
8
+ * What the Score Represents:
9
+ * -------------------------
10
+ * The exercise score is a comprehensive measure of training stress that combines:
11
+ *
12
+ * 1. **Energy Expenditure (40%)**: Actual calories burned during exercise + afterburn (EPOC)
13
+ * 2. **Exercise Complexity (25%)**: Difficulty level, stability demands, muscle groups involved
14
+ * 3. **Effort Level (20%)**: RPE/RIR indicating how hard you worked
15
+ * 4. **Goal Alignment (10%)**: How well exercise matches your fitness goals
16
+ * 5. **Personal Factors (5%)**: Age, gender, weight, fitness level adjustments
17
+ *
18
+ * Score Ranges:
19
+ * -------------
20
+ * - 0-20: Light effort (walking, easy stretching)
21
+ * - 21-40: Moderate effort (bodyweight exercises, light weights)
22
+ * - 41-60: Hard effort (challenging sets, compound movements)
23
+ * - 61-80: Very hard effort (heavy weights, high-intensity cardio)
24
+ * - 81-100: Extremely hard effort (max effort sets, intense HIIT)
25
+ * - 100+: Elite/competition level effort
26
+ *
27
+ * Usage:
28
+ * ------
29
+ * - Each SET gets its own score
30
+ * - Higher scores = higher training stress
31
+ * - Use for tracking progress, planning recovery
32
+ * - Compare different workouts objectively
33
+ */
34
+ export type TWorkoutSummary = {
35
+ totalScore: number;
36
+ averageScore: number;
37
+ totalCalories: number;
38
+ totalEpocCalories: number;
39
+ workoutDuration: number;
40
+ exerciseSummaries: TExerciseSummary[];
41
+ muscleRecovery: TMuscleRecoveryMap;
42
+ fatigueLevel: "light" | "moderate" | "hard" | "very_hard" | "extreme";
43
+ recommendedRestDays: number;
44
+ };
45
+ export type TExerciseSummary = {
46
+ exercise: TExercise;
47
+ sets: TSetSummary[];
48
+ totalScore: number;
49
+ averageScore: number;
50
+ totalCalories: number;
51
+ bestSet: {
52
+ setNumber: number;
53
+ score: number;
54
+ reason: string;
55
+ };
56
+ muscleActivation: {
57
+ primary: number;
58
+ secondary: number;
59
+ };
60
+ };
61
+ export type TSetSummary = {
62
+ setNumber: number;
63
+ record: TRefinedRecord;
64
+ scoreBreakdown: TScoreBreakdown;
65
+ description: string;
66
+ };
67
+ export type TMuscleRecoveryMap = {
68
+ [muscle in TBodyPartKeys[number]]: {
69
+ fatigueLevel: number;
70
+ recoveryHours: number;
71
+ trainingStress: number;
72
+ exercises: string[];
73
+ };
74
+ };
75
+ /**
76
+ * Calculate comprehensive workout summary
77
+ */
78
+ export declare const calculateWorkoutSummary: (exercises: Array<{
79
+ exercise: TExercise;
80
+ records: TRefinedRecord[];
81
+ startTime?: Date;
82
+ endTime?: Date;
83
+ }>, userProfile?: TUserProfile) => TWorkoutSummary;
84
+ /**
85
+ * Generate user-friendly workout summary text
86
+ */
87
+ export declare const generateWorkoutSummaryText: (summary: TWorkoutSummary) => string;
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateWorkoutSummaryText = exports.calculateWorkoutSummary = void 0;
4
+ const scoring_utils_1 = require("./scoring.utils");
5
+ /**
6
+ * Calculate comprehensive workout summary
7
+ */
8
+ const calculateWorkoutSummary = (exercises, userProfile) => {
9
+ var _a, _b;
10
+ let totalScore = 0;
11
+ let totalCalories = 0;
12
+ let totalEpocCalories = 0;
13
+ let totalSets = 0;
14
+ const exerciseSummaries = [];
15
+ const muscleStressMap = new Map();
16
+ // Calculate workout duration
17
+ const workoutStart = ((_a = exercises[0]) === null || _a === void 0 ? void 0 : _a.startTime) || new Date();
18
+ const workoutEnd = ((_b = exercises[exercises.length - 1]) === null || _b === void 0 ? void 0 : _b.endTime) || new Date();
19
+ const workoutDuration = Math.max(1, (workoutEnd.getTime() - workoutStart.getTime()) / (1000 * 60));
20
+ // Process each exercise
21
+ exercises.forEach((exerciseData) => {
22
+ const { exercise, records } = exerciseData;
23
+ const sets = [];
24
+ let exerciseScore = 0;
25
+ let exerciseCalories = 0;
26
+ let bestSetScore = 0;
27
+ let bestSetNumber = 1;
28
+ let bestSetReason = "highest score";
29
+ // Process each set/record
30
+ records.forEach((record, index) => {
31
+ if (!record.isDone)
32
+ return;
33
+ const scoreBreakdown = (0, scoring_utils_1.computeScoreFromRecord)({
34
+ record,
35
+ exercise,
36
+ userProfile,
37
+ isTimeIntervalModeEnabled: !!(record.workDurationSecs && record.restDurationSecs),
38
+ });
39
+ const setNumber = index + 1;
40
+ const setDescription = generateSetDescription(record, exercise, scoreBreakdown);
41
+ sets.push({
42
+ setNumber,
43
+ record,
44
+ scoreBreakdown,
45
+ description: setDescription,
46
+ });
47
+ exerciseScore += scoreBreakdown.finalScore;
48
+ exerciseCalories += scoreBreakdown.caloriesBurned;
49
+ totalSets++;
50
+ // Track best set
51
+ if (scoreBreakdown.finalScore > bestSetScore) {
52
+ bestSetScore = scoreBreakdown.finalScore;
53
+ bestSetNumber = setNumber;
54
+ bestSetReason = getBestSetReason(record, exercise);
55
+ }
56
+ // Accumulate muscle stress
57
+ exercise.primaryMuscles.forEach((muscle) => {
58
+ const currentStress = muscleStressMap.get(muscle) || 0;
59
+ muscleStressMap.set(muscle, currentStress + scoreBreakdown.finalScore * 1.0); // Primary muscles get full stress
60
+ });
61
+ exercise.secondaryMuscles.forEach((muscle) => {
62
+ const currentStress = muscleStressMap.get(muscle) || 0;
63
+ muscleStressMap.set(muscle, currentStress + scoreBreakdown.finalScore * 0.6); // Secondary muscles get 60% stress
64
+ });
65
+ });
66
+ if (sets.length > 0) {
67
+ exerciseSummaries.push({
68
+ exercise,
69
+ sets,
70
+ totalScore: exerciseScore,
71
+ averageScore: exerciseScore / sets.length,
72
+ totalCalories: exerciseCalories,
73
+ bestSet: {
74
+ setNumber: bestSetNumber,
75
+ score: bestSetScore,
76
+ reason: bestSetReason,
77
+ },
78
+ muscleActivation: calculateMuscleActivation(exercise, sets),
79
+ });
80
+ }
81
+ });
82
+ // Calculate totals
83
+ totalScore = exerciseSummaries.reduce((sum, ex) => sum + ex.totalScore, 0);
84
+ totalCalories = exerciseSummaries.reduce((sum, ex) => sum + ex.totalCalories, 0);
85
+ totalEpocCalories = exerciseSummaries.reduce((sum, ex) => sum +
86
+ ex.sets.reduce((setSum, set) => setSum + set.scoreBreakdown.epocCalories, 0), 0);
87
+ // Calculate muscle recovery
88
+ const muscleRecovery = calculateMuscleRecovery(muscleStressMap, exercises);
89
+ // Determine overall fatigue level
90
+ const averageScore = totalSets > 0 ? totalScore / totalSets : 0;
91
+ const fatigueLevel = getFatigueLevel(averageScore, totalScore, workoutDuration);
92
+ const recommendedRestDays = getRecommendedRestDays(fatigueLevel, muscleRecovery);
93
+ return {
94
+ totalScore: Math.round(totalScore),
95
+ averageScore: Math.round(averageScore),
96
+ totalCalories: Math.round(totalCalories),
97
+ totalEpocCalories: Math.round(totalEpocCalories),
98
+ workoutDuration: Math.round(workoutDuration),
99
+ exerciseSummaries,
100
+ muscleRecovery,
101
+ fatigueLevel,
102
+ recommendedRestDays,
103
+ };
104
+ };
105
+ exports.calculateWorkoutSummary = calculateWorkoutSummary;
106
+ /**
107
+ * Generate human-readable set description
108
+ */
109
+ const generateSetDescription = (record, exercise, scoreBreakdown) => {
110
+ const effort = record.rpe
111
+ ? `RPE ${record.rpe}`
112
+ : record.rir
113
+ ? `${record.rir} RIR`
114
+ : "";
115
+ const calories = `${Math.round(scoreBreakdown.caloriesBurned)} cal`;
116
+ switch (record.type) {
117
+ case "weight-reps":
118
+ return `${record.kg}kg × ${record.reps} reps ${effort} • ${calories}`;
119
+ case "reps-only":
120
+ const auxWeight = record.auxWeightKg ? ` (+${record.auxWeightKg}kg)` : "";
121
+ return `${record.reps} reps${auxWeight} ${effort} • ${calories}`;
122
+ case "duration":
123
+ const auxWeightDur = record.auxWeightKg
124
+ ? ` (+${record.auxWeightKg}kg)`
125
+ : "";
126
+ return `${record.durationSecs}${auxWeightDur} ${effort} • ${calories}`;
127
+ case "distance":
128
+ let pace = "";
129
+ if (record.avgPaceSecsPerKm) {
130
+ const minutes = Math.floor(record.avgPaceSecsPerKm / 60);
131
+ const seconds = (record.avgPaceSecsPerKm % 60).toFixed(0);
132
+ const secondsFormatted = seconds.length === 1 ? "0" + seconds : seconds;
133
+ pace = ` (${minutes}:${secondsFormatted}/km)`;
134
+ }
135
+ return `${record.distanceKm}km in ${record.durationSecs}${pace} ${effort} • ${calories}`;
136
+ default:
137
+ return `${effort} • ${calories}`;
138
+ }
139
+ };
140
+ /**
141
+ * Determine reason for best set
142
+ */
143
+ const getBestSetReason = (record, exercise) => {
144
+ switch (record.type) {
145
+ case "weight-reps":
146
+ return "heaviest weight";
147
+ case "reps-only":
148
+ return "most reps";
149
+ case "duration":
150
+ return "longest hold";
151
+ case "distance":
152
+ return "fastest pace";
153
+ default:
154
+ return "highest effort";
155
+ }
156
+ };
157
+ /**
158
+ * Calculate muscle activation levels
159
+ */
160
+ const calculateMuscleActivation = (exercise, sets) => {
161
+ if (sets.length === 0)
162
+ return { primary: 0, secondary: 0 };
163
+ const avgScore = sets.reduce((sum, set) => sum + set.scoreBreakdown.finalScore, 0) /
164
+ sets.length;
165
+ const maxPossibleScore = 100; // Reference point
166
+ // Activation based on exercise difficulty and average set score
167
+ const baseActivation = Math.min(100, (avgScore / maxPossibleScore) * 100);
168
+ const difficultyBonus = exercise.difficultyLevel * 2; // Up to 20% bonus
169
+ return {
170
+ primary: Math.min(100, Math.round(baseActivation + difficultyBonus)),
171
+ secondary: Math.min(100, Math.round((baseActivation + difficultyBonus) * 0.7)), // 70% of primary
172
+ };
173
+ };
174
+ /**
175
+ * Calculate muscle recovery times
176
+ */
177
+ const calculateMuscleRecovery = (muscleStressMap, exercises) => {
178
+ const recovery = {};
179
+ muscleStressMap.forEach((totalStress, muscle) => {
180
+ // Base recovery time based on muscle group
181
+ const baseRecovery = getMuscleBaseRecovery(muscle);
182
+ // Stress level (0-100)
183
+ const fatigueLevel = Math.min(100, (totalStress / 200) * 100); // 200 = very high stress threshold
184
+ // Recovery time increases with fatigue level
185
+ let recoveryHours = baseRecovery;
186
+ if (fatigueLevel > 80)
187
+ recoveryHours *= 1.5; // 50% longer for high fatigue
188
+ else if (fatigueLevel > 60)
189
+ recoveryHours *= 1.3; // 30% longer for moderate-high fatigue
190
+ else if (fatigueLevel > 40)
191
+ recoveryHours *= 1.1; // 10% longer for moderate fatigue
192
+ // Find exercises that worked this muscle - using indexOf instead of includes
193
+ const involvedExercises = exercises
194
+ .filter(({ exercise }) => exercise.primaryMuscles.indexOf(muscle) !==
195
+ -1 ||
196
+ exercise.secondaryMuscles.indexOf(muscle) !==
197
+ -1)
198
+ .map(({ exercise }) => exercise.name);
199
+ recovery[muscle] = {
200
+ fatigueLevel: Math.round(fatigueLevel),
201
+ recoveryHours: Math.round(recoveryHours),
202
+ trainingStress: Math.round(totalStress),
203
+ exercises: involvedExercises,
204
+ };
205
+ });
206
+ return recovery;
207
+ };
208
+ /**
209
+ * Get base recovery time for different muscle groups
210
+ */
211
+ const getMuscleBaseRecovery = (muscle) => {
212
+ // Recovery times in hours based on muscle size and recovery capacity
213
+ const recoveryTimes = {
214
+ "pectoralis-major": 48,
215
+ "pectoralis-minor": 48,
216
+ "latissimus-dorsi": 48,
217
+ trapezius: 36,
218
+ rhomboids: 36,
219
+ "deltoids-anterior": 36,
220
+ "deltoids-middle": 36,
221
+ "bicep-short-inner": 24,
222
+ "bicep-long-outer": 24,
223
+ "tricep-brachii-long": 24,
224
+ "tricep-brachii-lateral": 24,
225
+ quadriceps: 48,
226
+ hamstrings: 48,
227
+ "maximus-lower": 48,
228
+ "medius-upper": 36,
229
+ "calf-inner": 24,
230
+ "calf-outer": 24,
231
+ "abs-upper": 24,
232
+ "abs-lower": 24,
233
+ obliques: 24,
234
+ "lower-back": 48,
235
+ "fore-arm-inner": 18,
236
+ "fore-arm-outer": 18,
237
+ };
238
+ return recoveryTimes[muscle] || 36; // Default 36 hours
239
+ };
240
+ /**
241
+ * Determine overall fatigue level
242
+ */
243
+ const getFatigueLevel = (averageScore, totalScore, workoutDuration) => {
244
+ const intensity = averageScore;
245
+ const volume = totalScore / 100; // Normalize volume
246
+ const density = totalScore / workoutDuration; // Score per minute
247
+ if (intensity > 80 || volume > 15 || density > 8)
248
+ return "extreme";
249
+ if (intensity > 65 || volume > 12 || density > 6)
250
+ return "very_hard";
251
+ if (intensity > 50 || volume > 8 || density > 4)
252
+ return "hard";
253
+ if (intensity > 35 || volume > 5 || density > 2)
254
+ return "moderate";
255
+ return "light";
256
+ };
257
+ /**
258
+ * Get recommended rest days based on fatigue
259
+ */
260
+ const getRecommendedRestDays = (fatigueLevel, muscleRecovery) => {
261
+ // Base rest days by fatigue level
262
+ const baseDays = {
263
+ light: 0,
264
+ moderate: 1,
265
+ hard: 1,
266
+ very_hard: 2,
267
+ extreme: 3,
268
+ };
269
+ const restDays = baseDays[fatigueLevel];
270
+ // Consider muscle recovery times - using manual iteration instead of Object.values
271
+ const recoveryHours = [];
272
+ for (const muscle in muscleRecovery) {
273
+ if (muscleRecovery.hasOwnProperty(muscle)) {
274
+ recoveryHours.push(muscleRecovery[muscle].recoveryHours);
275
+ }
276
+ }
277
+ const maxRecoveryHours = recoveryHours.length > 0 ? Math.max.apply(Math, recoveryHours) : 0;
278
+ const muscleBasedDays = Math.ceil(maxRecoveryHours / 24);
279
+ return Math.max(restDays, muscleBasedDays);
280
+ };
281
+ /**
282
+ * Generate user-friendly workout summary text
283
+ */
284
+ const generateWorkoutSummaryText = (summary) => {
285
+ const { totalScore, averageScore, totalCalories, totalEpocCalories, workoutDuration, fatigueLevel, recommendedRestDays, exerciseSummaries, } = summary;
286
+ const intensityMap = {
287
+ light: "Light workout",
288
+ moderate: "Moderate workout",
289
+ hard: "Hard workout",
290
+ very_hard: "Very hard workout",
291
+ extreme: "Extreme workout",
292
+ };
293
+ const intensityText = intensityMap[fatigueLevel];
294
+ const restText = recommendedRestDays === 0
295
+ ? "You can train again tomorrow"
296
+ : `Recommended rest: ${recommendedRestDays} day${recommendedRestDays > 1 ? "s" : ""}`;
297
+ return `
298
+ 🏋️ **${intensityText}** (Score: ${totalScore})
299
+ ⏱️ Duration: ${workoutDuration} minutes
300
+ 🔥 Calories: ${totalCalories} + ${totalEpocCalories} afterburn
301
+ 📊 Average set intensity: ${averageScore}/100
302
+
303
+ 💪 **Exercises completed:**
304
+ ${exerciseSummaries
305
+ .map((ex) => `• ${ex.exercise.name}: ${ex.sets.length} sets (${Math.round(ex.totalScore)} pts)`)
306
+ .join("\n")}
307
+
308
+ 🛌 **Recovery:** ${restText}
309
+ `.trim();
310
+ };
311
+ exports.generateWorkoutSummaryText = generateWorkoutSummaryText;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.1.36",
3
+ "version": "1.1.37",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",