@dgpholdings/greatoak-shared 1.1.35 → 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;
@@ -2,7 +2,8 @@ export type TRecord = {
2
2
  isDone: boolean;
3
3
  rpe?: string;
4
4
  setNote?: string;
5
- restInBetweenDurationSecs?: string;
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
- enableRestTime?: boolean;
45
+ enableTimeIntervalMode?: boolean;
45
46
  };
46
47
  export type TApiExerciseRecordUpdateReq = {
47
48
  templateId: 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; } });
@@ -3,7 +3,8 @@ type RefinedBase = {
3
3
  isDone: boolean;
4
4
  rpe?: number;
5
5
  rir?: number;
6
- restInBetweenDurationSecs?: number;
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 TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord;
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)((0, number_util_1.toNumber)(entry.restInBetweenDurationSecs)) && {
9
- restInBetweenDurationSecs: (0, number_util_1.toNumber)(entry.restInBetweenDurationSecs),
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;
@@ -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,20 +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
- avgRestInBetweenDurationSecs?: number;
21
- exerciseDefaultDifficultyMultiplier?: number;
29
+ avgRestDurationSecs?: number;
30
+ isTimeIntervalModeEnabled?: boolean;
22
31
  };
23
32
  /**
24
- * 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
33
+ * Enhanced training stress score computation using metabolic data
30
34
  */
31
- export declare const computeScoreFromRecord: ({ exerciseDefaultDifficultyMultiplier, avgRestInBetweenDurationSecs, record, userProfile, }: TParams) => TScoreBreakdown;
35
+ export declare const computeScoreFromRecord: ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled, }: TParams) => TScoreBreakdown;
32
36
  export {};
@@ -3,102 +3,368 @@ 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
- *
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
6
+ * Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
12
7
  */
13
- const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRestInBetweenDurationSecs, record, userProfile, }) => {
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;
16
+ }
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) => {
14
32
  var _a, _b, _c, _d, _e;
15
- if (!record.isDone) {
16
- return {
17
- baseScore: 0,
18
- plus: [],
19
- minus: [],
20
- finalScore: 0,
21
- };
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
44
+ switch (record.type) {
45
+ case "weight-reps": {
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;
60
+ break;
61
+ }
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
69
+ }
70
+ else if (userWeight < 60) {
71
+ calculatedMET *= 0.95; // Lighter people have slight advantage
72
+ }
73
+ break;
74
+ }
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
+ }
87
+ break;
88
+ }
89
+ case "distance": {
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) {
93
+ const speedKmh = (distance * 3600) / durationSecs;
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;
108
+ }
109
+ break;
110
+ }
111
+ }
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;
22
146
  }
23
- // Step 1: Compute load
24
- let load = 0;
147
+ };
148
+ /**
149
+ * Calculate exercise duration in minutes
150
+ */
151
+ const getExerciseDurationMinutes = (record, exercise) => {
152
+ var _a, _b;
153
+ let durationSecs = 0;
25
154
  switch (record.type) {
26
155
  case "weight-reps":
27
- load = record.kg * record.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
+ }
28
166
  break;
167
+ }
29
168
  case "duration": {
30
- const durationSecs = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
31
- const auxWeight = (_b = record.auxWeightKg) !== null && _b !== void 0 ? _b : 0;
32
- load = auxWeight > 0 ? auxWeight * durationSecs : durationSecs;
169
+ durationSecs = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
33
170
  break;
34
171
  }
35
- case "reps-only": {
36
- const reps = (_c = record.reps) !== null && _c !== void 0 ? _c : 0;
37
- const auxWeight = (_d = record.auxWeightKg) !== null && _d !== void 0 ? _d : 0;
38
- const baseWeight = auxWeight > 0 ? auxWeight : (_e = userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) !== null && _e !== void 0 ? _e : 0;
39
- load = baseWeight * reps;
172
+ case "distance": {
173
+ durationSecs = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
40
174
  break;
41
175
  }
42
176
  }
43
- // Step 2: Compute effort
44
- let effortFactor = 0;
45
- if (typeof record.rpe === "number") {
46
- effortFactor = record.rpe;
177
+ // Add rest time if available (but not for distance exercises)
178
+ if (record.type !== "distance" && record.restDurationSecs) {
179
+ durationSecs += record.restDurationSecs;
47
180
  }
48
- else if (typeof record.rir === "number") {
49
- effortFactor = 10 - record.rir;
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
+ };
50
220
  }
51
- // Step 3: Base score using log scale
52
- const base = Math.log10(1 + Math.max(0, load));
53
221
  const plus = [];
54
222
  const minus = [];
