feat(combat): generalize the web combat action and expose potions/monster intent
This commit is contained in:
@@ -149,9 +149,10 @@ export interface HuntResult {
|
||||
}
|
||||
|
||||
export type CombatStatus = 'ACTIVE' | 'WON' | 'LOST';
|
||||
export type CombatEventType = 'DAMAGE' | 'COMBAT_WON' | 'COMBAT_LOST';
|
||||
export type CombatEventType = 'DAMAGE' | 'HEAL' | 'DEFEND' | 'TELEGRAPH' | 'INTERRUPT' | 'COMBAT_WON' | 'COMBAT_LOST';
|
||||
export type CombatSide = 'PLAYER' | 'MONSTER';
|
||||
export type CombatAction = 'ATTACK';
|
||||
export type CombatAction = 'ATTACK' | 'HEAVY_STRIKE' | 'SHIELD_BASH' | 'DEFEND' | 'POTION';
|
||||
export type CombatMonsterIntent = 'HEAVY_ATTACK';
|
||||
|
||||
export interface CombatEvent {
|
||||
round: number;
|
||||
@@ -166,6 +167,8 @@ export interface CombatPlayer {
|
||||
name: string;
|
||||
maxHp: number;
|
||||
currentHp: number;
|
||||
potionsRemaining: number;
|
||||
potionsMax: number;
|
||||
}
|
||||
|
||||
export interface CombatMonster {
|
||||
@@ -175,6 +178,7 @@ export interface CombatMonster {
|
||||
maxHp: number;
|
||||
currentHp: number;
|
||||
artworkPath: string;
|
||||
pendingIntent: CombatMonsterIntent | null;
|
||||
}
|
||||
|
||||
export interface Combat {
|
||||
|
||||
@@ -9,7 +9,7 @@ const runningCombat: Combat = {
|
||||
id: 'combat-running',
|
||||
status: 'ACTIVE',
|
||||
round: 4,
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 62 },
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 62, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
@@ -17,6 +17,7 @@ const runningCombat: Combat = {
|
||||
maxHp: 75,
|
||||
currentHp: 30,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
pendingIntent: null,
|
||||
},
|
||||
events: [],
|
||||
rewards: null,
|
||||
|
||||
@@ -10,7 +10,7 @@ const startedCombat: Combat = {
|
||||
id: 'combat-1',
|
||||
status: 'ACTIVE',
|
||||
round: 1,
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100 },
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
@@ -18,6 +18,7 @@ const startedCombat: Combat = {
|
||||
maxHp: 45,
|
||||
currentHp: 45,
|
||||
artworkPath: '/images/monsters/ash-rat.png',
|
||||
pendingIntent: null,
|
||||
},
|
||||
events: [],
|
||||
rewards: null,
|
||||
@@ -139,14 +140,22 @@ describe('CombatStore', () => {
|
||||
it('sends only the ATTACK action and replaces combat with the server response', async () => {
|
||||
await store.startCombat('encounter-1');
|
||||
|
||||
await store.attack();
|
||||
await store.performAction('ATTACK');
|
||||
|
||||
expect(api.performCombatAction).toHaveBeenCalledWith('combat-1', 'ATTACK');
|
||||
expect(store.combat()).toEqual(afterAttack);
|
||||
});
|
||||
|
||||
it('sends whichever action is requested', async () => {
|
||||
await store.startCombat('encounter-1');
|
||||
|
||||
await store.performAction('HEAVY_STRIKE');
|
||||
|
||||
expect(api.performCombatAction).toHaveBeenCalledWith('combat-1', 'HEAVY_STRIKE');
|
||||
});
|
||||
|
||||
it('does nothing when attacking without a loaded combat', async () => {
|
||||
await store.attack();
|
||||
await store.performAction('ATTACK');
|
||||
|
||||
expect(api.performCombatAction).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -162,9 +171,9 @@ describe('CombatStore', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const first = store.attack();
|
||||
const first = store.performAction('ATTACK');
|
||||
expect(store.actionPending()).toBe(true);
|
||||
const second = store.attack();
|
||||
const second = store.performAction('ATTACK');
|
||||
|
||||
resolveAttack(afterAttack);
|
||||
await Promise.all([first, second]);
|
||||
@@ -176,7 +185,7 @@ describe('CombatStore', () => {
|
||||
await store.startCombat('encounter-1');
|
||||
api.performCombatAction.mockReturnValue(throwError(() => new Error('Netzwerkfehler')));
|
||||
|
||||
await store.attack();
|
||||
await store.performAction('ATTACK');
|
||||
|
||||
expect(store.actionPending()).toBe(false);
|
||||
expect(store.combat()).toEqual(startedCombat);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Combat } from '../../core/api/game-api.models';
|
||||
import { Combat, CombatAction } from '../../core/api/game-api.models';
|
||||
import { GameApiService } from '../../core/api/game-api.service';
|
||||
|
||||
const GENERIC_ERROR_MESSAGE = 'Der Kampf konnte nicht geladen werden.';
|
||||
@@ -16,6 +16,7 @@ const COMBAT_ERROR_MESSAGES: Readonly<Record<string, string>> = {
|
||||
COMBAT_ALREADY_ACTIVE: 'Du befindest dich bereits in einem Kampf.',
|
||||
COMBAT_NOT_FOUND: 'Dieser Kampf wurde nicht gefunden.',
|
||||
COMBAT_ALREADY_FINISHED: 'Dieser Kampf ist bereits beendet.',
|
||||
COMBAT_NO_POTIONS_REMAINING: 'Du hast keine Tränke mehr.',
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -83,7 +84,7 @@ export class CombatStore {
|
||||
}
|
||||
}
|
||||
|
||||
async attack(): Promise<void> {
|
||||
async performAction(action: CombatAction): Promise<void> {
|
||||
const combat = this.combatState();
|
||||
if (!combat || this.actionPendingState()) {
|
||||
return;
|
||||
@@ -93,7 +94,7 @@ export class CombatStore {
|
||||
this.clearError();
|
||||
|
||||
try {
|
||||
const updated = await firstValueFrom(this.api.performCombatAction(combat.id, 'ATTACK'));
|
||||
const updated = await firstValueFrom(this.api.performCombatAction(combat.id, action));
|
||||
this.combatState.set(updated);
|
||||
} catch (error) {
|
||||
this.setError(error);
|
||||
|
||||
@@ -51,7 +51,7 @@ const startedCombat: Combat = {
|
||||
id: 'combat-2',
|
||||
status: 'ACTIVE',
|
||||
round: 1,
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100 },
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100, potionsRemaining: 2, potionsMax: 2 },
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
@@ -59,6 +59,7 @@ const startedCombat: Combat = {
|
||||
maxHp: 75,
|
||||
currentHp: 75,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
pendingIntent: null,
|
||||
},
|
||||
events: [],
|
||||
rewards: null,
|
||||
|
||||
Reference in New Issue
Block a user