fix: 후계자 없는 군주 사망 시 국가 멸망을 함께 처리

This commit is contained in:
2026-09-06 12:09:09 +00:00
parent d3443ec7f5
commit 4d64978cb6
5 changed files with 335 additions and 1 deletions
+66 -1
View File
@@ -9,12 +9,13 @@ import type {
TurnSchedule,
UnitSetDefinition,
} from '@sammo-ts/logic';
import { getNextTurnAt, readScenarioGeneralPoolClaim } from '@sammo-ts/logic';
import { getNextTurnAt, readScenarioGeneralPoolClaim, LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
import {
GAME_TICKS_PER_TURN,
GameClock,
assertGameplayCommitAllowed,
inferClockPhase,
JosaUtil,
type GameClockMode,
type GameClockPhase,
type TurnRecoveryWindow,
@@ -78,6 +79,7 @@ export interface GeneralTurnResult {
troopIds?: number[];
};
destroyedNationIds?: number[];
successorlessNationId?: number;
lifecycleEvent?: GeneralLifecycleEvent;
}
@@ -2028,6 +2030,11 @@ export class InMemoryTurnWorld {
this.removeTroop(troopId);
}
}
if (result.successorlessNationId !== undefined) {
// 사망 군주도 삭제 전 archive의 장수 목록과 멸망 로그에 포함한다.
this.generals.set(currentGeneral.id, result.general ?? currentGeneral);
this.dissolveNationWithoutSuccessor(result.successorlessNationId, currentGeneral.id);
}
if (result.deleted?.general) {
this.removeGeneral(currentGeneral.id);
}
@@ -2227,6 +2234,64 @@ export class InMemoryTurnWorld {
return changes;
}
dissolveNationWithoutSuccessor(nationId: number, dyingLordId?: number): boolean {
const nation = this.nations.get(nationId);
if (!nation) {
return false;
}
const members = this.listGenerals().filter((general) => general.nationId === nationId);
const dyingLord = members.find((general) => general.id === dyingLordId);
if (
(dyingLordId !== undefined && dyingLord?.officerLevel !== 12) ||
members.some(
(general) => general.id !== dyingLordId && (general.npcState !== 5 || general.officerLevel === 12)
)
) {
throw new Error(`Nation ${nationId} still has a ruler or successor.`);
}
// Ref nextRuler() -> deleteNation(true): 부대장(npc=5)은 후계자가
// 될 수 없다. 자원 약탈·포상·난수 소비 없이 도시를 공백지로 돌린다.
for (const city of this.listCities()) {
if (city.nationId === nationId) {
this.updateCity(city.id, { nationId: 0, frontState: 0 });
}
}
const orderedMembers = members.sort((left, right) => {
if (left.id === dyingLordId) return 1;
if (right.id === dyingLordId) return -1;
return left.id - right.id;
});
const pushHistory = (): void => {
this.pushLog({
scope: LogScope.SYSTEM,
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
text: `<R><b>【멸망】</b></><D><b>${nation.name}</b></>${JosaUtil.pick(nation.name, '은')} <R>멸망</>했습니다.`,
});
};
for (const general of orderedMembers) {
if (general.id === dyingLordId) pushHistory();
// Ref applyDB()는 개인 역사 bucket을 행동 bucket보다 먼저 저장한다.
this.pushLog({
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
generalId: general.id,
format: LogFormat.YEAR_MONTH,
text: `<D><b>${nation.name}</b></>${JosaUtil.pick(nation.name, '이')} <R>멸망</>`,
});
this.pushLog({
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: general.id,
format: LogFormat.PLAIN,
text: `<D><b>${nation.name}</b></>${JosaUtil.pick(nation.name, '이')} <R>멸망</>했습니다.`,
});
}
// 과거 누락으로 군주가 이미 삭제된 국가의 운영 복구도 같은 정산을 쓴다.
if (dyingLordId === undefined) pushHistory();
return this.collapseNation(nationId);
}
collapseNation(nationId: number): boolean {
const nation = this.nations.get(nationId);
if (!nation) {
@@ -2252,6 +2252,7 @@ export const createReservedTurnHandler = async (options: {
? 'retired'
: 'active';
let deleteGeneral = false;
let successorlessNationId: number | undefined;
const deletedTroopIds = Array.from(commandDeletedTroopIds);
const lifecycleSnapshot = cloneTurnGeneral(currentGeneral);
if (currentGeneral.meta.killturn <= 0) {
@@ -2351,6 +2352,10 @@ export const createReservedTurnHandler = async (options: {
`<Y>${successor.name}</>이 <D><b>${currentNation.name}</b></>의 유지를 이어 받았습니다`
)
);
} else {
// Ref nextRuler()는 후계자가 없으면 군주 삭제 전에
// deleteNation($general, true)로 국가 전체를 정산한다.
successorlessNationId = currentNation.id;
}
}
if (currentGeneral.troopId === currentGeneral.id) {
@@ -2424,6 +2429,7 @@ export const createReservedTurnHandler = async (options: {
}
: undefined),
...(destroyedNationIds.size > 0 ? { destroyedNationIds: [...destroyedNationIds] } : undefined),
...(successorlessNationId !== undefined ? { successorlessNationId } : {}),
lifecycleEvent: {
generalId: currentGeneral.id,
outcome: lifecycleOutcome,