Calculator simplification

This commit is contained in:
Dominic Ferrando
2025-10-17 16:36:18 -04:00
parent 331d20d272
commit 06580e0a81
5 changed files with 109 additions and 145 deletions
+17 -4
View File
@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { import {
Metrics,
Standard,
Standards, Standards,
StandardsConfig, type ActivityStandards,
type Metrics,
type Standard,
type StandardsConfig,
} from "$lib/services/calculator/main"; } from "$lib/services/calculator/main";
import allStandardsRaw from "$lib/data/standards.json" assert { type: "json" };
import { import {
Activity, Activity,
cmToIn, cmToIn,
@@ -24,6 +26,17 @@
const { selected, allStandards }: Props = $props(); const { selected, allStandards }: Props = $props();
const allAnchorStandards = $derived.by(() => {
const standardsCfg = structuredClone(selected.cfg);
for (const activityCfg of Object.values(standardsCfg.activity)) {
activityCfg.disableGeneration = true;
}
return new Standards(
allStandardsRaw as ActivityStandards,
standardsCfg,
);
});
const getLvlValue = ( const getLvlValue = (
activity: Activity, activity: Activity,
standard: Standard, standard: Standard,
+5 -20
View File
@@ -4738,10 +4738,7 @@
"attribute": "Endurance", "attribute": "Endurance",
"generators": [ "generators": [
{ {
"metric": "weight", "metric": "weight"
"spread": 3,
"step": 12,
"ratio": "normal"
} }
], ],
"unit": "ms", "unit": "ms",
@@ -5231,16 +5228,10 @@
"attribute": "Power", "attribute": "Power",
"generators": [ "generators": [
{ {
"metric": "age", "metric": "age"
"spread": 4,
"step": 10,
"ratio": "inverse"
}, },
{ {
"metric": "weight", "metric": "weight"
"spread": 3,
"step": 12,
"ratio": "inverse"
} }
], ],
"unit": "cm", "unit": "cm",
@@ -5282,16 +5273,10 @@
"attribute": "Agility", "attribute": "Agility",
"generators": [ "generators": [
{ {
"metric": "weight", "metric": "weight"
"spread": 3,
"step": 12,
"ratio": "normal"
}, },
{ {
"metric": "age", "metric": "age"
"spread": 4,
"step": 10,
"ratio": "normal"
} }
], ],
"unit": "ms", "unit": "ms",
+26 -33
View File
@@ -10,11 +10,6 @@ import { Activity, Attribute, Gender, getAvgWeight, kgToLb, lbToKg, type Activit
// Broad Jump: // Broad Jump:
// https://nrpt.co.uk/training/tests/power/broad.htm // https://nrpt.co.uk/training/tests/power/broad.htm
type LevelCalculatorConfig = {
expandIters?: number;
compressTo?: number;
}
export type LevelCalculatorOutput = { export type LevelCalculatorOutput = {
player: number, player: number,
attributes: Record<Attribute, number> attributes: Record<Attribute, number>
@@ -36,10 +31,7 @@ export interface Standard {
} }
type Generator = { type Generator = {
metric: NumberMetric, metric: NumberMetric
spread: number,
step: number,
ratio: "normal" | "inverse"
} }
export type ActivityStandards = Record<Activity, { export type ActivityStandards = Record<Activity, {
@@ -185,8 +177,8 @@ export type StandardsConfig = {
weightModifier: number, weightModifier: number,
weightSkew: number, weightSkew: number,
ageModifier: number, ageModifier: number,
disableGeneration: boolean, enableGeneration: boolean,
performanceSkew: number difficultyModifier: number
}> }>
} }
@@ -195,15 +187,18 @@ export class Standards {
private activityStandards: ActivityStandards; private activityStandards: ActivityStandards;
constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) { constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) {
// TODO: set these somewhere
const defaultActivitiesConfig = Object.fromEntries( const defaultActivitiesConfig = Object.fromEntries(
Object.values(Activity).map(activity => [ Object.values(Activity).map(activity => [
activity, activity,
{ {
disableGeneration: false, enableGeneration: true,
weightModifier: 0.1, weightModifier: activity === Activity.BroadJump ?
-0.1 :
0.1,
weightSkew: 0, weightSkew: 0,
ageModifier: 0.1, ageModifier: 0.1,
performanceSkew: 0 difficultyModifier: 0
}, },
]) ])
) as Record<Activity, StandardsConfig["activity"][Activity]>; ) as Record<Activity, StandardsConfig["activity"][Activity]>;
@@ -226,22 +221,24 @@ export class Standards {
// apply skew config // apply skew config
for (const level in standard.levels) { for (const level in standard.levels) {
standard.levels[level] = standard.levels[level] * (1 + this.cfg.activity[activity].performanceSkew) standard.levels[level] = standard.levels[level] * (1 + this.cfg.activity[activity].difficultyModifier)
} }
} }
// generate data // generate data
const allGenerators = this.cfg.activity[activity].disableGeneration ? const allGenerators = this.cfg.activity[activity].enableGeneration ?
[] : this.activityStandards[activity].metadata.generators :
this.activityStandards[activity].metadata.generators; [];
const ageGenerators = allGenerators.filter(g => g.metric === "age"); const ageGenerators = allGenerators.filter(g => g.metric === "age");
for (const ageGenerator of ageGenerators) { for (const ageGenerator of ageGenerators) {
for (const gender of Object.values(Gender)) { for (const gender of Object.values(Gender)) {
const referenceAge = 50; const ageStep = 10;
const referenceAge = 25;
const medianAge = 50;
const minAge = referenceAge - (ageGenerator.step * ageGenerator.spread); const minAge = medianAge - (3 * ageStep);
const maxAge = referenceAge + (ageGenerator.step * ageGenerator.spread); const maxAge = medianAge + (3 * ageStep);
const referenceStandard = this const referenceStandard = this
.byActivity(activity) .byActivity(activity)
@@ -264,9 +261,7 @@ export class Standards {
if (!referenceStandard.levels[lvl]) continue; if (!referenceStandard.levels[lvl]) continue;
const scalingExponent = this.cfg.activity[activity].ageModifier; const scalingExponent = this.cfg.activity[activity].ageModifier;
const coefficient = ageGenerator.ratio === "inverse" ? const coefficient = currAge / referenceAge;
referenceAge / currAge :
currAge / referenceAge;
newStandard.levels[lvl] = referenceStandard.levels[lvl] * (coefficient ** scalingExponent); newStandard.levels[lvl] = referenceStandard.levels[lvl] * (coefficient ** scalingExponent);
} }
@@ -278,7 +273,7 @@ export class Standards {
if (!overlappingStandard) if (!overlappingStandard)
this.activityStandards[activity].standards.push(newStandard); this.activityStandards[activity].standards.push(newStandard);
currAge += ageGenerator.step; currAge += ageStep;
} }
} }
@@ -291,11 +286,11 @@ export class Standards {
for (const weightGenerator of weightGenerators) { for (const weightGenerator of weightGenerators) {
for (const gender of Object.values(Gender)) { for (const gender of Object.values(Gender)) {
for (const age of this.agesFor(activity, gender)) { for (const age of this.agesFor(activity, gender)) {
const weightStep = 12;
const referenceWeight = getAvgWeight(gender, age); const referenceWeight = getAvgWeight(gender, age);
const skewRatio = this.cfg.activity[activity].weightSkew; // 0 = normal, 1 = max skew const minWeight = referenceWeight - (3 * weightStep);
const minWeight = referenceWeight - (weightGenerator.step * weightGenerator.spread * (1 - skewRatio)); const maxWeight = referenceWeight + (3 * weightStep);
const maxWeight = referenceWeight + (weightGenerator.step * weightGenerator.spread * (1 + skewRatio));
const referenceStandard = this const referenceStandard = this
.byActivity(activity) .byActivity(activity)
@@ -317,10 +312,8 @@ export class Standards {
for (const lvl in referenceStandard.levels) { for (const lvl in referenceStandard.levels) {
if (!referenceStandard.levels[lvl]) continue; if (!referenceStandard.levels[lvl]) continue;
const scalingExponent = this.cfg.activity[activity].weightModifier; const scalingExponent = this.cfg.activity[activity].weightModifier
const coefficient = weightGenerator.ratio === "inverse" ? const coefficient = currWeight / referenceWeight;
referenceWeight / currWeight :
currWeight / referenceWeight;
newStandard.levels[lvl] = referenceStandard.levels[lvl] * (coefficient ** scalingExponent); newStandard.levels[lvl] = referenceStandard.levels[lvl] * (coefficient ** scalingExponent);
} }
@@ -333,7 +326,7 @@ export class Standards {
if (!overlappingStandard) if (!overlappingStandard)
this.activityStandards[activity].standards.push(newStandard); this.activityStandards[activity].standards.push(newStandard);
currWeight += weightGenerator.step; currWeight += weightStep;
} }
} }
} }
-37
View File
@@ -67,43 +67,6 @@ export const msToTime = (ms: number, includeMs = false): string => {
export const range = (length: number) => Array.from({ length: length }, (_, i) => i + 1); export const range = (length: number) => Array.from({ length: length }, (_, i) => i + 1);
// export const getAvgWeight = (gender: Gender, age: number) => {
// type AvgWeightData = {
// metadata: {
// unit: StandardUnit
// },
// weights: Metrics[]
// }
// let avgWeightData = avgWeightDataRaw as AvgWeightData;
//
// const avgWeights = avgWeightData.weights.filter(a => a.gender === gender);
//
// let lower = avgWeights[0]; // best <= target
// let upper = avgWeights[0]; // best >= target
// let hasLower = false, hasUpper = false;
//
// for (const avg of avgWeights) {
// if (avg.age <= age) {
// if (!hasLower || avg.age > lower.age) { lower = avg; hasLower = true; }
// }
// if (avg.age >= age) {
// if (!hasUpper || avg.age < upper.age) { upper = avg; hasUpper = true; }
// }
// }
//
// // clamp to ends if one side missing
// if (!hasLower) lower = avgWeights[0];
// if (!hasUpper) upper = avgWeights[avgWeights.length - 1];
//
// // lerp
// let ageRatio = upper === lower ? 1 : (age - lower.age) / (upper.age - lower.age);
// ageRatio = Math.max(0, Math.min(1, ageRatio));
// const interpolatedAvg = lower.weight + (upper.weight - lower.weight) * ageRatio;
//
// return interpolatedAvg;
// }
export const getAvgWeight = (gender: Gender, age: number) => { export const getAvgWeight = (gender: Gender, age: number) => {
type AvgWeightData = { type AvgWeightData = {
metadata: { metadata: {
+61 -51
View File
@@ -12,7 +12,11 @@
let activeTab = $state("standards"); let activeTab = $state("standards");
let inputSelected = $state({ const defaultStandards = new Standards(
allStandardsRaw as ActivityStandards,
);
let input = $state({
activity: Activity.BenchPress, activity: Activity.BenchPress,
metrics: { metrics: {
gender: Gender.Male, gender: Gender.Male,
@@ -21,51 +25,57 @@
}, },
cfg: { cfg: {
global: { global: {
maxLevel: "100", maxLevel: String(defaultStandards.cfg.global.maxLevel),
}, },
activity: Object.fromEntries( activity: Object.fromEntries(
Object.values(Activity).map((activity) => [ Object.values(Activity).map((activity) => {
activity, return [
{ activity,
enableGeneration: true, {
weightModifier: ".1", enableGeneration:
// weightSkew: "0", defaultStandards.cfg.activity[activity]
ageModifier: ".1", .enableGeneration,
performanceSkew: "0", weightAdvantage: String(
}, defaultStandards.cfg.activity[activity]
]), .weightModifier,
),
ageModifier: String(
defaultStandards.cfg.activity[activity]
.ageModifier,
),
difficultyModifier: String(
defaultStandards.cfg.activity[activity]
.difficultyModifier,
),
},
];
}),
), ),
}, },
}); });
const selected = $derived({ const selected = $derived({
activity: inputSelected.activity, activity: input.activity,
metrics: { metrics: {
gender: inputSelected.metrics.gender, gender: input.metrics.gender,
age: +inputSelected.metrics.age, age: +input.metrics.age,
weight: lbToKg(+inputSelected.metrics.weight), weight: lbToKg(+input.metrics.weight),
} as Metrics, } as Metrics,
cfg: { cfg: {
global: { global: {
maxLevel: +inputSelected.cfg.global.maxLevel, maxLevel: +input.cfg.global.maxLevel,
}, },
activity: Object.fromEntries( activity: Object.fromEntries(
Object.values(Activity).map((activity) => [ Object.values(Activity).map((activity) => [
activity, activity,
{ {
weightModifier: weightModifier:
+inputSelected.cfg.activity[activity] +input.cfg.activity[activity].weightAdvantage,
.weightModifier, ageModifier: +input.cfg.activity[activity].ageModifier,
weightSkew: 0, enableGeneration:
// +inputSelected.cfg.activity[activity].weightSkew, input.cfg.activity[activity].enableGeneration,
ageModifier: difficultyModifier:
+inputSelected.cfg.activity[activity].ageModifier, +input.cfg.activity[activity].difficultyModifier,
disableGeneration:
!inputSelected.cfg.activity[activity]
.enableGeneration,
performanceSkew:
+inputSelected.cfg.activity[activity]
.performanceSkew,
}, },
]), ]),
), ),
@@ -78,16 +88,16 @@
$effect(() => { $effect(() => {
console.log(selected); console.log(selected);
if (!inputSelected.metrics.age) inputSelected.metrics.weight = ""; if (!input.metrics.age) input.metrics.weight = "";
}); });
$effect(() => { $effect(() => {
const savedParameters = localStorage.getItem("parameters"); const savedParameters = localStorage.getItem("parameters");
if (savedParameters) inputSelected = JSON.parse(savedParameters); if (savedParameters) input = JSON.parse(savedParameters);
}); });
$effect(() => { $effect(() => {
localStorage.setItem("parameters", JSON.stringify(inputSelected)); localStorage.setItem("parameters", JSON.stringify(input));
}); });
</script> </script>
@@ -110,7 +120,7 @@
min="1" min="1"
max="100" max="100"
placeholder="5" placeholder="5"
bind:value={inputSelected.cfg.global.maxLevel} bind:value={input.cfg.global.maxLevel}
/> />
</label> </label>
</div> </div>
@@ -126,7 +136,7 @@
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
inputSelected.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.enableGeneration .enableGeneration
} }
class="toggle toggle-lg m-auto" class="toggle toggle-lg m-auto"
@@ -142,11 +152,11 @@
step=".05" step=".05"
placeholder=".1" placeholder=".1"
bind:value={ bind:value={
inputSelected.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.weightModifier .weightAdvantage
} }
disabled={selected.cfg.activity[selected.activity] disabled={!selected.cfg.activity[selected.activity]
.disableGeneration} .enableGeneration}
/> />
</label> </label>
<!-- <label> --> <!-- <label> -->
@@ -161,8 +171,8 @@
<!-- inputSelected.cfg.activity[selected.activity] --> <!-- inputSelected.cfg.activity[selected.activity] -->
<!-- .weightSkew --> <!-- .weightSkew -->
<!-- } --> <!-- } -->
<!-- disabled={selected.cfg.activity[selected.activity] --> <!-- disabled={!selected.cfg.activity[selected.activity] -->
<!-- .disableGeneration} --> <!-- .enableGeneration} -->
<!-- /> --> <!-- /> -->
<!-- </label> --> <!-- </label> -->
<label> <label>
@@ -174,11 +184,11 @@
max="1" max="1"
step=".05" step=".05"
bind:value={ bind:value={
inputSelected.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.performanceSkew .difficultyModifier
} }
disabled={selected.cfg.activity[selected.activity] disabled={!selected.cfg.activity[selected.activity]
.disableGeneration} .enableGeneration}
/> />
</label> </label>
<label> <label>
@@ -190,11 +200,11 @@
max="1" max="1"
step=".05" step=".05"
bind:value={ bind:value={
inputSelected.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.ageModifier .ageModifier
} }
disabled={selected.cfg.activity[selected.activity] disabled={!selected.cfg.activity[selected.activity]
.disableGeneration} .enableGeneration}
/> />
</label> </label>
</div> </div>
@@ -213,7 +223,7 @@
<select <select
class="select w-full" class="select w-full"
name="activities" name="activities"
bind:value={inputSelected.activity} bind:value={input.activity}
> >
{#each Object.values(Activity) as activity} {#each Object.values(Activity) as activity}
<option value={activity}> <option value={activity}>
@@ -229,7 +239,7 @@
<select <select
class="select w-full" class="select w-full"
name="genders" name="genders"
bind:value={inputSelected.metrics.gender} bind:value={input.metrics.gender}
> >
{#each Object.values(Gender) as gender} {#each Object.values(Gender) as gender}
<option value={gender}>{gender}</option> <option value={gender}>{gender}</option>
@@ -245,7 +255,7 @@
min="0" min="0"
max="100" max="100"
placeholder="18" placeholder="18"
bind:value={inputSelected.metrics.age} bind:value={input.metrics.age}
/> />
</label> </label>
@@ -259,7 +269,7 @@
placeholder={selected.metrics.age placeholder={selected.metrics.age
? "170" ? "170"
: "Requires age"} : "Requires age"}
bind:value={inputSelected.metrics.weight} bind:value={input.metrics.weight}
disabled={!selected.metrics.age} disabled={!selected.metrics.age}
/> />
</label> </label>