Просмотр исходного кода

added spellbook and reworked the spell adding, creation and edition process.

Warafear 9 месяцев назад
Родитель
Сommit
579fcd2d46
22 измененных файлов с 939 добавлено и 193 удалено
  1. 4 4
      src/app/journal/journal-home/navigation-panel/navigation-panel.component.html
  2. 0 0
      src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.html
  3. 0 0
      src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.scss
  4. 0 0
      src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.spec.ts
  5. 0 0
      src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.ts
  6. 43 5
      src/app/journal/journal-spellbook/journal-spellbook.component.html
  7. 96 46
      src/app/journal/journal-spellbook/journal-spellbook.component.scss
  8. 432 49
      src/app/journal/journal-spellbook/journal-spellbook.component.ts
  9. 3 2
      src/app/journal/journal-spellcards/journal-spellcards.component.html
  10. 5 2
      src/app/journal/journal-spellcards/journal-spellcards.component.scss
  11. 41 32
      src/app/journal/journal-spellcards/journal-spellcards.component.ts
  12. 1 1
      src/app/journal/journal.module.ts
  13. 2 2
      src/app/journal/spell-modal/spell-modal.component.ts
  14. 44 16
      src/app/shared-components/full-spellcard/full-spellcard.component.html
  15. 1 1
      src/app/shared-components/full-spellcard/full-spellcard.component.scss
  16. 34 14
      src/app/shared-components/full-spellcard/full-spellcard.component.ts
  17. 26 2
      src/assets/i18n/de.json
  18. 25 1
      src/assets/i18n/en.json
  19. BIN
      src/assets/images/texture.png
  20. 126 0
      src/services/data/data.service.ts
  21. 12 0
      src/services/modal/modal.service.ts
  22. 44 16
      src/services/spells/spells.service.ts

+ 4 - 4
src/app/journal/journal-home/navigation-panel/navigation-panel.component.html

@@ -47,9 +47,9 @@
         class="navigation-entry"
         [class]="active === 5 ? 'active' : ''"
         (click)="setActiveProperty(5); closeAll(); closePanel()"
-        [routerLink]="'./notes'"
+        [routerLink]="'./spellbook'"
       >
-        {{ "navigation.notes" | translate }}
+        {{ "navigation.spellbook" | translate }}
       </div>
     </li>
     <li>
@@ -57,9 +57,9 @@
         class="navigation-entry"
         [class]="active === 6 ? 'active' : ''"
         (click)="setActiveProperty(6); closeAll(); closePanel()"
-        [routerLink]="'./spellbook'"
+        [routerLink]="'./notes'"
       >
-        {{ "navigation.spellbook" | translate }}
+        {{ "navigation.notes" | translate }}
       </div>
     </li>
     <li>

+ 0 - 0
src/app/journal/journal-spellcards/custom-spells-modal/custom-spells-modal.component.html → src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.html


+ 0 - 0
src/app/journal/journal-spellcards/custom-spells-modal/custom-spells-modal.component.scss → src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.scss


+ 0 - 0
src/app/journal/journal-spellcards/custom-spells-modal/custom-spells-modal.component.spec.ts → src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.spec.ts


+ 0 - 0
src/app/journal/journal-spellcards/custom-spells-modal/custom-spells-modal.component.ts → src/app/journal/journal-spellbook/custom-spells-modal/custom-spells-modal.component.ts


+ 43 - 5
src/app/journal/journal-spellbook/journal-spellbook.component.html

@@ -1,7 +1,21 @@
 <div class="spellbook-container">
-  <h1>Zauberbuch</h1>
-  <hr />
-  <div class="top-row">
+  <div class="content">
+    <div class="header-row">
+      <h1>Zauberbuch</h1>
+      <div class="button-container">
+        <button class="top-button" (click)="openManageCustomSpellsModal()">
+          <img src="assets/icons/UIIcons/settings.svg" />
+          {{ "spellbook.manage" | translate }}
+        </button>
+
+        <button class="top-button" (click)="openSpellCreationModal(false)">
+          <img src="assets/icons/UIIcons/add.svg" />
+          {{ "spellbook.add" | translate }}
+        </button>
+      </div>
+    </div>
+
+    <hr />
     <div class="class-picker">
       @for (className of translator.magicClasses; track className) {
         <div class="class">
@@ -15,7 +29,31 @@
       }
     </div>
 
-    <div class="button-container"></div>
+    <ul ngbNav #nav="ngbNav" [(activeId)]="currentLevel" class="nav-tabs">
+      @for (level of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; track level) {
+        <li [ngbNavItem]="level">
+          <button
+            ngbNavLink
+            (click)="currentLevel = level; refreshFilteredSpells(true)"
+          >
+            {{ "spellbook.levels." + level | translate }}
+          </button>
+          <ng-template ngbNavContent>
+            <div class="card-container">
+              @for (spell of filteredSpells; track spell) {
+                <spellcard
+                  [spell]="spell"
+                  (click)="showFullSpellcard(spell)"
+                ></spellcard>
+              } @empty {
+                <div class="empty">{{ "spellbook.noSpells" | translate }}</div>
+              }
+            </div>
+          </ng-template>
+        </li>
+      }
+    </ul>
+
+    <div [ngbNavOutlet]="nav"></div>
   </div>
-  <div class="selector-row"></div>
 </div>

+ 96 - 46
src/app/journal/journal-spellbook/journal-spellbook.component.scss

@@ -1,5 +1,3 @@
-// @import url("src/responsive.scss");
-
 .spellbook-container {
   width: 100%;
   height: 100%;
@@ -9,44 +7,57 @@
   flex-direction: column;
   gap: 1rem;
   background-image: url("/assets/images/bg.jpg");
-  //   position: relative;
 }
 
