코드 수정
This commit is contained in:
@@ -18,6 +18,8 @@ import {
|
||||
type UnitSetDefinition,
|
||||
type WarBattleOutcome,
|
||||
type WarActionModule,
|
||||
type WarUnitReport,
|
||||
type CrewTypeDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import { type BattleSimJobPayload, type BattleSimLogBuckets, type BattleSimResultPayload } from './types.js';
|
||||
@@ -217,7 +219,7 @@ const resolveCityRiceConsumption = (options: {
|
||||
year: number;
|
||||
startYear: number;
|
||||
}): number => {
|
||||
const cityReport = options.battle.reports.find((report: any) => report.type === 'city');
|
||||
const cityReport = options.battle.reports.find((report: WarUnitReport) => report.type === 'city');
|
||||
if (!cityReport) {
|
||||
return 0;
|
||||
}
|
||||
@@ -225,7 +227,9 @@ const resolveCityRiceConsumption = (options: {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const crewType = options.unitSet.crewTypes?.find((item: any) => item.id === options.castleCrewTypeId);
|
||||
const crewType = options.unitSet.crewTypes?.find(
|
||||
(item: CrewTypeDefinition) => item.id === options.castleCrewTypeId
|
||||
);
|
||||
const riceCoef = crewType?.rice ?? 1;
|
||||
const tech = Number(options.defenderNation.meta.tech ?? 0);
|
||||
const trainAtmos = resolveCityTrainAtmos(options.year, options.startYear);
|
||||
@@ -333,7 +337,9 @@ export const processBattleSimJob = (payload: BattleSimJobPayload): BattleSimResu
|
||||
});
|
||||
|
||||
lastBattle = outcome;
|
||||
const attackerReport = outcome.reports.find((report: any) => report.type === 'general' && report.isAttacker);
|
||||
const attackerReport = outcome.reports.find(
|
||||
(report: WarUnitReport) => report.type === 'general' && report.isAttacker
|
||||
);
|
||||
const killed = attackerReport?.killed ?? 0;
|
||||
const dead = attackerReport?.dead ?? 0;
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ const evaluateAvailability = (
|
||||
reason: result.reason,
|
||||
};
|
||||
}
|
||||
const missingKinds = new Set(result.missing.map((req: any) => req.kind));
|
||||
const missingKinds = new Set(result.missing.map((req: RequirementKey) => req.kind));
|
||||
const inputOnlyMissing =
|
||||
missingKinds.size === 0 ? reqArg : Array.from(missingKinds).every((kind) => INPUT_REQUIREMENT_KINDS.has(kind));
|
||||
if (inputOnlyMissing) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
nearCity,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
|
||||
existsDestCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export interface ReturnArgs { }
|
||||
export interface ReturnArgs {}
|
||||
|
||||
const ACTION_NAME = '귀환';
|
||||
const ACTION_KEY = 'che_귀환';
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
allow,
|
||||
existsDestNation,
|
||||
existsDestGeneral,
|
||||
notSameDestNation,
|
||||
destGeneralInDestNation,
|
||||
notLord,
|
||||
readMetaNumberFromUnknown,
|
||||
unknownOrDeny,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
@@ -44,6 +42,8 @@ export class ActionResolver<
|
||||
> implements GeneralActionResolver<TriggerState, AcceptScoutArgs> {
|
||||
readonly key = ACTION_KEY;
|
||||
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
|
||||
resolve(
|
||||
context: AcceptScoutResolveContext<TriggerState>,
|
||||
_args: AcceptScoutArgs
|
||||
@@ -59,7 +59,6 @@ export class ActionResolver<
|
||||
|
||||
// 1. Logs
|
||||
const destNationName = destNation.name;
|
||||
const recruiterName = destGeneral.name;
|
||||
const generalName = general.name;
|
||||
|
||||
const josaRo = JosaUtil.pick(destNationName, '로');
|
||||
@@ -67,28 +66,14 @@ export class ActionResolver<
|
||||
|
||||
// Self Log
|
||||
context.addLog(`<D>${destNationName}</>${josaRo} 망명하여 수도로 이동합니다.`, {
|
||||
// Text says "Move to Capital", but logic might move to recruiter city.
|
||||
// Legacy log says "수도로 이동합니다", but implementation moves to destGeneral city if present!
|
||||
// We should match implementation or text? Text is just flavor.
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.InGame, // Using InGame or specific format?
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
|
||||
// Recruiter Log
|
||||
// We need to add log to recruiter? `context.addSideEffectLog`?
|
||||
// Current system mostly logs for the actor.
|
||||
// If we want to log for recruiter, we might need a way to push logs to others in `effects` or strictly via `addLog` with target?
|
||||
// `GeneralActionResolveContext` usually implies logs are for the actor.
|
||||
// But `GeneralTurnOutcome` doesn't explicitly return logs for others.
|
||||
// We can create a patch for recruiter that appends to their log?
|
||||
// Or usage of `addLog` might support target? No, `addLog` in context usually targets actor.
|
||||
// We will skip Recruiter Log for now or rely on Global Log.
|
||||
|
||||
// Global Log
|
||||
context.addLog(`<Y>${generalName}</>${josaYi} <D><b>${destNationName}</b></>${josaRo} <S>망명</>하였습니다.`, {
|
||||
category: LogCategory.ACTION, // Global category?
|
||||
format: LogFormat.InGame, // Global logs are handled by system?
|
||||
// In new system, we might need to specify it.
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
});
|
||||
|
||||
// 2. Recruiter Rewards
|
||||
@@ -103,12 +88,9 @@ export class ActionResolver<
|
||||
);
|
||||
|
||||
// 3. Betrayal Logic
|
||||
// If currentNation exists (and > 0), handle betrayal return logic.
|
||||
const defaultGold = 1000; // From env? context.env.defaultNpcGold? Or GameConst?
|
||||
// Using context.env values if available. SystemEnv has `baseGold`?
|
||||
// Legacy: GameConst::$defaultGold (usually 1000/2000).
|
||||
const safeGold = context.env.baseGold || 1000;
|
||||
const safeRice = context.env.baseRice || 1000;
|
||||
const safeGold = this.env.baseGold || 1000;
|
||||
const safeRice = this.env.baseRice || 1000;
|
||||
|
||||
let newGold = general.gold;
|
||||
let newRice = general.rice;
|
||||
@@ -147,10 +129,6 @@ export class ActionResolver<
|
||||
// Penalty
|
||||
// 10% * betray count deduction
|
||||
const penaltyFactor = 1 - 0.1 * betrayCount;
|
||||
if (penaltyFactor < 0) {
|
||||
// Should not be less than 0? capped at ?
|
||||
// Legacy: (1 - 0.1 * betray).
|
||||
}
|
||||
// Apply penalty
|
||||
newExp = Math.floor(newExp * Math.max(0, penaltyFactor));
|
||||
newDed = Math.floor(newDed * Math.max(0, penaltyFactor));
|
||||
@@ -162,9 +140,8 @@ export class ActionResolver<
|
||||
}
|
||||
|
||||
// 4. Update General (Self)
|
||||
let targetCityId = destGeneral.cityId; // Join recruiter
|
||||
// If recruiter is not valid city?
|
||||
if (!targetCityId) targetCityId = destNation.capitalCityId!;
|
||||
const targetCityId = destNation.capitalCityId;
|
||||
if (!targetCityId) throw new Error('Capital city not found.');
|
||||
|
||||
effects.push(
|
||||
createGeneralPatchEffect(
|
||||
@@ -176,30 +153,17 @@ export class ActionResolver<
|
||||
gold: newGold,
|
||||
rice: newRice,
|
||||
officerLevel: 1, // Reset rank
|
||||
// officer_city: 0 via meta
|
||||
crew: general.crew, // Keep crew? Legacy implies checking troop leader.
|
||||
// If troop leader, disband troop.
|
||||
// TS entity `troopId`.
|
||||
troopId: 0, // Quit troop
|
||||
meta: {
|
||||
...general.meta,
|
||||
officer_city: 0,
|
||||
betray: newBetray,
|
||||
// killturn logic?
|
||||
},
|
||||
},
|
||||
general.id
|
||||
)
|
||||
);
|
||||
|
||||
// 5. Update Nations Gen Count (Visual only? or real count)
|
||||
// Legacy updates `gennum`.
|
||||
// We can create patches for nations if `gennum` is part of Nation entity?
|
||||
// Nation entity usually doesn't store computed `gennum` in TS domain?
|
||||
// If it's real column, we can update.
|
||||
// Checking entity: `Nation` interface does NOT have `gennum`.
|
||||
// So we skip updating gennum on Nation entity.
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
@@ -211,26 +175,18 @@ export class ActionDefinition<
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor() {
|
||||
this.resolver = new ActionResolver();
|
||||
constructor(env: TurnCommandEnv) {
|
||||
this.resolver = new ActionResolver(env);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): AcceptScoutArgs | null {
|
||||
// Validate args
|
||||
const args = raw as Partial<AcceptScoutArgs>;
|
||||
if (typeof args.destNationId !== 'number' || typeof args.destGeneralId !== 'number') return null;
|
||||
return { destNationId: args.destNationId, destGeneralId: args.destGeneralId };
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: AcceptScoutArgs): Constraint[] {
|
||||
return [
|
||||
// notBeNeutral(), // Ignored to allow betrayal
|
||||
existsDestNation(),
|
||||
existsDestGeneral(), // Need to check if destGeneral exists
|
||||
notSameDestNation(),
|
||||
destGeneralInDestNation(),
|
||||
notLord(),
|
||||
];
|
||||
return [existsDestNation(), existsDestGeneral(), notSameDestNation(), destGeneralInDestNation(), notLord()];
|
||||
}
|
||||
|
||||
resolve(
|
||||
@@ -242,16 +198,15 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
// Populate destNation and destGeneral
|
||||
const args = base.args as Partial<AcceptScoutArgs>;
|
||||
const args = options.actionArgs as Partial<AcceptScoutArgs>;
|
||||
let destNation: Nation | undefined;
|
||||
let destGeneral: General | undefined;
|
||||
|
||||
if (args.destNationId) {
|
||||
destNation = options.worldRef?.getNation(args.destNationId);
|
||||
destNation = options.worldRef?.getNationById(args.destNationId) ?? undefined;
|
||||
}
|
||||
if (args.destGeneralId) {
|
||||
destGeneral = options.worldRef?.getGeneral(args.destGeneralId);
|
||||
destGeneral = options.worldRef?.getGeneralById(args.destGeneralId) ?? undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -263,14 +218,11 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
|
||||
export const commandSpec: GeneralTurnCommandSpec = {
|
||||
key: 'che_등용수락',
|
||||
category: '계략', // Strategy? or '인사'(Personnel)? Legacy not checked for category. "군사" in task.md?
|
||||
// che_등용수락 is usually separate.
|
||||
// che_등용 is 인사(Personnel).
|
||||
// Let's use '인사'.
|
||||
category: '인사',
|
||||
reqArg: true,
|
||||
args: {
|
||||
destNationId: 'number',
|
||||
destGeneralId: 'number',
|
||||
},
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ export class GeneralTurnCommandLoader {
|
||||
|
||||
constructor(
|
||||
private readonly importers: Record<GeneralTurnCommandKey, GeneralTurnCommandImporter> = defaultImporters
|
||||
) { }
|
||||
) {}
|
||||
|
||||
async load(key: GeneralTurnCommandKey): Promise<GeneralTurnCommandModule> {
|
||||
const cached = this.cache.get(key);
|
||||
|
||||
@@ -114,11 +114,11 @@ export const suppliedDestCity = (): Constraint => ({
|
||||
requires: (ctx) =>
|
||||
resolveDestCityId(ctx) !== undefined
|
||||
? [
|
||||
{
|
||||
kind: 'destCity',
|
||||
id: resolveDestCityId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
{
|
||||
kind: 'destCity',
|
||||
id: resolveDestCityId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
test: (ctx, view) => {
|
||||
const destCity = readDestCity(ctx, view);
|
||||
@@ -250,11 +250,11 @@ export const existsDestCity = (): Constraint => ({
|
||||
requires: (ctx) =>
|
||||
resolveDestCityId(ctx) !== undefined
|
||||
? [
|
||||
{
|
||||
kind: 'destCity',
|
||||
id: resolveDestCityId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
{
|
||||
kind: 'destCity',
|
||||
id: resolveDestCityId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
test: (ctx, view) => {
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
@@ -316,11 +316,11 @@ export const notNeutralDestCity = (): Constraint => ({
|
||||
requires: (ctx) =>
|
||||
resolveDestCityId(ctx) !== undefined
|
||||
? [
|
||||
{
|
||||
kind: 'destCity',
|
||||
id: resolveDestCityId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
{
|
||||
kind: 'destCity',
|
||||
id: resolveDestCityId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
test: (ctx, view) => {
|
||||
const destCity = readDestCity(ctx, view);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { InMemoryWorld, TestGameRunner } from '../../testEnv.js';
|
||||
import { evaluateActionConstraints } from '../../../src/constraints/evaluate.js';
|
||||
import type { TurnCommandEnv } from '../../../src/actions/turn/commandEnv.js';
|
||||
import type { ConstraintContext, RequirementKey, StateView } from '../../../src/constraints/types.js';
|
||||
import { readGeneral } from '../../../src/constraints/helpers.js';
|
||||
|
||||
const MOCK_SCENARIO_BASE = {
|
||||
title: 'Test',
|
||||
@@ -84,15 +83,15 @@ const systemEnv: TurnCommandEnv = {
|
||||
maxResourceActionAmount: 1000,
|
||||
};
|
||||
|
||||
function createConstraintContext(actor: General, year: number = 200, args: any = {}): ConstraintContext {
|
||||
function createConstraintContext(actorId: number, cityId: number, nationId: number, args: any = {}): ConstraintContext {
|
||||
return {
|
||||
actorId: actor.id,
|
||||
cityId: actor.cityId,
|
||||
nationId: actor.nationId || 0,
|
||||
actorId,
|
||||
cityId,
|
||||
nationId,
|
||||
args,
|
||||
env: {
|
||||
...systemEnv,
|
||||
world: { currentYear: year },
|
||||
world: { currentYear: 200 },
|
||||
openingPartYear: systemEnv.openingPartYear,
|
||||
map: MINIMAL_MAP,
|
||||
cities: MINIMAL_MAP.cities,
|
||||
@@ -186,7 +185,12 @@ describe('che_등용수락', () => {
|
||||
experience: 0,
|
||||
dedication: 0,
|
||||
officerLevel: 1,
|
||||
role: null as any,
|
||||
role: {
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
},
|
||||
injury: 0,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
@@ -218,18 +222,17 @@ describe('che_등용수락', () => {
|
||||
world.snapshot.generals.push(recruiterGen);
|
||||
world.snapshot.nations.push(nation2);
|
||||
|
||||
const { commandSpec } = await import('../../../src/actions/turn/general/che_등용수락.js');
|
||||
|
||||
await runner.runTurn([
|
||||
{
|
||||
generalId: neutralGen.id,
|
||||
commandKey: 'che_등용수락',
|
||||
resolver: (
|
||||
await import('../../../src/actions/turn/general/che_등용수락.js')
|
||||
).commandSpec.createDefinition({} as any),
|
||||
resolver: commandSpec.createDefinition(systemEnv),
|
||||
args: { destNationId: 2, destGeneralId: 2 },
|
||||
context: {
|
||||
destNation: nation2,
|
||||
destGeneral: recruiterGen,
|
||||
env: systemEnv,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -238,7 +241,7 @@ describe('che_등용수락', () => {
|
||||
const updatedRecruiter = world.getGeneral(recruiterGen.id);
|
||||
|
||||
expect(updatedSelf?.nationId).toBe(2);
|
||||
expect(updatedSelf?.cityId).toBe(102);
|
||||
expect(updatedSelf?.cityId).toBe(102); // Capital of Nation2
|
||||
expect(updatedSelf?.experience).toBe(100);
|
||||
expect(updatedSelf?.dedication).toBe(100);
|
||||
|
||||
@@ -265,7 +268,12 @@ describe('che_등용수락', () => {
|
||||
experience: 1000,
|
||||
dedication: 1000,
|
||||
officerLevel: 1,
|
||||
role: null as any,
|
||||
role: {
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
},
|
||||
injury: 0,
|
||||
gold: 2000,
|
||||
rice: 2000,
|
||||
@@ -302,7 +310,12 @@ describe('che_등용수락', () => {
|
||||
experience: 0,
|
||||
dedication: 0,
|
||||
officerLevel: 1,
|
||||
role: null as any,
|
||||
role: {
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
},
|
||||
injury: 0,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
@@ -334,18 +347,17 @@ describe('che_등용수락', () => {
|
||||
world.snapshot.nations.push(nation1);
|
||||
world.snapshot.nations.push(nation2);
|
||||
|
||||
const { commandSpec } = await import('../../../src/actions/turn/general/che_등용수락.js');
|
||||
|
||||
await runner.runTurn([
|
||||
{
|
||||
generalId: betrayer.id,
|
||||
commandKey: 'che_등용수락',
|
||||
resolver: (
|
||||
await import('../../../src/actions/turn/general/che_등용수락.js')
|
||||
).commandSpec.createDefinition({} as any),
|
||||
resolver: commandSpec.createDefinition(systemEnv),
|
||||
args: { destNationId: 2, destGeneralId: 2 },
|
||||
context: {
|
||||
destNation: nation2,
|
||||
destGeneral: recruiterGen,
|
||||
env: systemEnv,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -453,22 +465,16 @@ describe('che_등용수락', () => {
|
||||
world.snapshot.nations.push(nation1);
|
||||
world.snapshot.nations.push(nation2);
|
||||
|
||||
// Manually check constraints for denial
|
||||
const def = (await import('../../../src/actions/turn/general/che_등용수락.js')).commandSpec.createDefinition(
|
||||
{} as any
|
||||
);
|
||||
const { commandSpec } = await import('../../../src/actions/turn/general/che_등용수락.js');
|
||||
const def = commandSpec.createDefinition(systemEnv);
|
||||
const args = { destNationId: 2, destGeneralId: 2 };
|
||||
|
||||
const ctx = createConstraintContext(monarch, 200, args);
|
||||
const ctx = createConstraintContext(monarch.id, monarch.cityId, monarch.nationId!, args);
|
||||
const view = createViewState(world, 200);
|
||||
|
||||
const result = evaluateActionConstraints(def, ctx, view, args);
|
||||
expect(result.kind).toBe('deny');
|
||||
if (result.kind === 'deny') {
|
||||
const reason = result.constraintName || result.reason;
|
||||
// notLord constraint failure.
|
||||
// In TS presets, notLord(monarch) returns deny.
|
||||
// ConstraintName should be 'notLord' or 'NotLord'.
|
||||
expect(result.constraintName).toMatch(/NotLord/i);
|
||||
}
|
||||
});
|
||||
@@ -490,7 +496,12 @@ describe('che_등용수락', () => {
|
||||
experience: 0,
|
||||
dedication: 0,
|
||||
officerLevel: 1,
|
||||
role: null as any,
|
||||
role: {
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
},
|
||||
injury: 0,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
@@ -516,8 +527,6 @@ describe('che_등용수락', () => {
|
||||
typeCode: 'che_def',
|
||||
meta: {},
|
||||
};
|
||||
// Recruiter also in same nation? Or different?
|
||||
// Arg destNationId is key.
|
||||
const recruiterGen: General = {
|
||||
id: 2,
|
||||
name: 'Recruiter',
|
||||
@@ -546,12 +555,11 @@ describe('che_등용수락', () => {
|
||||
world.snapshot.generals.push(recruiterGen);
|
||||
world.snapshot.nations.push(nation1);
|
||||
|
||||
const def = (await import('../../../src/actions/turn/general/che_등용수락.js')).commandSpec.createDefinition(
|
||||
{} as any
|
||||
);
|
||||
const args = { destNationId: 1, destGeneralId: 2 }; // Target same nation 1
|
||||
const { commandSpec } = await import('../../../src/actions/turn/general/che_등용수락.js');
|
||||
const def = commandSpec.createDefinition(systemEnv);
|
||||
const args = { destNationId: 1, destGeneralId: 2 };
|
||||
|
||||
const ctx = createConstraintContext(general, 200, args);
|
||||
const ctx = createConstraintContext(general.id, general.cityId, general.nationId!, args);
|
||||
const view = createViewState(world, 200);
|
||||
|
||||
const result = evaluateActionConstraints(def, ctx, view, args);
|
||||
|
||||
Reference in New Issue
Block a user