piclets / src /lib /db /encounterService.ts
Fraser's picture
better first enc
7ea1165
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<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 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<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 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<number> {
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<PicletInstance> {
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<PicletInstance, 'id'> = {
...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 };
}
}