fix(frontend): restore Ref general stat presets

This commit is contained in:
2026-08-09 03:25:21 +00:00
parent 73edd0230f
commit 1880289ae8
4 changed files with 206 additions and 27 deletions
+16
View File
@@ -157,6 +157,22 @@ test('prioritizes core general fields and keeps context and inheritance progress
await expect(page.getByLabel('장수명')).toHaveValue('생성장수');
await expect(page.getByLabel('성격')).toBeVisible();
await expect(page.locator('.create-form').getByLabel('통솔')).toHaveValue('55');
const statActions = page.getByRole('group', { name: '능력치 빠른 설정' });
await expect(statActions.getByRole('button')).toHaveText([
'랜덤형',
'통솔무력형',
'통솔지력형',
'무력지력형',
]);
await page.evaluate(() => {
const values = [0.9, 0.8, 0.5];
Math.random = () => values.shift() ?? 0.5;
});
await statActions.getByRole('button', { name: '통솔무력형' }).click();
await expect(page.locator('.create-form').getByLabel('통솔')).toHaveValue('75');
await expect(page.locator('.create-form').getByLabel('무력')).toHaveValue('75');
await expect(page.locator('.create-form').getByLabel('지력')).toHaveValue('15');
await expect(page.locator('.stat-summary')).toContainText('능력치 합계: 165');
await expect(advanced).not.toHaveAttribute('open');
await expect(page.getByText('전투 특기 선택')).toBeHidden();
expect(state.mapRequests).toBe(0);
+139
View File
@@ -0,0 +1,139 @@
export type GeneralStatRules = {
min: number;
max: number;
total: number;
};
export type GeneralStats = [leadership: number, strength: number, intel: number];
type RandomSource = () => number;
export const abilityRand = (stats: GeneralStatRules, random: RandomSource = Math.random): GeneralStats => {
let leadership = random() * 65 + 10;
let strength = random() * 65 + 10;
let intel = random() * 65 + 10;
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
leadership += 1;
}
if (
leadership > stats.max ||
strength > stats.max ||
intel > stats.max ||
leadership < stats.min ||
strength < stats.min ||
intel < stats.min
) {
return abilityRand(stats, random);
}
return [leadership, strength, intel];
};
export const abilityLeadpow = (stats: GeneralStatRules, random: RandomSource = Math.random): GeneralStats => {
let leadership = random() * 6;
let strength = random() * 6;
let intel = random();
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
strength += 1;
}
if (intel < stats.min) {
leadership -= stats.min - intel;
intel = stats.min;
}
if (leadership > stats.max) {
strength += leadership - stats.max;
leadership = stats.max;
}
if (strength > stats.max) {
leadership += strength - stats.max;
strength = stats.max;
}
if (leadership > stats.max) {
intel += leadership - stats.max;
leadership = stats.max;
}
return [leadership, strength, intel];
};
export const abilityLeadint = (stats: GeneralStatRules, random: RandomSource = Math.random): GeneralStats => {
let leadership = random() * 6;
let strength = random();
let intel = random() * 6;
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
intel += 1;
}
if (strength < stats.min) {
leadership -= stats.min - strength;
strength = stats.min;
}
if (leadership > stats.max) {
intel += leadership - stats.max;
leadership = stats.max;
}
if (intel > stats.max) {
leadership += intel - stats.max;
intel = stats.max;
}
if (leadership > stats.max) {
strength += leadership - stats.max;
leadership = stats.max;
}
return [leadership, strength, intel];
};
export const abilityPowint = (stats: GeneralStatRules, random: RandomSource = Math.random): GeneralStats => {
let leadership = random();
let strength = random() * 6;
let intel = random() * 6;
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
intel += 1;
}
if (leadership < stats.min) {
strength -= stats.min - leadership;
leadership = stats.min;
}
if (strength > stats.max) {
intel += strength - stats.max;
strength = stats.max;
}
if (intel > stats.max) {
strength += intel - stats.max;
intel = stats.max;
}
if (strength > stats.max) {
leadership += strength - stats.max;
strength = stats.max;
}
return [leadership, strength, intel];
};
+29 -27
View File
@@ -10,6 +10,7 @@ import { cityLevelMap, formatOfficerLevelText, regionMap } from '../utils/nation
import { getNpcColor } from '../utils/npcColor';
import { formatSeoulDateTime } from '../utils/legacyDateTime';
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
import { abilityLeadint, abilityLeadpow, abilityPowint, abilityRand, type GeneralStats } from '../utils/generalStats';
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
type JoinInput = Parameters<typeof trpc.join.createGeneral.mutate>[0];
@@ -365,8 +366,6 @@ const inheritTurntimeChoice = computed<string>({
},
});
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
const applyBalancedStats = () => {
const rules = statRules.value;
if (!rules) {
@@ -378,36 +377,40 @@ const applyBalancedStats = () => {
form.value.intel = base;
};
const applyStats = (stats: GeneralStats) => {
[form.value.leadership, form.value.strength, form.value.intel] = stats;
};
const applyRandomStats = () => {
const rules = statRules.value;
if (!rules) {
return;
}
for (let i = 0; i < 40; i += 1) {
const leadership = randomInt(rules.min, rules.max);
const strength = randomInt(rules.min, rules.max);
const intel = rules.total - leadership - strength;
if (intel >= rules.min && intel <= rules.max) {
form.value.leadership = leadership;
form.value.strength = strength;
form.value.intel = intel;
return;
}
}
applyBalancedStats();
applyStats(abilityRand(rules));
};
const applyFocusedStats = (focus: 'leadership' | 'strength' | 'intel') => {
const applyLeadpowStats = () => {
const rules = statRules.value;
if (!rules) {
return;
}
const focusValue = Math.min(rules.max, rules.min + Math.floor(rules.total * 0.45));
const remain = rules.total - focusValue;
const side = Math.floor(remain / 2);
form.value.leadership = focus === 'leadership' ? focusValue : side;
form.value.strength = focus === 'strength' ? focusValue : side;
form.value.intel = focus === 'intel' ? focusValue : remain - side;
applyStats(abilityLeadpow(rules));
};
const applyLeadintStats = () => {
const rules = statRules.value;
if (!rules) {
return;
}
applyStats(abilityLeadint(rules));
};
const applyPowintStats = () => {
const rules = statRules.value;
if (!rules) {
return;
}
applyStats(abilityPowint(rules));
};
const loadConfig = async () => {
@@ -709,12 +712,11 @@ onUnmounted(() => {
</label>
</div>
<div class="stat-actions" aria-label="능력치 빠른 설정">
<div class="stat-actions" role="group" aria-label="능력치 빠른 설정">
<button type="button" @click="applyRandomStats">랜덤형</button>
<button type="button" @click="applyFocusedStats('leadership')">통솔형</button>
<button type="button" @click="applyFocusedStats('strength')">력형</button>
<button type="button" @click="applyFocusedStats('intel')">지력형</button>
<button type="button" @click="applyBalancedStats">균형형</button>
<button type="button" @click="applyLeadpowStats">통솔무력</button>
<button type="button" @click="applyLeadintStats">통솔지력형</button>
<button type="button" @click="applyPowintStats">무력지력형</button>
</div>
<div v-if="accountIcons.length" class="icon-choice">
@@ -751,7 +753,7 @@ onUnmounted(() => {
<button class="primary-action" type="submit" :disabled="!canSubmit || submitting">
{{ submitting ? '생성 중...' : '장수 생성' }}
</button>
<button type="button" class="ghost" @click="applyBalancedStats">균형형으로 되돌리기</button>
<button type="button" class="ghost" @click="applyBalancedStats">능력치 초기화</button>
</div>
</form>
</PanelCard>
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { abilityLeadint, abilityLeadpow, abilityPowint, abilityRand } from '../src/utils/generalStats.ts';
const rules = { min: 15, max: 80, total: 165 };
const sequence = (...values: number[]) => {
let index = 0;
return () => values[index++] ?? values.at(-1) ?? 0.5;
};
void describe('generalStats Ref presets', () => {
void it('normalizes the random preset to the configured total', () => {
assert.deepEqual(abilityRand(rules, sequence(0.2, 0.4, 0.6)), [36, 55, 74]);
});
void it('preserves the Ref two-stat weighted distributions and min/max correction order', () => {
assert.deepEqual(abilityLeadpow(rules, sequence(0.9, 0.8, 0.5)), [75, 75, 15]);
assert.deepEqual(abilityLeadint(rules, sequence(0.9, 0.5, 0.8)), [75, 15, 75]);
assert.deepEqual(abilityPowint(rules, sequence(0.5, 0.9, 0.8)), [15, 75, 75]);
});
});