Explorar el Código

implemented a new spell system that completely works. Styling still missing

Warafear hace 1 año
padre
commit
71151db645

+ 72 - 0
.nx/cache/d/daemon.log

@@ -463402,3 +463402,75 @@ To fix this, set a unique name for each project in a project.json inside the pro
     at async processFilesAndCreateAndSerializeProjectGraph (c:\Softwareprojekte\DnD\node_modules\nx\src\daemon\server\project-graph-incremental-recomputation.js:138:17)
 [NX Daemon Server] - 2024-01-26T06:34:52.561Z - Time taken for 'hash changed files from watcher' 26.63969999551773ms
 [NX Daemon Server] - 2024-01-26T06:34:52.561Z - Done responding to the client null
+[NX Daemon Server] - 2024-01-27T08:03:11.547Z - Started listening on: \\.\pipe\nx\C:\Users\chris\AppData\Local\Temp\83d14e7134fc08a15480\d.sock
+[NX Daemon Server] - 2024-01-27T08:03:11.550Z - [WATCHER]: Subscribed to changes within: c:\Softwareprojekte\DnD (native)
+[NX Daemon Server] - 2024-01-27T08:03:11.558Z - Established a connection. Number of open connections: 1
+[NX Daemon Server] - 2024-01-27T08:03:11.561Z - Closed a connection. Number of open connections: 0
+[NX Daemon Server] - 2024-01-27T08:03:11.562Z - Established a connection. Number of open connections: 1
+[NX Daemon Server] - 2024-01-27T08:03:11.565Z - [REQUEST]: Client Request for Project Graph Received
+[NX Daemon Server] - 2024-01-27T08:03:12.486Z - Error detected when recomputing project file map: The following projects are defined in multiple locations:
+- DnDTools: 
+  - 
+  - .
+
+To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.
+[NX Daemon Server] - 2024-01-27T08:03:12.486Z - [REQUEST]: Responding to the client with an error. Error when preparing serialized project graph. The following projects are defined in multiple locations:
+- DnDTools: 
+  - 
+  - .
+
+To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.
+Error: The following projects are defined in multiple locations:
+- DnDTools: 
+  - 
+  - .
+
+To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.
+    at readProjectConfigurationsFromRootMap (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\project-configuration-utils.js:97:15)
+    at buildProjectsConfigurationsFromProjectPathsAndPlugins (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\project-configuration-utils.js:70:19)
+    at createProjectConfigurations (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:131:129)
+    at WorkspaceContext.<anonymous> (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:82:39)
+    at getProjectConfigurationsFromContext (c:\Softwareprojekte\DnD\node_modules\nx\src\utils\workspace-context.js:26:29)
+    at _retrieveProjectConfigurations (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:81:72)
+    at retrieveProjectConfigurations (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:58:12)
+    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
+    at async processCollectedUpdatedAndDeletedFiles (c:\Softwareprojekte\DnD\node_modules\nx\src\daemon\server\project-graph-incremental-recomputation.js:102:34)
+    at async processFilesAndCreateAndSerializeProjectGraph (c:\Softwareprojekte\DnD\node_modules\nx\src\daemon\server\project-graph-incremental-recomputation.js:138:17)
+[NX Daemon Server] - 2024-01-27T08:03:12.487Z - Time taken for 'hash changed files from watcher' 17.576200008392334ms
+[NX Daemon Server] - 2024-01-27T08:03:12.488Z - Done responding to the client null
+[NX Daemon Server] - 2024-01-27T15:44:47.258Z - Started listening on: \\.\pipe\nx\C:\Users\chris\AppData\Local\Temp\83d14e7134fc08a15480\d.sock
+[NX Daemon Server] - 2024-01-27T15:44:47.265Z - [WATCHER]: Subscribed to changes within: c:\Softwareprojekte\DnD (native)
+[NX Daemon Server] - 2024-01-27T15:44:47.267Z - Established a connection. Number of open connections: 1
+[NX Daemon Server] - 2024-01-27T15:44:47.268Z - Established a connection. Number of open connections: 2
+[NX Daemon Server] - 2024-01-27T15:44:47.269Z - Closed a connection. Number of open connections: 1
+[NX Daemon Server] - 2024-01-27T15:44:47.272Z - [REQUEST]: Client Request for Project Graph Received
+[NX Daemon Server] - 2024-01-27T15:44:48.283Z - Error detected when recomputing project file map: The following projects are defined in multiple locations:
+- DnDTools: 
+  - 
+  - .
+
+To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.
+[NX Daemon Server] - 2024-01-27T15:44:48.284Z - [REQUEST]: Responding to the client with an error. Error when preparing serialized project graph. The following projects are defined in multiple locations:
+- DnDTools: 
+  - 
+  - .
+
+To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.
+Error: The following projects are defined in multiple locations:
+- DnDTools: 
+  - 
+  - .
+
+To fix this, set a unique name for each project in a project.json inside the project's root. If the project does not currently have a project.json, you can create one that contains only a name.
+    at readProjectConfigurationsFromRootMap (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\project-configuration-utils.js:97:15)
+    at buildProjectsConfigurationsFromProjectPathsAndPlugins (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\project-configuration-utils.js:70:19)
+    at createProjectConfigurations (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:131:129)
+    at WorkspaceContext.<anonymous> (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:82:39)
+    at getProjectConfigurationsFromContext (c:\Softwareprojekte\DnD\node_modules\nx\src\utils\workspace-context.js:26:29)
+    at _retrieveProjectConfigurations (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:81:72)
+    at retrieveProjectConfigurations (c:\Softwareprojekte\DnD\node_modules\nx\src\project-graph\utils\retrieve-workspace-files.js:58:12)
+    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
+    at async processCollectedUpdatedAndDeletedFiles (c:\Softwareprojekte\DnD\node_modules\nx\src\daemon\server\project-graph-incremental-recomputation.js:102:34)
+    at async processFilesAndCreateAndSerializeProjectGraph (c:\Softwareprojekte\DnD\node_modules\nx\src\daemon\server\project-graph-incremental-recomputation.js:138:17)
+[NX Daemon Server] - 2024-01-27T15:44:48.285Z - Time taken for 'hash changed files from watcher' 22.69249999523163ms
+[NX Daemon Server] - 2024-01-27T15:44:48.286Z - Done responding to the client null

