piclets / src /lib /components /Pages /Encounters.svelte
Fraser's picture
rm db monsters
23a36f6
<script lang="ts">
import { onMount } from 'svelte';
import { fade, fly } from 'svelte/transition';
import type { Encounter, GameState, PicletInstance } from '$lib/db/schema';
import { EncounterType } from '$lib/db/schema';
import { EncounterService } from '$lib/db/encounterService';
import { getOrCreateGameState, incrementCounter, addProgressPoints } from '$lib/db/gameState';
import { db } from '$lib/db';
import { uiStore } from '$lib/stores/ui';
import Battle from './Battle.svelte';
import PullToRefresh from '../UI/PullToRefresh.svelte';
let encounters: Encounter[] = [];
let isLoading = true;
let isRefreshing = false;
let monsterImages: Map<string, string> = new Map();
// Battle state
let showBattle = false;
let battlePlayerPiclet: PicletInstance | null = null;
let battleEnemyPiclet: PicletInstance | null = null;
let battleIsWild = true;
let battleRosterPiclets: PicletInstance[] = [];
onMount(async () => {
await loadEncounters();
});
async function loadEncounters() {
isLoading = true;
try {
// Check if we have any piclet instances
const playerPiclets = await db.picletInstances.toArray();
if (playerPiclets.length === 0) {
// No piclets discovered/caught - show empty state
encounters = [];
isLoading = false;
return;
}
// Player has piclets - always generate fresh encounters with wild piclets
console.log('Player has piclets - generating fresh encounters with wild piclets');
await EncounterService.forceEncounterRefresh();
encounters = await EncounterService.generateEncounters();
console.log('Final encounters:', encounters.map(e => ({ type: e.type, title: e.title })));
// Load piclet images for wild encounters
await loadPicletImages();
} catch (error) {
console.error('Error loading encounters:', error);
}
isLoading = false;
}
async function loadPicletImages() {
const wildEncounters = encounters.filter(e =>
e.type === EncounterType.WILD_PICLET && e.picletTypeId
);
for (const encounter of wildEncounters) {
if (!encounter.picletTypeId) continue;
// Find a piclet instance with this typeId
const piclet = await db.picletInstances
.where('typeId')
.equals(encounter.picletTypeId)
.first();
if (piclet && piclet.imageData) {
monsterImages.set(encounter.picletTypeId, piclet.imageData);
}
}
// Trigger reactive update
monsterImages = monsterImages;
}
async function handleRefresh() {
isRefreshing = true;
try {
// Force refresh encounters
console.log('Force refreshing encounters...');
encounters = await EncounterService.generateEncounters();
// Load piclet images for new encounters
await loadPicletImages();
// Update game state with new refresh time
const gameState = await getOrCreateGameState();
await db.gameState.update(gameState.id!, {
lastEncounterRefresh: new Date()
});
} catch (error) {
console.error('Error refreshing encounters:', error);
}
isRefreshing = false;
}
async function handleEncounterTap(encounter: Encounter) {
if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
if (encounter.title === 'Your First Piclet!') {
// First catch - auto catch without battle
try {
isLoading = true;
const caughtPiclet = await EncounterService.catchWildPiclet(encounter);
await incrementCounter('picletsCapured');
await addProgressPoints(100);
// Show success message
alert(`You caught ${caughtPiclet.nickname}!`);
// Force refresh encounters
await forceEncounterRefresh();
} catch (error) {
console.error('Error catching piclet:', error);
}
isLoading = false;
} else {
// Regular wild encounter - start battle
await startBattle(encounter);
}
} else if (encounter.type === EncounterType.SHOP) {
await handleShopEncounter();
} else if (encounter.type === EncounterType.HEALTH_CENTER) {
await handleHealthCenterEncounter();
} else if (encounter.type === EncounterType.TRAINER_BATTLE) {
alert('Trainer battles coming soon!');
}
}
async function handleShopEncounter() {
alert('Shop features coming soon!');
await forceEncounterRefresh();
}
async function handleHealthCenterEncounter() {
try {
// Heal all piclets
const piclets = await db.picletInstances.toArray();
for (const piclet of piclets) {
await db.picletInstances.update(piclet.id!, {
currentHp: piclet.maxHp
});
}
alert('All your piclets have been healed to full health!');
await forceEncounterRefresh();
} catch (error) {
console.error('Error at health center:', error);
}
}
async function forceEncounterRefresh() {
isRefreshing = true;
try {
await EncounterService.forceEncounterRefresh();
encounters = await EncounterService.generateEncounters();
await loadPicletImages();
} catch (error) {
console.error('Error refreshing encounters:', error);
}
isRefreshing = false;
}
function getEncounterIcon(encounter: Encounter): string {
switch (encounter.type) {
case EncounterType.SHOP:
return '🛍️';
case EncounterType.HEALTH_CENTER:
return '❤️';
case EncounterType.TRAINER_BATTLE:
return '🏆';
case EncounterType.WILD_PICLET:
default:
return '⚔️';
}
}
function getEncounterColor(encounter: Encounter): string {
switch (encounter.type) {
case EncounterType.WILD_PICLET:
return '#4caf50';
case EncounterType.TRAINER_BATTLE:
return '#ff9800';
case EncounterType.SHOP:
return '#2196f3';
case EncounterType.HEALTH_CENTER:
return '#9c27b0';
default:
return '#607d8b';
}
}
async function startBattle(encounter: Encounter) {
try {
// Get all piclet instances
const allPiclets = await db.picletInstances.toArray();
// Filter piclets that have a roster position (0-5)
const rosterPiclets = allPiclets.filter(p =>
p.rosterPosition !== undefined &&
p.rosterPosition !== null &&
p.rosterPosition >= 0 &&
p.rosterPosition <= 5
);
// Sort by roster position
rosterPiclets.sort((a, b) => (a.rosterPosition ?? 0) - (b.rosterPosition ?? 0));
// Get healthy piclets
const healthyPiclets = rosterPiclets.filter(p => p.currentHp > 0);
if (healthyPiclets.length === 0) {
alert('You need at least one healthy piclet in your roster to battle!');
return;
}
// Check if there's at least one piclet in position 0
const hasPosition0 = rosterPiclets.some(p => p.rosterPosition === 0);
if (!hasPosition0) {
alert('You need a piclet in the first roster slot (position 0) to battle!');
return;
}
// Generate enemy piclet for battle
const enemyPiclet = await generateEnemyPiclet(encounter);
if (!enemyPiclet) return;
// Set up battle
battlePlayerPiclet = healthyPiclets[0];
battleEnemyPiclet = enemyPiclet;
battleIsWild = true;
battleRosterPiclets = rosterPiclets; // Pass all roster piclets
showBattle = true;
uiStore.enterBattle();
} catch (error) {
console.error('Error starting battle:', error);
}
}
async function generateEnemyPiclet(encounter: Encounter): Promise<PicletInstance | null> {
if (!encounter.picletTypeId || !encounter.enemyLevel) return null;
// Get a piclet instance with this typeId to use as a template
const templatePiclet = await db.picletInstances
.where('typeId')
.equals(encounter.picletTypeId)
.first();
if (!templatePiclet) {
console.error('Piclet template not found for typeId:', encounter.picletTypeId);
return null;
}
// Calculate stats based on template's base stats and encounter level
const level = encounter.enemyLevel;
// Calculate current 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 maxHp = calculateHp(templatePiclet.baseHp, level);
// Create enemy piclet instance based on template
const enemyPiclet: PicletInstance = {
...templatePiclet,
id: -1, // Temporary ID for enemy
level: level,
xp: 0,
currentHp: maxHp,
maxHp: maxHp,
attack: calculateStat(templatePiclet.baseAttack, level),
defense: calculateStat(templatePiclet.baseDefense, level),
fieldAttack: calculateStat(templatePiclet.baseFieldAttack, level),
fieldDefense: calculateStat(templatePiclet.baseFieldDefense, level),
speed: calculateStat(templatePiclet.baseSpeed, level),
// Reset move PP to full
moves: templatePiclet.moves.map(move => ({
...move,
currentPp: move.pp
})),
isInRoster: false,
caughtAt: new Date()
};
return enemyPiclet;
}
function handleBattleEnd(result: any) {
showBattle = false;
uiStore.exitBattle();
if (result === true) {
// Victory
console.log('Battle won!');
} else if (result === false) {
// Defeat or ran away
console.log('Battle lost or fled');
} else if (result && result.id) {
// Caught a piclet
console.log('Piclet caught!', result);
incrementCounter('picletsCapured');
addProgressPoints(100);
}
// Force refresh encounters after battle
forceEncounterRefresh();
}
</script>
{#if showBattle && battlePlayerPiclet && battleEnemyPiclet}
<Battle
playerPiclet={battlePlayerPiclet}
enemyPiclet={battleEnemyPiclet}
isWildBattle={battleIsWild}
rosterPiclets={battleRosterPiclets}
onBattleEnd={handleBattleEnd}
/>
{:else}
<div class="encounters-page">
<PullToRefresh onRefresh={handleRefresh}>
{#if isLoading}
<div class="loading">
<div class="spinner"></div>
<p>Loading encounters...</p>
</div>
{:else if encounters.length === 0}
<div class="empty-state">
<div class="empty-icon">📸</div>
<h2>No Piclets Discovered</h2>
<p>To start your adventure, select the Snap logo image:</p>
<div class="logo-instruction">
<img src="/assets/snap_logo.png" alt="Snap Logo" class="snap-logo-preview" />
<p class="instruction-text">↑ Select this image in the scanner</p>
</div>
</div>
{:else}
<div class="encounters-list">
{#each encounters as encounter, index (encounter.id)}
<button
class="encounter-card"
style="border-color: {getEncounterColor(encounter)}30"
on:click={() => handleEncounterTap(encounter)}
in:fly={{ y: 20, delay: index * 50 }}
disabled={isRefreshing}
>
<div class="encounter-icon">
{#if encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId}
{#if encounter.title === 'Your First Piclet!'}
<div class="piclet-silhouette">
{#if monsterImages.has(encounter.picletTypeId)}
<img
src={monsterImages.get(encounter.picletTypeId)}
alt="Mystery Piclet"
class="silhouette-img"
/>
{:else}
<div class="silhouette-fallback">?</div>
{/if}
</div>
{:else}
{#if monsterImages.has(encounter.picletTypeId)}
<img
src={monsterImages.get(encounter.picletTypeId)}
alt="Wild Piclet"
/>
{:else}
<div class="fallback-icon">{getEncounterIcon(encounter)}</div>
{/if}
{/if}
{:else}
<span class="type-icon">{getEncounterIcon(encounter)}</span>
{/if}
</div>
<div class="encounter-info">
<h3>{encounter.title}</h3>
<p>{encounter.description}</p>
</div>
<div class="encounter-arrow"></div>
</button>
{/each}
</div>
{/if}
</PullToRefresh>
</div>
{/if}
<style>
.encounters-page {
height: 100%;
overflow: hidden; /* PullToRefresh handles scrolling */
}
.loading, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
text-align: center;
padding: 1rem;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #f0f0f0;
border-top-color: #4caf50;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state h2 {
margin: 0 0 0.5rem;
font-size: 1.25rem;
color: #333;
}
.empty-state p {
color: #666;
font-size: 0.9rem;
}
.encounters-list {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
padding-bottom: 5rem;
}
.encounter-card {
display: flex;
align-items: center;
gap: 1rem;
background: #fff;
border: 2px solid;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: all 0.2s ease;
cursor: pointer;
width: 100%;
text-align: left;
}
.encounter-card:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.encounter-card:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.encounter-icon {
width: 60px;
height: 60px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.encounter-icon img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.piclet-silhouette {
width: 100%;
height: 100%;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.silhouette-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(100%) brightness(0.3) contrast(0.5);
opacity: 0.8;
}
.silhouette-fallback {
font-size: 2rem;
font-weight: bold;
color: #999;
background: #e0e0e0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.type-icon, .fallback-icon {
font-size: 2rem;
}
.encounter-info {
flex: 1;
}
.encounter-info h3 {
margin: 0 0 0.25rem;
font-size: 1.1rem;
font-weight: 600;
color: #1a1a1a;
}
.encounter-info p {
margin: 0;
font-size: 0.875rem;
color: #666;
}
.encounter-arrow {
font-size: 1.5rem;
color: #999;
}
.logo-instruction {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.snap-logo-preview {
width: 120px;
height: 120px;
object-fit: contain;
border: 2px dashed #007bff;
border-radius: 12px;
padding: 1rem;
background: #f0f7ff;
}
.instruction-text {
font-size: 0.875rem;
color: #007bff;
font-weight: 500;
margin: 0;
}
</style>