From 6dbc76ff62af0c86891fd91d48e8eafd3894f384 Mon Sep 17 00:00:00 2001 From: Kyle Kemp Date: Sat, 17 Aug 2024 06:06:09 -0500 Subject: [PATCH] closes #43 --- src/app/helpers/constants.ts | 11 +- src/app/helpers/export/npc.ts | 8 +- src/app/helpers/npc.ts | 2 +- src/app/helpers/schemas/_helpers.ts | 207 ++++++++++++++++-- src/app/helpers/schemas/dialog.ts | 29 ++- src/app/helpers/schemas/droptable.ts | 5 +- src/app/helpers/schemas/item.ts | 21 +- src/app/helpers/schemas/npc.ts | 109 +++++++-- src/app/helpers/schemas/quest.ts | 48 +++- src/app/helpers/schemas/recipe.ts | 21 +- src/app/helpers/schemas/spawner.ts | 10 +- src/app/helpers/validators/recipe.ts | 7 + .../items-editor/items-editor.component.ts | 3 +- .../npcs/npcs-editor/npcs-editor.component.ts | 55 ++++- src/interfaces/schema.ts | 11 +- 15 files changed, 468 insertions(+), 79 deletions(-) diff --git a/src/app/helpers/constants.ts b/src/app/helpers/constants.ts index a691d0f..6e0b2a9 100644 --- a/src/app/helpers/constants.ts +++ b/src/app/helpers/constants.ts @@ -256,13 +256,20 @@ export const typePropSets: Record = { export const typePropDefaults: Record< string, - Record> + Record< + string, + | number + | string + | boolean + | Partial + | Array<{ id: string; text: string }> + > > = { Arrow: { shots: 1000, tier: 1, damageClass: 'physical' }, Bottle: { ounces: 1 }, Food: { ounces: 1 }, Gem: {}, - Book: { bookPages: 1, bookItemFilter: '', bookFindablePages: '' }, + Book: { bookPages: [], bookItemFilter: '', bookFindablePages: '' }, Trap: { trapUses: 1 }, Twig: { type: 'Staff' }, }; diff --git a/src/app/helpers/export/npc.ts b/src/app/helpers/export/npc.ts index c433ba5..597acfe 100644 --- a/src/app/helpers/export/npc.ts +++ b/src/app/helpers/export/npc.ts @@ -188,9 +188,11 @@ export function formatNPCs(npcs: INPCDefinition[]): INPCDefinition[] { delete npc.items.equipment[slot]; }); - npc.triggers.leash.messages = npc.triggers.leash.messages.filter(Boolean); - npc.triggers.spawn.messages = npc.triggers.spawn.messages.filter(Boolean); - npc.triggers.leash.messages = npc.triggers.leash.messages.filter(Boolean); + ['leash', 'spawn', 'combat'].forEach((triggerType) => { + if (!npc.triggers?.[triggerType]?.messages) return; + npc.triggers[triggerType].messages = + npc.triggers[triggerType].messages.filter(Boolean); + }); return fillInNPCProperties(npc as INPCDefinition); }); diff --git a/src/app/helpers/npc.ts b/src/app/helpers/npc.ts index 8ad092f..604fa75 100644 --- a/src/app/helpers/npc.ts +++ b/src/app/helpers/npc.ts @@ -61,7 +61,7 @@ export const defaultNPC: () => INPCDefinition = () => ({ leash: { messages: [''], sfx: { - name: '', + name: undefined as unknown as string, maxChance: 0, }, }, diff --git a/src/app/helpers/schemas/_helpers.ts b/src/app/helpers/schemas/_helpers.ts index f4cdf59..f0c9eed 100644 --- a/src/app/helpers/schemas/_helpers.ts +++ b/src/app/helpers/schemas/_helpers.ts @@ -1,8 +1,175 @@ -import { difference, get, isNumber, isUndefined } from 'lodash'; -import { HasIdentification, Schema } from '../../../interfaces'; +import { difference, get, isNumber, isString, isUndefined } from 'lodash'; +import { + Allegiance, + HasIdentification, + ItemSlot, + QuestRewardType, + Schema, + SchemaValidator, + SchemaValidatorMessage, + Skill, + Stat, +} from '../../../interfaces'; -export function isInteger(num: any): boolean { - return isNumber(num) && Math.floor(num) === num; +export function isArrayOf(validator: SchemaValidator): SchemaValidator { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return (val: any) => val.every(validator); +} + +export function isArrayOfAtLeastLength(length: number): SchemaValidator { + return (val: any) => val.filter(Boolean).length >= length; +} + +export function isArrayOfAtMostLength(length: number): SchemaValidator { + return (val: any) => val.filter(Boolean).length <= length; +} + +export function isObjectWith(keys: string[]): SchemaValidator { + return (val: any) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (Object.keys(val).length !== keys.length) return false; + return keys.every((k) => !isUndefined(val[k])); + }; +} + +export function isObjectWithFailure(keys: string[]): SchemaValidatorMessage { + return (val: any) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (Object.keys(val).length !== keys.length) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return `keys: ${keys.join(', ')} vs ${Object.keys(val).join( + ', ' + )} is of different length`; + return `keys: ${keys + .filter((k) => isUndefined(val[k])) + .join(', ')} must not be undefined`; + }; +} + +export function isObjectWithSome(keys: string[]): SchemaValidator { + return (val: any) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Object.keys(val).every((k) => keys.includes(k)); + }; +} + +export function isObjectWithSomeFailure( + keys: string[] +): SchemaValidatorMessage { + return (val: any) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return `extraneous keys: ${Object.keys(val) + .map((k) => (keys.includes(k) ? '' : k)) + .filter(Boolean) + .join(', ')}`; + }; +} + +export function isPartialObjectOf(possibleVals: T[]): SchemaValidator { + return (val: any) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.keys(val).every((k) => possibleVals.includes(k as T)); +} + +export function isPartialObjectOfFailure( + possibleVals: T[] +): SchemaValidatorMessage { + return (val: any) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + `extraneous keys: ${Object.keys(val) + .map((k) => (possibleVals.includes(k as T) ? '' : k)) + .filter(Boolean) + .join(', ')}`; +} + +export const isPartialStatObject = isPartialObjectOf(Object.values(Stat)); +export const isPartialStatObjectFailure = isPartialObjectOfFailure( + Object.values(Stat) +); + +export const isPartialSkillObject = isPartialObjectOf( + Object.values(Skill) +); +export const isPartialSkillObjectFailure = isPartialObjectOfFailure( + Object.values(Skill) +); + +export const isPartialEquipmentObject = isPartialObjectOf( + Object.values(ItemSlot) +); +export const isPartialEquipmentObjectFailure = + isPartialObjectOfFailure(Object.values(ItemSlot)); + +export const isPartialReputationObject = isPartialObjectOf( + Object.values(Allegiance) +); +export const isPartialReputationObjectFailure = + isPartialObjectOfFailure(Object.values(Allegiance)); + +export function isItemSlot(val: any): boolean { + return Object.values(ItemSlot).includes(val as ItemSlot); +} + +export function isTraitObject(val: any): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Object.keys(val).every((k) => isInteger(val[k])); +} + +export function isStat(val: any): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Object.values(Stat).includes(val); +} + +export function isAllegiance(val: any): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Object.values(Allegiance).includes(val); +} + +export function isRepMod(val: any): boolean { + return isInteger(val.delta) && isAllegiance(val.allegiance); +} + +export function isRandomStatObject(val: any): boolean { + const allStats = Object.values(Stat); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Object.keys(val).every( + (stat) => + allStats.includes(stat as Stat) && + isObjectWith(['min', 'max'])(val[stat]) && + isInteger(val[stat].min) && + isInteger(val[stat].max) + ); +} + +export function isRandomTraitObject(val: any): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return ( + isObjectWith(['name', 'level'])(val) && + isArrayOf(isString)(val.name) && + isRandomNumber(val.level) + ); +} + +export function isNPCEffect(val: any): boolean { + return ( + val.endsAt === -1 && + isString(val.name) && + isObjectWithSome(['potency', 'damageType', 'enrageTimer'])(val.extra) + ); +} + +export function isQuestReward(val: any): boolean { + return ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.values(QuestRewardType).includes(val.type) && + isInteger(val.value) && + (val.statName ? isStat(val.statName) || isAllegiance(val.statName) : true) + ); +} + +export function isOzIngredient(val: any): boolean { + return isString(val.filter) && isString(val.display) && isInteger(val.ounces); } export function isCosmetic(cos: any): boolean { @@ -17,21 +184,19 @@ 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); + return !!(rol.chance && rol.result); } export function isTrait(trait: any): boolean { return !!(trait.name && isNumber(trait.level)); } -export function isIntegerBetween(min: any, max: any): (val: any) => boolean { +export function isInteger(num: any): boolean { + return isNumber(num) && Math.floor(num) === num; +} + +export function isIntegerBetween(min: any, max: any): SchemaValidator { return (num) => num >= min && num <= max && isInteger(num); } @@ -43,16 +208,24 @@ export function isEncrust(enc: any): boolean { return !!(enc.stats || enc.equipEffect || enc.strikeEffect); } +export function isBookPage(val: any): boolean { + return val.id === '' && isString(val.text); +} + export function isDropPool(pool: any): boolean { return ( isInteger(pool.choose.min) && isInteger(pool.choose.max) && - isRollable(pool.items) + isArrayOf(isRollable)(pool.items) ); } export function isRandomNumber(num: any) { - return isInteger(num.min) && isInteger(num.max); + return ( + isObjectWith(['min', 'max'])(num) && + isInteger(num.min) && + isInteger(num.max) + ); } export function validateSchema( @@ -62,7 +235,7 @@ export function validateSchema( ): string[] { const errors: string[] = []; - schema.forEach(([prop, required, validator]) => { + schema.forEach(([prop, required, validator, message]) => { const value = get(obj, prop); if (isUndefined(value)) { @@ -75,7 +248,9 @@ export function validateSchema( if (!validator(value)) errors.push( - `Property '${prop}' does not pass validation for '${label}'.` + `Property '${prop}' does not pass validation for '${label}' (${ + message?.(value) ?? 'no additional information provided' + }).` ); }); diff --git a/src/app/helpers/schemas/dialog.ts b/src/app/helpers/schemas/dialog.ts index 82a67c6..063475a 100644 --- a/src/app/helpers/schemas/dialog.ts +++ b/src/app/helpers/schemas/dialog.ts @@ -1,6 +1,14 @@ -import { isArray, isNumber, isObject, isString } from 'lodash'; +import { isNumber, isObject, isString } from 'lodash'; import { Schema } from '../../../interfaces'; -import { isRandomNumber } from './_helpers'; +import { + isArrayOf, + isObjectWith, + isPartialEquipmentObject, + isPartialEquipmentObjectFailure, + isPartialStatObject, + isPartialStatObjectFailure, + isRandomNumber, +} from './_helpers'; export const dialogSchema: Schema = [ ['tag', true, isString], @@ -14,10 +22,15 @@ export const dialogSchema: Schema = [ ['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], + ['otherStats', false, isPartialStatObject, isPartialStatObjectFailure], + ['usableSkills', false, isArrayOf(isString)], + ['items', false, isObjectWith(['equipment'])], + [ + 'items.equipment', + false, + isPartialEquipmentObject, + isPartialEquipmentObjectFailure, + ], + ['dialog', false, isObjectWith(['keyword'])], + ['behaviors', false, isArrayOf(isObject)], ]; diff --git a/src/app/helpers/schemas/droptable.ts b/src/app/helpers/schemas/droptable.ts index b9c6290..f1b7d61 100644 --- a/src/app/helpers/schemas/droptable.ts +++ b/src/app/helpers/schemas/droptable.ts @@ -1,9 +1,10 @@ -import { isArray, isBoolean, isString } from 'lodash'; +import { isBoolean, isString } from 'lodash'; import { Schema } from '../../../interfaces'; +import { isArrayOf, isRollable } from './_helpers'; export const droptableSchema: Schema = [ ['mapName', false, isString], ['regionName', false, isString], ['isGlobal', false, isBoolean], - ['drops', true, isArray], + ['drops', true, isArrayOf(isRollable)], ]; diff --git a/src/app/helpers/schemas/item.ts b/src/app/helpers/schemas/item.ts index 32189a6..5719a50 100644 --- a/src/app/helpers/schemas/item.ts +++ b/src/app/helpers/schemas/item.ts @@ -1,10 +1,16 @@ -import { isArray, isBoolean, isInteger, isObject, isString } from 'lodash'; +import { isBoolean, isInteger, isString } from 'lodash'; import { Schema } from '../../../interfaces'; import { + isArrayOf, + isBookPage, isCosmetic, isEffect, isEncrust, isIntegerBetween, + isPartialStatObject, + isPartialStatObjectFailure, + isRandomStatObject, + isRandomTraitObject, isRequirement, isRollable, isSuccor, @@ -27,7 +33,7 @@ export const itemSchema: Schema = [ ['isSackable', false, isBoolean], ['isHeavy', false, isBoolean], ['secondaryType', false, isString], - ['stats', false, isObject], + ['stats', false, isPartialStatObject, isPartialStatObjectFailure], ['maxUpgrades', false, isInteger], ['canUpgradeWith', false, isBoolean], ['recipe', false, isString], @@ -68,14 +74,13 @@ export const itemSchema: Schema = [ ['trapEffect.potency', false, isInteger], ['trapEffect.duration', false, isInteger], ['trapEffect.uses', false, isInteger], + ['trapEffect.isPositive', false, isBoolean], ['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], @@ -92,7 +97,7 @@ export const itemSchema: Schema = [ ['trapUses', false, isInteger], - ['containedItems', false, isRollable], + ['containedItems', false, isArrayOf(isRollable)], ['succorInfo', false, isSuccor], ['succorInfo.map', false, isString], @@ -107,7 +112,7 @@ export const itemSchema: Schema = [ ['bookItemFilter', false, isString], ['bookPage', false, isInteger], ['bookCurrentPage', false, isInteger], - ['bookPages', false, isArray], + ['bookPages', false, isArrayOf(isBookPage)], ['ounces', false, isInteger], ['notUsableAfterHours', false, isInteger], @@ -115,6 +120,6 @@ export const itemSchema: Schema = [ ['quality', false, isInteger], ['sellValue', false, isInteger], - ['randomStats', false, isObject], - ['randomTrait', false, isObject], + ['randomStats', false, isRandomStatObject], + ['randomTrait', false, isRandomTraitObject], ]; diff --git a/src/app/helpers/schemas/npc.ts b/src/app/helpers/schemas/npc.ts index 3cebcb7..35f4ddc 100644 --- a/src/app/helpers/schemas/npc.ts +++ b/src/app/helpers/schemas/npc.ts @@ -1,15 +1,54 @@ -import { isArray, isBoolean, isInteger, isObject, isString } from 'lodash'; -import { Schema } from '../../../interfaces'; +import { isBoolean, isInteger, isNumber, isString } from 'lodash'; +import { ItemSlot, Schema, SchemaProperty } from '../../../interfaces'; import { + isArrayOf, isDropPool, - isOptionalRollable, + isNPCEffect, + isObjectWith, + isObjectWithFailure, + isObjectWithSome, + isObjectWithSomeFailure, + isPartialEquipmentObject, + isPartialEquipmentObjectFailure, + isPartialReputationObject, + isPartialReputationObjectFailure, + isPartialSkillObject, + isPartialSkillObjectFailure, + isPartialStatObject, + isPartialStatObjectFailure, isRandomNumber, + isRepMod, isRollable, + isTraitObject, } from './_helpers'; +const equipmentValidators: SchemaProperty[] = Object.values(ItemSlot).map( + (slot) => [`items.equipment.${slot}`, false, isArrayOf(isRollable)] +); + +const triggerValidators: SchemaProperty[] = ['leash', 'spawn', 'combat'] + .map((triggerType) => [ + [ + `triggers.${triggerType}`, + false, + isObjectWithSome(['messages', 'sfx']), + isObjectWithSomeFailure(['messages', 'sfx']), + ], + [`triggers.${triggerType}.messages`, false, isArrayOf(isString)], + [ + `triggers.${triggerType}.sfx`, + false, + isObjectWith(['name', 'maxChance']), + isObjectWithFailure(['name', 'maxChance']), + ], + [`triggers.${triggerType}.sfx.name`, false, isString], + [`triggers.${triggerType}.sfx.maxChance`, false, isNumber], + ]) + .flat() as SchemaProperty[]; + export const npcSchema: Schema = [ ['npcId', true, isString], - ['sprite', true, isArray], + ['sprite', true, isArrayOf(isInteger)], ['cr', true, isInteger], ['hp', true, isRandomNumber], ['mp', false, isRandomNumber], @@ -17,22 +56,35 @@ export const npcSchema: Schema = [ ['gold', true, isRandomNumber], ['skillOnKill', true, isInteger], - ['name', false, isArray], + ['name', false, isArrayOf(isString)], ['alignment', false, isString], ['affiliation', false, isString], ['allegiance', false, isString], - ['allegianceReputation', false, isObject], + [ + 'allegianceReputation', + false, + isPartialReputationObject, + isPartialReputationObjectFailure, + ], ['aquaticOnly', false, isBoolean], ['avoidWater', false, isBoolean], ['baseClass', false, isString], - ['baseEffects', false, isArray], - ['copyDrops', false, isRollable], + ['baseEffects', false, isArrayOf(isNPCEffect)], + ['copyDrops', false, isArrayOf(isRollable)], ['dropPool', false, isDropPool], - ['drops', false, isRollable], + ['drops', false, isArrayOf(isRollable)], ['forceAI', false, isString], - ['items', false, isObject], - ['items.equipment', false, isObject], - ['items.sack', false, isOptionalRollable], + ['items', false, isObjectWith(['equipment', 'sack', 'belt'])], + [ + 'items.equipment', + false, + isPartialEquipmentObject, + isPartialEquipmentObjectFailure, + ], + ...equipmentValidators, + + ['items.sack', false, isArrayOf(isRollable)], + ['items.belt', false, isArrayOf(isRollable)], ['level', true, isInteger], ['hpMult', false, isInteger], @@ -42,18 +94,35 @@ export const npcSchema: Schema = [ ['hostility', false, isString], ['noCorpseDrop', false, isBoolean], ['noItemDrop', false, isBoolean], - ['repMod', false, isArray], + ['repMod', false, isArrayOf(isRepMod)], - ['stats', true, isObject], - ['skills', true, isObject], + ['stats', true, isPartialStatObject, isPartialStatObjectFailure], + ['skills', true, isPartialSkillObject, isPartialSkillObjectFailure], - ['summonStatModifiers', false, isObject], - ['summonSkillModifiers', false, isObject], + [ + 'summonStatModifiers', + false, + isPartialStatObject, + isPartialStatObjectFailure, + ], + [ + 'summonSkillModifiers', + false, + isPartialSkillObject, + isPartialSkillObjectFailure, + ], ['tanSkillRequired', false, isInteger], ['tansFor', false, isString], - ['traitLevels', false, isObject], - ['triggers', false, isObject], + ['traitLevels', false, isTraitObject], + [ + 'triggers', + false, + isObjectWithSome(['leash', 'spawn', 'combat']), + isObjectWithSomeFailure(['leash', 'spawn', 'combat']), + ], + + ...triggerValidators, - ['usableSkills', false, isArray], + ['usableSkills', false, isArrayOf(isRollable)], ]; diff --git a/src/app/helpers/schemas/quest.ts b/src/app/helpers/schemas/quest.ts index b9336f5..57fd401 100644 --- a/src/app/helpers/schemas/quest.ts +++ b/src/app/helpers/schemas/quest.ts @@ -1,5 +1,25 @@ -import { isArray, isBoolean, isNumber, isObject, isString } from 'lodash'; +import { isBoolean, isNumber, isString } from 'lodash'; import { Schema } from '../../../interfaces'; +import { + isArrayOf, + isItemSlot, + isObjectWith, + isObjectWithSome, + isObjectWithSomeFailure, + isQuestReward, +} from './_helpers'; + +const requirementKeys = [ + 'type', + 'npcIds', + 'item', + 'fromHands', + 'fromSack', + 'killsRequired', + 'countRequired', + 'itemsRequired', + 'slot', +]; export const questSchema: Schema = [ ['name', true, isString], @@ -9,22 +29,38 @@ export const questSchema: Schema = [ ['isDaily', false, isBoolean], ['isRepeatable', false, isBoolean], - ['messages', false, isObject], + [ + 'messages', + false, + isObjectWith([ + 'kill', + 'complete', + 'incomplete', + 'alreadyHas', + 'permComplete', + ]), + ], ['messages.kill', false, isString], ['messages.complete', false, isString], ['messages.incomplete', false, isString], ['messages.alreadyHas', false, isString], ['messages.permComplete', false, isString], - ['requirements', false, isObject], + [ + 'requirements', + false, + isObjectWithSome(requirementKeys), + isObjectWithSomeFailure(requirementKeys), + ], + ['requirements.slot', false, isArrayOf(isItemSlot)], ['requirements.type', false, isString], - ['requirements.npcIds', false, isArray], + ['requirements.npcIds', false, isArrayOf(isString)], ['requirements.item', false, isString], ['requirements.fromHands', false, isBoolean], ['requirements.fromSack', false, isBoolean], ['requirements.killsRequired', false, isNumber], ['requirements.countRequired', false, isNumber], - ['requirements.itemssRequired', false, isNumber], + ['requirements.itemsRequired', false, isNumber], - ['rewards', false, isArray], + ['rewards', false, isArrayOf(isQuestReward)], ]; diff --git a/src/app/helpers/schemas/recipe.ts b/src/app/helpers/schemas/recipe.ts index 1f785b1..e29b703 100644 --- a/src/app/helpers/schemas/recipe.ts +++ b/src/app/helpers/schemas/recipe.ts @@ -1,5 +1,6 @@ -import { isArray, isBoolean, isNumber, isString } from 'lodash'; +import { isBoolean, isNumber, isString } from 'lodash'; import { Schema } from '../../../interfaces'; +import { isArrayOf, isArrayOfAtMostLength, isOzIngredient } from './_helpers'; export const recipeSchema: Schema = [ ['category', true, isString], @@ -12,10 +13,22 @@ export const recipeSchema: Schema = [ ['skillGained', false, isNumber], ['xpGained', false, isNumber], ['copySkillToPotency', false, isBoolean], - ['ingredients', false, isArray], - ['ozIngredients', false, isArray], + ['ingredients', false, isArrayOf(isString)], + [ + 'ingredients', + false, + isArrayOfAtMostLength(8), + () => 'ingredients must not have more than 8 elements', + ], + ['ozIngredients', false, isArrayOf(isOzIngredient)], + [ + 'ozIngredients', + false, + isArrayOfAtMostLength(2), + () => 'ozIngredients must not have more than 2 elements', + ], ['potencyScalar', false, isNumber], - ['requireClass', false, isArray], + ['requireClass', false, isArrayOf(isString)], ['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 index 51c8ee2..b9564b3 100644 --- a/src/app/helpers/schemas/spawner.ts +++ b/src/app/helpers/schemas/spawner.ts @@ -1,12 +1,12 @@ -import { isArray, isBoolean, isNumber, isString } from 'lodash'; +import { isBoolean, isNumber, isString } from 'lodash'; import { Schema } from '../../../interfaces'; -import { isRollable } from './_helpers'; +import { isArrayOf, isRollable } from './_helpers'; export const spawnerSchema: Schema = [ - ['npcIds', true, isRollable], + ['npcIds', true, isArrayOf(isRollable)], ['tag', true, isString], - ['paths', false, isArray], + ['paths', false, isArrayOf(isString)], ['respawnRate', false, isNumber], ['initialSpawn', false, isNumber], ['maxCreatures', false, isNumber], @@ -30,7 +30,7 @@ export const spawnerSchema: Schema = [ ['doInitialSpawnImmediately', false, isBoolean], ['attributeAddChance', false, isNumber], ['eliteTickCap', false, isNumber], - ['npcAISettings', false, isArray], + ['npcAISettings', false, isArrayOf(isString)], ['respectKnowledge', false, isBoolean], ['isDangerous', false, isBoolean], diff --git a/src/app/helpers/validators/recipe.ts b/src/app/helpers/validators/recipe.ts index 0e74213..856bb23 100644 --- a/src/app/helpers/validators/recipe.ts +++ b/src/app/helpers/validators/recipe.ts @@ -38,6 +38,13 @@ export function checkRecipes(mod: IModKit): ValidationMessageGroup { message: `Recipe ${recipe.name} (${recipe.category}) has copySkillToPotency set, but no potency scalar.`, }); } + + if (!recipe.ingredients?.length && !recipe.ozIngredients?.length) { + itemValidations.messages.push({ + type: 'error', + message: `Recipe ${recipe.name} (${recipe.category}) has no ingredients or ozIngredients.`, + }); + } }); return itemValidations; diff --git a/src/app/tabs/items/items-editor/items-editor.component.ts b/src/app/tabs/items/items-editor/items-editor.component.ts index 1c18cdb..5f27a06 100644 --- a/src/app/tabs/items/items-editor/items-editor.component.ts +++ b/src/app/tabs/items/items-editor/items-editor.component.ts @@ -55,6 +55,7 @@ export class ItemsEditorComponent | 'succorInfo' | 'containedItems' | 'recipe' + | 'bookPages' > > = { tier: 'number', @@ -68,7 +69,7 @@ export class ItemsEditorComponent canShoot: 'boolean', ounces: 'number', bookPage: 'number', - bookPages: 'number', + bookPages: 'bookPages', bookFindablePages: 'number', bookItemFilter: 'string', trapUses: 'number', 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 a8dacd3..1e3d31c 100644 --- a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts +++ b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts @@ -138,6 +138,32 @@ export class NpcsEditorComponent delta: reps.find((r) => r.allegiance === allegiance)?.delta ?? 0, })); + if (!npc.triggers) (npc as any).triggers = {}; + + ['leash', 'spawn', 'combat'].forEach((type) => { + const triggerType = type as keyof INPCDefinition['triggers']; + + npc.triggers[triggerType] ??= { + messages: [''], + sfx: { + name: undefined as unknown as string, + maxChance: 0, + }, + }; + + npc.triggers[triggerType].messages ??= ['']; + + npc.triggers[triggerType].sfx ??= { + name: undefined as unknown as string, + maxChance: 0, + }; + + if (type === 'combat') { + npc.triggers[triggerType].messages = + npc.triggers[triggerType].messages.filter(Boolean); + } + }); + this.editing.set(npc); super.ngOnInit(); @@ -265,6 +291,7 @@ export class NpcsEditorComponent extra: { potency: 1, damageType: undefined, + enrageTimer: undefined, }, }); this.editing.set(npc); @@ -424,10 +451,34 @@ export class NpcsEditorComponent npc.copyDrops = npc.copyDrops.filter((d) => d.result); npc.dropPool.items = npc.dropPool.items.filter((d) => d.result); - npc.triggers.combat.messages = npc.triggers.combat.messages.filter(Boolean); - npc.repMod = npc.repMod.filter((r) => r.delta); + ['leash', 'spawn', 'combat'].forEach((type) => { + const triggerType = type as keyof INPCDefinition['triggers']; + + npc.triggers[triggerType].messages = + npc.triggers[triggerType].messages.filter(Boolean); + + if (!npc.triggers[triggerType].sfx.name) { + delete (npc.triggers[triggerType] as any).sfx; + } + + if (!npc.triggers[triggerType].messages.length) { + delete (npc.triggers[triggerType] as any).messages; + } + + if ( + !npc.triggers[triggerType].messages && + !npc.triggers[triggerType].sfx + ) { + delete (npc.triggers as any)[triggerType]; + } + }); + + if (Object.keys(npc.triggers ?? {}).length === 0) { + delete (npc as any).triggers; + } + this.editing.set(npc); super.doSave(); diff --git a/src/interfaces/schema.ts b/src/interfaces/schema.ts index 5740dbd..1487b63 100644 --- a/src/interfaces/schema.ts +++ b/src/interfaces/schema.ts @@ -1,3 +1,12 @@ -export type SchemaProperty = [string, boolean, (value: any) => boolean]; +export type SchemaValidator = (value: any) => boolean; + +export type SchemaValidatorMessage = (value: any) => string; + +export type SchemaProperty = [ + string, + boolean, + SchemaValidator, + SchemaValidatorMessage? +]; export type Schema = SchemaProperty[];