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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 @@
+
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 }}
-
-
-
-
-
-
{{ 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) {
+
+}
+
+@case (1) {
+
+}
+
+@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;
}