diff --git a/package-lock.json b/package-lock.json index 1120c9f..6233b81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@angular/platform-browser": "18.1.3", "@angular/platform-browser-dynamic": "18.1.3", "@angular/router": "18.1.3", + "@ng-icons/core": "29.3.0", + "@ng-icons/heroicons": "29.3.0", "@ng-select/ng-select": "13.5.2", "@ngneat/overview": "6.0.0", "@ngxpert/hot-toast": "3.0.0", @@ -6001,6 +6003,27 @@ "@tybys/wasm-util": "^0.9.0" } }, + "node_modules/@ng-icons/core": { + "version": "29.3.0", + "resolved": "https://registry.npmjs.org/@ng-icons/core/-/core-29.3.0.tgz", + "integrity": "sha512-UZzt5gxquD1D30MSlUKMzQVMvfFubc9TsL87srEl4jakit7nCb8rWm7KaDImWpV4/3YG1Y9Qb7r85DWCus/Zrg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/common": ">=18.0.0", + "@angular/core": ">=18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ng-icons/heroicons": { + "version": "29.3.0", + "resolved": "https://registry.npmjs.org/@ng-icons/heroicons/-/heroicons-29.3.0.tgz", + "integrity": "sha512-2SdpQW++mGWdANZkctGuBrW7L9NRgywVi7Pv/6Jx4HNGONugB62MDdIzPlqklpOeqP6eJ4vq67xsfrp9Kj4pEQ==", + "dependencies": { + "tslib": "^2.2.0" + } + }, "node_modules/@ng-select/ng-select": { "version": "13.5.2", "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.5.2.tgz", diff --git a/package.json b/package.json index 743ec08..683552d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "@angular/platform-browser": "18.1.3", "@angular/platform-browser-dynamic": "18.1.3", "@angular/router": "18.1.3", + "@ng-icons/core": "29.3.0", + "@ng-icons/heroicons": "29.3.0", "@ng-select/ng-select": "13.5.2", "@ngneat/overview": "6.0.0", "@ngxpert/hot-toast": "3.0.0", diff --git a/src/app/app.icons.ts b/src/app/app.icons.ts new file mode 100644 index 0000000..2a8b2b7 --- /dev/null +++ b/src/app/app.icons.ts @@ -0,0 +1,21 @@ +import { + heroArrowDownOnSquare, + heroArrowsRightLeft, + heroDocumentDuplicate, + heroDocumentText, + heroMinus, + heroPencil, + heroPlus, + heroTrash, +} from '@ng-icons/heroicons/outline'; + +export const appIcons = { + heroArrowDownOnSquare, + heroDocumentDuplicate, + heroDocumentText, + heroPencil, + heroPlus, + heroMinus, + heroTrash, + heroArrowsRightLeft, +}; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4031ca6..528004b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,7 @@ import { AppRoutingModule } from './app-routing.module'; import { HomeModule } from './home/home.module'; +import { NgIconsModule, provideNgIconsConfig } from '@ng-icons/core'; import { provideHotToastConfig } from '@ngxpert/hot-toast'; import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; import { @@ -24,6 +25,7 @@ import { withNgxWebstorageConfig, } from 'ngx-webstorage'; import { AppComponent } from './app.component'; +import { appIcons } from './app.icons'; @NgModule({ declarations: [AppComponent], @@ -43,6 +45,9 @@ import { AppComponent } from './app.component'; SweetAlert2Module.forRoot({ provideSwal: () => import('sweetalert2/dist/sweetalert2.js'), }), + NgIconsModule.withIcons({ + ...appIcons, + }), ], providers: [ provideHttpClient(withInterceptorsFromDi()), @@ -51,6 +56,9 @@ import { AppComponent } from './app.component'; withNgxWebstorageConfig({ separator: ':', caseSensitive: true }), withLocalStorage() ), + provideNgIconsConfig({ + size: '1.5em', + }), ], exports: [], }) diff --git a/src/app/helpers/index.ts b/src/app/helpers/index.ts index 6cd0e9d..dfa1fbb 100644 --- a/src/app/helpers/index.ts +++ b/src/app/helpers/index.ts @@ -1,4 +1,6 @@ export * from './constants'; export * from './droptable'; +export * from './id'; export * from './item'; +export * from './npc'; export * from './recipe'; diff --git a/src/app/helpers/npc.ts b/src/app/helpers/npc.ts new file mode 100644 index 0000000..88bcc1a --- /dev/null +++ b/src/app/helpers/npc.ts @@ -0,0 +1,105 @@ +import { + AlignmentType, + BaseClassType, + INPCDefinition, + MonsterClassType, +} from '../../interfaces'; +import { id } from './id'; + +export const defaultNPC: () => INPCDefinition = () => ({ + _id: id(), + sprite: [0], + npcId: '', + name: '', + hostility: 'OnHit', + allegiance: 'Enemy', + monsterClass: undefined as unknown as MonsterClassType, + baseClass: undefined as unknown as BaseClassType, + affiliation: '', + alignment: 'Neutral' as AlignmentType, + cr: 0, + hpMult: 1, + stats: { + str: 0, + dex: 0, + agi: 0, + int: 0, + wis: 0, + wil: 0, + con: 0, + cha: 0, + luk: 0, + }, + level: 1, + skillLevels: 1, + skillOnKill: 1, + otherStats: {}, + gear: {}, + hp: { min: 0, max: 0 }, + mp: { min: 0, max: 0 }, + giveXp: { min: 0, max: 0 }, + gold: { min: 0, max: 0 }, + monsterGroup: '', + avoidWater: false, + aquaticOnly: false, + noCorpseDrop: false, + noItemDrop: false, + traitLevels: {}, + usableSkills: [], + baseEffects: [], + drops: [], + copyDrops: [], + dropPool: { + choose: { + min: 0, + max: 0, + }, + items: [], + }, + tansFor: '', + tanSkillRequired: 0, + triggers: { + leash: { + messages: [''], + sfx: { + name: '', + maxChance: 0, + }, + }, + spawn: { + messages: [''], + sfx: { + name: '', + maxChance: 0, + }, + }, + combat: { + messages: [], + }, + }, + items: { + sack: [], + belt: [], + equipment: { + rightHand: [], + leftHand: [], + head: [], + neck: [], + ear: [], + waist: [], + wrists: [], + ring1: [], + ring2: [], + hands: [], + feet: [], + armor: [], + robe1: [], + robe2: [], + trinket: [], + potion: [], + ammo: [], + }, + }, + repMod: [], + skills: {}, +}); diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index f89646f..800ef34 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { HomeRoutingModule } from './home-routing.module'; +import { NgIconsModule } from '@ng-icons/core'; import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; import { AgGridModule } from 'ag-grid-angular'; import { NgxFloatUiModule } from 'ngx-float-ui'; @@ -13,6 +14,7 @@ import { DroptablesComponent } from '../tabs/droptables/droptables.component'; import { ItemsEditorComponent } from '../tabs/items/items-editor/items-editor.component'; import { ItemsComponent } from '../tabs/items/items.component'; import { MapsComponent } from '../tabs/maps/maps.component'; +import { NpcsEditorComponent } from '../tabs/npcs/npcs-editor/npcs-editor.component'; import { NpcsComponent } from '../tabs/npcs/npcs.component'; import { QuestsComponent } from '../tabs/quests/quests.component'; import { RecipesEditorComponent } from '../tabs/recipes/recipes-editor/recipes-editor.component'; @@ -34,6 +36,7 @@ import { HomeComponent } from './home.component'; RecipesComponent, RecipesEditorComponent, SpawnersComponent, + NpcsEditorComponent, ], imports: [ CommonModule, @@ -42,6 +45,8 @@ import { HomeComponent } from './home.component'; SweetAlert2Module, AgGridModule, NgxFloatUiModule, + NgIconsModule, ], + exports: [NpcsEditorComponent], }) export class HomeModule {} diff --git a/src/app/services/mod.service.ts b/src/app/services/mod.service.ts index 46131e7..1537930 100644 --- a/src/app/services/mod.service.ts +++ b/src/app/services/mod.service.ts @@ -5,6 +5,7 @@ import { IEditorMap, IItemDefinition, IModKit, + INPCDefinition, IRecipe, } from '../../interfaces'; @@ -258,4 +259,29 @@ export class ModService { this.updateMod(mod); } + + // npc functions + public addNPC(npc: INPCDefinition) { + const mod = this.mod(); + mod.npcs.push(npc); + + this.updateMod(mod); + } + + public editNPC(oldNPC: INPCDefinition, newNPC: INPCDefinition) { + const mod = this.mod(); + const foundItemIdx = mod.npcs.findIndex((i) => i._id === oldNPC._id); + if (foundItemIdx === -1) return; + + mod.npcs[foundItemIdx] = newNPC; + + this.updateMod(mod); + } + + public removeNPC(npc: INPCDefinition) { + const mod = this.mod(); + mod.npcs = mod.npcs.filter((i) => i !== npc); + + 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 323a816..48b705b 100644 --- a/src/app/shared/components/cell-buttons/cell-buttons.component.html +++ b/src/app/shared/components/cell-buttons/cell-buttons.component.html @@ -1,42 +1,25 @@
@if(params.showCopyButton) { } @if(params.showRenameButton) { } @if(params.showEditButton) { } @if(params.showDeleteButton) { @if(params.showImportButton) { } @if(params.showNewButton) { }
diff --git a/src/app/shared/components/input-alignment/input-alignment.component.html b/src/app/shared/components/input-alignment/input-alignment.component.html new file mode 100644 index 0000000..32000d4 --- /dev/null +++ b/src/app/shared/components/input-alignment/input-alignment.component.html @@ -0,0 +1,6 @@ +
+ + + Alignment +
diff --git a/src/app/shared/components/input-alignment/input-alignment.component.scss b/src/app/shared/components/input-alignment/input-alignment.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/input-alignment/input-alignment.component.ts b/src/app/shared/components/input-alignment/input-alignment.component.ts new file mode 100644 index 0000000..05afa3f --- /dev/null +++ b/src/app/shared/components/input-alignment/input-alignment.component.ts @@ -0,0 +1,14 @@ +import { Component, model, output } from '@angular/core'; +import { Alignment, AlignmentType } from '../../../../interfaces'; + +@Component({ + selector: 'app-input-alignment', + templateUrl: './input-alignment.component.html', + styleUrl: './input-alignment.component.scss', +}) +export class InputAlignmentComponent { + public alignment = model.required(); + public change = output(); + + public values = [...Object.keys(Alignment)]; +} diff --git a/src/app/shared/components/input-allegiance/input-allegiance.component.html b/src/app/shared/components/input-allegiance/input-allegiance.component.html new file mode 100644 index 0000000..49dd9d9 --- /dev/null +++ b/src/app/shared/components/input-allegiance/input-allegiance.component.html @@ -0,0 +1,6 @@ +
+ + + Faction +
diff --git a/src/app/shared/components/input-allegiance/input-allegiance.component.scss b/src/app/shared/components/input-allegiance/input-allegiance.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/input-allegiance/input-allegiance.component.ts b/src/app/shared/components/input-allegiance/input-allegiance.component.ts new file mode 100644 index 0000000..dcbd961 --- /dev/null +++ b/src/app/shared/components/input-allegiance/input-allegiance.component.ts @@ -0,0 +1,14 @@ +import { Component, model, output } from '@angular/core'; +import { Allegiance, AllegianceType } from '../../../../interfaces'; + +@Component({ + selector: 'app-input-allegiance', + templateUrl: './input-allegiance.component.html', + styleUrl: './input-allegiance.component.scss', +}) +export class InputAllegianceComponent { + public allegiance = model.required(); + public change = output(); + + public values = [...Object.values(Allegiance)]; +} diff --git a/src/app/shared/components/input-category/input-category.component.html b/src/app/shared/components/input-category/input-category.component.html new file mode 100644 index 0000000..600a010 --- /dev/null +++ b/src/app/shared/components/input-category/input-category.component.html @@ -0,0 +1,6 @@ +
+ + + Category +
diff --git a/src/app/shared/components/input-category/input-category.component.scss b/src/app/shared/components/input-category/input-category.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/input-category/input-category.component.ts b/src/app/shared/components/input-category/input-category.component.ts new file mode 100644 index 0000000..7bf84aa --- /dev/null +++ b/src/app/shared/components/input-category/input-category.component.ts @@ -0,0 +1,14 @@ +import { Component, model, output } from '@angular/core'; +import { MonsterClass, MonsterClassType } from '../../../../interfaces'; + +@Component({ + selector: 'app-input-category', + templateUrl: './input-category.component.html', + styleUrl: './input-category.component.scss', +}) +export class InputCategoryComponent { + public category = model.required(); + public change = output(); + + public values = [...Object.values(MonsterClass)]; +} diff --git a/src/app/shared/components/input-challengerating/input-challengerating.component.html b/src/app/shared/components/input-challengerating/input-challengerating.component.html new file mode 100644 index 0000000..20785e1 --- /dev/null +++ b/src/app/shared/components/input-challengerating/input-challengerating.component.html @@ -0,0 +1,6 @@ +
+ + + Challenge Rating +
diff --git a/src/app/shared/components/input-challengerating/input-challengerating.component.scss b/src/app/shared/components/input-challengerating/input-challengerating.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/input-challengerating/input-challengerating.component.ts b/src/app/shared/components/input-challengerating/input-challengerating.component.ts new file mode 100644 index 0000000..d2d353d --- /dev/null +++ b/src/app/shared/components/input-challengerating/input-challengerating.component.ts @@ -0,0 +1,15 @@ +import { Component, model, output } from '@angular/core'; + +@Component({ + selector: 'app-input-challengerating', + templateUrl: './input-challengerating.component.html', + styleUrl: './input-challengerating.component.scss', +}) +export class InputChallengeratingComponent { + public rating = model.required(); + public change = output(); + + public values = Array(21) + .fill(0) + .map((x, i) => i - 10); +} diff --git a/src/app/shared/components/input-damageclass/input-damageclass.component.ts b/src/app/shared/components/input-damageclass/input-damageclass.component.ts index 5704d44..c577456 100644 --- a/src/app/shared/components/input-damageclass/input-damageclass.component.ts +++ b/src/app/shared/components/input-damageclass/input-damageclass.component.ts @@ -7,7 +7,7 @@ import { DamageClass, DamageClassType } from '../../../../interfaces'; styleUrl: './input-damageclass.component.scss', }) export class InputDamageclassComponent { - public damageClass = model.required(); + public damageClass = model.required(); public change = output(); public values = [...Object.values(DamageClass)]; diff --git a/src/app/shared/components/input-effect/input-effect.component.html b/src/app/shared/components/input-effect/input-effect.component.html index 66c93a5..254872b 100644 --- a/src/app/shared/components/input-effect/input-effect.component.html +++ b/src/app/shared/components/input-effect/input-effect.component.html @@ -8,7 +8,7 @@ {{ item.value }} -

{{ item.desc }}

+

{{ item.desc }}

diff --git a/src/app/shared/components/input-holiday/input-holiday.component.html b/src/app/shared/components/input-holiday/input-holiday.component.html index 226bf8e..3b878a0 100644 --- a/src/app/shared/components/input-holiday/input-holiday.component.html +++ b/src/app/shared/components/input-holiday/input-holiday.component.html @@ -1,6 +1,6 @@
+ placeholder="Select holiday..." (change)="change.emit($event?.value)" [searchFn]="search">
diff --git a/src/app/shared/components/input-holiday/input-holiday.component.ts b/src/app/shared/components/input-holiday/input-holiday.component.ts index cd10d2a..07e7239 100644 --- a/src/app/shared/components/input-holiday/input-holiday.component.ts +++ b/src/app/shared/components/input-holiday/input-holiday.component.ts @@ -38,4 +38,8 @@ export class InputHolidayComponent { constructor() { this.electronService.requestJSON('holidaydescs'); } + + public search(term: string, item: { value: string }) { + return item.value.toLowerCase().includes(term.toLowerCase()); + } } diff --git a/src/app/shared/components/input-hostility/input-hostility.component.html b/src/app/shared/components/input-hostility/input-hostility.component.html new file mode 100644 index 0000000..27e16d9 --- /dev/null +++ b/src/app/shared/components/input-hostility/input-hostility.component.html @@ -0,0 +1,6 @@ +
+ + + Hostility +
diff --git a/src/app/shared/components/input-hostility/input-hostility.component.scss b/src/app/shared/components/input-hostility/input-hostility.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/input-hostility/input-hostility.component.ts b/src/app/shared/components/input-hostility/input-hostility.component.ts new file mode 100644 index 0000000..0744fe2 --- /dev/null +++ b/src/app/shared/components/input-hostility/input-hostility.component.ts @@ -0,0 +1,14 @@ +import { Component, model, output } from '@angular/core'; +import { Hostility, HostilityType } from '../../../../interfaces'; + +@Component({ + selector: 'app-input-hostility', + templateUrl: './input-hostility.component.html', + styleUrl: './input-hostility.component.scss', +}) +export class InputHostilityComponent { + public hostility = model.required(); + public change = output(); + + public values = [...Object.keys(Hostility)]; +} diff --git a/src/app/shared/components/input-item/input-item.component.html b/src/app/shared/components/input-item/input-item.component.html index e829137..8dcda8c 100644 --- a/src/app/shared/components/input-item/input-item.component.html +++ b/src/app/shared/components/input-item/input-item.component.html @@ -1,7 +1,7 @@
{{ item | json }} + placeholder="Select item..." (change)="change.emit($event?.value)" [compareWith]="itemCompare" [searchFn]="search">
diff --git a/src/app/shared/components/input-item/input-item.component.ts b/src/app/shared/components/input-item/input-item.component.ts index 88aa9d6..e0b0f61 100644 --- a/src/app/shared/components/input-item/input-item.component.ts +++ b/src/app/shared/components/input-item/input-item.component.ts @@ -48,4 +48,8 @@ export class InputItemComponent implements OnInit { public itemCompare(itemA: ItemModel, itemB: ItemModel): boolean { return itemA.value === itemB.value; } + + public search(term: string, item: { value: string }) { + return item.value.toLowerCase().includes(term.toLowerCase()); + } } diff --git a/src/app/shared/components/input-spell/input-spell.component.html b/src/app/shared/components/input-spell/input-spell.component.html index cb71709..97fa938 100644 --- a/src/app/shared/components/input-spell/input-spell.component.html +++ b/src/app/shared/components/input-spell/input-spell.component.html @@ -1,20 +1,6 @@
- - -
- {{ item }} -
-
- - -
-
- {{ item }} -
-
-
{{ label() }} diff --git a/src/app/shared/components/input-spell/input-spell.component.ts b/src/app/shared/components/input-spell/input-spell.component.ts index 1dc5ad6..8407dff 100644 --- a/src/app/shared/components/input-spell/input-spell.component.ts +++ b/src/app/shared/components/input-spell/input-spell.component.ts @@ -30,4 +30,8 @@ export class InputSpellComponent { constructor() { this.electronService.requestJSON('spells'); } + + public search(term: string, item: { value: string }) { + return item.value.toLowerCase().includes(term.toLowerCase()); + } } diff --git a/src/app/shared/components/input-sprite/input-sprite.component.html b/src/app/shared/components/input-sprite/input-sprite.component.html index e26378b..5792e15 100644 --- a/src/app/shared/components/input-sprite/input-sprite.component.html +++ b/src/app/shared/components/input-sprite/input-sprite.component.html @@ -1,9 +1,9 @@
- +
- Sprite diff --git a/src/app/shared/components/input-sprite/input-sprite.component.ts b/src/app/shared/components/input-sprite/input-sprite.component.ts index eb2d31a..9c0fa3b 100644 --- a/src/app/shared/components/input-sprite/input-sprite.component.ts +++ b/src/app/shared/components/input-sprite/input-sprite.component.ts @@ -1,4 +1,4 @@ -import { Component, model } from '@angular/core'; +import { Component, computed, input, model } from '@angular/core'; @Component({ selector: 'app-input-sprite', @@ -7,4 +7,6 @@ import { Component, model } from '@angular/core'; }) export class InputSpriteComponent { public sprite = model.required(); + public type = input<'creatures' | 'items'>('items'); + public step = computed(() => (this.type() === 'creatures' ? 5 : 1)); } diff --git a/src/app/shared/components/input-stat/input-stat.component.html b/src/app/shared/components/input-stat/input-stat.component.html index 23c991d..22b614d 100644 --- a/src/app/shared/components/input-stat/input-stat.component.html +++ b/src/app/shared/components/input-stat/input-stat.component.html @@ -1,5 +1,5 @@
- Stat diff --git a/src/app/shared/components/input-stat/input-stat.component.ts b/src/app/shared/components/input-stat/input-stat.component.ts index aa508c3..37a2753 100644 --- a/src/app/shared/components/input-stat/input-stat.component.ts +++ b/src/app/shared/components/input-stat/input-stat.component.ts @@ -1,4 +1,4 @@ -import { Component, model, output } from '@angular/core'; +import { Component, computed, input, model, output } from '@angular/core'; import { ItemClassType, StatType } from '../../../../interfaces'; import { coreStats, extraStats } from '../../../helpers'; @@ -10,9 +10,16 @@ import { coreStats, extraStats } from '../../../helpers'; export class InputStatComponent { public stat = model.required(); public change = output(); + public allowCore = input(true); + public allowExtra = input(true); - public values = [ - ...coreStats.map((x) => x.stat), - ...extraStats.map((x) => x.stat), - ]; + public values = computed(() => { + const allowCore = this.allowCore(); + const allowExtra = this.allowExtra(); + + return [ + ...(allowCore ? coreStats.map((x) => x.stat) : []), + ...(allowExtra ? extraStats.map((x) => x.stat) : []), + ]; + }); } diff --git a/src/app/shared/components/input-trait/input-trait.component.html b/src/app/shared/components/input-trait/input-trait.component.html index 4a64d39..c6aace4 100644 --- a/src/app/shared/components/input-trait/input-trait.component.html +++ b/src/app/shared/components/input-trait/input-trait.component.html @@ -8,7 +8,7 @@ {{ item.value }}
-

{{ item.desc }}

+

{{ item.desc }}

diff --git a/src/app/shared/components/input-trait/input-trait.component.ts b/src/app/shared/components/input-trait/input-trait.component.ts index d224d84..c318953 100644 --- a/src/app/shared/components/input-trait/input-trait.component.ts +++ b/src/app/shared/components/input-trait/input-trait.component.ts @@ -18,13 +18,15 @@ export class InputTraitComponent { private electronService = inject(ElectronService); private modService = inject(ModService); - public trait = model.required(); + public trait = model.required(); public label = input('Trait'); public change = output(); public values = computed(() => { const traitObj = this.modService.json()['traits'] as Record; + return Object.keys(traitObj ?? {}) + .filter((t) => !traitObj[t].spellGiven) .sort() .map((t) => ({ value: t, desc: traitObj[t].desc ?? 'No description' })); }); diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 607c223..f41613f 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -2,12 +2,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NgIconsModule } from '@ng-icons/core'; import { NgSelectModule } from '@ng-select/ng-select'; import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; import { AgGridModule } from 'ag-grid-angular'; import { NgxFloatUiModule } from 'ngx-float-ui'; import { CellButtonsComponent } from './components/cell-buttons/cell-buttons.component'; import { CellSpriteComponent } from './components/cell-sprite/cell-sprite.component'; +import { DebugViewComponent } from './components/debug-view/debug-view.component'; import { EditorBaseTableComponent } from './components/editor-base-table/editor-base-table.component'; import { EditorBaseComponent } from './components/editor-base/editor-base.component'; import { EditorViewTableComponent } from './components/editor-view-table/editor-view-table.component'; @@ -16,21 +18,25 @@ import { InputClassComponent } from './components/input-class/input-class.compon import { InputDamageclassComponent } from './components/input-damageclass/input-damageclass.component'; import { InputEffectComponent } from './components/input-effect/input-effect.component'; import { InputFloatingLabelComponent } from './components/input-floating-label/input-floating-label.component'; +import { InputHolidayComponent } from './components/input-holiday/input-holiday.component'; import { InputItemComponent } from './components/input-item/input-item.component'; import { InputItemclassComponent } from './components/input-itemclass/input-itemclass.component'; +import { InputMapComponent } from './components/input-map/input-map.component'; +import { InputRegionComponent } from './components/input-region/input-region.component'; import { InputSkillComponent } from './components/input-skill/input-skill.component'; +import { InputSpellComponent } from './components/input-spell/input-spell.component'; import { InputSpriteComponent } from './components/input-sprite/input-sprite.component'; import { InputStatComponent } from './components/input-stat/input-stat.component'; +import { InputTradeskillComponent } from './components/input-tradeskill/input-tradeskill.component'; import { InputTraitComponent } from './components/input-trait/input-trait.component'; import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component'; import { SpriteComponent } from './components/sprite/sprite.component'; import { WebviewDirective } from './directives/'; -import { InputMapComponent } from './components/input-map/input-map.component'; -import { InputRegionComponent } from './components/input-region/input-region.component'; -import { InputHolidayComponent } from './components/input-holiday/input-holiday.component'; -import { InputTradeskillComponent } from './components/input-tradeskill/input-tradeskill.component'; -import { InputSpellComponent } from './components/input-spell/input-spell.component'; -import { DebugViewComponent } from './components/debug-view/debug-view.component'; +import { InputAlignmentComponent } from './components/input-alignment/input-alignment.component'; +import { InputHostilityComponent } from './components/input-hostility/input-hostility.component'; +import { InputAllegianceComponent } from './components/input-allegiance/input-allegiance.component'; +import { InputCategoryComponent } from './components/input-category/input-category.component'; +import { InputChallengeratingComponent } from './components/input-challengerating/input-challengerating.component'; @NgModule({ declarations: [ @@ -59,6 +65,11 @@ import { DebugViewComponent } from './components/debug-view/debug-view.component InputTradeskillComponent, InputSpellComponent, DebugViewComponent, + InputAlignmentComponent, + InputHostilityComponent, + InputAllegianceComponent, + InputCategoryComponent, + InputChallengeratingComponent, ], imports: [ CommonModule, @@ -67,6 +78,7 @@ import { DebugViewComponent } from './components/debug-view/debug-view.component SweetAlert2Module, AgGridModule, NgxFloatUiModule, + NgIconsModule, ], exports: [ WebviewDirective, @@ -94,6 +106,11 @@ import { DebugViewComponent } from './components/debug-view/debug-view.component InputTradeskillComponent, InputSpellComponent, DebugViewComponent, + InputAlignmentComponent, + InputHostilityComponent, + InputAllegianceComponent, + InputCategoryComponent, + InputChallengeratingComponent, ], }) export class SharedModule {} 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 b5fae00..fb2dbb9 100644 --- a/src/app/tabs/items/items-editor/items-editor.component.html +++ b/src/app/tabs/items/items-editor/items-editor.component.html @@ -164,10 +164,7 @@
@@ -204,18 +201,11 @@
@@ -290,10 +280,7 @@
@@ -317,10 +304,7 @@
diff --git a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.html b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.html new file mode 100644 index 0000000..81cc290 --- /dev/null +++ b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.html @@ -0,0 +1,393 @@ +@let editingData = editing(); + +
+ + @for(tab of tabs; let i = $index; track tab.name) { + + {{ tab.name }} + + } + +
+ + +
+ +
+ +@switch (activeTab()) { +@case (0) { +
+
+
+ Internal ID + +
+ +
+ Name (optional) + +
+ +
+ Affiliation (optional) + +
+ +
+
+
+ Level + +
+
+ +
+
+ Skill + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ Hostility Group (optional) + +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+ @for(sprite of editingData.sprite; track $index) { +
+
+
+ +
+
+ +
+
+ @if($index === 0) { + + } @else { + + } +
+
+
+ } +
+ +
+
+
+
+ +
+
+ +
+
+ HP Multiplier + +
+
+ +
+
+ Skill on Kill + +
+
+
+ + @for(prop of coreProps; track $index) { +
+
+
+ {{ prop.name }} Min + +
+
+ +
+
+ {{ prop.name }} Max + +
+
+
+ } + +
+ +
+ +
+ + @for(stat of statOrder; track $index) { +
+
+ {{ stat | uppercase }} + +
+
+ } +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + @for(stat of statsInOrder(); track $index) { +
+
+
+ {{ stat }} + +
+
+ +
+
+ +
+
+
+ } +
+
+} + +@case (1) { +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + @for(trait of traitsInOrder(); track $index) { +
+
+
+ {{ trait }} Level + +
+
+ +
+
+ +
+
+
+ } +
+ +
+
+ +
+ +
+ + @for(spell of editingData.usableSkills; track $index) { +
+
+
+ +
+
+ +
+
+ Use Chance (Weight) + +
+
+ +
+
+ +
+
+
+ } +
+
+ +
+
+ +
+ + @for(baseEffect of editingData.baseEffects; track $index) { +
+
+
+ +
+
+ + @if(baseEffect.name === 'Attribute') { +
+
+ +
+
+ } + +
+
+ Potency + +
+
+ +
+
+ +
+
+
+ } +
+
+} + +@case (2) { +} + +@case (3) { +} + +@case (4) { +} +} + + + {{ editingData | json }} + diff --git a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.scss b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.scss new file mode 100644 index 0000000..f717bdb --- /dev/null +++ b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.scss @@ -0,0 +1,11 @@ +.sprite-col { + flex: 3; +} + +.stat-container { + flex-wrap: wrap; + + .stat-column { + flex: 1 1 30%; + } +} diff --git a/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts new file mode 100644 index 0000000..db8c002 --- /dev/null +++ b/src/app/tabs/npcs/npcs-editor/npcs-editor.component.ts @@ -0,0 +1,218 @@ +import { + Component, + computed, + effect, + inject, + OnInit, + signal, +} from '@angular/core'; +import { isNumber, sortBy } from 'lodash'; +import { INPCDefinition, StatType } from '../../../../interfaces'; +import { ElectronService } from '../../../services/electron.service'; +import { EditorBaseComponent } from '../../../shared/components/editor-base/editor-base.component'; + +@Component({ + selector: 'app-npcs-editor', + templateUrl: './npcs-editor.component.html', + styleUrl: './npcs-editor.component.scss', +}) +export class NpcsEditorComponent + extends EditorBaseComponent + implements OnInit +{ + private electronService = inject(ElectronService); + + public readonly key = 'npcs'; + public readonly tabs = [ + { name: 'Core Stats' }, + { name: 'Traits, Spells & Attributes' }, + { name: 'Gear' }, + { name: 'Drops' }, + { name: 'Triggers' }, + ]; + + public readonly coreProps: Array<{ + name: string; + prop: keyof INPCDefinition; + }> = [ + { name: 'HP', prop: 'hp' }, + { name: 'MP', prop: 'mp' }, + { name: 'XP', prop: 'giveXp' }, + { name: 'Gold', prop: 'gold' }, + ]; + + public readonly statOrder: StatType[] = [ + 'str', + 'dex', + 'agi', + 'int', + 'wis', + 'wil', + 'con', + 'cha', + 'luk', + ]; + + public currentStat = signal(undefined); + public currentTrait = signal(undefined); + + public statsInOrder = computed(() => { + const npc = this.editing(); + return sortBy(Object.keys(npc.otherStats)) as StatType[]; + }); + + public traitsInOrder = computed(() => { + const npc = this.editing(); + return sortBy(Object.keys(npc.traitLevels)); + }); + + public linkStats = signal(true); + + public canSave = computed(() => { + const data = this.editing(); + return data.npcId; + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + private challengeData = computed(() => this.modService.json()['challenge']); + + constructor() { + super(); + this.electronService.requestJSON('challenge'); + + effect(() => { + const data = this.challengeData(); + if (!data) return; + + this.changeCRStats(); + }); + } + + public addTrait(trait: string | undefined, value = 0) { + if (!trait) return; + + const npc = structuredClone(this.editing()); + npc.traitLevels[trait] = value; + this.editing.set(npc); + } + + public removeTrait(trait: string) { + const npc = structuredClone(this.editing()); + delete npc.traitLevels[trait]; + this.editing.set(npc); + } + + public hasTrait(trait: string | undefined) { + if (!trait) return; + return isNumber(this.editing().traitLevels[trait]); + } + + public addStat(stat: StatType, value = 0) { + const npc = structuredClone(this.editing()); + npc.otherStats[stat] = value; + this.editing.set(npc); + } + + public removeStat(stat: StatType) { + const npc = structuredClone(this.editing()); + delete npc.otherStats[stat]; + this.editing.set(npc); + } + + public hasStat(stat: StatType) { + return isNumber(this.editing().otherStats[stat]); + } + + public addSprite() { + this.update('sprite', [...this.editing().sprite, 0]); + } + + public removeSprite(index: number) { + const spriteArr = this.editing().sprite; + spriteArr.splice(index, 1); + + this.update('sprite', spriteArr); + } + + public addSpell() { + const npc = this.editing(); + npc.usableSkills.push({ + result: undefined as unknown as string, + chance: 1, + }); + this.editing.set(npc); + } + + public removeSpell(index: number) { + const npc = this.editing(); + npc.usableSkills.splice(index, 1); + this.editing.set(npc); + } + + public addBaseEffect() { + const npc = this.editing(); + npc.baseEffects.push({ + endsAt: -1, + name: undefined as unknown as string, + extra: { + potency: 1, + damageType: undefined, + }, + }); + this.editing.set(npc); + } + + public removeBaseEffect(index: number) { + const npc = this.editing(); + npc.baseEffects.splice(index, 1); + this.editing.set(npc); + } + + public changeCRStats() { + const npc = this.editing(); + const challengeData = this.challengeData(); + + const level = npc.level; + npc.hp = structuredClone(challengeData.global.stats.hp[level]); + npc.mp = structuredClone(challengeData.global.stats.mp[level]); + npc.giveXp = structuredClone(challengeData.global.stats.giveXp[level]); + npc.gold = structuredClone(challengeData.global.stats.gold[level]); + + npc.hp.min = Math.floor(npc.hp.min * npc.hpMult); + npc.hp.max = Math.floor(npc.hp.max * npc.hpMult); + + this.editing.set(npc); + } + + public updateStatsIfLinked(statValue: number) { + if (!this.linkStats()) return; + + this.statOrder.forEach((stat) => { + this.editing.update((d) => { + d.stats[stat] = statValue; + return d; + }); + }); + } + + private checkLinkedStats() { + const stats = this.editing().stats; + const firstStat = stats[this.statOrder[0]]; + const isLinked = this.statOrder.every((s) => stats[s] === firstStat); + this.linkStats.set(isLinked); + } + + ngOnInit(): void { + this.checkLinkedStats(); + super.ngOnInit(); + } + + doSave() { + const npc = this.editing(); + npc.usableSkills = npc.usableSkills.filter((f) => f.result); + npc.baseEffects = npc.baseEffects.filter((f) => f.name); + this.editing.set(npc); + + super.doSave(); + } +} diff --git a/src/app/tabs/npcs/npcs.component.html b/src/app/tabs/npcs/npcs.component.html index cf40e6d..61cf422 100644 --- a/src/app/tabs/npcs/npcs.component.html +++ b/src/app/tabs/npcs/npcs.component.html @@ -1 +1,7 @@ -

npcs works!

+@if(!isEditing()) { + +} @else { + +} diff --git a/src/app/tabs/npcs/npcs.component.ts b/src/app/tabs/npcs/npcs.component.ts index ddc436f..4b3e94c 100644 --- a/src/app/tabs/npcs/npcs.component.ts +++ b/src/app/tabs/npcs/npcs.component.ts @@ -1,8 +1,119 @@ -import { Component } from '@angular/core'; +import { Component, computed } from '@angular/core'; +import { ColDef } from 'ag-grid-community'; + +import { INPCDefinition } from '../../../interfaces'; +import { defaultNPC } from '../../helpers'; +import { CellButtonsComponent } from '../../shared/components/cell-buttons/cell-buttons.component'; +import { CellSpriteComponent } from '../../shared/components/cell-sprite/cell-sprite.component'; +import { EditorBaseTableComponent } from '../../shared/components/editor-base-table/editor-base-table.component'; +import { HeaderButtonsComponent } from '../../shared/components/header-buttons/header-buttons.component'; + +type EditingType = INPCDefinition; @Component({ selector: 'app-npcs', templateUrl: './npcs.component.html', styleUrl: './npcs.component.scss', }) -export class NpcsComponent {} +export class NpcsComponent extends EditorBaseTableComponent { + public defaultData = defaultNPC; + + public tableItems = computed(() => this.modService.mod().npcs); + public tableColumns: ColDef[] = [ + { + field: 'sprite', + headerName: '', + resizable: false, + sortable: false, + width: 100, + cellRenderer: CellSpriteComponent, + cellRendererParams: { type: 'creatures' }, + }, + { + field: 'npcId', + headerName: 'ID', + flex: 1, + cellDataType: 'text', + filter: 'agTextColumnFilter', + sort: 'asc', + }, + { + field: 'level', + flex: 1, + cellDataType: 'number', + filter: 'agNumberColumnFilter', + }, + { + field: 'cr', + headerName: 'Challenge Rating', + flex: 1, + cellDataType: 'number', + filter: 'agNumberColumnFilter', + }, + { + field: 'baseClass', + headerName: 'Class', + flex: 1, + cellDataType: 'text', + filter: 'agTextColumnFilter', + }, + { + field: 'monsterClass', + headerName: 'Category', + flex: 1, + cellDataType: 'text', + filter: 'agTextColumnFilter', + }, + { + field: 'allegiance', + headerName: 'Faction', + flex: 1, + cellDataType: 'text', + filter: 'agTextColumnFilter', + }, + { + field: '', + width: 200, + sortable: false, + suppressMovable: true, + headerComponent: HeaderButtonsComponent, + headerComponentParams: { + showNewButton: true, + newCallback: () => this.createNew(), + }, + cellRenderer: CellButtonsComponent, + cellClass: 'no-adjust', + cellRendererParams: { + showCopyButton: true, + copyCallback: (item: EditingType) => { + const newItem = structuredClone(item); + newItem.npcId = `${newItem.npcId} (copy)`; + this.saveNewData(newItem); + }, + showEditButton: true, + editCallback: (item: EditingType) => this.editExisting(item), + showDeleteButton: true, + deleteCallback: (item: EditingType) => this.deleteData(item), + }, + }, + ]; + + public saveNewData(data: EditingType) { + super.saveNewData(data); + + const oldItem = this.oldData(); + if (oldItem) { + this.oldData.set(undefined); + this.modService.editNPC(oldItem, data); + return; + } + + this.modService.addNPC(data); + } + + public deleteData(data: EditingType) { + super.deleteData(data); + + this.modService.removeNPC(data); + } +} diff --git a/src/interfaces/building-blocks.ts b/src/interfaces/building-blocks.ts index 02214c8..0d2505c 100644 --- a/src/interfaces/building-blocks.ts +++ b/src/interfaces/building-blocks.ts @@ -19,12 +19,16 @@ export enum Allegiance { GM = 'GM', } +export type AllegianceType = `${Allegiance}`; + export enum Alignment { Good = 'good', Neutral = 'neutral', Evil = 'evil', } +export type AlignmentType = `${Alignment}`; + export enum DamageClass { Physical = 'physical', Necrotic = 'necrotic', @@ -243,6 +247,8 @@ export enum Hostility { Always = 'Always', } +export type HostilityType = `${Hostility}`; + export enum FOVVisibility { CantSee = 0, CanSee = 1, @@ -290,6 +296,8 @@ export enum MonsterClass { Undead = 'Undead', } +export type MonsterClassType = `${MonsterClass}`; + export type SkillBlock = Partial>; export type TradeskillBlock = Partial>; diff --git a/src/interfaces/npc.ts b/src/interfaces/npc.ts index 98854fb..c356256 100644 --- a/src/interfaces/npc.ts +++ b/src/interfaces/npc.ts @@ -1,20 +1,21 @@ import { IBehavior, IDialogTree } from './behavior'; import { - Alignment, - Allegiance, - BaseClass, - Hostility, + AlignmentType, + AllegianceType, + BaseClassType, + DamageClassType, + HostilityType, ItemSlot, - MonsterClass, + MonsterClassType, RandomNumber, Rollable, Skill, SkillBlock, Stat, StatBlock, + StatType, } from './building-blocks'; import { HasIdentification } from './identified'; -import { IStatusEffectInfo } from './mod-stripped'; export enum NPCTriggerType { Spawn = 'spawn', @@ -26,22 +27,22 @@ export interface INPCDefinition extends HasIdentification { npcId: string; // the sprite or sprites this creature can be - sprite: number | number[]; + sprite: number[]; // the npc name - optional - if unspecified, generated randomly - name?: string[]; + name?: string; // the npc "guild" that it belongs to affiliation?: string; // the alignment of this npc - alignment?: Alignment; + alignment?: AlignmentType; // the allegiance of the npc - determines basic reps - allegiance?: Allegiance; + allegiance: AllegianceType; // the current reputation (how it views other allegiances) - allegianceReputation?: Partial>; + allegianceReputation?: Partial>; // whether the npc can only use water aquaticOnly?: boolean; @@ -50,13 +51,16 @@ export interface INPCDefinition extends HasIdentification { avoidWater?: boolean; // the base class of the creature - baseClass?: BaseClass; + baseClass?: BaseClassType; // the base effects given to the creature (usually attributes/truesight/etc) - baseEffects?: Array<{ + baseEffects: Array<{ name: string; endsAt: number; - extra: IStatusEffectInfo; + extra: { + potency: number; + damageType?: DamageClassType; + }; }>; // the behaviors for the npc @@ -82,7 +86,7 @@ export interface INPCDefinition extends HasIdentification { drops?: Rollable[]; // the hp multiplier for the npc - hpMult?: number; + hpMult: number; // extra properties pulled in from the map, varies depending on the NPC extraProps?: any; @@ -104,7 +108,7 @@ export interface INPCDefinition extends HasIdentification { maxWanderRandomlyDistance?: number; // the creature class (used for rippers, etc) - monsterClass?: MonsterClass; + monsterClass?: MonsterClassType; // the monster grouping, so Hostility.Always dont infight with themselves monsterGroup?: string; @@ -113,10 +117,10 @@ export interface INPCDefinition extends HasIdentification { owner?: string; // the "other stats" for this npc, inherited from NPC definition - otherStats?: Partial>; + otherStats: Partial>; // how hostile the creature is (default: always) - hostility?: Hostility; + hostility: HostilityType; // the base hp/mp/gold/xp for the creature hp: RandomNumber; @@ -131,11 +135,13 @@ export interface INPCDefinition extends HasIdentification { noItemDrop?: boolean; // the reputation modifications for the killer when this npc is killed - repMod: Array<{ allegiance: Allegiance; delta: number }>; + repMod: Array<{ allegiance: AllegianceType; delta: number }>; // the amount of skill gained by the party when this creature is killed skillOnKill: number; + skillLevels: number; + // the skills this creature has skills: SkillBlock; @@ -155,7 +161,7 @@ export interface INPCDefinition extends HasIdentification { tansFor?: string; // the trait levels this creature has - traitLevels?: Record; + traitLevels: Record; // npc triggers triggers?: Partial>; @@ -170,5 +176,5 @@ export interface INPCDefinition extends HasIdentification { y?: number; // the challenge rating of the creature - scales the hp / damageFactor - cr?: number; + cr: number; } diff --git a/src/styles.scss b/src/styles.scss index 0ab6eb2..188cf3f 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -94,4 +94,5 @@ ag-grid-angular { @apply input-sm; @apply input-bordered; @apply w-full; + @apply h-9; }