fix: 장수 런타임 레벨 상한을 Ref 기준으로 복구

가입 능력치 배분 상한과 런타임 레벨 상한을 분리하고, Ref 기본값 255를 공유 상수로 통합한다. 모병 후 레벨 유지와 국가 장수 목록의 고레벨 표시를 회귀 테스트로 고정한다.
This commit is contained in:
2026-08-21 08:37:38 +00:00
parent bba0d3b5c0
commit deb01f7f2b
21 changed files with 124 additions and 49 deletions
+6 -13
View File
@@ -8,7 +8,7 @@ import type {
TurnCommandEnv,
UnitSetDefinition,
} from '@sammo-ts/logic';
import { evaluateConstraints } from '@sammo-ts/logic';
import { evaluateConstraints, LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic';
import type { ConstraintContext } from '@sammo-ts/logic';
import { GAME_TICKS_PER_TURN, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import { simpleSerialize } from '@sammo-ts/logic/war/utils.js';
@@ -872,17 +872,14 @@ export class GeneralAI {
? resolveLegacyAiStatsWithModules(
candidate,
this.nation,
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max,
this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL,
this.commandEnv.generalActionModules,
this.worldRef,
this.world,
this.startYear
).fullLeadership
: resolveLegacyAiStats(
candidate,
this.nation,
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max
).fullLeadership;
: resolveLegacyAiStats(candidate, this.nation, this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL)
.fullLeadership;
if (fullLeadership >= this.nationPolicy.minNpcWarLeadership) {
npcWarGenerals[candidate.id] = candidate;
} else {
@@ -1055,17 +1052,13 @@ export class GeneralAI {
? resolveLegacyAiStatsWithModules(
this.general,
this.nation,
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max,
this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL,
this.commandEnv.generalActionModules,
this.worldRef,
this.world,
this.startYear
)
: resolveLegacyAiStats(
this.general,
this.nation,
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max
);
: resolveLegacyAiStats(this.general, this.nation, this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL);
this.general.meta = {
...this.general.meta,
...stats,
@@ -1,5 +1,6 @@
import type { GeneralAI } from '../core.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
import type { TurnGeneral } from '../../../types.js';
import { asRecord, readMetaNumber, readRequiredMetaNumber } from '../../aiUtils.js';
@@ -72,12 +73,12 @@ const getFullLeadership = (ai: GeneralAI, general: TurnGeneral): number => {
'leadership',
general.stats.leadership
);
const maxStat = ai.commandEnv.maxStatLevel ?? ai.scenarioConfig.stat.max;
const maxStat = ai.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
return Math.trunc(Math.max(0, Math.min(Number(adjusted), maxStat)));
}
const nationLevel = ai.nation?.level ?? 0;
const officerBonus = general.officerLevel === 12 ? nationLevel * 2 : general.officerLevel >= 5 ? nationLevel : 0;
const maxStat = ai.commandEnv.maxStatLevel ?? ai.scenarioConfig.stat.max;
const maxStat = ai.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
return Math.max(0, Math.min(general.stats.leadership + officerBonus, maxStat));
};
@@ -10,6 +10,7 @@ import type {
import {
LEGACY_RANDOM_GENERAL_FIRST_NAMES,
LEGACY_RANDOM_GENERAL_LAST_NAMES,
LEGACY_DEFAULT_MAX_LEVEL,
loadGeneralTurnCommandSpecs,
loadNationTurnCommandSpecs,
loadActionModuleBundle,
@@ -132,7 +133,9 @@ export const buildCommandEnv = (config: ScenarioConfig, unitSet?: UnitSetDefinit
]),
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], DEFAULT_INITIAL_NATION_GEN_LIMIT),
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], DEFAULT_MAX_TECH_LEVEL),
maxStatLevel: resolveNumber(constValues, ['maxLevel'], config.stat.max),
// `stat.max` bounds join-time allocation (80 by default), while Ref
// runtime stat calculations use GameConst::$maxLevel.
maxStatLevel: resolveNumber(constValues, ['maxLevel'], LEGACY_DEFAULT_MAX_LEVEL),
maxDedicationLevel: resolveNumber(constValues, ['maxDedLevel'], 30),
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], 5),
@@ -34,6 +34,7 @@ import {
rollUniqueLottery,
getNextTurnAt,
getBillByLevel,
LEGACY_DEFAULT_MAX_LEVEL,
type ItemModule,
type UniqueLotteryRunner,
} from '@sammo-ts/logic';
@@ -126,7 +127,7 @@ export const applyLegacyGeneralProgression = (
env: TurnCommandEnv,
logs: LogEntryDraft[]
): TurnGeneral => {
const maxStatLevel = env.maxStatLevel ?? 255;
const maxStatLevel = env.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
const maxDedicationLevel = env.maxDedicationLevel ?? 30;
const expLevel = Math.max(
0,
@@ -4,6 +4,7 @@ import type { TurnSchedule } from '@sammo-ts/logic/turn/calendar.js';
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
import { applyLegacyGeneralProgression } from '../src/turn/reservedTurnHandler.js';
import { buildCommandEnv } from '../src/turn/reservedTurnCommands.js';
const start = new Date('0200-01-01T00:00:00.000Z');
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 10 }] };
@@ -154,6 +155,21 @@ const makeState = (): TurnWorldState => ({
});
describe('legacy general-turn execution contract', () => {
it('does not reuse the join stat allocation maximum as the runtime level cap', () => {
const previous = makeGeneral({
experience: 144_000,
meta: { killturn: 24, explevel: 120 },
});
const afterRecruitment = makeGeneral({
experience: 144_001,
meta: { killturn: 24, explevel: 120 },
});
const env = buildCommandEnv(makeSnapshot(previous).scenarioConfig);
expect(env.maxStatLevel).toBe(255);
expect(applyLegacyGeneralProgression(afterRecruitment, previous, 'che_모병', env, []).meta.explevel).toBe(120);
});
it('preserves the battle-computed level across legacy INT rounding', () => {
const previous = makeGeneral({
experience: 6_700,
@@ -92,4 +92,17 @@ describe('tracked scenario resources', () => {
expect([...secretScenarioKeys!].filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(20);
expect(secretScenarioKeys?.has('event_전투특기_격노')).toBe(true);
});
it('keeps join allocation bounds separate from the Ref runtime stat level limit', async () => {
const scenario = await loadScenarioDefinitionById(1);
expect(scenario.config.stat.max).toBe(80);
expect(buildCommandEnv(scenario.config).maxStatLevel).toBe(255);
expect(
buildCommandEnv({
...scenario.config,
const: { ...scenario.config.const, maxLevel: 512 },
}).maxStatLevel
).toBe(512);
});
});