File size: 7,748 Bytes
465b043 7428b13 465b043 7428b13 465b043 7428b13 90a88fc 7428b13 465b043 7428b13 465b043 7428b13 465b043 90a88fc 465b043 90a88fc 465b043 90a88fc 465b043 90a88fc 465b043 90a88fc 465b043 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
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.name.toLowerCase().replace(/\s+/g, '-'),
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();
// Get all discovered monsters
const discoveredMonsters = await db.monsters.toArray();
if (discoveredMonsters.length === 0) return encounters;
// Get player's caught piclets to potentially exclude them
const caughtPiclets = await db.picletInstances.toArray();
const caughtTypeIds = new Set(caughtPiclets.map(p => p.typeId));
// Filter for uncaught monsters (optional - you might want to allow catching duplicates)
const availableMonsters = discoveredMonsters; // Or filter: .filter(m => !caughtTypeIds.has(m.name.toLowerCase().replace(/\s+/g, '-')))
const encounterCount = MIN_WILD_ENCOUNTERS + Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1));
for (let i = 0; i < encounterCount; i++) {
// Pick a random monster from discovered ones
const monster = availableMonsters[Math.floor(Math.random() * availableMonsters.length)];
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 ${monster.name} Appeared!`,
description: `A level ${enemyLevel} ${monster.name} blocks your path!`,
picletTypeId: monster.name.toLowerCase().replace(/\s+/g, '-'),
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> {
if (!encounter.picletTypeId) throw new Error('No piclet type specified');
// Import the conversion function
const { monsterToPicletInstance } = await import('./piclets');
// Get the monster data by name (typeId is the lowercase hyphenated name)
const monsterName = encounter.picletTypeId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const monster = await db.monsters
.where('name')
.equals(monsterName)
.first();
if (!monster) {
throw new Error(`Monster not found: ${monsterName}`);
}
// Convert monster to piclet instance
const picletData = await monsterToPicletInstance(monster, encounter.enemyLevel || 5);
// Set roster position 0 if this is the first piclet
const existingPiclets = await db.picletInstances.toArray();
if (existingPiclets.length === 0) {
picletData.rosterPosition = 0;
}
// Save the new piclet
const id = await db.picletInstances.add(picletData);
return { ...picletData, id };
}
} |