feat: 장수 관련 명령어 처리 및 설정 기능 추가
This commit is contained in:
@@ -22,6 +22,7 @@ export interface WorldStateRow {
|
|||||||
|
|
||||||
export interface GeneralRow {
|
export interface GeneralRow {
|
||||||
id: number;
|
id: number;
|
||||||
|
userId: number | null;
|
||||||
name: string;
|
name: string;
|
||||||
nationId: number;
|
nationId: number;
|
||||||
cityId: number;
|
cityId: number;
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ import { loadWorldMap } from './maps/worldMap.js';
|
|||||||
const zRunReason = z.enum(['schedule', 'manual', 'poke']);
|
const zRunReason = z.enum(['schedule', 'manual', 'poke']);
|
||||||
const zMessageType = z.enum(['private', 'public', 'national', 'diplomacy']);
|
const zMessageType = z.enum(['private', 'public', 'national', 'diplomacy']);
|
||||||
|
|
||||||
|
const zGeneralSettings = z.object({
|
||||||
|
tnmt: z.number().int().optional(),
|
||||||
|
defence_train: z.number().int().optional(),
|
||||||
|
use_treatment: z.number().int().optional(),
|
||||||
|
use_auto_nation_turn: z.number().int().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
const zTurnRunBudget = z.object({
|
const zTurnRunBudget = z.object({
|
||||||
budgetMs: z.number().int().positive(),
|
budgetMs: z.number().int().positive(),
|
||||||
maxGenerals: z.number().int().positive(),
|
maxGenerals: z.number().int().positive(),
|
||||||
@@ -60,6 +67,19 @@ const toWorldStateSnapshot = (row: WorldStateRow) => ({
|
|||||||
updatedAt: row.updatedAt.toISOString(),
|
updatedAt: row.updatedAt.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getMyGeneral = async (ctx: { db: any, auth: any }) => {
|
||||||
|
if (!ctx.auth?.user.id) {
|
||||||
|
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
const general = await ctx.db.general.findFirst({
|
||||||
|
where: { userId: parseInt(ctx.auth.user.id) },
|
||||||
|
});
|
||||||
|
if (!general) {
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'General not found' });
|
||||||
|
}
|
||||||
|
return general;
|
||||||
|
};
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
health: router({
|
health: router({
|
||||||
ping: procedure.query(({ ctx }) => ({
|
ping: procedure.query(({ ctx }) => ({
|
||||||
@@ -693,6 +713,161 @@ export const appRouter = router({
|
|||||||
return { ok: true, wasLeader: result.wasLeader };
|
return { ok: true, wasLeader: result.wasLeader };
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
general: router({
|
||||||
|
dieOnPrestart: authedProcedure.mutation(async ({ ctx }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'dieOnPrestart',
|
||||||
|
generalId: general.id,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'dieOnPrestart') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
buildNationCandidate: authedProcedure.mutation(async ({ ctx }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'buildNationCandidate',
|
||||||
|
generalId: general.id,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'buildNationCandidate') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
instantRetreat: authedProcedure.mutation(async ({ ctx }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'instantRetreat',
|
||||||
|
generalId: general.id,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'instantRetreat') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
vacation: authedProcedure.mutation(async ({ ctx }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'vacation',
|
||||||
|
generalId: general.id,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'vacation') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
setMySetting: authedProcedure
|
||||||
|
.input(zGeneralSettings)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'setMySetting',
|
||||||
|
generalId: general.id,
|
||||||
|
settings: input,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'setMySetting') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
dropItem: authedProcedure
|
||||||
|
.input(z.object({ itemType: z.string() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'dropItem',
|
||||||
|
generalId: general.id,
|
||||||
|
itemType: input.itemType,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'dropItem') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
nation: router({
|
||||||
|
changePermission: authedProcedure
|
||||||
|
.input(z.object({
|
||||||
|
isAmbassador: z.boolean(),
|
||||||
|
targetGeneralIds: z.array(z.number().int().positive()),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'changePermission',
|
||||||
|
generalId: general.id,
|
||||||
|
isAmbassador: input.isAmbassador,
|
||||||
|
targetGeneralIds: input.targetGeneralIds,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'changePermission') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
kick: authedProcedure
|
||||||
|
.input(z.object({ destGeneralId: z.number().int().positive() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'kick',
|
||||||
|
generalId: general.id,
|
||||||
|
destGeneralId: input.destGeneralId,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'kick') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
appoint: authedProcedure
|
||||||
|
.input(z.object({
|
||||||
|
destGeneralId: z.number().int().nonnegative(),
|
||||||
|
destCityId: z.number().int().nonnegative(),
|
||||||
|
officerLevel: z.number().int().nonnegative(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const general = await getMyGeneral(ctx);
|
||||||
|
const result = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'appoint',
|
||||||
|
generalId: general.id,
|
||||||
|
destGeneralId: input.destGeneralId,
|
||||||
|
destCityId: input.destCityId,
|
||||||
|
officerLevel: input.officerLevel,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'appoint') {
|
||||||
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
}),
|
||||||
turnDaemon: router({
|
turnDaemon: router({
|
||||||
run: procedure
|
run: procedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -246,13 +246,35 @@ export class TurnDaemonLifecycle {
|
|||||||
return;
|
return;
|
||||||
case 'troopJoin':
|
case 'troopJoin':
|
||||||
case 'troopExit':
|
case 'troopExit':
|
||||||
|
case 'dieOnPrestart':
|
||||||
|
case 'buildNationCandidate':
|
||||||
|
case 'instantRetreat':
|
||||||
|
case 'vacation':
|
||||||
|
case 'setMySetting':
|
||||||
|
case 'dropItem':
|
||||||
|
case 'changePermission':
|
||||||
|
case 'kick':
|
||||||
|
case 'appoint':
|
||||||
await this.handleMutationCommand(command);
|
await this.handleMutationCommand(command);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleMutationCommand(
|
private async handleMutationCommand(
|
||||||
command: Extract<TurnDaemonCommand, { type: 'troopJoin' | 'troopExit' }>
|
command: Extract<
|
||||||
|
TurnDaemonCommand,
|
||||||
|
| { type: 'troopJoin' }
|
||||||
|
| { type: 'troopExit' }
|
||||||
|
| { type: 'dieOnPrestart' }
|
||||||
|
| { type: 'buildNationCandidate' }
|
||||||
|
| { type: 'instantRetreat' }
|
||||||
|
| { type: 'vacation' }
|
||||||
|
| { type: 'setMySetting' }
|
||||||
|
| { type: 'dropItem' }
|
||||||
|
| { type: 'changePermission' }
|
||||||
|
| { type: 'kick' }
|
||||||
|
| { type: 'appoint' }
|
||||||
|
>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let result: TurnDaemonCommandResult | null = null;
|
let result: TurnDaemonCommandResult | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -264,14 +286,8 @@ export class TurnDaemonLifecycle {
|
|||||||
type: command.type,
|
type: command.type,
|
||||||
ok: false,
|
ok: false,
|
||||||
generalId: command.generalId,
|
generalId: command.generalId,
|
||||||
...(command.type === 'troopJoin'
|
reason: '턴 데몬이 명령을 처리할 수 없습니다.',
|
||||||
? {
|
...(command.type === 'troopJoin' ? { troopId: command.troopId } : {}),
|
||||||
troopId: command.troopId,
|
|
||||||
reason: '턴 데몬이 명령을 처리할 수 없습니다.',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
reason: '턴 데몬이 명령을 처리할 수 없습니다.',
|
|
||||||
}),
|
|
||||||
} as TurnDaemonCommandResult;
|
} as TurnDaemonCommandResult;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -281,9 +297,8 @@ export class TurnDaemonLifecycle {
|
|||||||
type: command.type,
|
type: command.type,
|
||||||
ok: false,
|
ok: false,
|
||||||
generalId: command.generalId,
|
generalId: command.generalId,
|
||||||
...(command.type === 'troopJoin'
|
reason,
|
||||||
? { troopId: command.troopId, reason }
|
...(command.type === 'troopJoin' ? { troopId: command.troopId } : {}),
|
||||||
: { reason }),
|
|
||||||
} as TurnDaemonCommandResult;
|
} as TurnDaemonCommandResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const createDatabaseTurnHooks = async (
|
|||||||
nations,
|
nations,
|
||||||
troops,
|
troops,
|
||||||
deletedTroops,
|
deletedTroops,
|
||||||
|
deletedGenerals,
|
||||||
diplomacy,
|
diplomacy,
|
||||||
logs,
|
logs,
|
||||||
createdGenerals,
|
createdGenerals,
|
||||||
@@ -300,6 +301,12 @@ export const createDatabaseTurnHooks = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deletedGenerals.length > 0) {
|
||||||
|
await prisma.general.deleteMany({
|
||||||
|
where: { id: { in: deletedGenerals } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...generals
|
...generals
|
||||||
.filter((general) => !createdIds.has(general.id))
|
.filter((general) => !createdIds.has(general.id))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
City,
|
City,
|
||||||
LogEntryDraft,
|
LogEntryDraft,
|
||||||
Nation,
|
Nation,
|
||||||
|
ScenarioConfig,
|
||||||
Troop,
|
Troop,
|
||||||
TurnSchedule,
|
TurnSchedule,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
@@ -190,7 +191,9 @@ export class InMemoryTurnWorld {
|
|||||||
private readonly createdTroopIds = new Set<number>();
|
private readonly createdTroopIds = new Set<number>();
|
||||||
private readonly createdDiplomacyKeys = new Set<string>();
|
private readonly createdDiplomacyKeys = new Set<string>();
|
||||||
private readonly deletedTroopIds = new Set<number>();
|
private readonly deletedTroopIds = new Set<number>();
|
||||||
|
private readonly deletedGeneralIds = new Set<number>();
|
||||||
private readonly logs: LogEntryDraft[] = [];
|
private readonly logs: LogEntryDraft[] = [];
|
||||||
|
private readonly scenarioConfig: ScenarioConfig;
|
||||||
private checkpoint?: TurnCheckpoint;
|
private checkpoint?: TurnCheckpoint;
|
||||||
private state: TurnWorldState;
|
private state: TurnWorldState;
|
||||||
|
|
||||||
@@ -200,6 +203,7 @@ export class InMemoryTurnWorld {
|
|||||||
options: InMemoryTurnWorldOptions
|
options: InMemoryTurnWorldOptions
|
||||||
) {
|
) {
|
||||||
this.state = { ...state };
|
this.state = { ...state };
|
||||||
|
this.scenarioConfig = snapshot.scenarioConfig;
|
||||||
this.schedule = options.schedule;
|
this.schedule = options.schedule;
|
||||||
this.generalTurnHandler =
|
this.generalTurnHandler =
|
||||||
options.generalTurnHandler ??
|
options.generalTurnHandler ??
|
||||||
@@ -237,6 +241,10 @@ export class InMemoryTurnWorld {
|
|||||||
return { ...this.state };
|
return { ...this.state };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getScenarioConfig(): ScenarioConfig {
|
||||||
|
return this.scenarioConfig;
|
||||||
|
}
|
||||||
|
|
||||||
getGeneralById(id: number): TurnGeneral | null {
|
getGeneralById(id: number): TurnGeneral | null {
|
||||||
return this.generals.get(id) ?? null;
|
return this.generals.get(id) ?? null;
|
||||||
}
|
}
|
||||||
@@ -312,6 +320,39 @@ export class InMemoryTurnWorld {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeGeneral(id: number): boolean {
|
||||||
|
if (!this.generals.has(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.generals.delete(id);
|
||||||
|
this.dirtyGeneralIds.delete(id);
|
||||||
|
this.createdGeneralIds.delete(id);
|
||||||
|
this.deletedGeneralIds.add(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCity(id: number, patch: Partial<City>): City | null {
|
||||||
|
const target = this.cities.get(id);
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const next = { ...target, ...patch };
|
||||||
|
this.cities.set(id, next);
|
||||||
|
this.dirtyCityIds.add(id);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNation(id: number, patch: Partial<Nation>): Nation | null {
|
||||||
|
const target = this.nations.get(id);
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const next = { ...target, ...patch };
|
||||||
|
this.nations.set(id, next);
|
||||||
|
this.dirtyNationIds.add(id);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
updateTroop(id: number, patch: Partial<Troop>): Troop | null {
|
updateTroop(id: number, patch: Partial<Troop>): Troop | null {
|
||||||
const target = this.troops.get(id);
|
const target = this.troops.get(id);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@@ -548,6 +589,7 @@ export class InMemoryTurnWorld {
|
|||||||
nations: Nation[];
|
nations: Nation[];
|
||||||
troops: Troop[];
|
troops: Troop[];
|
||||||
deletedTroops: number[];
|
deletedTroops: number[];
|
||||||
|
deletedGenerals: number[];
|
||||||
diplomacy: TurnDiplomacy[];
|
diplomacy: TurnDiplomacy[];
|
||||||
logs: LogEntryDraft[];
|
logs: LogEntryDraft[];
|
||||||
createdGenerals: TurnGeneral[];
|
createdGenerals: TurnGeneral[];
|
||||||
@@ -579,6 +621,7 @@ export class InMemoryTurnWorld {
|
|||||||
.map((key) => this.diplomacy.get(key))
|
.map((key) => this.diplomacy.get(key))
|
||||||
.filter((entry): entry is TurnDiplomacy => Boolean(entry));
|
.filter((entry): entry is TurnDiplomacy => Boolean(entry));
|
||||||
const deletedTroops = Array.from(this.deletedTroopIds);
|
const deletedTroops = Array.from(this.deletedTroopIds);
|
||||||
|
const deletedGenerals = Array.from(this.deletedGeneralIds);
|
||||||
const logs = this.logs.splice(0, this.logs.length);
|
const logs = this.logs.splice(0, this.logs.length);
|
||||||
|
|
||||||
this.dirtyGeneralIds.clear();
|
this.dirtyGeneralIds.clear();
|
||||||
@@ -590,6 +633,7 @@ export class InMemoryTurnWorld {
|
|||||||
this.createdTroopIds.clear();
|
this.createdTroopIds.clear();
|
||||||
this.createdDiplomacyKeys.clear();
|
this.createdDiplomacyKeys.clear();
|
||||||
this.deletedTroopIds.clear();
|
this.deletedTroopIds.clear();
|
||||||
|
this.deletedGeneralIds.clear();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generals,
|
generals,
|
||||||
@@ -597,6 +641,7 @@ export class InMemoryTurnWorld {
|
|||||||
nations,
|
nations,
|
||||||
troops,
|
troops,
|
||||||
deletedTroops,
|
deletedTroops,
|
||||||
|
deletedGenerals,
|
||||||
diplomacy,
|
diplomacy,
|
||||||
logs,
|
logs,
|
||||||
createdGenerals,
|
createdGenerals,
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
import type { TurnDaemonHooks, TurnDaemonCommandHandler, TurnDaemonCommandResult, TurnRunResult } from '../lifecycle/types.js';
|
|
||||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
|
||||||
|
|
||||||
const buildFlushResult = (world: InMemoryTurnWorld): TurnRunResult => {
|
|
||||||
const state = world.getState();
|
|
||||||
return {
|
|
||||||
lastTurnTime: state.lastTurnTime.toISOString(),
|
|
||||||
processedGenerals: 0,
|
|
||||||
processedTurns: 0,
|
|
||||||
durationMs: 0,
|
|
||||||
partial: false,
|
|
||||||
checkpoint: world.getCheckpoint(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const flushWorld = async (
|
|
||||||
world: InMemoryTurnWorld,
|
|
||||||
hooks?: TurnDaemonHooks
|
|
||||||
): Promise<void> => {
|
|
||||||
if (!hooks?.flushChanges) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await hooks.flushChanges(buildFlushResult(world));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createTurnDaemonCommandHandler = (options: {
|
|
||||||
world: InMemoryTurnWorld;
|
|
||||||
hooks?: TurnDaemonHooks;
|
|
||||||
}): TurnDaemonCommandHandler => {
|
|
||||||
return {
|
|
||||||
handle: async (command): Promise<TurnDaemonCommandResult | null> => {
|
|
||||||
if (command.type === 'troopJoin') {
|
|
||||||
const general = options.world.getGeneralById(command.generalId);
|
|
||||||
if (!general) {
|
|
||||||
return {
|
|
||||||
type: 'troopJoin',
|
|
||||||
ok: false,
|
|
||||||
generalId: command.generalId,
|
|
||||||
troopId: command.troopId,
|
|
||||||
reason: '장수 정보를 찾을 수 없습니다.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (general.troopId !== 0) {
|
|
||||||
return {
|
|
||||||
type: 'troopJoin',
|
|
||||||
ok: false,
|
|
||||||
generalId: command.generalId,
|
|
||||||
troopId: command.troopId,
|
|
||||||
reason: '이미 부대에 소속되어 있습니다.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (general.nationId <= 0) {
|
|
||||||
return {
|
|
||||||
type: 'troopJoin',
|
|
||||||
ok: false,
|
|
||||||
generalId: command.generalId,
|
|
||||||
troopId: command.troopId,
|
|
||||||
reason: '국가에 소속되어 있지 않습니다.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const troop = options.world.getTroopById(command.troopId);
|
|
||||||
if (!troop || troop.nationId !== general.nationId) {
|
|
||||||
return {
|
|
||||||
type: 'troopJoin',
|
|
||||||
ok: false,
|
|
||||||
generalId: command.generalId,
|
|
||||||
troopId: command.troopId,
|
|
||||||
reason: '부대가 올바르지 않습니다.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
options.world.updateGeneral(command.generalId, {
|
|
||||||
troopId: command.troopId,
|
|
||||||
});
|
|
||||||
await flushWorld(options.world, options.hooks);
|
|
||||||
return {
|
|
||||||
type: 'troopJoin',
|
|
||||||
ok: true,
|
|
||||||
generalId: command.generalId,
|
|
||||||
troopId: command.troopId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.type === 'troopExit') {
|
|
||||||
const general = options.world.getGeneralById(command.generalId);
|
|
||||||
if (!general) {
|
|
||||||
return {
|
|
||||||
type: 'troopExit',
|
|
||||||
ok: false,
|
|
||||||
generalId: command.generalId,
|
|
||||||
reason: '장수 정보를 찾을 수 없습니다.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (general.troopId === 0) {
|
|
||||||
return {
|
|
||||||
type: 'troopExit',
|
|
||||||
ok: false,
|
|
||||||
generalId: command.generalId,
|
|
||||||
reason: '부대에 소속되어 있지 않습니다.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (general.troopId !== general.id) {
|
|
||||||
options.world.updateGeneral(command.generalId, {
|
|
||||||
troopId: 0,
|
|
||||||
});
|
|
||||||
await flushWorld(options.world, options.hooks);
|
|
||||||
return {
|
|
||||||
type: 'troopExit',
|
|
||||||
ok: true,
|
|
||||||
generalId: command.generalId,
|
|
||||||
wasLeader: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const troopId = general.troopId;
|
|
||||||
const members = options.world
|
|
||||||
.listGenerals()
|
|
||||||
.filter((entry) => entry.troopId === troopId);
|
|
||||||
for (const member of members) {
|
|
||||||
options.world.updateGeneral(member.id, { troopId: 0 });
|
|
||||||
}
|
|
||||||
options.world.removeTroop(troopId);
|
|
||||||
await flushWorld(options.world, options.hooks);
|
|
||||||
return {
|
|
||||||
type: 'troopExit',
|
|
||||||
ok: true,
|
|
||||||
generalId: command.generalId,
|
|
||||||
wasLeader: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -26,7 +26,7 @@ import { createGatewayAdminActionConsumer } from './gatewayAdminActions.js';
|
|||||||
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
||||||
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
||||||
import { createReservedTurnStore } from './reservedTurnStore.js';
|
import { createReservedTurnStore } from './reservedTurnStore.js';
|
||||||
import { createTurnDaemonCommandHandler } from './troopCommandHandler.js';
|
import { createTurnDaemonCommandHandler } from './worldCommandHandler.js';
|
||||||
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
||||||
import { loadTurnWorldFromDatabase } from './worldLoader.js';
|
import { loadTurnWorldFromDatabase } from './worldLoader.js';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
import type { TurnDaemonHooks, TurnDaemonCommandHandler, TurnDaemonCommandResult, TurnRunResult } from '../lifecycle/types.js';
|
||||||
|
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||||
|
|
||||||
|
const buildFlushResult = (world: InMemoryTurnWorld): TurnRunResult => {
|
||||||
|
const state = world.getState();
|
||||||
|
return {
|
||||||
|
lastTurnTime: state.lastTurnTime.toISOString(),
|
||||||
|
processedGenerals: 0,
|
||||||
|
processedTurns: 0,
|
||||||
|
durationMs: 0,
|
||||||
|
partial: false,
|
||||||
|
checkpoint: world.getCheckpoint(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushWorld = async (
|
||||||
|
world: InMemoryTurnWorld,
|
||||||
|
hooks?: TurnDaemonHooks
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!hooks?.flushChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await hooks.flushChanges(buildFlushResult(world));
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CommandHandlerContext {
|
||||||
|
world: InMemoryTurnWorld;
|
||||||
|
hooks?: TurnDaemonHooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTroopJoin(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'troopJoin' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return {
|
||||||
|
type: 'troopJoin',
|
||||||
|
ok: false,
|
||||||
|
generalId: command.generalId,
|
||||||
|
troopId: command.troopId,
|
||||||
|
reason: '장수 정보를 찾을 수 없습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (general.troopId !== 0) {
|
||||||
|
return {
|
||||||
|
type: 'troopJoin',
|
||||||
|
ok: false,
|
||||||
|
generalId: command.generalId,
|
||||||
|
troopId: command.troopId,
|
||||||
|
reason: '이미 부대에 소속되어 있습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (general.nationId <= 0) {
|
||||||
|
return {
|
||||||
|
type: 'troopJoin',
|
||||||
|
ok: false,
|
||||||
|
generalId: command.generalId,
|
||||||
|
troopId: command.troopId,
|
||||||
|
reason: '국가에 소속되어 있지 않습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const troop = world.getTroopById(command.troopId);
|
||||||
|
if (!troop || troop.nationId !== general.nationId) {
|
||||||
|
return {
|
||||||
|
type: 'troopJoin',
|
||||||
|
ok: false,
|
||||||
|
generalId: command.generalId,
|
||||||
|
troopId: command.troopId,
|
||||||
|
reason: '부대가 올바르지 않습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
world.updateGeneral(command.generalId, {
|
||||||
|
troopId: command.troopId,
|
||||||
|
});
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return {
|
||||||
|
type: 'troopJoin',
|
||||||
|
ok: true,
|
||||||
|
generalId: command.generalId,
|
||||||
|
troopId: command.troopId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTroopExit(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'troopExit' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return {
|
||||||
|
type: 'troopExit',
|
||||||
|
ok: false,
|
||||||
|
generalId: command.generalId,
|
||||||
|
reason: '장수 정보를 찾을 수 없습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (general.troopId === 0) {
|
||||||
|
return {
|
||||||
|
type: 'troopExit',
|
||||||
|
ok: false,
|
||||||
|
generalId: command.generalId,
|
||||||
|
reason: '부대에 소속되어 있지 않습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (general.troopId !== general.id) {
|
||||||
|
world.updateGeneral(command.generalId, {
|
||||||
|
troopId: 0,
|
||||||
|
});
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return {
|
||||||
|
type: 'troopExit',
|
||||||
|
ok: true,
|
||||||
|
generalId: command.generalId,
|
||||||
|
wasLeader: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const troopId = general.troopId;
|
||||||
|
const members = world
|
||||||
|
.listGenerals()
|
||||||
|
.filter((entry) => entry.troopId === troopId);
|
||||||
|
for (const member of members) {
|
||||||
|
world.updateGeneral(member.id, { troopId: 0 });
|
||||||
|
}
|
||||||
|
world.removeTroop(troopId);
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return {
|
||||||
|
type: 'troopExit',
|
||||||
|
ok: true,
|
||||||
|
generalId: command.generalId,
|
||||||
|
wasLeader: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDieOnPrestart(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'dieOnPrestart' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'dieOnPrestart', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
const worldState = world.getState();
|
||||||
|
const opentime = worldState.meta.opentime as string | undefined;
|
||||||
|
if (opentime && new Date(worldState.lastTurnTime) > new Date(opentime)) {
|
||||||
|
return { type: 'dieOnPrestart', ok: false, generalId: command.generalId, reason: '가오픈 기간이 아닙니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (general.npcState !== 0 || general.nationId !== 0) {
|
||||||
|
return { type: 'dieOnPrestart', ok: false, generalId: command.generalId, reason: '삭제할 수 없는 상태입니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
world.removeGeneral(command.generalId);
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return { type: 'dieOnPrestart', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBuildNationCandidate(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'buildNationCandidate' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'buildNationCandidate', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
if (general.nationId !== 0) {
|
||||||
|
return { type: 'buildNationCandidate', ok: false, generalId: command.generalId, reason: '이미 국가에 소속되어 있습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const worldState = world.getState();
|
||||||
|
const opentime = worldState.meta.opentime as string | undefined;
|
||||||
|
if (opentime && new Date(worldState.lastTurnTime) > new Date(opentime)) {
|
||||||
|
return { type: 'buildNationCandidate', ok: false, generalId: command.generalId, reason: '가오픈 기간이 아닙니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'buildNationCandidate', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleInstantRetreat(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'instantRetreat' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'instantRetreat', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = world.getScenarioConfig();
|
||||||
|
const availableInstantAction = config.const.availableInstantAction as Record<string, boolean> | undefined;
|
||||||
|
if (!availableInstantAction?.instantRetreat) {
|
||||||
|
return { type: 'instantRetreat', ok: false, generalId: command.generalId, reason: '즉시 귀환이 허용되지 않는 서버입니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'instantRetreat', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVacation(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'vacation' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'vacation', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
return { type: 'vacation', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetMySetting(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'setMySetting' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'setMySetting', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
world.updateGeneral(command.generalId, {
|
||||||
|
meta: {
|
||||||
|
...general.meta,
|
||||||
|
...command.settings,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return { type: 'setMySetting', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDropItem(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'dropItem' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'dropItem', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
const { itemType } = command;
|
||||||
|
const items = { ...general.role.items };
|
||||||
|
if (items.horse === itemType) items.horse = null;
|
||||||
|
else if (items.weapon === itemType) items.weapon = null;
|
||||||
|
else if (items.book === itemType) items.book = null;
|
||||||
|
else if (items.item === itemType) items.item = null;
|
||||||
|
else {
|
||||||
|
return { type: 'dropItem', ok: false, generalId: command.generalId, reason: '아이템을 가지고 있지 않습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
world.updateGeneral(command.generalId, {
|
||||||
|
role: {
|
||||||
|
...general.role,
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return { type: 'dropItem', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleChangePermission(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'changePermission' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'changePermission', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
const nation = world.getNationById(general.nationId);
|
||||||
|
if (!nation || nation.chiefGeneralId !== general.id) {
|
||||||
|
return { type: 'changePermission', ok: false, generalId: command.generalId, reason: '권한이 없습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const targetId of command.targetGeneralIds) {
|
||||||
|
const target = world.getGeneralById(targetId);
|
||||||
|
if (target && target.nationId === general.nationId) {
|
||||||
|
world.updateGeneral(targetId, {
|
||||||
|
meta: {
|
||||||
|
...target.meta,
|
||||||
|
permission: command.isAmbassador ? 'ambassador' : 'auditor',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return { type: 'changePermission', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleKick(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'kick' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'kick', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
const nation = world.getNationById(general.nationId);
|
||||||
|
if (!nation || nation.chiefGeneralId !== general.id) {
|
||||||
|
return { type: 'kick', ok: false, generalId: command.generalId, reason: '권한이 없습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = world.getGeneralById(command.destGeneralId);
|
||||||
|
if (!target || target.nationId !== general.nationId) {
|
||||||
|
return { type: 'kick', ok: false, generalId: command.generalId, reason: '대상을 찾을 수 없거나 같은 국가가 아닙니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
world.updateGeneral(command.destGeneralId, {
|
||||||
|
nationId: 0,
|
||||||
|
officerLevel: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return { type: 'kick', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAppoint(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'appoint' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
const general = world.getGeneralById(command.generalId);
|
||||||
|
if (!general) {
|
||||||
|
return { type: 'appoint', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
const nation = world.getNationById(general.nationId);
|
||||||
|
if (!nation || nation.chiefGeneralId !== general.id) {
|
||||||
|
return { type: 'appoint', ok: false, generalId: command.generalId, reason: '권한이 없습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = world.getGeneralById(command.destGeneralId);
|
||||||
|
if (command.destGeneralId !== 0 && (!target || target.nationId !== general.nationId)) {
|
||||||
|
return { type: 'appoint', ok: false, generalId: command.generalId, reason: '대상을 찾을 수 없거나 같은 국가가 아닙니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.officerLevel >= 5) {
|
||||||
|
for (const g of world.listGenerals()) {
|
||||||
|
if (g.nationId === general.nationId && g.officerLevel === command.officerLevel) {
|
||||||
|
world.updateGeneral(g.id, { officerLevel: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (command.destGeneralId !== 0) {
|
||||||
|
world.updateGeneral(command.destGeneralId, { officerLevel: command.officerLevel });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const city = world.getCityById(command.destCityId);
|
||||||
|
if (!city || city.nationId !== general.nationId) {
|
||||||
|
return { type: 'appoint', ok: false, generalId: command.generalId, reason: '도시를 찾을 수 없거나 아군 도시가 아닙니다.' };
|
||||||
|
}
|
||||||
|
for (const g of world.listGenerals()) {
|
||||||
|
if (g.nationId === general.nationId && g.meta.officerCity === command.destCityId && g.officerLevel === command.officerLevel) {
|
||||||
|
world.updateGeneral(g.id, { officerLevel: 0, meta: { ...g.meta, officerCity: 0 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (command.destGeneralId !== 0) {
|
||||||
|
world.updateGeneral(command.destGeneralId, {
|
||||||
|
officerLevel: command.officerLevel,
|
||||||
|
meta: { ...target!.meta, officerCity: command.destCityId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return { type: 'appoint', ok: true, generalId: command.generalId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTurnDaemonCommandHandler = (options: {
|
||||||
|
world: InMemoryTurnWorld;
|
||||||
|
hooks?: TurnDaemonHooks;
|
||||||
|
}): TurnDaemonCommandHandler => {
|
||||||
|
const ctx = { world: options.world, hooks: options.hooks };
|
||||||
|
|
||||||
|
return {
|
||||||
|
handle: async (command): Promise<TurnDaemonCommandResult | null> => {
|
||||||
|
switch (command.type) {
|
||||||
|
case 'troopJoin': return handleTroopJoin(ctx, command);
|
||||||
|
case 'troopExit': return handleTroopExit(ctx, command);
|
||||||
|
case 'dieOnPrestart': return handleDieOnPrestart(ctx, command);
|
||||||
|
case 'buildNationCandidate': return handleBuildNationCandidate(ctx, command);
|
||||||
|
case 'instantRetreat': return handleInstantRetreat(ctx, command);
|
||||||
|
case 'vacation': return handleVacation(ctx, command);
|
||||||
|
case 'setMySetting': return handleSetMySetting(ctx, command);
|
||||||
|
case 'dropItem': return handleDropItem(ctx, command);
|
||||||
|
case 'changePermission': return handleChangePermission(ctx, command);
|
||||||
|
case 'kick': return handleKick(ctx, command);
|
||||||
|
case 'appoint': return handleAppoint(ctx, command);
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -27,6 +27,8 @@ export interface TurnRunResult {
|
|||||||
durationMs: number;
|
durationMs: number;
|
||||||
partial: boolean;
|
partial: boolean;
|
||||||
checkpoint?: TurnCheckpoint;
|
checkpoint?: TurnCheckpoint;
|
||||||
|
deletedGenerals?: number[];
|
||||||
|
deletedTroops?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TurnDaemonStatus {
|
export interface TurnDaemonStatus {
|
||||||
@@ -55,7 +57,39 @@ export type TurnDaemonCommand =
|
|||||||
| { type: 'shutdown'; reason?: string }
|
| { type: 'shutdown'; reason?: string }
|
||||||
| { type: 'getStatus'; requestId?: string }
|
| { type: 'getStatus'; requestId?: string }
|
||||||
| { type: 'troopJoin'; requestId?: string; generalId: number; troopId: number }
|
| { type: 'troopJoin'; requestId?: string; generalId: number; troopId: number }
|
||||||
| { type: 'troopExit'; requestId?: string; generalId: number };
|
| { type: 'troopExit'; requestId?: string; generalId: number }
|
||||||
|
| { type: 'dieOnPrestart'; requestId?: string; generalId: number }
|
||||||
|
| { type: 'buildNationCandidate'; requestId?: string; generalId: number }
|
||||||
|
| { type: 'instantRetreat'; requestId?: string; generalId: number }
|
||||||
|
| { type: 'vacation'; requestId?: string; generalId: number }
|
||||||
|
| {
|
||||||
|
type: 'setMySetting';
|
||||||
|
requestId?: string;
|
||||||
|
generalId: number;
|
||||||
|
settings: {
|
||||||
|
tnmt?: number;
|
||||||
|
defence_train?: number;
|
||||||
|
use_treatment?: number;
|
||||||
|
use_auto_nation_turn?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| { type: 'dropItem'; requestId?: string; generalId: number; itemType: string }
|
||||||
|
| {
|
||||||
|
type: 'changePermission';
|
||||||
|
requestId?: string;
|
||||||
|
generalId: number;
|
||||||
|
isAmbassador: boolean;
|
||||||
|
targetGeneralIds: number[];
|
||||||
|
}
|
||||||
|
| { type: 'kick'; requestId?: string; generalId: number; destGeneralId: number }
|
||||||
|
| {
|
||||||
|
type: 'appoint';
|
||||||
|
requestId?: string;
|
||||||
|
generalId: number;
|
||||||
|
destGeneralId: number;
|
||||||
|
destCityId: number;
|
||||||
|
officerLevel: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type TurnDaemonCommandResult =
|
export type TurnDaemonCommandResult =
|
||||||
| {
|
| {
|
||||||
@@ -82,7 +116,16 @@ export type TurnDaemonCommandResult =
|
|||||||
ok: false;
|
ok: false;
|
||||||
generalId: number;
|
generalId: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
};
|
}
|
||||||
|
| { type: 'dieOnPrestart'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'buildNationCandidate'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'instantRetreat'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'vacation'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'setMySetting'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'dropItem'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'changePermission'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'kick'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
| { type: 'appoint'; ok: boolean; generalId: number; reason?: string };
|
||||||
|
|
||||||
export type TurnDaemonEvent =
|
export type TurnDaemonEvent =
|
||||||
| { type: 'status'; requestId?: string; status: TurnDaemonStatus }
|
| { type: 'status'; requestId?: string; status: TurnDaemonStatus }
|
||||||
|
|||||||
Reference in New Issue
Block a user