piclets / src /lib /db /encounterService.ts
Fraser's picture
btl
90a88fc
raw
history blame
7.75 kB
import { db } from './index';
import type { Encounter, PicletInstance } from './schema';
import { EncounterType } from './schema';
import { getOrCreateGameState, markEncountersRefreshed } from './gameState';
// Configuration
const ENCOUNTER_REFRESH_HOURS = 2;
const MIN_WILD_ENCOUNTERS = 2;
const MAX_WILD_ENCOUNTERS = 3;
const LEVEL_VARIANCE = 2;
export class EncounterService {
// Check if encounters should be refreshed
static async shouldRefreshEncounters(): Promise<boolean> {
const state = await getOrCreateGameState();
const hoursSinceRefresh = (Date.now() - state.lastEncounterRefresh.getTime()) / (1000 * 60 * 60);
return hoursSinceRefresh >= ENCOUNTER_REFRESH_HOURS;
}
// Force encounter refresh
static async forceEncounterRefresh(): Promise<void> {
await db.encounters.clear();
await markEncountersRefreshed();
}
// Get current encounters
static async getCurrentEncounters(): Promise<Encounter[]> {
return await db.encounters
.orderBy('createdAt')
.reverse()
.toArray();
}
// Clear all encounters
static async clearEncounters(): Promise<void> {
await db.encounters.clear();
}
// Generate new encounters
static async generateEncounters(): Promise<Encounter[]> {
const encounters: Omit<Encounter, 'id'>[] = [];
// Check if player has caught any piclets first
const playerPiclets = await db.picletInstances.toArray();
if (playerPiclets.length === 0) {
// No piclets caught yet - check for discovered piclets
// For now, we'll check the monsters collection as a proxy for discovered piclets
// In a real app, this would check a remote database for piclets discovered by scanning
const discoveredPiclets = await db.monsters.toArray();
if (discoveredPiclets.length === 0) {
// No piclets discovered yet - return empty encounters
await db.encounters.clear();
await markEncountersRefreshed();
return [];
}
// Player has discovered but not caught any piclets - show ONLY first catch encounter
const firstDiscovered = discoveredPiclets[0];
encounters.push({
type: EncounterType.WILD_PICLET,
title: 'Your First Piclet!',
description: 'A friendly piclet appears! This one seems easy to catch.',
picletTypeId: firstDiscovered.name.toLowerCase().replace(/\s+/g, '-'),
enemyLevel: 5,
createdAt: new Date()
});
// IMPORTANT: Return here - don't add shop/health center for first catch
await db.encounters.clear();
await db.encounters.add(encounters[0]);
await markEncountersRefreshed();
return await this.getCurrentEncounters();
}
// Player has piclets - generate normal encounters
// Always add shop and health center first
encounters.push({
type: EncounterType.SHOP,
title: 'Piclet Shop',
description: 'Buy items and supplies for your journey',
createdAt: new Date()
});
encounters.push({
type: EncounterType.HEALTH_CENTER,
title: 'Health Center',
description: 'Heal your piclets back to full health',
createdAt: new Date()
});
// Generate wild piclet encounters
const wildEncounters = await this.generateWildEncounters();
encounters.push(...wildEncounters);
// Clear existing encounters and add new ones
await db.encounters.clear();
for (const encounter of encounters) {
await db.encounters.add(encounter);
}
await markEncountersRefreshed();
return await this.getCurrentEncounters();
}
// Create first catch encounter
private static async createFirstCatchEncounter(): Promise<Omit<Encounter, 'id'>> {
// TODO: Replace with actual piclet data when available
// For now, using placeholder data
return {
type: EncounterType.WILD_PICLET,
title: 'Your First Piclet!',
description: 'A friendly piclet appears! This one seems easy to catch.',
picletTypeId: 'starter-001', // Placeholder ID
enemyLevel: 5,
createdAt: new Date()
};
}
// Generate wild piclet encounters
private static async generateWildEncounters(): Promise<Omit<Encounter, 'id'>[]> {
const encounters: Omit<Encounter, 'id'>[] = [];
// Get player's average level
const avgLevel = await this.getPlayerAverageLevel();
// Get all discovered monsters
const discoveredMonsters = await db.monsters.toArray();
if (discoveredMonsters.length === 0) return encounters;
// Get player's caught piclets to potentially exclude them
const caughtPiclets = await db.picletInstances.toArray();
const caughtTypeIds = new Set(caughtPiclets.map(p => p.typeId));
// Filter for uncaught monsters (optional - you might want to allow catching duplicates)
const availableMonsters = discoveredMonsters; // Or filter: .filter(m => !caughtTypeIds.has(m.name.toLowerCase().replace(/\s+/g, '-')))
const encounterCount = MIN_WILD_ENCOUNTERS + Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1));
for (let i = 0; i < encounterCount; i++) {
// Pick a random monster from discovered ones
const monster = availableMonsters[Math.floor(Math.random() * availableMonsters.length)];
const levelVariance = Math.floor(Math.random() * (LEVEL_VARIANCE * 2 + 1)) - LEVEL_VARIANCE;
const enemyLevel = Math.max(1, avgLevel + levelVariance);
encounters.push({
type: EncounterType.WILD_PICLET,
title: `Wild ${monster.name} Appeared!`,
description: `A level ${enemyLevel} ${monster.name} blocks your path!`,
picletTypeId: monster.name.toLowerCase().replace(/\s+/g, '-'),
enemyLevel,
createdAt: new Date()
});
}
return encounters;
}
// Get player's average piclet level
private static async getPlayerAverageLevel(): Promise<number> {
const rosterPiclets = await db.picletInstances
.where('isInRoster')
.equals(1) // Dexie uses 1 for true in indexed fields
.toArray();
if (rosterPiclets.length === 0) {
const allPiclets = await db.picletInstances.toArray();
if (allPiclets.length === 0) return 5; // Default starting level
const totalLevel = allPiclets.reduce((sum, p) => sum + p.level, 0);
return Math.round(totalLevel / allPiclets.length);
}
const totalLevel = rosterPiclets.reduce((sum, p) => sum + p.level, 0);
return Math.round(totalLevel / rosterPiclets.length);
}
// Catch a wild piclet (for first encounter)
static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
if (!encounter.picletTypeId) throw new Error('No piclet type specified');
// Import the conversion function
const { monsterToPicletInstance } = await import('./piclets');
// Get the monster data by name (typeId is the lowercase hyphenated name)
const monsterName = encounter.picletTypeId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const monster = await db.monsters
.where('name')
.equals(monsterName)
.first();
if (!monster) {
throw new Error(`Monster not found: ${monsterName}`);
}
// Convert monster to piclet instance
const picletData = await monsterToPicletInstance(monster, encounter.enemyLevel || 5);
// Set roster position 0 if this is the first piclet
const existingPiclets = await db.picletInstances.toArray();
if (existingPiclets.length === 0) {
picletData.rosterPosition = 0;
}
// Save the new piclet
const id = await db.picletInstances.add(picletData);
return { ...picletData, id };
}
}