+ 1 - 1
.nx/cache/d/server-process.json

@@ -1 +1 @@
-{"processId":1292}
+{"processId":22332}

+ 1 - 1
src/app/character/character-creator/character-creator.component.ts

@@ -354,7 +354,7 @@ export class CharacterCreatorComponent {
         this.characterName,
         {
           spells: [],
-          id: 0,
+          id: 1000,
         },
         'customSpells'
       ),

+ 12 - 40
src/app/journal/journal-spellcards/add-card/add-card.component.html

@@ -8,54 +8,20 @@
     class="add-button"
     (click)="continueToSpellSelection(false); state = 3"
   >
-    Offiziellen Zauber auswählen
+    Aus bekannten Zaubern wählen
   </button>
   <button
     class="add-button"
     (click)="continueToSpellSelection(true); state = 3"
   >
-    Offiziellen Zauber bearbeiten
+    Einen bekannten Zauber bearbeiten
   </button>
   <button class="add-button" (click)="emitNewSpell()">
-    Neuen Zauber erstellen
+    Einen neuen Zauber erstellen
   </button>
-  } @else if (state === 3) {
 
-  <input
-    type="text"
-    class="spell-name"
-    [(ngModel)]="newSpellName"
-    placeholder="Zauber durchsuchen"
-    (keyup)="
-      isModification ? filterSpellArrayForModification() : filterSpellArray()
-    "
-  />
-  <div class="available-spells">
-    <ul>
-      @for(spell of availableSpells; track spell) {
-      <li>
-        <button (click)="spellSelected(spell)">
-          {{ spell.german }}
-        </button>
-      </li>
-      } @empty { Keine Zauber gefunden }
-    </ul>
-  </div>
   <button class="abort-button" (click)="resetThis()">Abbrechen</button>
-  }
-
-  <!-- @if(isModification == undefined) {
-  <img class="add-icon" src="assets/icons/UIIcons/add.svg" />
-  <button class="add-button" (click)="continueToSpellSelection(false)">
-    Offiziellen Zauber auswählen
-  </button>
-  <button class="add-button" (click)="continueToSpellSelection(true)">
-    Offiziellen Zauber bearbeiten
-  </button>
-  <button class="add-button" (click)="emitNewSpell()">
-    Neuen Zauber erstellen
-  </button>
-  } @else {
+  } @else if (state === 3) {
 
   <input
     type="text"
@@ -70,7 +36,13 @@
     <ul>
       @for(spell of availableSpells; track spell) {
       <li>
-        <button (click)="spellSelected(spell)">
+        <button
+          (click)="
+            isModification
+              ? emitNewSpellFromOfficial(spell)
+              : spellSelected(spell)
+          "
+        >
           {{ spell.german }}
         </button>
       </li>
@@ -78,5 +50,5 @@
     </ul>
   </div>
   <button class="abort-button" (click)="resetThis()">Abbrechen</button>
-  } -->
+  }
 </div>

