fix nation symbol legacy boundaries
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user