import { db } from './index'; import type { Encounter, PicletInstance } from './schema'; import { EncounterType } from './schema'; import { getOrCreateGameState, markEncountersRefreshed } from './gameState'; import { getCaughtPiclets, getUncaughtPiclets } from './piclets'; // 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 for "Your First Piclet" scenario first const caughtPiclets = await getCaughtPiclets(); const uncaughtPiclets = await getUncaughtPiclets(); if (caughtPiclets.length === 0) { // Player has no caught piclets - return empty encounters (no shop/heal until first piclet is caught) console.log('Player has no caught piclets - returning empty encounters'); await db.encounters.clear(); await markEncountersRefreshed(); return []; } // Player has caught piclets - generate normal encounters console.log('Generating normal encounters for player with caught 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 caught piclets only (these can appear as wild encounters) const caughtPiclets = await getCaughtPiclets(); console.log('Caught piclets for wild encounters:', caughtPiclets.length); if (caughtPiclets.length === 0) { console.log('No caught piclets - returning empty wild encounters'); return encounters; } // Use caught piclets as templates for wild encounters const availablePiclets = caughtPiclets; 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 caughtPiclets = await getCaughtPiclets(); if (caughtPiclets.length === 0) return 5; // Default starting level const totalLevel = caughtPiclets.reduce((sum, p) => sum + p.level, 0); return Math.round(totalLevel / caughtPiclets.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 a caught piclet instance with this typeId to use as a template const caughtPiclets = await getCaughtPiclets(); const templatePiclet = caughtPiclets.find(p => p.typeId === encounter.picletTypeId); 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() }; // Wild piclets are always caught newPiclet.caught = true; // Set roster position 0 if this is the first caught piclet const existingCaughtPiclets = await getCaughtPiclets(); if (existingCaughtPiclets.length === 1) { // Only one caught piclet exists (the template) newPiclet.rosterPosition = 0; newPiclet.isInRoster = true; } // Save the new piclet const id = await db.picletInstances.add(newPiclet); return { ...newPiclet, id }; } }