feat: add resource schemas and validation scripts
- Introduced new resource schemas for maps, scenarios, unit sets, and turn commands using Zod. - Implemented a script to generate JSON schemas from Zod schemas. - Added a validation script to ensure resource JSON files conform to their respective schemas. - Updated the logic package to export new resource schemas. - Added new dependencies for schema generation and validation. - Created a tools-scripts package for resource management tasks.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
|
||||
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
|
||||
import { asStringArray, isRecord } from '@sammo-ts/common';
|
||||
import { asStringArray } from '@sammo-ts/common';
|
||||
import { TurnCommandProfileInputSchema } from '../../resources/turnCommandSchema.js';
|
||||
|
||||
export interface TurnCommandProfile {
|
||||
general: GeneralTurnCommandKey[];
|
||||
@@ -44,18 +45,20 @@ export const parseTurnCommandProfile = (
|
||||
raw: unknown,
|
||||
fallback: TurnCommandProfile = DEFAULT_TURN_COMMAND_PROFILE
|
||||
): TurnCommandProfile => {
|
||||
if (!isRecord(raw)) {
|
||||
const parsed = TurnCommandProfileInputSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return fallback;
|
||||
}
|
||||
const data = parsed.data;
|
||||
return {
|
||||
general: parseKeyList({
|
||||
raw: raw.general,
|
||||
raw: data.general,
|
||||
defaults: fallback.general,
|
||||
isKey: isGeneralTurnCommandKey,
|
||||
label: 'general',
|
||||
}),
|
||||
nation: parseKeyList({
|
||||
raw: raw.nation,
|
||||
raw: data.nation,
|
||||
defaults: fallback.nation,
|
||||
isKey: isNationTurnCommandKey,
|
||||
label: 'nation',
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './logging/index.js';
|
||||
export * from './messages/index.js';
|
||||
export * from './items/index.js';
|
||||
export { ITEM_KEYS, createItemActionModules, createItemModuleRegistry, loadItemModules } from './items/index.js';
|
||||
export * from './resources/index.js';
|
||||
export * from './ports/world.js';
|
||||
export * from './ports/worldSnapshot.js';
|
||||
export * from './scenario/index.js';
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './mapSchema.js';
|
||||
export * from './scenarioSchema.js';
|
||||
export * from './unitSetSchema.js';
|
||||
export * from './turnCommandSchema.js';
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const MapCityStatsSchema = z.object({
|
||||
population: z.number(),
|
||||
agriculture: z.number(),
|
||||
commerce: z.number(),
|
||||
security: z.number(),
|
||||
defence: z.number(),
|
||||
wall: z.number(),
|
||||
});
|
||||
|
||||
export const MapCityDefinitionSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
level: z.number(),
|
||||
region: z.number(),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
}),
|
||||
connections: z.array(z.number()),
|
||||
max: MapCityStatsSchema,
|
||||
initial: MapCityStatsSchema,
|
||||
meta: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const MapDefaultsSchema = z
|
||||
.object({
|
||||
trust: z.number(),
|
||||
trade: z.number(),
|
||||
supplyState: z.number(),
|
||||
frontState: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const MapDefinitionSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
cities: z.array(MapCityDefinitionSchema),
|
||||
defaults: MapDefaultsSchema.optional(),
|
||||
meta: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const RegionMapSchema = z.record(z.string(), z.record(z.string(), z.string()));
|
||||
|
||||
export const MapResourceSchema = z.union([MapDefinitionSchema, RegionMapSchema]);
|
||||
|
||||
export type MapDefinitionInput = z.infer<typeof MapDefinitionSchema>;
|
||||
export type RegionMapInput = z.infer<typeof RegionMapSchema>;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ScenarioStatBlockSchema = z
|
||||
.object({
|
||||
total: z.number(),
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
npcTotal: z.number(),
|
||||
npcMax: z.number(),
|
||||
npcMin: z.number(),
|
||||
chiefMin: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const ScenarioDefaultsInputSchema = z.object({
|
||||
stat: ScenarioStatBlockSchema.optional(),
|
||||
iconPath: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ScenarioDefinitionInputSchema = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
startYear: z.number().optional(),
|
||||
life: z.number().optional(),
|
||||
fiction: z.number().optional(),
|
||||
history: z.array(z.string()).optional(),
|
||||
iconPath: z.string().optional(),
|
||||
stat: ScenarioStatBlockSchema.optional(),
|
||||
map: z.record(z.string(), z.unknown()).optional(),
|
||||
const: z.record(z.string(), z.unknown()).optional(),
|
||||
nation: z.array(z.unknown()).optional(),
|
||||
diplomacy: z.array(z.unknown()).optional(),
|
||||
general: z.array(z.unknown()).optional(),
|
||||
general_ex: z.array(z.unknown()).optional(),
|
||||
general_neutral: z.array(z.unknown()).optional(),
|
||||
cities: z.array(z.unknown()).optional(),
|
||||
events: z.array(z.unknown()).optional(),
|
||||
initialEvents: z.array(z.unknown()).optional(),
|
||||
initialActions: z.array(z.unknown()).optional(),
|
||||
ignoreDefaultEvents: z.boolean().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const ScenarioResourceSchema = z.union([ScenarioDefaultsInputSchema, ScenarioDefinitionInputSchema]);
|
||||
|
||||
export type ScenarioDefaultsInput = z.infer<typeof ScenarioDefaultsInputSchema>;
|
||||
export type ScenarioDefinitionInput = z.infer<typeof ScenarioDefinitionInputSchema>;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const TurnCommandProfileInputSchema = z.object({
|
||||
general: z.array(z.string()),
|
||||
nation: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type TurnCommandProfileInput = z.infer<typeof TurnCommandProfileInputSchema>;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const numericRecordSchema = z.record(z.string(), z.number());
|
||||
const numericArraySchema = z.array(z.number());
|
||||
|
||||
export const CrewTypeRequirementSchema = z.union([
|
||||
z.object({ type: z.literal('ReqTech'), tech: z.number() }),
|
||||
z.object({ type: z.literal('ReqRegions'), regions: z.array(z.string()) }),
|
||||
z.object({ type: z.literal('ReqCities'), cities: z.array(z.string()) }),
|
||||
z.object({ type: z.literal('ReqCitiesWithCityLevel'), level: z.number(), cities: z.array(z.string()) }),
|
||||
z.object({ type: z.literal('ReqHighLevelCities'), level: z.number(), count: z.number() }),
|
||||
z.object({
|
||||
type: z.literal('ReqNationAux'),
|
||||
key: z.string(),
|
||||
op: z.string(),
|
||||
value: z.union([z.number(), z.string()]),
|
||||
}),
|
||||
z.object({ type: z.literal('ReqMinRelYear'), year: z.number() }),
|
||||
z.object({ type: z.literal('ReqChief') }),
|
||||
z.object({ type: z.literal('ReqNotChief') }),
|
||||
z.object({ type: z.literal('Impossible') }),
|
||||
z.looseObject({ type: z.string() }),
|
||||
]);
|
||||
|
||||
export const CrewTypeDefinitionInputSchema = z.object({
|
||||
id: z.number(),
|
||||
armType: z.number(),
|
||||
name: z.string(),
|
||||
attack: z.number(),
|
||||
defence: z.number(),
|
||||
speed: z.number(),
|
||||
avoid: z.number(),
|
||||
magicCoef: z.number(),
|
||||
cost: z.number(),
|
||||
rice: z.number(),
|
||||
requirements: z.array(CrewTypeRequirementSchema),
|
||||
attackCoef: z.union([numericRecordSchema, numericArraySchema]),
|
||||
defenceCoef: z.union([numericRecordSchema, numericArraySchema]),
|
||||
info: z.array(z.string()),
|
||||
initSkillTrigger: z.array(z.string()).nullable(),
|
||||
phaseSkillTrigger: z.array(z.string()).nullable(),
|
||||
iActionList: z.array(z.string()).nullable(),
|
||||
});
|
||||
|
||||
export const UnitSetDefinitionInputSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
defaultCrewTypeId: z.number().optional(),
|
||||
armTypes: z.record(z.string(), z.string()).optional(),
|
||||
crewTypes: z.array(CrewTypeDefinitionInputSchema).optional(),
|
||||
meta: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export type UnitSetDefinitionInput = z.infer<typeof UnitSetDefinitionInputSchema>;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import { asNullableNumber, asNullableString, asNumber, asString, asStringArray, isRecord } from '@sammo-ts/common';
|
||||
|
||||
import { ScenarioDefaultsInputSchema, ScenarioDefinitionInputSchema } from '../resources/scenarioSchema.js';
|
||||
|
||||
import type {
|
||||
ScenarioConfig,
|
||||
ScenarioDefaults,
|
||||
@@ -24,56 +26,7 @@ const FALLBACK_STAT: ScenarioStatBlock = {
|
||||
chiefMin: 0,
|
||||
};
|
||||
|
||||
const toRecordOrUndefined = (value: unknown): UnknownRecord | undefined => (isRecord(value) ? value : undefined);
|
||||
|
||||
const toArrayOrUndefined = (value: unknown): unknown[] | undefined => (Array.isArray(value) ? value : undefined);
|
||||
|
||||
const zRecord = z.record(z.string(), z.unknown());
|
||||
const zUnknownArray = z.array(z.unknown());
|
||||
const zOptionalRecord = z.preprocess(toRecordOrUndefined, zRecord.optional());
|
||||
const zOptionalArray = z.preprocess(toArrayOrUndefined, zUnknownArray.optional());
|
||||
const zStatInput = z
|
||||
.object({
|
||||
total: z.number().optional(),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
npcTotal: z.number().optional(),
|
||||
npcMax: z.number().optional(),
|
||||
npcMin: z.number().optional(),
|
||||
chiefMin: z.number().optional(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
const zScenarioDefaults = z
|
||||
.object({
|
||||
stat: z.preprocess(toRecordOrUndefined, zStatInput.optional()),
|
||||
iconPath: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const zScenarioInput = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
startYear: z.number().optional(),
|
||||
life: z.number().optional(),
|
||||
fiction: z.number().optional(),
|
||||
history: zOptionalArray,
|
||||
iconPath: z.string().optional(),
|
||||
stat: z.preprocess(toRecordOrUndefined, zStatInput.optional()),
|
||||
map: zOptionalRecord,
|
||||
const: zOptionalRecord,
|
||||
nation: zOptionalArray,
|
||||
diplomacy: zOptionalArray,
|
||||
general: zOptionalArray,
|
||||
general_ex: zOptionalArray,
|
||||
general_neutral: zOptionalArray,
|
||||
cities: zOptionalArray,
|
||||
events: zOptionalArray,
|
||||
initialEvents: zOptionalArray,
|
||||
initialActions: zOptionalArray,
|
||||
ignoreDefaultEvents: z.boolean().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const parseScenarioStatBlock = (value: unknown, fallback: ScenarioStatBlock): ScenarioStatBlock => {
|
||||
const data = isRecord(value) ? value : {};
|
||||
@@ -211,7 +164,7 @@ const parseDiplomacyRows = (rows: unknown[]): ScenarioDiplomacy[] =>
|
||||
|
||||
export const parseScenarioDefaults = (raw: unknown): ScenarioDefaults => {
|
||||
// 기본 시나리오 설정값을 안전하게 읽는다.
|
||||
const data = zScenarioDefaults.parse(raw);
|
||||
const data = ScenarioDefaultsInputSchema.parse(raw);
|
||||
const stat = parseScenarioStatBlock(data.stat, FALLBACK_STAT);
|
||||
const iconPath = asString(data.iconPath, '.');
|
||||
return { stat, iconPath };
|
||||
@@ -219,7 +172,7 @@ export const parseScenarioDefaults = (raw: unknown): ScenarioDefaults => {
|
||||
|
||||
export const parseScenarioDefinition = (raw: unknown, defaults: ScenarioDefaults): ScenarioDefinition => {
|
||||
// 시나리오 JSON을 런타임에서 쓰는 구조로 정규화한다.
|
||||
const data = zScenarioInput.parse(raw);
|
||||
const data = ScenarioDefinitionInputSchema.parse(raw);
|
||||
const stat = parseScenarioStatBlock(data.stat, defaults.stat);
|
||||
const mapConfig = data.map ?? {};
|
||||
const constConfig = data.const ?? {};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
asStringArray,
|
||||
isRecord,
|
||||
} from '@sammo-ts/common';
|
||||
import { UnitSetDefinitionInputSchema } from '../resources/unitSetSchema.js';
|
||||
|
||||
const DEFAULT_REGION_MAP: Record<string, number> = {
|
||||
하북: 1,
|
||||
@@ -114,7 +115,7 @@ const parseCrewType = (value: unknown): CrewTypeDefinition | null => {
|
||||
};
|
||||
|
||||
export const parseUnitSetDefinition = (value: unknown): UnitSetDefinition => {
|
||||
const data = asRecord(value);
|
||||
const data = UnitSetDefinitionInputSchema.parse(value);
|
||||
const id = asString(data.id, 'unknown');
|
||||
const name = asString(data.name, id);
|
||||
const defaultCrewTypeId =
|
||||
|
||||
Reference in New Issue
Block a user