fix inheritance accounting for unowned generals
This commit is contained in:
@@ -1123,7 +1123,9 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
kind === 'nation' &&
|
kind === 'nation' &&
|
||||||
!usedFallback &&
|
!usedFallback &&
|
||||||
resolution.completed &&
|
resolution.completed &&
|
||||||
definition.countsAsInheritanceActiveAction
|
definition.countsAsInheritanceActiveAction &&
|
||||||
|
Boolean(currentGeneral.userId) &&
|
||||||
|
currentGeneral.npcState < 2
|
||||||
) {
|
) {
|
||||||
const meta = { ...currentGeneral.meta };
|
const meta = { ...currentGeneral.meta };
|
||||||
const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0;
|
const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0;
|
||||||
@@ -1135,7 +1137,9 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
kind === 'general' &&
|
kind === 'general' &&
|
||||||
!usedFallback &&
|
!usedFallback &&
|
||||||
resolution.completed &&
|
resolution.completed &&
|
||||||
executionDefinition.getInheritanceActiveActionAmount
|
executionDefinition.getInheritanceActiveActionAmount &&
|
||||||
|
Boolean(currentGeneral.userId) &&
|
||||||
|
currentGeneral.npcState < 2
|
||||||
) {
|
) {
|
||||||
const amount = executionDefinition.getInheritanceActiveActionAmount(actionContext, actionArgs);
|
const amount = executionDefinition.getInheritanceActiveActionAmount(actionContext, actionArgs);
|
||||||
if (Number.isFinite(amount) && amount !== 0) {
|
if (Number.isFinite(amount) && amount !== 0) {
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
|||||||
|
|
||||||
const mockDate = new Date('0189-01-01T00:00:00Z');
|
const mockDate = new Date('0189-01-01T00:00:00Z');
|
||||||
|
|
||||||
const createChief = (): TurnGeneral => ({
|
const createChief = (userId: string | null): TurnGeneral => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
|
userId,
|
||||||
name: '군주',
|
name: '군주',
|
||||||
nationId: 1,
|
nationId: 1,
|
||||||
cityId: 1,
|
cityId: 1,
|
||||||
@@ -38,131 +39,137 @@ const createChief = (): TurnGeneral => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('레거시 사령부 턴 실행 호환성', () => {
|
describe('레거시 사령부 턴 실행 호환성', () => {
|
||||||
it('화시병 연구는 선행 11턴을 누적한 뒤 12번째 턴에만 완료한다', async () => {
|
it.each([
|
||||||
const cities = buildLargeTestCities();
|
{ ownerLabel: '소유 장수', userId: 'test-user', expectedActiveAction: 1 },
|
||||||
for (const city of cities) {
|
{ ownerLabel: '무소유 장수', userId: null, expectedActiveAction: undefined },
|
||||||
city.nationId = city.id === 1 ? 1 : city.id === 2 ? 2 : 0;
|
])(
|
||||||
}
|
'화시병 연구는 $ownerLabel에서 12번째 턴에 완료하고 소유자에게만 능동 행동을 준다',
|
||||||
const snapshot: TurnWorldSnapshot = {
|
async ({ userId, expectedActiveAction }) => {
|
||||||
generals: [createChief()],
|
const cities = buildLargeTestCities();
|
||||||
cities,
|
for (const city of cities) {
|
||||||
nations: [
|
city.nationId = city.id === 1 ? 1 : city.id === 2 ? 2 : 0;
|
||||||
{
|
}
|
||||||
id: 1,
|
const snapshot: TurnWorldSnapshot = {
|
||||||
name: '테스트국',
|
generals: [createChief(userId)],
|
||||||
color: '#aa0000',
|
cities,
|
||||||
capitalCityId: 1,
|
nations: [
|
||||||
chiefGeneralId: 1,
|
{
|
||||||
gold: 1_000_000,
|
id: 1,
|
||||||
rice: 1_000_000,
|
name: '테스트국',
|
||||||
power: 0,
|
color: '#aa0000',
|
||||||
level: 1,
|
capitalCityId: 1,
|
||||||
typeCode: 'large_test_map_def',
|
chiefGeneralId: 1,
|
||||||
meta: { can_화시병사용: 0 },
|
gold: 1_000_000,
|
||||||
|
rice: 1_000_000,
|
||||||
|
power: 0,
|
||||||
|
level: 1,
|
||||||
|
typeCode: 'large_test_map_def',
|
||||||
|
meta: { can_화시병사용: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '상대국',
|
||||||
|
color: '#0000aa',
|
||||||
|
capitalCityId: 2,
|
||||||
|
chiefGeneralId: null,
|
||||||
|
gold: 1_000_000,
|
||||||
|
rice: 1_000_000,
|
||||||
|
power: 0,
|
||||||
|
level: 1,
|
||||||
|
typeCode: 'large_test_map_def',
|
||||||
|
meta: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
troops: [],
|
||||||
|
diplomacy: [
|
||||||
|
{ fromNationId: 1, toNationId: 2, state: 0, term: 1200, dead: 0, meta: {} },
|
||||||
|
{ fromNationId: 2, toNationId: 1, state: 0, term: 1200, dead: 0, meta: {} },
|
||||||
|
],
|
||||||
|
events: [],
|
||||||
|
initialEvents: [],
|
||||||
|
map: LARGE_TEST_MAP,
|
||||||
|
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,
|
||||||
|
maxTechLevel: 12000,
|
||||||
|
},
|
||||||
|
environment: { mapName: 'large_test_map', unitSet: 'default' },
|
||||||
},
|
},
|
||||||
{
|
scenarioMeta: { startYear: 189 } as never,
|
||||||
id: 2,
|
unitSet: {} as never,
|
||||||
name: '상대국',
|
};
|
||||||
color: '#0000aa',
|
const state: TurnWorldState = {
|
||||||
capitalCityId: 2,
|
id: 1,
|
||||||
chiefGeneralId: null,
|
currentYear: 189,
|
||||||
gold: 1_000_000,
|
currentMonth: 1,
|
||||||
rice: 1_000_000,
|
tickSeconds: 600,
|
||||||
power: 0,
|
lastTurnTime: mockDate,
|
||||||
level: 1,
|
meta: { seed: 1 },
|
||||||
typeCode: 'large_test_map_def',
|
};
|
||||||
meta: {},
|
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 10 }] };
|
||||||
|
const resolvedActions: Array<{ requestedAction: string; actionKey: string; blockedReason?: string }> = [];
|
||||||
|
const { world, reservedTurnStore, runOneTick } = await createTurnTestHarness({
|
||||||
|
snapshot,
|
||||||
|
state,
|
||||||
|
schedule,
|
||||||
|
map: LARGE_TEST_MAP,
|
||||||
|
reservedTurnStoreOptions: { maxGeneralTurns: 12, maxNationTurns: 12 },
|
||||||
|
onActionResolved: (event) => {
|
||||||
|
if (event.kind === 'nation') {
|
||||||
|
resolvedActions.push(event);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
});
|
||||||
troops: [],
|
const turns = reservedTurnStore.getNationTurns(1, 12);
|
||||||
diplomacy: [
|
turns.fill({ action: 'event_화시병연구', args: {} });
|
||||||
{ fromNationId: 1, toNationId: 2, state: 0, term: 1200, dead: 0, meta: {} },
|
|
||||||
{ fromNationId: 2, toNationId: 1, state: 0, term: 1200, dead: 0, meta: {} },
|
for (let index = 1; index <= 11; index += 1) {
|
||||||
],
|
await runOneTick();
|
||||||
events: [],
|
const nation = world.getNationById(1)!;
|
||||||
initialEvents: [],
|
expect(nation.meta.can_화시병사용 ?? 0).toBe(0);
|
||||||
map: LARGE_TEST_MAP,
|
expect(nation.meta.turn_last_12).toMatchObject({
|
||||||
scenarioConfig: {
|
command: '화시병 연구',
|
||||||
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
term: index,
|
||||||
iconPath: '',
|
});
|
||||||
map: {},
|
}
|
||||||
const: {
|
|
||||||
openingPartYear: 3,
|
|
||||||
develCost: 10,
|
|
||||||
baseGold: 1000,
|
|
||||||
baseRice: 1000,
|
|
||||||
maxResourceActionAmount: 10000,
|
|
||||||
maxTechLevel: 12000,
|
|
||||||
},
|
|
||||||
environment: { mapName: 'large_test_map', unitSet: 'default' },
|
|
||||||
},
|
|
||||||
scenarioMeta: { startYear: 189 } as never,
|
|
||||||
unitSet: {} as never,
|
|
||||||
};
|
|
||||||
const state: TurnWorldState = {
|
|
||||||
id: 1,
|
|
||||||
currentYear: 189,
|
|
||||||
currentMonth: 1,
|
|
||||||
tickSeconds: 600,
|
|
||||||
lastTurnTime: mockDate,
|
|
||||||
meta: { seed: 1 },
|
|
||||||
};
|
|
||||||
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 10 }] };
|
|
||||||
const resolvedActions: Array<{ requestedAction: string; actionKey: string; blockedReason?: string }> = [];
|
|
||||||
const { world, reservedTurnStore, runOneTick } = await createTurnTestHarness({
|
|
||||||
snapshot,
|
|
||||||
state,
|
|
||||||
schedule,
|
|
||||||
map: LARGE_TEST_MAP,
|
|
||||||
reservedTurnStoreOptions: { maxGeneralTurns: 12, maxNationTurns: 12 },
|
|
||||||
onActionResolved: (event) => {
|
|
||||||
if (event.kind === 'nation') {
|
|
||||||
resolvedActions.push(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const turns = reservedTurnStore.getNationTurns(1, 12);
|
|
||||||
turns.fill({ action: 'event_화시병연구', args: {} });
|
|
||||||
|
|
||||||
for (let index = 1; index <= 11; index += 1) {
|
|
||||||
await runOneTick();
|
await runOneTick();
|
||||||
const nation = world.getNationById(1)!;
|
const nation = world.getNationById(1)!;
|
||||||
expect(nation.meta.can_화시병사용 ?? 0).toBe(0);
|
expect(nation.meta.can_화시병사용).toBe(1);
|
||||||
expect(nation.meta.turn_last_12).toMatchObject({
|
expect(nation.meta.turn_last_12).toMatchObject({
|
||||||
command: '화시병 연구',
|
command: '화시병 연구',
|
||||||
term: index,
|
term: 0,
|
||||||
});
|
});
|
||||||
|
expect(world.getGeneralById(1)!.meta.inherit_active_action).toBe(expectedActiveAction);
|
||||||
|
expect(world.getGeneralById(1)!.experience).toBe(60);
|
||||||
|
expect(world.getGeneralById(1)!.dedication).toBe(60);
|
||||||
|
|
||||||
|
reservedTurnStore.getNationTurns(1, 12)[0] = {
|
||||||
|
action: 'che_종전제의',
|
||||||
|
args: { destNationId: 2 },
|
||||||
|
};
|
||||||
|
await runOneTick();
|
||||||
|
expect(resolvedActions.at(-1)?.blockedReason).toBeUndefined();
|
||||||
|
expect(resolvedActions.at(-1)).toMatchObject({
|
||||||
|
requestedAction: 'che_종전제의',
|
||||||
|
actionKey: 'che_종전제의',
|
||||||
|
});
|
||||||
|
expect(world.peekDirtyState().messages).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
msgType: 'diplomacy',
|
||||||
|
dest: expect.objectContaining({ nationId: 2 }),
|
||||||
|
option: { action: 'stopWar', deletable: false },
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
await runOneTick();
|
|
||||||
const nation = world.getNationById(1)!;
|
|
||||||
expect(nation.meta.can_화시병사용).toBe(1);
|
|
||||||
expect(nation.meta.turn_last_12).toMatchObject({
|
|
||||||
command: '화시병 연구',
|
|
||||||
term: 0,
|
|
||||||
});
|
|
||||||
expect(world.getGeneralById(1)!.meta.inherit_active_action).toBe(1);
|
|
||||||
expect(world.getGeneralById(1)!.experience).toBe(60);
|
|
||||||
expect(world.getGeneralById(1)!.dedication).toBe(60);
|
|
||||||
|
|
||||||
reservedTurnStore.getNationTurns(1, 12)[0] = {
|
|
||||||
action: 'che_종전제의',
|
|
||||||
args: { destNationId: 2 },
|
|
||||||
};
|
|
||||||
await runOneTick();
|
|
||||||
expect(resolvedActions.at(-1)?.blockedReason).toBeUndefined();
|
|
||||||
expect(resolvedActions.at(-1)).toMatchObject({
|
|
||||||
requestedAction: 'che_종전제의',
|
|
||||||
actionKey: 'che_종전제의',
|
|
||||||
});
|
|
||||||
expect(world.peekDirtyState().messages).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
msgType: 'diplomacy',
|
|
||||||
dest: expect.objectContaining({ nationId: 2 }),
|
|
||||||
option: { action: 'stopWar', deletable: false },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('MONTH action 전에 전략·외교 제한, 임시 세율, 첩보 기간을 갱신한다', async () => {
|
it('MONTH action 전에 전략·외교 제한, 임시 세율, 첩보 기간을 갱신한다', async () => {
|
||||||
const updates: Array<{ id: number; patch: Record<string, unknown> }> = [];
|
const updates: Array<{ id: number; patch: Record<string, unknown> }> = [];
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export const projectCoreDatabaseSnapshot = (rows: {
|
|||||||
atmos: row.atmos,
|
atmos: row.atmos,
|
||||||
age: row.age,
|
age: row.age,
|
||||||
npcState: row.npcState,
|
npcState: row.npcState,
|
||||||
|
hasOwner: typeof row.userId === 'string' && row.userId.length > 0,
|
||||||
turnTime: row.turnTime instanceof Date ? serializeDate(row.turnTime) : row.turnTime,
|
turnTime: row.turnTime instanceof Date ? serializeDate(row.turnTime) : row.turnTime,
|
||||||
recentWarTime: row.recentWarTime instanceof Date ? serializeDate(row.recentWarTime) : row.recentWarTime,
|
recentWarTime: row.recentWarTime instanceof Date ? serializeDate(row.recentWarTime) : row.recentWarTime,
|
||||||
lastTurn: row.lastTurn,
|
lastTurn: row.lastTurn,
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ const buildGeneral = (row: Record<string, unknown>, fallbackTurnTime: Date): Tur
|
|||||||
atmos: readNumber(row, 'atmos'),
|
atmos: readNumber(row, 'atmos'),
|
||||||
age: readNumber(row, 'age', 30),
|
age: readNumber(row, 'age', 30),
|
||||||
npcState: readNumber(row, 'npcState'),
|
npcState: readNumber(row, 'npcState'),
|
||||||
|
userId: row.hasOwner === true ? 'turn-differential-owner' : null,
|
||||||
penalty: row.penalty,
|
penalty: row.penalty,
|
||||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||||
meta: {
|
meta: {
|
||||||
@@ -585,6 +586,7 @@ const projectWorld = (
|
|||||||
atmos: toDatabaseInt(general.atmos),
|
atmos: toDatabaseInt(general.atmos),
|
||||||
age: general.age,
|
age: general.age,
|
||||||
npcState: general.npcState,
|
npcState: general.npcState,
|
||||||
|
hasOwner: Boolean(general.userId),
|
||||||
turnTime: general.turnTime.toISOString(),
|
turnTime: general.turnTime.toISOString(),
|
||||||
recentWarTime: general.recentWarTime?.toISOString() ?? null,
|
recentWarTime: general.recentWarTime?.toISOString() ?? null,
|
||||||
lastTurn: general.lastTurn ?? null,
|
lastTurn: general.lastTurn ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user