+ 16 - 11
src/app/journal/journal-spellcards/add-card/add-card.component.ts

@@ -1,7 +1,6 @@
 import { Component, Input, Output, EventEmitter } from '@angular/core';
 import { DataService } from 'src/services/data/data.service';
-
-// import { FormsModule } from '@angular/forms';
+import { Spell } from 'src/interfaces/spell';
 import { SpellsService } from 'src/services/spells/spells.service';
 
 @Component({
@@ -14,6 +13,7 @@ export class AddCardComponent {
   @Input() alreadyUsedSpells!: any[];
 
   @Output() createNewSpell = new EventEmitter<any>();
+  @Output() createNewSpellFromOfficial = new EventEmitter<any>();
   @Output() onSpellSelected = new EventEmitter<any>();
 
   public newSpellName: string = '';
@@ -28,13 +28,10 @@ export class AddCardComponent {
   ) {}
 
   public ngOnInit(): void {
-    this.allAvailableSpells = this.spellsAccessor.getSpellsByLevel(
+    this.allAvailableSpells = this.spellsAccessor.getAvailableSpells(
       this.level,
       this.dataAccessor.characterData.class
     );
-    console.warn(this.allAvailableSpells);
-    console.warn('Already used spells: ', this.alreadyUsedSpells);
-
     this.filterSpellArray();
     this.spellsAccessor.closeSubject$.subscribe((level) => {
       if (level !== this.level) {
@@ -44,11 +41,23 @@ export class AddCardComponent {
   }
 
   public emitNewSpell(): void {
+    console.warn('Emitting new spell to create: ', this.level);
     this.createNewSpell.emit(this.level);
     this.resetThis();
   }
 
+  public emitNewSpellFromOfficial(spell: Spell): void {
+    console.warn('Emitting new spell to modify from official: ', spell);
+
+    this.createNewSpellFromOfficial.emit(spell);
+    this.resetThis();
+  }
+
   public continueToSpellSelection(modify: boolean): void {
+    this.allAvailableSpells = this.spellsAccessor.getAvailableSpells(
+      this.level,
+      this.dataAccessor.characterData.class
+    );
     this.filterSpellArray();
     this.isModification = modify;
     if (modify) {
@@ -60,11 +69,7 @@ export class AddCardComponent {
   }
 
   public spellSelected(spell: any): void {
-    const response = {
-      spell: spell,
-      isToModify: this.isModification,
-    };
-    this.onSpellSelected.emit(response);
+    this.onSpellSelected.emit(spell);
     this.resetThis();
   }
 

+ 7 - 3
src/app/journal/journal-spellcards/journal-spellcards.component.html

@@ -1,6 +1,7 @@
 <div class="spellcards-container">
   <div cdkDropListGroup>
-    @for(level of [0,1,2,3,4,5,6,7,8,9]; track level; let index = $index) {
+    <!-- TODO: revert array to 0-9 -->
+    @for(level of [0,1]; track level; let index = $index) {
 
     <div class="example-container">
       <div
@@ -38,14 +39,17 @@
           (click)="showFullSpellcard(spell, level, spellIndex)"
         ></spellcard>
         } @if (draggingIndex === index){
-        <div class="deletion-card" [id]="'deletion' + index">
+        <div class="removal-card" [id]="'deletion' + index">
           Hier zum Löschen ablegen
         </div>
         } @else {
         <add-card
           [level]="index"
           [alreadyUsedSpells]="getUsedIDs(index)"
-          (createNewSpell)="openSpellModal(false, index)"
+          (createNewSpell)="openSpellCreationModal(index, false)"
+          (createNewSpellFromOfficial)="
+            openSpellCreationModal(index, true, $event)
+          "
           (onSpellSelected)="handleSpellSelection($event, index)"
         ></add-card>
         }

+ 5 - 6
src/app/journal/journal-spellcards/journal-spellcards.component.scss

@@ -103,9 +103,9 @@
   }
 }
 
-.deletion-card {
-  height: 14rem;
-  width: 10rem;
+.removal-card {
+  height: 20rem;
+  width: 15rem;
   font-size: 1.25rem;
   font-weight: 600;
   border: solid 1px var(--border-color);
@@ -119,7 +119,7 @@
   box-shadow: var(--shadow);
 
   &:hover {
-    scale: 1.1;
+    scale: 1.05;
   }
 }
 
@@ -137,8 +137,7 @@
 .cdk-drag-placeholder {
   background: #e0e0e0;
   border: dotted 3px #999;
-  border-radius: 10px;
-  min-height: 60px;
+  border-radius: 100px;
   transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
   color: transparent !important;
 

+ 80 - 30
src/app/journal/journal-spellcards/journal-spellcards.component.ts

@@ -54,7 +54,8 @@ export class JournalSpellcardsComponent {
 
   public constructor(
     public dataAccessor: DataService,
-    private modalAccessor: ModalService
+    private modalAccessor: ModalService,
+    private spellsService: SpellsService
   ) {
     this.loadSpells();
     this.hideEmptySpelllists();
@@ -88,11 +89,18 @@ export class JournalSpellcardsComponent {
       (result) => {
         resultSubscription.unsubscribe();
         if (result.state === 'delete') {
+          this.spellsService.deleteCustomSpell(spell);
+          this.dataAccessor.deleteCustomSpell(spell);
+          this.dataAccessor.removeFavoriteSpell(spell);
+          this.getSpellList(level).splice(spellIndex, 1);
+          this.updateSpellsInDatabase(level);
+        } else if (result.state === 'remove') {
+          this.dataAccessor.removeFavoriteSpell(spell);
           this.getSpellList(level).splice(spellIndex, 1);
           this.updateSpellsInDatabase(level);
         } else if (result.state === 'update') {
           setTimeout(() => {
-            this.openSpellModal(true, level, spellIndex);
+            this.openSpellModificationModal(level, spellIndex);
           }, 100);
         } else if (result.state === 'add') {
           this.dataAccessor.addFavoriteSpell(spell);
@@ -101,62 +109,74 @@ export class JournalSpellcardsComponent {
     );
   }
 
-  public openSpellModal(
-    isUpdate: boolean,
-    level: number,
-    spellIndex?: number
-  ): void {
+  // TODO: Update favorites, when modifying a spell
+  public openSpellModificationModal(level: number, index: number): void {
     this.modalAccessor.openModal(SpellModalComponent, {
       spell:
-        spellIndex !== undefined
-          ? JSON.parse(JSON.stringify(this.getSpellList(level)![spellIndex]))
+        index !== undefined
+          ? JSON.parse(JSON.stringify(this.getSpellList(level)![index]))
           : undefined,
-      isUpdate: isUpdate,
-      level: level,
+      isModification: true,
     });
     const resultSubscription = this.modalAccessor.result$.subscribe(
       (result) => {
         if (result.state === 'update') {
           // level was not modified
           if (level === result.data.level) {
-            this.updateSpell(result.data, level!, spellIndex!);
+            this.updateSpell(result.data, level, index);
+            this.dataAccessor.updateFavoriteSpell(result.data);
+            this.dataAccessor.updateCustomSpell(result.data);
           } else {
             // level was modified
-            this.getSpellList(level).splice(spellIndex!, 1);
+            this.getSpellList(level).splice(index, 1);
             this.addSpell(result.data, result.data.level);
           }
-        } else if (result.state === 'add') {
-          this.addSpell(result.data, level);
+        } else {
+          throw new Error('REsult state from modal: ' + result.state);
         }
         resultSubscription.unsubscribe();
       }
     );
   }
 
-  public openSpellModalModifyOfficial(spell: Spell): void {
+  /**
+   * In this modal new spells can be created. This can be completely new spells or
+   * modified official spells. If successful, the spell is added to the spell list,
+   * sent to the daService and sent to the spellsService which in return sends it to the dataBase
+   * @param level
+   * @param isBasedOnOfficialSpell
+   * @param spell
+   */
+  public openSpellCreationModal(
+    level: number,
+    isBasedOnOfficialSpell: boolean,
+    spell?: Spell
+  ): void {
     this.modalAccessor.openModal(SpellModalComponent, {
-      spell: spell,
-      isUpdate: true,
-      level: spell.level,
+      spell: isBasedOnOfficialSpell ? spell : undefined,
+      level: level,
+      id: this.dataAccessor.customSpellId,
+      isBasedOnOfficialSpell: isBasedOnOfficialSpell,
+      classes: [this.dataAccessor.characterData.class],
     });
     const resultSubscription = this.modalAccessor.result$.subscribe(
       (result) => {
-        if (result.state === 'update') {
-          this.addSpell(result.data, result.data.level);
+        if (result.state === 'add') {
+          console.warn('Add spell: ', result.data);
+
+          this.addSpell(result.data, level);
+          // this.spellsService.addCustomSpell(result.data);
+          this.dataAccessor.addCustomSpell(result.data);
+        } else {
+          console.warn('Result state from modal: ' + result.state);
         }
         resultSubscription.unsubscribe();
       }
     );
   }
 
-  public handleSpellSelection(data: any, level: number): void {
-    const newSpell = data.spell;
-    const isToModify = data.isToModify;
-    if (isToModify) {
-      this.openSpellModalModifyOfficial(newSpell);
-    } else {
-      this.addSpell(newSpell, level);
-    }
+  public handleSpellSelection(spell: Spell, level: number): void {
+    this.addSpell(spell, level);
   }
 
   public addSpell(spell: Spell, level: number) {
@@ -253,42 +273,72 @@ export class JournalSpellcardsComponent {
         case 'cdk-drop-list-0':
           movedSpell.level = 0;
           this.updateSpellsInDatabase(0);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-1':
           movedSpell.level = 1;
           this.updateSpellsInDatabase(1);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-2':
           movedSpell.level = 2;
           this.updateSpellsInDatabase(2);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-3':
           movedSpell.level = 3;
           this.updateSpellsInDatabase(3);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-4':
           movedSpell.level = 4;
           this.updateSpellsInDatabase(4);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-5':
           movedSpell.level = 5;
           this.updateSpellsInDatabase(5);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-6':
           movedSpell.level = 6;
           this.updateSpellsInDatabase(6);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-7':
           movedSpell.level = 7;
           this.updateSpellsInDatabase(7);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-8':
           movedSpell.level = 8;
           this.updateSpellsInDatabase(8);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
         case 'cdk-drop-list-9':
           movedSpell.level = 9;
           this.updateSpellsInDatabase(9);
+          if (movedSpell.isCustom) {
+            this.dataAccessor.updateCustomSpell(movedSpell);
+          }
           break;
       }
     }
@@ -326,7 +376,7 @@ export class JournalSpellcardsComponent {
   }
 
   public dragEnd(event: any) {
-    if (event.event.target.classList.contains('deletion-card')) {
+    if (event.event.target.classList.contains('removal-card')) {
       this.getSpellList(this.draggingIndex!).splice(
         event.source.element.nativeElement.id,
         1

+ 2 - 0
src/app/journal/journal-spellcards/spellcard/spellcard.component.html

@@ -2,4 +2,6 @@
   <div class="name">{{ spell.german }}</div>
   <div>{{ spell.cost }}</div>
   <div>{{ spell.school }}</div>
+  <div>ID: {{ spell.id }}</div>
+  <div>{{ spell.classes }}</div>
 </div>

+ 3 - 3
src/app/journal/spell-modal/spell-modal.component.html

@@ -1,6 +1,6 @@
 <div class="modal-dimensions">
   <h2 style="text-align: center">
-    @if(isUpdate){Zauber anpassen}@else{Zauber hinzufügen}
+    @if(isModification){Zauber bearbeiten}@else{Zauber erstellen}
   </h2>
 
   <div class="add-form-group">
@@ -145,7 +145,7 @@
 
     <!-- Button section -->
     <div class="button-wrapper-2-block">
-      @if(isUpdate){
+      @if(isModification){
       <ui-button
         [type]="'update'"
         [size]="'xlarge'"
@@ -154,7 +154,7 @@
       ></ui-button>
       }@else{
       <ui-button
-        *ngIf="!isUpdate"
+        *ngIf="!isModification"
         [type]="'add'"
         [size]="'xlarge'"
         [color]="'primary'"

+ 27 - 9
src/app/journal/spell-modal/spell-modal.component.ts

@@ -12,13 +12,15 @@ import { Spell } from 'src/interfaces/spell';
 export class SpellModalComponent {
   public constructor(private modalAccessor: ModalService) {}
   @Input() public spell: any;
-  @Input() public isUpdate: boolean = false;
   @Input() public level: number = 0;
+  @Input() classes: string[] = [];
+  @Input() public id: number = 0;
+  @Input() public isModification: boolean = false;
+  @Input() public isBasedOnOfficialSpell: boolean = false;
 
+  // #region Properties
   public german: string = '';
   public english: string = '';
-  public id: number = 0;
-  public classes: string[] = [];
   public cost: string = 'action';
   public canRitual: boolean = false;
   public needsConcentration: boolean = false;
@@ -45,7 +47,9 @@ export class SpellModalComponent {
   public radius: number = 0;
   public areaOfEffectType: string = '';
 
-  // Options for the select boxes
+  // #endregion
+
+  // #region OPTIONS
   public areaTypes: any[] = [
     { display: 'Kegel', value: 'cone' },
     { display: 'Kugel', value: 'sphere' },
@@ -112,13 +116,15 @@ export class SpellModalComponent {
     { display: 'Reaktion', value: 'reaction' },
   ];
 
+  // #endregion
+
   public ngOnInit(): void {
-    if (this.isUpdate) {
+    if (this.isModification || this.isBasedOnOfficialSpell) {
       this.loadspell();
     }
   }
 
-  // RESPONSES
+  // #region RESPONSES
 
   public cancel(): void {
     this.modalAccessor.handleModalClosing('cancel', undefined);
@@ -135,11 +141,20 @@ export class SpellModalComponent {
     this.resetSpell();
   }
 
+  // #endregion
+
+  // #region FUNCTIONS
+
   private loadspell(): void {
-    this.id = this.spell.id;
-    this.german = this.spell.german;
+    if (this.isModification) {
+      this.id = this.spell.id;
+      this.german = this.spell.german;
+      this.classes = this.spell.classes;
+    } else {
+      this.german = this.spell.german + ' (Kopie)';
+    }
+
     this.english = this.spell.english;
-    this.classes = this.spell.classes;
     this.level = this.spell.level;
     this.cost = this.spell.cost;
     this.canRitual = this.spell.canRitual;
@@ -167,6 +182,7 @@ export class SpellModalComponent {
   private createSpell(): Spell {
     const spell: Spell = {
       id: this.id,
+      isCustom: true,
       english: this.english,
       german: this.german,
       classes: this.classes,
@@ -232,4 +248,6 @@ export class SpellModalComponent {
   public removeDamage(index: number): void {
     this.damage.splice(index, 1);
   }
+
+  // #endregion
 }

+ 9 - 3
src/app/shared-components/full-spellcard/full-spellcard.component.html

@@ -113,7 +113,11 @@
   </div>
 
   <div class="delete-row">
-    @if(!isFromDashboard){
+    @if(isFromDashboard){
+    <button class="delete-button" (click)="delete()">Entfernen</button>
+    }@else {
+
+    <!-- Add to favorites -->
     <button
       [class]="alreadyInFavorites ? 'disabled add-button' : 'add-button'"
       (click)="alreadyInFavorites ? '' : addToFavorites()"
@@ -121,10 +125,12 @@
       @if(alreadyInFavorites){ Bereits in Favoriten} @else{ Zu Favoriten
       hinzufügen }
     </button>
+    <!-- Modify spell (only available for custom spells) -->
+    @if(spell.isCustom){
     <button class="edit-button" (click)="update()">Anpassen</button>
+    }
+    <button class="delete-button" (click)="remove()">Entfernen</button>
     <button class="delete-button" (click)="delete()">Löschen</button>
-    } @else {
-    <button class="delete-button" (click)="delete()">Entfernen</button>
     }
   </div>
 </div>

+ 4 - 0
src/app/shared-components/full-spellcard/full-spellcard.component.ts

@@ -18,6 +18,10 @@ export class FullSpellcardComponent {
     this.modalAccessor.handleModalClosing('delete', undefined);
   }
 
+  public remove(): void {
+    this.modalAccessor.handleModalClosing('remove', undefined);
+  }
+
   public update(): void {
     this.modalAccessor.handleModalClosing('update', undefined);
   }

+ 1 - 0
src/interfaces/spell.ts

@@ -1,5 +1,6 @@
 export interface Spell {
   id: number;
+  isCustom: boolean;
   german: string;
   english: string;
   classes: string[];

+ 77 - 5
src/services/data/data.service.ts

@@ -9,6 +9,7 @@ import { Ability } from 'src/interfaces/ability';
 import { Trait } from 'src/interfaces/traits';
 import { SimpleItem } from 'src/interfaces/simple-item';
 import { Food } from 'src/interfaces/food';
+import { SpellsService } from '../spells/spells.service';
 
 @Injectable({
   providedIn: 'root',
@@ -19,9 +20,9 @@ export class DataService {
   public dataLoaded = false;
   public characterName: string = '';
 
-  constructor() {
+  constructor(private spellsService: SpellsService) {
     this.db = new Localbase('DNDTools');
-    // this.db.config.debug = false;
+    this.db.config.debug = false;
   }
 
   async loadData(): Promise<any> {
@@ -103,7 +104,8 @@ export class DataService {
 
     // Spells
 
-    this.customSpells = customSpellsData.data;
+    this.customSpellId = customSpellsData.id;
+    this.customSpells = customSpellsData.spells;
     this.spellslots = spellslotsData;
     this.favoriteSpells = favoriteSpellsData.spells;
     this.spellLevel0 = spellLevel0.spells;
@@ -117,6 +119,10 @@ export class DataService {
     this.spellLevel8 = spellLevel8.spells;
     this.spellLevel9 = spellLevel9.spells;
 
+    // Upon initialization of the data, the custom spells need to be added to the spellsService
+
+    this.spellsService.customSpells = this.customSpells;
+
     // Items
     this.favoriteWeapons = favoriteWeaponsData.data;
     this.weaponsAndArmor = weaponsAndArmorData.data;
@@ -187,6 +193,27 @@ export class DataService {
     this.setData('favoriteSpells', { spells: this._favoriteSpells });
   }
 
+  /**
+   * Checks if the given spell is in the favorite spells array.
+   * If so, it is matched by the id and updated.
+   * @param spell The spell to be updated.
+   */
+  public updateFavoriteSpell(spell: Spell): void {
+    const index = this._favoriteSpells.findIndex(
+      (favorite) => favorite.id === spell.id
+    );
+    if (index > -1) {
+      this._favoriteSpells[index] = spell;
+      this.setData('favoriteSpells', { spells: this._favoriteSpells });
+    }
+  }
+
+  public removeFavoriteSpell(spell: Spell): void {
+    const index = this._favoriteSpells.indexOf(spell);
+    this._favoriteSpells.splice(index, 1);
+    this.setData('favoriteSpells', { spells: this._favoriteSpells });
+  }
+
   private _customSpells: Spell[] = [];
 
   public get customSpells(): Spell[] {
@@ -198,15 +225,60 @@ export class DataService {
     this.setData('customSpells', { spells: spells, id: this.customSpellId });
   }
 
+  /**
+   * Adds a custom spell to the data service and uploads the resulting array to the database.
+   * Additionally, the custom spell ID is incremented by one.
+   * The custom spell array in the spells service is also updated.
+   * @param spell The spell to be added.
+   */
+  public addCustomSpell(spell: Spell): void {
+    this._customSpells.push(spell);
+    this._customSpellId++;
+    this.spellsService.customSpells = this._customSpells;
+    this.setData('customSpells', {
+      spells: this._customSpells,
+      id: this.customSpellId,
+    });
+  }
+
+  /**
+   * Updates a custom spell in the data service and uploads the resulting array to the database.
+   * Moreover, the custom spell array in the spells service is also updated.
+   * @param spell The spell to be updated.
+   */
+  public updateCustomSpell(spell: Spell): void {
+    const index = this._customSpells.findIndex(
+      (customSpell) => customSpell.id === spell.id
+    );
+    this._customSpells[index] = spell;
+    this.spellsService.customSpells = this._customSpells;
+    this.setData('customSpells', {
+      spells: this._customSpells,
+      id: this.customSpellId,
+    });
+  }
+
+  /**
+   * Deletes a custom spell from the data service and uploads the resulting array to the database.
+   * @param spell The spell to be deleted.
+   */
+  public deleteCustomSpell(spell: Spell): void {
+    const index = this._customSpells.indexOf(spell);
+    this._customSpells.splice(index, 1);
+    this.setData('customSpells', {
+      spells: this._customSpells,
+      id: this.customSpellId,
+    });
+  }
+
   private _customSpellId: number = 100;
 
   public get customSpellId(): number {
     return this._customSpellId;
   }
 
-  public set customSpellId(id: number) {
+  private set customSpellId(id: number) {
     this._customSpellId = id;
-    this.setData('customSpells', { data: this.customSpells, id: id });
   }
 
   private _spellLevel0: Spell[] = [];

+ 39 - 4
src/services/spells/spells.service.ts

@@ -12,23 +12,56 @@ export class SpellsService {
 
   public closeSubject$ = this._closeSubject.asObservable();
 
+  constructor() {}
+
   public closeAllOthers(level: number): void {
     this._closeSubject.next(level);
   }
 
-  public getSpellsByLevel(level: number, characterClass: string): any[] {
-    return this.spells
+  // Custom Spells
+
+  public customSpells: Spell[] = [];
+
+  /**
+   * Returns an array of available spells for the given level and character class.
+   * The results come from the official apells array and the custom spells array.
+   * @param level The level of the spell.
+   * @param characterClass The character class of the spell.
+   * @returns An array of available spells.
+   */
+  public getAvailableSpells(level: number, characterClass: string): Spell[] {
+    let result = this.spells
       .filter((spell) => spell.level === level)
       .filter((spell) => spell.classes.includes(characterClass));
+    result.push(...this.customSpells.filter((spell) => spell.level === level));
+    return result;
   }
 
-  public getSpellById(id: number): any {
-    return this.spells.find((spell) => spell.id === id);
+  /**
+   * Deletets a single spell from the custom spells array and the full spells array.
+   * @param spellToDelete The spell to be deleted from the custom spells.
+   */
+  public deleteCustomSpell(spellToDelete: Spell): void {
+    const customIndex = this.customSpells.findIndex(
+      (spell) => spell.id === spellToDelete.id
+    );
+    if (customIndex > -1) {
+      this.customSpells.splice(customIndex, 1);
+    }
+    const fullIndex = this.spells.findIndex(
+      (spell) => spell.id === spellToDelete.id
+    );
+    if (fullIndex > -1) {
+      this.spells.splice(fullIndex, 1);
+    }
   }
 
+  //
+
   private spells: Spell[] = [
     {
       id: 0,
+      isCustom: false,
       german: 'Göttliche Führung',
       english: 'Guidance',
       classes: ['Cleric', 'Druid'],
@@ -57,6 +90,7 @@ export class SpellsService {
     },
     {
       id: 1,
+      isCustom: false,
       german: 'Totenläuten',
       english: 'Toll the Dead',
       classes: ['Cleric', 'Warlock', 'Wizard'],
@@ -86,6 +120,7 @@ export class SpellsService {
     },
     {
       id: 2,
+      isCustom: false,
       german: 'Thaumaturgie',
       english: 'Thaumaturgy',
       classes: ['Cleric'],