@dgpholdings/greatoak-shared 1.1.22 → 1.1.23

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.
@@ -39,6 +39,7 @@ export type TExercise = {
39
39
  secondaryMuscles: TBodyPartKeys;
40
40
  trainingTypes: TTrainingType[];
41
41
  avgSetDurationInSecs?: number;
42
+ difficultyScoreMultiplier: number;
42
43
  difficultyLevel: number;
43
44
  hypertrophyLevel: number;
44
45
  strengthGainLevel: number;
@@ -4,16 +4,29 @@ type TUserProfile = {
4
4
  gender?: "male" | "female" | "non-binary" | "unspecified";
5
5
  weightKg?: number;
6
6
  };
7
+ type TScoreBreakdown = {
8
+ baseScore: number;
9
+ plus: {
10
+ [reason: string]: number;
11
+ }[];
12
+ minus: {
13
+ [reason: string]: number;
14
+ }[];
15
+ finalScore: number;
16
+ };
17
+ type TParams = {
18
+ record: TRefinedRecord;
19
+ userProfile?: TUserProfile;
20
+ avgRestInBetweenDurationSecs?: number;
21
+ exerciseDefaultDifficultyMultiplier?: number;
22
+ };
7
23
  /**
8
- * The score represents the training stimulus or workload stress generated by a specific set of exercise inputs.
9
- * in short: Training Stress Score (TSS)
10
- * We can use the score for:
11
- 1.Tracking session intensity or set difficulty over time
12
- 2. Deciding muscle recovery readiness
13
- 3. Creating training summaries, like:
14
- - Daily workload
15
- - Volume comparisons
16
- - Weekly fatigue trends
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
17
30
  */
18
- export declare const computeScoreFromRecord: (record: TRefinedRecord, userProfile?: TUserProfile) => number;
31
+ export declare const computeScoreFromRecord: ({ exerciseDefaultDifficultyMultiplier, avgRestInBetweenDurationSecs, record, userProfile, }: TParams) => TScoreBreakdown;
19
32
  export {};
@@ -3,60 +3,63 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeScoreFromRecord = void 0;
4
4
  const time_util_1 = require("./time.util");
5
5
  /**
6
- * The score represents the training stimulus or workload stress generated by a specific set of exercise inputs.
7
- * in short: Training Stress Score (TSS)
8
- * We can use the score for:
9
- 1.Tracking session intensity or set difficulty over time
10
- 2. Deciding muscle recovery readiness
11
- 3. Creating training summaries, like:
12
- - Daily workload
13
- - Volume comparisons
14
- - Weekly fatigue trends
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
15
12
  */
16
- const computeScoreFromRecord = (record, userProfile) => {
17
- var _a, _b, _c, _d, _e, _f;
18
- if (!record.isDone)
19
- return 0;
20
- // Determine load and effortFactor from record based on type
21
- let load = 0;
22
- let effortFactor = 0;
23
- const rpe = (_a = record.rpe) !== null && _a !== void 0 ? _a : undefined;
24
- const rir = (_b = record.rir) !== null && _b !== void 0 ? _b : undefined;
25
- // Step 1: Determine effort factor
26
- if (typeof rpe === "number") {
27
- effortFactor = rpe;
13
+ const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRestInBetweenDurationSecs, record, userProfile, }) => {
14
+ var _a, _b, _c, _d, _e;
15
+ if (!record.isDone) {
16
+ return {
17
+ baseScore: 0,
18
+ plus: [],
19
+ minus: [],
20
+ finalScore: 0,
21
+ };
28
22
  }
29
- else if (typeof rir === "number") {
30
- effortFactor = 10 - rir; // lower RIR means more effort
31
- }
32
- // Step 2: Determine load
23
+ // Step 1: Compute load
24
+ let load = 0;
33
25
  switch (record.type) {
34
- case "weight-reps": {
26
+ case "weight-reps":
35
27
  load = record.kg * record.reps;
36
28
  break;
37
- }
38
29
  case "duration": {
39
- const durationSecs = (0, time_util_1.mmssToSecs)((_c = record.durationSecs) !== null && _c !== void 0 ? _c : "00:00");
40
- const auxWeight = (_d = record.auxWeightKg) !== null && _d !== void 0 ? _d : 0;
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;
41
32
  load = auxWeight > 0 ? auxWeight * durationSecs : durationSecs;
42
33
  break;
43
34
  }
44
35
  case "reps-only": {
45
- const reps = (_e = record.reps) !== null && _e !== void 0 ? _e : 0;
46
- const auxWeight = (_f = record.auxWeightKg) !== null && _f !== void 0 ? _f : 0;
47
- load = auxWeight > 0 ? auxWeight * reps : reps;
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;
48
40
  break;
49
41
  }
50
- default:
51
- return 0;
52
42
  }
53
- return computeFinalScore(load, effortFactor, userProfile);
54
- };
55
- exports.computeScoreFromRecord = computeScoreFromRecord;
56
- const computeFinalScore = (load, effortFactor, userProfile) => {
43
+ // Step 2: Compute effort
44
+ let effortFactor = 0;
45
+ if (typeof record.rpe === "number") {
46
+ effortFactor = record.rpe;
47
+ }
48
+ else if (typeof record.rir === "number") {
49
+ effortFactor = 10 - record.rir;
50
+ }
51
+ // Step 3: Base score using log scale
57
52
  const base = Math.log10(1 + Math.max(0, load));
58
- const effortBoost = 1 + Math.min(effortFactor, 10) * 0.05;
59
- // Optional scaling
53
+ const plus = [];
54
+ 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
60
63
  let adjustment = 1;
61
64
  if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female")
62
65
  adjustment *= 1.1;
@@ -66,5 +69,36 @@ const computeFinalScore = (load, effortFactor, userProfile) => {
66
69
  const normalizedWeight = 70;
67
70
  adjustment *= normalizedWeight / userProfile.weightKg;
68
71
  }
69
- return base * effortBoost * adjustment;
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
+ }
90
+ }
91
+ let finalScore = parseFloat(rawScore.toFixed(2));
92
+ // Adjusting exerciseDefaultDifficultyMultiplier
93
+ finalScore *= exerciseDefaultDifficultyMultiplier;
94
+ plus.push({
95
+ exerciseDefaultDifficultyMultiplier: parseFloat(exerciseDefaultDifficultyMultiplier.toFixed(5)),
96
+ });
97
+ return {
98
+ baseScore: parseFloat(base.toFixed(2)),
99
+ plus,
100
+ minus,
101
+ finalScore,
102
+ };
70
103
  };
104
+ exports.computeScoreFromRecord = computeScoreFromRecord;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",