File size: 8,655 Bytes
465b043 5fe1a3d 465b043 5fe1a3d 465b043 99ecf7d 7428b13 7ea1165 7428b13 23a36f6 465b043 7428b13 99ecf7d 7428b13 a21678d 465b043 99ecf7d 90a88fc 99ecf7d a21678d 90a88fc 99ecf7d 23a36f6 90a88fc 465b043 a21678d 465b043 23a36f6 90a88fc 465b043 23a36f6 a21678d 465b043 23a36f6 465b043 a21678d 465b043 a21678d 465b043 99ecf7d 465b043 99ecf7d 465b043 5fe1a3d 23a36f6 465b043 90a88fc 99ecf7d 90a88fc 23a36f6 90a88fc 23a36f6 90a88fc 99ecf7d 23a36f6 90a88fc 465b043 90a88fc 23a36f6 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 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
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 };
}
} |