Registers HuntingModule (POST /api/hunts) into the DI graph alongside its new entities, and adds possibleMonsters (enabled LocationMonster pool, weight-descending, empty when hunting is disabled) to WorldService.getCurrentLocation. Also updates the pre-existing DB-less app.e2e-spec.ts to override HuntingModule the same way the other feature modules already are, since it now needs a real DataSource.
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { Character } from '../characters/entities/character.entity';
|
|
import { LocationMonster } from '../monsters/entities/location-monster.entity';
|
|
import { TravelService } from '../travel/travel.service';
|
|
import { LocationConnection } from './entities/location-connection.entity';
|
|
|
|
export interface LocationSummary {
|
|
id: string;
|
|
key: string;
|
|
name: string;
|
|
}
|
|
|
|
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[];
|
|
possibleMonsters: string[];
|
|
}
|
|
|
|
@Injectable()
|
|
export class WorldService {
|
|
constructor(
|
|
private readonly travelService: TravelService,
|
|
@InjectRepository(Character)
|
|
private readonly characters: Repository<Character>,
|
|
@InjectRepository(LocationConnection)
|
|
private readonly connections: Repository<LocationConnection>,
|
|
@InjectRepository(LocationMonster)
|
|
private readonly locationMonsters: Repository<LocationMonster>,
|
|
) {}
|
|
|
|
async getCurrentLocation(
|
|
characterId: string,
|
|
): Promise<CurrentLocationResponse> {
|
|
await this.travelService.completeTravelIfDue(characterId);
|
|
|
|
const character = await this.characters.findOne({
|
|
where: { id: characterId },
|
|
relations: { currentLocation: true },
|
|
});
|
|
if (!character) {
|
|
throw new NotFoundException('Character not found.');
|
|
}
|
|
|
|
const connections = await this.connections.find({
|
|
where: { fromLocationId: character.currentLocationId, enabled: true },
|
|
relations: { toLocation: true },
|
|
});
|
|
const location = character.currentLocation;
|
|
|
|
const possibleMonsters = location.huntingEnabled
|
|
? await this.getPossibleMonsters(location.id)
|
|
: [];
|
|
|
|
return {
|
|
id: location.id,
|
|
key: location.key,
|
|
name: location.name,
|
|
description: location.description,
|
|
regionKey: location.regionKey,
|
|
minRecommendedLevel: location.minRecommendedLevel,
|
|
maxRecommendedLevel: location.maxRecommendedLevel,
|
|
dangerLevel: location.dangerLevel,
|
|
isSafe: location.isSafe,
|
|
huntingEnabled: location.huntingEnabled,
|
|
artworkPath: location.artworkPath,
|
|
connections: connections
|
|
.filter((connection) => connection.enabled)
|
|
.map((connection) => ({
|
|
targetLocation: {
|
|
id: connection.toLocation.id,
|
|
key: connection.toLocation.key,
|
|
name: connection.toLocation.name,
|
|
},
|
|
travelDurationSeconds: connection.travelDurationSeconds,
|
|
danger: this.toDangerRating(connection.ambushChance),
|
|
})),
|
|
possibleMonsters,
|
|
};
|
|
}
|
|
|
|
private async getPossibleMonsters(locationId: string): Promise<string[]> {
|
|
const pool = await this.locationMonsters.find({
|
|
where: { locationId, enabled: true },
|
|
relations: { monster: true },
|
|
order: { weight: 'DESC' },
|
|
});
|
|
return pool.map((entry) => entry.monster.name);
|
|
}
|
|
|
|
private toDangerRating(ambushChance: string): 'LOW' | 'HIGH' {
|
|
return Number(ambushChance) <= 0.05 ? 'LOW' : 'HIGH';
|
|
}
|
|
}
|