fix nation symbol legacy boundaries

This commit is contained in:
2026-07-26 13:39:05 +00:00
parent 6d642bb759
commit 935f6ebe97
8 changed files with 399 additions and 17 deletions
@@ -3,6 +3,7 @@ import type { UniqueLotteryRunner } from '@sammo-ts/logic/rewards/uniqueLottery.
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
import type { GeneralWorldView } from '@sammo-ts/logic/triggers/general.js';
export interface ActionRandomSource {
nextFloat1(): number;
@@ -18,6 +19,7 @@ export type ActionContextBase = {
general: ActionContextGeneral;
city?: City;
nation?: Nation | null;
worldView?: GeneralWorldView;
rng: ActionRandomSource;
uniqueLottery?: UniqueLotteryRunner;
};
@@ -53,12 +53,28 @@ const NATION_COLORS = [
'#A9A9A9',
];
const resolveNationColorIndex = (value: number | string | boolean): number | null => {
let index: number;
if (typeof value === 'boolean') {
index = value ? 1 : 0;
} else if (typeof value === 'number') {
if (!Number.isFinite(value)) {
return null;
}
index = Math.trunc(value);
} else {
if (!/^(?:0|[1-9]\d*|-[1-9]\d*)$/.test(value)) {
return null;
}
index = Number(value);
}
return Number.isSafeInteger(index) && index >= 0 && index < NATION_COLORS.length ? index : null;
};
const ARGS_SCHEMA = z.object({
colorType: z
.number()
.int()
.min(0)
.max(NATION_COLORS.length - 1),
.union([z.number(), z.string(), z.boolean()])
.refine((value) => resolveNationColorIndex(value) !== null),
});
export type ChangeFlagArgs = z.infer<typeof ARGS_SCHEMA>;
@@ -100,7 +116,11 @@ export class ActionDefinition<
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
}
const color = NATION_COLORS[args.colorType];
const colorIndex = resolveNationColorIndex(args.colorType);
if (colorIndex === null) {
return { effects: [] };
}
const color = NATION_COLORS[colorIndex];
const generalName = general.name;
const nationName = nation.name;
@@ -120,11 +140,11 @@ export class ActionDefinition<
),
// Global Action Log
createLogEffect(
`<Y>${generalName}</>${josaYi} <span style='color:${color};'><b>국기</b></span>를 변경하였습니다.`,
`<Y>${generalName}</>${josaYi} <span style='color:${color};'><b>국기</b></span>를 변경하였습니다`,
{
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
),
// Global History Log
@@ -138,7 +158,7 @@ export class ActionDefinition<
),
// Actor Nation History Log
createLogEffect(
`<Y>${generalName}</>${josaYi} <span style='color:${color};'><b>국기</b></span>를 변경하였습니다.`,
`<Y>${generalName}</>${josaYi} <span style='color:${color};'><b>국기</b></span>를 변경하였습니다`,
{
scope: LogScope.NATION,
nationId: nation.id,
@@ -14,9 +14,17 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
import type { NationTurnCommandSpec } from './index.js';
import { z } from 'zod';
import { parseArgsWithSchema } from '../parseArgs.js';
import { getLegacyStringWidth } from '@sammo-ts/logic/troop/management.js';
const ARGS_SCHEMA = z.object({
nationName: z.string().trim().min(1).max(8),
nationName: z.string().superRefine((nationName, ctx) => {
if (nationName === '' || getLegacyStringWidth(nationName) > 18) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '국호는 전각 9자 또는 반각 18자 이하여야 합니다.',
});
}
}),
});
export type ChangeNationNameArgs = z.infer<typeof ARGS_SCHEMA>;
@@ -65,8 +73,21 @@ export class ActionDefinition<
const oldNationName = nation.name;
const newNationName = args.nationName;
if (context.worldView?.listNations?.().some((candidate) => candidate.name === newNationName)) {
return {
completed: false,
effects: [
createLogEffect(`이미 같은 국호를 가진 곳이 있습니다. ${ACTION_NAME} 실패`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
const josaYi = JosaUtil.pick(generalName, '이');
const josaYiNation = JosaUtil.pick(newNationName, '이');
const josaYiNation = JosaUtil.pick(oldNationName, '이');
const josaRo = JosaUtil.pick(newNationName, '로');
const effects: Array<GeneralActionEffect<TriggerState>> = [
@@ -83,8 +104,8 @@ export class ActionDefinition<
// Global Action Log
createLogEffect(`<Y>${generalName}</>${josaYi} 국호를 <D><b>${newNationName}</b></>${josaRo} 변경합니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}),
// Global History Log
createLogEffect(
+1
View File
@@ -6,6 +6,7 @@ import { TriggerCaller, type Trigger } from './core.js';
export interface GeneralWorldView<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
listGenerals(): General<TriggerState>[];
listGeneralsByCity?(cityId: number): General<TriggerState>[];
listNations?(): Nation[];
}
export interface GeneralActionLogSink {
@@ -3,7 +3,14 @@ import type { City, General, Nation } from '../../../src/domain/entities.js';
import { resolveGeneralAction } from '../../../src/actions/engine.js';
import { ActionDefinition as DeclareWarAction } from '../../../src/actions/turn/nation/che_선전포고.js';
import { ActionDefinition as MoveCapitalAction } from '../../../src/actions/turn/nation/che_천도.js';
import { ActionDefinition as ChangeNationNameAction } from '../../../src/actions/turn/nation/che_국호변경.js';
import {
ActionDefinition as ChangeNationNameAction,
commandSpec as changeNationNameCommandSpec,
} from '../../../src/actions/turn/nation/che_국호변경.js';
import {
ActionDefinition as ChangeNationFlagAction,
commandSpec as changeNationFlagCommandSpec,
} from '../../../src/actions/turn/nation/che_국기변경.js';
import { ActionDefinition as ExpandCityAction } from '../../../src/actions/turn/nation/che_증축.js';
import { ActionDefinition as LastStandAction } from '../../../src/actions/turn/nation/che_필사즉생.js';
import { ActionDefinition as DeceptionAction } from '../../../src/actions/turn/nation/che_허보.js';
@@ -97,6 +104,8 @@ const buildNation = (id: number, name = 'Nation'): Nation => ({
const schedule: TurnSchedule = {
entries: [{ startMinute: 0, tickMinutes: 60 }],
};
const changeNationNameArgsSchema = changeNationNameCommandSpec.argsSchema!;
const changeNationFlagArgsSchema = changeNationFlagCommandSpec.argsSchema!;
describe('Nation Actions', () => {
describe('che_선전포고 (Declare War)', () => {
@@ -312,6 +321,12 @@ describe('Nation Actions', () => {
});
describe('che_국호변경 (Change Nation Name)', () => {
it('uses the legacy display width without trimming the name', () => {
expect(changeNationNameArgsSchema.safeParse({ nationName: '가나다라마바사아자' }).success).toBe(true);
expect(changeNationNameArgsSchema.safeParse({ nationName: ' ' }).success).toBe(true);
expect(changeNationNameArgsSchema.safeParse({ nationName: '가나다라마바사아자차' }).success).toBe(false);
});
it('changes nation name', () => {
const nation = buildNation(1, 'OldName');
const general = buildGeneral(1, 1, 1);
@@ -333,6 +348,75 @@ describe('Nation Actions', () => {
})
);
});
it('returns a failed original action when any nation already has the name', () => {
const nation = buildNation(1, 'OldName');
const duplicateNation = buildNation(2, 'Duplicate');
const general = buildGeneral(1, 1, 1);
const definition = new ChangeNationNameAction();
const resolution = definition.resolve(
{
general,
nation,
worldView: {
listGenerals: () => [general],
listNations: () => [nation, duplicateNation],
},
rng: {} as any,
addLog: () => {},
},
{ nationName: duplicateNation.name }
);
expect(resolution.completed).toBe(false);
expect(resolution.effects).toContainEqual(
expect.objectContaining({
type: 'log',
entry: expect.objectContaining({
text: '이미 같은 국호를 가진 곳이 있습니다. 국호변경 실패',
}),
})
);
});
});
describe('che_국기변경 (Change Nation Flag)', () => {
it('accepts the same scalar array keys as PHP and preserves the raw argument', () => {
for (const colorType of [0, '1', true, false, 1.9, -0.9, 32.9]) {
const parsed = changeNationFlagArgsSchema.safeParse({ colorType });
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.colorType).toBe(colorType);
}
}
for (const colorType of ['01', '1.5', 33, -1, null]) {
expect(changeNationFlagArgsSchema.safeParse({ colorType }).success).toBe(false);
}
});
it('resolves a numeric string to the matching legacy color', () => {
const nation = buildNation(1);
const general = buildGeneral(1, 1, 1);
const definition = new ChangeNationFlagAction();
const resolution = definition.resolve(
{
general,
nation,
rng: {} as any,
addLog: () => {},
},
{ colorType: '1' }
);
expect(resolution.effects).toContainEqual(
expect.objectContaining({
type: 'nation:patch',
patch: expect.objectContaining({ color: '#800000' }),
})
);
});
});
describe('che_증축 (City Expansion)', () => {