@dgpholdings/greatoak-shared 1.1.34 → 1.1.36

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