Merge branch 'main' into feature/best-general-data-parity

This commit is contained in:
2026-07-26 06:03:32 +00:00
37 changed files with 858 additions and 155 deletions
@@ -16,7 +16,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { parseArgsWithSchema } from '../parseArgs.js';
import { normalizeResourceActionAmount } from './resourceAmount.js';
import { normalizeResourceActionAmount } from '../resourceAmount.js';
export interface TradeEnvironment {
exchangeFee?: number;
@@ -20,7 +20,7 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { parseArgsWithSchema } from '../parseArgs.js';
import { normalizeResourceActionAmount } from './resourceAmount.js';
import { normalizeResourceActionAmount } from '../resourceAmount.js';
const ACTION_NAME = '증여';
const ACTION_KEY = 'che_증여';
@@ -21,7 +21,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import { parseArgsWithSchema } from '../parseArgs.js';
import { normalizeResourceActionAmount } from './resourceAmount.js';
import { normalizeResourceActionAmount } from '../resourceAmount.js';
const ACTION_NAME = '헌납';
const ACTION_KEY = 'che_헌납';
@@ -24,13 +24,11 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
import { clamp } from 'es-toolkit';
import { z } from 'zod';
import { parseArgsWithSchema } from '../parseArgs.js';
import { normalizeResourceActionAmount } from '../resourceAmount.js';
const ARGS_SCHEMA = z.object({
isGold: z.boolean(),
amount: z.preprocess(
(value) => (typeof value === 'number' ? Math.floor(value / 100) * 100 : value),
z.number().int().positive()
),
amount: z.number(),
destGeneralID: z.number(),
});
export type SeizureArgs = z.infer<typeof ARGS_SCHEMA>;
@@ -56,9 +54,13 @@ export class ActionDefinition<
if (!data) {
return null;
}
const amount = normalizeResourceActionAmount(data.amount, this.env.maxResourceActionAmount);
if (amount === null) {
return null;
}
return {
...data,
amount: clamp(data.amount, 100, this.env.maxResourceActionAmount ?? 10000),
amount,
};
}
@@ -144,10 +144,10 @@ export class ActionResolver<
createLogEffect(
`<Y>${context.destGeneral.name}</>에게 ${label} <C>${amountText}</>${amountJosa} 수여했습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
)
);
@@ -199,7 +199,7 @@ export class ActionDefinition<
requirements.push({ kind: 'destGeneral', id: ctx.destGeneralId });
}
if (ctx.destGeneralId === ctx.actorId) {
if (args.destGeneralId === ctx.actorId) {
return [denyWithReason('본인입니다')];
}
+5 -2
View File
@@ -124,8 +124,11 @@ export const parsePercent = (value: string): number | null => {
export type CompareOperator = '>' | '>=' | '==' | '<=' | '<' | '!=' | '===' | '!==';
export const compareValues = (target: unknown, op: CompareOperator, source: unknown): boolean => {
const lhs = target as any;
const rhs = source as any;
// The cast is type-only: JavaScript still applies its native relational
// coercion rules to the original runtime values, matching the legacy
// constraint evaluator without opting the whole comparison into `any`.
const lhs = target as number;
const rhs = source as number;
switch (op) {
case '<':
return lhs < rhs;
+2 -6
View File
@@ -196,10 +196,7 @@ const resolveUnitReport = (unit: WarUnit): WarUnitReport => {
};
};
const buildTraceUnitSnapshot = (
unit: WarUnit,
defenderCity: City
): WarBattleTraceUnitSnapshot => {
const buildTraceUnitSnapshot = (unit: WarUnit, defenderCity: City): WarBattleTraceUnitSnapshot => {
const common = {
kind: unit instanceof WarUnitGeneral ? ('general' as const) : ('city' as const),
id: unit instanceof WarUnitGeneral ? unit.getGeneral().id : (unit as WarUnitCity).getCityId(),
@@ -339,7 +336,6 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
);
const iter = defenderUnits.values();
let defender: WarUnit<TriggerState> | null = null;
const getNextDefender = (
_prevDefender: WarUnit<TriggerState> | null,
@@ -359,7 +355,7 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
return candidate;
};
defender = getNextDefender(null, true);
let defender = getNextDefender(null, true);
let conquerCity = false;
let logWritten = false;
let traceSeq = 0;
@@ -225,7 +225,7 @@ describe('migrated general commands', () => {
expect(updatedLord.experience).toBe(700);
});
it('che_증여: 최소 보유량을 넘는 자원만 이전한다', async () => {
it('che_증여: 금은 레거시 최소 보유량 0을 적용해 요청한 금액을 이전한다', async () => {
const actor = makeGeneral({ id: 1, nationId: 1, cityId: 1, name: '증여자', gold: 1300 });
const dest = makeGeneral({ id: 2, nationId: 1, cityId: 1, name: '수령자', gold: 200 });
const nation = makeNation({ id: 1, name: '오', chiefGeneralId: 1, capitalCityId: 1, level: 1 });
@@ -246,8 +246,8 @@ describe('migrated general commands', () => {
},
]);
expect(world.getGeneral(actor.id)!.gold).toBe(1000);
expect(world.getGeneral(dest.id)!.gold).toBe(500);
expect(world.getGeneral(actor.id)!.gold).toBe(800);
expect(world.getGeneral(dest.id)!.gold).toBe(700);
});
it('che_해산: 방랑군 해산 시 세력과 소속을 정리한다', async () => {
@@ -258,7 +258,6 @@ describe('General Commands New Scenario', () => {
// 6. Retire (Needs age >= 60)
// Manually set age
// Manually set age
const gToRetire = { ...g1_after_resign, age: 65 };
world.snapshot.generals = world.snapshot.generals.map((g) => (g.id === 1 ? gToRetire : g));
const retireDef = retireSpec.createDefinition(systemEnv);
@@ -274,7 +273,7 @@ describe('General Commands New Scenario', () => {
const g1_after_retire = world.getGeneral(1)!;
expect(g1_after_retire.age).toBe(20);
// General::rebirth()는 앞선 명령으로 누적된 경험을 초기화하지 않고 절반으로 줄인다.
expect(g1_after_retire.experience).toBe(142);
expect(g1_after_retire.experience).toBe(Math.round(gToRetire.experience * 0.5));
});
it('should execute employ and sabotage commands', async () => {