feat: Enhance scenario installation options and UI integration

- Added new installation options to GatewayAdminActionRecord and implemented parsing logic in gatewayOrchestrator.ts.
- Updated resolveResetSeedInfo to accommodate new scenario installation parameters.
- Introduced a new scenario catalog module to manage scenario previews and details.
- Enhanced AdminView.vue to include a comprehensive installation form with various configuration options.
- Implemented scenario listing and selection functionality in the frontend.
- Updated profile repository to support scenario updates.
- Added tests to cover new functionalities and ensure stability.
This commit is contained in:
2026-01-17 12:24:57 +00:00
parent f13f8bc1cf
commit bc76b6e725
12 changed files with 1362 additions and 28 deletions
+17
View File
@@ -16,15 +16,32 @@ export interface GameProfile {
export const zWorldStateConfig = z.object({
maxUserCnt: z.number().optional(),
fictionMode: z.string().optional(),
fiction: z.number().optional(),
joinMode: z.string().optional(),
blockGeneralCreate: z.number().optional(),
npcMode: z.number().optional(),
showImgLevel: z.number().optional(),
tournamentTrig: z.boolean().optional(),
extendedGeneral: z.boolean().optional(),
turnTermMinutes: z.number().optional(),
syncTurnTime: z.boolean().optional(),
});
export type WorldStateConfig = z.infer<typeof zWorldStateConfig>;
export const zWorldStateMeta = z.object({
starttime: z.string().optional(),
opentime: z.string().optional(),
preopenAt: z.string().optional(),
turntime: z.string().optional(),
otherTextInfo: z.string().optional(),
isUnited: z.number().optional(),
autorun_user: z
.object({
limit_minutes: z.number().optional(),
options: z.record(z.string(), z.boolean()).optional(),
})
.nullable()
.optional(),
});
export type WorldStateMeta = z.infer<typeof zWorldStateMeta>;
+43 -5
View File
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { randomBytes } from 'node:crypto';
import type { WorldStateRow } from '../../context.js';
import { authedProcedure, router } from '../../trpc.js';
@@ -43,6 +44,17 @@ const resolveJoinStat = (worldState: WorldStateRow) => {
};
};
const resolveJoinPolicy = (worldState: WorldStateRow) => {
const config = asRecord(worldState.config);
const joinMode = typeof config.joinMode === 'string' ? config.joinMode : 'full';
const blockGeneralCreate =
typeof config.blockGeneralCreate === 'number' && Number.isFinite(config.blockGeneralCreate)
? Math.floor(config.blockGeneralCreate)
: 0;
return { joinMode, blockGeneralCreate };
};
const hashString = (value: string): number => {
let hash = 0;
for (let i = 0; i < value.length; i += 1) {
@@ -169,6 +181,14 @@ export const joinRouter = router({
});
}
const joinPolicy = resolveJoinPolicy(worldState);
if (joinPolicy.blockGeneralCreate === 1) {
throw new TRPCError({
code: 'FORBIDDEN',
message: '장수 생성이 제한된 서버입니다.',
});
}
const statRule = resolveJoinStat(worldState);
const statTotal = input.leadership + input.strength + input.intel;
@@ -195,12 +215,30 @@ export const joinRouter = router({
const personalityOptions = await loadPersonalityOptions();
const personalityKeys = personalityOptions.map((trait) => trait.key);
const resolveGeneralName = async (): Promise<string> => {
if (joinPolicy.blockGeneralCreate !== 2) {
return input.name;
}
for (let attempt = 0; attempt < 5; attempt += 1) {
const candidate = randomBytes(5).toString('hex');
const exists = await ctx.db.general.findFirst({ where: { name: candidate } });
if (!exists) {
return candidate;
}
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: '랜덤 장수명 생성에 실패했습니다.',
});
};
const generalName = await resolveGeneralName();
const chosenPersonality =
input.character === 'Random'
? pickFromList(personalityKeys, `${userId}:${input.name}`) ?? 'None'
? pickFromList(personalityKeys, `${userId}:${generalName}`) ?? 'None'
: isPersonalityTraitKey(input.character)
? input.character
: 'None';
? input.character
: 'None';
return ctx.db.$transaction(async (db) => {
const existing = await db.general.findFirst({ where: { userId } });
@@ -210,7 +248,7 @@ export const joinRouter = router({
message: '이미 장수가 생성되어 있습니다.',
});
}
const nameExists = await db.general.findFirst({ where: { name: input.name } });
const nameExists = await db.general.findFirst({ where: { name: generalName } });
if (nameExists) {
throw new TRPCError({
code: 'CONFLICT',
@@ -234,7 +272,7 @@ export const joinRouter = router({
data: {
id: nextId,
userId,
name: input.name,
name: generalName,
nationId: 0,
cityId,
troopId: 0,