Fraser commited on
Commit
465b043
·
1 Parent(s): e7b0a32
src/lib/components/Pages/Encounters.svelte CHANGED
@@ -1,32 +1,207 @@
1
  <script lang="ts">
2
- // Placeholder for future implementation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  </script>
4
 
5
  <div class="encounters-page">
6
- <div class="coming-soon">
7
- <img
8
- src="https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/encounters_logo.png"
9
- alt="Encounters"
10
- class="page-icon"
11
- />
12
- <h2>Encounters</h2>
13
- <p>Battle trainers and catch wild Piclets!</p>
14
- <div class="feature-preview">
15
- <div class="preview-card">
16
- <h3>⚔️ Battle System</h3>
17
- <p>Turn-based combat with your Piclets</p>
18
- </div>
19
- <div class="preview-card">
20
- <h3>🎯 Catch Mechanics</h3>
21
- <p>Find and capture new monsters</p>
22
  </div>
23
- <div class="preview-card">
24
- <h3>🏆 Trainer Battles</h3>
25
- <p>Challenge other trainers</p>
26
- </div>
27
- </div>
28
- <p class="coming-soon-text">Coming Soon</p>
29
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  </div>
31
 
32
  <style>
@@ -34,57 +209,184 @@
34
  height: 100%;
35
  overflow-y: auto;
36
  -webkit-overflow-scrolling: touch;
37
- padding: 2rem 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
- .coming-soon {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  text-align: center;
42
- max-width: 400px;
43
- margin: 0 auto;
44
  }
45
 
46
- .page-icon {
47
- width: 80px;
48
- height: 80px;
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  margin-bottom: 1rem;
50
- opacity: 0.8;
51
  }
52
 
53
- h2 {
54
  margin: 0 0 0.5rem;
 
55
  color: #333;
56
  }
57
 
58
- .feature-preview {
 
 
 
 
 
59
  display: flex;
60
  flex-direction: column;
61
  gap: 1rem;
62
- margin: 2rem 0;
63
  }
64
 
65
- .preview-card {
66
- background: #f8f9fa;
67
- padding: 1.5rem;
 
 
 
68
  border-radius: 12px;
 
 
 
 
 
69
  text-align: left;
70
  }
71
 
72
- .preview-card h3 {
73
- margin: 0 0 0.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  font-size: 1.1rem;
75
- color: #333;
 
76
  }
77
 
78
- .preview-card p {
79
  margin: 0;
 
80
  color: #666;
81
- font-size: 0.9rem;
82
  }
83
 
84
- .coming-soon-text {
85
- color: #007bff;
 
 
 
 
 
 
 
 
 
 
 
86
  font-weight: 600;
87
- font-size: 1.2rem;
88
- margin-top: 2rem;
 
 
 
 
89
  }
90
  </style>
 
1
  <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { fade, fly } from 'svelte/transition';
