fix: 선택 불가 국가 성향을 입력 목록에서 제외
Ref의 availableNationType 13개를 공용 목록으로 정의하고 건국, NPC/AI 건국, 전투 시뮬레이터 입력과 검증에 적용한다.
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { isAvailableNationTraitKey } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import type { BattleSimRequestPayload } from './types.js';
|
import type { BattleSimRequestPayload } from './types.js';
|
||||||
|
|
||||||
const zBattleSimGeneral = z.object({
|
const zBattleSimGeneral = z.object({
|
||||||
@@ -71,7 +73,7 @@ const zBattleSimCity = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const zBattleSimNation = z.object({
|
const zBattleSimNation = z.object({
|
||||||
type: z.string().min(1),
|
type: z.string().refine(isAvailableNationTraitKey),
|
||||||
tech: z.number().min(0),
|
tech: z.number().min(0),
|
||||||
level: z.number().int().min(0),
|
level: z.number().int().min(0),
|
||||||
capital: z.number().int().min(0),
|
capital: z.number().int().min(0),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AVAILABLE_NATION_TRAIT_KEYS,
|
||||||
ITEM_KEYS,
|
ITEM_KEYS,
|
||||||
EVENT_DOMESTIC_TRAIT_KEYS,
|
EVENT_DOMESTIC_TRAIT_KEYS,
|
||||||
loadEventDomesticTraitModules,
|
loadEventDomesticTraitModules,
|
||||||
@@ -6,7 +7,6 @@ import {
|
|||||||
loadNationTraitModules,
|
loadNationTraitModules,
|
||||||
loadPersonalityTraitModules,
|
loadPersonalityTraitModules,
|
||||||
loadWarTraitModules,
|
loadWarTraitModules,
|
||||||
NATION_TRAIT_KEYS,
|
|
||||||
PERSONALITY_TRAIT_KEYS,
|
PERSONALITY_TRAIT_KEYS,
|
||||||
WAR_TRAIT_KEYS,
|
WAR_TRAIT_KEYS,
|
||||||
type ItemModule,
|
type ItemModule,
|
||||||
@@ -110,7 +110,7 @@ export const loadBattleSimTraitOptions = async (): Promise<{
|
|||||||
}> => {
|
}> => {
|
||||||
if (!cachedTraitOptions) {
|
if (!cachedTraitOptions) {
|
||||||
cachedTraitOptions = Promise.all([
|
cachedTraitOptions = Promise.all([
|
||||||
loadNationTraitModules([...NATION_TRAIT_KEYS]),
|
loadNationTraitModules([...AVAILABLE_NATION_TRAIT_KEYS]),
|
||||||
loadEventDomesticTraitModules([...EVENT_DOMESTIC_TRAIT_KEYS]),
|
loadEventDomesticTraitModules([...EVENT_DOMESTIC_TRAIT_KEYS]),
|
||||||
loadWarTraitModules([...WAR_TRAIT_KEYS]),
|
loadWarTraitModules([...WAR_TRAIT_KEYS]),
|
||||||
loadPersonalityTraitModules([...PERSONALITY_TRAIT_KEYS]),
|
loadPersonalityTraitModules([...PERSONALITY_TRAIT_KEYS]),
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const buildBattleRequest = () => ({
|
|||||||
conflict: '{}',
|
conflict: '{}',
|
||||||
},
|
},
|
||||||
attackerNation: {
|
attackerNation: {
|
||||||
type: 'test',
|
type: 'che_도적',
|
||||||
tech: 1000,
|
tech: 1000,
|
||||||
level: 1,
|
level: 1,
|
||||||
capital: 1,
|
capital: 1,
|
||||||
@@ -193,7 +193,7 @@ const buildBattleRequest = () => ({
|
|||||||
conflict: '{}',
|
conflict: '{}',
|
||||||
},
|
},
|
||||||
defenderNation: {
|
defenderNation: {
|
||||||
type: 'test',
|
type: 'che_도적',
|
||||||
tech: 1000,
|
tech: 1000,
|
||||||
level: 1,
|
level: 1,
|
||||||
capital: 2,
|
capital: 2,
|
||||||
@@ -309,6 +309,27 @@ describe('battle router orchestration', () => {
|
|||||||
expect(battleSim.simulateCalls).toBe(0);
|
expect(battleSim.simulateCalls).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects the neutral storage nation type before preparing or queuing a simulation', async () => {
|
||||||
|
const battleSim = new QueuedBattleSimTransport();
|
||||||
|
const state: WorldStateRow = {
|
||||||
|
id: 1,
|
||||||
|
scenarioCode: 'default',
|
||||||
|
currentYear: 200,
|
||||||
|
currentMonth: 1,
|
||||||
|
tickSeconds: 600,
|
||||||
|
config: {},
|
||||||
|
meta: {},
|
||||||
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
||||||
|
};
|
||||||
|
const caller = appRouter.createCaller(buildContext({ state, battleSim }));
|
||||||
|
const request = buildBattleRequest();
|
||||||
|
request.attackerNation.type = 'che_중립';
|
||||||
|
|
||||||
|
await expect(caller.battle.prepareSimulation(request)).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
||||||
|
await expect(caller.battle.simulate(request)).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
||||||
|
expect(battleSim.simulateCalls).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns queued then completed results via transport', async () => {
|
it('returns queued then completed results via transport', async () => {
|
||||||
const battleSim = new QueuedBattleSimTransport();
|
const battleSim = new QueuedBattleSimTransport();
|
||||||
const state: WorldStateRow = {
|
const state: WorldStateRow = {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { AVAILABLE_NATION_TRAIT_KEYS } from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
import { loadBattleSimTraitOptions } from '../src/battleSim/simulatorOptions.js';
|
||||||
|
|
||||||
|
describe('selectable trait options', () => {
|
||||||
|
it('uses the Ref available nation-type list for founding and battle simulation inputs', async () => {
|
||||||
|
const options = await loadBattleSimTraitOptions();
|
||||||
|
|
||||||
|
expect(options.nationTypes.map((entry) => entry.key)).toEqual(AVAILABLE_NATION_TRAIT_KEYS);
|
||||||
|
expect(options.nationTypes).not.toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ key: 'che_중립' })])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,7 +13,10 @@ import type { ConstraintContext } from '@sammo-ts/logic';
|
|||||||
import { GAME_TICKS_PER_TURN, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
import { GAME_TICKS_PER_TURN, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||||
import { simpleSerialize } from '@sammo-ts/logic/war/utils.js';
|
import { simpleSerialize } from '@sammo-ts/logic/war/utils.js';
|
||||||
import { resolveStartYear, resolveTurnTermMinutes } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
|
import { resolveStartYear, resolveTurnTermMinutes } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
|
||||||
import { NATION_TRAIT_KEYS } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
|
import {
|
||||||
|
AVAILABLE_NATION_TRAIT_KEYS,
|
||||||
|
isAvailableNationTraitKey,
|
||||||
|
} from '@sammo-ts/logic/actionModules/traits/nation/index.js';
|
||||||
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
|
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
|
||||||
|
|
||||||
import type { ReservedTurnEntry } from '../../reservedTurnStore.js';
|
import type { ReservedTurnEntry } from '../../reservedTurnStore.js';
|
||||||
@@ -348,8 +351,10 @@ export class GeneralAI {
|
|||||||
chiefStatMin: this.scenarioConfig.stat.chiefMin,
|
chiefStatMin: this.scenarioConfig.stat.chiefMin,
|
||||||
npcMessageFreqByDay: readNumber(constValues.npcMessageFreqByDay, 0),
|
npcMessageFreqByDay: readNumber(constValues.npcMessageFreqByDay, 0),
|
||||||
availableNationTypes: Array.isArray(constValues.availableNationType)
|
availableNationTypes: Array.isArray(constValues.availableNationType)
|
||||||
? constValues.availableNationType.filter((value) => typeof value === 'string')
|
? constValues.availableNationType.filter(
|
||||||
: NATION_TRAIT_KEYS.filter((value) => value !== 'che_중립'),
|
(value): value is string => typeof value === 'string' && isAvailableNationTraitKey(value)
|
||||||
|
)
|
||||||
|
: [...AVAILABLE_NATION_TRAIT_KEYS],
|
||||||
};
|
};
|
||||||
|
|
||||||
const generalPolicy = new AutorunGeneralPolicy(
|
const generalPolicy = new AutorunGeneralPolicy(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
LogCategory,
|
LogCategory,
|
||||||
LogFormat,
|
LogFormat,
|
||||||
LogScope,
|
LogScope,
|
||||||
NATION_TRAIT_KEYS,
|
AVAILABLE_NATION_TRAIT_KEYS,
|
||||||
getCityDistance,
|
getCityDistance,
|
||||||
type City,
|
type City,
|
||||||
type MapDefinition,
|
type MapDefinition,
|
||||||
@@ -54,7 +54,6 @@ const NATION_COLORS = [
|
|||||||
'#FFFFFF',
|
'#FFFFFF',
|
||||||
'#A9A9A9',
|
'#A9A9A9',
|
||||||
] as const;
|
] as const;
|
||||||
const AVAILABLE_NATION_TYPES = NATION_TRAIT_KEYS.filter((key) => key !== 'che_중립');
|
|
||||||
const NPC_TYPE = 6;
|
const NPC_TYPE = 6;
|
||||||
const NPC_PREFIX = 'ⓤ';
|
const NPC_PREFIX = 'ⓤ';
|
||||||
const STAT_TYPE_WEIGHTS = { 무: 1, 지: 1 } as const;
|
const STAT_TYPE_WEIGHTS = { 무: 1, 지: 1 } as const;
|
||||||
@@ -301,7 +300,7 @@ export const createRaiseNpcNationHandler = (options: {
|
|||||||
|
|
||||||
const nationId = world.getNextNationId();
|
const nationId = world.getNextNationId();
|
||||||
const color = rng.choice([...NATION_COLORS]);
|
const color = rng.choice([...NATION_COLORS]);
|
||||||
const typeCode = rng.choice([...AVAILABLE_NATION_TYPES]);
|
const typeCode = rng.choice([...AVAILABLE_NATION_TRAIT_KEYS]);
|
||||||
const nation: Nation = {
|
const nation: Nation = {
|
||||||
id: nationId,
|
id: nationId,
|
||||||
name: `${NPC_PREFIX}${city.name}`,
|
name: `${NPC_PREFIX}${city.name}`,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const simulatorOptions = {
|
|||||||
{ id: 200, name: '궁병', armType: 2 },
|
{ id: 200, name: '궁병', armType: 2 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
nationTypes: [{ key: 'che_중립', name: '중립', info: '특별한 효과 없음' }],
|
nationTypes: [{ key: 'che_도적', name: '도적', info: '금 수입 증가, 쌀 수입 감소' }],
|
||||||
eventDomesticTraits: [{ key: 'che_event_신산', name: '신산', info: '계략 강화' }],
|
eventDomesticTraits: [{ key: 'che_event_신산', name: '신산', info: '계략 강화' }],
|
||||||
warTraits: [{ key: 'che_필살', name: '필살', info: '필살 확률 증가' }],
|
warTraits: [{ key: 'che_필살', name: '필살', info: '필살 확률 증가' }],
|
||||||
personalities: [{ key: 'che_대담', name: '대담', info: '공격적인 성격' }],
|
personalities: [{ key: 'che_대담', name: '대담', info: '공격적인 성격' }],
|
||||||
@@ -352,6 +352,10 @@ test('operates independent/game presets, imports my general, and renders battle
|
|||||||
await page.setViewportSize({ width: 1280, height: 900 });
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
await gotoSimulator(page);
|
await gotoSimulator(page);
|
||||||
|
|
||||||
|
const nationTypeSelects = page.locator('[data-parity-id="attacker-nation"] select').first();
|
||||||
|
await expect(nationTypeSelects).toHaveValue('che_도적');
|
||||||
|
await expect(nationTypeSelects.locator('option[value="che_중립"]')).toHaveCount(0);
|
||||||
|
|
||||||
const notice = page.getByLabel('시뮬레이터 데이터 안내');
|
const notice = page.getByLabel('시뮬레이터 데이터 안내');
|
||||||
const noticeRect = await notice.boundingBox();
|
const noticeRect = await notice.boundingBox();
|
||||||
expect(noticeRect?.width).toBeLessThan(100);
|
expect(noticeRect?.width).toBeLessThan(100);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const inputOptions = {
|
|||||||
],
|
],
|
||||||
crewTypes: [{ value: 1100, label: '보병' }],
|
crewTypes: [{ value: 1100, label: '보병' }],
|
||||||
armTypes: [{ value: 1, label: '보병' }],
|
armTypes: [{ value: 1, label: '보병' }],
|
||||||
nationTypes: [{ value: 'che_중립', label: '중립' }],
|
nationTypes: [{ value: 'che_도적', label: '도적', description: '금 수입 증가, 쌀 수입 감소' }],
|
||||||
colors: [{ value: 0, label: '색상 1', color: '#ff0000' }],
|
colors: [{ value: 0, label: '색상 1', color: '#ff0000' }],
|
||||||
items: { horse: [{ value: 'None', label: '판매/해제' }] },
|
items: { horse: [{ value: 'None', label: '판매/해제' }] },
|
||||||
recruitment: {
|
recruitment: {
|
||||||
@@ -567,6 +567,61 @@ test('renders and accepts every Ref strategy command at mobile width', async ({
|
|||||||
await picker.screenshot({ path: test.info().outputPath('all-strategy-commands-mobile.png') });
|
await picker.screenshot({ path: test.info().outputPath('all-strategy-commands-mobile.png') });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('defaults founding to a Ref-selectable nation trait without exposing the neutral storage trait', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const foundingCommandTable = {
|
||||||
|
general: [
|
||||||
|
{
|
||||||
|
category: '국가',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
key: 'che_건국',
|
||||||
|
name: '건국',
|
||||||
|
reqArg: true,
|
||||||
|
possible: true,
|
||||||
|
status: 'needsInput',
|
||||||
|
inputFields: [
|
||||||
|
{ key: 'nationName', label: '국가명', kind: 'text', required: true, min: 1, max: 18 },
|
||||||
|
{
|
||||||
|
key: 'nationType',
|
||||||
|
label: '국가 성향',
|
||||||
|
kind: 'select',
|
||||||
|
required: true,
|
||||||
|
optionSource: 'nationTypes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'colorType',
|
||||||
|
label: '국기 색상',
|
||||||
|
kind: 'select',
|
||||||
|
required: true,
|
||||||
|
optionSource: 'colors',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nation: [],
|
||||||
|
inputOptions,
|
||||||
|
};
|
||||||
|
await install(page, false, foundingCommandTable);
|
||||||
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
|
|
||||||
|
const picker = page.getByTestId('command-picker');
|
||||||
|
await picker.getByRole('button', { name: '국가', exact: true }).click();
|
||||||
|
await picker.getByRole('button', { name: '건국', exact: true }).click();
|
||||||
|
const nationType = picker.getByLabel('국가 성향');
|
||||||
|
await expect(nationType).toHaveValue('che_도적');
|
||||||
|
await expect(nationType.locator('option[value="che_중립"]')).toHaveCount(0);
|
||||||
|
await expect(nationType.locator('option')).toHaveText(['도적']);
|
||||||
|
await nationType.focus();
|
||||||
|
await expect(nationType).toBeFocused();
|
||||||
|
await picker.screenshot({ path: test.info().outputPath('founding-selectable-nation-trait-desktop-1200.png') });
|
||||||
|
});
|
||||||
|
|
||||||
test('reserves force move, retirement, and resignation from the user command picker', async ({ page }) => {
|
test('reserves force move, retirement, and resignation from the user command picker', async ({ page }) => {
|
||||||
const specialCommandTable = {
|
const specialCommandTable = {
|
||||||
general: [
|
general: [
|
||||||
|
|||||||
@@ -254,6 +254,11 @@ const toExportedGeneral = (general: GeneralDraft): GeneralExport => ({
|
|||||||
inheritBuff: { ...general.inheritBuff },
|
inheritBuff: { ...general.inheritBuff },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resolveAvailableNationType = (candidate?: string | null): string => {
|
||||||
|
const available = options.value?.nationTypes ?? [];
|
||||||
|
return available.some((entry) => entry.key === candidate) ? (candidate as string) : (available[0]?.key ?? '');
|
||||||
|
};
|
||||||
|
|
||||||
const initializeDefaults = async () => {
|
const initializeDefaults = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
@@ -267,9 +272,9 @@ const initializeDefaults = async () => {
|
|||||||
repeatCnt.value = 1;
|
repeatCnt.value = 1;
|
||||||
seed.value = '';
|
seed.value = '';
|
||||||
|
|
||||||
const nationTypeDefault = context.nationTypes[0]?.key ?? 'che_중립';
|
const nationTypeDefault = resolveAvailableNationType(me?.nation?.typeCode);
|
||||||
attackerNation.type = me?.nation?.typeCode ?? nationTypeDefault;
|
attackerNation.type = nationTypeDefault;
|
||||||
defenderNation.type = me?.nation?.typeCode ?? nationTypeDefault;
|
defenderNation.type = nationTypeDefault;
|
||||||
|
|
||||||
attackerNation.level = me?.nation?.level ?? 0;
|
attackerNation.level = me?.nation?.level ?? 0;
|
||||||
defenderNation.level = me?.nation?.level ?? 0;
|
defenderNation.level = me?.nation?.level ?? 0;
|
||||||
@@ -302,10 +307,10 @@ const applyGameEnvironment = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const me = gameDefaults.value;
|
const me = gameDefaults.value;
|
||||||
const nationTypeDefault = options.value.nationTypes[0]?.key ?? 'che_중립';
|
const nationTypeDefault = resolveAvailableNationType(me?.nation?.typeCode);
|
||||||
year.value = options.value.world.currentYear;
|
year.value = options.value.world.currentYear;
|
||||||
month.value = options.value.world.currentMonth;
|
month.value = options.value.world.currentMonth;
|
||||||
attackerNation.type = me?.nation?.typeCode ?? nationTypeDefault;
|
attackerNation.type = nationTypeDefault;
|
||||||
defenderNation.type = attackerNation.type;
|
defenderNation.type = attackerNation.type;
|
||||||
attackerNation.level = me?.nation?.level ?? 0;
|
attackerNation.level = me?.nation?.level ?? 0;
|
||||||
defenderNation.level = attackerNation.level;
|
defenderNation.level = attackerNation.level;
|
||||||
@@ -324,7 +329,7 @@ const applyIndependentEnvironment = () => {
|
|||||||
if (!options.value) {
|
if (!options.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nationTypeDefault = options.value.nationTypes[0]?.key ?? 'che_중립';
|
const nationTypeDefault = resolveAvailableNationType();
|
||||||
year.value = options.value.world.startYear;
|
year.value = options.value.world.startYear;
|
||||||
month.value = 1;
|
month.value = 1;
|
||||||
seed.value = '';
|
seed.value = '';
|
||||||
@@ -742,13 +747,13 @@ const importBattle = (data: BattleExport) => {
|
|||||||
month.value = data.month;
|
month.value = data.month;
|
||||||
repeatCnt.value = data.repeatCnt;
|
repeatCnt.value = data.repeatCnt;
|
||||||
|
|
||||||
attackerNation.type = data.attackerNation.type;
|
attackerNation.type = resolveAvailableNationType(data.attackerNation.type);
|
||||||
attackerNation.level = data.attackerNation.level;
|
attackerNation.level = data.attackerNation.level;
|
||||||
attackerNation.tech = Math.floor(data.attackerNation.tech / 1000);
|
attackerNation.tech = Math.floor(data.attackerNation.tech / 1000);
|
||||||
attackerNation.isCapital = data.attackerNation.capital === 1;
|
attackerNation.isCapital = data.attackerNation.capital === 1;
|
||||||
attackerCity.level = data.attackerCity.level;
|
attackerCity.level = data.attackerCity.level;
|
||||||
|
|
||||||
defenderNation.type = data.defenderNation.type;
|
defenderNation.type = resolveAvailableNationType(data.defenderNation.type);
|
||||||
defenderNation.level = data.defenderNation.level;
|
defenderNation.level = data.defenderNation.level;
|
||||||
defenderNation.tech = Math.floor(data.defenderNation.tech / 1000);
|
defenderNation.tech = Math.floor(data.defenderNation.tech / 1000);
|
||||||
defenderNation.isCapital = data.defenderNation.capital === 3;
|
defenderNation.isCapital = data.defenderNation.capital === 3;
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ export const NATION_TRAIT_KEYS = [
|
|||||||
|
|
||||||
export type NationTraitKey = (typeof NATION_TRAIT_KEYS)[number];
|
export type NationTraitKey = (typeof NATION_TRAIT_KEYS)[number];
|
||||||
|
|
||||||
|
// Ref GameConst::$availableNationType excludes the neutral storage/default trait.
|
||||||
|
// Founding, NPC founding, and the battle simulator must only expose this list.
|
||||||
|
export type AvailableNationTraitKey = Exclude<NationTraitKey, 'che_중립'>;
|
||||||
|
|
||||||
|
export const AVAILABLE_NATION_TRAIT_KEYS: readonly AvailableNationTraitKey[] = NATION_TRAIT_KEYS.filter(
|
||||||
|
(key): key is Exclude<NationTraitKey, 'che_중립'> => key !== 'che_중립'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const isAvailableNationTraitKey = (value: string): value is AvailableNationTraitKey =>
|
||||||
|
AVAILABLE_NATION_TRAIT_KEYS.includes(value as AvailableNationTraitKey);
|
||||||
|
|
||||||
export type NationTraitModule = TraitModule;
|
export type NationTraitModule = TraitModule;
|
||||||
|
|
||||||
export type NationTraitImporter = () => Promise<TraitModuleExport>;
|
export type NationTraitImporter = () => Promise<TraitModuleExport>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NATION_TRAIT_KEYS } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
|
import { isAvailableNationTraitKey } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
|
||||||
import { getLegacyStringWidth } from '@sammo-ts/logic/troop/management.js';
|
import { getLegacyStringWidth } from '@sammo-ts/logic/troop/management.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -38,14 +38,12 @@ export const NATION_COLORS = [
|
|||||||
'#A9A9A9',
|
'#A9A9A9',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const SELECTABLE_NATION_TYPES = new Set<string>(NATION_TRAIT_KEYS.filter((key) => key !== 'che_중립'));
|
|
||||||
|
|
||||||
export const FOUNDING_ARGS_SCHEMA = z.object({
|
export const FOUNDING_ARGS_SCHEMA = z.object({
|
||||||
nationName: z
|
nationName: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.refine((value) => getLegacyStringWidth(value) <= 18),
|
.refine((value) => getLegacyStringWidth(value) <= 18),
|
||||||
nationType: z.string().refine((value) => SELECTABLE_NATION_TYPES.has(value)),
|
nationType: z.string().refine(isAvailableNationTraitKey),
|
||||||
colorType: z
|
colorType: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AVAILABLE_NATION_TRAIT_KEYS,
|
||||||
|
isAvailableNationTraitKey,
|
||||||
|
NATION_TRAIT_KEYS,
|
||||||
|
} from '../src/actionModules/traits/nation/index.js';
|
||||||
|
|
||||||
|
describe('Ref-selectable nation traits', () => {
|
||||||
|
it('keeps the neutral storage trait valid internally but unavailable to user selection', () => {
|
||||||
|
expect(NATION_TRAIT_KEYS).toContain('che_중립');
|
||||||
|
expect(AVAILABLE_NATION_TRAIT_KEYS).toEqual([
|
||||||
|
'che_도적',
|
||||||
|
'che_명가',
|
||||||
|
'che_음양가',
|
||||||
|
'che_종횡가',
|
||||||
|
'che_불가',
|
||||||
|
'che_오두미도',
|
||||||
|
'che_태평도',
|
||||||
|
'che_도가',
|
||||||
|
'che_묵가',
|
||||||
|
'che_덕가',
|
||||||
|
'che_병가',
|
||||||
|
'che_유가',
|
||||||
|
'che_법가',
|
||||||
|
]);
|
||||||
|
expect(isAvailableNationTraitKey('che_중립')).toBe(false);
|
||||||
|
expect(isAvailableNationTraitKey('che_도적')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user