diff --git a/app/handlers/resources.ts b/app/handlers/resources.ts index e20d3a9..28edff0 100644 --- a/app/handlers/resources.ts +++ b/app/handlers/resources.ts @@ -73,6 +73,7 @@ export async function updateResources(sendToUI: SendToUI) { 'spawners', 'spells', 'traits', + 'trait-trees', ]; for await (let json of jsons) { diff --git a/src/app/app.icons.ts b/src/app/app.icons.ts index 62f97c7..f6921cb 100644 --- a/src/app/app.icons.ts +++ b/src/app/app.icons.ts @@ -7,6 +7,7 @@ import { heroDocumentText, heroFaceFrown, heroFaceSmile, + heroInformationCircle, heroMinus, heroPencil, heroPlus, @@ -26,4 +27,5 @@ export const appIcons = { heroFaceSmile, heroArrowPathRoundedSquare, heroArrowTopRightOnSquare, + heroInformationCircle, }; diff --git a/src/app/helpers/autocontent/index.ts b/src/app/helpers/autocontent/index.ts new file mode 100644 index 0000000..827232b --- /dev/null +++ b/src/app/helpers/autocontent/index.ts @@ -0,0 +1,2 @@ +export * from './lorescrolls'; +export * from './traitscrolls'; diff --git a/src/app/helpers/autocontent/lorescrolls.ts b/src/app/helpers/autocontent/lorescrolls.ts new file mode 100644 index 0000000..1d20615 --- /dev/null +++ b/src/app/helpers/autocontent/lorescrolls.ts @@ -0,0 +1,110 @@ +import { + IItemDefinition, + IModKit, + ItemClass, + Skill, +} from '../../../interfaces'; +import { id } from '../id'; + +const LORE_DROP_RATE = 10000; +const LORE_PREFIX = `Lore Scroll - Gem -`; + +export function generateLoreScrolls(mod: IModKit): IItemDefinition[] { + const allGems = mod.items.filter( + (x) => + x.itemClass === 'Gem' && + !['Solokar', 'Orikurnis'].some((region) => x.name.includes(region)) + ); + + const allGemScrollDescs = allGems.map((x) => { + const allKeys = Object.keys(x.encrustGive?.stats ?? {}).map((z) => + z.toUpperCase() + ); + + const allGemEffects = []; + + if (allKeys.length > 0) { + allGemEffects.push(`boost your ${allKeys.join(', ')}`); + } + + if (x.useEffect) { + allGemEffects.push( + `grant the spell ${x.useEffect.name.toUpperCase()} when used` + ); + } + + if (x.encrustGive?.strikeEffect) { + allGemEffects.push( + `grant the on-hit spell ${x.encrustGive.strikeEffect.name.toUpperCase()} when encrusted` + ); + } + + if (allGemEffects.length === 0) { + allGemEffects.push(`sell for a lot of gold`); + } + + const effectText = allGemEffects.join(' and '); + + const bonusText = x.encrustGive?.slots + ? `- be careful though, it can only be used in ${x.encrustGive?.slots.join( + ', ' + )} equipment` + : ''; + + return { + _itemName: x.name, + scrollDesc: `If you find ${x.desc}, it will ${effectText} ${bonusText}`, + }; + }); + + const allGemLoreItems: IItemDefinition[] = allGemScrollDescs.map( + ({ _itemName, scrollDesc }) => { + const itemName = `${LORE_PREFIX} ${_itemName}`; + + return { + _id: `${id()}-AUTOGENERATED`, + name: itemName, + sprite: 224, + value: 1, + desc: `Twean's Gem Codex: ${scrollDesc}`.trim(), + itemClass: ItemClass.Scroll, + isSackable: true, + type: Skill.Martial, + } as unknown as IItemDefinition; + } + ); + + return allGemLoreItems; +} + +export function cleanOldLoreScrolls(mod: IModKit): void { + mod.items = mod.items.filter((item) => !item.name.includes(LORE_PREFIX)); + + mod.drops.forEach((droptable) => { + droptable.drops = droptable.drops.filter((item) => + item.result.includes(LORE_PREFIX) + ); + }); +} + +export function countExistingLoreScrolls(mod: IModKit): number { + return mod.items.filter((i) => i.name.includes(LORE_PREFIX)).length; +} + +export function applyLoreScrolls(mod: IModKit, lore: IItemDefinition[]): void { + mod.items.push(...lore); + + lore.forEach((loreItem) => { + const loreItemName = loreItem.name.split(LORE_PREFIX).join('').trim(); + + mod.drops.forEach((droptable) => { + if (!droptable.drops.some((i) => i.result === loreItemName)) return; + + droptable.drops.push({ + result: loreItem.name, + chance: 1, + maxChance: LORE_DROP_RATE, + }); + }); + }); +} diff --git a/src/app/helpers/autocontent/traitscrolls.ts b/src/app/helpers/autocontent/traitscrolls.ts new file mode 100644 index 0000000..71f96bb --- /dev/null +++ b/src/app/helpers/autocontent/traitscrolls.ts @@ -0,0 +1,99 @@ +import { startCase } from 'lodash'; +import { + IItemDefinition, + IModKit, + ItemClass, + Skill, +} from '../../../interfaces'; +import { id } from '../id'; + +const romans: Record = { + 1: 'I', + 2: 'II', + 3: 'III', + 4: 'IV', + 5: 'V', +}; + +const TRAIT_PREFIX = `Rune Scroll - `; + +export function generateTraitScrolls( + mod: IModKit, + allTraitTrees: any = {} +): IItemDefinition[] { + const scrollToClass: Record = {}; + const allRuneScrolls = new Set(); + + const returnedRuneScrolls: IItemDefinition[] = []; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.keys(allTraitTrees).forEach((classTree) => { + if (classTree === 'Ancient') return; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.keys(allTraitTrees[classTree].trees).forEach((treeName) => { + if (treeName === 'Ancient') return; + + const tree = allTraitTrees[classTree].trees[treeName].tree; + + tree.forEach(({ traits }: any) => { + traits.forEach(({ name, maxLevel }: any) => { + if (!name || maxLevel <= 1) return; + + scrollToClass[name] ??= []; + + allRuneScrolls.add(name as string); + + if (classTree !== 'Core' && treeName !== 'Core') { + scrollToClass[name].push(classTree); + } + }); + }); + }); + }); + + Array.from(allRuneScrolls).forEach((scrollName) => { + for (let i = 1; i <= 5; i++) { + const scrollSpaced = startCase(scrollName); + const itemName = `${TRAIT_PREFIX} ${scrollSpaced} ${romans[i]}`; + + returnedRuneScrolls.push({ + _id: `${id()}-AUTOGENERATED`, + name: itemName, + sprite: 681, + animation: 10, + desc: `a runic scroll imbued with the empowerment "${scrollSpaced} ${romans[i]}"`, + trait: { + name: scrollName, + level: i, + restrict: scrollToClass[scrollName], + }, + requirements: { + level: 5 + (i - 1) * 10, + }, + value: 1, + itemClass: ItemClass.Scroll, + type: Skill.Martial, + stats: {}, + isSackable: true, + } as IItemDefinition); + } + }); + + return returnedRuneScrolls; +} + +export function countExistingTraitScrolls(mod: IModKit): number { + return mod.items.filter((i) => i.name.includes(TRAIT_PREFIX)).length; +} + +export function applyTraitScrolls( + mod: IModKit, + scrolls: IItemDefinition[] +): void { + mod.items.push(...scrolls); +} + +export function cleanOldTraitScrolls(mod: IModKit): void { + mod.items = mod.items.filter((item) => !item.name.includes(TRAIT_PREFIX)); +} diff --git a/src/app/helpers/schemas/item.ts b/src/app/helpers/schemas/item.ts index e8c6f0c..401e7a2 100644 --- a/src/app/helpers/schemas/item.ts +++ b/src/app/helpers/schemas/item.ts @@ -122,6 +122,7 @@ export const itemSchema: Schema = [ ['trait', false, isTrait], ['trait.name', false, isString], ['trait.level', false, isInteger], + ['trait.restrict', false, isArrayOf(isString)], ['bookFindablePages', false, isInteger], ['bookItemFilter', false, isString], diff --git a/src/app/helpers/validate.ts b/src/app/helpers/validate.ts index d3c8583..a851011 100644 --- a/src/app/helpers/validate.ts +++ b/src/app/helpers/validate.ts @@ -1,6 +1,7 @@ import { sortBy } from 'lodash'; import { IModKit, ValidationMessageGroup } from '../../interfaces'; import { + checkAutogenerated, checkItemStats, checkItemUses, checkMapNPCDialogs, @@ -47,6 +48,7 @@ export function validationMessagesForMod( nonexistentItems(mod), nonexistentNPCs(mod), nonexistentRecipes(mod), + checkAutogenerated(mod), ]; validationContainer.forEach((v) => { diff --git a/src/app/helpers/validators/autogenerated.ts b/src/app/helpers/validators/autogenerated.ts new file mode 100644 index 0000000..d7212e9 --- /dev/null +++ b/src/app/helpers/validators/autogenerated.ts @@ -0,0 +1,22 @@ +import { IModKit, ValidationMessageGroup } from '../../../interfaces'; +import { countExistingLoreScrolls, generateLoreScrolls } from '../autocontent'; + +export function checkAutogenerated(mod: IModKit): ValidationMessageGroup { + // check npc dialog refs, make sure they exist + const autoValidations: ValidationMessageGroup = { + header: `Autogenerated Content`, + messages: [], + }; + + const existingLore = countExistingLoreScrolls(mod); + const allAvailableLore = generateLoreScrolls(mod); + + if (allAvailableLore.length !== existingLore) { + autoValidations.messages.push({ + type: 'error', + message: `Outdated number of lore scrolls found: the mod has ${existingLore} but there are ${allAvailableLore.length} available.`, + }); + } + + return autoValidations; +} diff --git a/src/app/helpers/validators/index.ts b/src/app/helpers/validators/index.ts index bacd91e..2c5ce08 100644 --- a/src/app/helpers/validators/index.ts +++ b/src/app/helpers/validators/index.ts @@ -1,3 +1,4 @@ +export * from './autogenerated'; export * from './dialog'; export * from './droptable'; export * from './item'; diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 397e215..a2ed752 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -35,6 +35,7 @@
  • Test Mod!
  • +
  • Update Autogenerated Content
  • Update Resources
  • diff --git a/src/app/services/electron.service.ts b/src/app/services/electron.service.ts index eeca660..c11a040 100644 --- a/src/app/services/electron.service.ts +++ b/src/app/services/electron.service.ts @@ -102,6 +102,17 @@ export class ElectronService { } ); + [ + 'effect-data', + 'holidaydescs', + 'spells', + 'traits', + 'challenge', + 'trait-trees', + ].forEach((neededJSON) => { + this.requestJSON(neededJSON); + }); + this.send('READY_CHECK'); } diff --git a/src/app/services/mod.service.ts b/src/app/services/mod.service.ts index 62cf7c4..07378ce 100644 --- a/src/app/services/mod.service.ts +++ b/src/app/services/mod.service.ts @@ -8,6 +8,16 @@ import { ItemSlotType, } from '../../interfaces'; import { id } from '../helpers'; +import { + applyLoreScrolls, + applyTraitScrolls, + cleanOldLoreScrolls, + cleanOldTraitScrolls, + generateLoreScrolls, + generateTraitScrolls, +} from '../helpers/autocontent'; +import { ensureIds } from '../helpers/import'; +import { NotifyService } from './notify.service'; import { SettingsService } from './settings.service'; export function defaultModKit(): IModKit { @@ -35,6 +45,7 @@ export function defaultModKit(): IModKit { providedIn: 'root', }) export class ModService { + private notifyService = inject(NotifyService); private localStorage = inject(LocalStorageService); private settingsService = inject(SettingsService); @@ -51,6 +62,7 @@ export class ModService { constructor() { const oldModData: IModKit = this.localStorage.retrieve('mod'); if (oldModData) { + ensureIds(oldModData); this.updateMod(oldModData); } @@ -453,4 +465,28 @@ export class ModService { const items = this.availableItems(); return items.find((i) => i.name === itemName); } + + // autogenerated + public updateAutogenerated() { + const mod = this.mod(); + + const loreItems = generateLoreScrolls(mod); + cleanOldLoreScrolls(mod); + applyLoreScrolls(mod, loreItems); + + this.notifyService.info({ + message: `Created and updated ${loreItems.length} lore scrolls.`, + }); + + console.log(this.json()['trait-trees']); + const runeItems = generateTraitScrolls(mod, this.json()['trait-trees']); + cleanOldTraitScrolls(mod); + applyTraitScrolls(mod, runeItems); + + this.notifyService.info({ + message: `Created and updated ${runeItems.length} rune scrolls.`, + }); + + this.updateMod(mod); + } } diff --git a/src/app/shared/components/cell-buttons/cell-buttons.component.html b/src/app/shared/components/cell-buttons/cell-buttons.component.html index 64e510a..3ba8bf4 100644 --- a/src/app/shared/components/cell-buttons/cell-buttons.component.html +++ b/src/app/shared/components/cell-buttons/cell-buttons.component.html @@ -1,6 +1,7 @@
    @if(params.showCopyButton) { - } diff --git a/src/app/tabs/items/items-editor/items-editor.component.html b/src/app/tabs/items/items-editor/items-editor.component.html index b60b9e0..12d937c 100644 --- a/src/app/tabs/items/items-editor/items-editor.component.html +++ b/src/app/tabs/items/items-editor/items-editor.component.html @@ -1,5 +1,12 @@ @let editingData = editing(); +@if(isAutogenerated()) { + +} +
    @for(tab of tabs; let i = $index; track tab.name) { 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 a8a1e78..029b843 100644 --- a/src/app/tabs/items/items-editor/items-editor.component.ts +++ b/src/app/tabs/items/items-editor/items-editor.component.ts @@ -89,9 +89,18 @@ export class ItemsEditorComponent public currentTrait = signal(undefined); public allStatEdits = signal([]); + public isAutogenerated = computed(() => + this.editing()._id.includes('AUTOGENERATED') + ); + public canSave = computed(() => { const data = this.editing(); - return data.name && data.itemClass && this.satisfiesUnique(); + return ( + data.name && + data.itemClass && + this.satisfiesUnique() && + !this.isAutogenerated() + ); }); public satisfiesUnique = computed(() => { @@ -169,6 +178,7 @@ export class ItemsEditorComponent item.requirements ??= { baseClass: undefined, level: 0, quest: undefined }; item.cosmetic ??= { name: '', isPermanent: false }; + item.stats ??= {}; item.trait ??= { name: '', level: 0 }; item.randomTrait ??= { name: [], level: { min: 0, max: 0 } }; @@ -381,12 +391,10 @@ export class ItemsEditorComponent if (item.randomTrait.name.length > 0) { this.currentTraitTab.set('random'); } - - console.log(item, item.randomTrait, item.trait); } private extractSetStats(stats: StatBlock) { - Object.keys(stats).forEach((statKey) => { + Object.keys(stats ?? {}).forEach((statKey) => { const stat = statKey as StatType; this.allStatEdits.update((s) => [ ...s, diff --git a/src/app/tabs/items/items.component.ts b/src/app/tabs/items/items.component.ts index d1a4624..029633c 100644 --- a/src/app/tabs/items/items.component.ts +++ b/src/app/tabs/items/items.component.ts @@ -35,6 +35,7 @@ export class ItemsComponent extends EditorBaseTableComponent { field: 'name', flex: 1, cellDataType: 'text', + cellClass: 'leading-4 whitespace-break-spaces', filter: 'agTextColumnFilter', sort: 'asc', },