Skip to content

Commit

Permalink
autogenerate rune/lore, #44
Browse files Browse the repository at this point in the history
  • Loading branch information
seiyria committed Aug 19, 2024
1 parent 0b56ec2 commit 4c2f00d
Show file tree
Hide file tree
Showing 16 changed files with 310 additions and 5 deletions.
1 change: 1 addition & 0 deletions app/handlers/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export async function updateResources(sendToUI: SendToUI) {
'spawners',
'spells',
'traits',
'trait-trees',
];

for await (let json of jsons) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
heroDocumentText,
heroFaceFrown,
heroFaceSmile,
heroInformationCircle,
heroMinus,
heroPencil,
heroPlus,
Expand All @@ -26,4 +27,5 @@ export const appIcons = {
heroFaceSmile,
heroArrowPathRoundedSquare,
heroArrowTopRightOnSquare,
heroInformationCircle,
};
2 changes: 2 additions & 0 deletions src/app/helpers/autocontent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lorescrolls';
export * from './traitscrolls';
110 changes: 110 additions & 0 deletions src/app/helpers/autocontent/lorescrolls.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
}
99 changes: 99 additions & 0 deletions src/app/helpers/autocontent/traitscrolls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { startCase } from 'lodash';
import {
IItemDefinition,
IModKit,
ItemClass,
Skill,
} from '../../../interfaces';
import { id } from '../id';

const romans: Record<number, string> = {
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<string, string[]> = {};
const allRuneScrolls = new Set<string>();

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));
}
1 change: 1 addition & 0 deletions src/app/helpers/schemas/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
2 changes: 2 additions & 0 deletions src/app/helpers/validate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sortBy } from 'lodash';
import { IModKit, ValidationMessageGroup } from '../../interfaces';
import {
checkAutogenerated,
checkItemStats,
checkItemUses,
checkMapNPCDialogs,
Expand Down Expand Up @@ -47,6 +48,7 @@ export function validationMessagesForMod(
nonexistentItems(mod),
nonexistentNPCs(mod),
nonexistentRecipes(mod),
checkAutogenerated(mod),
];

validationContainer.forEach((v) => {
Expand Down
22 changes: 22 additions & 0 deletions src/app/helpers/validators/autogenerated.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions src/app/helpers/validators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './autogenerated';
export * from './dialog';
export * from './droptable';
export * from './item';
Expand Down
1 change: 1 addition & 0 deletions src/app/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<li><a (click)="closeMenu(); toggleTester()">Test Mod!</a></li>

<li class="menu-title"><a>Updates</a></li>
<li><a (click)="closeMenu(); modService.updateAutogenerated()">Update Autogenerated Content</a></li>
<li><a (click)="closeMenu(); electronService.send('UPDATE_RESOURCES')">Update Resources</a></li>

<li class="menu-title"><a>Danger Zone</a></li>
Expand Down
11 changes: 11 additions & 0 deletions src/app/services/electron.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ export class ElectronService {
}
);

[
'effect-data',
'holidaydescs',
'spells',
'traits',
'challenge',
'trait-trees',
].forEach((neededJSON) => {
this.requestJSON(neededJSON);
});

this.send('READY_CHECK');
}

Expand Down
36 changes: 36 additions & 0 deletions src/app/services/mod.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand All @@ -51,6 +62,7 @@ export class ModService {
constructor() {
const oldModData: IModKit = this.localStorage.retrieve('mod');
if (oldModData) {
ensureIds(oldModData);
this.updateMod(oldModData);
}

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="flex justify-end items-center h-full">
@if(params.showCopyButton) {
<button class="ml-2 btn btn-sm btn-info" (click)="params.copyCallback?.(params.data)" floatUi="Copy">
<button class="ml-2 btn btn-sm btn-info" [class.btn-disabled]="params.data._id?.includes('AUTOGENERATED')"
(click)="params.copyCallback?.(params.data)" floatUi="Copy">
<ng-icon name="heroDocumentDuplicate"></ng-icon>
</button>
}
Expand Down
7 changes: 7 additions & 0 deletions src/app/tabs/items/items-editor/items-editor.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
@let editingData = editing();

@if(isAutogenerated()) {
<div role="alert" class="alert alert-info mb-1">
<ng-icon name="heroInformationCircle"></ng-icon>
<span>This item is automatically generated. You will not be able to save any changes to it.</span>
</div>
}

<div role="tablist" class="tabs tabs-boxed rounded-none mb-3">

@for(tab of tabs; let i = $index; track tab.name) {
Expand Down
Loading

0 comments on commit 4c2f00d

Please sign in to comment.