feat: add signal-driven world state
This commit is contained in:
48
apps/web/src/app/core/api/game-api.models.ts
Normal file
48
apps/web/src/app/core/api/game-api.models.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export interface LocationSummary {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CharacterResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
level: number;
|
||||
experience: number;
|
||||
currentHp: number;
|
||||
maxHp: number;
|
||||
attack: number;
|
||||
currentLocation: LocationSummary;
|
||||
}
|
||||
|
||||
export interface CurrentLocationConnection {
|
||||
targetLocation: LocationSummary;
|
||||
travelDurationSeconds: number;
|
||||
danger: 'LOW' | 'HIGH';
|
||||
}
|
||||
|
||||
export interface CurrentLocationResponse {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
regionKey: string;
|
||||
minRecommendedLevel: number;
|
||||
maxRecommendedLevel: number;
|
||||
dangerLevel: number;
|
||||
isSafe: boolean;
|
||||
huntingEnabled: boolean;
|
||||
artworkPath: string;
|
||||
connections: CurrentLocationConnection[];
|
||||
}
|
||||
|
||||
export type CurrentTravel =
|
||||
| { status: 'IDLE' }
|
||||
| {
|
||||
status: 'TRAVELLING';
|
||||
originLocation: LocationSummary;
|
||||
targetLocation: LocationSummary;
|
||||
startedAt: string;
|
||||
arrivesAt: string;
|
||||
}
|
||||
| { status: 'COMPLETED'; targetLocation: LocationSummary };
|
||||
50
apps/web/src/app/core/api/game-api.service.spec.ts
Normal file
50
apps/web/src/app/core/api/game-api.service.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { GameApiService } from './game-api.service';
|
||||
|
||||
describe('GameApiService', () => {
|
||||
let service: GameApiService;
|
||||
let http: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [GameApiService, provideHttpClient(), provideHttpClientTesting()],
|
||||
});
|
||||
|
||||
service = TestBed.inject(GameApiService);
|
||||
http = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
http.verify();
|
||||
});
|
||||
|
||||
it('uses relative API URLs for all read requests', () => {
|
||||
service.getCharacter().subscribe();
|
||||
service.getCurrentLocation().subscribe();
|
||||
service.getCurrentTravel().subscribe();
|
||||
|
||||
const requests = http.match((request) => request.method === 'GET');
|
||||
|
||||
expect(requests.map((request) => request.request.url)).toEqual([
|
||||
'/api/characters/me',
|
||||
'/api/world/current-location',
|
||||
'/api/travel/current',
|
||||
]);
|
||||
expect(requests.every((request) => !request.request.url.includes('://'))).toBe(true);
|
||||
|
||||
for (const request of requests) {
|
||||
request.flush({});
|
||||
}
|
||||
});
|
||||
|
||||
it('posts only the target location ID when starting travel', () => {
|
||||
service.startTravel('target-uuid').subscribe();
|
||||
|
||||
const request = http.expectOne('/api/travel');
|
||||
expect(request.request.method).toBe('POST');
|
||||
expect(request.request.body).toEqual({ targetLocationId: 'target-uuid' });
|
||||
request.flush({ status: 'IDLE' });
|
||||
});
|
||||
});
|
||||
25
apps/web/src/app/core/api/game-api.service.ts
Normal file
25
apps/web/src/app/core/api/game-api.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CharacterResponse, CurrentLocationResponse, CurrentTravel } from './game-api.models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GameApiService {
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
getCharacter(): Observable<CharacterResponse> {
|
||||
return this.http.get<CharacterResponse>('/api/characters/me');
|
||||
}
|
||||
|
||||
getCurrentLocation(): Observable<CurrentLocationResponse> {
|
||||
return this.http.get<CurrentLocationResponse>('/api/world/current-location');
|
||||
}
|
||||
|
||||
startTravel(targetLocationId: string): Observable<CurrentTravel> {
|
||||
return this.http.post<CurrentTravel>('/api/travel', { targetLocationId });
|
||||
}
|
||||
|
||||
getCurrentTravel(): Observable<CurrentTravel> {
|
||||
return this.http.get<CurrentTravel>('/api/travel/current');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user