fix: 사령부 모바일 현재 턴과 NPC 대상을 호환

daemon 진행 뒤 국가 명령을 현재 슬롯에 재기준화하고 Ref 명령별 장수 대상 필터를 적용한다. 모바일 고급 편집기 12턴과 500px 8수뇌 행렬을 실제 Chromium 기준으로 복원한다.
This commit is contained in:
2026-08-17 10:48:52 +00:00
parent a80d58f761
commit 3202509051
13 changed files with 440 additions and 51 deletions
+29 -14
View File
@@ -28,13 +28,14 @@ import {
repeatNationTurns,
setGeneralTurn,
setGeneralTurns,
setNationTurn,
setNationTurns,
setNationTurnAtCurrentPosition,
setNationTurnsAtCurrentPositions,
shiftGeneralTurns,
shiftNationTurns,
} from '../../turns/reservedTurns.js';
import { getOwnedGeneral } from '../shared/general.js';
import type { GameApiContext, GeneralRow, WorldStateRow } from '../../context.js';
import { buildRefGeneralTargetOptions } from '../../turns/commandTargets.js';
const zPushAmount = z
.number()
@@ -181,9 +182,15 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
orderBy: { id: 'asc' },
}),
ctx.db.general.findMany({
where: { npcState: { lt: 2 } },
select: { id: true, name: true, nationId: true, cityId: true },
orderBy: { id: 'asc' },
select: {
id: true,
name: true,
nationId: true,
cityId: true,
npcState: true,
officerLevel: true,
},
orderBy: [{ npcState: 'asc' }, { name: 'asc' }, { id: 'asc' }],
}),
environmentPromise,
loadBattleSimTraitOptions(),
@@ -192,7 +199,13 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
]);
const nationById = new Map(nations.map((entry) => [entry.id, entry]));
const cityById = new Map(cities.map((entry) => [entry.id, entry]));
const generalTargetOptions = buildRefGeneralTargetOptions({
actorId: general.id,
actorNationId: general.nationId,
generals,
nationNames: new Map(nations.map((entry) => [entry.id, entry.name])),
cityNames: new Map(cities.map((entry) => [entry.id, entry.name])),
});
const items: TurnCommandInputOptions['items'] = {
horse: [{ value: 'None', label: '판매/해제' }],
weapon: [{ value: 'None', label: '판매/해제' }],
@@ -226,12 +239,8 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
label: entry.name,
color: entry.color,
})),
generals: generals.map((entry) => ({
value: entry.id,
label: `${entry.name} (${nationById.get(entry.nationId)?.name ?? '무소속'} · ${
cityById.get(entry.cityId)?.name ?? '재야'
})`,
})),
generals: generalTargetOptions.generals,
generalTargets: generalTargetOptions.generalTargets,
crewTypes: (environment.unitSet.crewTypes ?? [])
.filter((entry) => !entry.requirements.some((requirement) => requirement.type === 'Impossible'))
.map((entry) => ({ value: entry.id, label: entry.name })),
@@ -446,7 +455,7 @@ export const turnsRouter = router({
await assertReservedTurnPermission(worldState, general, 'nation', input.action, args);
const snapshot = await mutateReservedTurns(() =>
setNationTurn(
setNationTurnAtCurrentPosition(
ctx.db,
general.nationId,
general.officerLevel,
@@ -559,7 +568,13 @@ export const turnsRouter = router({
await assertReservedTurnPermission(worldState, general, 'nation', update.action, update.args);
}
const snapshot = await mutateReservedTurns(() =>
setNationTurns(ctx.db, general.nationId, general.officerLevel, updates, input.expectedRevision)
setNationTurnsAtCurrentPositions(
ctx.db,
general.nationId,
general.officerLevel,
updates,
input.expectedRevision
)
);
return { ok: true, ...snapshot };
}),
+1
View File
@@ -71,6 +71,7 @@ export interface TurnCommandInputOptions {
cities: TurnCommandOption[];
nations: TurnCommandOption[];
generals: TurnCommandOption[];
generalTargets?: Record<string, TurnCommandOption[]>;
crewTypes: TurnCommandOption[];
armTypes: TurnCommandOption[];
nationTypes: TurnCommandOption[];
+1
View File
@@ -771,6 +771,7 @@ export const buildTurnCommandTable = async (options: {
cities: [],
nations: [],
generals: [],
generalTargets: {},
crewTypes: [],
armTypes: [],
nationTypes: [],
+55
View File
@@ -0,0 +1,55 @@
import type { TurnCommandOption } from './commandInput.js';
export interface GeneralTargetSource {
id: number;
name: string;
nationId: number;
cityId: number;
npcState: number;
officerLevel: number;
}
export interface RefGeneralTargetOptions {
generals: TurnCommandOption[];
generalTargets: Record<string, TurnCommandOption[]>;
}
const SAME_NATION_GENERAL_COMMANDS = ['che_증여'] as const;
const SAME_NATION_NATION_COMMANDS = ['che_발령', 'che_포상', 'che_몰수', 'che_부대탈퇴지시'] as const;
/** Ref 각 처리 화면의 SELECT 조건을 공통 command table의 명령별 option으로 투영한다. */
export const buildRefGeneralTargetOptions = (options: {
actorId: number;
actorNationId: number;
generals: readonly GeneralTargetSource[];
nationNames: ReadonlyMap<number, string>;
cityNames: ReadonlyMap<number, string>;
}): RefGeneralTargetOptions => {
const toOption = (entry: GeneralTargetSource): TurnCommandOption => ({
value: entry.id,
label: `${entry.name} (${options.nationNames.get(entry.nationId) ?? '무소속'} · ${
options.cityNames.get(entry.cityId) ?? '재야'
})`,
});
const project = (predicate: (entry: GeneralTargetSource) => boolean): TurnCommandOption[] =>
options.generals.filter(predicate).map(toOption);
const sameNation = project((entry) => entry.nationId === options.actorNationId);
const generalTargets: Record<string, TurnCommandOption[]> = {};
for (const action of SAME_NATION_GENERAL_COMMANDS) generalTargets[action] = sameNation;
for (const action of SAME_NATION_NATION_COMMANDS) generalTargets[action] = sameNation;
generalTargets.che_선양 = project(
(entry) => entry.nationId !== 0 && entry.nationId === options.actorNationId && entry.id !== options.actorId
);
generalTargets.che_등용 = project(
(entry) => entry.npcState < 2 && entry.officerLevel !== 12 && entry.id !== options.actorId
);
generalTargets.che_장수대상임관 = project((entry) => entry.id !== options.actorId);
return {
// 기존 profile의 공통 fallback은 유저장 목록을 유지한다.
generals: project((entry) => entry.npcState < 2),
generalTargets,
};
};
+43
View File
@@ -396,6 +396,32 @@ export const setNationTurns = async (
return { revision, turns: serializeTurnList(turns) };
};
/**
* 국가 턴 입력은 화면을 연 뒤 daemon이 선두 턴을 소비했더라도 사용자가 고른
* 슬롯 번호를 현재 큐에 적용한다. 큐 lease가 실제로 잡혀 있는 충돌은 그대로
* 거절하고, revision이 앞으로 진행한 경우에만 새 revision으로 재기준화한다.
*/
export const setNationTurnsAtCurrentPositions = async (
db: DatabaseClient,
nationId: number,
officerLevel: number,
updates: readonly ReservedTurnUpdate[],
expectedRevision: number
): Promise<ReservedTurnSnapshot> => {
let revision = expectedRevision;
for (let attempt = 0; attempt < 8; attempt += 1) {
try {
return await setNationTurns(db, nationId, officerLevel, updates, revision);
} catch (error) {
if (!(error instanceof ReservedTurnRevisionConflictError) || error.currentRevision === revision) {
throw error;
}
revision = error.currentRevision;
}
}
throw new ReservedTurnRevisionConflictError(revision, revision);
};
export const setNationTurn = async (
db: DatabaseClient,
nationId: number,
@@ -407,6 +433,23 @@ export const setNationTurn = async (
): Promise<ReservedTurnSnapshot> =>
setNationTurns(db, nationId, officerLevel, [{ turnIndices: [turnIndex], action, args }], expectedRevision);
export const setNationTurnAtCurrentPosition = async (
db: DatabaseClient,
nationId: number,
officerLevel: number,
turnIndex: number,
action: string,
args: unknown,
expectedRevision: number
): Promise<ReservedTurnSnapshot> =>
setNationTurnsAtCurrentPositions(
db,
nationId,
officerLevel,
[{ turnIndices: [turnIndex], action, args }],
expectedRevision
);
export const shiftNationTurns = async (
db: DatabaseClient,
nationId: number,