fix(frontend): restore Ref general stat presets
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user