4
+ import type { Encounter, GameState } from '$lib/db/schema';
5
+ import { EncounterType } from '$lib/db/schema';
6
+ import { EncounterService } from '$lib/db/encounterService';
7
+ import { getOrCreateGameState, incrementCounter, addProgressPoints } from '$lib/db/gameState';
8
+ import { db } from '$lib/db';
9
+
10
+ let encounters: Encounter[] = [];
11
+ let gameState: GameState | null = null;
12
+ let isLoading = true;
13
+ let isRefreshing = false;
14
+
15
+ onMount(async () => {
16
+ await loadEncounters();
17
+ });
18
+
19
+ async function loadEncounters() {
20
+ isLoading = true;
21
+ try {
22
+ // Load game state
23
+ gameState = await getOrCreateGameState();
24
+
25
+ // Get current encounters
26
+ const currentEncounters = await EncounterService.getCurrentEncounters();
27
+
28
+ // Check if we need to refresh encounters
29
+ if (await EncounterService.shouldRefreshEncounters() || currentEncounters.length === 0) {
30
+ // Generate new encounters
31
+ console.log('Generating new encounters...');
32
+ encounters = await EncounterService.generateEncounters();
33
+ } else {
34
+ // Use existing encounters
35
+ console.log('Using existing encounters:', currentEncounters.length);
36
+ encounters = currentEncounters;
37
+ }
38
+ } catch (error) {
39
+ console.error('Error loading encounters:', error);
40
+ }
41
+ isLoading = false;
42
+ }
43
+
44
+ async function handleEncounterTap(encounter: Encounter) {
45
+ if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
46
+ if (encounter.title === 'Your First Piclet!') {
47
+ // First catch - auto catch without battle
48
+ try {
49
+ isLoading = true;
50
+ const caughtPiclet = await EncounterService.catchWildPiclet(encounter);
51
+ await incrementCounter('picletsCapured');
52
+ await addProgressPoints(100);
53
+
54
+ // Show success message
55
+ alert(`You caught ${caughtPiclet.nickname}!`);
56
+
57
+ // Force refresh encounters
58
+ await forceEncounterRefresh();
59
+ } catch (error) {
60
+ console.error('Error catching piclet:', error);
61
+ }
62
+ isLoading = false;
63
+ } else {
64
+ // Regular wild encounter - would start battle
65
+ alert('Battle system coming soon!');
66
+ }
67
+ } else if (encounter.type === EncounterType.SHOP) {
68
+ await handleShopEncounter();
69
+ } else if (encounter.type === EncounterType.HEALTH_CENTER) {
70
+ await handleHealthCenterEncounter();
71
+ } else if (encounter.type === EncounterType.TRAINER_BATTLE) {
72
+ alert('Trainer battles coming soon!');
73
+ }
74
+ }
75
+
76
+ async function handleShopEncounter() {
77
+ alert('Shop features coming soon!');
78
+ await forceEncounterRefresh();
79
+ }
80
+
81
+ async function handleHealthCenterEncounter() {
82
+ try {
83
+ // Heal all piclets
84
+ const piclets = await db.picletInstances.toArray();
85
+ for (const piclet of piclets) {
86
+ await db.picletInstances.update(piclet.id!, {
87
+ currentHp: piclet.maxHp
88
+ });
89
+ }
90
+
91
+ alert('All your piclets have been healed to full health!');
92
+ await forceEncounterRefresh();
93
+ } catch (error) {
94
+ console.error('Error at health center:', error);
95
+ }
96
+ }
97
+
98
+ async function forceEncounterRefresh() {
99
+ isRefreshing = true;
100
+ try {
101
+ await EncounterService.forceEncounterRefresh();
102
+ encounters = await EncounterService.generateEncounters();
103
+ gameState = await getOrCreateGameState();
104
+ } catch (error) {
105
+ console.error('Error refreshing encounters:', error);
106
+ }
107
+ isRefreshing = false;
108
+ }
109
+
110
+ function getEncounterIcon(encounter: Encounter): string {
111
+ switch (encounter.type) {
112
+ case EncounterType.SHOP:
113
+ return '🛍️';
114
+ case EncounterType.HEALTH_CENTER:
115
+ return '❤️';
116
+ case EncounterType.TRAINER_BATTLE:
117
+ return '🏆';
118
+ case EncounterType.WILD_PICLET:
119
+ default:
120
+ return '⚔️';
121
+ }
122
+ }
123
+
124
+ function getEncounterColor(encounter: Encounter): string {
125
+ switch (encounter.type) {
126
+ case EncounterType.WILD_PICLET:
127
+ return '#4caf50';
128
+ case EncounterType.TRAINER_BATTLE:
129
+ return '#ff9800';
130
+ case EncounterType.SHOP:
131
+ return '#2196f3';
132
+ case EncounterType.HEALTH_CENTER:
133
+ return '#9c27b0';
134
+ default:
135
+ return '#607d8b';
136
+ }
137
+ }
138
  </script>
139
 
140
  <div class="encounters-page">
