piclets / src /lib /db /encounterService.ts
Fraser's picture
fix encounters
11f6bcf
raw
history blame
11 kB
import { db } from './index';
import type { Encounter, PicletInstance } from './schema';
import { EncounterType } from './schema';
import { getOrCreateGameState, markEncountersRefreshed } from './gameState';
import { getCaughtPiclets, getUncaughtPiclets } from './piclets';
import { calculateStat, calculateHp } from '../services/levelingService';
// 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
if (uncaughtPiclets.length > 0) {
// Player has scanned piclets but hasn't caught any - create first piclet encounter
console.log('Player has scanned piclets but no caught ones - creating first piclet encounter');
const firstPicletEncounter = await this.createFirstCatchEncounter(uncaughtPiclets);
encounters.push(firstPicletEncounter);
} else {
// Player has no piclets at all - return empty encounters
console.log('Player has no piclets at all - returning empty encounters');
await db.encounters.clear();
await markEncountersRefreshed();
return [];
}
// Save the first piclet encounter and return
await db.encounters.clear();
for (const encounter of encounters) {
await db.encounters.add(encounter);
}
await markEncountersRefreshed();
return await this.getCurrentEncounters();
}
// 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(uncaughtPiclets: PicletInstance[]): Promise<Omit<Encounter, 'id'>> {
// Use the most recently scanned (last in array) uncaught piclet
const latestPiclet = uncaughtPiclets[uncaughtPiclets.length - 1];
// Use the piclet's nickname or typeId for display
const displayName = latestPiclet.nickname || latestPiclet.typeId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
return {
type: EncounterType.FIRST_PICLET,
title: 'Your First Piclet!',
description: `A friendly ${displayName} appears! This one seems easy to catch.`,
picletInstanceId: latestPiclet.id, // Reference to the specific piclet instance
picletTypeId: latestPiclet.typeId,
enemyLevel: 5, // Easy level for first encounter
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 uncaught piclets (these can appear as wild encounters to be caught)
const uncaughtPiclets = await getUncaughtPiclets();
console.log('Uncaught piclets for wild encounters:', uncaughtPiclets.length);
if (uncaughtPiclets.length === 0) {
console.log('No uncaught piclets - returning empty wild encounters');
return encounters;
}
// Use uncaught piclets as templates for wild encounters
const availablePiclets = uncaughtPiclets;
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 (marks uncaught piclet as caught, or creates new instance for recatches)
static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
if (!encounter.picletTypeId) throw new Error('No piclet type specified');
// First check if this is an uncaught piclet that can be directly marked as caught
const uncaughtPiclets = await getUncaughtPiclets();
const uncaughtPiclet = uncaughtPiclets.find(p => p.typeId === encounter.picletTypeId);
if (uncaughtPiclet) {
// This is the first time catching this type - mark the existing uncaught piclet as caught
const newLevel = encounter.enemyLevel || uncaughtPiclet.level;
// Update the existing uncaught piclet
const updates = {
caught: true,
caughtAt: new Date(),
level: newLevel,
xp: 0,
currentHp: calculateHp(uncaughtPiclet.baseHp, newLevel),
maxHp: calculateHp(uncaughtPiclet.baseHp, newLevel),
attack: calculateStat(uncaughtPiclet.baseAttack, newLevel),
defense: calculateStat(uncaughtPiclet.baseDefense, newLevel),
fieldAttack: calculateStat(uncaughtPiclet.baseFieldAttack, newLevel),
fieldDefense: calculateStat(uncaughtPiclet.baseFieldDefense, newLevel),
speed: calculateStat(uncaughtPiclet.baseSpeed, newLevel),
// Reset move PP to full
moves: uncaughtPiclet.moves.map(move => ({
...move,
currentPp: move.pp
}))
};
// Set roster position 0 if this is the first caught piclet
const existingCaughtPiclets = await getCaughtPiclets();
if (existingCaughtPiclets.length === 0) {
Object.assign(updates, {
rosterPosition: 0,
isInRoster: true
});
}
// Update the existing piclet
await db.picletInstances.update(uncaughtPiclet.id!, updates);
// Return the updated piclet
return { ...uncaughtPiclet, ...updates };
}
// If no uncaught piclet found, this is a recatch - create a new instance using caught piclet as 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 for recatch
const newLevel = encounter.enemyLevel || 5;
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 new catch
isInRoster: false,
rosterPosition: undefined,
caught: true,
caughtAt: new Date()
};
// Save the new piclet
const id = await db.picletInstances.add(newPiclet);
return { ...newPiclet, id };
}
}