-.top-row {
+.header-row {
   display: flex;
   flex-direction: row;
   justify-content: space-between;
+  align-items: center;
   width: 100%;
-  margin-bottom: 2rem;
-  //   border: 1px solid red;
 }
 
-.class-picker {
+.button-container {
   display: flex;
   flex-direction: row;
   gap: 1rem;
-  width: 80%;
-  height: 6rem;
-  div.class:first-child {
-    margin-right: 2rem;
-  }
 }
 
-.button-container {
+.top-button {
+  font-size: 1.25rem;
+  font-weight: 600;
+  width: 18.5rem;
   display: flex;
-  flex-direction: column;
-  gap: 1rem;
-  width: 20%;
-  height: 6rem;
-  //   border: 1px solid green;
+  align-items: center;
+  gap: 0.5rem;
+  border-radius: 10px;
+  padding: 0.5rem 1rem;
+  background-image: url("/assets/images/texture.png");
+  border: none;
+  box-shadow: var(--shadow);
+}
+
+.content {
+  width: 1300px;
+  margin: 0 auto;
 }
 
-.selector-row {
+.class-picker {
   display: flex;
   flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding-top: 0.25rem;
+  margin-bottom: 1.5rem;
   gap: 1rem;
   width: 100%;
-  height: 3rem;
+  height: 8rem;
+  div.class:first-child {
+    margin-right: 2rem;
+  }
+  background-image: url("/assets/images/texture.png");
+  border-radius: 10px;
+  box-shadow: var(--shadow);
 }
 
 // Classes
@@ -64,6 +75,23 @@
   }
 }
 
+.class {
+  width: 100px;
+  height: 100px;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+
+  label {
+    width: 10rem;
+    text-align: center;
+    font-weight: 500;
+    color: var(--text);
+  }
+}
+
 .icon-active {
   opacity: 1 !important;
   &::after {
@@ -79,14 +107,56 @@
   }
 }
 
-.class {
-  width: 100px;
-  height: 100px;
-  cursor: pointer;
+.nav-link {
+  // background-color: var(--tab);
+  background-color: #cbbea4;
+  background-color: #b5a27d;
+
+  color: black;
+  font-size: 1.123rem;
+  font-weight: 500;
+  width: 8rem;
+  height: 4rem;
+  border-radius: 10px 10px 0 0;
+  transition: all 0.2s ease-in-out;
+  border: var(--border);
+}
+
+.nav-link.active {
+  // background-color: var(--tab-active);
+  background-color: #b5a27d;
+  background-color: #9b8559;
+
+  width: 9.25rem;
+  font-size: 1.25rem;
+  font-weight: 550;
+}
+
+.card-container {
   display: flex;
-  flex-direction: column;
+  flex-wrap: wrap;
+  gap: 1rem;
+  width: 100%;
+  min-height: 400px;
+  padding: 1rem;
+  padding-top: 2rem;
+  background-image: url("/assets/images/texture.png");
+  border-radius: 0 0 10px 10px;
+  box-shadow: var(--shadow);
+}
+
+spellcard {
+  display: inline-block;
+}
+
+.empty {
+  display: flex;
+  justify-content: center;
   align-items: center;
-  justify-content: space-between;
+  width: 100%;
+  height: 320px;
+  font-size: 1.5rem;
+  font-weight: 600;
 }
 
 .all-icon {
@@ -138,23 +208,3 @@
   @include class-icon;
   background-image: url("/assets/images/classes/wizard.jpg");
 }
-
-// Buttons
-
-.manage-spells {
-  position: absolute;
-  right: 2rem;
-  top: 1rem;
-  font-size: 1.25rem;
-  font-weight: 600;
-  display: flex;
-  align-items: center;
-  gap: 0.5rem;
-  border-radius: 10px;
-  padding: 0.5rem 1rem;
-  box-shadow: var(--shadow);
-  background: var(--edit);
-  &:hover {
-    background: var(--edit-hover);
-  }
-}

+ 432 - 49
src/app/journal/journal-spellbook/journal-spellbook.component.ts

@@ -1,12 +1,19 @@
 import { Component, inject } from '@angular/core';
 import { TranslatorService } from 'src/services/translator/translator.service';
-
+import { ModalService } from 'src/services/modal/modal.service';
+import { DataService } from 'src/services/data/data.service';
+import { Spell } from 'src/interfaces/spell';
+import { CustomSpellsModalComponent } from './custom-spells-modal/custom-spells-modal.component';
+import { SpellsService } from 'src/services/spells/spells.service';
+import { FullSpellcardComponent } from 'src/app/shared-components/full-spellcard/full-spellcard.component';
+import { SpellModalComponent } from '../spell-modal/spell-modal.component';
 @Component({
   selector: 'app-journal-spellbook',
   templateUrl: './journal-spellbook.component.html',
   styleUrls: ['./journal-spellbook.component.scss'],
 })
 export class JournalSpellbookComponent {
+  public filterAll = false;
   public filterArtificer = false;
   public filterBard = false;
   public filterCleric = false;
@@ -16,50 +23,124 @@ export class JournalSpellbookComponent {
   private filterSorcerer = false;
   private filterWarlock = false;
   private filterWizard = false;
+  private previousClasses: string[] = [];
+
+  public currentLevel: number = 0;
+  public filteredSpells: Spell[] = [];
 
+  // Services
   public translator = inject(TranslatorService);
+  public modalAccessor = inject(ModalService);
+  public dataAccessor = inject(DataService);
+  private spellsService = inject(SpellsService);
 
-  // public classNames = [
-  //   'artificer',
-  //   'bard',
-  //   'cleric',
-  //   'druid',
-  //   'paladin',
-  //   'ranger',
-  //   'sorcerer',
-  //   'warlock',
-  //   'wizard',
-  // ];
+  public ngAfterViewInit(): void {
+    const className: string = this.dataAccessor.characterData.class;
+    this.toggleClassFilter(className);
+    this.refreshFilteredSpells();
+  }
+
+  // SPELLS FILTER
 
   /**
    * Toggles the selection of a given class icon. It also updates the filtered spells.
+   * When the all classes button was (de-)selects all classes.
    * @param className The provided class which icon will be toggled.
    */
   public toggleClassSelection(className: string) {
-    const element = document.getElementById(className);
-    element!.classList.toggle('icon-active');
+    if (className === 'all') {
+      if (this.filterAll) {
+        this.setAllClassesInactive();
+        this.restorePreviousClasses();
+        this.refreshFilteredSpells();
+      } else {
+        this.previousClasses = this.getActiveClasses();
+        this.setAllClassesActive();
+        this.refreshFilteredSpells();
+      }
+      return;
+    }
     this.toggleClassFilter(className);
+    // Hier kann man noch darauf warten, ob vielleicht mehrere Klassen kurz nacheinander ausgewählt werden
     this.refreshFilteredSpells();
   }
 
-  private refreshFilteredSpells() {
-    console.log('Refresh filtered spells');
-    console.log('Artificer: ' + this.filterArtificer);
-
-    // console.log('Bard: ' + this.filterBard);
-    console.log('Cleric: ' + this.filterCleric);
-    // console.log('Druid: ' + this.filterDruid);
+  /**
+   * Retrieves the filtered spells from the SpellsService.
+   */
+  public refreshFilteredSpells(isDelayed: boolean = false) {
+    if (isDelayed) {
+      setTimeout(() => {
+        this.filteredSpells = this.spellsService.getSpellsByClasslistAndLevel(
+          this.getActiveClasses(),
+          this.currentLevel,
+        );
+      }, 150);
+    } else {
+      this.filteredSpells = this.spellsService.getSpellsByClasslistAndLevel(
+        this.getActiveClasses(),
+        this.currentLevel,
+      );
+    }
   }
 
-  // Utils
-
   /**
    * Toggles if a given class is used to filter the spells.
    * @param className The class which icon will be toggled.
    */
   private toggleClassFilter(className: string) {
-    const isActive: boolean = this.getClassFilter(className);
+    console.log('Toggle class filter: ' + className);
+
+    this.filterAll = false;
+    document.getElementById('all')!.classList.remove('icon-active');
+    const isActive: boolean = this.getClassVariabel(className);
     this.setClassFilter(className, !isActive);
+    if (isActive) {
+      document.getElementById(className)!.classList.remove('icon-active');
+    } else {
+      document.getElementById(className)!.classList.add('icon-active');
+    }
+  }
+
+  /**
+   * Sets a value to the corresponding variable to a classname.
+   * Active classes will be used to filter the spells.
+   * @param className The class which variable is looked for.
+   * @param value The value to be set.
+   */
+  private setClassFilter(className: string, value: boolean) {
+    switch (className) {
+      case 'all':
+        this.filterAll = value;
+        break;
+      case 'artificer':
+        this.filterArtificer = value;
+        break;
+      case 'bard':
+        this.filterBard = value;
+        break;
+      case 'cleric':
+        this.filterCleric = value;
+        break;
+      case 'druid':
+        this.filterDruid = value;
+        break;
+      case 'paladin':
+        this.filterPaladin = value;
+        break;
+      case 'ranger':
+        this.filterRanger = value;
+        break;
+      case 'sorcerer':
+        this.filterSorcerer = value;
+        break;
+      case 'warlock':
+        this.filterWarlock = value;
+        break;
+      case 'wizard':
+        this.filterWizard = value;
+        break;
+    }
   }
 
   /**
@@ -67,8 +148,10 @@ export class JournalSpellbookComponent {
    * @param className The class which varibale is looked for.
    * @returns Returns the given class filter variable.
    */
-  private getClassFilter(className: string): boolean {
+  private getClassVariabel(className: string): boolean {
     switch (className) {
+      case 'all':
+        return this.filterAll;
       case 'artificer':
         return this.filterArtificer;
       case 'bard':
@@ -93,40 +176,340 @@ export class JournalSpellbookComponent {
   }
 
   /**
-   * Sets a value to the corresponding variable to a classname.
-   * Active classes will be used to filter the spells.
-   * @param className The class which variable is looked for.
-   * @param value The value to be set.
+   * Returns all active classes.
+   * @returns Returns all active classes.
    */
-  private setClassFilter(className: string, value: boolean) {
-    switch (className) {
-      case 'artificer':
-        this.filterArtificer = value;
+  private getActiveClasses(): string[] {
+    const activeClasses: string[] = [];
+    if (this.filterArtificer) {
+      activeClasses.push('artificer');
+    }
+    if (this.filterBard) {
+      activeClasses.push('bard');
+    }
+    if (this.filterCleric) {
+      activeClasses.push('cleric');
+    }
+    if (this.filterDruid) {
+      activeClasses.push('druid');
+    }
+    if (this.filterPaladin) {
+      activeClasses.push('paladin');
+    }
+    if (this.filterRanger) {
+      activeClasses.push('ranger');
+    }
+    if (this.filterSorcerer) {
+      activeClasses.push('sorcerer');
+    }
+    if (this.filterWarlock) {
+      activeClasses.push('warlock');
+    }
+    if (this.filterWizard) {
+      activeClasses.push('wizard');
+    }
+    return activeClasses;
+  }
+
+  /**
+   * Sets all class filters to true and activates all class icons.
+   * This is used when the all classes button is selected
+   */
+  private setAllClassesActive(): void {
+    this.filterAll = true;
+    this.filterArtificer = true;
+    this.filterBard = true;
+    this.filterCleric = true;
+    this.filterDruid = true;
+    this.filterPaladin = true;
+    this.filterRanger = true;
+    this.filterSorcerer = true;
+    this.filterWarlock = true;
+    this.filterWizard = true;
+    document.getElementById('all')!.classList.add('icon-active');
+    document.getElementById('artificer')!.classList.add('icon-active');
+    document.getElementById('bard')!.classList.add('icon-active');
+    document.getElementById('cleric')!.classList.add('icon-active');
+    document.getElementById('druid')!.classList.add('icon-active');
+    document.getElementById('paladin')!.classList.add('icon-active');
+    document.getElementById('ranger')!.classList.add('icon-active');
+    document.getElementById('sorcerer')!.classList.add('icon-active');
+    document.getElementById('warlock')!.classList.add('icon-active');
+    document.getElementById('wizard')!.classList.add('icon-active');
+  }
+
+  /**
+   * Sets all class filters to false and unselects all class buttons.
+   * This is used when the all classes button is deselected.
+   */
+  private setAllClassesInactive(): void {
+    this.filterAll = false;
+    this.filterArtificer = false;
+    this.filterBard = false;
+    this.filterCleric = false;
+    this.filterDruid = false;
+    this.filterPaladin = false;
+    this.filterRanger = false;
+    this.filterSorcerer = false;
+    this.filterWarlock = false;
+    this.filterWizard = false;
+    document.getElementById('all')!.classList.remove('icon-active');
+    document.getElementById('artificer')!.classList.remove('icon-active');
+    document.getElementById('bard')!.classList.remove('icon-active');
+    document.getElementById('cleric')!.classList.remove('icon-active');
+    document.getElementById('druid')!.classList.remove('icon-active');
+    document.getElementById('paladin')!.classList.remove('icon-active');
+    document.getElementById('ranger')!.classList.remove('icon-active');
+    document.getElementById('sorcerer')!.classList.remove('icon-active');
+    document.getElementById('warlock')!.classList.remove('icon-active');
+    document.getElementById('wizard')!.classList.remove('icon-active');
+  }
+
+  /**
+   * Activates all classes that were active before the all classes button was selected.
+   */
+  private restorePreviousClasses(): void {
+    this.previousClasses.forEach((className) => {
+      const element = document.getElementById(className);
+      element!.classList.toggle('icon-active');
+      this.toggleClassFilter(className);
+    });
+  }
+
+  // CUSTOM SPELLS
+
+  /**
+   * 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 copyOfficalSpell
+   * @param spell
+   */
+  public openSpellCreationModal(
+    copyOfficalSpell: boolean,
+    spell?: Spell,
+  ): void {
+    this.modalAccessor.openModal(SpellModalComponent, {
+      spell: copyOfficalSpell ? spell : undefined,
+      level: this.currentLevel,
+      id: this.dataAccessor.customSpellId,
+      copyOfficalSpell: copyOfficalSpell,
+      classes: [this.dataAccessor.characterData.class],
+    });
+    const resultSubscription = this.modalAccessor.result$.subscribe(
+      (result) => {
+        if (result.state === 'add') {
+          console.log('Add spell, caught in spellbook component');
+          this.dataAccessor.addCustomSpell(result.data);
+          this.refreshFilteredSpells();
+        }
+        resultSubscription.unsubscribe();
+      },
+    );
+  }
+
+  /**
+   * Opens the modal to manage custom spells. Here, custom spells can be deleted.
+   */
+  public openManageCustomSpellsModal(): void {
+    this.modalAccessor.openModal(CustomSpellsModalComponent, {
+      spells: this.dataAccessor.customSpells,
+    });
+    const resultSubscription = this.modalAccessor.result$.subscribe(
+      (result) => {
+        if (result.state === 'delete') {
+          result.data.forEach((spell: Spell) => {
+            // TODO: Implement deletion of custom spells
+            // this.deleteCustomSpell(spell);
+          });
+        }
+        resultSubscription.unsubscribe();
+      },
+    );
+  }
+
+  // SPELLCARDS
+
+  /**
+   * Opens the full spellcard modal for a given spell. The spellcard can be used to add the spell to the prepared spells and/or to the favorites.
+   * @param spell The spell for which the full spellcard should be opened.
+   */
+  public showFullSpellcard(spell: Spell): void {
+    const favorites = this.dataAccessor.favoriteSpells;
+    const alreadyInFavorites = favorites.some(
+      (currentSpell) => currentSpell.id === spell.id,
+    );
+    const alreadyPrepared = this.dataAccessor
+      .getAllPreparedSpells()
+      .some((currentSpell) => currentSpell.id === spell.id);
+    this.modalAccessor.openModal(FullSpellcardComponent, {
+      spell: spell,
+      origin: 'spellbook',
+      alreadyPrepared: alreadyPrepared,
+      alreadyInFavorites: alreadyInFavorites,
+    });
+    const actionSubscription = this.modalAccessor.action$.subscribe(
+      (message) => {
+        if (message.action === 'addToFavorites') {
+          this.dataAccessor.addFavoriteSpell(spell);
+        } else if (message.action === 'removeFromFavorites') {
+          this.dataAccessor.removeFavoriteSpell(spell);
+        } else if (message.action === 'addToPrepared') {
+          this.addSpellToPrepared(spell);
+        } else if (message.action === 'removeFromPrepared') {
+          this.removeSpellFromPrepared(spell);
+        }
+      },
+    );
+    const resultSubscription = this.modalAccessor.result$.subscribe(
+      (result) => {
+        resultSubscription.unsubscribe();
+        actionSubscription.unsubscribe();
+
+        if (result.state === 'copy') {
+          setTimeout(() => {
+            this.openSpellCreationModal(true, spell);
+          }, 100);
+        } else if (result.state === 'update') {
+          console.log('Update spell, caught in spellbook component');
+          // TODO: Implement Modification of spells
+          setTimeout(() => {
+            this.openSpellModificationModal(spell);
+          }, 100);
+        }
+      },
+    );
+  }
+
+  /**
+   * Opens a modal to modify an existing spell. The spell is then updated in the dataAccessor and favorites.
+   * @param spell The Spell that is to be modified.
+   */
+  public openSpellModificationModal(spell: Spell): void {
+    this.modalAccessor.openModal(SpellModalComponent, {
+      spell: spell,
+      isModification: true,
+    });
+    const resultSubscription = this.modalAccessor.result$.subscribe(
+      (result) => {
+        if (result.state === 'update') {
+          this.dataAccessor.updateCustomSpell(result.data);
+        }
+        resultSubscription.unsubscribe();
+        this.refreshFilteredSpells();
+      },
+    );
+  }
+
+  /**
+   * Adds a given spell to the list of prepared spells. The spell will be added to the corresponding spellArray.
+   * Moreover it will also be added to the database in the dataAccessor.
+   * @param spell The spell that will be added to its corresponding spellArray.
+   */
+  private addSpellToPrepared(spell: Spell): void {
+    switch (spell.level) {
+      case 0:
+        this.dataAccessor.addSpellToLevel0(spell);
         break;
-      case 'bard':
-        this.filterBard = value;
+      case 1:
+        this.dataAccessor.addSpellToLevel1(spell);
         break;
-      case 'cleric':
-        this.filterCleric = value;
+      case 2:
+        this.dataAccessor.addSpellToLevel2(spell);
         break;
-      case 'druid':
-        this.filterDruid = value;
+      case 3:
+        this.dataAccessor.addSpellToLevel3(spell);
         break;
-      case 'paladin':
-        this.filterPaladin = value;
+      case 4:
+        this.dataAccessor.addSpellToLevel4(spell);
         break;
-      case 'ranger':
-        this.filterRanger = value;
+      case 5:
+        this.dataAccessor.addSpellToLevel5(spell);
         break;
-      case 'sorcerer':
-        this.filterSorcerer = value;
+      case 6:
+        this.dataAccessor.addSpellToLevel6(spell);
         break;
-      case 'warlock':
-        this.filterWarlock = value;
+      case 7:
+        this.dataAccessor.addSpellToLevel7(spell);
         break;
-      case 'wizard':
-        this.filterWizard = value;
+      case 8:
+        this.dataAccessor.addSpellToLevel8(spell);
+        break;
+      case 9:
+        this.dataAccessor.addSpellToLevel9(spell);
         break;
     }
   }
+
+  /**
+   * Removes a given spell from the list of prepared spells. The spell will be removed from the corresponding spellArray.
+   * Moreover it will also be removed from the database in the dataAccessor.
+   * @param spell The spell that will be removed from its corresponding spellArray.
+   */
+  private removeSpellFromPrepared(spell: Spell): void {
+    switch (spell.level) {
+      case 0:
+        this.dataAccessor.removeSpellFromLevel0(spell);
+        break;
+      case 1:
+        this.dataAccessor.removeSpellFromLevel1(spell);
+        break;
+      case 2:
+        this.dataAccessor.removeSpellFromLevel2(spell);
+        break;
+      case 3:
+        this.dataAccessor.removeSpellFromLevel3(spell);
+        break;
+      case 4:
+        this.dataAccessor.removeSpellFromLevel4(spell);
+        break;
+      case 5:
+        this.dataAccessor.removeSpellFromLevel5(spell);
+        break;
+      case 6:
+        this.dataAccessor.removeSpellFromLevel6(spell);
+        break;
+      case 7:
+        this.dataAccessor.removeSpellFromLevel7(spell);
+        break;
+      case 8:
+        this.dataAccessor.removeSpellFromLevel8(spell);
+        break;
+      case 9:
+        this.dataAccessor.removeSpellFromLevel9(spell);
+        break;
+    }
+  }
+
+  /**
+   * Returns a list of prepared spells for a given level.
+   * @param level The level of the list of prepared spells.
+   * @returns Returns the complete list of prepared spells for the given level.
+   */
+  private getPreparedSpellList(level: number): Spell[] {
+    switch (level) {
+      case 0:
+        return this.dataAccessor.spellLevel0;
+      case 1:
+        return this.dataAccessor.spellLevel1;
+      case 2:
+        return this.dataAccessor.spellLevel2;
+      case 3:
+        return this.dataAccessor.spellLevel3;
+      case 4:
+        return this.dataAccessor.spellLevel4;
+      case 5:
+        return this.dataAccessor.spellLevel5;
+      case 6:
+        return this.dataAccessor.spellLevel6;
+      case 7:
+        return this.dataAccessor.spellLevel7;
+      case 8:
+        return this.dataAccessor.spellLevel8;
+      case 9:
+        return this.dataAccessor.spellLevel9;
+      default:
+        return [];
+    }
+  }
 }

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

@@ -1,8 +1,9 @@
 <div class="spellcards-container">
-  <button class="manage-spells" (click)="openManageCustomSpellsModal()">
+  <!-- TODO: DELETE -->
+  <!-- <button class="manage-spells" (click)="openManageCustomSpellsModal()">
     <img src="assets/icons/UIIcons/settings.svg" />
     {{ "spellcards.manage" | translate }}
-  </button>
+  </button> -->
 
   <div cdkDropListGroup>
     @for (

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

@@ -6,6 +6,7 @@
   display: flex;
   flex-direction: column;
   position: relative;
+  background-image: url("/assets/images/bg.jpg");
 }
 
 .manage-spells {
@@ -34,7 +35,8 @@
   width: 16rem;
   gap: 1rem;
   padding: 0.25rem 1rem;
-  background: white;
+  // background: white;
+  background-image: url("/assets/images/texture.png");
   border: solid 1px var(--border-color);
   border-bottom: none;
   border-radius: 10px 10px 0 0;
@@ -86,7 +88,8 @@
 .spell-list {
   border: solid 1px var(--border-color);
   height: 22rem;
-  background: white;
+  // background: white;
+  background-image: url("/assets/images/texture.png");
   border-radius: 0 10px 10px 10px;
   display: flex;
   flex-direction: row;

+ 41 - 32
src/app/journal/journal-spellcards/journal-spellcards.component.ts

@@ -10,7 +10,7 @@ import { ModalService } from 'src/services/modal/modal.service';
 import { SpellsService } from 'src/services/spells/spells.service';
 import { SpellModalComponent } from 'src/app/journal/spell-modal/spell-modal.component';
 import { FullSpellcardComponent } from 'src/app/shared-components/full-spellcard/full-spellcard.component';
-import { CustomSpellsModalComponent } from './custom-spells-modal/custom-spells-modal.component';
+// import { CustomSpellsModalComponent } from '../journal-spellbook/custom-spells-modal/custom-spells-modal.component';
 
 @Component({
   selector: 'app-journal-spellcards',
@@ -63,6 +63,13 @@ export class JournalSpellcardsComponent {
     return usedIDs;
   }
 
+  /**
+   * Opens a modal to show the full spellcard of a spell. The modal also allows the user to add the spell to the favorites and remove it again.
+   * Moreover, the user can remove the spell from the prepared spells list.
+   * @param spell The spell to show.
+   * @param level The level of the spell.
+   * @param spellIndex The index of the spell in the spell list.
+   */
   public showFullSpellcard(
     spell: Spell,
     level: number,
@@ -74,31 +81,32 @@ export class JournalSpellcardsComponent {
     );
     this.modalAccessor.openModal(FullSpellcardComponent, {
       spell: spell,
-      isFromDashboard: false,
+      origin: 'spellcards',
       alreadyInFavorites: alreadyInFavorites,
     });
+    const actionSubscription = this.modalAccessor.action$.subscribe(
+      (message) => {
+        if (message.action === 'addToFavorites') {
+          this.dataAccessor.addFavoriteSpell(spell);
+        } else if (message.action === 'removeFromFavorites') {
+          this.dataAccessor.removeFavoriteSpell(spell);
+        }
+      },
+    );
     const resultSubscription = this.modalAccessor.result$.subscribe(
       (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') {
+        actionSubscription.unsubscribe();
+        if (result.state === 'remove') {
           this.dataAccessor.removeFavoriteSpell(spell);
           this.getSpellList(level).splice(spellIndex, 1);
           this.updateSpellsInDatabase(level);
-        } else if (result.state === 'update') {
-          setTimeout(() => {
-            this.openSpellModificationModal(level, spellIndex);
-          }, 100);
-        } else if (result.state === 'add') {
-          this.dataAccessor.addFavoriteSpell(spell);
-        } else if (result.state === 'removeFromFavorites') {
-          this.dataAccessor.removeFavoriteSpell(spell);
         }
+        // else if (result.state === 'update') {
+        //   setTimeout(() => {
+        //     this.openSpellModificationModal(level, spellIndex);
+        //   }, 100);
+        // }
       },
     );
   }
@@ -171,21 +179,22 @@ export class JournalSpellcardsComponent {
   /**
    * Opens the modal to manage custom spells. Here, custom spells can be deleted.
    */
-  public openManageCustomSpellsModal(): void {
-    this.modalAccessor.openModal(CustomSpellsModalComponent, {
-      spells: this.dataAccessor.customSpells,
-    });
-    const resultSubscription = this.modalAccessor.result$.subscribe(
-      (result) => {
-        if (result.state === 'delete') {
-          result.data.forEach((spell: Spell) => {
-            this.deleteCustomSpell(spell);
-          });
-        }
-        resultSubscription.unsubscribe();
-      },
-    );
-  }
+  // TODO: DELETE
+  // public openManageCustomSpellsModal(): void {
+  //   this.modalAccessor.openModal(CustomSpellsModalComponent, {
+  //     spells: this.dataAccessor.customSpells,
+  //   });
+  //   const resultSubscription = this.modalAccessor.result$.subscribe(
+  //     (result) => {
+  //       if (result.state === 'delete') {
+  //         result.data.forEach((spell: Spell) => {
+  //           this.deleteCustomSpell(spell);
+  //         });
+  //       }
+  //       resultSubscription.unsubscribe();
+  //     },
+  //   );
+  // }
 
   /**
    * Deletes a custom spell from the custom spells list.

+ 1 - 1
src/app/journal/journal.module.ts

@@ -91,7 +91,7 @@ import { BackgroundComponent } from './journal-character/background/background.c
 import { StoryComponent } from './journal-character/story/story.component';
 import { FavoriteSpellsModalComponent } from './journal-stats/weapons-container/spell-table/favorite-spells-modal/favorite-spells-modal.component';
 import { MatRippleModule } from '@angular/material/core';
-import { CustomSpellsModalComponent } from './journal-spellcards/custom-spells-modal/custom-spells-modal.component';
+import { CustomSpellsModalComponent } from './journal-spellbook/custom-spells-modal/custom-spells-modal.component';
 import { DurationPipe } from '../../pipes/duration/duration.pipe';
 import { CombinedComponent } from './journal-character/combined/combined.component';
 

+ 2 - 2
src/app/journal/spell-modal/spell-modal.component.ts

@@ -17,7 +17,7 @@ export class SpellModalComponent {
   @Input() classes: string[] = [];
   @Input() public id: number = 0;
   @Input() public isModification: boolean = false;
-  @Input() public isBasedOnOfficialSpell: boolean = false;
+  @Input() public copyOfficalSpell: boolean = false;
 
   // #region Properties
   public name: string = '';
@@ -95,7 +95,7 @@ export class SpellModalComponent {
   ) {}
 
   public ngOnInit(): void {
-    if (this.isModification || this.isBasedOnOfficialSpell) {
+    if (this.isModification || this.copyOfficalSpell) {
       this.loadspell();
     }
   }

+ 44 - 16
src/app/shared-components/full-spellcard/full-spellcard.component.html

@@ -244,24 +244,52 @@
     "
   ></div>
   <div class="button-container">
-    @if (spell.isCustom && !isFromDashboard) {
-      <button class="green" (click)="update()">Bearbeiten</button>
-    }
-    <button
-      [class]="alreadyInFavorites ? 'red' : 'green'"
-      (click)="alreadyInFavorites ? removeFromFavorites() : addToFavorites()"
-    >
-      @if (alreadyInFavorites) {
-        Aus Favoriten entfernen
-      } @else {
-        Zu Favoriten hinzufügen
-      }
-    </button>
-    @if (!isFromDashboard) {
-      <button class="red" (click)="remove()">Entfernen</button>
+    @if (origin === "spellbook") {
       @if (spell.isCustom) {
-        <button class="red" (click)="delete()">Endgültig löschen</button>
+        <button class="green" (click)="update()">Bearbeiten</button>
       }
+      <button
+        [class]="alreadyPrepared ? 'red' : 'green'"
+        (click)="alreadyPrepared ? removeFromPrepared() : addToPrepared()"
+      >
+        @if (alreadyPrepared) {
+          {{ "fullSpellcards.removeFromPrepared" | translate }}
+        } @else {
+          {{ "fullSpellcards.addToPrepared" | translate }}
+        }
+      </button>
+
+      @if (alreadyPrepared) {
+        <button
+          [class]="alreadyInFavorites ? 'red' : 'green'"
+          (click)="
+            alreadyInFavorites ? removeFromFavorites() : addToFavorites()
+          "
+        >
+          @if (alreadyInFavorites) {
+            {{ "fullSpellcards.removeFromFavorites" | translate }}
+          } @else {
+            {{ "fullSpellcards.addToFavorites" | translate }}
+          }
+        </button>
+      }
+      <button class="green" (click)="copy()">
+        {{ "fullSpellcards.copy" | translate }}
+      </button>
+    } @else if (origin === "spellcards") {
+      <button
+        [class]="alreadyInFavorites ? 'red' : 'green'"
+        (click)="alreadyInFavorites ? removeFromFavorites() : addToFavorites()"
+      >
+        @if (alreadyInFavorites) {
+          {{ "fullSpellcards.removeFromFavorites" | translate }}
+        } @else {
+          {{ "fullSpellcards.addToFavorites" | translate }}
+        }
+      </button>
+      <button class="red" (click)="remove()">
+        {{ "fullSpellcards.remove" | translate }}
+      </button>
     }
   </div>
 </div>

+ 1 - 1
src/app/shared-components/full-spellcard/full-spellcard.component.scss

@@ -208,7 +208,7 @@
     font-size: 1.25rem;
     font-weight: 600;
     height: 4.25rem;
-    width: 10rem;
+    width: 12rem;
     border-radius: 10px;
     cursor: pointer;
     box-shadow: var(--shadow);

+ 34 - 14
src/app/shared-components/full-spellcard/full-spellcard.component.ts

@@ -1,4 +1,4 @@
-import { Component, Input } from '@angular/core';
+import { Component, Input, inject } from '@angular/core';
 import { Spell } from 'src/interfaces/spell';
 import { ModalService } from 'src/services/modal/modal.service';
 import { UtilsService } from 'src/services/utils/utils.service';
@@ -11,38 +11,58 @@ import { TranslateService } from '@ngx-translate/core';
 })
 export class FullSpellcardComponent {
   @Input() public spell!: Spell;
-  @Input() public isFromDashboard!: boolean;
-  @Input() public alreadyInFavorites: boolean = true;
+  @Input() public origin: string = 'dashboard';
+  @Input() public alreadyInFavorites: boolean = false;
+  @Input() public alreadyPrepared: boolean = false;
 
-  public constructor(
-    private modalAccessor: ModalService,
-    public utils: UtilsService,
-    public translate: TranslateService,
-  ) {}
+  private modalService = inject(ModalService);
+  public utils = inject(UtilsService);
+  public translate = inject(TranslateService);
+
+  ngAfterViewInit(): void {
+    console.log(this.origin);
+    console.log(this.alreadyInFavorites);
+    console.log(this.alreadyPrepared);
+  }
 
   public setBackupImage(event: any): void {
     event.target.src = 'assets/images/spells/backup.jpg';
   }
 
   public delete(): void {
-    this.modalAccessor.handleModalClosing('delete', undefined);
+    this.modalService.handleModalClosing('delete');
   }
 
   public remove(): void {
-    this.modalAccessor.handleModalClosing('remove', undefined);
+    this.modalService.handleModalClosing('remove');
   }
 
   public update(): void {
-    this.modalAccessor.handleModalClosing('update', undefined);
+    this.modalService.handleModalClosing('update');
+  }
+
+  public copy(): void {
+    this.modalService.handleModalClosing('copy');
   }
 
   public addToFavorites(): void {
-    this.modalAccessor.handleModalClosing('add', undefined);
+    this.alreadyInFavorites = true;
+    this.modalService.sendMessage('addToFavorites');
   }
 
   public removeFromFavorites(): void {
-    console.log('removeFromFavorites');
+    this.alreadyInFavorites = false;
+    this.modalService.sendMessage('removeFromFavorites');
+  }
+
+  public addToPrepared(): void {
+    this.alreadyPrepared = true;
+    this.modalService.sendMessage('addToPrepared');
+  }
 
-    this.modalAccessor.handleModalClosing('removeFromFavorites', undefined);
+  public removeFromPrepared(): void {
+    this.alreadyPrepared = false;
+    this.alreadyInFavorites = false;
+    this.modalService.sendMessage('removeFromPrepared');
   }
 }

+ 26 - 2
src/assets/i18n/de.json

@@ -183,6 +183,7 @@
     "yuanTi": "Yuan-Ti"
   },
   "classes": {
+    "all": "Alle Klassen",
     "artificer": "Magieschmied",
     "barbarian": "Barbar",
     "bard": "Barde",
@@ -720,7 +721,6 @@
     }
   },
   "spellcards": {
-    "manage": "Eigene Zauber verwalten",
     "cantrips": "Zaubertricks",
     "delete": "Zum Löschen hier ablegen",
     "add": {
@@ -775,8 +775,32 @@
     "self": "Selbst",
     "effect": "Effekt",
     "range": "Reichweite",
-    "concentration": "Konzentration"
+    "concentration": "Konzentration",
+    "addToPrepared": "Zu vorbereiteten Zaubern hinzufügen",
+    "removeFromPrepared": "Aus vorbereiteten Zauber entfernen",
+    "addToFavorites": "Zu Favoriten hinzufügen",
+    "removeFromFavorites": "Aus Favoriten entfernen",
+    "copy": "Zauber kopieren",
+    "remove": "Zauber entfernen"
+  },
+  "spellbook": {
+    "levels": {
+      "0": "Zaubertricks",
+      "1": "1. Grad",
+      "2": "2. Grad",
+      "3": "3. Grad",
+      "4": "4. Grad",
+      "5": "5. Grad",
+      "6": "6. Grad",
+      "7": "7. Grad",
+      "8": "8. Grad",
+      "9": "9. Grad"
+    },
+    "manage": "Eigene Zauber verwalten",
+    "add": "Neuen Zauber erstellen",
+    "noSpells": "Für diesen Filter gibt es keine Zauber"
   },
+
   "creator": {
     "new": "Neuen Charakter erstellen",
     "name": "Name",

+ 25 - 1
src/assets/i18n/en.json

@@ -180,6 +180,7 @@
     "yuanTi": "Yuan-Ti"
   },
   "classes": {
+    "all": "All Classes",
     "artificer": "Artificer",
     "barbarian": "Barbarian",
     "bard": "Bard",
@@ -769,7 +770,30 @@
     "self": "Self",
     "effect": "Effect",
     "range": "Range",
-    "concentration": "Concentration"
+    "concentration": "Concentration",
+    "addToPrepared": "Add to prepared Spells",
+    "removeFromPrepared": "Remove from prepared Spells",
+    "addToFavorites": "Add to favorites",
+    "removeFromFavorites": "Remove from favorites",
+    "copy": "Copy Spell",
+    "remove": "Remove Spell"
+  },
+  "spellbook": {
+    "levels": {
+      "0": "Cantrips",
+      "1": "1st Level",
+      "2": "2nd Level",
+      "3": "3rd Level",
+      "4": "4th Level",
+      "5": "5th Level",
+      "6": "6th Level",
+      "7": "7th Level",
+      "8": "8th Level",
+      "9": "9th Level"
+    },
+    "manage": "Manage custom spells",
+    "add": "Create new spell",
+    "noSpells": "No spells found for this filter"
   },
   "creator": {
     "new": "Create New Character",

BIN
src/assets/images/texture.png


+ 126 - 0
src/services/data/data.service.ts

@@ -312,6 +312,7 @@ export class DataService {
   private set customSpellId(id: number) {
     this._customSpellId = id;
   }
+  // LEVEL 0
 
   private _spellLevel0: Spell[] = [];
 
@@ -324,6 +325,18 @@ export class DataService {
     this.setData('spellLevel0', { spells: spells });
   }
 
+  public addSpellToLevel0(spell: Spell): void {
+    this._spellLevel0.push(spell);
+    this.setData('spellLevel0', { spells: this._spellLevel0 });
+  }
+
+  public removeSpellFromLevel0(spell: Spell): void {
+    const index = this._spellLevel0.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel0.splice(index, 1);
+    this.setData('spellLevel0', { spells: this._spellLevel0 });
+  }
+
+  // LEVEL 1
   private _spellLevel1: Spell[] = [];
 
   public get spellLevel1(): Spell[] {
@@ -335,6 +348,19 @@ export class DataService {
     this.setData('spellLevel1', { spells: spells });
   }
 
+  public addSpellToLevel1(spell: Spell): void {
+    this._spellLevel1.push(spell);
+    this.setData('spellLevel1', { spells: this._spellLevel1 });
+  }
+
+  public removeSpellFromLevel1(spell: Spell): void {
+    const index = this._spellLevel1.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel1.splice(index, 1);
+    this.setData('spellLevel1', { spells: this._spellLevel1 });
+  }
+
+  // LEVEL 2
+
   private _spellLevel2: Spell[] = [];
 
   public get spellLevel2(): Spell[] {
@@ -346,6 +372,19 @@ export class DataService {
     this.setData('spellLevel2', { spells: spells });
   }
 
+  public addSpellToLevel2(spell: Spell): void {
+    this._spellLevel2.push(spell);
+    this.setData('spellLevel2', { spells: this._spellLevel2 });
+  }
+
+  public removeSpellFromLevel2(spell: Spell): void {
+    const index = this._spellLevel2.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel2.splice(index, 1);
+    this.setData('spellLevel2', { spells: this._spellLevel2 });
+  }
+
+  // LEVEL 3
+
   private _spellLevel3: Spell[] = [];
 
   public get spellLevel3(): Spell[] {
@@ -357,6 +396,19 @@ export class DataService {
     this.setData('spellLevel3', { spells: spells });
   }
 
+  public addSpellToLevel3(spell: Spell): void {
+    this._spellLevel3.push(spell);
+    this.setData('spellLevel3', { spells: this._spellLevel3 });
+  }
+
+  public removeSpellFromLevel3(spell: Spell): void {
+    const index = this._spellLevel3.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel3.splice(index, 1);
+    this.setData('spellLevel3', { spells: this._spellLevel3 });
+  }
+
+  // LEVEL 4
+
   private _spellLevel4: Spell[] = [];
 
   public get spellLevel4(): Spell[] {
@@ -368,6 +420,18 @@ export class DataService {
     this.setData('spellLevel4', { spells: spells });
   }
 
+  public addSpellToLevel4(spell: Spell): void {
+    this._spellLevel4.push(spell);
+    this.setData('spellLevel4', { spells: this._spellLevel4 });
+  }
+
+  public removeSpellFromLevel4(spell: Spell): void {
+    const index = this._spellLevel4.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel4.splice(index, 1);
+    this.setData('spellLevel4', { spells: this._spellLevel4 });
+  }
+
+  // LEVEL 5
   private _spellLevel5: Spell[] = [];
 
   public get spellLevel5(): Spell[] {
@@ -379,6 +443,19 @@ export class DataService {
     this.setData('spellLevel5', { spells: spells });
   }
 
+  public addSpellToLevel5(spell: Spell): void {
+    this._spellLevel5.push(spell);
+    this.setData('spellLevel5', { spells: this._spellLevel5 });
+  }
+
+  public removeSpellFromLevel5(spell: Spell): void {
+    const index = this._spellLevel5.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel5.splice(index, 1);
+    this.setData('spellLevel5', { spells: this._spellLevel5 });
+  }
+
+  // LEVEL 6
+
   private _spellLevel6: Spell[] = [];
 
   public get spellLevel6(): Spell[] {
@@ -390,6 +467,18 @@ export class DataService {
     this.setData('spellLevel6', { spells: spells });
   }
 
+  public addSpellToLevel6(spell: Spell): void {
+    this._spellLevel6.push(spell);
+    this.setData('spellLevel6', { spells: this._spellLevel6 });
+  }
+
+  public removeSpellFromLevel6(spell: Spell): void {
+    const index = this._spellLevel6.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel6.splice(index, 1);
+    this.setData('spellLevel6', { spells: this._spellLevel6 });
+  }
+
+  // LEVEL 7
   private _spellLevel7: Spell[] = [];
 
   public get spellLevel7(): Spell[] {
@@ -401,6 +490,19 @@ export class DataService {
     this.setData('spellLevel7', { spells: spells });
   }
 
+  public addSpellToLevel7(spell: Spell): void {
+    this._spellLevel7.push(spell);
+    this.setData('spellLevel7', { spells: this._spellLevel7 });
+  }
+
+  public removeSpellFromLevel7(spell: Spell): void {
+    const index = this._spellLevel7.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel7.splice(index, 1);
+    this.setData('spellLevel7', { spells: this._spellLevel7 });
+  }
+
+  // LEVEL 8
+
   private _spellLevel8: Spell[] = [];
 
   public get spellLevel8(): Spell[] {
@@ -412,6 +514,19 @@ export class DataService {
     this.setData('spellLevel8', { spells: spells });
   }
 
+  public addSpellToLevel8(spell: Spell): void {
+    this._spellLevel8.push(spell);
+    this.setData('spellLevel8', { spells: this._spellLevel8 });
+  }
+
+  public removeSpellFromLevel8(spell: Spell): void {
+    const index = this._spellLevel8.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel8.splice(index, 1);
+    this.setData('spellLevel8', { spells: this._spellLevel8 });
+  }
+
+  // LEVEL 9
+
   private _spellLevel9: Spell[] = [];
 
   public get spellLevel9(): Spell[] {
@@ -423,6 +538,17 @@ export class DataService {
     this.setData('spellLevel9', { spells: spells });
   }
 
+  public addSpellToLevel9(spell: Spell): void {
+    this._spellLevel9.push(spell);
+    this.setData('spellLevel9', { spells: this._spellLevel9 });
+  }
+
+  public removeSpellFromLevel9(spell: Spell): void {
+    const index = this._spellLevel9.findIndex((obj) => obj.id === spell.id);
+    this._spellLevel9.splice(index, 1);
+    this.setData('spellLevel9', { spells: this._spellLevel9 });
+  }
+
   public getAllPreparedSpells(): Spell[] {
     return [
       ...this._spellLevel0,

+ 12 - 0
src/services/modal/modal.service.ts

@@ -14,6 +14,9 @@ export class ModalService {
   private _resultSubject = new Subject<any>();
   result$ = this._resultSubject.asObservable();
 
+  private _actionSubject = new Subject<any>();
+  action$ = this._actionSubject.asObservable();
+
   private _isModalOpen = false;
   public get isModalOpen(): boolean {
     return this._isModalOpen;
@@ -40,4 +43,13 @@ export class ModalService {
     this._closeModalSubject.next('close');
     this._isModalOpen = false;
   }
+
+  /**
+   * Sends a message to the host component that openend the modal.
+   * @param action The action to perform.
+   * @param data The optional data to send.
+   */
+  public sendMessage(action: string, data?: any) {
+    this._actionSubject.next({ action, data });
+  }
 }

+ 44 - 16
src/services/spells/spells.service.ts

@@ -1,22 +1,22 @@
-import { Injectable } from '@angular/core';
+import { Injectable, inject } from '@angular/core';
 import { Spell } from 'src/interfaces/spell';
 import { Subject } from 'rxjs';
+import { TranslateService } from '@ngx-translate/core';
 
 @Injectable({
   providedIn: 'root',
 })
 export class SpellsService {
-  // Functions
-
+  private translateService = inject(TranslateService);
+  // PROBABLY DEPRECATED BECAUSE OF THE SPELLBOOK COMPONENT
   private _closeSubject = new Subject<number>();
 
   public closeSubject$ = this._closeSubject.asObservable();
 
-  constructor() {}
-
   public closeAllOthers(level: number): void {
     this._closeSubject.next(level);
   }
+  //
 
   // Custom Spells
 
@@ -31,7 +31,7 @@ export class SpellsService {
    */
   public getAvailableSpells(level: number, characterClass?: string): Spell[] {
     let result: Spell[] = [];
-
+    // If a character class is provided, the result will be filtered by the character class.
     if (characterClass !== undefined) {
       result = this.spells
         .filter((spell) => spell.level === level)
@@ -39,16 +39,44 @@ export class SpellsService {
       result.push(
         ...this.customSpells.filter((spell) => spell.level === level),
       );
-
-      return result;
-    } else {
+    }
+    // Else all spells with the given level will be provided.
+    else {
       result = this.spells.filter((spell) => spell.level === level);
       result.push(
         ...this.customSpells.filter((spell) => spell.level === level),
       );
+    }
+    return result;
+  }
 
-      return result;
+  /**
+   * Returns a list of SPells filtered by a given list of classes and a level.
+   * @param classList The list of classes to filter the spells.
+   * @param level The level of the spells.
+   * @return An array of spells.
+   */
+  public getSpellsByClasslistAndLevel(
+    classList: string[],
+    level: number,
+  ): Spell[] {
+    let result: Spell[] = [];
+    classList.forEach((className) => {
+      const classSpells: Spell[] = this.getAvailableSpells(level, className);
+      // Falls noch nicht in result add
+      classSpells.forEach((spell) => {
+        if (!result.includes(spell)) {
+          result.push(spell);
+        }
+      });
+    });
+    // sort by language specific name
+    if (this.translateService.getDefaultLang() == 'de') {
+      result.sort((a, b) => (a.german < b.german ? -1 : 1));
+    } else {
+      result.sort((a, b) => (a.english < b.english ? -1 : 1));
     }
+    return result;
   }
 
   /**
@@ -778,7 +806,7 @@ export class SpellsService {
       doesDamage: false,
       doesHeal: false,
       description_de: `
-      <p>Als Teil der Aktion, mit der du diesen Zauber wirkst, musst du einen Nahkampfangriff mit einer Waffe gegen eine Kreatur in der Reichweite des Zaubers ausführen, sonst misslingt der Zauber. Bei einem Treffer erleidet das Ziel die normalen Auswirkungen des Angriffs, und grünes Feuer springt vom Ziel auf eine andere Kreatur deiner Wahl, die du sehen kannst und die sich innerhalb von 5 Fußn zum Ziel befindet. Die zweite Kreatur erleidet Feuerschaden gleich dem Modifikator deines Attributs zum Zauberwirken.</p>
+      <p>Als Teil der Aktion, mit der du diesen Zauber wirkst, musst du einen Nahkampfangriff mit einer Waffe gegen eine Kreatur in der Reichweite des Zaubers ausführen, sonst misslingt der Zauber. Bei einem Treffer erleidet das Ziel die normalen Auswirkungen des Angriffs, und grünes Feuer springt vom Ziel auf eine andere Kreatur deiner Wahl, die du sehen kannst und die sich innerhalb von 5 Fuß zum Ziel befindet. Die zweite Kreatur erleidet Feuerschaden gleich dem Modifikator deines Attributs zum Zauberwirken.</p>
       <p><b>Auf höheren Stufen:</b> Auf der 5. Stufe fügt der Nahkampfangriff dem Ziel zusätzlich 1W8 Punkte Feuerschaden zu, und der Schaden, den das zweite Ziel erleidet, steigt um 1W8 + der Modifikator deines Attributs zum Zauberwirken. Beide Schadensarten steigen in der 11. und 17. Stufe um 1W8.</p>
       `,
       description_en: `
@@ -986,7 +1014,7 @@ export class SpellsService {
       damage: [{ diceNumber: 1, diceType: 8, damageType: 'fire' }],
       doesHeal: false,
       description_de: `
-      <p>Du erschaffst ein Lagerfeuer in Reichweite auf dem Boden, den du sehen kannst. Bis der Zauber endet, füllt das Lagerfeuer einen Würfel von 5 Fußn. Jede Kreatur, die sich zum Zeitpunkt des Zaubers im Bereich des Feuers befindet, muss einen Rettungswurf auf Geschicklichkeit ablegen oder 1W8 Feuerschaden erleiden. Eine Kreatur muss den Rettungswurf auch machen, wenn sie den Raum des Lagerfeuers zum ersten Mal in einem Zug betritt oder ihren Zug dort beendet.</p>
+      <p>Du erschaffst ein Lagerfeuer in Reichweite auf dem Boden, den du sehen kannst. Bis der Zauber endet, füllt das Lagerfeuer einen Würfel von 5 Fuß. Jede Kreatur, die sich zum Zeitpunkt des Zaubers im Bereich des Feuers befindet, muss einen Rettungswurf auf Geschicklichkeit ablegen oder 1W8 Feuerschaden erleiden. Eine Kreatur muss den Rettungswurf auch machen, wenn sie den Raum des Lagerfeuers zum ersten Mal in einem Zug betritt oder ihren Zug dort beendet.</p>
       <p><b>Auf höheren Stufen:</b> Der Schaden des Zaubers erhöht sich um je 1W8 wenn du Stufe 5 (auf 2W8), Stufe 11 (auf 3W8) und Stufe 17 (auf 4W8) erreichst.</p>
       `,
       description_en: `
@@ -1339,7 +1367,7 @@ export class SpellsService {
       doesDamage: false,
       doesHeal: false,
       description_de: `
-        <p>Du erschaffst bis zu vier fackelgroße Lichter in Reichweite, die als Fackeln, Laternen oder leuchtende Kugeln erscheinen, die für die Dauer der Aktion in der Luft schweben. Du kannst die vier Lichter auch zu einer leuchtenden, vage humanoiden Form von mittlerer Größe kombinieren. Unabhängig davon, welche Form du wählst, verströmt jedes Licht ein schwaches Licht in einem Radius von 10 Fuß. Als Bonusaktion in deinem Zug kannst du die Lichter bis zu 300 Fuß weit an einen neuen Platz in Reichweite bewegen. Ein Licht muss sich in einem Umkreis von 20 Fußn um ein anderes durch diesen Zauber erzeugtes Licht befinden, und ein Licht erlischt, wenn es den Wirkungsbereich des Zaubers verlässt.</p>
+        <p>Du erschaffst bis zu vier fackelgroße Lichter in Reichweite, die als Fackeln, Laternen oder leuchtende Kugeln erscheinen, die für die Dauer der Aktion in der Luft schweben. Du kannst die vier Lichter auch zu einer leuchtenden, vage humanoiden Form von mittlerer Größe kombinieren. Unabhängig davon, welche Form du wählst, verströmt jedes Licht ein schwaches Licht in einem Radius von 10 Fuß. Als Bonusaktion in deinem Zug kannst du die Lichter bis zu 300 Fuß weit an einen neuen Platz in Reichweite bewegen. Ein Licht muss sich in einem Umkreis von 20 Fuß um ein anderes durch diesen Zauber erzeugtes Licht befinden, und ein Licht erlischt, wenn es den Wirkungsbereich des Zaubers verlässt.</p>
       `,
       description_en: `
         <p>You create up to four torch-sized lights within range, making them appear as torches, lanterns, or glowing orbs that hover in the air for the duration. You can also combine the four lights into one glowing vaguely humanoid form of Medium size. Whichever form you choose, each light sheds dim light in a 10-foot radius.</p>
@@ -1377,7 +1405,7 @@ export class SpellsService {
           <li>Du erzeugst einen unmittelbaren, harmlosen sensorischen Effekt, etwa einen Funkenregen, einen Windstoß, eine leise Melodie oder einen merkwürdigen Geruch.</li>
           <li>Du entzündest oder löschst unmittelbar eine Kerze, eine Fackel oder ein kleines Lagerfeuer.</li>
           <li>Du kannst unmittelbar einen Gegenstand, der nicht größer als ein Würfel mit 1 Fuß Kantenlänge ist, säubern oder verschmutzen.</li>
-          <li>Du kannst nichtlebendes Material, das nicht größer als ein Würfel mit 1 Fußn Kantenlänge ist, abkühlen, erhitzen oder würzen. Dies hält eine Stunde lang an.</li>
+          <li>Du kannst nichtlebendes Material, das nicht größer als ein Würfel mit 1 Fuß Kantenlänge ist, abkühlen, erhitzen oder würzen. Dies hält eine Stunde lang an.</li>
           <li>Du lässt einen Farbfleck, ein Mal oder ein Symbol eine Stunde lang auf einem Gegenstand oder einer Oberfläche erscheinen.</li>
           <li>Du erzeugst ein nichtmagisches Schmuckstück oder ein illusorisches Bild, das in deine Hand passt und das bis zum Ende deines nächsten Zuges erhalten bleibt.</li>
         </ul>
@@ -1574,7 +1602,7 @@ export class SpellsService {
       doesDamage: false,
       doesHeal: false,
       description_de: `
-        <p>Du wählst eine Wasserfläche, die du in Reichweite sehen kannst und die in einen Würfel von 5 Fußn passt. Du manipulierst es auf eine der folgenden Arten:</p>
+        <p>Du wählst eine Wasserfläche, die du in Reichweite sehen kannst und die in einen Würfel von 5 Fuß passt. Du manipulierst es auf eine der folgenden Arten:</p>
         <ul>
           <li>Du bewegst das Wasser augenblicklich oder änderst es auf andere Weise, wie du es dir vorstellst, bis zu 5 Fuß in jede Richtung. Diese Bewegung hat nicht genug Kraft, um Schaden zu verursachen.</li>
           <li>Du bringst das Wasser dazu, sich zu einfachen Formen zu formen und sich auf deine Anweisung hin zu bewegen. Diese Veränderung hält 1 Stunde lang an.</li>
@@ -2561,7 +2589,7 @@ export class SpellsService {
       doesDamage: false,
       doesHeal: false,
       description_de:
-        'Während der Wirkungsdauer nimmst du die Gegenwart und die Position von Giften, giftigen Kreaturen und Krankheiten im Abstand von bis zu neun Metern von dir wahr. Du kannst auch die Art des Gifts, der giftigen Kreatur oder der Krankheit bestimmen. Dieser Zauber durchdringt die meisten Barrieren, wird aber von 1 Fußn Stein, 2,5 Zentimetern gewöhnlichem Metall, dünnem Bleiblech sowie von einem Meter Holz oder Erde blockiert.',
+        'Während der Wirkungsdauer nimmst du die Gegenwart und die Position von Giften, giftigen Kreaturen und Krankheiten im Abstand von bis zu neun Metern von dir wahr. Du kannst auch die Art des Gifts, der giftigen Kreatur oder der Krankheit bestimmen. Dieser Zauber durchdringt die meisten Barrieren, wird aber von 1 Fuß Stein, 2,5 Zentimetern gewöhnlichem Metall, dünnem Bleiblech sowie von einem Meter Holz oder Erde blockiert.',
       description_en:
         'For the duration, you can sense the presence and location of poisons, poisonous creatures, and diseases within 30 feet of you. You also identify the kind of poison, poisonous creature, or disease in each case. The spell can penetrate most barriers, but it is blocked by 1 foot of stone, 1 inch of common metal, a thin sheet of lead, or 3 feet of wood or dirt.',
       school: 'divination',