264
src/data/editorValidation.ts
Normal file
264
src/data/editorValidation.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { Character, ItemCatalogOverride, WorldType } from '../types';
|
||||
import { CharacterPresetOverride } from './characterPresets';
|
||||
import { MonsterPreset, MonsterPresetOverride } from './hostileNpcPresets';
|
||||
import { SceneNpcPresetOverride, ScenePreset, ScenePresetOverride } from './scenePresets';
|
||||
|
||||
function pushError(errors: string[], message: string) {
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
function isPositiveNumber(value: number | undefined) {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
function isKnownGender(value: unknown): value is 'male' | 'female' {
|
||||
return value === 'male' || value === 'female';
|
||||
}
|
||||
|
||||
function isNonEmptyStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every(item => typeof item === 'string' && item.trim().length > 0);
|
||||
}
|
||||
|
||||
function validateBuildBuffs(errors: string[], ownerId: string, label: string, buffs: unknown) {
|
||||
if (!Array.isArray(buffs)) {
|
||||
pushError(errors, `${ownerId} ${label} must be an array.`);
|
||||
return;
|
||||
}
|
||||
|
||||
buffs.forEach((buff, index) => {
|
||||
if (!buff || typeof buff !== 'object') {
|
||||
pushError(errors, `${ownerId} ${label}[${index}] must be an object.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const typedBuff = buff as {
|
||||
name?: unknown;
|
||||
tags?: unknown;
|
||||
durationTurns?: unknown;
|
||||
};
|
||||
|
||||
if (typeof typedBuff.name !== 'string' || !typedBuff.name.trim()) {
|
||||
pushError(errors, `${ownerId} ${label}[${index}] is missing a valid name.`);
|
||||
}
|
||||
|
||||
if (!isNonEmptyStringArray(typedBuff.tags)) {
|
||||
pushError(errors, `${ownerId} ${label}[${index}].tags must be a non-empty string array.`);
|
||||
}
|
||||
|
||||
if (typeof typedBuff.durationTurns !== 'number' || !Number.isFinite(typedBuff.durationTurns) || typedBuff.durationTurns <= 0) {
|
||||
pushError(errors, `${ownerId} ${label}[${index}].durationTurns must be > 0.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function validateCharacterOverrides(
|
||||
overrideMap: Record<string, CharacterPresetOverride>,
|
||||
characters: Character[],
|
||||
scenesByWorld: Partial<Record<WorldType, Array<Pick<ScenePreset, 'id'>>>>,
|
||||
) {
|
||||
const errors: string[] = [];
|
||||
const validCharacterIds = new Set(characters.map(character => character.id));
|
||||
const validSceneIdsByWorld = {
|
||||
[WorldType.WUXIA]: new Set((scenesByWorld[WorldType.WUXIA] ?? []).map(scene => scene.id)),
|
||||
[WorldType.XIANXIA]: new Set((scenesByWorld[WorldType.XIANXIA] ?? []).map(scene => scene.id)),
|
||||
[WorldType.CUSTOM]: new Set((scenesByWorld[WorldType.CUSTOM] ?? []).map(scene => scene.id)),
|
||||
};
|
||||
|
||||
Object.entries(overrideMap).forEach(([characterId, override]) => {
|
||||
if (!validCharacterIds.has(characterId)) {
|
||||
pushError(errors, `未知角色覆盖:${characterId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (override.gender !== undefined && !isKnownGender(override.gender)) {
|
||||
pushError(errors, `${characterId} gender must be "male" or "female".`);
|
||||
}
|
||||
|
||||
if (override.combatTags !== undefined && !isNonEmptyStringArray(override.combatTags)) {
|
||||
pushError(errors, `${characterId} combatTags must be a non-empty string array.`);
|
||||
}
|
||||
|
||||
if (override.skills) {
|
||||
override.skills.forEach((skill, index) => {
|
||||
const skillLabel = `${characterId} skill ${skill.id || index + 1}`;
|
||||
if (!skill.id?.trim()) pushError(errors, `${skillLabel} is missing id.`);
|
||||
if (!skill.name?.trim()) pushError(errors, `${skillLabel} is missing name.`);
|
||||
if (!isPositiveNumber(skill.range)) pushError(errors, `${skillLabel} range must be > 0.`);
|
||||
if (typeof skill.damage !== 'number' || skill.damage < 0) pushError(errors, `${skillLabel} damage must be >= 0.`);
|
||||
if (typeof skill.manaCost !== 'number' || skill.manaCost < 0) pushError(errors, `${skillLabel} manaCost must be >= 0.`);
|
||||
if (typeof skill.cooldownTurns !== 'number' || skill.cooldownTurns < 0) pushError(errors, `${skillLabel} cooldownTurns must be >= 0.`);
|
||||
if (skill.buildBuffs !== undefined) {
|
||||
validateBuildBuffs(errors, characterId, `skill ${skill.id || index + 1} buildBuffs`, skill.buildBuffs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (override.sceneBindings) {
|
||||
Object.entries(override.sceneBindings).forEach(([world, binding]) => {
|
||||
if (!binding) return;
|
||||
const worldType = world as WorldType;
|
||||
const validSceneIds = validSceneIdsByWorld[worldType];
|
||||
|
||||
if (binding.homeSceneId && !validSceneIds.has(binding.homeSceneId)) {
|
||||
pushError(errors, `${characterId} has invalid homeSceneId for ${worldType}: ${binding.homeSceneId}`);
|
||||
}
|
||||
|
||||
(binding.npcSceneIds ?? []).forEach(sceneId => {
|
||||
if (!validSceneIds.has(sceneId)) {
|
||||
pushError(errors, `${characterId} has invalid npcSceneId for ${worldType}: ${sceneId}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateMonsterOverrides(
|
||||
overrideMap: Record<string, MonsterPresetOverride>,
|
||||
monsters: MonsterPreset[],
|
||||
) {
|
||||
const errors: string[] = [];
|
||||
const validMonsterIds = new Set(monsters.map(monster => monster.id));
|
||||
|
||||
Object.entries(overrideMap).forEach(([monsterId, override]) => {
|
||||
if (!validMonsterIds.has(monsterId)) {
|
||||
pushError(errors, `未知怪物覆盖:${monsterId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(override.baseStats ?? {}).forEach(([key, value]) => {
|
||||
const numericValue = typeof value === 'number' ? value : undefined;
|
||||
if (!isPositiveNumber(numericValue)) {
|
||||
pushError(errors, `${monsterId} baseStats.${key} must be > 0.`);
|
||||
}
|
||||
});
|
||||
|
||||
if (override.combatTags !== undefined && !isNonEmptyStringArray(override.combatTags)) {
|
||||
pushError(errors, `${monsterId} combatTags must be a non-empty string array.`);
|
||||
}
|
||||
|
||||
Object.entries(override.animations ?? {}).forEach(([animation, rawConfig]) => {
|
||||
const config = rawConfig as { frames?: number; fps?: number } | undefined;
|
||||
if (!config) return;
|
||||
if (!isPositiveNumber(config.frames)) pushError(errors, `${monsterId} ${animation}.frames must be > 0.`);
|
||||
if (!isPositiveNumber(config.fps)) pushError(errors, `${monsterId} ${animation}.fps must be > 0.`);
|
||||
});
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateSceneOverrides(
|
||||
overrideMap: Record<string, ScenePresetOverride>,
|
||||
scenes: ScenePreset[],
|
||||
monstersByWorld: Partial<Record<WorldType, MonsterPreset[]>>,
|
||||
) {
|
||||
const errors: string[] = [];
|
||||
const sceneById = new Map(scenes.map(scene => [scene.id, scene]));
|
||||
const validSceneIds = new Set(scenes.map(scene => scene.id));
|
||||
const validMonsterIdsByWorld = {
|
||||
[WorldType.WUXIA]: new Set((monstersByWorld[WorldType.WUXIA] ?? []).map(monster => monster.id)),
|
||||
[WorldType.XIANXIA]: new Set((monstersByWorld[WorldType.XIANXIA] ?? []).map(monster => monster.id)),
|
||||
[WorldType.CUSTOM]: new Set((monstersByWorld[WorldType.CUSTOM] ?? []).map(monster => monster.id)),
|
||||
};
|
||||
|
||||
Object.entries(overrideMap).forEach(([sceneId, override]) => {
|
||||
const scene = sceneById.get(sceneId);
|
||||
if (!scene) {
|
||||
pushError(errors, `未知场景覆盖:${sceneId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (override.forwardSceneId && !validSceneIds.has(override.forwardSceneId)) {
|
||||
pushError(errors, `${sceneId} has invalid forwardSceneId: ${override.forwardSceneId}`);
|
||||
}
|
||||
|
||||
(override.connectedSceneIds ?? []).forEach(targetSceneId => {
|
||||
if (!validSceneIds.has(targetSceneId)) {
|
||||
pushError(errors, `${sceneId} has invalid connectedSceneId: ${targetSceneId}`);
|
||||
}
|
||||
});
|
||||
|
||||
(override.monsterIds ?? []).forEach(monsterId => {
|
||||
if (!validMonsterIdsByWorld[scene.worldType].has(monsterId)) {
|
||||
pushError(errors, `${sceneId} has invalid monsterId: ${monsterId}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateSceneNpcOverrides(
|
||||
overrideMap: Record<string, SceneNpcPresetOverride>,
|
||||
validNpcIds: string[],
|
||||
characters: Character[],
|
||||
) {
|
||||
const errors: string[] = [];
|
||||
const npcIdSet = new Set(validNpcIds);
|
||||
const characterIdSet = new Set(characters.map(character => character.id));
|
||||
|
||||
Object.entries(overrideMap).forEach(([npcId, override]) => {
|
||||
if (!npcIdSet.has(npcId)) {
|
||||
pushError(errors, `未知场景角色覆盖:${npcId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (override.gender !== undefined && !isKnownGender(override.gender)) {
|
||||
pushError(errors, `${npcId} gender must be "male" or "female".`);
|
||||
}
|
||||
|
||||
if (override.characterId && !characterIdSet.has(override.characterId)) {
|
||||
pushError(errors, `${npcId} has invalid characterId: ${override.characterId}`);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateItemOverrides(
|
||||
overrideMap: Record<string, ItemCatalogOverride>,
|
||||
validItemIds: string[],
|
||||
) {
|
||||
const errors: string[] = [];
|
||||
const itemIdSet = new Set(validItemIds);
|
||||
|
||||
Object.entries(overrideMap).forEach(([itemId, override]) => {
|
||||
if (!itemIdSet.has(itemId)) {
|
||||
pushError(errors, `未知物品覆盖:${itemId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (override.name !== undefined && !override.name.trim()) {
|
||||
pushError(errors, `${itemId} name cannot be empty.`);
|
||||
}
|
||||
|
||||
if (override.category !== undefined && !override.category.trim()) {
|
||||
pushError(errors, `${itemId} category cannot be empty.`);
|
||||
}
|
||||
|
||||
if (override.description !== undefined && !override.description.trim()) {
|
||||
pushError(errors, `${itemId} description cannot be empty.`);
|
||||
}
|
||||
|
||||
if (override.tags !== undefined && !isNonEmptyStringArray(override.tags)) {
|
||||
pushError(errors, `${itemId} tags must be a non-empty string array.`);
|
||||
}
|
||||
|
||||
if (override.buildProfile?.tags !== undefined && !isNonEmptyStringArray(override.buildProfile.tags)) {
|
||||
pushError(errors, `${itemId} buildProfile.tags must be a non-empty string array.`);
|
||||
}
|
||||
|
||||
if (override.buildProfile?.craftTags !== undefined && !isNonEmptyStringArray(override.buildProfile.craftTags)) {
|
||||
pushError(errors, `${itemId} buildProfile.craftTags must be a non-empty string array.`);
|
||||
}
|
||||
|
||||
if (override.useProfile?.buildBuffs !== undefined) {
|
||||
validateBuildBuffs(errors, itemId, 'useProfile.buildBuffs', override.useProfile.buildBuffs);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
Reference in New Issue
Block a user