diff --git a/src/app/components/characters/character-dashboard/character-dashboard.component.html b/src/app/components/characters/character-dashboard/character-dashboard.component.html index b1fac21..439a393 100644 --- a/src/app/components/characters/character-dashboard/character-dashboard.component.html +++ b/src/app/components/characters/character-dashboard/character-dashboard.component.html @@ -3,121 +3,50 @@ {{ character().age }}, {{ character().gender }}, {{ character().nation }} , {{ character().religion }} -
-
- - - - - - - - -
+
+
+
+ + + + + + + + +
-
- -
-
- - - - - +
+ +
+
+ + + + + +
+
+ + + + + +
+
+ + + +
-
- - - - - -
-
- - - -
-
- -
-
+ + +
+ diff --git a/src/app/components/characters/character-dashboard/character-dashboard.component.ts b/src/app/components/characters/character-dashboard/character-dashboard.component.ts index 654ddec..ae1415d 100644 --- a/src/app/components/characters/character-dashboard/character-dashboard.component.ts +++ b/src/app/components/characters/character-dashboard/character-dashboard.component.ts @@ -1,20 +1,19 @@ -import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CharactersStore } from '../../../stores/characters.store'; import { filter, map } from 'rxjs'; import { toSignal } from '@angular/core/rxjs-interop'; import { H1Component } from '../../headings/h1/h1.component'; import { CharacterStatisticComponent } from '../character-statistic/character-statistic.component'; -import { CharacterEntity } from '../../../models/character/character.entity'; import { ContentGroupComponent } from '../../content-group/content-group.component'; import { EditModeStore } from '../../../stores/edit-mode.store'; -import { CharactersService } from '../../../services/characters.service'; import { SecondaryH1Component } from '../../headings/secondary-h1/secondary-h1.component'; +import { NonNullableFormBuilder, ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'pap-character-dashboard', standalone: true, - imports: [H1Component, CharacterStatisticComponent, ContentGroupComponent, SecondaryH1Component], + imports: [H1Component, CharacterStatisticComponent, ContentGroupComponent, SecondaryH1Component, ReactiveFormsModule], templateUrl: './character-dashboard.component.html', styleUrl: './character-dashboard.component.css', changeDetection: ChangeDetectionStrategy.OnPush, @@ -23,7 +22,7 @@ export default class CharacterDashboardComponent { protected readonly editModeStore = inject(EditModeStore); private readonly activatedRoute = inject(ActivatedRoute); private readonly characterStore = inject(CharactersStore); - private readonly charactersService = inject(CharactersService); + // private readonly charactersService = inject(CharactersService); private readonly id = toSignal( this.activatedRoute.paramMap.pipe( map(params => params.get('characterId')), @@ -35,136 +34,62 @@ export default class CharacterDashboardComponent { return this.characterStore.entityMap()[this.id()!]; }); - protected updateStrength(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - main: { ...character.main, strength: character.main.strength + increment }, - }); - } - - protected updateAgility(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - main: { ...character.main, agility: character.main.agility + increment }, - }); - } - - protected updateStamina(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - main: { ...character.main, stamina: character.main.stamina + increment }, - }); - } - - protected updateMagic(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - main: { ...character.main, magic: character.main.magic + increment }, - }); - } - - protected updateSpirit(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - main: { ...character.main, spirit: character.main.spirit + increment }, - }); - } - - protected updateIntelligence(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - main: { ...character.main, intelligence: character.main.intelligence + increment }, - }); - } - - protected updateLife(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, life: character.value.life + increment }, - }); - } - - protected updatePower(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, power: character.value.power + increment }, - }); - } + private readonly formBuilder = inject(NonNullableFormBuilder); + protected readonly formGroup = this.formBuilder.group({ + main: this.formBuilder.group({ + agility: [0], + magic: [0], + spirit: [0], + intelligence: [0], + stamina: [0], + strength: [0], + }), + value: this.formBuilder.group({ + life: [0], + power: [0], + energy: [0], + strike: [0], + perception: [0], + magicDefense: [0], + magicTolerance: [0], + magicControl: [0], + mana: [0], + manaRegeneration: [0], + reaction: [0], + cover: [0], + authority: [0], + }), + }); - protected updateEnergy(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, energy: character.value.energy + increment }, - }); - } + constructor() { + effect( + () => { + const character = this.character(); - protected updateStrike(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, strike: character.value.strike + increment }, - }); - } + this.formGroup.reset({ main: character.main, value: character.value }); + }, + { allowSignalWrites: true }, + ); // The form internally uses a signal - protected updatePerception(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, perception: character.value.perception + increment }, - }); - } + effect(() => { + const isEditModeEnabled = this.editModeStore.isEnabled(); - protected updateMagicDefense(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, magicDefense: character.value.magicDefense + increment }, - }); - } + if (isEditModeEnabled) { + return; + } - protected updateMagicTolerance(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, magicTolerance: character.value.magicTolerance + increment }, - }); - } + if (this.formGroup.pristine) { + return; + } - protected updateMagicControl(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, magicControl: character.value.magicControl + increment }, - }); - } - - protected updateMana(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, mana: character.value.mana + increment }, - }); - } + const character = this.character(); - protected updateManaRegeneration(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, manaRegeneration: character.value.manaRegeneration + increment }, - }); - } + // We might want to save the changes + const statistics = this.formGroup.getRawValue(); - protected updateReaction(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, reaction: character.value.reaction + increment }, - }); - } - - protected updateCover(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, cover: character.value.cover + increment }, - }); - } + console.log('effect'); - protected updateAuthority(character: CharacterEntity, increment: number) { - void this.charactersService.update({ - ...character, - value: { ...character.value, authority: character.value.authority + increment }, + void this.characterStore.updateStatistics({ ...character, main: statistics.main, value: statistics.value }); }); } } diff --git a/src/app/components/characters/character-statistic/character-statistic.component.html b/src/app/components/characters/character-statistic/character-statistic.component.html index 5eec777..6f65bf7 100644 --- a/src/app/components/characters/character-statistic/character-statistic.component.html +++ b/src/app/components/characters/character-statistic/character-statistic.component.html @@ -1,44 +1,48 @@
-

{{ label }}

+

{{ label() }}

- @if (!allowEdit) { -

{{ value }}

+ @if (!allowEdit()) { +

{{ value() }}

} - @if (allowEdit) { + @if (allowEdit()) {
-

{{ value }}

+

{{ value() }}

- @if (allowTenIncrement) { + @if (showTens()) { } - @if (allowTenIncrement) { + @if (showTens()) { diff --git a/src/app/components/characters/character-statistic/character-statistic.component.ts b/src/app/components/characters/character-statistic/character-statistic.component.ts index e24f363..0e70f44 100644 --- a/src/app/components/characters/character-statistic/character-statistic.component.ts +++ b/src/app/components/characters/character-statistic/character-statistic.component.ts @@ -1,4 +1,6 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, forwardRef, input, signal } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ValueUpdater } from '../../../types'; @Component({ selector: 'pap-character-statistic', @@ -7,12 +9,43 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from templateUrl: './character-statistic.component.html', styleUrl: './character-statistic.component.css', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CharacterStatisticComponent), + multi: true, + }, + ], }) -export class CharacterStatisticComponent { - @Input({ required: true }) label!: string; - @Input() allowEdit = false; - @Input() allowOneIncrement = true; - @Input() allowTenIncrement = true; - @Input({ required: true }) value!: number; - @Output() incrementChange = new EventEmitter(); +export class CharacterStatisticComponent implements ControlValueAccessor { + label = input.required(); + allowEdit = input(false); + minValue = input(0); + maxValue = input(Number.MAX_SAFE_INTEGER); + showTens = input(true); + remainingPoints = input(Number.MAX_SAFE_INTEGER); + protected value = signal(0); + protected canUseTenIncrement = computed(() => this.remainingPoints() >= 10 && this.value() + 10 <= this.maxValue()); + protected canUseOneIncrement = computed(() => this.remainingPoints() >= 1 && this.value() + 1 <= this.maxValue()); + protected canUseTenDecrement = computed(() => this.value() - 10 >= this.minValue()); + protected canUseOneDecrement = computed(() => this.value() - 1 >= this.minValue()); + private onChange!: ValueUpdater; + private onTouched!: VoidFunction; + + writeValue(value: number): void { + this.value.set(value); + } + + registerOnChange(fn: ValueUpdater): void { + this.onChange = fn; + } + + registerOnTouched(fn: VoidFunction): void { + this.onTouched = fn; + } + + updateValue(update: number): void { + this.value.update(value => value + update); + this.onChange(this.value()); + } } diff --git a/src/app/components/characters/create-character/create-character.component.html b/src/app/components/characters/create-character/create-character.component.html index 81212a8..a10c821 100644 --- a/src/app/components/characters/create-character/create-character.component.html +++ b/src/app/components/characters/create-character/create-character.component.html @@ -8,5 +8,20 @@ +
+ + + + + + + + +
+ Charakter erstellen diff --git a/src/app/components/characters/create-character/create-character.component.ts b/src/app/components/characters/create-character/create-character.component.ts index 9b5af27..d7904da 100644 --- a/src/app/components/characters/create-character/create-character.component.ts +++ b/src/app/components/characters/create-character/create-character.component.ts @@ -1,29 +1,74 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import { H1Component } from '../../headings/h1/h1.component'; -import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { FormsModule, NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { TextInputComponent } from '../../inputs/text-input/text-input.component'; import { SubmitButtonComponent } from '../../inputs/submit-button/submit-button.component'; import { Router, RouterLink } from '@angular/router'; import { CharactersService } from '../../../services/characters.service'; +import { CharacterStatisticComponent } from '../character-statistic/character-statistic.component'; +import { ContentGroupComponent } from '../../content-group/content-group.component'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { map } from 'rxjs'; +import { requiredPointsToDistributeValidator } from '../../../validators/required-points-to-distribute-validator'; +import { CharactersStore } from '../../../stores/characters.store'; + +const INITIAL_POINTS_TO_DISTRIBUTE = 200; @Component({ selector: 'pap-create-character', standalone: true, - imports: [H1Component, TextInputComponent, SubmitButtonComponent, FormsModule, ReactiveFormsModule, RouterLink], + imports: [ + H1Component, + TextInputComponent, + SubmitButtonComponent, + FormsModule, + ReactiveFormsModule, + RouterLink, + CharacterStatisticComponent, + ContentGroupComponent, + ], templateUrl: './create-character.component.html', styleUrl: './create-character.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) export default class CreateCharacterComponent { - protected readonly formGroup = inject(FormBuilder).group({ + protected readonly charactersService = inject(CharactersService); + protected readonly charactersStore = inject(CharactersStore); + private readonly formBuilder = inject(NonNullableFormBuilder); + protected readonly formGroup = this.formBuilder.group({ name: ['', Validators.required], nation: ['', Validators.required], gender: ['', Validators.required], age: [0, [Validators.required, Validators.min(0)]], religion: ['', Validators.required], group: ['', Validators.required], + main: this.formBuilder.group( + { + agility: [0], + magic: [0], + spirit: [0], + intelligence: [0], + stamina: [0], + strength: [0], + }, + { + validators: [requiredPointsToDistributeValidator(this.charactersService, INITIAL_POINTS_TO_DISTRIBUTE)], + }, + ), + }); + private readonly pointsChange = toSignal( + this.formGroup.controls['main'].valueChanges.pipe(map(() => this.formGroup.controls['main'].getRawValue())), + ); + + protected readonly remainingPoints = computed(() => { + const distributedPoints = this.pointsChange(); + + if (!distributedPoints) { + return INITIAL_POINTS_TO_DISTRIBUTE; + } + + return INITIAL_POINTS_TO_DISTRIBUTE - this.charactersService.sumMainStatistics(distributedPoints); }); - protected readonly charactersService = inject(CharactersService); private readonly router = inject(Router); protected async submit(): Promise { @@ -31,14 +76,7 @@ export default class CreateCharacterComponent { return; } - const character = await this.charactersService.add( - this.formGroup.value.name!, - this.formGroup.value.gender!, - this.formGroup.value.age!, - this.formGroup.value.nation!, - this.formGroup.value.religion!, - this.formGroup.value.group!, - ); + const character = await this.charactersStore.add(this.formGroup.getRawValue()); await this.router.navigate(['/characters', character.id, 'dashboard']); } diff --git a/src/app/components/content-group/content-group.component.html b/src/app/components/content-group/content-group.component.html index bc2bacc..32ad869 100644 --- a/src/app/components/content-group/content-group.component.html +++ b/src/app/components/content-group/content-group.component.html @@ -1,9 +1,12 @@
- {{ label }} - + {{ title() }}
+ + @if (footer(); as subTitle) { + {{ subTitle }} + }
diff --git a/src/app/components/content-group/content-group.component.ts b/src/app/components/content-group/content-group.component.ts index 684edc4..bf38043 100644 --- a/src/app/components/content-group/content-group.component.ts +++ b/src/app/components/content-group/content-group.component.ts @@ -1,14 +1,17 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { H2Component } from '../headings/h2/h2.component'; +import { H3Component } from '../headings/h3/h3.component'; +import { SecondaryH1Component } from '../headings/secondary-h1/secondary-h1.component'; @Component({ selector: 'pap-content-group', standalone: true, - imports: [H2Component], + imports: [H2Component, H3Component, SecondaryH1Component], templateUrl: './content-group.component.html', styleUrl: './content-group.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ContentGroupComponent { - @Input({ required: true }) label!: string; + title = input.required(); + footer = input(); } diff --git a/src/app/models/character/character-level.ts b/src/app/models/character/character-level.ts new file mode 100644 index 0000000..ba828c8 --- /dev/null +++ b/src/app/models/character/character-level.ts @@ -0,0 +1,5 @@ +export interface CharacterLevel { + level: number; + points: number; + remainingPoints: number; +} diff --git a/src/app/models/character/character.entity.ts b/src/app/models/character/character.entity.ts index 69f5234..7cddaf6 100644 --- a/src/app/models/character/character.entity.ts +++ b/src/app/models/character/character.entity.ts @@ -1,7 +1,8 @@ import { MainStatistics } from './main-statistics'; import { ValueStatistics } from './value-statistics'; +import { Statistics } from './statistics'; -export interface CharacterEntity { +export interface CharacterEntity extends Statistics { id: number; name: string; gender: string; diff --git a/src/app/models/character/statistics.ts b/src/app/models/character/statistics.ts new file mode 100644 index 0000000..f3910c1 --- /dev/null +++ b/src/app/models/character/statistics.ts @@ -0,0 +1,7 @@ +import { MainStatistics } from './main-statistics'; +import { ValueStatistics } from './value-statistics'; + +export interface Statistics { + main: MainStatistics; + value: ValueStatistics; +} diff --git a/src/app/models/character/transaction.entity.ts b/src/app/models/character/transaction.entity.ts new file mode 100644 index 0000000..256d331 --- /dev/null +++ b/src/app/models/character/transaction.entity.ts @@ -0,0 +1,32 @@ +import { MainStatistics } from './main-statistics'; +import { ValueStatistics } from './value-statistics'; +import { CharacterEntity } from './character.entity'; + +export enum TransactionType { + CharacterCreation = 'character-creation', + MainStatistic = 'main-statistic', + ValueStatistic = 'value-statistic', +} + +export interface TransactionEntity { + id: number; + characterId: number; + transaction: Transaction; + timestamp: number; +} + +export interface Transaction { + type: TransactionType; + value: T; +} + +export interface StatisticsTransaction extends Transaction { + type: TransactionType.MainStatistic | TransactionType.ValueStatistic; + statistic: keyof MainStatistics | keyof ValueStatistics; + value: number; +} + +export interface CharacterCreationTransaction extends Transaction { + type: TransactionType.CharacterCreation; + value: CharacterEntity; +} diff --git a/src/app/services/characters.service.ts b/src/app/services/characters.service.ts index 21c81b0..d6549f4 100644 --- a/src/app/services/characters.service.ts +++ b/src/app/services/characters.service.ts @@ -1,33 +1,30 @@ import { inject, Injectable } from '@angular/core'; import { CharacterEntity } from '../models/character/character.entity'; -import { CharactersStore } from '../stores/characters.store'; +import { MainStatistics } from '../models/character/main-statistics'; +import { CharactersTable } from './tables/characters.table'; +import { TransactionsService } from './transactions.service'; +import { Statistics } from '../models/character/statistics'; @Injectable({ providedIn: 'root', }) export class CharactersService { - private readonly charactersStore = inject(CharactersStore); + private readonly charactersTable = inject(CharactersTable); + private readonly transactionsService = inject(TransactionsService); - async update(character: CharacterEntity) { - await this.charactersStore.update(character); + async updateStatistics(character: CharacterEntity) { + await this.charactersTable.update(character); + await this.transactionsService.writeMainStatistics(character.id, character.main); + await this.transactionsService.writeValueStatistics(character.id, character.value); } - async add(name: string, gender: string, age: number, nation: string, religion: string, group: string): Promise { - return this.charactersStore.add({ - name, - gender, - age, - nation, - religion, - group, - main: { - agility: 0, - magic: 0, - spirit: 0, - stamina: 0, - strength: 0, - intelligence: 0, - }, + sumMainStatistics(statistics: MainStatistics): number { + return statistics.agility + statistics.magic + statistics.spirit + statistics.intelligence + statistics.stamina + statistics.strength; + } + + async add(character: Omit): Promise { + const characterEntity = await this.charactersTable.add({ + ...character, value: { life: 0, power: 0, @@ -44,5 +41,49 @@ export class CharactersService { authority: 0, }, }); + + await this.transactionsService.writeCharacterCreation(characterEntity); + + return characterEntity; + } + + async list(): Promise { + return await this.charactersTable.list(); + } + + generateStatisticsDiff(a: Statistics, b: Statistics): Statistics { + return { + main: { + strength: b.main.strength - a.main.strength, + agility: b.main.agility - a.main.agility, + stamina: b.main.stamina - a.main.stamina, + magic: b.main.magic - a.main.magic, + spirit: b.main.spirit - a.main.spirit, + intelligence: b.main.intelligence - a.main.intelligence, + }, + value: { + life: b.value.life - a.value.life, + power: b.value.power - a.value.power, + energy: b.value.energy - a.value.energy, + magicDefense: b.value.magicDefense - a.value.magicDefense, + magicTolerance: b.value.magicTolerance - a.value.magicTolerance, + magicControl: b.value.magicControl - a.value.magicControl, + strike: b.value.strike - a.value.strike, + perception: b.value.perception - a.value.perception, + mana: b.value.mana - a.value.mana, + manaRegeneration: b.value.manaRegeneration - a.value.manaRegeneration, + reaction: b.value.reaction - a.value.reaction, + cover: b.value.cover - a.value.cover, + authority: b.value.authority - a.value.authority, + }, + }; + } + + hasStatisticsChanges(a: Statistics, b: Statistics): boolean { + const diff = this.generateStatisticsDiff(a, b); + const mainStatsZero = Object.values(diff.main).every(value => value === 0); + const valueStatsZero = Object.values(diff.value).every(value => value === 0); + + return !mainStatsZero || !valueStatsZero; } } diff --git a/src/app/services/database.service.ts b/src/app/services/database.service.ts index 7faeaba..7157be8 100644 --- a/src/app/services/database.service.ts +++ b/src/app/services/database.service.ts @@ -3,6 +3,7 @@ import Dexie, { Table } from 'dexie'; import 'dexie-export-import'; import { CharacterEntity } from '../models/character/character.entity'; import { SettingsEntity } from '../models/settings.entity'; +import { TransactionEntity } from '../models/character/transaction.entity'; @Injectable({ providedIn: 'root', @@ -10,6 +11,7 @@ import { SettingsEntity } from '../models/settings.entity'; export class DatabaseService extends Dexie { readonly characters!: Table; readonly settings!: Table; + readonly transactions!: Table, number>; constructor() { super('tipis-pap-tools'); @@ -17,9 +19,14 @@ export class DatabaseService extends Dexie { this.version(1).stores({ characters: '++id', }); + this.version(2).stores({ settings: '++id', }); + + this.version(3).stores({ + transactions: '++id,characterId,timestamp', + }); } initialize(): void { diff --git a/src/app/services/tables/characters.table.ts b/src/app/services/tables/characters.table.ts index 7c60616..6f9943c 100644 --- a/src/app/services/tables/characters.table.ts +++ b/src/app/services/tables/characters.table.ts @@ -11,9 +11,7 @@ export class CharactersTable { } async add(character: Omit) { - const id = await this.databaseService.characters.add({ - ...character, - } as CharacterEntity); + const id = await this.databaseService.characters.add(character as CharacterEntity); const newCharacter = await this.databaseService.characters.get(id); return newCharacter!; diff --git a/src/app/services/tables/transactions.table.ts b/src/app/services/tables/transactions.table.ts new file mode 100644 index 0000000..9a0ffdc --- /dev/null +++ b/src/app/services/tables/transactions.table.ts @@ -0,0 +1,16 @@ +import { inject, Injectable } from '@angular/core'; +import { DatabaseService } from '../database.service'; +import { TransactionEntity } from '../../models/character/transaction.entity'; +import { DateTime } from 'luxon'; + +@Injectable({ providedIn: 'root' }) +export class TransactionsTable { + private readonly databaseService = inject(DatabaseService); + + async add(transaction: Omit, 'id' | 'timestamp'>): Promise { + await this.databaseService.transactions.add({ + ...transaction, + timestamp: DateTime.now().toMillis(), + } as TransactionEntity); + } +} diff --git a/src/app/services/transactions.service.ts b/src/app/services/transactions.service.ts new file mode 100644 index 0000000..94d144d --- /dev/null +++ b/src/app/services/transactions.service.ts @@ -0,0 +1,55 @@ +import { inject, Injectable } from '@angular/core'; +import { TransactionsTable } from './tables/transactions.table'; +import { MainStatistics } from '../models/character/main-statistics'; +import { ValueStatistics } from '../models/character/value-statistics'; +import { CharacterCreationTransaction, StatisticsTransaction, TransactionType } from '../models/character/transaction.entity'; +import { CharacterEntity } from '../models/character/character.entity'; + +@Injectable({ providedIn: 'root' }) +export class TransactionsService { + private readonly transactionsTable = inject(TransactionsTable); + + async writeCharacterCreation(character: CharacterEntity): Promise { + const transaction: CharacterCreationTransaction = { + type: TransactionType.CharacterCreation, + value: character, + }; + + await this.transactionsTable.add({ + transaction: transaction, + characterId: character.id, + }); + } + + async writeMainStatistics(characterId: number, main: MainStatistics): Promise { + for (const unsafeKey in main) { + const key = unsafeKey as keyof MainStatistics; + await this.writeStatistic(characterId, key, main[key], TransactionType.MainStatistic); + } + } + + async writeValueStatistics(characterId: number, value: ValueStatistics): Promise { + for (const unsafeKey in value) { + const key = unsafeKey as keyof ValueStatistics; + await this.writeStatistic(characterId, key, value[key], TransactionType.ValueStatistic); + } + } + + private async writeStatistic( + characterId: number, + name: keyof MainStatistics | keyof ValueStatistics, + value: number, + type: TransactionType.MainStatistic | TransactionType.ValueStatistic, + ): Promise { + const transaction: StatisticsTransaction = { + type: type, + statistic: name, + value, + }; + + await this.transactionsTable.add({ + transaction: transaction, + characterId, + }); + } +} diff --git a/src/app/stores/characters.store.ts b/src/app/stores/characters.store.ts index c224a33..6f229e2 100644 --- a/src/app/stores/characters.store.ts +++ b/src/app/stores/characters.store.ts @@ -3,32 +3,35 @@ import { addEntity, setAllEntities, updateEntity, withEntities } from '@ngrx/sig import { CharacterEntity } from '../models/character/character.entity'; import { computed, effect, inject, Injector } from '@angular/core'; import { withAppInitialization } from './features/with-app-initialization'; -import { CharactersTable } from '../services/tables/characters.table'; +import { CharactersService } from '../services/characters.service'; export const CharactersStore = signalStore( { providedIn: 'root' }, withEntities(), withMethods(store => { - const charactersTable = inject(CharactersTable); + const charactersService = inject(CharactersService); return { restore: async () => { - const characters = await charactersTable.list(); + const characters = await charactersService.list(); patchState(store, setAllEntities(characters)); }, - add: async (character: Omit) => { - const newCharacter = await charactersTable.add(character); + add: async (character: Omit) => { + const newCharacter = await charactersService.add(character); patchState(store, addEntity(newCharacter)); return newCharacter; }, - update: async (character: CharacterEntity) => { - await charactersTable.update(character); + updateStatistics: async (character: CharacterEntity) => { + await charactersService.updateStatistics(character); patchState( store, updateEntity({ id: character.id, - changes: character, + changes: { + main: character.main, + value: character.value, + }, }), ); }, diff --git a/src/app/types.ts b/src/app/types.ts new file mode 100644 index 0000000..ebff4ec --- /dev/null +++ b/src/app/types.ts @@ -0,0 +1 @@ +export type ValueUpdater = (value: number) => void; diff --git a/src/app/validators/required-points-to-distribute-validator.ts b/src/app/validators/required-points-to-distribute-validator.ts new file mode 100644 index 0000000..29366a6 --- /dev/null +++ b/src/app/validators/required-points-to-distribute-validator.ts @@ -0,0 +1,14 @@ +import { CharactersService } from '../services/characters.service'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { MainStatistics } from '../models/character/main-statistics'; + +export function requiredPointsToDistributeValidator(charactersService: CharactersService, requiredPointsToDistribute: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const sum = charactersService.sumMainStatistics(control.value); + if (sum !== requiredPointsToDistribute) { + return { invalidDistributedPoints: true }; + } + + return null; + }; +}