From 6a25a935ded14d85e691730e0ef9e6a35de6054c Mon Sep 17 00:00:00 2001 From: Kyle Kemp Date: Sat, 17 Aug 2024 12:03:46 -0500 Subject: [PATCH] update dialogs and item validation --- src/app/helpers/dialog.ts | 3 ++ src/app/helpers/export/npc.ts | 18 ++++---- src/app/helpers/schemas/_helpers.ts | 29 +++++++------ src/app/helpers/schemas/dialog.ts | 8 +++- src/app/helpers/validators/item.ts | 16 +++---- src/app/helpers/validators/npc.ts | 4 +- src/app/helpers/validators/spawner.ts | 2 +- src/app/services/electron.service.ts | 5 ++- src/app/shared/shared.module.ts | 3 ++ .../dialogs-editor.component.html | 25 ++++++++++- .../dialogs-editor.component.ts | 28 +++++++++++-- .../npcs-editor/npcs-editor.component.html | 42 +------------------ src/interfaces/npc.ts | 20 +++++---- src/interfaces/npcscript.ts | 6 +++ 14 files changed, 118 insertions(+), 91 deletions(-) diff --git a/src/app/helpers/dialog.ts b/src/app/helpers/dialog.ts index 2258e2b..4b20201 100644 --- a/src/app/helpers/dialog.ts +++ b/src/app/helpers/dialog.ts @@ -20,6 +20,8 @@ export const defaultNPCScript: () => INPCScript = () => ({ }, otherStats: {}, usableSkills: [], + maxWanderRandomlyDistance: 0, + noLeash: false, items: { equipment: { rightHand: undefined as unknown as string, @@ -44,5 +46,6 @@ export const defaultNPCScript: () => INPCScript = () => ({ dialog: { keyword: {}, }, + baseEffects: [], behaviors: [], }); diff --git a/src/app/helpers/export/npc.ts b/src/app/helpers/export/npc.ts index 597acfe..7825c34 100644 --- a/src/app/helpers/export/npc.ts +++ b/src/app/helpers/export/npc.ts @@ -179,14 +179,16 @@ export function formatNPCs(npcs: INPCDefinition[]): INPCDefinition[] { npc.repMod = npc.repMod.filter((rep: any) => rep.delta !== 0); - if (npc.drops.length === 0) delete npc.drops; - if (npc.copyDrops.length === 0) delete npc.copyDrops; - if (npc.dropPool.items.length === 0) delete npc.dropPool; - - Object.values(ItemSlot).forEach((slot) => { - if (npc.items.equipment[slot]?.length > 0) return; - delete npc.items.equipment[slot]; - }); + if (!npc.drops || npc.drops.length === 0) delete npc.drops; + if (!npc.copyDrops || npc.copyDrops.length === 0) delete npc.copyDrops; + if (!npc.dropPool || npc.dropPool.items.length === 0) delete npc.dropPool; + + if (npc.items?.equipment) { + Object.values(ItemSlot).forEach((slot) => { + if (npc.items.equipment[slot]?.length > 0) return; + delete npc.items.equipment[slot]; + }); + } ['leash', 'spawn', 'combat'].forEach((triggerType) => { if (!npc.triggers?.[triggerType]?.messages) return; diff --git a/src/app/helpers/schemas/_helpers.ts b/src/app/helpers/schemas/_helpers.ts index 9d3d944..9a77067 100644 --- a/src/app/helpers/schemas/_helpers.ts +++ b/src/app/helpers/schemas/_helpers.ts @@ -11,6 +11,15 @@ import { Stat, } from '../../../interfaces'; +const itemSlots = [ + 'weapon', + 'armor', + 'shield', + 'ring', + 'robe', + ...Object.values(ItemSlot), +] as ItemSlot[]; + export function isArrayOf(validator: SchemaValidator): SchemaValidator { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return (val: any) => val.every(validator); @@ -49,7 +58,7 @@ export function isObjectWithFailure(keys: string[]): SchemaValidatorMessage { 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)); + return Object.keys(val ?? {}).every((k) => keys.includes(k)); }; } @@ -68,7 +77,7 @@ export function isObjectWithSomeFailure( 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)); + Object.keys(val ?? {}).every((k) => possibleVals.includes(k as T)); } export function isPartialObjectOfFailure( @@ -94,17 +103,9 @@ export const isPartialSkillObjectFailure = isPartialObjectOfFailure( Object.values(Skill) ); -export const isPartialEquipmentObject = isPartialObjectOf([ - 'weapon', - 'armor', - ...Object.values(ItemSlot), -] as ItemSlot[]); +export const isPartialEquipmentObject = isPartialObjectOf(itemSlots); export const isPartialEquipmentObjectFailure = - isPartialObjectOfFailure([ - 'weapon', - 'armor', - ...Object.values(ItemSlot), - ] as ItemSlot[]); + isPartialObjectOfFailure(itemSlots); export const isPartialReputationObject = isPartialObjectOf( Object.values(Allegiance) @@ -113,9 +114,7 @@ export const isPartialReputationObjectFailure = isPartialObjectOfFailure(Object.values(Allegiance)); export function isItemSlot(val: any): boolean { - return ( - ['weapon', 'armor', ...Object.values(ItemSlot)] as ItemSlot[] - ).includes(val as ItemSlot); + return itemSlots.includes(val as ItemSlot); } export function isTraitObject(val: any): boolean { diff --git a/src/app/helpers/schemas/dialog.ts b/src/app/helpers/schemas/dialog.ts index 063475a..bfc0a0a 100644 --- a/src/app/helpers/schemas/dialog.ts +++ b/src/app/helpers/schemas/dialog.ts @@ -1,7 +1,8 @@ -import { isNumber, isObject, isString } from 'lodash'; +import { isBoolean, isNumber, isObject, isString } from 'lodash'; import { Schema } from '../../../interfaces'; import { isArrayOf, + isNPCEffect, isObjectWith, isPartialEquipmentObject, isPartialEquipmentObjectFailure, @@ -15,6 +16,7 @@ export const dialogSchema: Schema = [ ['name', false, isString], ['affiliation', false, isString], + ['forceAI', false, isString], ['allegiance', false, isString], ['alignment', false, isString], ['hostility', false, isString], @@ -22,6 +24,9 @@ export const dialogSchema: Schema = [ ['hp', false, isRandomNumber], ['mp', false, isRandomNumber], + ['noLeash', false, isBoolean], + ['maxWanderRandomlyDistance', false, isNumber], + ['otherStats', false, isPartialStatObject, isPartialStatObjectFailure], ['usableSkills', false, isArrayOf(isString)], ['items', false, isObjectWith(['equipment'])], @@ -33,4 +38,5 @@ export const dialogSchema: Schema = [ ], ['dialog', false, isObjectWith(['keyword'])], ['behaviors', false, isArrayOf(isObject)], + ['baseEffects', false, isArrayOf(isNPCEffect)], ]; diff --git a/src/app/helpers/validators/item.ts b/src/app/helpers/validators/item.ts index f7fa606..54690d1 100644 --- a/src/app/helpers/validators/item.ts +++ b/src/app/helpers/validators/item.ts @@ -73,11 +73,11 @@ export function checkItemUses(mod: IModKit): ValidationMessageGroup { }); mod.npcs.forEach((npc) => { - npc.items.sack.forEach((item) => { + npc.items?.sack?.forEach((item) => { addItemCount(item.result); }); - Object.keys(npc.items.equipment || {}).forEach((slot) => { + Object.keys(npc.items?.equipment ?? {}).forEach((slot) => { (npc.items.equipment[slot as ItemSlotType] || []).forEach((item) => { addItemCount(item.result); }); @@ -87,11 +87,11 @@ export function checkItemUses(mod: IModKit): ValidationMessageGroup { addItemCount(npc.tansFor); } - npc.drops.forEach((item) => { + npc.drops?.forEach((item) => { addItemCount(item.result); }); - npc.dropPool.items.forEach((item) => { + npc.dropPool?.items.forEach((item) => { addItemCount(item.result); }); }); @@ -153,7 +153,7 @@ export function nonexistentItems(mod: IModKit): ValidationMessageGroup { }); mod.npcs.forEach((npc) => { - npc.items.sack.forEach((checkRollable) => { + npc.items?.sack?.forEach((checkRollable) => { if (allItemNames[checkRollable.result]) return; itemValidations.messages.push({ @@ -162,7 +162,7 @@ export function nonexistentItems(mod: IModKit): ValidationMessageGroup { }); }); - Object.keys(npc.items.equipment).forEach((itemslot) => { + Object.keys(npc.items?.equipment ?? {}).forEach((itemslot) => { npc.items.equipment[itemslot as ItemSlotType]?.forEach( (checkRollable) => { if (allItemNames[checkRollable.result]) return; @@ -175,7 +175,7 @@ export function nonexistentItems(mod: IModKit): ValidationMessageGroup { ); }); - npc.drops.forEach((checkRollable) => { + npc.drops?.forEach((checkRollable) => { if (allItemNames[checkRollable.result]) return; itemValidations.messages.push({ @@ -184,7 +184,7 @@ export function nonexistentItems(mod: IModKit): ValidationMessageGroup { }); }); - npc.dropPool.items.forEach((checkRollable) => { + npc.dropPool?.items?.forEach((checkRollable) => { if (allItemNames[checkRollable.result]) return; itemValidations.messages.push({ diff --git a/src/app/helpers/validators/npc.ts b/src/app/helpers/validators/npc.ts index 2f3cf45..37528b8 100644 --- a/src/app/helpers/validators/npc.ts +++ b/src/app/helpers/validators/npc.ts @@ -35,7 +35,7 @@ export function checkNPCUsages(mod: IModKit) { }); mod.quests.forEach((quest) => { - quest.requirements.npcIds.forEach((npcId) => { + quest.requirements.npcIds?.forEach((npcId) => { addItemCount(npcId); }); }); @@ -131,7 +131,7 @@ export function nonexistentNPCs(mod: IModKit): ValidationMessageGroup { }); mod.quests.forEach((quest) => { - quest.requirements.npcIds.forEach((npcId) => { + quest.requirements.npcIds?.forEach((npcId) => { if (allNPCIds[npcId]) return; itemValidations.messages.push({ diff --git a/src/app/helpers/validators/spawner.ts b/src/app/helpers/validators/spawner.ts index f51be84..60c9aff 100644 --- a/src/app/helpers/validators/spawner.ts +++ b/src/app/helpers/validators/spawner.ts @@ -21,7 +21,7 @@ export function checkSpawners(mod: IModKit): ValidationMessageGroup { }); } - if (!spawner.npcAISettings.includes('default')) { + if (!spawner.npcAISettings?.includes('default')) { itemValidations.messages.push({ type: 'warning', message: `Spawner ${spawner.tag} does not have the default AI setting.`, diff --git a/src/app/services/electron.service.ts b/src/app/services/electron.service.ts index fe8d497..eeca660 100644 --- a/src/app/services/electron.service.ts +++ b/src/app/services/electron.service.ts @@ -80,8 +80,9 @@ export class ElectronService { }); // the mod has no backup, which means it was a clean export. it might need some reformatting to get it back in - window.api.receive('loadmod', () => { - // this.modService.updateMod(mod); + window.api.receive('loadmod', (mod: IModKit) => { + const importedMod = importMod(mod); + this.modService.updateMod(importedMod); }); // import the mod raw from the backup. diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 2111746..2572019 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -49,6 +49,7 @@ import { SpriteWithInlineNameComponent } from './components/sprite-with-inline-n import { SpriteComponent } from './components/sprite/sprite.component'; import { TestViewComponent } from './components/test-view/test-view.component'; import { WebviewDirective } from './directives/'; +import { EditBaseeffectComponent } from './components/edit-baseeffect/edit-baseeffect.component'; @NgModule({ declarations: [ @@ -93,6 +94,7 @@ import { WebviewDirective } from './directives/'; InputQuestComponent, InputItemslotEncrustComponent, TestViewComponent, + EditBaseeffectComponent, ], imports: [ CommonModule, @@ -146,6 +148,7 @@ import { WebviewDirective } from './directives/'; InputQuestComponent, InputItemslotEncrustComponent, TestViewComponent, + EditBaseeffectComponent, ], }) export class SharedModule {} diff --git a/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.html b/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.html index 05b4e5e..734a5de 100644 --- a/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.html +++ b/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.html @@ -83,6 +83,29 @@
+ +
+ Max Randomly Wander Distance + +
+ +
+ +
+ +
+ +
+ + @for(baseEffect of editingData.baseEffects; track $index) { + + }
@@ -195,4 +218,4 @@ {{ editingData | json }} - + \ No newline at end of file diff --git a/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.ts b/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.ts index 515be00..709f48c 100644 --- a/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.ts +++ b/src/app/tabs/dialogs/dialogs-editor/dialogs-editor.component.ts @@ -56,7 +56,7 @@ export class DialogsEditorComponent public statsInOrder = computed(() => { const npc = this.editing(); - return Object.keys(npc.otherStats).sort() as StatType[]; + return Object.keys(npc.otherStats ?? {}).sort() as StatType[]; }); public behaviorError = computed(() => { @@ -80,19 +80,21 @@ export class DialogsEditorComponent ngOnInit(): void { const npc = this.editing(); - if (npc.behaviors.length > 0) { + if (npc.behaviors?.length > 0) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.behaviorText.set(yaml.dump(npc.behaviors)); this.behaviorModel.value = this.behaviorText(); } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - if (Object.keys(npc.dialog.keyword ?? {}).length > 0) { + if (Object.keys(npc.dialog?.keyword ?? {}).length > 0) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.dialogText.set(yaml.dump(npc.dialog)); this.dialogModel.value = this.dialogText(); } + npc.baseEffects ??= []; + super.ngOnInit(); } @@ -138,6 +140,26 @@ export class DialogsEditorComponent this.dialogText.set(dialogText); } + public addBaseEffect() { + const npc = this.editing(); + npc.baseEffects.push({ + endsAt: -1, + name: undefined as unknown as string, + extra: { + potency: 1, + damageType: undefined, + enrageTimer: undefined, + }, + }); + this.editing.set(npc); + } + + public removeBaseEffect(index: number) { + const npc = this.editing(); + npc.baseEffects.splice(index, 1); + this.editing.set(npc); + } + doSave() { const npc = this.editing(); npc.usableSkills = npc.usableSkills.filter(Boolean); diff --git a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.html b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.html index 6f52655..45dd59f 100644 --- a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.html +++ b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.html @@ -345,47 +345,7 @@
@for(baseEffect of editingData.baseEffects; track $index) { -
-
-
- -
-
- - @if(baseEffect.name === 'Attribute') { -
-
- -
-
- } - - @if(baseEffect.name === 'Mood') { -
-
- Enrage Timer (ms) - -
-
- } - -
-
- Potency - -
-
- -
-
- -
-
-
+ } diff --git a/src/interfaces/npc.ts b/src/interfaces/npc.ts index 21ecfcc..a33ae04 100644 --- a/src/interfaces/npc.ts +++ b/src/interfaces/npc.ts @@ -22,6 +22,16 @@ export enum NPCTriggerType { Combat = 'combat', } +export interface INPCEffect { + name: string; + endsAt: number; + extra: { + potency: number; + damageType?: DamageClassType; + enrageTimer?: number; + }; +} + export interface INPCDefinition extends HasIdentification { npcId: string; @@ -53,15 +63,7 @@ export interface INPCDefinition extends HasIdentification { baseClass?: BaseClassType; // the base effects given to the creature (usually attributes/truesight/etc) - baseEffects: Array<{ - name: string; - endsAt: number; - extra: { - potency: number; - damageType?: DamageClassType; - enrageTimer?: number; - }; - }>; + baseEffects: INPCEffect[]; // the drop chance for copying items that are already equipped copyDrops: Rollable[]; diff --git a/src/interfaces/npcscript.ts b/src/interfaces/npcscript.ts index 3db93bd..82f5054 100644 --- a/src/interfaces/npcscript.ts +++ b/src/interfaces/npcscript.ts @@ -11,6 +11,7 @@ import { } from './building-blocks'; import { HasIdentification } from './identified'; import { ISimpleItem } from './item'; +import { INPCEffect } from './npc'; export enum BehaviorType { Trainer = 'trainer', @@ -224,4 +225,9 @@ export interface INPCScript extends HasIdentification { allegiance: AllegianceType; alignment: AlignmentType; behaviors: IBehavior[]; + + maxWanderRandomlyDistance: number; + noLeash: boolean; + + baseEffects: INPCEffect[]; }