|
import { db } from './index'; |
|
import type { Encounter, PicletInstance } from './schema'; |
|
import { EncounterType } from './schema'; |
|
import { getOrCreateGameState, markEncountersRefreshed } from './gameState'; |
|
|
|
|
|
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 playerPiclets = await db.picletInstances.toArray(); |
|
|
|
if (playerPiclets.length === 0) { |
|
|
|
|
|
|
|
const discoveredPiclets = await db.monsters.toArray(); |
|
|
|
if (discoveredPiclets.length === 0) { |
|
|
|
await db.encounters.clear(); |
|
await markEncountersRefreshed(); |
|
return []; |
|
} |
|
|
|
|
|
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() |
|
}); |
|
|
|
|
|
await db.encounters.clear(); |
|
await db.encounters.add(encounters[0]); |
|
await markEncountersRefreshed(); |
|
return await this.getCurrentEncounters(); |
|
} |
|
|
|
|
|
|
|
|
|
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() |
|
}); |
|
|
|
|
|
const wildEncounters = await this.generateWildEncounters(); |
|
encounters.push(...wildEncounters); |
|
|
|
|
|
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 discoveredMonsters = await db.monsters.toArray(); |
|
if (discoveredMonsters.length === 0) return encounters; |
|
|
|
|
|
const caughtPiclets = await db.picletInstances.toArray(); |
|
const caughtTypeIds = new Set(caughtPiclets.map(p => p.typeId)); |
|
|
|
|
|
const availableMonsters = discoveredMonsters; |
|
|
|
const encounterCount = MIN_WILD_ENCOUNTERS + Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1)); |
|
|
|
for (let i = 0; i < encounterCount; i++) { |
|
|
|
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; |
|
} |
|
|
|
|
|
private static async getPlayerAverageLevel(): Promise<number> { |
|
const rosterPiclets = await db.picletInstances |
|
.where('isInRoster') |
|
.equals(1) |
|
.toArray(); |
|
|
|
if (rosterPiclets.length === 0) { |
|
const allPiclets = await db.picletInstances.toArray(); |
|
if (allPiclets.length === 0) return 5; |
|
|
|
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); |
|
} |
|
|
|
|
|
static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> { |
|
if (!encounter.picletTypeId) throw new Error('No piclet type specified'); |
|
|
|
|
|
const { monsterToPicletInstance } = await import('./piclets'); |
|
|
|
|
|
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}`); |
|
} |
|
|
|
|
|
const picletData = await monsterToPicletInstance(monster, encounter.enemyLevel || 5); |
|
|
|
|
|
const existingPiclets = await db.picletInstances.toArray(); |
|
if (existingPiclets.length === 0) { |
|
picletData.rosterPosition = 0; |
|
} |
|
|
|
|
|
const id = await db.picletInstances.add(picletData); |
|
return { ...picletData, id }; |
|
} |
|
} |