55
- let rawScore = base;
56
- // Step 4: Effort Boost
57
- // Clamp effort factor to [0,10] range to avoid invalid boosts
58
- const boundedEffortFactor = Math.max(0, Math.min(effortFactor, 10));
59
- const effortBoost = 1 + boundedEffortFactor * 0.05;
60
- rawScore *= effortBoost;
61
- plus.push({ effortBoost: parseFloat((effortBoost - 1).toFixed(10)) });
62
- // Step 5: User Profile Scaling
63
- let adjustment = 1;
64
- if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female")
65
- adjustment *= 1.1;
66
- if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) && userProfile.age > 50)
67
- adjustment *= 1.15;
68
- if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
69
- const normalizedWeight = 70;
70
- adjustment *= normalizedWeight / userProfile.weightKg;
71
- }
72
- if (adjustment !== 1) {
73
- rawScore *= adjustment;
74
- plus.push({ profileAdjustment: parseFloat((adjustment - 1).toFixed(10)) });
75
- }
76
- // Step 6: Rest Penalty
77
- if (typeof record.restInBetweenDurationSecs === "number" &&
78
- typeof avgRestInBetweenDurationSecs === "number") {
79
- const restUsed = record.restInBetweenDurationSecs;
80
- const grace = 5 + effortFactor * 2;
81
- const cap = grace + 25;
82
- const restOver = restUsed - avgRestInBetweenDurationSecs;
83
- if (restOver > grace) {
84
- const cappedExcess = Math.min(restOver, cap);
85
- const penaltyRatio = (cappedExcess - grace) / (cap - grace); // 0–1
86
- const restPenalty = parseFloat((penaltyRatio * 0.2 * rawScore).toFixed(10));
87
- rawScore -= restPenalty;
88
- minus.push({ restPenalty });
89
- }
223
+ // Calculate effort factor from RPE/RIR
224
+ let effortFactor = 5; // Default moderate effort
225
+ if (typeof record.rpe === "number") {
226
+ effortFactor = Math.max(1, Math.min(10, record.rpe));
90
227
  }
91
- let finalScore = parseFloat(rawScore.toFixed(2));
92
- // Adjusting exerciseDefaultDifficultyMultiplier
93
- finalScore *= exerciseDefaultDifficultyMultiplier;
228
+ else if (typeof record.rir === "number") {
229
+ effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
230
+ }
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;
94
244
  plus.push({
95
- exerciseDefaultDifficultyMultiplier: parseFloat(exerciseDefaultDifficultyMultiplier.toFixed(5)),
245
+ exerciseDifficulty: parseFloat((difficultyMultiplier - 1).toFixed(3)),
96
246
  });
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
+ }
273
+ }
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)
288
+ if (isTimeIntervalModeEnabled) {
289
+ const intervalScoreBonus = 1.08;
290
+ workingScore *= intervalScoreBonus;
291
+ plus.push({
292
+ intervalTraining: parseFloat((intervalScoreBonus - 1).toFixed(3)),
293
+ });
294
+ }
295
+ // User profile adjustments
296
+ let profileMultiplier = 1;
297
+ if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female") {
298
+ profileMultiplier *= 1.08; // Reduced from 1.12 since metabolic calculations are more accurate
299
+ }
300
+ if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) {
301
+ if (userProfile.age > 50)
302
+ profileMultiplier *= 1.06;
303
+ if (userProfile.age > 65)
304
+ profileMultiplier *= 1.1;
305
+ }
306
+ if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
307
+ const weightAdjustment = 70 / userProfile.weightKg;
308
+ profileMultiplier *= Math.max(0.85, Math.min(1.25, weightAdjustment));
309
+ }
310
+ if (profileMultiplier !== 1) {
311
+ workingScore *= profileMultiplier;
312
+ plus.push({
313
+ profileAdjustment: parseFloat((profileMultiplier - 1).toFixed(3)),
314
+ });
315
+ }
316
+ // Enhanced rest efficiency penalty
317
+ if (typeof record.restDurationSecs === "number" &&
318
+ typeof avgRestDurationSecs === "number") {
319
+ const restUsed = record.restDurationSecs;
320
+ const baseGrace = 8 + exercise.difficultyLevel * 1.5; // More grace for complex exercises
321
+ const effortGrace = effortFactor * 2.5;
322
+ const totalGrace = baseGrace + effortGrace;
323
+ const maxPenaltyThreshold = totalGrace + 45;
324
+ const excessRest = restUsed - avgRestDurationSecs;
325
+ if (excessRest > totalGrace) {
326
+ const penaltyExcess = Math.min(excessRest - totalGrace, maxPenaltyThreshold - totalGrace);
327
+ const penaltyRatio = penaltyExcess / (maxPenaltyThreshold - totalGrace);
328
+ const restPenalty = penaltyRatio * 0.12 * workingScore; // Reduced penalty
329
+ workingScore -= restPenalty;
330
+ minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
331
+ }
332
+ }
333
+ // Work duration adherence for interval training
334
+ if (isTimeIntervalModeEnabled && record.workDurationSecs) {
335
+ let actualDuration = 0;
336
+ if (record.type === "duration") {
337
+ actualDuration = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
338
+ }
339
+ else if (record.type === "distance") {
340
+ actualDuration = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
341
+ }
342
+ if (actualDuration > 0) {
343
+ const expectedDuration = exercise.avgSetDurationInSecs || record.workDurationSecs;
344
+ const durationDiff = Math.abs(actualDuration - expectedDuration);
345
+ const tolerance = expectedDuration * 0.25; // 25% tolerance
346
+ if (durationDiff > tolerance) {
347
+ const excessPercent = (durationDiff - tolerance) / expectedDuration;
348
+ const timingPenalty = Math.min(0.08 * workingScore, excessPercent * 0.05 * workingScore);
349
+ workingScore -= timingPenalty;
350
+ minus.push({
351
+ timingDeviationPenalty: parseFloat(timingPenalty.toFixed(3)),
352
+ });
353
+ }
354
+ }
355
+ }
356
+ // Final score normalization
357
+ let finalScore = Math.max(0, workingScore);
358
+ finalScore *= 12; // Scale to intuitive range (0-120+ for typical exercises)
359
+ finalScore = Math.min(200, finalScore); // Cap extreme scores
97
360
  return {
98
361
  baseScore: parseFloat(base.toFixed(2)),
99
362
  plus,
100
363
  minus,
101
- finalScore,
364
+ finalScore: parseFloat(finalScore.toFixed(1)),
365
+ caloriesBurned: calorieData.totalCalories,
366
+ metValue,
367
+ epocCalories: calorieData.epocCalories,
102
368
  };
103
369
  };
104
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.35",
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",