Fraser commited on
Commit
5fe1a3d
·
1 Parent(s): 74f8c2c

first encounter

Browse files
src/lib/components/Pages/Encounters.svelte CHANGED
@@ -9,6 +9,7 @@
9
  import { uiStore } from '$lib/stores/ui';
10
  import Battle from './Battle.svelte';
11
  import PullToRefresh from '../UI/PullToRefresh.svelte';
 
12
 
13
  let encounters: Encounter[] = [];
14
  let isLoading = true;
@@ -22,6 +23,10 @@
22
  let battleIsWild = true;
23
  let battleRosterPiclets: PicletInstance[] = [];
24
 
 
 
 
 
25
  onMount(async () => {
26
  await loadEncounters();
27
  });
@@ -98,28 +103,28 @@
98
  }
99
 
100
  async function handleEncounterTap(encounter: Encounter) {
101
- if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
102
- if (encounter.title === 'Your First Piclet!') {
103
- // First catch - auto catch without battle
104
- try {
105
- isLoading = true;
106
- const caughtPiclet = await EncounterService.catchWildPiclet(encounter);
107
- await incrementCounter('picletsCapured');
108
- await addProgressPoints(100);
109
-
110
- // Show success message
111
- alert(`You caught ${caughtPiclet.nickname}!`);
112
-
113
- // Force refresh encounters
114
- await forceEncounterRefresh();
115
- } catch (error) {
116
- console.error('Error catching piclet:', error);
117
- }
118
- isLoading = false;
119
- } else {
120
- // Regular wild encounter - start battle
121
- await startBattle(encounter);
122
  }
 
 
 
 
123
  } else if (encounter.type === EncounterType.SHOP) {
124
  await handleShopEncounter();
125
  } else if (encounter.type === EncounterType.HEALTH_CENTER) {
@@ -349,28 +354,19 @@
349
  disabled={isRefreshing}
350
  >
351
  <div class="encounter-icon">
352
- {#if encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId}
353
- {#if encounter.title === 'Your First Piclet!'}
354
- <div class="piclet-silhouette">
355
- {#if monsterImages.has(encounter.picletTypeId)}
356
- <img
357
- src={monsterImages.get(encounter.picletTypeId)}
358
- alt="Mystery Piclet"
359
- class="silhouette-img"
360
- />
361
- {:else}
362
- <div class="silhouette-fallback">?</div>
363
- {/if}
364
- </div>
365
  {:else}
366
- {#if monsterImages.has(encounter.picletTypeId)}
367
- <img
368
- src={monsterImages.get(encounter.picletTypeId)}
369
- alt="Wild Piclet"
370
- />
371
- {:else}
372
- <div class="fallback-icon">{getEncounterIcon(encounter)}</div>
373
- {/if}
374
  {/if}
375
  {:else}
376
  <span class="type-icon">{getEncounterIcon(encounter)}</span>
@@ -391,6 +387,17 @@
391
  </div>
392
  {/if}
393
 
 
 
 
 
 
 
 
 
 
 
 
394
  <style>
395
  .encounters-page {
396
  height: 100%;
@@ -567,4 +574,40 @@
567
  font-weight: 500;
568
  margin: 0;
569
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  </style>
 
9
  import { uiStore } from '$lib/stores/ui';
10
  import Battle from './Battle.svelte';
11
  import PullToRefresh from '../UI/PullToRefresh.svelte';
12
+ import NewlyCaughtPicletDetail from '../Piclets/NewlyCaughtPicletDetail.svelte';
13
 
14
  let encounters: Encounter[] = [];
15
  let isLoading = true;
 
23
  let battleIsWild = true;
24
  let battleRosterPiclets: PicletInstance[] = [];
25
 
26
+ // Newly caught Piclet state
27
+ let showNewlyCaught = false;
28
+ let newlyCaughtPiclet: PicletInstance | null = null;
29
+
30
  onMount(async () => {
31
  await loadEncounters();
32
  });
 
103
  }
104
 
105
  async function handleEncounterTap(encounter: Encounter) {
106
+ if (encounter.type === EncounterType.FIRST_PICLET) {
107
+ // Your First Piclet - auto catch and show special detail page
108
+ try {
109
+ isLoading = true;
110
+ const caughtPiclet = await EncounterService.catchFirstPiclet(encounter);
111
+ await incrementCounter('picletsCapured');
112
+ await addProgressPoints(100);
113
+
114
+ // Show the special newly caught detail page
115
+ newlyCaughtPiclet = caughtPiclet;
116
+ showNewlyCaught = true;
117
+
118
+ // Force refresh encounters to remove the first piclet encounter
119
+ await forceEncounterRefresh();
120
+ } catch (error) {
121
+ console.error('Error catching first piclet:', error);
122
+ alert(`Error catching your first Piclet: ${error.message}`);
 
 
 
 
123
  }
124
+ isLoading = false;
125
+ } else if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
126
+ // Regular wild encounter - start battle
127
+ await startBattle(encounter);
128
  } else if (encounter.type === EncounterType.SHOP) {
129
  await handleShopEncounter();
130
  } else if (encounter.type === EncounterType.HEALTH_CENTER) {
 
354
  disabled={isRefreshing}
355
  >
356
  <div class="encounter-icon">
357
+ {#if encounter.type === EncounterType.FIRST_PICLET}
358
+ <div class="first-piclet-icon">
359
+ <div class="golden-glow-effect">✨</div>
360
+ <div class="mystery-silhouette">?</div>
361
+ </div>
362
+ {:else if encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId}
363
+ {#if monsterImages.has(encounter.picletTypeId)}
364
+ <img
365
+ src={monsterImages.get(encounter.picletTypeId)}
366
+ alt="Wild Piclet"
367
+ />
 
 
368
  {:else}
369
+ <div class="fallback-icon">{getEncounterIcon(encounter)}</div>
 
 
 
 
 
 
 
370
  {/if}
371
  {:else}
372
  <span class="type-icon">{getEncounterIcon(encounter)}</span>
 
387
  </div>
388
  {/if}
389
 
390
+ <!-- Newly Caught Piclet Dialog -->
391
+ {#if showNewlyCaught && newlyCaughtPiclet}
392
+ <NewlyCaughtPicletDetail
393
+ instance={newlyCaughtPiclet}
394
+ onClose={() => {
395
+ showNewlyCaught = false;
396
+ newlyCaughtPiclet = null;
397
+ }}
398
+ />
399
+ {/if}
400
+
401
  <style>
402
  .encounters-page {
403
  height: 100%;
 
574
  font-weight: 500;
575
  margin: 0;
576
  }
577
+
578
+ /* First Piclet encounter styles */
579
+ .first-piclet-icon {
580
+ position: relative;
581
+ width: 64px;
582
+ height: 64px;
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ background: linear-gradient(135deg, #FFD700, #FFA500);
587
+ border-radius: 16px;
588
+ overflow: hidden;
589
+ }
590
+
591
+ .golden-glow-effect {
592
+ position: absolute;
593
+ top: 0;
594
+ left: 0;
595
+ font-size: 1.5rem;
596
+ animation: sparkleRotate 2s ease-in-out infinite;
597
+ }
598
+
599
+ .mystery-silhouette {
600
+ font-size: 2rem;
601
+ font-weight: bold;
602
+ color: white;
603
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
604
+ z-index: 2;
605
+ }
606
+
607
+ @keyframes sparkleRotate {
608
+ 0%, 100% { transform: rotate(0deg) scale(1); opacity: 0.8; }
609
+ 25% { transform: rotate(-10deg) scale(1.1); opacity: 1; }
610
+ 50% { transform: rotate(0deg) scale(1.2); opacity: 0.9; }
611
+ 75% { transform: rotate(10deg) scale(1.1); opacity: 1; }
612
+ }
613
  </style>
src/lib/components/Pages/Pictuary.svelte CHANGED
@@ -1,7 +1,7 @@
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
  import { getAllMonsters } from '$lib/db/monsters';
4
- import { getAllPicletInstances, getRosterPiclets, moveToRoster, swapRosterPositions, moveToStorage } from '$lib/db/piclets';
5
  import type { Monster, PicletInstance } from '$lib/db/schema';
6
  import PicletCard from '../Piclets/PicletCard.svelte';
7
  import EmptySlotCard from '../Piclets/EmptySlotCard.svelte';
@@ -68,7 +68,7 @@
68
  try {
69
  // Run type migration first time to fix any invalid types
70
 
71
- const allInstances = await getAllPicletInstances();
72
 
73
  // Filter based on rosterPosition instead of isInRoster
74
  rosterPiclets = allInstances.filter(p =>
 
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
  import { getAllMonsters } from '$lib/db/monsters';
4
+ import { getCaughtPiclets, getRosterPiclets, moveToRoster, swapRosterPositions, moveToStorage } from '$lib/db/piclets';
5
  import type { Monster, PicletInstance } from '$lib/db/schema';
6
  import PicletCard from '../Piclets/PicletCard.svelte';
7
  import EmptySlotCard from '../Piclets/EmptySlotCard.svelte';
 
68
  try {
69
  // Run type migration first time to fix any invalid types
70
 
71
+ const allInstances = await getCaughtPiclets();
72
 
73
  // Filter based on rosterPosition instead of isInRoster
74
  rosterPiclets = allInstances.filter(p =>
src/lib/components/PicletGenerator/PicletGenerator.svelte CHANGED
@@ -9,6 +9,7 @@
9
  import { extractPicletMetadata } from '$lib/services/picletMetadata';
10
  import { savePicletInstance, monsterToPicletInstance } from '$lib/db/piclets';
11
  import { PicletType, TYPE_DATA } from '$lib/types/picletTypes';
 
12
 
13
  interface Props extends PicletGeneratorProps {}
14
 
@@ -103,7 +104,7 @@ Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit
103
  // Step 5: Generate monster image
104
  await generateMonsterImage();
105
 
106
- // Step 6: Auto-save the piclet
107
  await autoSavePiclet();
108
 
109
  state.currentStep = 'complete';
@@ -893,7 +894,14 @@ Write your response within \`\`\`json\`\`\``;
893
  // Convert to PicletInstance format and save
894
  const picletInstance = await monsterToPicletInstance(picletData);
895
  const picletId = await savePicletInstance(picletInstance);
896
- console.log('Piclet auto-saved with ID:', picletId);
 
 
 
 
 
 
 
897
  } catch (err) {
898
  console.error('Failed to auto-save piclet:', err);
899
  console.error('Piclet data that failed to save:', {
 
9
  import { extractPicletMetadata } from '$lib/services/picletMetadata';
10
  import { savePicletInstance, monsterToPicletInstance } from '$lib/db/piclets';
11
  import { PicletType, TYPE_DATA } from '$lib/types/picletTypes';
12
+ import { EncounterService } from '$lib/db/encounterService';
13
 
14
  interface Props extends PicletGeneratorProps {}
15
 
 
104
  // Step 5: Generate monster image
105
  await generateMonsterImage();
106
 
107
+ // Step 6: Auto-save the piclet as uncaught
108
  await autoSavePiclet();
109
 
110
  state.currentStep = 'complete';
 
894
  // Convert to PicletInstance format and save
895
  const picletInstance = await monsterToPicletInstance(picletData);
896
  const picletId = await savePicletInstance(picletInstance);
897
+ console.log('Piclet auto-saved as uncaught with ID:', picletId);
898
+
899
+ // Check if this should create a "Your First Piclet" encounter
900
+ const shouldCreateFirstEncounter = await EncounterService.shouldCreateFirstPicletEncounter();
901
+ if (shouldCreateFirstEncounter === picletId) {
902
+ console.log('Creating first Piclet encounter for ID:', picletId);
903
+ await EncounterService.createFirstPicletEncounter(picletId);
904
+ }
905
  } catch (err) {
906
  console.error('Failed to auto-save piclet:', err);
907
  console.error('Piclet data that failed to save:', {
src/lib/components/Piclets/NewlyCaughtPicletDetail.svelte ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { PicletInstance } from '$lib/db/schema';
4
+ import { TYPE_DATA } from '$lib/types/picletTypes';
5
+ import AbilityDisplay from './AbilityDisplay.svelte';
6
+ import MoveDisplay from './MoveDisplay.svelte';
7
+ import { picletInstanceToBattleDefinition } from '$lib/utils/battleConversion';
8
+ import { recalculatePicletStats, getXpProgress, getXpTowardsNextLevel } from '$lib/services/levelingService';
9
+ import { isSpecialAbilityUnlocked } from '$lib/services/unlockLevels';
10
+
11
+ interface Props {
12
+ instance: PicletInstance;
13
+ onClose: () => void;
14
+ }
15
+
16
+ let { instance, onClose }: Props = $props();
17
+ let selectedTab = $state<'about' | 'abilities'>('about');
18
+ let showCelebration = $state(true);
19
+
20
+ // Ensure stats are up-to-date with current level and nature
21
+ const updatedInstance = $derived(recalculatePicletStats(instance));
22
+
23
+ // Convert to battle definition to get enhanced ability data
24
+ const battleDefinition = $derived(picletInstanceToBattleDefinition(updatedInstance));
25
+
26
+ // XP and level calculations
27
+ const xpProgress = $derived(getXpProgress(updatedInstance.level, updatedInstance.xp));
28
+ const xpToNext = $derived(getXpTowardsNextLevel(updatedInstance.level, updatedInstance.xp));
29
+ const isMaxLevel = $derived(updatedInstance.level >= 100);
30
+
31
+ // Calculate BST and handle type data
32
+ const typeData = $derived(TYPE_DATA[updatedInstance.primaryType]);
33
+ const tier = $derived(() => {
34
+ const bst = updatedInstance.bst;
35
+ if (bst >= 600) return { name: 'Legendary', color: '#FFD700', bg: 'linear-gradient(135deg, #FFD700, #FFA500)' };
36
+ if (bst >= 530) return { name: 'Elite', color: '#9932CC', bg: 'linear-gradient(135deg, #9932CC, #8A2BE2)' };
37
+ if (bst >= 480) return { name: 'Advanced', color: '#1E90FF', bg: 'linear-gradient(135deg, #1E90FF, #0066CC)' };
38
+ if (bst >= 420) return { name: 'Standard', color: '#32CD32', bg: 'linear-gradient(135deg, #32CD32, #228B22)' };
39
+ return { name: 'Basic', color: '#808080', bg: 'linear-gradient(135deg, #808080, #696969)' };
40
+ });
41
+
42
+ let celebrationTimeout: NodeJS.Timeout;
43
+
44
+ onMount(() => {
45
+ // Auto-hide celebration after 3 seconds
46
+ celebrationTimeout = setTimeout(() => {
47
+ showCelebration = false;
48
+ }, 3000);
49
+
50
+ return () => {
51
+ if (celebrationTimeout) clearTimeout(celebrationTimeout);
52
+ };
53
+ });
54
+
55
+ function dismissCelebration() {
56
+ showCelebration = false;
57
+ if (celebrationTimeout) clearTimeout(celebrationTimeout);
58
+ }
59
+ </script>
60
+
61
+ <div class="detail-overlay" on:click={onClose}>
62
+ <div class="detail-container newly-caught" on:click|stopPropagation>
63
+
64
+ <!-- Celebration overlay -->
65
+ {#if showCelebration}
66
+ <div class="celebration-overlay" on:click={dismissCelebration}>
67
+ <div class="celebration-content">
68
+ <div class="celebration-sparkles">✨</div>
69
+ <h1 class="celebration-title">Welcome to your team!</h1>
70
+ <div class="celebration-piclet-name">{updatedInstance.nickname}</div>
71
+ <p class="celebration-subtitle">Your first Piclet has been caught!</p>
72
+ <div class="celebration-sparkles">🎉</div>
73
+ <div class="tap-to-continue">Tap to continue</div>
74
+ </div>
75
+ </div>
76
+ {/if}
77
+
78
+ <!-- Header with special "newly caught" styling -->
79
+ <div class="detail-header" style="background: {tier().bg}">
80
+ <button class="close-button" on:click={onClose}>×</button>
81
+ <div class="newly-caught-badge">
82
+ <span class="badge-text">✨ NEWLY CAUGHT ✨</span>
83
+ </div>
84
+ <div class="header-content">
85
+ <div class="piclet-image-container">
86
+ <img
87
+ src={updatedInstance.imageUrl}
88
+ alt={updatedInstance.nickname}
89
+ class="piclet-image"
90
+ />
91
+ <div class="golden-glow"></div>
92
+ </div>
93
+ <div class="piclet-info">
94
+ <h1 class="piclet-name">{updatedInstance.nickname}</h1>
95
+ <div class="piclet-meta">
96
+ <div class="type-badge" style="background: {typeData.color}">
97
+ {typeData.icon} {updatedInstance.primaryType}
98
+ </div>
99
+ <div class="tier-badge" style="color: {tier().color}">
100
+ {tier().name}
101
+ </div>
102
+ <div class="level-badge">Lv. {updatedInstance.level}</div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Navigation tabs -->
109
+ <div class="tab-navigation">
110
+ <button
111
+ class="tab-button"
112
+ class:active={selectedTab === 'about'}
113
+ on:click={() => selectedTab = 'about'}
114
+ >
115
+ About
116
+ </button>
117
+ <button
118
+ class="tab-button"
119
+ class:active={selectedTab === 'abilities'}
120
+ on:click={() => selectedTab = 'abilities'}
121
+ >
122
+ Abilities
123
+ </button>
124
+ </div>
125
+
126
+ <!-- Tab content -->
127
+ <div class="detail-content">
128
+ {#if selectedTab === 'about'}
129
+ <div class="about-tab">
130
+ <div class="description-section">
131
+ <h3>Description</h3>
132
+ <p class="description-text">{updatedInstance.description}</p>
133
+ </div>
134
+
135
+ <div class="stats-section">
136
+ <h3>Combat Stats</h3>
137
+ <div class="stats-grid">
138
+ <div class="stat-item">
139
+ <span class="stat-label">HP</span>
140
+ <span class="stat-value">{updatedInstance.maxHp}</span>
141
+ </div>
142
+ <div class="stat-item">
143
+ <span class="stat-label">Attack</span>
144
+ <span class="stat-value">{updatedInstance.attack}</span>
145
+ </div>
146
+ <div class="stat-item">
147
+ <span class="stat-label">Defense</span>
148
+ <span class="stat-value">{updatedInstance.defense}</span>
149
+ </div>
150
+ <div class="stat-item">
151
+ <span class="stat-label">Speed</span>
152
+ <span class="stat-value">{updatedInstance.speed}</span>
153
+ </div>
154
+ </div>
155
+ <div class="bst-display">
156
+ <span class="bst-label">Base Stat Total:</span>
157
+ <span class="bst-value" style="color: {tier().color}">{updatedInstance.bst}</span>
158
+ </div>
159
+ </div>
160
+
161
+ <div class="xp-section">
162
+ <h3>Experience</h3>
163
+ <div class="level-xp-section">
164
+ <div class="xp-bar-container">
165
+ <div class="xp-bar">
166
+ <div class="xp-fill" style="width: {xpProgress}%"></div>
167
+ </div>
168
+ <div class="xp-text">
169
+ {#if isMaxLevel}
170
+ <span>MAX LEVEL</span>
171
+ {:else}
172
+ <span>{xpToNext} XP to level {updatedInstance.level + 1}</span>
173
+ {/if}
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ {:else if selectedTab === 'abilities'}
180
+ <div class="abilities-tab">
181
+ <div class="special-ability-section">
182
+ <h3>Special Ability</h3>
183
+ {#if isSpecialAbilityUnlocked(updatedInstance)}
184
+ <AbilityDisplay ability={battleDefinition.specialAbility} />
185
+ {:else}
186
+ <div class="locked-ability">
187
+ <span class="lock-icon">🔒</span>
188
+ <span class="locked-text">Unlocks at level {updatedInstance.specialAbilityUnlockLevel}</span>
189
+ </div>
190
+ {/if}
191
+ </div>
192
+
193
+ <div class="moves-section">
194
+ <h3>Moves</h3>
195
+ <div class="moves-grid">
196
+ {#each updatedInstance.moves as move}
197
+ <MoveDisplay {move} picletLevel={updatedInstance.level} />
198
+ {/each}
199
+ </div>
200
+ </div>
201
+ </div>
202
+ {/if}
203
+ </div>
204
+ </div>
205
+ </div>
206
+
207
+ <style>
208
+ .detail-overlay {
209
+ position: fixed;
210
+ inset: 0;
211
+ background: rgba(0, 0, 0, 0.8);
212
+ backdrop-filter: blur(4px);
213
+ z-index: 2000;
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ padding: 1rem;
218
+ animation: fadeIn 0.3s ease-out;
219
+ }
220
+
221
+ .detail-container.newly-caught {
222
+ background: white;
223
+ border-radius: 24px;
224
+ max-width: 500px;
225
+ width: 100%;
226
+ max-height: 90vh;
227
+ overflow: hidden;
228
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
229
+ animation: slideInUp 0.4s ease-out;
230
+ position: relative;
231
+ }
232
+
233
+ /* Celebration overlay */
234
+ .celebration-overlay {
235
+ position: absolute;
236
+ inset: 0;
237
+ background: radial-gradient(circle, rgba(255, 215, 0, 0.95) 0%, rgba(255, 140, 0, 0.9) 100%);
238
+ z-index: 100;
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ cursor: pointer;
243
+ animation: celebrationPulse 2s ease-in-out infinite;
244
+ }
245
+
246
+ .celebration-content {
247
+ text-align: center;
248
+ color: white;
249
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
250
+ }
251
+
252
+ .celebration-sparkles {
253
+ font-size: 3rem;
254
+ animation: sparkle 1.5s ease-in-out infinite;
255
+ margin: 0.5rem 0;
256
+ }
257
+
258
+ .celebration-title {
259
+ font-size: 2.5rem;
260
+ font-weight: 800;
261
+ margin: 1rem 0 0.5rem;
262
+ animation: titleGlow 2s ease-in-out infinite;
263
+ }
264
+
265
+ .celebration-piclet-name {
266
+ font-size: 2rem;
267
+ font-weight: 700;
268
+ margin: 0.5rem 0;
269
+ text-transform: uppercase;
270
+ letter-spacing: 2px;
271
+ }
272
+
273
+ .celebration-subtitle {
274
+ font-size: 1.2rem;
275
+ margin: 1rem 0;
276
+ opacity: 0.9;
277
+ }
278
+
279
+ .tap-to-continue {
280
+ font-size: 1rem;
281
+ margin-top: 2rem;
282
+ opacity: 0.8;
283
+ animation: pulse 1.5s ease-in-out infinite;
284
+ }
285
+
286
+ /* Header styling */
287
+ .detail-header {
288
+ position: relative;
289
+ padding: 2rem 1.5rem 1.5rem;
290
+ color: white;
291
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
292
+ }
293
+
294
+ .newly-caught-badge {
295
+ position: absolute;
296
+ top: 1rem;
297
+ left: 50%;
298
+ transform: translateX(-50%);
299
+ background: rgba(255, 255, 255, 0.2);
300
+ backdrop-filter: blur(10px);
301
+ border: 2px solid rgba(255, 255, 255, 0.3);
302
+ border-radius: 20px;
303
+ padding: 0.5rem 1rem;
304
+ animation: badgeGlow 2s ease-in-out infinite;
305
+ }
306
+
307
+ .badge-text {
308
+ font-weight: 700;
309
+ font-size: 0.9rem;
310
+ letter-spacing: 1px;
311
+ }
312
+
313
+ .close-button {
314
+ position: absolute;
315
+ top: 1rem;
316
+ right: 1rem;
317
+ background: rgba(255, 255, 255, 0.2);
318
+ border: none;
319
+ border-radius: 50%;
320
+ width: 40px;
321
+ height: 40px;
322
+ color: white;
323
+ font-size: 1.5rem;
324
+ cursor: pointer;
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ transition: all 0.2s ease;
329
+ z-index: 10;
330
+ }
331
+
332
+ .close-button:hover {
333
+ background: rgba(255, 255, 255, 0.3);
334
+ transform: scale(1.1);
335
+ }
336
+
337
+ .header-content {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 1.5rem;
341
+ margin-top: 2rem;
342
+ }
343
+
344
+ .piclet-image-container {
345
+ position: relative;
346
+ flex-shrink: 0;
347
+ }
348
+
349
+ .piclet-image {
350
+ width: 100px;
351
+ height: 100px;
352
+ border-radius: 16px;
353
+ object-fit: cover;
354
+ border: 3px solid rgba(255, 255, 255, 0.3);
355
+ position: relative;
356
+ z-index: 2;
357
+ }
358
+
359
+ .golden-glow {
360
+ position: absolute;
361
+ inset: -10px;
362
+ background: radial-gradient(circle, rgba(255, 215, 0, 0.6), transparent 70%);
363
+ border-radius: 50%;
364
+ animation: goldenGlow 2s ease-in-out infinite;
365
+ z-index: 1;
366
+ }
367
+
368
+ .piclet-info {
369
+ flex: 1;
370
+ min-width: 0;
371
+ }
372
+
373
+ .piclet-name {
374
+ font-size: 1.8rem;
375
+ font-weight: 700;
376
+ margin: 0 0 0.5rem;
377
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
378
+ }
379
+
380
+ .piclet-meta {
381
+ display: flex;
382
+ flex-wrap: wrap;
383
+ gap: 0.5rem;
384
+ }
385
+
386
+ .type-badge, .tier-badge, .level-badge {
387
+ padding: 0.25rem 0.75rem;
388
+ border-radius: 20px;
389
+ font-size: 0.8rem;
390
+ font-weight: 600;
391
+ text-transform: uppercase;
392
+ letter-spacing: 0.5px;
393
+ }
394
+
395
+ .type-badge {
396
+ color: white;
397
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
398
+ }
399
+
400
+ .tier-badge {
401
+ background: rgba(255, 255, 255, 0.2);
402
+ backdrop-filter: blur(10px);
403
+ }
404
+
405
+ .level-badge {
406
+ background: rgba(255, 255, 255, 0.3);
407
+ color: white;
408
+ }
409
+
410
+ /* Tab navigation */
411
+ .tab-navigation {
412
+ display: flex;
413
+ border-bottom: 1px solid #e0e0e0;
414
+ background: #f8f9fa;
415
+ }
416
+
417
+ .tab-button {
418
+ flex: 1;
419
+ padding: 1rem;
420
+ border: none;
421
+ background: none;
422
+ font-weight: 600;
423
+ color: #666;
424
+ cursor: pointer;
425
+ transition: all 0.2s ease;
426
+ position: relative;
427
+ }
428
+
429
+ .tab-button.active {
430
+ color: #007bff;
431
+ }
432
+
433
+ .tab-button.active::after {
434
+ content: '';
435
+ position: absolute;
436
+ bottom: 0;
437
+ left: 0;
438
+ right: 0;
439
+ height: 3px;
440
+ background: #007bff;
441
+ }
442
+
443
+ /* Content styling */
444
+ .detail-content {
445
+ max-height: 60vh;
446
+ overflow-y: auto;
447
+ padding: 1.5rem;
448
+ }
449
+
450
+ .about-tab, .abilities-tab {
451
+ display: flex;
452
+ flex-direction: column;
453
+ gap: 1.5rem;
454
+ }
455
+
456
+ .description-section h3,
457
+ .stats-section h3,
458
+ .xp-section h3,
459
+ .special-ability-section h3,
460
+ .moves-section h3 {
461
+ margin: 0 0 1rem;
462
+ font-size: 1.2rem;
463
+ font-weight: 600;
464
+ color: #333;
465
+ }
466
+
467
+ .description-text {
468
+ color: #666;
469
+ line-height: 1.6;
470
+ margin: 0;
471
+ }
472
+
473
+ .stats-grid {
474
+ display: grid;
475
+ grid-template-columns: repeat(2, 1fr);
476
+ gap: 1rem;
477
+ margin-bottom: 1rem;
478
+ }
479
+
480
+ .stat-item {
481
+ display: flex;
482
+ justify-content: space-between;
483
+ align-items: center;
484
+ padding: 0.75rem;
485
+ background: #f8f9fa;
486
+ border-radius: 8px;
487
+ }
488
+
489
+ .stat-label {
490
+ font-weight: 600;
491
+ color: #666;
492
+ }
493
+
494
+ .stat-value {
495
+ font-weight: 700;
496
+ color: #333;
497
+ font-size: 1.1rem;
498
+ }
499
+
500
+ .bst-display {
501
+ display: flex;
502
+ justify-content: space-between;
503
+ align-items: center;
504
+ padding: 1rem;
505
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
506
+ border-radius: 12px;
507
+ border: 2px solid #dee2e6;
508
+ }
509
+
510
+ .bst-label {
511
+ font-weight: 600;
512
+ color: #666;
513
+ }
514
+
515
+ .bst-value {
516
+ font-weight: 700;
517
+ font-size: 1.3rem;
518
+ }
519
+
520
+ .level-xp-section {
521
+ display: flex;
522
+ flex-direction: column;
523
+ gap: 0.5rem;
524
+ }
525
+
526
+ .xp-bar-container {
527
+ display: flex;
528
+ flex-direction: column;
529
+ gap: 0.5rem;
530
+ }
531
+
532
+ .xp-bar {
533
+ height: 8px;
534
+ background: #e9ecef;
535
+ border-radius: 4px;
536
+ overflow: hidden;
537
+ }
538
+
539
+ .xp-fill {
540
+ height: 100%;
541
+ background: linear-gradient(90deg, #28a745, #20c997);
542
+ transition: width 0.3s ease;
543
+ }
544
+
545
+ .xp-text {
546
+ text-align: center;
547
+ font-size: 0.9rem;
548
+ color: #666;
549
+ }
550
+
551
+ .locked-ability {
552
+ display: flex;
553
+ align-items: center;
554
+ gap: 0.5rem;
555
+ padding: 1rem;
556
+ background: #f8f9fa;
557
+ border: 2px dashed #dee2e6;
558
+ border-radius: 12px;
559
+ color: #666;
560
+ font-style: italic;
561
+ }
562
+
563
+ .lock-icon {
564
+ font-size: 1.2rem;
565
+ }
566
+
567
+ .moves-grid {
568
+ display: flex;
569
+ flex-direction: column;
570
+ gap: 0.75rem;
571
+ }
572
+
573
+ /* Animations */
574
+ @keyframes fadeIn {
575
+ from { opacity: 0; }
576
+ to { opacity: 1; }
577
+ }
578
+
579
+ @keyframes slideInUp {
580
+ from {
581
+ opacity: 0;
582
+ transform: translateY(100px) scale(0.9);
583
+ }
584
+ to {
585
+ opacity: 1;
586
+ transform: translateY(0) scale(1);
587
+ }
588
+ }
589
+
590
+ @keyframes celebrationPulse {
591
+ 0%, 100% { transform: scale(1); }
592
+ 50% { transform: scale(1.02); }
593
+ }
594
+
595
+ @keyframes sparkle {
596
+ 0%, 100% { transform: rotate(0deg) scale(1); }
597
+ 25% { transform: rotate(-5deg) scale(1.1); }
598
+ 75% { transform: rotate(5deg) scale(1.1); }
599
+ }
600
+
601
+ @keyframes titleGlow {
602
+ 0%, 100% { text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); }
603
+ 50% { text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5), 0 0 20px rgba(255, 255, 255, 0.8); }
604
+ }
605
+
606
+ @keyframes pulse {
607
+ 0%, 100% { opacity: 0.8; }
608
+ 50% { opacity: 1; }
609
+ }
610
+
611
+ @keyframes badgeGlow {
612
+ 0%, 100% { box-shadow: 0 0 10px rgba(255, 255, 255, 0.3); }
613
+ 50% { box-shadow: 0 0 20px rgba(255, 255, 255, 0.6), 0 0 30px rgba(255, 255, 255, 0.4); }
614
+ }
615
+
616
+ @keyframes goldenGlow {
617
+ 0%, 100% { opacity: 0.6; }
618
+ 50% { opacity: 0.9; }
619
+ }
620
+
621
+ @media (max-width: 768px) {
622
+ .detail-container {
623
+ margin: 0.5rem;
624
+ max-height: 95vh;
625
+ }
626
+
627
+ .celebration-title {
628
+ font-size: 2rem;
629
+ }
630
+
631
+ .celebration-piclet-name {
632
+ font-size: 1.5rem;
633
+ }
634
+
635
+ .header-content {
636
+ flex-direction: column;
637
+ text-align: center;
638
+ gap: 1rem;
639
+ }
640
+
641
+ .stats-grid {
642
+ grid-template-columns: 1fr;
643
+ }
644
+ }
645
+ </style>
src/lib/db/encounterService.ts CHANGED
@@ -2,6 +2,7 @@ import { db } from './index';
2
  import type { Encounter, PicletInstance } from './schema';
3
  import { EncounterType } from './schema';
4
  import { getOrCreateGameState, markEncountersRefreshed } from './gameState';
 
5
 
6
  // Configuration
7
  const ENCOUNTER_REFRESH_HOURS = 2;
@@ -36,6 +37,42 @@ export class EncounterService {
36
  await db.encounters.clear();
37
  }
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  // Generate new encounters
40
  static async generateEncounters(): Promise<Encounter[]> {
41
  const encounters: Omit<Encounter, 'id'>[] = [];
@@ -168,6 +205,40 @@ export class EncounterService {
168
  return Math.round(totalLevel / rosterPiclets.length);
169
  }
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  // Catch a wild piclet (creates a new instance based on existing piclet type)
172
  static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
173
  if (!encounter.picletTypeId) throw new Error('No piclet type specified');
 
2
  import type { Encounter, PicletInstance } from './schema';
3
  import { EncounterType } from './schema';
4
  import { getOrCreateGameState, markEncountersRefreshed } from './gameState';
5
+ import { getCaughtPiclets, getUncaughtPiclets } from './piclets';
6
 
7
  // Configuration
8
  const ENCOUNTER_REFRESH_HOURS = 2;
 
37
  await db.encounters.clear();
38
  }
39
 
40
+ // Create a "Your First Piclet" encounter from an uncaught Piclet
41
+ static async createFirstPicletEncounter(picletId: number): Promise<Encounter> {
42
+ const piclet = await db.picletInstances.get(picletId);
43
+ if (!piclet) {
44
+ throw new Error(`Piclet with ID ${picletId} not found`);
45
+ }
46
+
47
+ const encounter: Omit<Encounter, 'id'> = {
48
+ type: EncounterType.FIRST_PICLET,
49
+ title: '✨ Your First Piclet! ✨',
50
+ description: `${piclet.nickname} wants to join your team! This special encounter will automatically catch them for you.`,
51
+ picletInstanceId: picletId,
52
+ createdAt: new Date()
53
+ };
54
+
55
+ const id = await db.encounters.add(encounter);
56
+ return { ...encounter, id };
57
+ }
58
+
59
+ // Check if the player should get a "Your First Piclet" encounter
60
+ static async shouldCreateFirstPicletEncounter(): Promise<number | null> {
61
+ const caughtPiclets = await getCaughtPiclets();
62
+ if (caughtPiclets.length > 0) {
63
+ return null; // Player already has caught Piclets
64
+ }
65
+
66
+ const uncaughtPiclets = await getUncaughtPiclets();
67
+ if (uncaughtPiclets.length === 0) {
68
+ return null; // No uncaught Piclets available
69
+ }
70
+
71
+ // Return the ID of the most recently created uncaught Piclet
72
+ const mostRecent = uncaughtPiclets.sort((a, b) => (b.id || 0) - (a.id || 0))[0];
73
+ return mostRecent.id || null;
74
+ }
75
+
76
  // Generate new encounters
77
  static async generateEncounters(): Promise<Encounter[]> {
78
  const encounters: Omit<Encounter, 'id'>[] = [];
 
205
  return Math.round(totalLevel / rosterPiclets.length);
206
  }
207
 
208
+ // Catch your first Piclet (marks an existing uncaught Piclet as caught)
209
+ static async catchFirstPiclet(encounter: Encounter): Promise<PicletInstance> {
210
+ if (!encounter.picletInstanceId) {
211
+ throw new Error('No piclet instance ID specified for first Piclet encounter');
212
+ }
213
+
214
+ const piclet = await db.picletInstances.get(encounter.picletInstanceId);
215
+ if (!piclet) {
216
+ throw new Error(`Piclet with ID ${encounter.picletInstanceId} not found`);
217
+ }
218
+
219
+ if (piclet.caught) {
220
+ throw new Error('This Piclet has already been caught');
221
+ }
222
+
223
+ // Mark the Piclet as caught and put it in roster position 0 (first Piclet)
224
+ const updatedPiclet: PicletInstance = {
225
+ ...piclet,
226
+ caught: true,
227
+ caughtAt: new Date(),
228
+ isInRoster: true,
229
+ rosterPosition: 0
230
+ };
231
+
232
+ await db.picletInstances.update(encounter.picletInstanceId, {
233
+ caught: true,
234
+ caughtAt: new Date(),
235
+ isInRoster: true,
236
+ rosterPosition: 0
237
+ });
238
+
239
+ return updatedPiclet;
240
+ }
241
+
242
  // Catch a wild piclet (creates a new instance based on existing piclet type)
243
  static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
244
  if (!encounter.picletTypeId) throw new Error('No piclet type specified');
src/lib/db/piclets.ts CHANGED
@@ -109,7 +109,8 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
109
  rosterPosition: undefined,
110
 
111
  // Metadata
112
- caughtAt: new Date(),
 
113
  bst,
114
  tier: stats.tier, // Use tier from stats
115
  role: 'balanced', // Could be enhanced based on stat distribution
@@ -130,6 +131,26 @@ export async function savePicletInstance(piclet: Omit<PicletInstance, 'id'>): Pr
130
  return await db.picletInstances.add(piclet);
131
  }
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  // Get all PicletInstances
134
  export async function getAllPicletInstances(): Promise<PicletInstance[]> {
135
  return await db.picletInstances.toArray();
@@ -140,6 +161,7 @@ export async function getRosterPiclets(): Promise<PicletInstance[]> {
140
  const allPiclets = await db.picletInstances.toArray();
141
  return allPiclets
142
  .filter(p =>
 
143
  p.rosterPosition !== undefined &&
144
  p.rosterPosition !== null &&
145
  p.rosterPosition >= 0 &&
@@ -200,10 +222,11 @@ export async function moveToStorage(id: number): Promise<void> {
200
  export async function getStoragePiclets(): Promise<PicletInstance[]> {
201
  const allPiclets = await db.picletInstances.toArray();
202
  return allPiclets.filter(p =>
203
- p.rosterPosition === undefined ||
204
- p.rosterPosition === null ||
205
- p.rosterPosition < 0 ||
206
- p.rosterPosition > 5
 
207
  );
208
  }
209
 
 
109
  rosterPosition: undefined,
110
 
111
  // Metadata
112
+ caught: false, // Scanned Piclets start as uncaught
113
+ caughtAt: undefined, // Will be set when caught
114
  bst,
115
  tier: stats.tier, // Use tier from stats
116
  role: 'balanced', // Could be enhanced based on stat distribution
 
131
  return await db.picletInstances.add(piclet);
132
  }
133
 
134
+ // Mark a Piclet as caught
135
+ export async function catchPiclet(picletId: number): Promise<void> {
136
+ await db.picletInstances.update(picletId, {
137
+ caught: true,
138
+ caughtAt: new Date()
139
+ });
140
+ }
141
+
142
+ // Get only caught Piclets (for Pictuary and battle roster)
143
+ export async function getCaughtPiclets(): Promise<PicletInstance[]> {
144
+ const allPiclets = await db.picletInstances.toArray();
145
+ return allPiclets.filter(p => p.caught === true);
146
+ }
147
+
148
+ // Get uncaught Piclets (for encounters)
149
+ export async function getUncaughtPiclets(): Promise<PicletInstance[]> {
150
+ const allPiclets = await db.picletInstances.toArray();
151
+ return allPiclets.filter(p => p.caught === false);
152
+ }
153
+
154
  // Get all PicletInstances
155
  export async function getAllPicletInstances(): Promise<PicletInstance[]> {
156
  return await db.picletInstances.toArray();
 
161
  const allPiclets = await db.picletInstances.toArray();
162
  return allPiclets
163
  .filter(p =>
164
+ p.caught === true && // Only caught Piclets can be in roster
165
  p.rosterPosition !== undefined &&
166
  p.rosterPosition !== null &&
167
  p.rosterPosition >= 0 &&
 
222
  export async function getStoragePiclets(): Promise<PicletInstance[]> {
223
  const allPiclets = await db.picletInstances.toArray();
224
  return allPiclets.filter(p =>
225
+ p.caught === true && // Only caught Piclets can be in storage
226
+ (p.rosterPosition === undefined ||
227
+ p.rosterPosition === null ||
228
+ p.rosterPosition < 0 ||
229
+ p.rosterPosition > 5)
230
  );
231
  }
232
 
src/lib/db/schema.ts CHANGED
@@ -6,7 +6,8 @@ export enum EncounterType {
6
  WILD_PICLET = 'wildPiclet',
7
  TRAINER_BATTLE = 'trainerBattle',
8
  SHOP = 'shop',
9
- HEALTH_CENTER = 'healthCenter'
 
10
  }
11
 
12
  // Battle Move embedded object
@@ -61,7 +62,8 @@ export interface PicletInstance {
61
  rosterPosition?: number; // 0-5 when in roster
62
 
63
  // Metadata
64
- caughtAt: Date;
 
65
  bst: number; // Base Stat Total
66
  tier: string;
67
  role: string;
@@ -87,6 +89,7 @@ export interface Encounter {
87
  title: string;
88
  description: string;
89
  picletTypeId?: string; // For wild piclet encounters
 
90
  enemyLevel?: number;
91
 
92
  // Timing
 
6
  WILD_PICLET = 'wildPiclet',
7
  TRAINER_BATTLE = 'trainerBattle',
8
  SHOP = 'shop',
9
+ HEALTH_CENTER = 'healthCenter',
10
+ FIRST_PICLET = 'firstPiclet'
11
  }
12
 
13
  // Battle Move embedded object
 
62
  rosterPosition?: number; // 0-5 when in roster
63
 
64
  // Metadata
65
+ caught: boolean; // Whether this Piclet has been caught by the player
66
+ caughtAt?: Date; // When this Piclet was caught (undefined if not caught)
67
  bst: number; // Base Stat Total
68
  tier: string;
69
  role: string;
 
89
  title: string;
90
  description: string;
91
  picletTypeId?: string; // For wild piclet encounters
92
+ picletInstanceId?: number; // For first piclet encounters - specific Piclet to catch
93
  enemyLevel?: number;
94
 
95
  // Timing