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 { 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 { await db.encounters.clear(); await markEncountersRefreshed(); } // Get current encounters static async getCurrentEncounters(): Promise { return await db.encounters .orderBy('createdAt') .reverse() .toArray(); } // Clear all encounters static async clearEncounters(): Promise { await db.encounters.clear(); } // Generate new encounters static async generateEncounters(): Promise { const encounters: Omit[] = []; // Check if player has caught any piclets first const playerPiclets = await db.picletInstances.toArray(); if (playerPiclets.length === 0) { // No piclets caught yet - this shouldn't happen in normal flow since we use picletInstances // But just in case, return empty encounters console.log('No piclet instances found - returning empty encounters'); await db.encounters.clear(); await markEncountersRefreshed(); return []; } // Player has piclets - generate normal encounters console.log('Generating encounters for player with piclets'); // Generate wild piclet encounters FIRST to ensure they're included const wildEncounters = await this.generateWildEncounters(); console.log('Wild encounters generated:', wildEncounters.length); encounters.push(...wildEncounters); // Always add shop and health center 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() }); // 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> { // 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[]> { const encounters: Omit[] = []; // Get player's average level const avgLevel = await this.getPlayerAverageLevel(); // Get all piclet instances (these represent "discovered" piclets) const allPiclets = await db.picletInstances.toArray(); console.log('Total piclet instances for wild encounters:', allPiclets.length); if (allPiclets.length === 0) { console.log('No piclet instances - returning empty wild encounters'); return encounters; } // Get unique piclet types for encounters (allow duplicates) const availablePiclets = allPiclets; console.log('Available piclets for encounters:', availablePiclets.map(p => p.typeId)); const encounterCount = MIN_WILD_ENCOUNTERS + Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1)); console.log('Generating', encounterCount, 'wild encounters'); for (let i = 0; i < encounterCount; i++) { // Pick a random piclet from available ones const piclet = availablePiclets[Math.floor(Math.random() * availablePiclets.length)]; const levelVariance = Math.floor(Math.random() * (LEVEL_VARIANCE * 2 + 1)) - LEVEL_VARIANCE; const enemyLevel = Math.max(1, avgLevel + levelVariance); // Use the piclet's nickname or typeId for display const displayName = piclet.nickname || piclet.typeId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); const wildEncounter = { type: EncounterType.WILD_PICLET, title: `Wild ${displayName} Appeared!`, description: `A level ${enemyLevel} ${displayName} blocks your path!`, picletTypeId: piclet.typeId, enemyLevel, createdAt: new Date() }; console.log('Created wild encounter:', wildEncounter.title, 'with typeId:', wildEncounter.picletTypeId); encounters.push(wildEncounter); } console.log('Generated', encounters.length, 'wild encounters'); return encounters; } // Get player's average piclet level private static async getPlayerAverageLevel(): Promise { 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 (creates a new instance based on existing piclet type) static async catchWildPiclet(encounter: Encounter): Promise { if (!encounter.picletTypeId) throw new Error('No piclet type specified'); // Find an existing piclet instance with this typeId to use as a template const templatePiclet = await db.picletInstances .where('typeId') .equals(encounter.picletTypeId) .first(); if (!templatePiclet) { throw new Error(`Piclet type not found: ${encounter.picletTypeId}`); } // Create a new piclet instance based on the template but with different stats/level const newLevel = encounter.enemyLevel || 5; // Calculate new stats based on level (using template's base stats) const calculateStat = (base: number, level: number) => Math.floor((base * level) / 50 + 5); const calculateHp = (base: number, level: number) => Math.floor((base * level) / 50 + level + 10); const newPiclet: Omit = { ...templatePiclet, level: newLevel, xp: 0, currentHp: calculateHp(templatePiclet.baseHp, newLevel), maxHp: calculateHp(templatePiclet.baseHp, newLevel), attack: calculateStat(templatePiclet.baseAttack, newLevel), defense: calculateStat(templatePiclet.baseDefense, newLevel), fieldAttack: calculateStat(templatePiclet.baseFieldAttack, newLevel), fieldDefense: calculateStat(templatePiclet.baseFieldDefense, newLevel), speed: calculateStat(templatePiclet.baseSpeed, newLevel), // Reset move PP to full moves: templatePiclet.moves.map(move => ({ ...move, currentPp: move.pp })), // Clear roster info for wild catch isInRoster: false, rosterPosition: undefined, caughtAt: new Date() }; // Set roster position 0 if this is the first piclet const existingPiclets = await db.picletInstances.toArray(); if (existingPiclets.length === 1) { // Only the template exists newPiclet.rosterPosition = 0; newPiclet.isInRoster = true; } // Save the new piclet const id = await db.picletInstances.add(newPiclet); return { ...newPiclet, id }; } }