- @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;
+ };
+}