diff --git a/apps/api/src/database/migrations/2026-06-03.ts b/apps/api/src/database/migrations/2026-06-03.ts index 371fb43..9658982 100644 --- a/apps/api/src/database/migrations/2026-06-03.ts +++ b/apps/api/src/database/migrations/2026-06-03.ts @@ -27,16 +27,16 @@ export async function up(db: Kysely): Promise { .nullsNotDistinct() .execute(); - // TABLE: CALCULATOR_CONFIGS - await db.schema.createTable("calculator_configs") + // TABLE: STANDARDS_CONFIGS + await db.schema.createTable("standards_configs") .$call(addDefaultColumns) - .addColumn("standards_config", "jsonb", (cb) => cb.notNull()) + .addColumn("parameters", "jsonb", (cb) => cb.notNull()) .addColumn("is_selected", "boolean", (cb) => cb.notNull().defaultTo(false)) .execute(); - // INDEX: ONE_SELECTED_CALCULATOR_CONFIG - await db.schema.createIndex("idx_one_selected_calculator_config") - .on("calculator_configs") + // INDEX: ONE_SELECTED_STANDARDS_CONFIG + await db.schema.createIndex("idx_one_selected_standards_config") + .on("standards_configs") .column("is_selected") .unique() .where("is_selected", "=", true) @@ -50,9 +50,9 @@ export async function down(db: Kysely): Promise { // INDEX: ONE_ACTIVE_PRODUCT_SYNC await db.schema.dropIndex("idx_one_active_product_sync").ifExists().execute(); - // TABLE: CALCULATOR_CONFIGS - await db.schema.dropTable("calculator_configs").ifExists().execute() + // TABLE: STANDARDS_CONFIGS + await db.schema.dropTable("standards_configs").ifExists().execute() - // INDEX: ONE_SELECTED_CALCULATOR_CONFIG - await db.schema.dropIndex("idx_one_selected_calculator_config").ifExists().execute(); + // INDEX: ONE_SELECTED_STANDARDS_CONFIG + await db.schema.dropIndex("idx_one_selected_standards_config").ifExists().execute(); } diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 29418a8..16a53bb 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -18,7 +18,7 @@ import { CommerceService } from "./services/commerce"; import cluster from "node:cluster"; import { randomUUIDv7 } from "bun"; import { CalculatorService } from "./services/calculator"; -import { StandardsConfigSchema } from "@blade-and-brawn/calculator"; +import { StandardsParametersSchema } from "@blade-and-brawn/calculator"; // SERVICES // ----------- @@ -117,23 +117,23 @@ export const app = new Elysia() return { sessionId: token.sessionId.toString() }; }) .put("/config/:id", async ({ params: { id }, body: { standardsConfig } }) => { - await calculatorService.setConfig(id, standardsConfig); + await calculatorService.standards.setConfig(id, standardsConfig); }, { params: t.Object({ id: t.String() }), - body: t.Object({ standardsConfig: StandardsConfigSchema }) + body: t.Object({ standardsConfig: StandardsParametersSchema }) }) .post("/config", async ({ body: { standardsConfig } }) => { - await calculatorService.createConfig(standardsConfig); + await calculatorService.standards.createConfig(standardsConfig); }, { - body: t.Object({ standardsConfig: StandardsConfigSchema }) + body: t.Object({ standardsConfig: StandardsParametersSchema }) }) .post("/config/:id/select", async ({ params: { id } }) => { - await calculatorService.selectConfig(id); + await calculatorService.standards.selectConfig(id); }, { params: t.Object({ id: t.String() }) }) .get("/config", async () => { - return await calculatorService.getSelectedConfig(); + return await calculatorService.standards.getSelectedConfig(); }) ) diff --git a/apps/api/src/services/calculator.ts b/apps/api/src/services/calculator.ts index 33ca0d9..328c5e0 100644 --- a/apps/api/src/services/calculator.ts +++ b/apps/api/src/services/calculator.ts @@ -1,40 +1,39 @@ -import { LevelCalculator, Standards, DEFAULT_STANDARDS_DATA, type StandardsConfig, StandardsConfigSchema } from "@blade-and-brawn/calculator"; +import { LevelCalculator, Standards, DEFAULT_STANDARDS_DATA, type StandardsParameters, StandardsParametersSchema } from "@blade-and-brawn/calculator"; import type { ActivityPerformance, Player } from "@blade-and-brawn/domain"; import { db } from "../database/db"; import { Value } from "@sinclair/typebox/value"; import { log } from "../util"; -export class CalculatorService { - private static readonly CONFIG_REFRESH_INTERVAL_MS = 30_000; +let calculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATA)); +export class CalculatorService { + standards: StandardsSubService = new StandardsSubService(); + + calculate(player: Player, activityPerformances: ActivityPerformance[]) { + this.standards.refreshConfig({ onlyIfStale: true }).catch((err) => log.error({ err }, "failed to refresh standards config")); + return calculator.calculate(player, activityPerformances); + } +} + +class StandardsSubService { + private static readonly CONFIG_REFRESH_INTERVAL_MS = 30_000; private cachedStandardsConfigData: string | null = null; private lastRefreshedAt = 0; - private calculator: LevelCalculator; - - constructor() { - this.calculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATA)); - } - - calculate(player: Player, activityPerformances: ActivityPerformance[]) { - this.refreshConfig({ onlyIfStale: true }).catch((err) => log.error({ err }, "failed to refresh calculator config")); - return this.calculator.calculate(player, activityPerformances); - } - - async getSelectedConfig(opt: { skipCache?: boolean } = {}): Promise { - if (!opt.skipCache) return this.calculator.standards.cfg; + async getSelectedConfig(opt: { skipCache?: boolean } = {}): Promise { + if (!opt.skipCache) return calculator.standards.params; await this.refreshConfig(); - return this.calculator.standards.cfg; + return calculator.standards.params; } async selectConfig(id: string) { await db.transaction().execute(async (trx) => { - await trx.updateTable("calculator_configs") + await trx.updateTable("standards_configs") .set({ is_selected: false }) .where("is_selected", "=", true) .execute(); - const result = await trx.updateTable("calculator_configs") + const result = await trx.updateTable("standards_configs") .set({ is_selected: true }) .where("id", "=", id) .execute(); @@ -42,37 +41,37 @@ export class CalculatorService { }); } - async setConfig(id: string, standardsConfig: StandardsConfig) { - const result = await db.updateTable("calculator_configs") - .set({ standards_config: standardsConfig }) + async setConfig(id: string, parameters: StandardsParameters) { + const result = await db.updateTable("standards_configs") + .set({ parameters }) .where("id", "=", id) .execute(); if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`); } - async createConfig(standardsConfig: StandardsConfig) { - await db.insertInto("calculator_configs") - .values({ standards_config: standardsConfig }) + async createConfig(parameters: StandardsParameters) { + await db.insertInto("standards_configs") + .values({ parameters }) .execute(); } async refreshConfig(opt: { onlyIfStale?: boolean } = {}) { if (opt.onlyIfStale) - if (Date.now() - this.lastRefreshedAt < CalculatorService.CONFIG_REFRESH_INTERVAL_MS) return; + if (Date.now() - this.lastRefreshedAt < StandardsSubService.CONFIG_REFRESH_INTERVAL_MS) return; this.lastRefreshedAt = Date.now(); - const result = await db.selectFrom("calculator_configs") - .select(["standards_config"]) + const { parameters } = await db.selectFrom("standards_configs") + .select(["parameters"]) .where("is_selected", "=", true) .limit(1) - .executeTakeFirstOrThrow({ errorConstructor: () => new Error("No calculator config selected") }); - Value.Assert(StandardsConfigSchema, result.standards_config); + .executeTakeFirstOrThrow({ errorConstructor: () => new Error("No standards config selected") }); + Value.Assert(StandardsParametersSchema, parameters); - const standardsConfigData = JSON.stringify(result.standards_config); + const standardsConfigData = JSON.stringify(parameters); if (standardsConfigData === this.cachedStandardsConfigData) return; - this.calculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATA, result.standards_config)); + calculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATA, parameters)); this.cachedStandardsConfigData = standardsConfigData; } } diff --git a/apps/portal/src/lib/components/StandardsTable.svelte b/apps/portal/src/lib/components/StandardsTable.svelte index da93d4d..cc3c3e6 100644 --- a/apps/portal/src/lib/components/StandardsTable.svelte +++ b/apps/portal/src/lib/components/StandardsTable.svelte @@ -2,7 +2,7 @@ import { Standards, type Standard, - type StandardsConfig, + type StandardsParameters, } from "@blade-and-brawn/calculator"; import { Activity, @@ -17,7 +17,7 @@ selected: { activity: Activity; metrics: Metrics; - cfg: StandardsConfig; + params: StandardsParameters; }; } @@ -52,7 +52,7 @@

- {#if selected.cfg.activity[selected.activity].enableGeneration && allStandards + {#if selected.params.activity[selected.activity].enableGeneration && allStandards .byActivity(selected.activity) .getMetadata().generators.length} (Generated data: {allStandards @@ -87,7 +87,7 @@ ? parseFloat(kgToLb(standard.metrics.weight).toFixed(2)) : "None"} - {#each range(selected.cfg.global.maxLevel).map((i) => ++i) as lvl} + {#each range(selected.params.global.maxLevel).map((i) => ++i) as lvl} {getLvlValue(activity, standard, lvl)} {/each} @@ -99,7 +99,7 @@ Body weight - {#each range(selected.cfg.global.maxLevel).map((i) => ++i) as lvl} + {#each range(selected.params.global.maxLevel).map((i) => ++i) as lvl} {lvl} {/each} diff --git a/apps/portal/src/routes/(app)/calculator/+page.svelte b/apps/portal/src/routes/(app)/calculator/+page.svelte index be65c69..a8b1c30 100644 --- a/apps/portal/src/routes/(app)/calculator/+page.svelte +++ b/apps/portal/src/routes/(app)/calculator/+page.svelte @@ -2,7 +2,7 @@ import { DEFAULT_STANDARDS_DATA, Standards, - type StandardsConfig, + type StandardsParameters, } from "@blade-and-brawn/calculator"; import { Activity, @@ -24,7 +24,7 @@ age: null as number | null, weight: null as number | null, // lb }, - cfg: structuredClone(defaultStandards.cfg) as StandardsConfig, + params: structuredClone(defaultStandards.params) as StandardsParameters, }); const selected = $derived({ @@ -34,11 +34,11 @@ age: input.metrics.age ?? 0, weight: lbToKg(input.metrics.weight ?? 0), } as Metrics, - cfg: input.cfg, + params: input.params, }); const allStandards = $derived( - new Standards(DEFAULT_STANDARDS_DATA, selected.cfg), + new Standards(DEFAULT_STANDARDS_DATA, selected.params), ); $effect(() => { @@ -86,7 +86,7 @@ min="1" max="100" placeholder="5" - bind:value={input.cfg.global.maxLevel} + bind:value={input.params.global.maxLevel} />

@@ -102,7 +102,7 @@ @@ -137,7 +137,7 @@ - + @@ -150,10 +150,10 @@ max="1" step=".05" bind:value={ - input.cfg.activity[selected.activity] + input.params.activity[selected.activity] .difficultyModifier } - disabled={!selected.cfg.activity[selected.activity] + disabled={!selected.params.activity[selected.activity] .enableGeneration} /> @@ -166,10 +166,10 @@ max="1" step=".05" bind:value={ - input.cfg.activity[selected.activity] + input.params.activity[selected.activity] .ageModifier } - disabled={!selected.cfg.activity[selected.activity] + disabled={!selected.params.activity[selected.activity] .enableGeneration} /> @@ -182,9 +182,9 @@ max="100" step="1" bind:value={ - input.cfg.activity[selected.activity].peakAge + input.params.activity[selected.activity].peakAge } - disabled={!selected.cfg.activity[selected.activity] + disabled={!selected.params.activity[selected.activity] .enableGeneration} /> @@ -202,10 +202,10 @@ max="10" step="1" bind:value={ - input.cfg.activity[selected.activity] + input.params.activity[selected.activity] .stretch.lower } - disabled={!selected.cfg.activity[ + disabled={!selected.params.activity[ selected.activity ].enableGeneration} /> @@ -219,10 +219,10 @@ max="10" step="1" bind:value={ - input.cfg.activity[selected.activity] + input.params.activity[selected.activity] .stretch.upper } - disabled={!selected.cfg.activity[ + disabled={!selected.params.activity[ selected.activity ].enableGeneration} /> diff --git a/packages/calculator/src/avg-weights.ts b/packages/calculator/src/avg-weights.ts index e5843a9..723fd26 100644 --- a/packages/calculator/src/avg-weights.ts +++ b/packages/calculator/src/avg-weights.ts @@ -1,5 +1,5 @@ import type { Gender, Metrics, StandardUnit } from "@blade-and-brawn/domain"; -import avgWeightDataJson from "./data/avg-weights.json" assert { type: "json" }; +import avgWeightDataJson from "./data/avg-weights.json" with { type: "json" }; export const getAvgWeight = (gender: Gender, age: number) => { type AvgWeightData = { diff --git a/packages/calculator/src/index.ts b/packages/calculator/src/index.ts index 83f6c7d..fb33734 100644 --- a/packages/calculator/src/index.ts +++ b/packages/calculator/src/index.ts @@ -213,7 +213,7 @@ export class LevelCalculator { } } -const StandardsActivityConfigSchema = Type.Object({ +const StandardsActivityParametersSchema = Type.Object({ weightModifier: Type.Number(), weightSkew: Type.Number(), ageModifier: Type.Number(), @@ -226,22 +226,22 @@ const StandardsActivityConfigSchema = Type.Object({ }), }); -export const StandardsConfigSchema = Type.Object({ +export const StandardsParametersSchema = Type.Object({ global: Type.Object({ maxLevel: Type.Number(), }), - activity: Type.Record(Type.Enum(Activity), StandardsActivityConfigSchema), + activity: Type.Record(Type.Enum(Activity), StandardsActivityParametersSchema), }); -export type StandardsConfig = Static; +export type StandardsParameters = Static; export class Standards { - readonly cfg: StandardsConfig; + readonly params: StandardsParameters; private standardsData: StandardsData; - constructor(standardsData: StandardsData, cfg?: Partial) { - Value.Assert(StandardsConfigSchema, defaultConfig); - this.cfg = Object.assign({}, defaultConfig, cfg); + constructor(standardsData: StandardsData, params?: Partial) { + Value.Assert(StandardsParametersSchema, defaultConfig); + this.params = Object.assign({}, defaultConfig, params); // prepare data this.standardsData = structuredClone(standardsData); @@ -254,7 +254,7 @@ export class Standards { return (i: number) => A * Math.exp(-B * i) + C; } - if (!this.cfg.activity[activity].enableGeneration) { + if (!this.params.activity[activity].enableGeneration) { continue; } @@ -314,13 +314,13 @@ export class Standards { const newLevels: Levels = {}; const newLowerLevelCount = Math.max( Math.round( - this.cfg.activity[activity].stretch.lower, + this.params.activity[activity].stretch.lower, ), 0, ); const newUpperLevelCount = Math.max( Math.round( - this.cfg.activity[activity].stretch.upper, + this.params.activity[activity].stretch.upper, ), 0, ); @@ -362,7 +362,7 @@ export class Standards { let i = 1; while ( Object.keys(standard.levels).length < - this.cfg.global.maxLevel + this.params.global.maxLevel ) { standard.levels = this.expandLevels(standard.levels, i++); } @@ -370,11 +370,11 @@ export class Standards { // compress if ( Object.keys(standard.levels).length > - this.cfg.global.maxLevel + this.params.global.maxLevel ) { standard.levels = this.compressLevels( standard.levels, - this.cfg.global.maxLevel, + this.params.global.maxLevel, ); } @@ -382,12 +382,12 @@ export class Standards { for (const level in standard.levels) { standard.levels[level] = standard.levels[level] * - (1 + this.cfg.activity[activity].difficultyModifier); + (1 + this.params.activity[activity].difficultyModifier); } } // generate data - const allGenerators = this.cfg.activity[activity].enableGeneration + const allGenerators = this.params.activity[activity].enableGeneration ? this.standardsData[activity].metadata.generators.sort( (a, b) => metricPriority(a.metric) - metricPriority(b.metric), @@ -397,7 +397,7 @@ export class Standards { for (const generator of allGenerators) { if (generator.metric === "age") { for (const gender of Object.values(Gender)) { - const peakAge = this.cfg.activity[activity].peakAge; + const peakAge = this.params.activity[activity].peakAge; const ageStep = 10; const minAge = 0; @@ -427,7 +427,7 @@ export class Standards { if (base == null) continue; const ageModifier = - this.cfg.activity[activity].ageModifier; + this.params.activity[activity].ageModifier; const leftSpan = peakAge - minAge; const rightSpan = maxAge - peakAge; @@ -515,7 +515,7 @@ export class Standards { continue; const scalingExponent = - this.cfg.activity[activity] + this.params.activity[activity] .weightModifier; const coefficient = currWeight / referenceWeight;