141
+ <div class="header">
142
+ <h1>Encounters</h1>
143
+ {#if gameState}
144
+ <div class="progress-bar">
145
+ <div class="progress-fill" style="width: {(gameState.progressPoints / 1000) * 100}%"></div>
146
+ <span class="progress-text">{gameState.progressPoints}/1000</span>
 
 
 
 
 
 
 
 
 
 
147
  </div>
148
+ {/if}
 
 
 
 
 
149
  </div>
150
+
151
+ {#if isLoading}
152
+ <div class="loading">
153
+ <div class="spinner"></div>
154
+ <p>Loading encounters...</p>
155
+ </div>
156
+ {:else if encounters.length === 0}
157
+ <div class="empty-state">
158
+ <div class="empty-icon">🗺️</div>
159
+ <h2>No Encounters Available</h2>
160
+ <p>New encounters will appear soon!</p>
161
+ <button class="refresh-button" on:click={loadEncounters}>
162
+ Refresh
163
+ </button>
164
+ </div>
165
+ {:else}
166
+ <div class="encounters-list">
167
+ {#each encounters as encounter, index (encounter.id)}
168
+ <button
169
+ class="encounter-card"
170
+ style="border-color: {getEncounterColor(encounter)}30"
171
+ on:click={() => handleEncounterTap(encounter)}
172
+ in:fly={{ y: 20, delay: index * 50 }}
173
+ disabled={isRefreshing}
174
+ >
175
+ <div class="encounter-icon">
176
+ {#if encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId}
177
+ {#if encounter.title === 'Your First Piclet!'}
178
+ <div class="piclet-silhouette">?</div>
179
+ {:else}
180
+ <img
181
+ src={`https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`}
182
+ alt="Wild Piclet"
183
+ on:error={(e) => {
184
+ e.currentTarget.style.display = 'none';
185
+ e.currentTarget.nextElementSibling.style.display = 'block';
186
+ }}
187
+ />
188
+ <div class="fallback-icon" style="display: none">{getEncounterIcon(encounter)}</div>
189
+ {/if}
190
+ {:else}
191
+ <span class="type-icon">{getEncounterIcon(encounter)}</span>
192
+ {/if}
193
+ </div>
194
+
195
+ <div class="encounter-info">
196
+ <h3>{encounter.title}</h3>
197
+ <p>{encounter.description}</p>
198
+ </div>
199
+
200
+ <div class="encounter-arrow">›</div>
201
+ </button>
202
+ {/each}
203
+ </div>
204
+ {/if}
205
  </div>
206
 
207
  <style>
 
209
  height: 100%;
210
  overflow-y: auto;
211
  -webkit-overflow-scrolling: touch;
212
+ padding: 1rem;
213
+ padding-bottom: 5rem;
214
+ }
215
+
216
+ .header {
217
+ margin-bottom: 1.5rem;
218
+ }
219
+
220
+ .header h1 {
221
+ margin: 0 0 1rem;
222
+ font-size: 1.75rem;
223
+ font-weight: 700;
224
+ color: #1a1a1a;
225
  }
226
 
227
+ .progress-bar {
228
+ background: #e0e0e0;
229
+ height: 24px;
230
+ border-radius: 12px;
231
+ overflow: hidden;
232
+ position: relative;
233
+ }
234
+
235
+ .progress-fill {
236
+ background: linear-gradient(90deg, #4caf50, #66bb6a);
237
+ height: 100%;
238
+ transition: width 0.3s ease;
239
+ }
240
+
241
+ .progress-text {
242
+ position: absolute;
243
+ top: 50%;
244
+ left: 50%;
245
+ transform: translate(-50%, -50%);
246
+ font-size: 0.75rem;
247
+ font-weight: 600;
248
+ color: #333;
249
+ }
250
+
251
+ .loading, .empty-state {
252
+ display: flex;
253
+ flex-direction: column;
254
+ align-items: center;
255
+ justify-content: center;
256
+ height: 60vh;
257
  text-align: center;
 
 
258
  }
259
 
260
+ .spinner {
261
+ width: 48px;
262
+ height: 48px;
263
+ border: 4px solid #f0f0f0;
264
+ border-top-color: #4caf50;
265
+ border-radius: 50%;
266
+ animation: spin 1s linear infinite;
267
+ margin-bottom: 1rem;
268
+ }
269
+
270
+ @keyframes spin {
271
+ to { transform: rotate(360deg); }
272
+ }
273
+
274
+ .empty-icon {
275
+ font-size: 4rem;
276
  margin-bottom: 1rem;
 
277
  }
278
 
279
+ .empty-state h2 {
280
  margin: 0 0 0.5rem;
281
+ font-size: 1.25rem;
282
  color: #333;
283
  }
284
 
285
+ .empty-state p {
286
+ color: #666;
287
+ font-size: 0.9rem;
288
+ }
289
+
290
+ .encounters-list {
291
  display: flex;
292
  flex-direction: column;
293
  gap: 1rem;
 
294
  }
295
 
296
+ .encounter-card {
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 1rem;
300
+ background: #fff;
301
+ border: 2px solid;
302
  border-radius: 12px;
303
+ padding: 1rem;
304
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
305
+ transition: all 0.2s ease;
306
+ cursor: pointer;
307
+ width: 100%;
308
  text-align: left;
309
  }
310
 
311
+ .encounter-card:hover:not(:disabled) {
312
+ transform: translateY(-2px);
313
+ box-shadow: 0 4px 12px rgba(0,0,0,0.12);
314
+ }
315
+
316
+ .encounter-card:disabled {
317
+ opacity: 0.6;
318
+ cursor: not-allowed;
319
+ }
320
+
321
+ .encounter-icon {
322
+ width: 60px;
323
+ height: 60px;
324
+ flex-shrink: 0;
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ }
329
+
330
+ .encounter-icon img {
331
+ width: 100%;
332
+ height: 100%;
333
+ object-fit: cover;
334
+ border-radius: 8px;
335
+ }
336
+
337
+ .piclet-silhouette {
338
+ width: 100%;
339
+ height: 100%;
340
+ background: #e0e0e0;
341
+ border-radius: 8px;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ font-size: 2rem;
346
+ font-weight: bold;
347
+ color: #999;
348
+ }
349
+
350
+ .type-icon, .fallback-icon {
351
+ font-size: 2rem;
352
+ }
353
+
354
+ .encounter-info {
355
+ flex: 1;
356
+ }
357
+
358
+ .encounter-info h3 {
359
+ margin: 0 0 0.25rem;
360
  font-size: 1.1rem;
361
+ font-weight: 600;
362
+ color: #1a1a1a;
363
  }
364
 
365
+ .encounter-info p {
366
  margin: 0;
367
+ font-size: 0.875rem;
368
  color: #666;
 
369
  }
370
 
371
+ .encounter-arrow {
372
+ font-size: 1.5rem;
373
+ color: #999;
374
+ }
375
+
376
+ .refresh-button {
377
+ margin-top: 1rem;
378
+ padding: 0.75rem 1.5rem;
379
+ background: #4caf50;
380
+ color: white;
381
+ border: none;
382
+ border-radius: 8px;
383
+ font-size: 1rem;
384
  font-weight: 600;
385
+ cursor: pointer;
386
+ transition: background 0.2s ease;
387
+ }
388
+
389
+ .refresh-button:hover {
390
+ background: #45a049;
391
  }
392
  </style>
src/lib/db/encounterService.ts ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 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;
8
+ const MIN_WILD_ENCOUNTERS = 2;
9
+ const MAX_WILD_ENCOUNTERS = 3;
10
+ const LEVEL_VARIANCE = 2;
11
+
12
+ export class EncounterService {
13
+ // Check if encounters should be refreshed
14
+ static async shouldRefreshEncounters(): Promise<boolean> {
15
+ const state = await getOrCreateGameState();
16
+ const hoursSinceRefresh = (Date.now() - state.lastEncounterRefresh.getTime()) / (1000 * 60 * 60);
17
+ return hoursSinceRefresh >= ENCOUNTER_REFRESH_HOURS;
18
+ }
19
+
20
+ // Force encounter refresh
21
+ static async forceEncounterRefresh(): Promise<void> {
22
+ await db.encounters.clear();
23
+ await markEncountersRefreshed();
24
+ }
25
+
26
+ // Get current encounters
27
+ static async getCurrentEncounters(): Promise<Encounter[]> {
28
+ return await db.encounters
29
+ .orderBy('createdAt')
30
+ .reverse()
31
+ .toArray();
32
+ }
33
+
34
+ // Clear all encounters
35
+ static async clearEncounters(): Promise<void> {
36
+ await db.encounters.clear();
37
+ }
38
+
39
+ // Generate new encounters
40
+ static async generateEncounters(): Promise<Encounter[]> {
41
+ const encounters: Omit<Encounter, 'id'>[] = [];
42
+
43
+ // Check if player needs first catch encounter
44
+ const playerPiclets = await db.picletInstances.toArray();
45
+ if (playerPiclets.length === 0) {
46
+ encounters.push(await this.createFirstCatchEncounter());
47
+ } else {
48
+ // Generate wild piclet encounters
49
+ const wildEncounters = await this.generateWildEncounters();
50
+ encounters.push(...wildEncounters);
51
+ }
52
+
53
+ // Always add shop and health center
54
+ encounters.push({
55
+ type: EncounterType.SHOP,
56
+ title: 'Piclet Shop',
57
+ description: 'Buy items and supplies for your journey',
58
+ createdAt: new Date()
59
+ });
60
+
61
+ encounters.push({
62
+ type: EncounterType.HEALTH_CENTER,
63
+ title: 'Health Center',
64
+ description: 'Heal your piclets back to full health',
65
+ createdAt: new Date()
66
+ });
67
+
68
+ // Clear existing encounters and add new ones
69
+ await db.encounters.clear();
70
+ for (const encounter of encounters) {
71
+ await db.encounters.add(encounter);
72
+ }
73
+
74
+ await markEncountersRefreshed();
75
+ return await this.getCurrentEncounters();
76
+ }
77
+
78
+ // Create first catch encounter
79
+ private static async createFirstCatchEncounter(): Promise<Omit<Encounter, 'id'>> {
80
+ // TODO: Replace with actual piclet data when available
81
+ // For now, using placeholder data
82
+ return {
83
+ type: EncounterType.WILD_PICLET,
84
+ title: 'Your First Piclet!',
85
+ description: 'A friendly piclet appears! This one seems easy to catch.',
86
+ picletTypeId: 'starter-001', // Placeholder ID
87
+ enemyLevel: 5,
88
+ createdAt: new Date()
89
+ };
90
+ }
91
+
92
+ // Generate wild piclet encounters
93
+ private static async generateWildEncounters(): Promise<Omit<Encounter, 'id'>[]> {
94
+ const encounters: Omit<Encounter, 'id'>[] = [];
95
+
96
+ // Get player's average level
97
+ const avgLevel = await this.getPlayerAverageLevel();
98
+
99
+ // TODO: When piclet types are available, filter for uncaught piclets
100
+ // For now, generate placeholder encounters
101
+ const encounterCount = MIN_WILD_ENCOUNTERS + Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1));
102
+
103
+ for (let i = 0; i < encounterCount; i++) {
104
+ const levelVariance = Math.floor(Math.random() * (LEVEL_VARIANCE * 2 + 1)) - LEVEL_VARIANCE;
105
+ const enemyLevel = Math.max(1, avgLevel + levelVariance);
106
+
107
+ encounters.push({
108
+ type: EncounterType.WILD_PICLET,
109
+ title: `Wild Piclet Appeared!`,
110
+ description: `A level ${enemyLevel} piclet blocks your path!`,
111
+ picletTypeId: `wild-${i + 1}`, // Placeholder ID
112
+ enemyLevel,
113
+ createdAt: new Date()
114
+ });
115
+ }
116
+
117
+ return encounters;
118
+ }
119
+
120
+ // Get player's average piclet level
121
+ private static async getPlayerAverageLevel(): Promise<number> {
122
+ const rosterPiclets = await db.picletInstances
123
+ .where('isInRoster')
124
+ .equals(1) // Dexie uses 1 for true in indexed fields
125
+ .toArray();
126
+
127
+ if (rosterPiclets.length === 0) {
128
+ const allPiclets = await db.picletInstances.toArray();
129
+ if (allPiclets.length === 0) return 5; // Default starting level
130
+
131
+ const totalLevel = allPiclets.reduce((sum, p) => sum + p.level, 0);
132
+ return Math.round(totalLevel / allPiclets.length);
133
+ }
134
+
135
+ const totalLevel = rosterPiclets.reduce((sum, p) => sum + p.level, 0);
136
+ return Math.round(totalLevel / rosterPiclets.length);
137
+ }
138
+
139
+ // Catch a wild piclet (for first encounter)
140
+ static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
141
+ // TODO: Implement actual piclet catching logic
142
+ // For now, create a placeholder instance
143
+ const newPiclet: Omit<PicletInstance, 'id'> = {
144
+ typeId: encounter.picletTypeId!,
145
+ nickname: 'Starter Piclet',
146
+ primaryTypeString: 'normal',
147
+
148
+ // Stats
149
+ level: encounter.enemyLevel || 5,
150
+ xp: 0,
151
+ currentHp: 20,
152
+ maxHp: 20,
153
+ attack: 10,
154
+ defense: 10,
155
+ fieldAttack: 10,
156
+ fieldDefense: 10,
157
+ speed: 10,
158
+
159
+ // Base stats
160
+ baseHp: 20,
161
+ baseAttack: 10,
162
+ baseDefense: 10,
163
+ baseFieldAttack: 10,
164
+ baseFieldDefense: 10,
165
+ baseSpeed: 10,
166
+
167
+ // Battle
168
+ moves: [],
169
+ nature: 'hardy',
170
+
171
+ // Roster
172
+ isInRoster: true,
173
+ rosterPosition: 0,
174
+
175
+ // Metadata
176
+ caughtAt: new Date(),
177
+ bst: 60,
178
+ tier: 'common',
179
+ role: 'balanced',
180
+ variance: 1,
181
+
182
+ // Visuals
183
+ imageUrl: `https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`,
184
+ imageCaption: 'A friendly starter piclet',
185
+ concept: 'starter',
186
+ imagePrompt: 'cute starter monster'
187
+ };
188
+
189
+ const id = await db.picletInstances.add(newPiclet);
190
+ return { ...newPiclet, id };
191
+ }
192
+ }