piclets / src /lib /db /encounterService.ts
Fraser's picture
MORE
7428b13
raw
history blame
8.06 kB
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<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 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<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();
// 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<number> {
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<PicletInstance> {
// 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<PicletInstance, 'id'> = {
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 };
}
}