|
import { db } from './index'; |
|
import type { Encounter, PicletInstance } from './schema'; |
|
import { EncounterType } from './schema'; |
|
import { getOrCreateGameState, markEncountersRefreshed } from './gameState'; |
|
import { getCaughtPiclets, getUncaughtPiclets } from './piclets'; |
|
|
|
|
|
const ENCOUNTER_REFRESH_HOURS = 2; |
|
const MIN_WILD_ENCOUNTERS = 2; |
|
const MAX_WILD_ENCOUNTERS = 3; |
|
const LEVEL_VARIANCE = 2; |
|
|
|
export class EncounterService { |
|
|
|
static async shouldRefreshEncounters(): Promise<boolean> { |
|
const state = await getOrCreateGameState(); |
|
const hoursSinceRefresh = (Date.now() - state.lastEncounterRefresh.getTime()) / (1000 * 60 * 60); |
|
return hoursSinceRefresh >= ENCOUNTER_REFRESH_HOURS; |
|
} |
|
|
|
|
|
static async forceEncounterRefresh(): Promise<void> { |
|
await db.encounters.clear(); |
|
await markEncountersRefreshed(); |
|
} |
|
|
|
|
|
static async getCurrentEncounters(): Promise<Encounter[]> { |
|
return await db.encounters |
|
.orderBy('createdAt') |
|
.reverse() |
|
.toArray(); |
|
} |
|
|
|
|
|
static async clearEncounters(): Promise<void> { |
|
await db.encounters.clear(); |
|
} |
|
|
|
|
|
|
|
static async generateEncounters(): Promise<Encounter[]> { |
|
const encounters: Omit<Encounter, 'id'>[] = []; |
|
|
|
|
|
const caughtPiclets = await getCaughtPiclets(); |
|
const uncaughtPiclets = await getUncaughtPiclets(); |
|
|
|
if (caughtPiclets.length === 0) { |
|
|
|
console.log('Player has no caught piclets - returning empty encounters'); |
|
await db.encounters.clear(); |
|
await markEncountersRefreshed(); |
|
return []; |
|
} |
|
|
|
|
|
console.log('Generating normal encounters for player with caught piclets'); |
|
|
|
|
|
const wildEncounters = await this.generateWildEncounters(); |
|
console.log('Wild encounters generated:', wildEncounters.length); |
|
encounters.push(...wildEncounters); |
|
|
|
|
|
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() |
|
}); |
|
|
|
|
|
await db.encounters.clear(); |
|
for (const encounter of encounters) { |
|
await db.encounters.add(encounter); |
|
} |
|
|
|
await markEncountersRefreshed(); |
|
return await this.getCurrentEncounters(); |
|
} |
|
|
|
|
|
private static async createFirstCatchEncounter(): Promise<Omit<Encounter, 'id'>> { |
|
|
|
|
|
return { |
|
type: EncounterType.WILD_PICLET, |
|
title: 'Your First Piclet!', |
|
description: 'A friendly piclet appears! This one seems easy to catch.', |
|
picletTypeId: 'starter-001', |
|
enemyLevel: 5, |
|
createdAt: new Date() |
|
}; |
|
} |
|
|
|
|
|
private static async generateWildEncounters(): Promise<Omit<Encounter, 'id'>[]> { |
|
const encounters: Omit<Encounter, 'id'>[] = []; |
|
|
|
|
|
const avgLevel = await this.getPlayerAverageLevel(); |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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++) { |
|
|
|
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); |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
private static async getPlayerAverageLevel(): Promise<number> { |
|
const rosterPiclets = await db.picletInstances |
|
.where('isInRoster') |
|
.equals(1) |
|
.toArray(); |
|
|
|
if (rosterPiclets.length === 0) { |
|
const caughtPiclets = await getCaughtPiclets(); |
|
if (caughtPiclets.length === 0) return 5; |
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> { |
|
if (!encounter.picletTypeId) throw new Error('No piclet type specified'); |
|
|
|
|
|
const caughtPiclets = await getCaughtPiclets(); |
|
const templatePiclet = caughtPiclets.find(p => p.typeId === encounter.picletTypeId); |
|
|
|
if (!templatePiclet) { |
|
throw new Error(`Piclet type not found: ${encounter.picletTypeId}`); |
|
} |
|
|
|
|
|
const newLevel = encounter.enemyLevel || 5; |
|
|
|
|
|
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), |
|
|
|
|
|
moves: templatePiclet.moves.map(move => ({ |
|
...move, |
|
currentPp: move.pp |
|
})), |
|
|
|
|
|
isInRoster: false, |
|
rosterPosition: undefined, |
|
caughtAt: new Date() |
|
}; |
|
|
|
|
|
newPiclet.caught = true; |
|
|
|
|
|
const existingCaughtPiclets = await getCaughtPiclets(); |
|
if (existingCaughtPiclets.length === 1) { |
|
newPiclet.rosterPosition = 0; |
|
newPiclet.isInRoster = true; |
|
} |
|
|
|
|
|
const id = await db.picletInstances.add(newPiclet); |
|
return { ...newPiclet, id }; |
|
} |
|
} |