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 - 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.id?.toString() || 'starter-001', 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> { // 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(); // TODO: When piclet types are available, filter for uncaught piclets // For now, generate placeholder encounters const encounterCount = MIN_WILD_ENCOUNTERS + Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1)); for (let i = 0; i < encounterCount; i++) { 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 Piclet Appeared!`, description: `A level ${enemyLevel} piclet blocks your path!`, picletTypeId: `wild-${i + 1}`, // Placeholder ID enemyLevel, createdAt: new Date() }); } 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 (for first encounter) static async catchWildPiclet(encounter: Encounter): Promise { // Get the discovered piclet data const discoveredPiclets = await db.monsters.toArray(); const picletData = discoveredPiclets.find(p => p.id?.toString() === encounter.picletTypeId) || discoveredPiclets[0]; const newPiclet: Omit = { typeId: encounter.picletTypeId!, nickname: picletData?.name || 'Starter Piclet', primaryTypeString: 'normal', // Stats based on level level: encounter.enemyLevel || 5, xp: 0, currentHp: 20 + (encounter.enemyLevel || 5) * 4, maxHp: 20 + (encounter.enemyLevel || 5) * 4, attack: 10 + (encounter.enemyLevel || 5) * 2, defense: 10 + (encounter.enemyLevel || 5) * 2, fieldAttack: 10 + (encounter.enemyLevel || 5) * 2, fieldDefense: 10 + (encounter.enemyLevel || 5) * 2, speed: 10 + (encounter.enemyLevel || 5) * 2, // Base stats baseHp: 20, baseAttack: 10, baseDefense: 10, baseFieldAttack: 10, baseFieldDefense: 10, baseSpeed: 10, // Battle moves moves: [ { name: 'Tackle', type: 'normal', power: 40, accuracy: 100, pp: 35, currentPp: 35, description: 'A physical attack' } ], nature: 'hardy', // Roster isInRoster: true, rosterPosition: 0, // Metadata caughtAt: new Date(), bst: 60, tier: 'common', role: 'balanced', variance: 1, // Visuals from discovered data imageUrl: picletData?.imageUrl || `https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`, imageData: picletData?.imageData, imageCaption: picletData?.imageCaption || 'A friendly starter piclet', concept: picletData?.concept || 'starter', imagePrompt: picletData?.imagePrompt || 'cute starter monster' }; const id = await db.picletInstances.add(newPiclet); return { ...newPiclet, id }; } }