diff --git a/src/app/helpers/export/item.ts b/src/app/helpers/export/item.ts index 5103e4a..7a8c7a2 100644 --- a/src/app/helpers/export/item.ts +++ b/src/app/helpers/export/item.ts @@ -184,7 +184,7 @@ function fillInItemProperties(itemData: IItemDefinition) { } export function formatItems(items: IItemDefinition[]): IItemDefinition[] { - return items.map((item: any) => { + return structuredClone(items).map((item: any) => { if (!item.sellValue) delete item.sellValue; if (!item.maxUpgrades) delete item.maxUpgrades; if (!item.secondaryType) delete item.secondaryType; @@ -193,8 +193,9 @@ export function formatItems(items: IItemDefinition[]): IItemDefinition[] { if (item.cosmetic && !item.cosmetic.name) delete item.cosmetic; if (item.containedItems && !item.containedItems.length) delete item.containedItems; - if (!item.trait.name) delete item.trait; - if (item.randomTrait.name.length === 0) delete item.randomTrait; + if (item.trait && !item.trait.name) delete item.trait; + if (item.randomTrait && item.randomTrait.name.length === 0) + delete item.randomTrait; if (item.useEffect && !item.useEffect.name) delete item.useEffect; if (item.strikeEffect && !item.strikeEffect.name) delete item.strikeEffect; if (item.breakEffect && !item.breakEffect.name) delete item.breakEffect; diff --git a/src/app/helpers/export/npc.ts b/src/app/helpers/export/npc.ts index 00a11be..c433ba5 100644 --- a/src/app/helpers/export/npc.ts +++ b/src/app/helpers/export/npc.ts @@ -171,7 +171,7 @@ function fillInNPCProperties(npc: INPCDefinition): INPCDefinition { } export function formatNPCs(npcs: INPCDefinition[]): INPCDefinition[] { - return npcs.map((npc: any) => { + return structuredClone(npcs).map((npc: any) => { delete npc.hp; delete npc.mp; delete npc.giveXp; diff --git a/src/app/helpers/schemas/_helpers.ts b/src/app/helpers/schemas/_helpers.ts new file mode 100644 index 0000000..f4cdf59 --- /dev/null +++ b/src/app/helpers/schemas/_helpers.ts @@ -0,0 +1,95 @@ +import { difference, get, isNumber, isUndefined } from 'lodash'; +import { HasIdentification, Schema } from '../../../interfaces'; + +export function isInteger(num: any): boolean { + return isNumber(num) && Math.floor(num) === num; +} + +export function isCosmetic(cos: any): boolean { + return !!cos.name; +} + +export function isRequirement(req: any): boolean { + return !!(req.level || req.baseClass || req.alignment || req.quest); +} + +export function isSuccor(suc: any): boolean { + return suc.map && isInteger(suc.x) && isInteger(suc.y); +} + +export function isOptionalRollable(rol: any): boolean { + if (!rol || rol.length === 0) return true; + + return !!(rol[0].chance && rol[0].result); +} + +export function isRollable(rol: any): boolean { + return !!(rol.length > 0 && rol[0].chance && rol[0].result); +} + +export function isTrait(trait: any): boolean { + return !!(trait.name && isNumber(trait.level)); +} + +export function isIntegerBetween(min: any, max: any): (val: any) => boolean { + return (num) => num >= min && num <= max && isInteger(num); +} + +export function isEffect(eff: any): boolean { + return eff.name && isNumber(eff.potency); +} + +export function isEncrust(enc: any): boolean { + return !!(enc.stats || enc.equipEffect || enc.strikeEffect); +} + +export function isDropPool(pool: any): boolean { + return ( + isInteger(pool.choose.min) && + isInteger(pool.choose.max) && + isRollable(pool.items) + ); +} + +export function isRandomNumber(num: any) { + return isInteger(num.min) && isInteger(num.max); +} + +export function validateSchema( + label: string, + obj: T, + schema: Schema +): string[] { + const errors: string[] = []; + + schema.forEach(([prop, required, validator]) => { + const value = get(obj, prop); + + if (isUndefined(value)) { + if (required) + errors.push( + `Property '${prop}' is required, but absent in '${label}'.` + ); + return; + } + + if (!validator(value)) + errors.push( + `Property '${prop}' does not pass validation for '${label}'.` + ); + }); + + const basePropsSchema = schema + .filter((x) => !x[0].includes('.')) + .map((x) => x[0]); + const baseObjProps = Object.keys(obj).filter((x) => !['_id'].includes(x)); + + const diff = difference(baseObjProps, basePropsSchema); + if (diff.length > 0) { + errors.push( + `Properties ${diff.join(',')} are in '${label}' but not in schema.` + ); + } + + return errors; +} diff --git a/src/app/helpers/schemas/dialog.ts b/src/app/helpers/schemas/dialog.ts new file mode 100644 index 0000000..82a67c6 --- /dev/null +++ b/src/app/helpers/schemas/dialog.ts @@ -0,0 +1,23 @@ +import { isArray, isNumber, isObject, isString } from 'lodash'; +import { Schema } from '../../../interfaces'; +import { isRandomNumber } from './_helpers'; + +export const dialogSchema: Schema = [ + ['tag', true, isString], + + ['name', false, isString], + ['affiliation', false, isString], + ['allegiance', false, isString], + ['alignment', false, isString], + ['hostility', false, isString], + ['level', false, isNumber], + ['hp', false, isRandomNumber], + ['mp', false, isRandomNumber], + + ['otherStats', false, isObject], + ['usableSkills', false, isArray], + ['items', false, isObject], + ['items.equipment', false, isObject], + ['dialog', false, isObject], + ['behaviors', false, isArray], +]; diff --git a/src/app/helpers/schemas/droptable.ts b/src/app/helpers/schemas/droptable.ts new file mode 100644 index 0000000..b9c6290 --- /dev/null +++ b/src/app/helpers/schemas/droptable.ts @@ -0,0 +1,9 @@ +import { isArray, isBoolean, isString } from 'lodash'; +import { Schema } from '../../../interfaces'; + +export const droptableSchema: Schema = [ + ['mapName', false, isString], + ['regionName', false, isString], + ['isGlobal', false, isBoolean], + ['drops', true, isArray], +]; diff --git a/src/app/helpers/schemas/index.ts b/src/app/helpers/schemas/index.ts new file mode 100644 index 0000000..cc048e6 --- /dev/null +++ b/src/app/helpers/schemas/index.ts @@ -0,0 +1,7 @@ +export * from './dialog'; +export * from './droptable'; +export * from './item'; +export * from './npc'; +export * from './quest'; +export * from './recipe'; +export * from './spawner'; diff --git a/src/app/helpers/schemas/item.ts b/src/app/helpers/schemas/item.ts new file mode 100644 index 0000000..32189a6 --- /dev/null +++ b/src/app/helpers/schemas/item.ts @@ -0,0 +1,120 @@ +import { isArray, isBoolean, isInteger, isObject, isString } from 'lodash'; +import { Schema } from '../../../interfaces'; +import { + isCosmetic, + isEffect, + isEncrust, + isIntegerBetween, + isRequirement, + isRollable, + isSuccor, + isTrait, +} from './_helpers'; + +export const itemSchema: Schema = [ + ['name', true, isString], + ['sprite', true, isInteger], + ['animation', false, isInteger], + ['value', true, isInteger], + ['desc', true, isString], + ['itemClass', true, isString], + ['type', true, isString], + + ['binds', false, isBoolean], + ['tellsBind', false, isBoolean], + ['extendedDesc', false, isString], + ['isBeltable', false, isBoolean], + ['isSackable', false, isBoolean], + ['isHeavy', false, isBoolean], + ['secondaryType', false, isString], + ['stats', false, isObject], + ['maxUpgrades', false, isInteger], + ['canUpgradeWith', false, isBoolean], + ['recipe', false, isString], + + ['cosmetic', false, isCosmetic], + ['cosmetic.isPermanent', false, isBoolean], + ['cosmetic.name', false, isString], + + ['requirements', false, isRequirement], + ['requirements.alignment', false, isString], + ['requirements.baseClass', false, isString], + ['requirements.quest', false, isString], + ['requirements.level', false, isInteger], + + ['equipEffect', false, isEffect], + ['equipEffect.name', false, isString], + ['equipEffect.potency', false, isInteger], + ['equipEffect.duration', false, isInteger], + ['equipEffect.range', false, isIntegerBetween(0, 5)], + + ['strikeEffect', false, isEffect], + ['strikeEffect.name', false, isString], + ['strikeEffect.potency', false, isInteger], + ['strikeEffect.chance', false, isIntegerBetween(0, 100)], + ['strikeEffect.duration', false, isInteger], + ['strikeEffect.range', false, isIntegerBetween(0, 5)], + + ['useEffect', false, isEffect], + ['useEffect.name', false, isString], + ['useEffect.potency', false, isInteger], + ['useEffect.canApply', false, isBoolean], + ['useEffect.duration', false, isInteger], + ['useEffect.uses', false, isInteger], + ['useEffect.range', false, isIntegerBetween(0, 5)], + + ['trapEffect', false, isEffect], + ['trapEffect.name', false, isString], + ['trapEffect.potency', false, isInteger], + ['trapEffect.duration', false, isInteger], + ['trapEffect.uses', false, isInteger], + ['trapEffect.range', false, isIntegerBetween(0, 5)], + + ['breakEffect', false, isEffect], + ['breakEffect.name', false, isString], + ['breakEffect.potency', false, isInteger], + + ['effect.extra', false, isObject], + + ['encrustGive', false, isEncrust], + + ['tier', false, isInteger], + ['destroyOnDrop', false, isBoolean], + ['twoHanded', false, isBoolean], + ['canShoot', false, isBoolean], + ['attackRange', false, isIntegerBetween(0, 5)], + ['offhand', false, isBoolean], + ['proneChance', false, isIntegerBetween(0, 100)], + ['returnsOnThrow', false, isBoolean], + + ['shots', false, isInteger], + ['damageClass', false, isString], + + ['trapUses', false, isInteger], + + ['containedItems', false, isRollable], + + ['succorInfo', false, isSuccor], + ['succorInfo.map', false, isString], + ['succorInfo.x', false, isInteger], + ['succorInfo.y', false, isInteger], + + ['trait', false, isTrait], + ['trait.name', false, isString], + ['trait.level', false, isInteger], + + ['bookFindablePages', false, isInteger], + ['bookItemFilter', false, isString], + ['bookPage', false, isInteger], + ['bookCurrentPage', false, isInteger], + ['bookPages', false, isArray], + + ['ounces', false, isInteger], + ['notUsableAfterHours', false, isInteger], + + ['quality', false, isInteger], + ['sellValue', false, isInteger], + + ['randomStats', false, isObject], + ['randomTrait', false, isObject], +]; diff --git a/src/app/helpers/schemas/npc.ts b/src/app/helpers/schemas/npc.ts new file mode 100644 index 0000000..3cebcb7 --- /dev/null +++ b/src/app/helpers/schemas/npc.ts @@ -0,0 +1,59 @@ +import { isArray, isBoolean, isInteger, isObject, isString } from 'lodash'; +import { Schema } from '../../../interfaces'; +import { + isDropPool, + isOptionalRollable, + isRandomNumber, + isRollable, +} from './_helpers'; + +export const npcSchema: Schema = [ + ['npcId', true, isString], + ['sprite', true, isArray], + ['cr', true, isInteger], + ['hp', true, isRandomNumber], + ['mp', false, isRandomNumber], + ['giveXp', true, isRandomNumber], + ['gold', true, isRandomNumber], + ['skillOnKill', true, isInteger], + + ['name', false, isArray], + ['alignment', false, isString], + ['affiliation', false, isString], + ['allegiance', false, isString], + ['allegianceReputation', false, isObject], + ['aquaticOnly', false, isBoolean], + ['avoidWater', false, isBoolean], + ['baseClass', false, isString], + ['baseEffects', false, isArray], + ['copyDrops', false, isRollable], + ['dropPool', false, isDropPool], + ['drops', false, isRollable], + ['forceAI', false, isString], + ['items', false, isObject], + ['items.equipment', false, isObject], + ['items.sack', false, isOptionalRollable], + + ['level', true, isInteger], + ['hpMult', false, isInteger], + + ['monsterClass', false, isString], + ['monsterGroup', false, isString], + ['hostility', false, isString], + ['noCorpseDrop', false, isBoolean], + ['noItemDrop', false, isBoolean], + ['repMod', false, isArray], + + ['stats', true, isObject], + ['skills', true, isObject], + + ['summonStatModifiers', false, isObject], + ['summonSkillModifiers', false, isObject], + + ['tanSkillRequired', false, isInteger], + ['tansFor', false, isString], + ['traitLevels', false, isObject], + ['triggers', false, isObject], + + ['usableSkills', false, isArray], +]; diff --git a/src/app/helpers/schemas/quest.ts b/src/app/helpers/schemas/quest.ts new file mode 100644 index 0000000..b9336f5 --- /dev/null +++ b/src/app/helpers/schemas/quest.ts @@ -0,0 +1,30 @@ +import { isArray, isBoolean, isNumber, isObject, isString } from 'lodash'; +import { Schema } from '../../../interfaces'; + +export const questSchema: Schema = [ + ['name', true, isString], + ['giver', true, isString], + ['desc', true, isString], + + ['isDaily', false, isBoolean], + ['isRepeatable', false, isBoolean], + + ['messages', false, isObject], + ['messages.kill', false, isString], + ['messages.complete', false, isString], + ['messages.incomplete', false, isString], + ['messages.alreadyHas', false, isString], + ['messages.permComplete', false, isString], + + ['requirements', false, isObject], + ['requirements.type', false, isString], + ['requirements.npcIds', false, isArray], + ['requirements.item', false, isString], + ['requirements.fromHands', false, isBoolean], + ['requirements.fromSack', false, isBoolean], + ['requirements.killsRequired', false, isNumber], + ['requirements.countRequired', false, isNumber], + ['requirements.itemssRequired', false, isNumber], + + ['rewards', false, isArray], +]; diff --git a/src/app/helpers/schemas/recipe.ts b/src/app/helpers/schemas/recipe.ts new file mode 100644 index 0000000..1f785b1 --- /dev/null +++ b/src/app/helpers/schemas/recipe.ts @@ -0,0 +1,22 @@ +import { isArray, isBoolean, isNumber, isString } from 'lodash'; +import { Schema } from '../../../interfaces'; + +export const recipeSchema: Schema = [ + ['category', true, isString], + ['item', true, isString], + ['name', true, isString], + ['recipeType', true, isString], + + ['maxSkillForGains', false, isNumber], + ['requireSkill', false, isNumber], + ['skillGained', false, isNumber], + ['xpGained', false, isNumber], + ['copySkillToPotency', false, isBoolean], + ['ingredients', false, isArray], + ['ozIngredients', false, isArray], + ['potencyScalar', false, isNumber], + ['requireClass', false, isArray], + ['requireLearn', false, isBoolean], + ['requireSpell', false, isString], + ['transferOwnerFrom', false, isString], +]; diff --git a/src/app/helpers/schemas/spawner.ts b/src/app/helpers/schemas/spawner.ts new file mode 100644 index 0000000..51c8ee2 --- /dev/null +++ b/src/app/helpers/schemas/spawner.ts @@ -0,0 +1,37 @@ +import { isArray, isBoolean, isNumber, isString } from 'lodash'; +import { Schema } from '../../../interfaces'; +import { isRollable } from './_helpers'; + +export const spawnerSchema: Schema = [ + ['npcIds', true, isRollable], + ['tag', true, isString], + + ['paths', false, isArray], + ['respawnRate', false, isNumber], + ['initialSpawn', false, isNumber], + ['maxCreatures', false, isNumber], + ['spawnRadius', false, isNumber], + ['randomWalkRadius', false, isNumber], + ['leashRadius', false, isNumber], + + ['shouldSerialize', false, isBoolean], + ['alwaysSpawn', false, isBoolean], + ['requireHoliday', false, isString], + ['requireDeadToRespawn', false, isBoolean], + ['canSlowDown', false, isBoolean], + + ['stripX', false, isNumber], + ['stripY', false, isNumber], + ['stripRadius', false, isNumber], + ['shouldEatTier', false, isNumber], + ['shouldStrip', false, isBoolean], + ['stripOnSpawner', false, isBoolean], + + ['doInitialSpawnImmediately', false, isBoolean], + ['attributeAddChance', false, isNumber], + ['eliteTickCap', false, isNumber], + ['npcAISettings', false, isArray], + + ['respectKnowledge', false, isBoolean], + ['isDangerous', false, isBoolean], +]; diff --git a/src/app/helpers/validate.ts b/src/app/helpers/validate.ts index f0d63a9..a34bc65 100644 --- a/src/app/helpers/validate.ts +++ b/src/app/helpers/validate.ts @@ -11,6 +11,13 @@ import { checkQuests, checkRecipes, checkSpawners, + validateDialogs, + validateDroptables, + validateItems, + validateNPCs, + validateQuests, + validateRecipes, + validateSpawners, } from './validators'; export function validationMessagesForMod( @@ -27,6 +34,13 @@ export function validationMessagesForMod( checkQuests(mod), ...checkMapProperties(mod), ...checkMapSpawners(mod), + validateDialogs(mod), + validateItems(mod), + validateDroptables(mod), + validateNPCs(mod), + validateQuests(mod), + validateRecipes(mod), + validateSpawners(mod), ].filter((c) => c.messages.length > 0); return sortBy(validationContainer, 'header'); diff --git a/src/app/helpers/validators/dialog.ts b/src/app/helpers/validators/dialog.ts new file mode 100644 index 0000000..4b7bd0c --- /dev/null +++ b/src/app/helpers/validators/dialog.ts @@ -0,0 +1,26 @@ +import { + IModKit, + INPCScript, + ValidationMessage, + ValidationMessageGroup, +} from '../../../interfaces'; +import { dialogSchema } from '../schemas'; +import { validateSchema } from '../schemas/_helpers'; + +export function validateDialogs(mod: IModKit): ValidationMessageGroup { + const itemValidations: ValidationMessageGroup = { + header: 'Invalid NPC Scripts', + messages: [], + }; + + mod.dialogs.forEach((item) => { + const failures = validateSchema(item.tag, item, dialogSchema); + const validationFailures: ValidationMessage[] = failures.map((f) => ({ + type: 'error', + message: f, + })); + itemValidations.messages.push(...validationFailures); + }); + + return itemValidations; +} diff --git a/src/app/helpers/validators/droptable.ts b/src/app/helpers/validators/droptable.ts new file mode 100644 index 0000000..0088d65 --- /dev/null +++ b/src/app/helpers/validators/droptable.ts @@ -0,0 +1,30 @@ +import { + IDroptable, + IModKit, + ValidationMessage, + ValidationMessageGroup, +} from '../../../interfaces'; +import { droptableSchema } from '../schemas'; +import { validateSchema } from '../schemas/_helpers'; + +export function validateDroptables(mod: IModKit): ValidationMessageGroup { + const itemValidations: ValidationMessageGroup = { + header: 'Invalid Droptables', + messages: [], + }; + + mod.drops.forEach((item) => { + const failures = validateSchema( + item.mapName || item.regionName || 'Global', + item, + droptableSchema + ); + const validationFailures: ValidationMessage[] = failures.map((f) => ({ + type: 'error', + message: f, + })); + itemValidations.messages.push(...validationFailures); + }); + + return itemValidations; +} diff --git a/src/app/helpers/validators/index.ts b/src/app/helpers/validators/index.ts index 3f817b8..bacd91e 100644 --- a/src/app/helpers/validators/index.ts +++ b/src/app/helpers/validators/index.ts @@ -1,3 +1,5 @@ +export * from './dialog'; +export * from './droptable'; export * from './item'; export * from './map'; export * from './npc'; diff --git a/src/app/helpers/validators/item.ts b/src/app/helpers/validators/item.ts index 3122c94..8d48dc3 100644 --- a/src/app/helpers/validators/item.ts +++ b/src/app/helpers/validators/item.ts @@ -1,9 +1,14 @@ import { get } from 'lodash'; import { + IItemDefinition, IModKit, ItemSlotType, + ValidationMessage, ValidationMessageGroup, } from '../../../interfaces'; +import { formatItems } from '../export'; +import { itemSchema } from '../schemas'; +import { validateSchema } from '../schemas/_helpers'; export function checkItemStats(mod: IModKit): ValidationMessageGroup { const itemValidations: ValidationMessageGroup = { @@ -116,3 +121,27 @@ export function checkItemUses(mod: IModKit): ValidationMessageGroup { return itemValidations; } + +export function validateItems(mod: IModKit): ValidationMessageGroup { + const itemValidations: ValidationMessageGroup = { + header: 'Invalid Items', + messages: [], + }; + + const formattedItems = formatItems(mod.items); + + formattedItems.forEach((item) => { + const failures = validateSchema( + item.name, + item, + itemSchema + ); + const validationFailures: ValidationMessage[] = failures.map((f) => ({ + type: 'error', + message: f, + })); + itemValidations.messages.push(...validationFailures); + }); + + return itemValidations; +} diff --git a/src/app/helpers/validators/map.ts b/src/app/helpers/validators/map.ts index 7418edb..d7f8dda 100644 --- a/src/app/helpers/validators/map.ts +++ b/src/app/helpers/validators/map.ts @@ -27,6 +27,13 @@ export function checkMapProperties(mod: IModKit): ValidationMessageGroup[] { }); }); + if (map.map.version > 1.1) { + mapValidations.messages.push({ + type: 'error', + message: `${map.name} is version >1.1. It must be 1.1 - the json2 format is not supported by the game.`, + }); + } + groups.push(mapValidations); }); diff --git a/src/app/helpers/validators/npc.ts b/src/app/helpers/validators/npc.ts index d727b23..b8dad6e 100644 --- a/src/app/helpers/validators/npc.ts +++ b/src/app/helpers/validators/npc.ts @@ -1,4 +1,12 @@ -import { IModKit, ValidationMessageGroup } from '../../../interfaces'; +import { + IModKit, + INPCDefinition, + ValidationMessage, + ValidationMessageGroup, +} from '../../../interfaces'; +import { formatNPCs } from '../export'; +import { npcSchema } from '../schemas'; +import { validateSchema } from '../schemas/_helpers'; export function checkMapNPCDialogs(mod: IModKit): ValidationMessageGroup { // check npc dialog refs, make sure they exist @@ -123,3 +131,27 @@ export function checkNPCSprites(mod: IModKit) { return npcValidations; } + +export function validateNPCs(mod: IModKit): ValidationMessageGroup { + const itemValidations: ValidationMessageGroup = { + header: 'Invalid NPCs', + messages: [], + }; + + const formattedNPCs = formatNPCs(mod.npcs); + + formattedNPCs.forEach((item) => { + const failures = validateSchema( + item.npcId, + item, + npcSchema + ); + const validationFailures: ValidationMessage[] = failures.map((f) => ({ + type: 'error', + message: f, + })); + itemValidations.messages.push(...validationFailures); + }); + + return itemValidations; +} diff --git a/src/app/helpers/validators/quest.ts b/src/app/helpers/validators/quest.ts index f324181..47c2f14 100644 --- a/src/app/helpers/validators/quest.ts +++ b/src/app/helpers/validators/quest.ts @@ -1,4 +1,11 @@ -import { IModKit, ValidationMessageGroup } from '../../../interfaces'; +import { + IModKit, + IQuest, + ValidationMessage, + ValidationMessageGroup, +} from '../../../interfaces'; +import { questSchema } from '../schemas'; +import { validateSchema } from '../schemas/_helpers'; export function checkQuests(mod: IModKit): ValidationMessageGroup { const itemValidations: ValidationMessageGroup = { @@ -24,3 +31,21 @@ export function checkQuests(mod: IModKit): ValidationMessageGroup { return itemValidations; } + +export function validateQuests(mod: IModKit): ValidationMessageGroup { + const itemValidations: ValidationMessageGroup = { + header: 'Invalid Quests', + messages: [], + }; + + mod.quests.forEach((item) => { + const failures = validateSchema(item.name, item, questSchema); + const validationFailures: ValidationMessage[] = failures.map((f) => ({ + type: 'error', + message: f, + })); + itemValidations.messages.push(...validationFailures); + }); + + return itemValidations; +} diff --git a/src/app/helpers/validators/recipe.ts b/src/app/helpers/validators/recipe.ts index 5711ca4..27ea1da 100644 --- a/src/app/helpers/validators/recipe.ts +++ b/src/app/helpers/validators/recipe.ts @@ -1,4 +1,11 @@ -import { IModKit, ValidationMessageGroup } from '../../../interfaces'; +import { + IModKit, + IRecipe, + ValidationMessage, + ValidationMessageGroup, +} from '../../../interfaces'; +import { recipeSchema } from '../schemas'; +import { validateSchema } from '../schemas/_helpers'; export function checkRecipes(mod: IModKit): ValidationMessageGroup { const itemValidations: ValidationMessageGroup = { @@ -41,3 +48,21 @@ export function checkRecipes(mod: IModKit): ValidationMessageGroup { return itemValidations; } + +export function validateRecipes(mod: IModKit): ValidationMessageGroup { + const itemValidations: ValidationMessageGroup = { + header: 'Invalid Recipes', + messages: [], + }; + + mod.recipes.forEach((item) => { + const failures = validateSchema(item.name, item, recipeSchema); + const validationFailures: ValidationMessage[] = failures.map((f) => ({ + type: 'error', + message: f, + })); + itemValidations.messages.push(...validationFailures); + }); + + return itemValidations; +} diff --git a/src/app/helpers/validators/spawner.ts b/src/app/helpers/validators/spawner.ts index 346c3e4..bdbef85 100644 --- a/src/app/helpers/validators/spawner.ts +++ b/src/app/helpers/validators/spawner.ts @@ -1,4 +1,11 @@ -import { IModKit, ValidationMessageGroup } from '../../../interfaces'; +import { + IModKit, + ISpawnerData, + ValidationMessage, + ValidationMessageGroup, +} from '../../../interfaces'; +import { spawnerSchema } from '../schemas'; +import { validateSchema } from '../schemas/_helpers'; export function checkSpawners(mod: IModKit): ValidationMessageGroup { const itemValidations: ValidationMessageGroup = { @@ -31,3 +38,25 @@ export function checkSpawners(mod: IModKit): ValidationMessageGroup { return itemValidations; } + +export function validateSpawners(mod: IModKit): ValidationMessageGroup { + const itemValidations: ValidationMessageGroup = { + header: 'Invalid Spawners', + messages: [], + }; + + mod.spawners.forEach((item) => { + const failures = validateSchema( + item.tag, + item, + spawnerSchema + ); + const validationFailures: ValidationMessage[] = failures.map((f) => ({ + type: 'error', + message: f, + })); + itemValidations.messages.push(...validationFailures); + }); + + return itemValidations; +} diff --git a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts index 7c5e625..a8dacd3 100644 --- a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts +++ b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts @@ -78,22 +78,22 @@ export class NpcsEditorComponent public statsInOrder = computed(() => { const npc = this.editing(); - return Object.keys(npc.otherStats).sort() as StatType[]; + return Object.keys(npc.otherStats ?? {}).sort() as StatType[]; }); public summonStatsInOrder = computed(() => { const npc = this.editing(); - return Object.keys(npc.summonStatModifiers).sort() as StatType[]; + return Object.keys(npc.summonStatModifiers ?? {}).sort() as StatType[]; }); public summonSkillsInOrder = computed(() => { const npc = this.editing(); - return Object.keys(npc.summonSkillModifiers).sort() as SkillType[]; + return Object.keys(npc.summonSkillModifiers ?? {}).sort() as SkillType[]; }); public traitsInOrder = computed(() => { const npc = this.editing(); - return Object.keys(npc.traitLevels).sort(); + return Object.keys(npc.traitLevels ?? {}).sort(); }); public linkStats = signal(true); diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 3e12748..daf5236 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -12,5 +12,6 @@ export * from './npc'; export * from './npcscript'; export * from './quest'; export * from './recipe'; +export * from './schema'; export * from './spawner'; export * from './validation'; diff --git a/src/interfaces/schema.ts b/src/interfaces/schema.ts new file mode 100644 index 0000000..5740dbd --- /dev/null +++ b/src/interfaces/schema.ts @@ -0,0 +1,3 @@ +export type SchemaProperty = [string, boolean, (value: any) => boolean]; + +export type Schema = SchemaProperty[];