fix: 외교문서와 수뇌 예약의 Ref 계약을 복원한다

This commit is contained in:
2026-08-24 16:30:33 +00:00
parent b5763290d2
commit 604e8fdaf2
7 changed files with 999 additions and 25 deletions
+80 -6
View File
@@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { loadActionModuleBundle } from '@sammo-ts/logic';
import { asRecord } from '@sammo-ts/common';
import { GamePrisma } from '@sammo-ts/infra';
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
import { buildBattleSimEnvironment } from '../../battleSim/environment.js';
@@ -23,6 +24,7 @@ import {
MAX_GENERAL_TURNS,
MAX_NATION_TURNS,
ReservedTurnRevisionConflictError,
type ReservedTurnUpdate,
expandGeneralTurnIndices,
getGeneralTurnSnapshot,
getNationTurnSnapshot,
@@ -143,6 +145,68 @@ const readGeneralMetaNumber = (meta: unknown, key: string): number | null => {
return null;
};
const assertNationTurnInputAllowed = (general: GeneralRow): void => {
if (!Object.prototype.hasOwnProperty.call(asRecord(general.penalty), 'noChiefTurnInput')) {
return;
}
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: '수뇌 턴 입력 불가능',
});
};
const refillNationTurnInputKillturn = async (
ctx: GameApiContext,
general: GeneralRow,
worldState: WorldStateRow
): Promise<boolean> => {
if (general.npcState >= 2) {
return false;
}
const worldKillturn = readGeneralMetaNumber(worldState.meta, 'killturn');
if (worldKillturn === null) {
return false;
}
const currentKillturn = readGeneralMetaNumber(general.meta, 'killturn');
const nextKillturn = Math.max(currentKillturn ?? 0, worldKillturn);
if (currentKillturn !== null && nextKillturn === currentKillturn) {
return false;
}
const updated = await ctx.db.$queryRaw<Array<{ id: number }>>(
GamePrisma.sql`
UPDATE general
SET meta = jsonb_set(
CASE
WHEN jsonb_typeof(meta) = 'object' THEN meta
ELSE '{}'::jsonb
END,
'{killturn}',
to_jsonb(
GREATEST(
CASE
WHEN jsonb_typeof(meta->'killturn') = 'number'
THEN (meta->>'killturn')::double precision
ELSE 0::double precision
END,
${nextKillturn}::double precision
)
),
true
)
WHERE id = ${general.id}
AND npc_state < 2
AND (
jsonb_typeof(meta->'killturn') IS DISTINCT FROM 'number'
OR (meta->>'killturn')::double precision < ${nextKillturn}
)
RETURNING id
`
);
return updated.length > 0;
};
const assertReservedTurnPermission = async (
worldState: WorldStateRow,
general: GeneralRow,
@@ -535,6 +599,7 @@ export const turnsRouter = router({
const args = await parseCommandArgs('nation', input.action, input.args);
const worldState = await getReservationWorldState(ctx);
await assertScenarioCommandAvailable('nation', input.action, worldState);
assertNationTurnInputAllowed(general);
await assertReservedTurnPermission(worldState, general, 'nation', input.action, args);
const snapshot = await mutateReservedTurns(() =>
@@ -548,6 +613,10 @@ export const turnsRouter = router({
input.expectedRevision
)
);
if (await refillNationTurnInputKillturn(ctx, general, worldState)) {
ctx.changeJournal?.mark('general.content', general.id);
ctx.changeJournal?.mark('dashboard.global');
}
return { ok: true, ...snapshot };
}),
shiftNation: authedProcedure
@@ -639,17 +708,18 @@ export const turnsRouter = router({
message: 'General is not an officer.',
});
}
const updates = await Promise.all(
input.entries.map(async (entry) => ({
const worldState = await getReservationWorldState(ctx);
const updates: ReservedTurnUpdate[] = [];
for (const entry of input.entries) {
const update = {
turnIndices: entry.turnList,
action: entry.action,
args: await parseCommandArgs('nation', entry.action, entry.args),
}))
);
const worldState = await getReservationWorldState(ctx);
for (const update of updates) {
};
await assertScenarioCommandAvailable('nation', update.action, worldState);
assertNationTurnInputAllowed(general);
await assertReservedTurnPermission(worldState, general, 'nation', update.action, update.args);
updates.push(update);
}
const snapshot = await mutateReservedTurns(() =>
setNationTurnsAtCurrentPositions(
@@ -660,6 +730,10 @@ export const turnsRouter = router({
input.expectedRevision
)
);
if (await refillNationTurnInputKillturn(ctx, general, worldState)) {
ctx.changeJournal?.mark('general.content', general.id);
ctx.changeJournal?.mark('dashboard.global');
}
return { ok: true, ...snapshot };
}),
}),