fix(game): complete clock after invader ending
This commit is contained in:
@@ -549,6 +549,7 @@ export const createInvaderEndingHandler = (options: {
|
|||||||
isUnited: 3,
|
isUnited: 3,
|
||||||
refreshLimit: readNumber(meta.refreshLimit) * 100,
|
refreshLimit: readNumber(meta.refreshLimit) * 100,
|
||||||
});
|
});
|
||||||
|
world.completeGameClock();
|
||||||
world.removeEvent(environment.currentEventID);
|
world.removeEvent(environment.currentEventID);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ const buildHarness = (options?: {
|
|||||||
currentMonth: 1,
|
currentMonth: 1,
|
||||||
tickSeconds: 600,
|
tickSeconds: 600,
|
||||||
lastTurnTime: new Date('0200-01-01T00:00:00.000Z'),
|
lastTurnTime: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
|
clockPhase: 'RUNNING',
|
||||||
meta: {
|
meta: {
|
||||||
hiddenSeed: 'raise-invader-fixture',
|
hiddenSeed: 'raise-invader-fixture',
|
||||||
serverId: 'fixture-server',
|
serverId: 'fixture-server',
|
||||||
@@ -410,6 +411,7 @@ describe('invader monthly actions', () => {
|
|||||||
await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'));
|
await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'));
|
||||||
|
|
||||||
expect(world.getState().meta).toMatchObject({ isunited: 3, isUnited: 3, refreshLimit: 300 });
|
expect(world.getState().meta).toMatchObject({ isunited: 3, isUnited: 3, refreshLimit: 300 });
|
||||||
|
expect(world.getState().clockPhase).toBe('COMPLETED');
|
||||||
expect(world.listEvents()).toHaveLength(0);
|
expect(world.listEvents()).toHaveLength(0);
|
||||||
expect(world.peekDirtyState().logs.map((log) => log.text)).toEqual([
|
expect(world.peekDirtyState().logs.map((log) => log.text)).toEqual([
|
||||||
'<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!',
|
'<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!',
|
||||||
@@ -449,6 +451,7 @@ describe('invader monthly actions', () => {
|
|||||||
await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'));
|
await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'));
|
||||||
|
|
||||||
expect(world.getState().meta).toMatchObject({ isunited: 3, isUnited: 3, refreshLimit: 300 });
|
expect(world.getState().meta).toMatchObject({ isunited: 3, isUnited: 3, refreshLimit: 300 });
|
||||||
|
expect(world.getState().clockPhase).toBe('COMPLETED');
|
||||||
expect(world.listEvents()).toHaveLength(0);
|
expect(world.listEvents()).toHaveLength(0);
|
||||||
expect(world.peekDirtyState().logs.map((log) => log.text)).toEqual([
|
expect(world.peekDirtyState().logs.map((log) => log.text)).toEqual([
|
||||||
'<L><b>【이벤트】</b></>중원은 이민족에 의해 혼란에 빠졌습니다.',
|
'<L><b>【이벤트】</b></>중원은 이민족에 의해 혼란에 빠졌습니다.',
|
||||||
|
|||||||
@@ -338,6 +338,12 @@ integration('RaiseInvader database persistence', () => {
|
|||||||
currentYear: 199,
|
currentYear: 199,
|
||||||
currentMonth: 12,
|
currentMonth: 12,
|
||||||
tickSeconds: 600,
|
tickSeconds: 600,
|
||||||
|
clockBaseTime: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
|
clockTick: 0n,
|
||||||
|
clockMode: 'realtime',
|
||||||
|
clockWallAnchor: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
|
lastTurnTick: 0n,
|
||||||
|
clockPhase: 'RUNNING',
|
||||||
config: {},
|
config: {},
|
||||||
meta: {
|
meta: {
|
||||||
hiddenSeed: serverId,
|
hiddenSeed: serverId,
|
||||||
@@ -354,6 +360,14 @@ integration('RaiseInvader database persistence', () => {
|
|||||||
currentMonth: 12,
|
currentMonth: 12,
|
||||||
tickSeconds: 600,
|
tickSeconds: 600,
|
||||||
lastTurnTime: new Date('0200-01-01T00:00:00.000Z'),
|
lastTurnTime: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
|
clockBaseTime: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
|
clockTick: 0,
|
||||||
|
clockMode: 'realtime',
|
||||||
|
clockWallAnchor: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
|
lastTurnTick: 0,
|
||||||
|
clockPhase: 'RUNNING',
|
||||||
|
clockRevision: 1,
|
||||||
|
deadlineGeneration: 1,
|
||||||
meta: {
|
meta: {
|
||||||
hiddenSeed: serverId,
|
hiddenSeed: serverId,
|
||||||
lastGeneralId: firstCreatedGeneralId - 1,
|
lastGeneralId: firstCreatedGeneralId - 1,
|
||||||
@@ -610,6 +624,7 @@ integration('RaiseInvader database persistence', () => {
|
|||||||
leasedNationKeys: [],
|
leasedNationKeys: [],
|
||||||
});
|
});
|
||||||
expect(await db.worldState.findUniqueOrThrow({ where: { id: stateRow.id } })).toMatchObject({
|
expect(await db.worldState.findUniqueOrThrow({ where: { id: stateRow.id } })).toMatchObject({
|
||||||
|
clockPhase: 'COMPLETED',
|
||||||
meta: expect.objectContaining({ isunited: 3, isUnited: 3, refreshLimit: 300 }),
|
meta: expect.objectContaining({ isunited: 3, isUnited: 3, refreshLimit: 300 }),
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('readReleaseManifest', () => {
|
|||||||
await expect(readReleaseManifest(workspaceRoot)).resolves.toMatchObject({
|
await expect(readReleaseManifest(workspaceRoot)).resolves.toMatchObject({
|
||||||
controllerProtocol: RELEASE_CONTROLLER_PROTOCOL,
|
controllerProtocol: RELEASE_CONTROLLER_PROTOCOL,
|
||||||
gatewaySchemaHead: '20260825000000_add_bulk_release_batches',
|
gatewaySchemaHead: '20260825000000_add_bulk_release_batches',
|
||||||
gameSchemaHead: '20260903183000_turn_daemon_lease_utc_wall',
|
gameSchemaHead: '20260903201500_complete_invader_game_clock',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
-- InvaderEnding is the terminal gameplay boundary. Older rows can have
|
||||||
|
-- isUnited=3 while the logical game clock remains RUNNING because the handler
|
||||||
|
-- historically removed its event without completing the clock.
|
||||||
|
--
|
||||||
|
-- Deployment runs game-schema migrations while the profile runtime is stopped,
|
||||||
|
-- so this backfill installs the missing terminal state without racing a daemon.
|
||||||
|
-- Any unchosen raise-invader alternatives are no longer actionable after the
|
||||||
|
-- world leaves the unification-wait state; close their GAME_TIME action state at
|
||||||
|
-- the terminal authoritative tick while preserving the WALL_TIME envelopes.
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE "world_state"
|
||||||
|
SET "clock_phase" = 'COMPLETED'
|
||||||
|
WHERE "clock_phase" IN ('RUNNING', 'MANUAL')
|
||||||
|
AND GREATEST(
|
||||||
|
CASE WHEN "meta"->>'isUnited' ~ '^[0-9]+$' THEN ("meta"->>'isUnited')::integer ELSE 0 END,
|
||||||
|
CASE WHEN "meta"->>'isunited' ~ '^[0-9]+$' THEN ("meta"->>'isunited')::integer ELSE 0 END
|
||||||
|
) >= 3;
|
||||||
|
|
||||||
|
UPDATE "message_action" AS action
|
||||||
|
SET "status" = 'RESOLVED',
|
||||||
|
"resolved_game_tick" = GREATEST(action."created_game_tick", world."clock_tick"),
|
||||||
|
"updated_at_wall" = CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
|
||||||
|
FROM "world_state" AS world
|
||||||
|
WHERE action."action_type" = 'raiseInvader'
|
||||||
|
AND action."status" = 'PENDING'
|
||||||
|
AND world."clock_tick" IS NOT NULL
|
||||||
|
AND GREATEST(
|
||||||
|
CASE WHEN world."meta"->>'isUnited' ~ '^[0-9]+$' THEN (world."meta"->>'isUnited')::integer ELSE 0 END,
|
||||||
|
CASE WHEN world."meta"->>'isunited' ~ '^[0-9]+$' THEN (world."meta"->>'isunited')::integer ELSE 0 END
|
||||||
|
) <> 2;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"controllerProtocol": 2,
|
"controllerProtocol": 2,
|
||||||
"gatewaySchemaHead": "20260825000000_add_bulk_release_batches",
|
"gatewaySchemaHead": "20260825000000_add_bulk_release_batches",
|
||||||
"gameSchemaHead": "20260903183000_turn_daemon_lease_utc_wall",
|
"gameSchemaHead": "20260903201500_complete_invader_game_clock",
|
||||||
"components": ["gateway-api", "gateway-frontend", "release-controller", "game-api", "game-engine", "game-frontend"]
|
"components": ["gateway-api", "gateway-frontend", "release-controller", "game-api", "game-engine", "game-frontend"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user