feat: 국가 멸망 처리 로직 추가 및 관련 테스트 케이스 작성
This commit is contained in:
@@ -241,6 +241,7 @@ export const createDatabaseTurnHooks = async (
|
||||
troops,
|
||||
deletedTroops,
|
||||
deletedGenerals,
|
||||
deletedNations,
|
||||
diplomacy,
|
||||
logs,
|
||||
createdGenerals,
|
||||
@@ -309,6 +310,23 @@ export const createDatabaseTurnHooks = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (deletedNations.length > 0) {
|
||||
await prisma.diplomacy.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ srcNationId: { in: deletedNations } },
|
||||
{ destNationId: { in: deletedNations } },
|
||||
],
|
||||
},
|
||||
});
|
||||
await prisma.nationTurn.deleteMany({
|
||||
where: { nationId: { in: deletedNations } },
|
||||
});
|
||||
await prisma.nation.deleteMany({
|
||||
where: { id: { in: deletedNations } },
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...generals
|
||||
.filter((general) => !createdIds.has(general.id))
|
||||
|
||||
@@ -183,6 +183,7 @@ export class InMemoryTurnWorld {
|
||||
private readonly createdDiplomacyKeys = new Set<string>();
|
||||
private readonly deletedTroopIds = new Set<number>();
|
||||
private readonly deletedGeneralIds = new Set<number>();
|
||||
private readonly deletedNationIds = new Set<number>();
|
||||
private readonly logs: LogEntryDraft[] = [];
|
||||
private readonly scenarioConfig: ScenarioConfig;
|
||||
private checkpoint?: TurnCheckpoint;
|
||||
@@ -376,6 +377,24 @@ export class InMemoryTurnWorld {
|
||||
return true;
|
||||
}
|
||||
|
||||
removeNation(id: number): boolean {
|
||||
if (!this.nations.has(id)) {
|
||||
return false;
|
||||
}
|
||||
this.nations.delete(id);
|
||||
this.dirtyNationIds.delete(id);
|
||||
this.createdNationIds.delete(id);
|
||||
this.deletedNationIds.add(id);
|
||||
for (const [key, entry] of this.diplomacy) {
|
||||
if (entry.fromNationId === id || entry.toNationId === id) {
|
||||
this.diplomacy.delete(key);
|
||||
this.dirtyDiplomacyKeys.delete(key);
|
||||
this.createdDiplomacyKeys.delete(key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
applyDiplomacyPatch(input: { srcNationId: number; destNationId: number; patch: DiplomacyPatch }): void {
|
||||
const key = buildDiplomacyKey(input.srcNationId, input.destNationId);
|
||||
const existed = this.diplomacy.has(key);
|
||||
@@ -586,6 +605,8 @@ export class InMemoryTurnWorld {
|
||||
}
|
||||
}
|
||||
|
||||
this.removeCollapsedNations();
|
||||
|
||||
return nextTurnAt;
|
||||
}
|
||||
|
||||
@@ -632,6 +653,7 @@ export class InMemoryTurnWorld {
|
||||
troops: Troop[];
|
||||
deletedTroops: number[];
|
||||
deletedGenerals: number[];
|
||||
deletedNations: number[];
|
||||
diplomacy: TurnDiplomacy[];
|
||||
logs: LogEntryDraft[];
|
||||
createdGenerals: TurnGeneral[];
|
||||
@@ -668,6 +690,7 @@ export class InMemoryTurnWorld {
|
||||
.filter((entry): entry is TurnDiplomacy => Boolean(entry));
|
||||
const deletedTroops = Array.from(this.deletedTroopIds);
|
||||
const deletedGenerals = Array.from(this.deletedGeneralIds);
|
||||
const deletedNations = Array.from(this.deletedNationIds);
|
||||
const logs = this.logs.splice(0, this.logs.length);
|
||||
|
||||
this.dirtyGeneralIds.clear();
|
||||
@@ -681,6 +704,7 @@ export class InMemoryTurnWorld {
|
||||
this.createdDiplomacyKeys.clear();
|
||||
this.deletedTroopIds.clear();
|
||||
this.deletedGeneralIds.clear();
|
||||
this.deletedNationIds.clear();
|
||||
|
||||
return {
|
||||
generals,
|
||||
@@ -689,6 +713,7 @@ export class InMemoryTurnWorld {
|
||||
troops,
|
||||
deletedTroops,
|
||||
deletedGenerals,
|
||||
deletedNations,
|
||||
diplomacy,
|
||||
logs,
|
||||
createdGenerals,
|
||||
@@ -698,6 +723,45 @@ export class InMemoryTurnWorld {
|
||||
};
|
||||
}
|
||||
|
||||
private removeCollapsedNations(): void {
|
||||
const collapsedNationIds: number[] = [];
|
||||
for (const nation of this.nations.values()) {
|
||||
if (nation.id <= 0) {
|
||||
continue;
|
||||
}
|
||||
const meta = nation.meta as Record<string, unknown>;
|
||||
if (meta.collapsed !== true) {
|
||||
continue;
|
||||
}
|
||||
const cityCount = Array.from(this.cities.values()).filter((city) => city.nationId === nation.id).length;
|
||||
if (cityCount > 0) {
|
||||
continue;
|
||||
}
|
||||
collapsedNationIds.push(nation.id);
|
||||
}
|
||||
|
||||
for (const nationId of collapsedNationIds) {
|
||||
for (const general of this.generals.values()) {
|
||||
if (general.nationId !== nationId) {
|
||||
continue;
|
||||
}
|
||||
const updated = applyGeneralPatch(general, {
|
||||
nationId: 0,
|
||||
officerLevel: 0,
|
||||
troopId: 0,
|
||||
});
|
||||
this.generals.set(general.id, normalizeGeneralTurnTime(updated, this.state.lastTurnTime));
|
||||
this.dirtyGeneralIds.add(general.id);
|
||||
}
|
||||
for (const troop of Array.from(this.troops.values())) {
|
||||
if (troop.nationId === nationId) {
|
||||
this.removeTroop(troop.id);
|
||||
}
|
||||
}
|
||||
this.removeNation(nationId);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureDiplomacyMatrix(): void {
|
||||
const nationIds = Array.from(this.nations.keys());
|
||||
for (const srcNationId of nationIds) {
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TurnSchedule, UnitSetDefinition } from '@sammo-ts/logic';
|
||||
import { DIPLOMACY_STATE } from '@sammo-ts/logic';
|
||||
import type { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
|
||||
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
||||
|
||||
const mockDate = new Date('0185-01-01T00:00:00Z');
|
||||
|
||||
const createNpcGeneral = (
|
||||
id: number,
|
||||
cityId: number,
|
||||
nationId: number,
|
||||
officerLevel: number,
|
||||
stats: { leadership: number; strength: number; intelligence: number },
|
||||
overrides: Partial<TurnGeneral> = {}
|
||||
): TurnGeneral => ({
|
||||
id,
|
||||
name: `NPC_${id}`,
|
||||
nationId,
|
||||
cityId,
|
||||
troopId: 0,
|
||||
stats,
|
||||
turnTime: mockDate,
|
||||
role: {
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
},
|
||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||
meta: {},
|
||||
officerLevel,
|
||||
experience: 0,
|
||||
dedication: 0,
|
||||
injury: 0,
|
||||
gold: 0,
|
||||
rice: 0,
|
||||
crew: 0,
|
||||
crewTypeId: 0,
|
||||
train: 0,
|
||||
atmos: 0,
|
||||
age: 30,
|
||||
npcState: 2,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const maxCityStats = (city: ReturnType<typeof buildLargeTestCities>[number]) => ({
|
||||
...city,
|
||||
population: city.populationMax,
|
||||
agriculture: city.agricultureMax,
|
||||
commerce: city.commerceMax,
|
||||
security: city.securityMax,
|
||||
defence: city.defenceMax,
|
||||
wall: city.wallMax,
|
||||
meta: { ...(city.meta ?? {}), trust: 100, trade: 100 },
|
||||
});
|
||||
|
||||
const weakCityStats = (city: ReturnType<typeof buildLargeTestCities>[number]) => ({
|
||||
...city,
|
||||
population: 1000,
|
||||
agriculture: 1,
|
||||
commerce: 1,
|
||||
security: 1,
|
||||
defence: 1,
|
||||
wall: 1,
|
||||
supplyState: 1,
|
||||
frontState: 1,
|
||||
meta: { ...(city.meta ?? {}), trust: 5, trade: 0 },
|
||||
});
|
||||
|
||||
describe('도시 점령 시 국가 멸망 처리', () => {
|
||||
it('마지막 도시를 점령하면 약소국이 삭제되어야 한다', async () => {
|
||||
const cities = buildLargeTestCities().map(maxCityStats);
|
||||
const weakCityId = 1;
|
||||
const strongFrontCityId = 2;
|
||||
|
||||
for (const city of cities) {
|
||||
city.nationId = 1;
|
||||
}
|
||||
const weakCity = cities.find((city) => city.id === weakCityId)!;
|
||||
weakCity.nationId = 2;
|
||||
Object.assign(weakCity, weakCityStats(weakCity));
|
||||
|
||||
const strongFrontCity = cities.find((city) => city.id === strongFrontCityId)!;
|
||||
strongFrontCity.frontState = 1;
|
||||
strongFrontCity.supplyState = 1;
|
||||
|
||||
const unitSet: UnitSetDefinition = {
|
||||
id: 'test_unit_set',
|
||||
name: 'TestUnitSet',
|
||||
defaultCrewTypeId: 1100,
|
||||
crewTypes: [
|
||||
{
|
||||
id: 1100,
|
||||
armType: 1,
|
||||
name: '보병',
|
||||
attack: 150,
|
||||
defence: 150,
|
||||
speed: 7,
|
||||
avoid: 10,
|
||||
magicCoef: 0,
|
||||
cost: 10,
|
||||
rice: 10,
|
||||
requirements: [],
|
||||
attackCoef: {},
|
||||
defenceCoef: {},
|
||||
info: [],
|
||||
initSkillTrigger: null,
|
||||
phaseSkillTrigger: null,
|
||||
iActionList: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const delayedTurnTime = new Date(mockDate.getTime() + 60 * 60 * 1000);
|
||||
|
||||
const strongLeader = createNpcGeneral(
|
||||
1,
|
||||
strongFrontCityId,
|
||||
1,
|
||||
12,
|
||||
{ leadership: 90, strength: 90, intelligence: 70 },
|
||||
{
|
||||
crew: 9000,
|
||||
crewTypeId: 1100,
|
||||
train: 100,
|
||||
atmos: 100,
|
||||
gold: 500000,
|
||||
rice: 500000,
|
||||
}
|
||||
);
|
||||
const generals: TurnGeneral[] = [strongLeader];
|
||||
for (let i = 2; i <= 6; i += 1) {
|
||||
generals.push(
|
||||
createNpcGeneral(i, strongFrontCityId, 1, 2, { leadership: 80, strength: 80, intelligence: 50 }, {
|
||||
crew: 8000,
|
||||
crewTypeId: 1100,
|
||||
train: 100,
|
||||
atmos: 100,
|
||||
gold: 100000,
|
||||
rice: 100000,
|
||||
turnTime: delayedTurnTime,
|
||||
})
|
||||
);
|
||||
}
|
||||
const weakGeneral = createNpcGeneral(
|
||||
100,
|
||||
weakCityId,
|
||||
2,
|
||||
12,
|
||||
{ leadership: 10, strength: 10, intelligence: 10 },
|
||||
{
|
||||
crew: 0,
|
||||
crewTypeId: 1100,
|
||||
train: 0,
|
||||
atmos: 0,
|
||||
gold: 0,
|
||||
rice: 0,
|
||||
turnTime: delayedTurnTime,
|
||||
}
|
||||
);
|
||||
generals.push(weakGeneral);
|
||||
|
||||
const snapshot: TurnWorldSnapshot = {
|
||||
generals: generals as any,
|
||||
cities: cities as any,
|
||||
nations: [
|
||||
{
|
||||
id: 1,
|
||||
name: '강국',
|
||||
color: '#AA0000',
|
||||
capitalCityId: strongFrontCityId,
|
||||
chiefGeneralId: 1,
|
||||
gold: 800000,
|
||||
rice: 800000,
|
||||
power: 10000,
|
||||
level: 1,
|
||||
typeCode: 'npc',
|
||||
meta: { tech: 10 },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '약소국',
|
||||
color: '#0000AA',
|
||||
capitalCityId: weakCityId,
|
||||
chiefGeneralId: 100,
|
||||
gold: 0,
|
||||
rice: 0,
|
||||
power: 0,
|
||||
level: 1,
|
||||
typeCode: 'npc',
|
||||
meta: { tech: 0 },
|
||||
},
|
||||
],
|
||||
troops: [],
|
||||
diplomacy: [
|
||||
{ fromNationId: 1, toNationId: 2, state: DIPLOMACY_STATE.WAR, term: 12, dead: 0, meta: {} },
|
||||
{ fromNationId: 2, toNationId: 1, state: DIPLOMACY_STATE.WAR, term: 12, dead: 0, meta: {} },
|
||||
],
|
||||
events: [],
|
||||
initialEvents: [],
|
||||
map: LARGE_TEST_MAP as any,
|
||||
scenarioConfig: {
|
||||
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||
iconPath: '',
|
||||
map: {},
|
||||
const: {
|
||||
openingPartYear: 3,
|
||||
develCost: 10,
|
||||
baseGold: 1000,
|
||||
baseRice: 1000,
|
||||
maxResourceActionAmount: 10000,
|
||||
minAvailableRecruitPop: 0,
|
||||
},
|
||||
environment: { mapName: 'large_test_map', unitSet: 'default' },
|
||||
},
|
||||
scenarioMeta: { startYear: 180 } as any,
|
||||
unitSet: unitSet as any,
|
||||
};
|
||||
|
||||
const state: TurnWorldState = {
|
||||
id: 1,
|
||||
currentYear: 185,
|
||||
currentMonth: 1,
|
||||
tickSeconds: 600,
|
||||
lastTurnTime: mockDate,
|
||||
meta: { seed: 1, initYear: 185, initMonth: 1 },
|
||||
};
|
||||
|
||||
const schedule: TurnSchedule = {
|
||||
entries: [{ startMinute: 0, tickMinutes: 10 }],
|
||||
};
|
||||
|
||||
const worldRef = { current: null as InMemoryTurnWorld | null };
|
||||
const { world, reservedTurnStore, runOneTick } = await createTurnTestHarness({
|
||||
snapshot,
|
||||
state,
|
||||
schedule,
|
||||
map: LARGE_TEST_MAP,
|
||||
worldRef,
|
||||
});
|
||||
|
||||
const turns = reservedTurnStore.getGeneralTurns(strongLeader.id);
|
||||
turns[0] = {
|
||||
action: 'che_출병',
|
||||
args: { destCityId: weakCityId },
|
||||
};
|
||||
|
||||
await runOneTick();
|
||||
|
||||
expect(world.getCityById(weakCityId)?.nationId).toBe(1);
|
||||
expect(world.getNationById(2)).toBeNull();
|
||||
expect(world.listNations().some((nation) => nation.id === 2)).toBe(false);
|
||||
|
||||
const updatedWeakGeneral = world.getGeneralById(weakGeneral.id);
|
||||
expect(updatedWeakGeneral?.nationId).toBe(0);
|
||||
expect(updatedWeakGeneral?.officerLevel).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user