Standards domain models shuffling and renaming
This commit is contained in:
@@ -27,16 +27,16 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||
.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<any>): Promise<void> {
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -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<StandardsConfig> {
|
||||
if (!opt.skipCache) return this.calculator.standards.cfg;
|
||||
async getSelectedConfig(opt: { skipCache?: boolean } = {}): Promise<StandardsParameters> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<p class="text-xs label">
|
||||
{#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"}</th
|
||||
>
|
||||
{#each range(selected.cfg.global.maxLevel).map((i) => ++i) as lvl}
|
||||
{#each range(selected.params.global.maxLevel).map((i) => ++i) as lvl}
|
||||
<td>{getLvlValue(activity, standard, lvl)}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
@@ -99,7 +99,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Body weight</th>
|
||||
{#each range(selected.cfg.global.maxLevel).map((i) => ++i) as lvl}
|
||||
{#each range(selected.params.global.maxLevel).map((i) => ++i) as lvl}
|
||||
<td>{lvl}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -102,7 +102,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={
|
||||
input.cfg.activity[selected.activity]
|
||||
input.params.activity[selected.activity]
|
||||
.enableGeneration
|
||||
}
|
||||
class="toggle toggle-lg m-auto"
|
||||
@@ -118,10 +118,10 @@
|
||||
step=".05"
|
||||
placeholder=".1"
|
||||
bind:value={
|
||||
input.cfg.activity[selected.activity]
|
||||
input.params.activity[selected.activity]
|
||||
.weightModifier
|
||||
}
|
||||
disabled={!selected.cfg.activity[selected.activity]
|
||||
disabled={!selected.params.activity[selected.activity]
|
||||
.enableGeneration}
|
||||
/>
|
||||
</label>
|
||||
@@ -137,7 +137,7 @@
|
||||
<!-- inputSelected.cfg.activity[selected.activity] -->
|
||||
<!-- .weightSkew -->
|
||||
<!-- } -->
|
||||
<!-- disabled={!selected.cfg.activity[selected.activity] -->
|
||||
<!-- disabled={!selected.params.activity[selected.activity] -->
|
||||
<!-- .enableGeneration} -->
|
||||
<!-- /> -->
|
||||
<!-- </label> -->
|
||||
@@ -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}
|
||||
/>
|
||||
</label>
|
||||
@@ -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}
|
||||
/>
|
||||
</label>
|
||||
@@ -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}
|
||||
/>
|
||||
</label>
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<typeof StandardsConfigSchema>;
|
||||
export type StandardsParameters = Static<typeof StandardsParametersSchema>;
|
||||
|
||||
export class Standards {
|
||||
readonly cfg: StandardsConfig;
|
||||
readonly params: StandardsParameters;
|
||||
private standardsData: StandardsData;
|
||||
|
||||
constructor(standardsData: StandardsData, cfg?: Partial<StandardsConfig>) {
|
||||
Value.Assert(StandardsConfigSchema, defaultConfig);
|
||||
this.cfg = Object.assign({}, defaultConfig, cfg);
|
||||
constructor(standardsData: StandardsData, params?: Partial<StandardsParameters>) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user