Fraser commited on
Commit
565e57b
·
1 Parent(s): 0268d3c

RESET TO MONSTER DISCOVERY SYSTEM

Browse files
Files changed (36) hide show
  1. CLAUDE.md +86 -28
  2. pokemon_emerald_docs/DEFINING_ATTACKS.md +0 -277
  3. pokemon_emerald_docs/battle_system_architecture.md +0 -363
  4. pokemon_emerald_docs/battle_system_mechanics.md +0 -475
  5. src/App.svelte +4 -4
  6. src/lib/components/Battle/ActionButtons.svelte +0 -113
  7. src/lib/components/Battle/ActionViewSelector.svelte +0 -508
  8. src/lib/components/Battle/FieldEffectIndicator.svelte +0 -117
  9. src/lib/components/Battle/LLMBattleEngine.svelte +0 -453
  10. src/lib/components/Battle/PicletInfo.svelte +0 -244
  11. src/lib/components/Battle/StatusEffectIndicator.svelte +0 -89
  12. src/lib/components/Battle/TypewriterText.svelte +0 -49
  13. src/lib/components/Layout/ProgressBar.svelte +21 -18
  14. src/lib/components/Layout/TabBar.svelte +2 -2
  15. src/lib/components/Pages/Activity.svelte +441 -0
  16. src/lib/components/Pages/Battle.svelte +0 -177
  17. src/lib/components/Pages/Encounters.svelte +0 -586
  18. src/lib/components/Pages/Pictuary.svelte +305 -507
  19. src/lib/components/Pages/ViewAll.svelte +0 -140
  20. src/lib/components/PicletGenerator/PicletGenerator.svelte +85 -37
  21. src/lib/components/PicletGenerator/PicletResult.svelte +12 -5
  22. src/lib/components/Piclets/AddToRosterDialog.svelte +0 -169
  23. src/lib/components/Piclets/DraggablePicletCard.svelte +0 -70
  24. src/lib/components/Piclets/EmptySlotCard.svelte +0 -61
  25. src/lib/components/Piclets/NewlyCaughtPicletDetail.svelte +0 -644
  26. src/lib/components/Piclets/PicletDetail.svelte +22 -8
  27. src/lib/components/Piclets/RosterSlot.svelte +0 -109
  28. src/lib/db/encounterService.ts +0 -115
  29. src/lib/db/gameState.ts +45 -36
  30. src/lib/db/index.ts +26 -2
  31. src/lib/db/piclets.ts +50 -47
  32. src/lib/db/schema.ts +60 -39
  33. src/lib/services/canonicalService.ts +214 -0
  34. src/lib/services/enhancedCaption.ts +167 -0
  35. src/lib/services/picletMetadata.ts +14 -24
  36. src/lib/types/index.ts +13 -6
CLAUDE.md CHANGED
@@ -4,14 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
 
5
  ## Project Overview
6
 
7
- This is a Svelte 5 + TypeScript + Vite single-page application for a Pokémon-style creature collection game called "Pictuary". It uses the latest Svelte 5 with runes syntax (`$state()`, `$derived()`, etc.).
8
 
9
  ### Main Features
10
- - **Monster Generation**: Upload images → AI generates unique creatures ("Piclets") with stats and abilities
11
- - **Battle System**: Turn-based combat between player and AI opponents
12
- - **Collection Management**: Roster of captured Piclets with detailed stats and move sets
13
- - **Creature Cards**: Trading card-style interface with type-based designs
14
- - **Image Processing**: AI-powered image captioning and creature concept generation
15
 
16
  ## Essential Commands
17
 
@@ -47,18 +47,19 @@ npm run test:ui
47
  - Uses Svelte 5 runes syntax (not Svelte 4 syntax)
48
 
49
  ### Key Components
50
- - **Pages**: `Scanner.svelte` (main), `Pictuary.svelte` (collection), `Battle.svelte` (combat)
51
- - **Monster Generation**: `MonsterGenerator.svelte`, `MonsterResult.svelte` (redesigned with PicletCard preview)
52
- - **Battle System**: `BattleField.svelte`, `ActionButtons.svelte`, turn-based combat logic
53
- - **Piclet Management**: `PicletCard.svelte`, `PicletDetail.svelte`, `AddToRosterDialog.svelte`
54
- - **Database**: IndexedDB with `schema.ts` defining PicletInstance, BattleMove, Monster types
55
 
56
  ### Key Patterns
57
  1. **State Management**: Use `$state()` rune for reactive state
58
- 2. **TypeScript**: All components should use `<script lang="ts">`
59
  3. **Imports**: Use ES module imports, components are default exports
60
  4. **Styling**: Component styles are scoped by default, global styles in `src/app.css`
61
- 5. **Database**: IndexedDB operations in `src/lib/db/` with async functions for CRUD operations
 
62
 
63
  ### Build Configuration
64
  - Vite handles bundling with `vite.config.ts`
@@ -84,20 +85,37 @@ const client = await window.gradioClient.Client.connect("space-name");
84
  ```
85
 
86
  **Current Gradio Connections:**
87
- - **Flux Image Generation**: `Fraser/flux`
88
- - **Joy Caption**: `fancyfeast/joy-caption-alpha-two`
89
- - **Zephyr-7B Text Generation**: `Fraser/zephyr-7b`
 
 
 
 
 
 
 
90
 
91
  **Build Notes:**
92
  - DO NOT install Gradio Client via npm (`npm install @gradio/client`) - it causes build failures
93
  - The CDN approach ensures compatibility with Vite bundling
94
  - All Gradio connections should use the established pattern from App.svelte
95
 
96
- ### Text Generation Architecture
97
- The project uses a simple, direct approach:
98
- 1. **Zephyr-7B**: Direct connection to `Fraser/zephyr-7b` space for all text generation
99
- 2. **Direct API calls**: Components use `zephyrClient.predict("/chat", [...])` directly
100
- 3. **No fallback complexity**: Simple, reliable single-client architecture
 
 
 
 
 
 
 
 
 
 
101
 
102
  ## Troubleshooting
103
 
@@ -106,18 +124,58 @@ The project uses a simple, direct approach:
106
  - **Type errors**: Run `npm run check` to identify TypeScript issues before building
107
  - **Missing dependencies**: Run `npm install` if packages are missing
108
 
109
- ### Monster Generation Issues
110
- - **Name extraction problems**: Check `MonsterGenerator.svelte` - regex should extract content after `# Monster Name`
111
- - **Zephyr-7B connection failures**: Verify `Fraser/zephyr-7b` space is accessible
112
- - **Image processing errors**: Verify Flux and Joy Caption clients are properly connected
 
113
 
114
  ### Performance
115
  - **Large image files**: Consider image compression before upload
116
- - **Slow generation**: Zephyr-7B may take 10-30 seconds for complex monster concepts
117
- - **Battle lag**: IndexedDB operations are async - ensure proper await usage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  ## Important Notes
120
  - This is NOT SvelteKit - no routing, SSR, or API routes
121
  - HMR preserves component state (can be toggled in vite.config.ts)
122
  - All paths in imports should be relative or use `$lib` alias for src/lib
123
- - IndexedDB is used for local storage - data persists between sessions
 
 
 
4
 
5
  ## Project Overview
6
 
7
+ This is a Svelte 5 + TypeScript + Vite single-page application for a discovery-based creature collection game called "Pictuary". It uses the latest Svelte 5 with runes syntax (`$state()`, `$derived()`, etc.).
8
 
9
  ### Main Features
10
+ - **Monster Discovery**: Upload images → AI identifies objects and generates/retrieves canonical "Piclets"
11
+ - **Canonical System**: Each real-world object has ONE canonical Piclet, with variations tracked
12
+ - **Collection Management**: Track discovered Piclets with metadata (discoverer, rarity, variations)
13
+ - **Activity Feed**: Leaderboard showing top discoverers and recent finds
14
+ - **Server Integration**: Connects to `../piclets-server/` for canonical Piclet database
15
 
16
  ## Essential Commands
17
 
 
47
  - Uses Svelte 5 runes syntax (not Svelte 4 syntax)
48
 
49
  ### Key Components
50
+ - **Pages**: `Scanner.svelte` (discovery), `Pictuary.svelte` (collection), `Activity.svelte` (leaderboard/feed)
51
+ - **Monster Discovery**: `PicletGenerator.svelte`, `PicletResult.svelte` with canonical/variation detection
52
+ - **Server Integration**: Services for canonical Piclet lookup and creation
53
+ - **Piclet Management**: `PicletCard.svelte`, `PicletDetail.svelte` with discovery metadata
54
+ - **Database**: IndexedDB with `schema.ts` for local caching + server sync for canonical data
55
 
56
  ### Key Patterns
57
  1. **State Management**: Use `$state()` rune for reactive state
58
+ 2. **TypeScript**: All components should use `<script lang="ts">`
59
  3. **Imports**: Use ES module imports, components are default exports
60
  4. **Styling**: Component styles are scoped by default, global styles in `src/app.css`
61
+ 5. **Database**: Hybrid approach - IndexedDB for local state, server API for canonical Piclets
62
+ 6. **Discovery Flow**: Caption → Extract object → Server lookup → Return canonical/variation/new
63
 
64
  ### Build Configuration
65
  - Vite handles bundling with `vite.config.ts`
 
85
  ```
86
 
87
  **Current Gradio Connections:**
88
+ - **Flux Image Generation**: `Fraser/flux`
89
+ - **Joy Caption**: `fancyfeast/joy-caption-alpha-two` (configured for object identification)
90
+ - **Qwen Text Generation**: For generating Piclet concepts from object captions
91
+
92
+ ### Server Integration
93
+ - **Endpoint**: `../piclets-server/` (local development)
94
+ - **Canonical Lookup**: POST `/api/piclets/search` with object keywords
95
+ - **Create Canonical**: POST `/api/piclets/canonical` for new discoveries
96
+ - **Create Variation**: POST `/api/piclets/variation` with canonicalId
97
+ - **Activity Feed**: GET `/api/activity/recent` for global discoveries
98
 
99
  **Build Notes:**
100
  - DO NOT install Gradio Client via npm (`npm install @gradio/client`) - it causes build failures
101
  - The CDN approach ensures compatibility with Vite bundling
102
  - All Gradio connections should use the established pattern from App.svelte
103
 
104
+ ### Discovery Architecture
105
+
106
+ #### Object Identification Flow
107
+ 1. **Image Caption**: Joy Caption extracts object type and visual attributes
108
+ 2. **Object Extraction**: Parse caption for primary object ("pillow", "pyramid", etc.)
109
+ 3. **Server Lookup**: Search for exact match or close variations
110
+ 4. **Response Types**:
111
+ - **Exact Match**: Return existing canonical Piclet + increment scan count
112
+ - **Close Match**: Create variation of canonical + link to parent
113
+ - **No Match**: Create new canonical Piclet + register discoverer
114
+
115
+ #### Rarity Calculation
116
+ - Track `scanCount` per canonical Piclet
117
+ - Rarity score = 1 / scanCount (lower scan count = higher rarity)
118
+ - Display rarity tiers: Common, Uncommon, Rare, Epic, Legendary
119
 
120
  ## Troubleshooting
121
 
 
124
  - **Type errors**: Run `npm run check` to identify TypeScript issues before building
125
  - **Missing dependencies**: Run `npm install` if packages are missing
126
 
127
+ ### Discovery System Issues
128
+ - **Object extraction**: Ensure Joy Caption is set to "Descriptive" mode with focus on object type
129
+ - **Server connection**: Verify `../piclets-server/` is running on expected port
130
+ - **Variation detection**: Check keyword matching algorithm for false positives
131
+ - **Rarity calculation**: Ensure scanCount is properly incremented on each discovery
132
 
133
  ### Performance
134
  - **Large image files**: Consider image compression before upload
135
+ - **Server latency**: Cache canonical Piclets locally after first fetch
136
+ - **Search optimization**: Use keyword indexing for O(1) lookups
137
+ - **Activity feed**: Paginate results, cache recent discoveries
138
+
139
+ ## Implementation Strategy
140
+
141
+ ### Caption Processing
142
+ ```typescript
143
+ // Example caption processing for object extraction
144
+ const caption = "A velvet blue pillow with golden tassels on a couch";
145
+ const primaryObject = extractObject(caption); // "pillow"
146
+ const attributes = extractAttributes(caption); // ["velvet", "blue"]
147
+ ```
148
+
149
+ ### Server Communication
150
+ ```typescript
151
+ // Canonical lookup
152
+ const result = await fetch('/api/piclets/search', {
153
+ method: 'POST',
154
+ body: JSON.stringify({
155
+ object: 'pillow',
156
+ attributes: ['velvet', 'blue']
157
+ })
158
+ });
159
+
160
+ // Response types
161
+ interface CanonicalMatch {
162
+ type: 'exact' | 'variation' | 'new';
163
+ piclet: PicletInstance;
164
+ canonicalId?: string;
165
+ discoveredBy?: string;
166
+ scanCount: number;
167
+ }
168
+ ```
169
+
170
+ ### Database Schema Updates
171
+ - Remove: level, xp, hp, attack, defense, speed, moves, battleStats
172
+ - Add: canonicalId, isCanonical, discoveredBy, discoveredAt, scanCount, variations[]
173
+ - Keep: typeId, primaryType, tier, imageUrl, description, concept
174
 
175
  ## Important Notes
176
  - This is NOT SvelteKit - no routing, SSR, or API routes
177
  - HMR preserves component state (can be toggled in vite.config.ts)
178
  - All paths in imports should be relative or use `$lib` alias for src/lib
179
+ - Hybrid storage: IndexedDB for local cache, server for canonical truth
180
+ - Object abstraction level is critical - "pillow" not "decorative cushion"
181
+ - Variations limited to 2-3 meaningful attributes (material, style, color)
pokemon_emerald_docs/DEFINING_ATTACKS.md DELETED
@@ -1,277 +0,0 @@
1
- # Defining Attacks in pokeemerald
2
-
3
- This document analyzes how pokeemerald defines battle actions (moves/attacks) and their effects, providing a clear system architecture for implementing similar battle mechanics in Pokemon-style games.
4
-
5
- ## Architecture Overview
6
-
7
- pokeemerald uses a **multi-layered, data-driven approach** to define attacks:
8
-
9
- 1. **Move Constants** - Unique IDs for each move
10
- 2. **Move Data Structure** - Statistical properties and effect assignments
11
- 3. **Effect Constants** - Categorization of move behaviors
12
- 4. **Battle Scripts** - Implementation logic for each effect
13
- 5. **Script Commands** - Low-level operations for battle mechanics
14
-
15
- ## Layer 1: Move Constants (`include/constants/moves.h`)
16
-
17
- Each move gets a unique constant identifier:
18
-
19
- ```c
20
- #define MOVE_NONE 0
21
- #define MOVE_POUND 1
22
- #define MOVE_KARATE_CHOP 2
23
- #define MOVE_DOUBLE_SLAP 3
24
- // ... continues for all moves
25
- ```
26
-
27
- **Key Benefits:**
28
- - Type-safe move references throughout codebase
29
- - Easy to add new moves without conflicts
30
- - Clear naming convention
31
-
32
- ## Layer 2: Move Data Structure (`src/data/battle_moves.h`)
33
-
34
- Each move is defined using the `BattleMove` struct:
35
-
36
- ```c
37
- [MOVE_POUND] = {
38
- .effect = EFFECT_HIT, // What the move does
39
- .power = 40, // Base damage
40
- .type = TYPE_NORMAL, // Move type (Normal, Fire, etc.)
41
- .accuracy = 100, // Hit chance (0-100)
42
- .pp = 35, // Power Points (usage count)
43
- .secondaryEffectChance = 0, // Chance of secondary effect
44
- .target = MOVE_TARGET_SELECTED, // Who can be targeted
45
- .priority = 0, // Move speed priority
46
- .flags = FLAG_MAKES_CONTACT | FLAG_PROTECT_AFFECTED | FLAG_KINGS_ROCK_AFFECTED,
47
- },
48
- ```
49
-
50
- **Key Features:**
51
- - **Separation of Concerns**: Stats vs. behavior logic
52
- - **Flag System**: Modular properties (contact, protection, etc.)
53
- - **Secondary Effects**: Built-in chance system for additional effects
54
- - **Targeting System**: Flexible target selection
55
-
56
- ## Layer 3: Effect Constants (`include/constants/battle_move_effects.h`)
57
-
58
- Effects categorize move behaviors into reusable types:
59
-
60
- ```c
61
- #define EFFECT_HIT 0 // Basic damage
62
- #define EFFECT_SLEEP 1 // Status effect
63
- #define EFFECT_POISON_HIT 2 // Damage + poison
64
- #define EFFECT_ABSORB 3 // Drain HP
65
- #define EFFECT_BURN_HIT 4 // Damage + burn
66
- #define EFFECT_MULTI_HIT 29 // Multiple strikes
67
- #define EFFECT_HIGH_CRITICAL 43 // Increased crit chance
68
- // ... 200+ different effects
69
- ```
70
-
71
- **Benefits:**
72
- - **Reusability**: Multiple moves can share the same effect
73
- - **Extensibility**: New effects can be added without changing existing moves
74
- - **Organization**: Related behaviors grouped together
75
-
76
- ## Layer 4: Battle Scripts (`data/battle_scripts_1.s`)
77
-
78
- Each effect maps to a battle script that defines the actual implementation:
79
-
80
- ```assembly
81
- gBattleScriptsForMoveEffects::
82
- .4byte BattleScript_EffectHit @ EFFECT_HIT
83
- .4byte BattleScript_EffectSleep @ EFFECT_SLEEP
84
- .4byte BattleScript_EffectPoisonHit @ EFFECT_POISON_HIT
85
- // ... maps all effects to their scripts
86
- ```
87
-
88
- ### Example Battle Scripts:
89
-
90
- **Basic Hit:**
91
- ```assembly
92
- BattleScript_EffectHit::
93
- attackcanceler # Check if attack can proceed
94
- accuracycheck BattleScript_PrintMoveMissed, ACC_CURR_MOVE
95
- attackstring # Display move name
96
- ppreduce # Consume PP
97
- critcalc # Calculate critical hits
98
- damagecalc # Calculate damage
99
- typecalc # Apply type effectiveness
100
- adjustnormaldamage # Apply damage modifications
101
- attackanimation # Play visual effects
102
- waitanimation # Wait for animation
103
- effectivenesssound # Play sound effects
104
- # ... continues with damage application
105
- ```
106
-
107
- **Poison Hit (Damage + Status):**
108
- ```assembly
109
- BattleScript_EffectPoisonHit::
110
- setmoveeffect MOVE_EFFECT_POISON # Set secondary effect
111
- goto BattleScript_EffectHit # Use standard hit logic
112
- ```
113
-
114
- **Multi-Hit:**
115
- ```assembly
116
- BattleScript_EffectMultiHit::
117
- attackcanceler
118
- accuracycheck BattleScript_PrintMoveMissed, ACC_CURR_MOVE
119
- attackstring
120
- ppreduce
121
- setmultihitcounter 0 # Initialize hit counter
122
- initmultihitstring # Setup hit count display
123
- BattleScript_MultiHitLoop::
124
- jumpifhasnohp BS_ATTACKER, BattleScript_MultiHitEnd
125
- jumpifhasnohp BS_TARGET, BattleScript_MultiHitPrintStrings
126
- # ... hit logic with loop control
127
- ```
128
-
129
- ## Layer 5: Script Commands (`src/battle_script_commands.c`)
130
-
131
- Low-level commands that scripts use:
132
-
133
- - **Flow Control**: `jumpif*`, `goto`, `call`, `return`
134
- - **Battle Mechanics**: `accuracycheck`, `critcalc`, `damagecalc`
135
- - **Status Effects**: `setmoveeffect`, `seteffectprimary`
136
- - **Animation**: `attackanimation`, `waitanimation`
137
- - **State Management**: `attackcanceler`, `movevaluescleanup`
138
-
139
- ## Complex Effect Patterns
140
-
141
- ### 1. **Composite Effects** (Hit + Status)
142
- ```c
143
- // Move data specifies base effect
144
- .effect = EFFECT_BURN_HIT,
145
-
146
- // Script combines damage with status
147
- BattleScript_EffectBurnHit::
148
- setmoveeffect MOVE_EFFECT_BURN # Add burn chance
149
- goto BattleScript_EffectHit # Execute standard damage
150
- ```
151
-
152
- ### 2. **Multi-Stage Effects** (Charging moves)
153
- ```c
154
- // Sky Attack: charge turn, then hit
155
- .effect = EFFECT_SKY_ATTACK,
156
-
157
- BattleScript_EffectSkyAttack::
158
- jumpifstatus2 BS_ATTACKER, STATUS2_MULTIPLETURNS, BattleScript_TwoTurnMovesSecondTurn
159
- jumpifword CMP_COMMON_BITS, gHitMarker, HITMARKER_NO_ATTACKSTRING, BattleScript_TwoTurnMovesSecondTurn
160
- # First turn: charge up
161
- # Second turn: attack
162
- ```
163
-
164
- ### 3. **Variable Effects** (Power/behavior changes)
165
- ```c
166
- // Moves that change behavior based on conditions
167
- jumpifnotmove MOVE_SURF, BattleScript_HitFromAtkCanceler
168
- jumpifnostatus3 BS_TARGET, STATUS3_UNDERWATER, BattleScript_HitFromAtkCanceler
169
- orword gHitMarker, HITMARKER_IGNORE_UNDERWATER
170
- setbyte sDMG_MULTIPLIER, 2 # Double damage underwater
171
- ```
172
-
173
- ## Key Design Principles
174
-
175
- ### 1. **Separation of Data and Logic**
176
- - Move stats (power, accuracy, PP) separate from behavior logic
177
- - Enables easy balancing without code changes
178
- - Clear data-driven approach
179
-
180
- ### 2. **Effect Reusability**
181
- - Many moves share the same effect type
182
- - New moves can reuse existing effects
183
- - Reduces code duplication
184
-
185
- ### 3. **Modular Flag System**
186
- ```c
187
- .flags = FLAG_MAKES_CONTACT | FLAG_PROTECT_AFFECTED | FLAG_MIRROR_MOVE_AFFECTED
188
- ```
189
- - Each flag represents an independent property
190
- - Easy to combine properties
191
- - Consistent interaction rules
192
-
193
- ### 4. **Script-Based Flexibility**
194
- - Complex logic implemented in battle scripts
195
- - Scripts can call other scripts (`goto`, `call`)
196
- - Allows for sophisticated move interactions
197
-
198
- ### 5. **Centralized Effect Handling**
199
- - All effect implementations in one location
200
- - Easy to debug and maintain
201
- - Consistent patterns across similar effects
202
-
203
- ## Implementation Recommendations
204
-
205
- For a Pokemon-style battle system:
206
-
207
- ### 1. **Start with Core Structure**
208
- ```c
209
- struct Move {
210
- u16 effect; // Links to effect implementation
211
- u8 power; // Base damage
212
- u8 type; // Element type
213
- u8 accuracy; // Hit chance
214
- u8 pp; // Usage count
215
- u8 secondaryChance; // Secondary effect probability
216
- u8 target; // Targeting rules
217
- s8 priority; // Speed priority
218
- u32 flags; // Behavior flags
219
- };
220
- ```
221
-
222
- ### 2. **Define Effect Categories**
223
- - Start with basic effects (HIT, SLEEP, POISON_HIT, ABSORB)
224
- - Add complex effects as needed
225
- - Group related effects together
226
-
227
- ### 3. **Implement Script System**
228
- - Use command-based scripts for flexibility
229
- - Implement basic commands first (damage, accuracy, animation)
230
- - Add advanced commands for complex interactions
231
-
232
- ### 4. **Use Flag-Based Properties**
233
- ```c
234
- #define FLAG_CONTACT (1 << 0)
235
- #define FLAG_PROTECTABLE (1 << 1)
236
- #define FLAG_REFLECTABLE (1 << 2)
237
- #define FLAG_KINGS_ROCK (1 << 3)
238
- ```
239
-
240
- ### 5. **Design for Extensibility**
241
- - Keep effect constants sequential for easy addition
242
- - Use lookup tables for effect-to-script mapping
243
- - Design script commands to be composable
244
-
245
- ## Special Effects Implementation
246
-
247
- ### Status Effects
248
- ```c
249
- // In move data:
250
- .effect = EFFECT_POISON_HIT,
251
- .secondaryEffectChance = 30,
252
-
253
- // In script:
254
- setmoveeffect MOVE_EFFECT_POISON // What status to apply
255
- goto BattleScript_EffectHit // How to apply it
256
- ```
257
-
258
- ### Multi-Hit Moves
259
- ```c
260
- // Requires special counter management
261
- setmultihitcounter 0 // Initialize
262
- BattleScript_MultiHitLoop:: // Loop label
263
- // Hit logic here
264
- decrementmultihit // Reduce counter
265
- jumpifnotdone BattleScript_MultiHitLoop // Continue if more hits
266
- ```
267
-
268
- ### Priority System
269
- ```c
270
- // In move data:
271
- .priority = 1, // +1 priority (Quick Attack)
272
- .priority = -6, // -6 priority (Counter)
273
-
274
- // Processed during move selection phase
275
- ```
276
-
277
- This architecture allows for clean separation between move definitions, their statistical properties, and their complex behavioral implementations, making it easy to add new moves while maintaining code organization and reusability.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pokemon_emerald_docs/battle_system_architecture.md DELETED
@@ -1,363 +0,0 @@
1
- # Pokemon Battle System Architecture Analysis
2
-
3
- ## Overview
4
-
5
- The Pokemon Emerald battle system is a complex, state-driven architecture that orchestrates combat between multiple Pokemon. Rather than being a single monolithic system, it's composed of several interconnected subsystems that handle different aspects of battle functionality.
6
-
7
- ## High-Level Architecture
8
-
9
- ### Core Components
10
-
11
- 1. **Battle State Machine** (`battle_main.c`)
12
- 2. **Battle Controllers** (`battle_controller_*.c` files)
13
- 3. **Animation System** (`battle_anim*.c` files)
14
- 4. **Script Engine** (`battle_script_commands.c`)
15
- 5. **User Interface** (`battle_interface.c`)
16
- 6. **Data Management** (Pokemon storage and battle state)
17
-
18
- ## 1. Battle State Management
19
-
20
- ### Main Battle Loop
21
- The battle system is built around a main function pointer (`gBattleMainFunc`) that controls the overall battle flow:
22
-
23
- ```c
24
- // Central battle state function pointer
25
- void (*gBattleMainFunc)(void);
26
- ```
27
-
28
- ### Key State Variables
29
- - `gBattleStruct` - Central battle state container
30
- - `gBattleMons[MAX_BATTLERS_COUNT]` - Active Pokemon data
31
- - `gBattlerPositions[]` - Battler position tracking
32
- - `gBattleTypeFlags` - Battle mode flags (wild, trainer, double, etc.)
33
-
34
- gBattleStruct definition:
35
- ```c
36
- struct BattleStruct
37
- {
38
- u8 turnEffectsTracker;
39
- u8 turnEffectsBattlerId;
40
- u8 unused_0;
41
- u8 turnCountersTracker;
42
- u8 wrappedMove[MAX_BATTLERS_COUNT * 2]; // Leftover from Ruby's ewram access.
43
- u8 moveTarget[MAX_BATTLERS_COUNT];
44
- u8 expGetterMonId;
45
- u8 unused_1;
46
- u8 wildVictorySong;
47
- u8 dynamicMoveType;
48
- u8 wrappedBy[MAX_BATTLERS_COUNT];
49
- u16 assistPossibleMoves[PARTY_SIZE * MAX_MON_MOVES]; // Each of mons can know max 4 moves.
50
- u8 focusPunchBattlerId;
51
- u8 battlerPreventingSwitchout;
52
- u8 moneyMultiplier;
53
- u8 savedTurnActionNumber;
54
- u8 switchInAbilitiesCounter;
55
- u8 faintedActionsState;
56
- u8 faintedActionsBattlerId;
57
- u16 expValue;
58
- u8 scriptPartyIdx; // for printing the nickname
59
- u8 sentInPokes;
60
- bool8 selectionScriptFinished[MAX_BATTLERS_COUNT];
61
- u8 battlerPartyIndexes[MAX_BATTLERS_COUNT];
62
- u8 monToSwitchIntoId[MAX_BATTLERS_COUNT];
63
- u8 battlerPartyOrders[MAX_BATTLERS_COUNT][PARTY_SIZE / 2];
64
- u8 runTries;
65
- u8 caughtMonNick[POKEMON_NAME_LENGTH + 1];
66
- u8 unused_2;
67
- u8 safariGoNearCounter;
68
- u8 safariPkblThrowCounter;
69
- u8 safariEscapeFactor;
70
- u8 safariCatchFactor;
71
- u8 linkBattleVsSpriteId_V; // The letter "V"
72
- u8 linkBattleVsSpriteId_S; // The letter "S"
73
- u8 formToChangeInto;
74
- u8 chosenMovePositions[MAX_BATTLERS_COUNT];
75
- u8 stateIdAfterSelScript[MAX_BATTLERS_COUNT];
76
- u8 unused_3[3];
77
- u8 prevSelectedPartySlot;
78
- u8 unused_4[2];
79
- u8 stringMoveType;
80
- u8 expGetterBattlerId;
81
- u8 unused_5;
82
- u8 absentBattlerFlags;
83
- u8 palaceFlags; // First 4 bits are "is <= 50% HP and not asleep" for each battler, last 4 bits are selected moves to pass to AI
84
- u8 field_93; // related to choosing pokemon?
85
- u8 wallyBattleState;
86
- u8 wallyMovesState;
87
- u8 wallyWaitFrames;
88
- u8 wallyMoveFrames;
89
- u8 lastTakenMove[MAX_BATTLERS_COUNT * 2 * 2]; // Last move that a battler was hit with. This field seems to erroneously take 16 bytes instead of 8.
90
- u16 hpOnSwitchout[NUM_BATTLE_SIDES];
91
- u32 savedBattleTypeFlags;
92
- u8 abilityPreventingSwitchout;
93
- u8 hpScale;
94
- u8 synchronizeMoveEffect;
95
- bool8 anyMonHasTransformed;
96
- void (*savedCallback)(void);
97
- u16 usedHeldItems[MAX_BATTLERS_COUNT];
98
- u8 chosenItem[MAX_BATTLERS_COUNT]; // why is this an u8?
99
- u8 AI_itemType[2];
100
- u8 AI_itemFlags[2];
101
- u16 choicedMove[MAX_BATTLERS_COUNT];
102
- u16 changedItems[MAX_BATTLERS_COUNT];
103
- u8 intimidateBattler;
104
- u8 switchInItemsCounter;
105
- u8 arenaTurnCounter;
106
- u8 turnSideTracker;
107
- u8 unused_6[3];
108
- u8 givenExpMons; // Bits for enemy party's Pokémon that gave exp to player's party.
109
- u8 lastTakenMoveFrom[MAX_BATTLERS_COUNT * MAX_BATTLERS_COUNT * 2]; // a 3-D array [target][attacker][byte]
110
- u16 castformPalette[NUM_CASTFORM_FORMS][16];
111
- union {
112
- struct LinkBattlerHeader linkBattlerHeader;
113
- u32 battleVideo[2];
114
- } multiBuffer;
115
- u8 wishPerishSongState;
116
- u8 wishPerishSongBattlerId;
117
- bool8 overworldWeatherDone;
118
- u8 atkCancellerTracker;
119
- struct BattleTvMovePoints tvMovePoints;
120
- struct BattleTv tv;
121
- u8 unused_7[0x28];
122
- u8 AI_monToSwitchIntoId[MAX_BATTLERS_COUNT];
123
- s8 arenaMindPoints[2];
124
- s8 arenaSkillPoints[2];
125
- u16 arenaStartHp[2];
126
- u8 arenaLostPlayerMons; // Bits for party member, lost as in referee's decision, not by fainting.
127
- u8 arenaLostOpponentMons;
128
- u8 alreadyStatusedMoveAttempt; // As bits for battlers; For example when using Thunder Wave on an already paralyzed Pokémon.
129
- };
130
- ```
131
-
132
- ### Battle Flow States
133
- The battle progresses through distinct phases:
134
- 1. **Initialization** - Setup battlers, load graphics
135
- 2. **Turn Selection** - Player/AI choose actions
136
- 3. **Action Resolution** - Execute moves in priority order
137
- 4. **Turn Cleanup** - Apply end-of-turn effects
138
- 5. **Battle End** - Victory/defeat/capture resolution
139
-
140
- ## 2. Controller Architecture
141
-
142
- ### Battler Controllers
143
- Each battler (player, opponent, partner, etc.) has its own controller that handles:
144
- - Input processing
145
- - Animation requests
146
- - Data updates
147
- - UI management
148
-
149
- ```c
150
- // Controller function pointer array
151
- void (*gBattlerControllerFuncs[MAX_BATTLERS_COUNT])(void);
152
- ```
153
-
154
- ### Controller Types
155
- - **Player Controller** (`battle_controller_player.c`) - Human input
156
- - **Opponent Controller** (`battle_controller_opponent.c`) - AI decisions
157
- - **Link Controllers** - Network battlers
158
- - **Special Controllers** - Safari Zone, Wally, etc.
159
-
160
- ### Command System
161
- Controllers communicate via command buffers:
162
- ```c
163
- // Controller commands (simplified)
164
- CONTROLLER_GETMONDATA // Request Pokemon data
165
- CONTROLLER_CHOOSEACTION // Select battle action
166
- CONTROLLER_MOVEANIMATION // Play move animation
167
- CONTROLLER_HEALTHBARUPDATE // Update HP display
168
- ```
169
-
170
- ## 3. Animation System
171
-
172
- ### Animation Pipeline
173
- Animations are script-driven and hierarchical:
174
-
175
- 1. **Move Animation Selection** - Choose appropriate animation
176
- 2. **Script Execution** - Run animation script commands
177
- 3. **Sprite Management** - Create/animate visual elements
178
- 4. **Audio Synchronization** - Play sound effects
179
- 5. **Cleanup** - Remove temporary sprites
180
-
181
- ### Animation Scripts
182
- Each move has an associated animation script:
183
- ```c
184
- const u8 *const gBattleAnims_Moves[];
185
- ```
186
-
187
- ### Animation Commands
188
- Scripts use specialized commands:
189
- - `loadspritegfx` - Load sprite graphics
190
- - `createsprite` - Create animated sprite
191
- - `playse` - Play sound effect
192
- - `delay` - Wait specified frames
193
- - `monbg` - Modify Pokemon background
194
-
195
- ### Visual Task System
196
- Complex animations use visual tasks for frame-by-frame control:
197
- ```c
198
- u8 gAnimVisualTaskCount; // Active visual task counter
199
- ```
200
-
201
- ## 4. Pokemon Data Storage and Retrieval
202
-
203
- ### Battle Pokemon Structure
204
- Active Pokemon are stored in `gBattleMons[]`:
205
- ```c
206
- struct BattlePokemon {
207
- u16 species;
208
- u16 attack;
209
- u16 defense;
210
- u16 speed;
211
- u16 spAttack;
212
- u16 spDefense;
213
- u16 moves[MAX_MON_MOVES];
214
- u32 hp;
215
- u8 level;
216
- u8 pp[MAX_MON_MOVES];
217
- // ... status, abilities, etc.
218
- };
219
- ```
220
-
221
- ### Data Synchronization
222
- The system maintains consistency between:
223
- - **Party Data** - Full Pokemon in party array
224
- - **Battle Data** - Reduced battle-specific data
225
- - **Display Data** - UI representation
226
-
227
- ### Data Flow
228
- 1. **Load Phase** - Copy party data to battle structure
229
- 2. **Battle Phase** - Modify battle data only
230
- 3. **Update Phase** - Sync changes back to party
231
-
232
- ## 5. Menu Systems and Input Handling
233
-
234
- ### Action Selection Menu
235
- The main battle menu uses a state-driven approach:
236
-
237
- ```c
238
- static void PlayerHandleChooseAction(void) {
239
- // Present: Fight, Pokemon, Bag, Run options
240
- // Handle input and set action type
241
- }
242
- ```
243
-
244
- ### Move Selection Interface
245
- Move selection involves:
246
- 1. **Display Moves** - Show available moves with PP
247
- 2. **Input Handling** - Process directional input
248
- 3. **Move Info** - Display type, power, accuracy
249
- 4. **Target Selection** - Choose targets for multi-target moves
250
-
251
- ### Menu State Management
252
- - Cursor position tracking
253
- - Input validation
254
- - Visual feedback (highlighting, animations)
255
- - Transition between menu levels
256
-
257
- ## 6. Action Processing and Effect Application
258
-
259
- ### Turn Order Resolution
260
- Actions are sorted by priority:
261
- 1. **Switch Pokemon** (highest priority)
262
- 2. **Use Items**
263
- 3. **Use Moves** (sorted by speed/priority)
264
- 4. **Flee**
265
-
266
- ### Move Execution Pipeline
267
- When a move is used:
268
-
269
- 1. **Validation** - Check if move can be used
270
- 2. **Target Selection** - Determine valid targets
271
- 3. **Script Execution** - Run move's battle script
272
- 4. **Animation** - Play visual/audio effects
273
- 5. **Damage Calculation** - Apply damage formulas
274
- 6. **Effect Application** - Apply status effects, stat changes
275
- 7. **Cleanup** - Update battle state
276
-
277
- ### Battle Script System
278
- Moves use script commands for effects:
279
- ```c
280
- static void Cmd_attackstring(void); // Display attack message
281
- static void Cmd_attackanimation(void); // Play attack animation
282
- static void Cmd_damagecalc(void); // Calculate damage
283
- static void Cmd_healthbarupdate(void); // Update HP bar
284
- ```
285
-
286
- ### Gradual Information Delivery
287
- The system delivers information to players progressively:
288
-
289
- 1. **Action Announcement** - "Pokemon used Move!"
290
- 2. **Animation Phase** - Visual move animation
291
- 3. **Result Messages** - Effectiveness, critical hits
292
- 4. **Damage Application** - Gradual HP bar drain
293
- 5. **Status Updates** - Additional effects
294
- 6. **Turn Cleanup** - End-of-turn effects
295
-
296
- ## 7. Key Programming Patterns
297
-
298
- ### State Machine Architecture
299
- The battle system extensively uses function pointers for state management:
300
- ```c
301
- void (*gBattleMainFunc)(void); // Main battle state
302
- void (*gBattlerControllerFuncs[])(void); // Controller states
303
- void (*gAnimScriptCallback)(void); // Animation states
304
- ```
305
-
306
- ### Command-Driven Controllers
307
- Controllers process commands via lookup tables:
308
- ```c
309
- static void (*const sPlayerBufferCommands[])(void) = {
310
- [CONTROLLER_GETMONDATA] = PlayerHandleGetMonData,
311
- [CONTROLLER_CHOOSEACTION] = PlayerHandleChooseAction,
312
- // ...
313
- };
314
- ```
315
-
316
- ### Script-Based Effects
317
- Both animations and move effects use bytecode scripts for flexibility:
318
- - Animation scripts control visual presentation
319
- - Battle scripts control game logic and effects
320
-
321
- ### Asynchronous Processing
322
- The system handles multiple concurrent operations:
323
- - Animations can run while processing user input
324
- - Multiple battlers can be in different states
325
- - Background tasks handle gradual effects (HP drain, etc.)
326
-
327
- ## 8. Memory Management
328
-
329
- ### Battle Resources Structure
330
- ```c
331
- struct BattleResources {
332
- struct SecretBase *secretBase;
333
- struct ResourceFlags *flags;
334
- struct BattleScriptsStack *battleScriptsStack;
335
- struct AI_ThinkingStruct *ai;
336
- struct BattleHistory *battleHistory;
337
- };
338
- ```
339
-
340
- ### Dynamic Allocation
341
- - Battle resources are allocated at battle start
342
- - Freed when battle ends
343
- - Sprites and animations use temporary allocation
344
-
345
- ## 9. Extension Points
346
-
347
- The architecture provides several extension mechanisms:
348
- - **New Move Effects** - Add scripts to move effect table
349
- - **Custom Animations** - Create new animation scripts
350
- - **Battle Types** - Add flags and specialized controllers
351
- - **AI Behaviors** - Extend AI decision trees
352
-
353
- ## Summary
354
-
355
- The Pokemon battle system demonstrates sophisticated game architecture through:
356
-
357
- 1. **Separation of Concerns** - Controllers, animations, scripts, and UI are distinct
358
- 2. **Data-Driven Design** - Moves, animations, and effects defined in data tables
359
- 3. **State Management** - Clear state machines for battle flow and individual components
360
- 4. **Asynchronous Processing** - Multiple concurrent operations with proper coordination
361
- 5. **Extensibility** - Modular design allows for easy addition of new content
362
-
363
- This architecture allows complex battle interactions while maintaining code organization and enabling the rich, interactive experience that defines Pokemon battles.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pokemon_emerald_docs/battle_system_mechanics.md DELETED
@@ -1,475 +0,0 @@
1
- # Pokémon Emerald Battle System Mechanics
2
-
3
- This document provides an in-depth technical reference for the underlying mechanics of the Pokémon Emerald battle system, including status effects, abilities, move mechanics, and turn execution flow.
4
-
5
- ## Table of Contents
6
-
7
- 1. [Status Effects](#status-effects)
8
- 2. [Type Effectiveness System](#type-effectiveness-system)
9
- 3. [Special Abilities](#special-abilities)
10
- 4. [Move Mechanics](#move-mechanics)
11
- 5. [Turn Execution Flow](#turn-execution-flow)
12
- 6. [Battle State Management](#battle-state-management)
13
-
14
- ## Status Effects
15
-
16
- ### Overview
17
-
18
- Status effects in Pokémon Emerald are divided into three categories, each stored in separate bit fields within the `BattlePokemon` structure:
19
-
20
- - **STATUS1**: Non-volatile status conditions that persist outside of battle
21
- - **STATUS2**: Volatile status conditions that are removed when switching
22
- - **STATUS3**: Additional volatile effects and special states
23
-
24
- ### Non-Volatile Status (STATUS1)
25
-
26
- Defined in `include/constants/battle.h:114-125`
27
-
28
- ```c
29
- #define STATUS1_SLEEP (1 << 0 | 1 << 1 | 1 << 2) // 3 bits for turn count
30
- #define STATUS1_POISON (1 << 3)
31
- #define STATUS1_BURN (1 << 4)
32
- #define STATUS1_FREEZE (1 << 5)
33
- #define STATUS1_PARALYSIS (1 << 6)
34
- #define STATUS1_TOXIC_POISON (1 << 7)
35
- #define STATUS1_TOXIC_COUNTER (1 << 8 | 1 << 9 | 1 << 10 | 1 << 11) // 4 bits for toxic counter
36
- ```
37
-
38
- #### Sleep
39
- - **Duration**: 2-5 turns (stored in first 3 bits)
40
- - **Effect**: Prevents most actions except Snore and Sleep Talk
41
- - **Wake-up**: Checked at the start of the turn before action
42
-
43
- #### Poison
44
- - **Damage**: 1/8 of max HP at end of turn
45
- - **Field Effect**: Takes damage every 4 steps in overworld
46
-
47
- #### Burn
48
- - **Damage**: 1/8 of max HP at end of turn
49
- - **Attack Reduction**: Physical attack reduced to 50%
50
- - **Immunity**: Fire-type Pokémon cannot be burned
51
-
52
- #### Freeze
53
- - **Thaw Chance**: 20% at the start of each turn
54
- - **Instant Thaw**: Fire-type moves, Scald, or being hit by a Fire move
55
- - **Immunity**: Ice-type Pokémon cannot be frozen
56
-
57
- #### Paralysis
58
- - **Speed Reduction**: Speed reduced to 25% of normal
59
- - **Full Paralysis**: 25% chance to be unable to move
60
- - **Immunity**: Electric-type Pokémon cannot be paralyzed
61
-
62
- #### Toxic Poison
63
- - **Progressive Damage**: 1/16 HP on turn 1, 2/16 on turn 2, etc.
64
- - **Counter Storage**: Uses bits 8-11 to track damage progression
65
-
66
- ### Volatile Status (STATUS2)
67
-
68
- Defined in `include/constants/battle.h:129-155`
69
-
70
- ```c
71
- #define STATUS2_CONFUSION (1 << 0 | 1 << 1 | 1 << 2) // 3 bits for turn count
72
- #define STATUS2_FLINCHED (1 << 3)
73
- #define STATUS2_UPROAR (1 << 4 | 1 << 5 | 1 << 6) // 3 bits for turn count
74
- #define STATUS2_BIDE (1 << 8 | 1 << 9) // 2 bits for turn count
75
- #define STATUS2_LOCK_CONFUSE (1 << 10 | 1 << 11) // Thrash/Outrage confusion
76
- #define STATUS2_MULTIPLETURNS (1 << 12)
77
- #define STATUS2_WRAPPED (1 << 13 | 1 << 14 | 1 << 15) // 3 bits for turn count
78
- #define STATUS2_INFATUATION (1 << 16 | 1 << 17 | 1 << 18 | 1 << 19) // 4 bits, one per battler
79
- #define STATUS2_FOCUS_ENERGY (1 << 20)
80
- #define STATUS2_TRANSFORMED (1 << 21)
81
- #define STATUS2_RECHARGE (1 << 22)
82
- #define STATUS2_RAGE (1 << 23)
83
- #define STATUS2_SUBSTITUTE (1 << 24)
84
- #define STATUS2_DESTINY_BOND (1 << 25)
85
- #define STATUS2_ESCAPE_PREVENTION (1 << 26)
86
- #define STATUS2_NIGHTMARE (1 << 27)
87
- #define STATUS2_CURSED (1 << 28)
88
- #define STATUS2_FORESIGHT (1 << 29)
89
- #define STATUS2_DEFENSE_CURL (1 << 30)
90
- #define STATUS2_TORMENT (1 << 31)
91
- ```
92
-
93
- #### Key Volatile Effects
94
-
95
- **Confusion**
96
- - **Duration**: 2-5 turns
97
- - **Effect**: 50% chance to hit self with 40 power typeless physical attack
98
- - **Self-damage Calculation**: Uses attack stat against defense stat
99
-
100
- **Substitute**
101
- - **HP Cost**: 25% of max HP to create
102
- - **Effect**: Blocks most status moves and damage
103
- - **Breaking**: Substitute breaks when its HP reaches 0
104
-
105
- **Infatuation**
106
- - **Effect**: 50% chance to be immobilized by love
107
- - **Requirement**: Opposite gender (or same gender with certain abilities)
108
- - **Storage**: Uses 4 bits to track which battler caused infatuation
109
-
110
- ### Additional Status (STATUS3)
111
-
112
- Defined in `include/constants/battle.h:158-178`
113
-
114
- ```c
115
- #define STATUS3_LEECHSEED (1 << 2)
116
- #define STATUS3_ALWAYS_HITS (1 << 3 | 1 << 4) // Lock-On/Mind Reader
117
- #define STATUS3_PERISH_SONG (1 << 5)
118
- #define STATUS3_ON_AIR (1 << 6) // Fly/Bounce
119
- #define STATUS3_UNDERGROUND (1 << 7) // Dig
120
- #define STATUS3_MINIMIZED (1 << 8)
121
- #define STATUS3_CHARGED_UP (1 << 9) // Charge
122
- #define STATUS3_ROOTED (1 << 10) // Ingrain
123
- #define STATUS3_YAWN (1 << 11 | 1 << 12) // Turn counter
124
- #define STATUS3_IMPRISONED_OTHERS (1 << 13)
125
- #define STATUS3_GRUDGE (1 << 14)
126
- #define STATUS3_UNDERWATER (1 << 18) // Dive
127
- #define STATUS3_SEMI_INVULNERABLE (STATUS3_UNDERGROUND | STATUS3_ON_AIR | STATUS3_UNDERWATER)
128
- ```
129
-
130
- ## Type Effectiveness System
131
-
132
- ### Type Chart Implementation
133
-
134
- Type effectiveness is stored in `gTypeEffectiveness[]` array in `src/battle_main.c`. The format is:
135
-
136
- ```c
137
- [Attacking Type, Defending Type, Multiplier, ...]
138
- ```
139
-
140
- Multiplier values:
141
- - `TYPE_MUL_NO_EFFECT` (0): No damage (0x multiplier)
142
- - `TYPE_MUL_NOT_EFFECTIVE` (5): Not very effective (0.5x multiplier)
143
- - `TYPE_MUL_NORMAL` (10): Normal damage (1x multiplier)
144
- - `TYPE_MUL_SUPER_EFFECTIVE` (20): Super effective (2x multiplier)
145
-
146
- ### Ground vs Flying Interaction
147
-
148
- The type chart includes:
149
- ```c
150
- TYPE_GROUND, TYPE_FLYING, TYPE_MUL_NO_EFFECT
151
- ```
152
-
153
- This is why Earthquake and other Ground-type moves don't affect Flying-type Pokémon - they receive a 0x damage multiplier.
154
-
155
- ### Type Calculation Process
156
-
157
- 1. **Base Calculation**: In `Cmd_typecalc()` function
158
- 2. **Ability Checks**: Levitate grants immunity to Ground moves
159
- 3. **Item Effects**: Type-enhancing items modify damage
160
- 4. **STAB Calculation**: Same Type Attack Bonus (1.5x) if move type matches user type
161
-
162
- ## Special Abilities
163
-
164
- ### Ability System Overview
165
-
166
- Abilities are passive effects that can trigger at various points during battle. They are defined in `include/constants/abilities.h` and processed by `AbilityBattleEffects()` in `src/battle_util.c`.
167
-
168
- ### Key Ability: Levitate
169
-
170
- ```c
171
- #define ABILITY_LEVITATE 26
172
- ```
173
-
174
- **Effect**: Grants immunity to Ground-type moves
175
- **Implementation**: Checked in multiple locations:
176
- - `Cmd_typecalc()` - During damage calculation
177
- - `CheckWonderGuardAndLevitate()` - For ability-specific immunity
178
- - Move target validation
179
-
180
- ### Ability Processing Points
181
-
182
- Abilities can trigger at:
183
- 1. **Switch-in**: Intimidate, Drizzle, Drought, Sand Stream
184
- 2. **Before Move**: Truant preventing action
185
- 3. **Damage Calculation**: Thick Fat, Filter, Solid Rock
186
- 4. **After Damage**: Rough Skin, Iron Barbs, Aftermath
187
- 5. **Status Application**: Immunity preventing poison
188
- 6. **End of Turn**: Speed Boost, Poison Heal
189
-
190
- ## Move Mechanics
191
-
192
- ### Move Data Structure
193
-
194
- From `include/pokemon.h:327-338`:
195
-
196
- ```c
197
- struct BattleMove
198
- {
199
- u8 effect; // Move effect ID
200
- u8 power; // Base power
201
- u8 type; // Move type
202
- u8 accuracy; // Accuracy (0-100)
203
- u8 pp; // Base PP
204
- u8 secondaryEffectChance; // % chance for secondary effect
205
- u8 target; // Target selection
206
- s8 priority; // Priority bracket (-7 to +5)
207
- u8 flags; // Move properties flags
208
- };
209
- ```
210
-
211
- ### Move Flags
212
-
213
- From `include/constants/pokemon.h:208-213`:
214
-
215
- ```c
216
- #define FLAG_MAKES_CONTACT (1 << 0) // Triggers contact abilities
217
- #define FLAG_PROTECT_AFFECTED (1 << 1) // Blocked by Protect/Detect
218
- #define FLAG_MAGIC_COAT_AFFECTED (1 << 2) // Reflected by Magic Coat
219
- #define FLAG_SNATCH_AFFECTED (1 << 3) // Stolen by Snatch
220
- #define FLAG_MIRROR_MOVE_AFFECTED (1 << 4) // Copyable by Mirror Move
221
- #define FLAG_KINGS_ROCK_AFFECTED (1 << 5) // Can cause flinch with King's Rock
222
- ```
223
-
224
- ### Protect/Detect Mechanics
225
-
226
- **Implementation**: `Cmd_setprotectlike()` in `src/battle_script_commands.c`
227
-
228
- 1. **Success Rate Calculation**:
229
- - First use: 100% success
230
- - Consecutive uses: Success rate halves each time
231
- - Stored in `sProtectSuccessRates[]` array
232
-
233
- 2. **Protection State**:
234
- ```c
235
- gProtectStructs[battler].protected = 1; // For Protect/Detect
236
- gProtectStructs[battler].endured = 1; // For Endure
237
- ```
238
-
239
- 3. **Move Blocking**:
240
- - Checked in `Cmd_attackcanceler()` via `DEFENDER_IS_PROTECTED` macro
241
- - Only moves with `FLAG_PROTECT_AFFECTED` are blocked
242
- - Some moves bypass Protect (Feint, Shadow Force, etc.)
243
-
244
- ### Multi-Target Move Mechanics
245
-
246
- Target selection types:
247
- - `MOVE_TARGET_SELECTED`: Single target chosen by player
248
- - `MOVE_TARGET_BOTH`: Hits both opponents in double battles
249
- - `MOVE_TARGET_FOES_AND_ALLY`: Hits all Pokémon except user
250
- - `MOVE_TARGET_ALL_BATTLERS`: Hits all Pokémon including user
251
-
252
- Damage reduction in multi-battles:
253
- - Moves hitting multiple targets deal 75% damage to each
254
-
255
- ## Turn Execution Flow
256
-
257
- ### Complete Turn Sequence
258
-
259
- The battle system executes turns through a series of carefully ordered phases:
260
-
261
- #### 1. Action Selection Phase
262
- **Function**: `HandleTurnActionSelectionState()`
263
-
264
- - Players and AI select actions (Fight/Pokémon/Bag/Run)
265
- - Move and target selection for Fight actions
266
- - All selections stored in `gChosenActionByBattler[]`
267
-
268
- #### 2. Turn Order Determination
269
- **Function**: `SetActionsAndBattlersTurnOrder()`
270
-
271
- Order priority:
272
- 1. **Pursuit** (if target is switching)
273
- 2. **Switching Pokémon**
274
- 3. **Using Items**
275
- 4. **Quick Claw activation** (20% chance to go first)
276
- 5. **Move Priority** (-7 to +5, higher goes first)
277
- 6. **Speed Stat** (higher goes first, with Speed ties being random)
278
-
279
- Arrays populated:
280
- - `gActionsByTurnOrder[]` - What action each slot will perform
281
- - `gBattlerByTurnOrder[]` - Which battler occupies each slot
282
-
283
- #### 3. Pre-Turn Checks
284
- **Function**: `CheckFocusPunch_ClearVarsBeforeTurnStarts()`
285
-
286
- - Focus Punch charging message
287
- - Clear temporary battle variables
288
- - Initialize turn counters
289
-
290
- #### 4. Action Execution
291
- **Function**: `RunTurnActionsFunctions()`
292
-
293
- For each action in turn order:
294
-
295
- ##### Move Execution Pipeline
296
- When `B_ACTION_USE_MOVE` is processed:
297
-
298
- 1. **Attack Canceler** (`Cmd_attackcanceler`)
299
- - Sleep check (can only use Snore/Sleep Talk)
300
- - Freeze check (20% thaw chance)
301
- - Paralysis check (25% full paralysis)
302
- - Confusion check (50% self-hit)
303
- - Flinch check
304
- - Disable/Taunt/Imprison checks
305
- - Protect check (for protected targets)
306
- - Pressure PP deduction
307
-
308
- 2. **Accuracy Check** (`Cmd_accuracycheck`)
309
- - Base accuracy calculation
310
- - Accuracy/Evasion stat stages
311
- - Ability modifications (Compound Eyes, Sand Veil)
312
- - Weather effects (Thunder in rain)
313
-
314
- 3. **Attack String** (`Cmd_attackstring`)
315
- - Display "[Pokémon] used [Move]!"
316
-
317
- 4. **PP Reduction** (`Cmd_ppreduce`)
318
- - Deduct PP from move (affected by Pressure)
319
-
320
- 5. **Critical Hit Calculation** (`Cmd_critcalc`)
321
- - Base 1/16 chance (6.25%)
322
- - Increased by Focus Energy, high crit moves, items
323
- - Cannot crit if target has Battle Armor/Shell Armor
324
-
325
- 6. **Damage Calculation** (`Cmd_damagecalc`)
326
- ```
327
- Damage = ((2 × Level / 5 + 2) × Power × Attack / Defense / 50 + 2)
328
- × STAB × Type × Random(85-100)/100
329
- ```
330
-
331
- 7. **Type Effectiveness** (`Cmd_typecalc`)
332
- - Apply type chart multipliers
333
- - Check abilities (Levitate, Wonder Guard)
334
- - Display effectiveness messages
335
-
336
- 8. **Attack Animation** (`Cmd_attackanimation`)
337
- - Play move animation and sound effects
338
-
339
- 9. **HP Update** (`Cmd_healthbarupdate`)
340
- - Animate HP bar decrease
341
- - Apply Substitute damage if applicable
342
-
343
- 10. **Result Messages** (`Cmd_resultmessage`)
344
- - "It's super effective!"
345
- - "It's not very effective..."
346
- - Critical hit notification
347
-
348
- 11. **Secondary Effects** (`Cmd_moveend`)
349
- - Status conditions
350
- - Stat changes
351
- - Additional effects
352
-
353
- ##### Other Action Types
354
-
355
- **B_ACTION_SWITCH**:
356
- - Pursuit check and execution
357
- - Return Pokémon
358
- - Send out new Pokémon
359
- - Entry hazard damage
360
- - Switch-in abilities
361
-
362
- **B_ACTION_USE_ITEM**:
363
- - Item effect application
364
- - Item consumption
365
- - Bag pocket update
366
-
367
- **B_ACTION_RUN**:
368
- - Escape attempt calculation
369
- - Battle end if successful
370
-
371
- #### 5. End of Turn Phase
372
- **Function**: `HandleEndTurn_BattleTerrain()`
373
-
374
- Processes in order:
375
- 1. **Future Sight/Doom Desire** damage
376
- 2. **Wish** healing
377
- 3. **Weather** damage (Sandstorm, Hail)
378
- 4. **Status** damage (Burn, Poison, Toxic)
379
- 5. **Leech Seed** HP drain
380
- 6. **Nightmare** damage
381
- 7. **Curse** damage
382
- 8. **Wrap/Bind** damage
383
- 9. **Uproar** wake-up and prevention
384
- 10. **Perish Song** counter
385
- 11. **Reflect/Light Screen** duration
386
- 12. **Safeguard/Mist** duration
387
- 13. **Trick Room** duration (Gen 4+ feature)
388
- 14. **Gravity** duration (Gen 4+ feature)
389
- 15. **Item effects** (Leftovers, Black Sludge)
390
- 16. **Ability effects** (Speed Boost, Moody)
391
-
392
- #### 6. Faint Handling
393
- **Function**: `HandleFaintedMonActions()`
394
-
395
- - Check for fainted Pokémon
396
- - Experience gain calculation
397
- - EVs distribution
398
- - Display faint messages
399
- - Force switches if needed
400
-
401
- #### 7. Turn Wrap-up
402
-
403
- - Increment turn counter
404
- - Check win/loss conditions
405
- - Return to Action Selection or end battle
406
-
407
- ### Battle State Machine
408
-
409
- The entire battle flow is controlled by function pointers:
410
-
411
- ```c
412
- void (*gBattleMainFunc)(void); // Main battle state
413
- ```
414
-
415
- Key states:
416
- - `HandleNewBattleRamData` - Initialize battle
417
- - `TryDoEventsBeforeFirstTurn` - Entry hazards, weather
418
- - `HandleTurnActionSelectionState` - Action selection
419
- - `SetActionsAndBattlersTurnOrder` - Sort actions
420
- - `RunTurnActionsFunctions` - Execute turn
421
- - `HandleEndTurn_FinishBattle` - Battle cleanup
422
-
423
- ## Battle State Management
424
-
425
- ### Core Data Structures
426
-
427
- **BattlePokemon** - Active Pokémon data:
428
- ```c
429
- struct BattlePokemon {
430
- u16 species;
431
- u16 attack, defense, speed, spAttack, spDefense;
432
- u16 moves[MAX_MON_MOVES];
433
- u32 hp, maxHP;
434
- u32 status1; // Non-volatile status
435
- u32 status2; // Volatile status
436
- u32 status3; // Additional effects
437
- u8 ability;
438
- u8 type1, type2;
439
- // ... more fields
440
- };
441
- ```
442
-
443
- **BattleStruct** - Global battle state:
444
- - Turn counters and trackers
445
- - Move history
446
- - Field effects
447
- - Battle mode flags
448
- - AI data
449
-
450
- ### Memory Layout
451
-
452
- Battle data is organized into:
453
- - **EWRAM**: Fast access for frequently used data
454
- - **Battle Resources**: Dynamically allocated at battle start
455
- - **Sprite Data**: Separate allocation for graphics
456
-
457
- ### Save State Integration
458
-
459
- During battle:
460
- - Party Pokémon are copied to battle structures
461
- - Changes applied to battle copies only
462
- - On battle end, sync back to party (HP, PP, status, etc.)
463
-
464
- This architecture ensures battle calculations don't corrupt party data and allows for move preview without committing changes.
465
-
466
- ## Summary
467
-
468
- The Pokémon Emerald battle system is a sophisticated state machine that processes turns through clearly defined phases. Its modular architecture separates concerns between controllers (input), scripts (logic), animations (presentation), and data management. The extensive use of bit flags for status conditions and move properties allows for complex interactions while maintaining performance on GBA hardware.
469
-
470
- Key design principles:
471
- - **Deterministic**: Same inputs produce same results (except RNG)
472
- - **Modular**: Each phase has clear responsibilities
473
- - **Extensible**: New effects can be added via scripts
474
- - **Efficient**: Bit manipulation and careful memory management
475
- - **Faithful**: Accurately replicates original game mechanics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/App.svelte CHANGED
@@ -6,7 +6,7 @@
6
  import ProgressBar from './lib/components/Layout/ProgressBar.svelte';
7
  import TabBar, { type TabId } from './lib/components/Layout/TabBar.svelte';
8
  import Scanner from './lib/components/Pages/Scanner.svelte';
9
- import Encounters from './lib/components/Pages/Encounters.svelte';
10
  import Pictuary from './lib/components/Pages/Pictuary.svelte';
11
  import type { HuggingFaceLibs, GradioLibs, GradioClient } from './lib/types';
12
  // import { setQwenClientResetter } from './lib/utils/qwenTimeout'; // Unused since qwen is disabled
@@ -32,7 +32,7 @@
32
  // Tab names mapping
33
  const tabNames: Record<TabId, string> = {
34
  scanner: 'Scanner',
35
- encounters: 'Encounters',
36
  pictuary: 'Pictuary'
37
  };
38
 
@@ -175,8 +175,8 @@
175
  {joyCaptionClient}
176
  {qwenClient}
177
  />
178
- {:else if activeTab === 'encounters'}
179
- <Encounters />
180
  {:else if activeTab === 'pictuary'}
181
  <Pictuary />
182
  {/if}
 
6
  import ProgressBar from './lib/components/Layout/ProgressBar.svelte';
7
  import TabBar, { type TabId } from './lib/components/Layout/TabBar.svelte';
8
  import Scanner from './lib/components/Pages/Scanner.svelte';
9
+ import Activity from './lib/components/Pages/Activity.svelte';
10
  import Pictuary from './lib/components/Pages/Pictuary.svelte';
11
  import type { HuggingFaceLibs, GradioLibs, GradioClient } from './lib/types';
12
  // import { setQwenClientResetter } from './lib/utils/qwenTimeout'; // Unused since qwen is disabled
 
32
  // Tab names mapping
33
  const tabNames: Record<TabId, string> = {
34
  scanner: 'Scanner',
35
+ activity: 'Activity',
36
  pictuary: 'Pictuary'
37
  };
38
 
 
175
  {joyCaptionClient}
176
  {qwenClient}
177
  />
178
+ {:else if activeTab === 'activity'}
179
+ <Activity />
180
  {:else if activeTab === 'pictuary'}
181
  <Pictuary />
182
  {/if}
src/lib/components/Battle/ActionButtons.svelte DELETED
@@ -1,113 +0,0 @@
1
- <script lang="ts">
2
- import ActionViewSelector, { type ActionView } from './ActionViewSelector.svelte';
3
- import type { PicletInstance, BattleMove } from '$lib/db/schema';
4
- import type { BattleState } from '$lib/battle-engine/types';
5
- import { getUnlockedMoves } from '$lib/services/unlockLevels';
6
-
7
- export let isWildBattle: boolean;
8
- export let playerPiclet: PicletInstance;
9
- export let enemyPiclet: PicletInstance | null = null;
10
- export let availablePiclets: PicletInstance[] = [];
11
- export let processingTurn: boolean = false;
12
- export let battleState: BattleState | undefined = undefined;
13
- export let capturePercentage: number = 0;
14
- export let onAction: (action: string) => void;
15
- export let onMoveSelect: (move: BattleMove) => void = () => {};
16
- export let onPicletSelect: (piclet: PicletInstance) => void = () => {};
17
-
18
- let currentView: ActionView = 'main';
19
-
20
- // Only show unlocked moves in battle
21
- $: unlockedMoves = getUnlockedMoves(playerPiclet.moves, playerPiclet.level);
22
-
23
- function handleViewChange(view: ActionView) {
24
- currentView = view;
25
- }
26
-
27
- function handleMoveSelected(move: BattleMove) {
28
- currentView = 'main';
29
- onMoveSelect(move);
30
- }
31
-
32
- function handlePicletSelected(piclet: PicletInstance) {
33
- currentView = 'main';
34
- onPicletSelect(piclet);
35
- }
36
-
37
- function handleCaptureAttempt() {
38
- currentView = 'main';
39
- onAction('catch');
40
- }
41
-
42
- // Add Run button outside of the action selector
43
- function handleRun() {
44
- onAction('run');
45
- }
46
- </script>
47
-
48
- <div class="battle-actions">
49
- <ActionViewSelector
50
- {currentView}
51
- onViewChange={handleViewChange}
52
- moves={unlockedMoves}
53
- {availablePiclets}
54
- {enemyPiclet}
55
- {isWildBattle}
56
- {battleState}
57
- {capturePercentage}
58
- onMoveSelected={handleMoveSelected}
59
- onPicletSelected={handlePicletSelected}
60
- onCaptureAttempt={handleCaptureAttempt}
61
- currentPicletId={playerPiclet.id}
62
- {processingTurn}
63
- />
64
-
65
- <!-- Run button (always visible) -->
66
- <button
67
- class="run-button"
68
- on:click={handleRun}
69
- disabled={processingTurn}
70
- >
71
- <span class="run-icon">🏃</span>
72
- <span>Run</span>
73
- </button>
74
- </div>
75
-
76
- <style>
77
- .battle-actions {
78
- display: flex;
79
- flex-direction: column;
80
- gap: 12px;
81
- height: 100%;
82
- }
83
-
84
- .run-button {
85
- display: flex;
86
- align-items: center;
87
- justify-content: center;
88
- gap: 8px;
89
- padding: 12px 24px;
90
- background: #ff3b30;
91
- color: white;
92
- border: none;
93
- border-radius: 12px;
94
- font-size: 17px;
95
- font-weight: 500;
96
- cursor: pointer;
97
- transition: all 0.2s;
98
- }
99
-
100
- .run-button:active:not(:disabled) {
101
- transform: scale(0.95);
102
- background: #d70015;
103
- }
104
-
105
- .run-button:disabled {
106
- opacity: 0.5;
107
- cursor: not-allowed;
108
- }
109
-
110
- .run-icon {
111
- font-size: 20px;
112
- }
113
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/ActionViewSelector.svelte DELETED
@@ -1,508 +0,0 @@
1
- <script context="module" lang="ts">
2
- export type ActionView = 'main' | 'moves' | 'piclets' | 'stats' | 'forcedSwap';
3
- </script>
4
-
5
- <script lang="ts">
6
- import type { PicletInstance, BattleMove } from '$lib/db/schema';
7
- import type { BattleState } from '$lib/battle-engine/types';
8
- import { generateMoveDescription } from '$lib/utils/moveDescriptions';
9
- import { getCaptureDescription } from '$lib/services/captureService';
10
-
11
- export let currentView: ActionView = 'main';
12
- export let onViewChange: (view: ActionView) => void;
13
- export let moves: BattleMove[] = [];
14
- export let availablePiclets: PicletInstance[] = [];
15
- export let enemyPiclet: PicletInstance | null = null;
16
- export let isWildBattle: boolean = false;
17
- export let battleState: BattleState | undefined = undefined;
18
- export let capturePercentage: number = 0;
19
- export let onMoveSelected: (move: BattleMove) => void = () => {};
20
- export let onPicletSelected: (piclet: PicletInstance) => void = () => {};
21
- export let onCaptureAttempt: () => void = () => {};
22
- export let currentPicletId: number | null = null;
23
- export let processingTurn: boolean = false;
24
-
25
- // Enhanced move information from battle state
26
- $: enhancedMoves = battleState?.playerPiclet?.moves || [];
27
-
28
- // Helper function to get type logo path
29
- function getTypeLogo(type: string): string {
30
- return `/classes/${type}.png`;
31
- }
32
-
33
- // Main action items
34
- const actions = [
35
- { title: 'Act', icon: '⚔️', view: 'moves' as ActionView },
36
- { title: 'Piclets', icon: '🔄', view: 'piclets' as ActionView },
37
- // { title: 'Stats', icon: '📊', view: 'stats' as ActionView }, // Debug only
38
- ];
39
-
40
- function handleActionClick(targetView: ActionView) {
41
- if (currentView === targetView) {
42
- onViewChange('main');
43
- } else {
44
- onViewChange(targetView);
45
- }
46
- }
47
- </script>
48
-
49
- <div class="action-view-container">
50
- <!-- Main action list -->
51
- {#if currentView !== 'forcedSwap'}
52
- <div class="action-list main-actions">
53
- {#each actions as action}
54
- <button
55
- class="action-item"
56
- class:active={currentView === action.view}
57
- on:click={() => handleActionClick(action.view)}
58
- disabled={processingTurn}
59
- >
60
- <span class="action-icon" class:active={currentView === action.view}>
61
- {action.icon}
62
- </span>
63
- <span class="action-title">{action.title}</span>
64
- <span class="chevron">›</span>
65
- </button>
66
- {/each}
67
- </div>
68
- {/if}
69
-
70
- <!-- Overlay sub-views -->
71
- {#if currentView !== 'main'}
72
- <div class="sub-view-overlay" class:forced={currentView === 'forcedSwap'}>
73
- <div class="sub-view-content">
74
- {#if currentView === 'moves'}
75
- <div class="sub-view-list">
76
- {#each moves as move, index}
77
- {@const isDisabled = move.currentPp <= 0}
78
- {@const enhancedMove = enhancedMoves[index]}
79
- {@const battleMove = enhancedMove?.move}
80
- <button
81
- class="sub-item move-item"
82
- on:click={() => !isDisabled && onMoveSelected(move)}
83
- disabled={isDisabled || processingTurn}
84
- >
85
- <img
86
- src={getTypeLogo(move.type)}
87
- alt={move.type}
88
- class="type-logo"
89
- class:disabled={isDisabled}
90
- />
91
- <div class="move-info">
92
- <div class="move-name" class:disabled={isDisabled}>
93
- {move.name}
94
- {#if battleMove?.power > 0}
95
- <span class="move-power">⚡{battleMove.power}</span>
96
- {/if}
97
- </div>
98
- <div class="move-desc" class:disabled={isDisabled}>
99
- {#if battleMove?.effects && battleMove.effects.length > 0}
100
- {battleMove.effects.length} effects • {generateMoveDescription(battleMove || move)}
101
- {:else}
102
- {generateMoveDescription(battleMove || move)}
103
- {/if}
104
- </div>
105
- {#if battleMove?.flags && battleMove.flags.length > 0}
106
- <div class="move-flags">
107
- {#each battleMove.flags as flag}
108
- <span class="flag-badge">{flag}</span>
109
- {/each}
110
- </div>
111
- {/if}
112
- </div>
113
- <div class="move-stats" class:disabled={isDisabled}>
114
- <div class="move-pp">PP: {move.currentPp}/{move.pp}</div>
115
- {#if battleMove}
116
- <div class="move-accuracy">Acc: {battleMove.accuracy}%</div>
117
- {/if}
118
- </div>
119
- </button>
120
- {/each}
121
- </div>
122
- {:else if currentView === 'piclets'}
123
- {@const availableHealthyPiclets = availablePiclets.filter(p =>
124
- p.currentHp > 0 && p.id !== currentPicletId
125
- )}
126
- <div class="sub-view-list">
127
- {#if availableHealthyPiclets.length === 0}
128
- <div class="empty-message">
129
- No other healthy piclets available
130
- </div>
131
- {:else}
132
- {#each availableHealthyPiclets as piclet}
133
- <button
134
- class="sub-item piclet-item"
135
- on:click={() => onPicletSelected(piclet)}
136
- disabled={processingTurn}
137
- >
138
- <img
139
- src={piclet.imageData || piclet.imageUrl}
140
- alt={piclet.nickname}
141
- class="piclet-thumb"
142
- />
143
- <div class="piclet-info">
144
- <div class="piclet-header">
145
- <span class="piclet-name">{piclet.nickname}</span>
146
- <img
147
- src={getTypeLogo(piclet.primaryType)}
148
- alt={piclet.primaryType}
149
- class="type-logo-small"
150
- />
151
- <span class="level-badge">Lv.{piclet.level}</span>
152
- </div>
153
- <div class="hp-row">
154
- <div class="hp-bar-small">
155
- <div
156
- class="hp-fill-small"
157
- style="width: {(piclet.currentHp / piclet.maxHp) * 100}%"
158
- ></div>
159
- </div>
160
- <span class="hp-text-small">{piclet.currentHp}/{piclet.maxHp}</span>
161
- </div>
162
- </div>
163
- </button>
164
- {/each}
165
- {/if}
166
- </div>
167
- {/if}
168
- </div>
169
- </div>
170
- {/if}
171
-
172
- <!-- Processing overlay -->
173
- {#if processingTurn}
174
- <div class="processing-overlay"></div>
175
- {/if}
176
- </div>
177
-
178
- <style>
179
- .action-view-container {
180
- position: relative;
181
- height: 100%;
182
- }
183
-
184
- /* Main action list */
185
- .action-list {
186
- background: white;
187
- border-radius: 12px;
188
- border: 0.5px solid #c6c6c8;
189
- overflow: hidden;
190
- }
191
-
192
- .action-item {
193
- display: flex;
194
- align-items: center;
195
- width: 100%;
196
- padding: 12px 16px;
197
- background: none;
198
- border: none;
199
- cursor: pointer;
200
- text-align: left;
201
- transition: background-color 0.2s;
202
- position: relative;
203
- }
204
-
205
- .action-item:not(:last-child)::after {
206
- content: '';
207
- position: absolute;
208
- bottom: 0;
209
- left: 16px;
210
- right: 0;
211
- height: 0.5px;
212
- background: #c6c6c8;
213
- }
214
-
215
- .action-item:active:not(:disabled) {
216
- background: #f2f2f7;
217
- }
218
-
219
- .action-item:disabled {
220
- opacity: 0.5;
221
- cursor: not-allowed;
222
- }
223
-
224
- .action-icon {
225
- width: 32px;
226
- height: 32px;
227
- display: flex;
228
- align-items: center;
229
- justify-content: center;
230
- font-size: 20px;
231
- border-radius: 8px;
232
- margin-right: 12px;
233
- }
234
-
235
- .action-icon.active {
236
- background: #e5f3ff;
237
- }
238
-
239
- .action-title {
240
- flex: 1;
241
- font-size: 17px;
242
- font-weight: 400;
243
- color: #000;
244
- }
245
-
246
- .chevron {
247
- color: #8e8e93;
248
- font-size: 16px;
249
- }
250
-
251
- /* Sub-view overlay */
252
- .sub-view-overlay {
253
- position: absolute;
254
- top: 0;
255
- left: 64px; /* Icon width + padding */
256
- right: 0;
257
- bottom: 0;
258
- background: white;
259
- border-radius: 12px;
260
- border: 0.5px solid #c6c6c8;
261
- box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
262
- animation: slideIn 0.2s ease-out;
263
- }
264
-
265
- .sub-view-overlay.forced {
266
- left: 0;
267
- }
268
-
269
- @keyframes slideIn {
270
- from {
271
- transform: translateX(20px);
272
- opacity: 0;
273
- }
274
- to {
275
- transform: translateX(0);
276
- opacity: 1;
277
- }
278
- }
279
-
280
- .sub-view-content {
281
- height: 100%;
282
- overflow-y: auto;
283
- }
284
-
285
- .sub-view-list {
286
- padding: 0;
287
- }
288
-
289
- /* Sub-items */
290
- .sub-item {
291
- display: flex;
292
- align-items: center;
293
- width: 100%;
294
- padding: 12px 16px;
295
- background: none;
296
- border: none;
297
- cursor: pointer;
298
- text-align: left;
299
- transition: background-color 0.2s;
300
- position: relative;
301
- }
302
-
303
- .sub-item:not(:last-child)::after {
304
- content: '';
305
- position: absolute;
306
- bottom: 0;
307
- left: 16px;
308
- right: 0;
309
- height: 0.5px;
310
- background: #c6c6c8;
311
- }
312
-
313
- .sub-item:active:not(:disabled) {
314
- background: #f2f2f7;
315
- }
316
-
317
- .sub-item:disabled {
318
- opacity: 0.5;
319
- cursor: not-allowed;
320
- }
321
-
322
- /* Move items */
323
- .type-logo {
324
- width: 24px;
325
- height: 24px;
326
- margin-right: 12px;
327
- object-fit: contain;
328
- }
329
-
330
- .type-logo.disabled {
331
- opacity: 0.3;
332
- }
333
-
334
- .type-logo-small {
335
- width: 16px;
336
- height: 16px;
337
- object-fit: contain;
338
- }
339
-
340
- .move-info {
341
- flex: 1;
342
- }
343
-
344
- .move-name {
345
- font-size: 16px;
346
- font-weight: 500;
347
- color: #000;
348
- margin-bottom: 2px;
349
- }
350
-
351
- .move-name.disabled {
352
- color: #8e8e93;
353
- }
354
-
355
- .move-desc {
356
- font-size: 13px;
357
- color: #8e8e93;
358
- }
359
-
360
- .move-desc.disabled {
361
- color: #c7c7cc;
362
- }
363
-
364
- .move-power {
365
- font-size: 11px;
366
- color: #ff6b35;
367
- font-weight: bold;
368
- margin-left: 6px;
369
- }
370
-
371
- .move-flags {
372
- display: flex;
373
- gap: 4px;
374
- margin-top: 4px;
375
- flex-wrap: wrap;
376
- }
377
-
378
- .flag-badge {
379
- font-size: 10px;
380
- padding: 2px 6px;
381
- background: #e3f2fd;
382
- color: #1976d2;
383
- border-radius: 8px;
384
- font-weight: 500;
385
- }
386
-
387
- .move-stats {
388
- display: flex;
389
- flex-direction: column;
390
- gap: 2px;
391
- align-items: flex-end;
392
- }
393
-
394
- .move-stats.disabled .move-pp,
395
- .move-stats.disabled .move-accuracy {
396
- background: #f2f2f7;
397
- color: #8e8e93;
398
- }
399
-
400
- .move-pp {
401
- font-size: 12px;
402
- padding: 4px 8px;
403
- background: #f2f2f7;
404
- border-radius: 12px;
405
- color: #000;
406
- white-space: nowrap;
407
- }
408
-
409
- .move-accuracy {
410
- font-size: 11px;
411
- padding: 2px 6px;
412
- background: #e8f5e8;
413
- color: #2e7d32;
414
- border-radius: 8px;
415
- white-space: nowrap;
416
- }
417
-
418
- /* Piclet items */
419
- .piclet-thumb {
420
- width: 48px;
421
- height: 48px;
422
- object-fit: contain;
423
- border-radius: 8px;
424
- margin-right: 12px;
425
- }
426
-
427
- .piclet-info {
428
- flex: 1;
429
- }
430
-
431
- .piclet-header {
432
- display: flex;
433
- align-items: center;
434
- gap: 4px;
435
- margin-bottom: 4px;
436
- }
437
-
438
- .piclet-name {
439
- font-size: 16px;
440
- font-weight: 500;
441
- color: #000;
442
- }
443
-
444
- .level-badge {
445
- font-size: 11px;
446
- font-weight: 700;
447
- padding: 2px 6px;
448
- background: #e5e5ea;
449
- border-radius: 12px;
450
- margin-left: auto;
451
- }
452
-
453
- .hp-row {
454
- display: flex;
455
- align-items: center;
456
- gap: 8px;
457
- }
458
-
459
- .hp-bar-small {
460
- flex: 1;
461
- height: 6px;
462
- background: #e5e5ea;
463
- border-radius: 3px;
464
- overflow: hidden;
465
- }
466
-
467
- .hp-fill-small {
468
- height: 100%;
469
- background: #4cd964;
470
- transition: width 0.3s ease;
471
- }
472
-
473
- .hp-text-small {
474
- font-size: 11px;
475
- color: #8e8e93;
476
- white-space: nowrap;
477
- }
478
-
479
-
480
- /* Empty states */
481
- .empty-message {
482
- padding: 24px;
483
- text-align: center;
484
- color: #8e8e93;
485
- font-size: 16px;
486
- }
487
-
488
- .empty-icon {
489
- font-size: 32px;
490
- display: block;
491
- margin-bottom: 8px;
492
- opacity: 0.5;
493
- }
494
-
495
- .empty-subtitle {
496
- font-size: 14px;
497
- color: #c7c7cc;
498
- margin-top: 4px;
499
- }
500
-
501
- /* Processing overlay */
502
- .processing-overlay {
503
- position: absolute;
504
- inset: 0;
505
- background: rgba(255, 255, 255, 0.8);
506
- border-radius: 12px;
507
- }
508
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/FieldEffectIndicator.svelte DELETED
@@ -1,117 +0,0 @@
1
- <script lang="ts">
2
- export let fieldEffects: Array<{ effect: string; turnsRemaining?: number; side?: string }> = [];
3
-
4
- function getFieldEffectColor(effect: string): string {
5
- // Defensive check: ensure effect is a string
6
- if (typeof effect !== 'string') {
7
- console.warn('FieldEffectIndicator: effect is not a string:', effect);
8
- return '#868e96';
9
- }
10
-
11
- if (effect.includes('weather')) return '#4dabf7';
12
- if (effect.includes('terrain')) return '#51cf66';
13
- if (effect.includes('hazard')) return '#ff6b6b';
14
- return '#868e96';
15
- }
16
-
17
- function getFieldEffectIcon(effect: string): string {
18
- // Defensive check: ensure effect is a string
19
- if (typeof effect !== 'string') {
20
- console.warn('FieldEffectIndicator: effect is not a string:', effect);
21
- return '🌀';
22
- }
23
-
24
- if (effect.includes('storm')) return '⛈️';
25
- if (effect.includes('rain')) return '🌧️';
26
- if (effect.includes('sun')) return '☀️';
27
- if (effect.includes('snow')) return '🌨️';
28
- if (effect.includes('spikes')) return '⚡';
29
- if (effect.includes('reflect')) return '🛡️';
30
- return '🌀';
31
- }
32
- </script>
33
-
34
- {#if fieldEffects.length > 0}
35
- <div class="field-effects">
36
- <div class="field-effects-header">Field Effects</div>
37
- {#each fieldEffects as effect}
38
- <div
39
- class="field-effect"
40
- style="background-color: {getFieldEffectColor(effect.effect)}"
41
- title="{effect.effect}{effect.turnsRemaining ? ` (${effect.turnsRemaining} turns)` : ''}"
42
- >
43
- <span class="effect-icon">{getFieldEffectIcon(effect.effect)}</span>
44
- <span class="effect-name">{effect.effect}</span>
45
- {#if effect.turnsRemaining}
46
- <span class="turns-remaining">{effect.turnsRemaining}</span>
47
- {/if}
48
- {#if effect.side}
49
- <span class="effect-side">({effect.side})</span>
50
- {/if}
51
- </div>
52
- {/each}
53
- </div>
54
- {/if}
55
-
56
- <style>
57
- .field-effects {
58
- position: absolute;
59
- top: 10px;
60
- left: 50%;
61
- transform: translateX(-50%);
62
- display: flex;
63
- flex-direction: column;
64
- align-items: center;
65
- gap: 4px;
66
- z-index: 10;
67
- }
68
-
69
- .field-effects-header {
70
- background: rgba(0, 0, 0, 0.7);
71
- color: white;
72
- padding: 2px 8px;
73
- border-radius: 12px;
74
- font-size: 0.7rem;
75
- font-weight: bold;
76
- }
77
-
78
- .field-effect {
79
- display: flex;
80
- align-items: center;
81
- gap: 4px;
82
- padding: 3px 8px;
83
- border-radius: 12px;
84
- color: white;
85
- font-size: 0.7rem;
86
- font-weight: bold;
87
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
88
- background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
89
- animation: shimmer 2s infinite;
90
- }
91
-
92
- .effect-icon {
93
- font-size: 0.8rem;
94
- }
95
-
96
- .effect-name {
97
- font-size: 0.6rem;
98
- text-transform: capitalize;
99
- }
100
-
101
- .turns-remaining {
102
- background: rgba(255, 255, 255, 0.3);
103
- border-radius: 8px;
104
- padding: 1px 4px;
105
- font-size: 0.6rem;
106
- }
107
-
108
- .effect-side {
109
- font-size: 0.6rem;
110
- opacity: 0.8;
111
- }
112
-
113
- @keyframes shimmer {
114
- 0%, 100% { background-position: -100% 0; }
115
- 50% { background-position: 100% 0; }
116
- }
117
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/LLMBattleEngine.svelte DELETED
@@ -1,453 +0,0 @@
1
- <script lang="ts">
2
- import type { PicletInstance } from '$lib/db/schema';
3
- import type { GradioClient } from '$lib/types';
4
-
5
- interface BattleUpdate {
6
- battle_updates: string[];
7
- player_pokemon_status: string;
8
- player_pokemon_hp: 'Empty' | 'Very Low' | 'Low' | 'Medium' | 'High' | 'Very High' | 'Full';
9
- enemy_pokemon_status: string;
10
- enemy_pokemon_hp: 'Empty' | 'Very Low' | 'Low' | 'Medium' | 'High' | 'Very High' | 'Full';
11
- next_to_act: 'player' | 'enemy';
12
- available_actions: string[];
13
- }
14
-
15
- interface Props {
16
- playerPiclet: PicletInstance;
17
- enemyPiclet: PicletInstance;
18
- qwenClient: GradioClient;
19
- onBattleEnd: (winner: 'player' | 'enemy') => void;
20
- rosterPiclets?: PicletInstance[]; // Optional roster for switching
21
- }
22
-
23
- let { playerPiclet, enemyPiclet, qwenClient, onBattleEnd, rosterPiclets }: Props = $props();
24
-
25
- // Battle state
26
- let battleState: BattleUpdate = $state({
27
- battle_updates: [],
28
- player_pokemon_status: 'Ready for battle',
29
- player_pokemon_hp: 'Full',
30
- enemy_pokemon_status: 'Ready for battle',
31
- enemy_pokemon_hp: 'Full',
32
- next_to_act: 'player',
33
- available_actions: []
34
- });
35
-
36
- let battleHistory: string[] = $state([]);
37
- let isProcessing: boolean = $state(false);
38
- let currentPlayerPiclet: PicletInstance = $state(playerPiclet);
39
- let showPicletSelector: boolean = $state(false);
40
-
41
- // Dice rolling system
42
- function rollDice(): number {
43
- return Math.floor(Math.random() * 20) + 1; // D20 roll
44
- }
45
-
46
- function getActionEffectiveness(roll: number): { success: string; description: string } {
47
- if (roll === 20) return { success: 'Critical Success', description: 'The action succeeds spectacularly!' };
48
- if (roll >= 15) return { success: 'Success', description: 'The action succeeds well!' };
49
- if (roll >= 10) return { success: 'Partial Success', description: 'The action has some effect.' };
50
- if (roll >= 5) return { success: 'Failure', description: 'The action fails but something minor happens.' };
51
- if (roll === 1) return { success: 'Critical Failure', description: 'The action backfires!' };
52
- return { success: 'Failure', description: 'The action fails.' };
53
- }
54
-
55
- // Generate text using Qwen client
56
- async function generateBattleUpdate(prompt: string): Promise<BattleUpdate> {
57
- const result = await qwenClient.predict("/model_chat", [
58
- prompt, // user message
59
- [], // chat history (empty for new conversation)
60
- "You are a Pokemon battle narrator. Create engaging battle descriptions and return valid JSON responses.", // system prompt
61
- 0.7, // temperature
62
- 1000 // max_tokens
63
- ]);
64
-
65
- const responseText = result.data[1] || ''; // Qwen returns [history, response]
66
- console.log('LLM Response:', responseText);
67
-
68
- // Extract JSON from response
69
- try {
70
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
71
- if (!jsonMatch) throw new Error('No JSON found in response');
72
-
73
- const battleUpdate: BattleUpdate = JSON.parse(jsonMatch[0]);
74
- return battleUpdate;
75
- } catch (error) {
76
- console.error('Failed to parse battle response:', error);
77
- // Fallback response
78
- return {
79
- battle_updates: ['The battle continues...'],
80
- player_pokemon_status: battleState.player_pokemon_status,
81
- player_pokemon_hp: battleState.player_pokemon_hp,
82
- enemy_pokemon_status: battleState.enemy_pokemon_status,
83
- enemy_pokemon_hp: battleState.enemy_pokemon_hp,
84
- next_to_act: battleState.next_to_act === 'player' ? 'enemy' : 'player',
85
- available_actions: ['Attack', 'Defend', 'Special Move']
86
- };
87
- }
88
- }
89
-
90
- // Initialize battle
91
- async function startBattle() {
92
- isProcessing = true;
93
-
94
- const initialPrompt = `Let's role play a Pokemon game using my custom Pokemon, the player will use ${currentPlayerPiclet.typeId} and the enemy will use ${enemyPiclet.typeId}.
95
- You will return a brief description on what happens because of the action.
96
- I will send you an update of the enemy or players move and the success of the action (in a DnD style). Be sure to be as creative and engaging as possible when defining battle updates and available actions.
97
-
98
- Player Pokemon: ${currentPlayerPiclet.typeId}
99
- ${currentPlayerPiclet.description}
100
-
101
- Enemy Pokemon: ${enemyPiclet.typeId}
102
- ${enemyPiclet.description}
103
-
104
- Each response should be a json object with fields:
105
- \`\`\`json
106
- {
107
- "battle_updates": [list with 1 sentence per entry describing what just happened in battle],
108
- "player_pokemon_status": "1 sentence description of how the Pokemon is doing",
109
- "player_pokemon_hp": "enum Empty, Very Low, Low, Medium, High, Very High, Full",
110
- "enemy_pokemon_status": "1 sentence description of how the Pokemon is doing",
111
- "enemy_pokemon_hp": "enum Empty, Very Low, Low, Medium, High, Very High, Full",
112
- "next_to_act": "enum player/enemy",
113
- "available_actions": ["short list of 1 sentence actions of what to have the next_to_act Pokemon do next"]
114
- }
115
- \`\`\`
116
- Start with just some intro updates describing both monsters being on the battlefield.`;
117
-
118
- try {
119
- const update = await generateBattleUpdate(initialPrompt);
120
- battleState = update;
121
- battleHistory.push(initialPrompt);
122
- } catch (error) {
123
- console.error('Failed to start battle:', error);
124
- }
125
-
126
- isProcessing = false;
127
- }
128
-
129
- // Execute player action
130
- async function executeAction(actionDescription: string) {
131
- if (isProcessing) return;
132
-
133
- isProcessing = true;
134
-
135
- const roll = rollDice();
136
- const effectiveness = getActionEffectiveness(roll);
137
-
138
- const prompt = `Player chooses: "${actionDescription}"
139
- Dice roll: ${roll}/20 (${effectiveness.success})
140
- Effect: ${effectiveness.description}
141
-
142
- Update the battle state based on this action and its effectiveness. Then have the enemy take their turn if appropriate.`;
143
-
144
- try {
145
- const update = await generateBattleUpdate(prompt);
146
- battleState = update;
147
- battleHistory.push(prompt);
148
-
149
- // Check for battle end conditions
150
- if (battleState.player_pokemon_hp === 'Empty') {
151
- onBattleEnd('enemy');
152
- } else if (battleState.enemy_pokemon_hp === 'Empty') {
153
- onBattleEnd('player');
154
- }
155
- } catch (error) {
156
- console.error('Failed to execute action:', error);
157
- }
158
-
159
- isProcessing = false;
160
- }
161
-
162
- // Switch Piclet function
163
- async function switchPiclet(newPiclet: PicletInstance) {
164
- if (isProcessing) return;
165
-
166
- isProcessing = true;
167
- showPicletSelector = false;
168
-
169
- const switchPrompt = `Player switches from ${currentPlayerPiclet.typeId} to ${newPiclet.typeId}!
170
-
171
- New Pokemon: ${newPiclet.typeId}
172
- ${newPiclet.description}
173
-
174
- Update the battle to show the switch and have the enemy react accordingly.`;
175
-
176
- try {
177
- currentPlayerPiclet = newPiclet;
178
- const update = await generateBattleUpdate(switchPrompt);
179
- battleState = update;
180
- battleHistory.push(switchPrompt);
181
- } catch (error) {
182
- console.error('Failed to switch Piclet:', error);
183
- }
184
-
185
- isProcessing = false;
186
- }
187
-
188
- // Auto-start battle when component mounts
189
- $effect(() => {
190
- startBattle();
191
- });
192
-
193
- // Export functions for parent component
194
- export { executeAction, switchPiclet };
195
- </script>
196
-
197
- <div class="llm-battle-engine">
198
- <!-- Battle Narrative Display -->
199
- <div class="battle-narrative">
200
- <h3>Battle Progress</h3>
201
- {#each battleState.battle_updates as update}
202
- <div class="battle-update">{update}</div>
203
- {/each}
204
- </div>
205
-
206
- <!-- Pokemon Status -->
207
- <div class="pokemon-status">
208
- <div class="player-status">
209
- <h4>{currentPlayerPiclet.typeId}</h4>
210
- <div class="hp-indicator hp-{battleState.player_pokemon_hp.toLowerCase().replace(' ', '-')}">{battleState.player_pokemon_hp}</div>
211
- <p>{battleState.player_pokemon_status}</p>
212
- </div>
213
-
214
- <div class="enemy-status">
215
- <h4>{enemyPiclet.typeId}</h4>
216
- <div class="hp-indicator hp-{battleState.enemy_pokemon_hp.toLowerCase().replace(' ', '-')}">{battleState.enemy_pokemon_hp}</div>
217
- <p>{battleState.enemy_pokemon_status}</p>
218
- </div>
219
- </div>
220
-
221
- <!-- Available Actions (when it's player's turn) -->
222
- {#if battleState.next_to_act === 'player' && !isProcessing}
223
- <div class="available-actions">
224
- <h4>Choose Your Action:</h4>
225
-
226
- <!-- Battle Actions -->
227
- {#each battleState.available_actions as action}
228
- <button
229
- class="action-button"
230
- onclick={() => executeAction(action)}
231
- >
232
- {action}
233
- </button>
234
- {/each}
235
-
236
- <!-- Piclet Switching -->
237
- {#if rosterPiclets && rosterPiclets.length > 1}
238
- <button
239
- class="switch-button"
240
- onclick={() => showPicletSelector = !showPicletSelector}
241
- >
242
- 🔄 Switch Piclet
243
- </button>
244
- {/if}
245
- </div>
246
-
247
- <!-- Piclet Selector -->
248
- {#if showPicletSelector && rosterPiclets}
249
- <div class="piclet-selector">
250
- <h4>Choose Piclet:</h4>
251
- <div class="piclet-grid">
252
- {#each rosterPiclets as piclet}
253
- {#if piclet.id !== currentPlayerPiclet.id}
254
- <button
255
- class="piclet-option"
256
- onclick={() => switchPiclet(piclet)}
257
- >
258
- <img src={piclet.imageUrl} alt={piclet.typeId} />
259
- <span>{piclet.typeId}</span>
260
- <span class="tier tier-{piclet.tier}">{piclet.tier}</span>
261
- </button>
262
- {/if}
263
- {/each}
264
- </div>
265
- </div>
266
- {/if}
267
- {:else if isProcessing}
268
- <div class="processing">
269
- <div class="spinner"></div>
270
- <p>Processing battle turn...</p>
271
- </div>
272
- {:else}
273
- <div class="enemy-turn">
274
- <p>Enemy is deciding their move...</p>
275
- </div>
276
- {/if}
277
- </div>
278
-
279
- <style>
280
- .llm-battle-engine {
281
- display: flex;
282
- flex-direction: column;
283
- gap: 1rem;
284
- padding: 1rem;
285
- }
286
-
287
- .battle-narrative {
288
- background: #f8f9fa;
289
- border-radius: 8px;
290
- padding: 1rem;
291
- max-height: 200px;
292
- overflow-y: auto;
293
- }
294
-
295
- .battle-update {
296
- margin-bottom: 0.5rem;
297
- padding: 0.5rem;
298
- background: white;
299
- border-radius: 4px;
300
- border-left: 3px solid #007bff;
301
- }
302
-
303
- .pokemon-status {
304
- display: grid;
305
- grid-template-columns: 1fr 1fr;
306
- gap: 1rem;
307
- }
308
-
309
- .player-status, .enemy-status {
310
- padding: 1rem;
311
- border-radius: 8px;
312
- text-align: center;
313
- }
314
-
315
- .player-status {
316
- background: rgba(0, 123, 255, 0.1);
317
- border: 2px solid #007bff;
318
- }
319
-
320
- .enemy-status {
321
- background: rgba(220, 53, 69, 0.1);
322
- border: 2px solid #dc3545;
323
- }
324
-
325
- .hp-indicator {
326
- font-weight: bold;
327
- padding: 0.25rem 0.5rem;
328
- border-radius: 16px;
329
- margin: 0.5rem 0;
330
- display: inline-block;
331
- }
332
-
333
- .hp-full { background: #28a745; color: white; }
334
- .hp-very-high { background: #40c757; color: white; }
335
- .hp-high { background: #6bc267; color: white; }
336
- .hp-medium { background: #ffc107; color: black; }
337
- .hp-low { background: #fd7e14; color: white; }
338
- .hp-very-low { background: #dc3545; color: white; }
339
- .hp-empty { background: #6c757d; color: white; }
340
-
341
- .available-actions {
342
- display: flex;
343
- flex-direction: column;
344
- gap: 0.5rem;
345
- }
346
-
347
- .action-button {
348
- padding: 0.75rem 1rem;
349
- background: #007bff;
350
- color: white;
351
- border: none;
352
- border-radius: 8px;
353
- cursor: pointer;
354
- font-size: 1rem;
355
- transition: background-color 0.2s;
356
- }
357
-
358
- .action-button:hover {
359
- background: #0056b3;
360
- }
361
-
362
- .switch-button {
363
- padding: 0.75rem 1rem;
364
- background: #28a745;
365
- color: white;
366
- border: none;
367
- border-radius: 8px;
368
- cursor: pointer;
369
- font-size: 1rem;
370
- transition: background-color 0.2s;
371
- margin-top: 0.5rem;
372
- }
373
-
374
- .switch-button:hover {
375
- background: #1e7e34;
376
- }
377
-
378
- .piclet-selector {
379
- background: #f8f9fa;
380
- border-radius: 8px;
381
- padding: 1rem;
382
- margin-top: 1rem;
383
- }
384
-
385
- .piclet-grid {
386
- display: grid;
387
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
388
- gap: 0.5rem;
389
- margin-top: 0.5rem;
390
- }
391
-
392
- .piclet-option {
393
- display: flex;
394
- flex-direction: column;
395
- align-items: center;
396
- gap: 0.25rem;
397
- padding: 0.5rem;
398
- background: white;
399
- border: 2px solid #dee2e6;
400
- border-radius: 8px;
401
- cursor: pointer;
402
- transition: all 0.2s;
403
- }
404
-
405
- .piclet-option:hover {
406
- border-color: #007bff;
407
- background: #f0f7ff;
408
- }
409
-
410
- .piclet-option img {
411
- width: 40px;
412
- height: 40px;
413
- object-fit: cover;
414
- border-radius: 4px;
415
- }
416
-
417
- .piclet-option span {
418
- font-size: 0.8rem;
419
- text-align: center;
420
- }
421
-
422
- .tier {
423
- padding: 0.1rem 0.3rem;
424
- border-radius: 8px;
425
- font-size: 0.7rem;
426
- font-weight: bold;
427
- text-transform: uppercase;
428
- }
429
-
430
- .tier-low { background: #6c757d; color: white; }
431
- .tier-medium { background: #28a745; color: white; }
432
- .tier-high { background: #fd7e14; color: white; }
433
- .tier-legendary { background: #dc3545; color: white; }
434
-
435
- .processing, .enemy-turn {
436
- text-align: center;
437
- padding: 2rem;
438
- }
439
-
440
- .spinner {
441
- width: 40px;
442
- height: 40px;
443
- border: 4px solid #f3f3f3;
444
- border-top: 4px solid #007bff;
445
- border-radius: 50%;
446
- animation: spin 1s linear infinite;
447
- margin: 0 auto 1rem;
448
- }
449
-
450
- @keyframes spin {
451
- to { transform: rotate(360deg); }
452
- }
453
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/PicletInfo.svelte DELETED
@@ -1,244 +0,0 @@
1
- <script lang="ts">
2
- import type { PicletInstance } from '$lib/db/schema';
3
- import { getXpProgress, getXpTowardsNextLevel } from '$lib/services/levelingService';
4
- import { TYPE_DATA } from '$lib/types/picletTypes';
5
-
6
- export let piclet: PicletInstance;
7
- export let hpPercentage: number;
8
- export let isPlayer: boolean;
9
- $: xpTowardsNext = isPlayer ? getXpTowardsNextLevel(piclet.xp, piclet.level, piclet.tier) : { current: 0, needed: 0, percentage: 0 };
10
-
11
- // Type-based styling
12
- $: typeData = TYPE_DATA[piclet.primaryType];
13
- $: typeColor = typeData.color;
14
- $: typeLogoPath = `/classes/${piclet.primaryType}.png`;
15
-
16
- $: hpColor = hpPercentage > 0.5 ? '#34c759' : hpPercentage > 0.25 ? '#ffcc00' : '#ff3b30';
17
- </script>
18
-
19
- <div class="piclet-info-wrapper {isPlayer ? 'player-info-wrapper' : 'enemy-info-wrapper'}">
20
- <div class="piclet-info">
21
- <!-- Type Logo (Foreground) -->
22
- <div class="type-logo-container">
23
- <img
24
- src={typeLogoPath}
25
- alt={piclet.primaryType}
26
- class="type-logo"
27
- />
28
- </div>
29
-
30
- <!-- Content Area -->
31
- <div class="content-area">
32
- <!-- Name and Level Row -->
33
- <div class="header-row">
34
- <div class="piclet-name">{piclet.nickname}</div>
35
- <div class="level-text">Lv.{piclet.level}</div>
36
- </div>
37
-
38
- <!-- HP Section -->
39
- <div class="stat-row">
40
- <div class="stat-label">HP</div>
41
- <div class="hp-bar">
42
- <div
43
- class="hp-fill"
44
- style="width: {hpPercentage * 100}%; background-color: {hpColor}"
45
- ></div>
46
- </div>
47
- </div>
48
-
49
- <!-- XP Section (Player only) -->
50
- {#if isPlayer}
51
- <div class="stat-row">
52
- <div class="stat-label">XP</div>
53
- <div class="xp-bar">
54
- <div
55
- class="xp-fill"
56
- style="width: {xpTowardsNext.percentage}%"
57
- ></div>
58
- </div>
59
- </div>
60
- {/if}
61
- </div>
62
- </div>
63
-
64
- <!-- Triangle Pointer -->
65
- <div class="triangle-pointer {isPlayer ? 'player-pointer' : 'enemy-pointer'}"></div>
66
- </div>
67
-
68
- <style>
69
- .piclet-info-wrapper {
70
- position: absolute;
71
- display: flex;
72
- align-items: center;
73
- }
74
-
75
- .player-info-wrapper {
76
- right: 16px;
77
- bottom: 20px;
78
- flex-direction: row-reverse;
79
- }
80
-
81
- .enemy-info-wrapper {
82
- left: 16px;
83
- top: 20px;
84
- flex-direction: row;
85
- }
86
-
87
- .piclet-info {
88
- background: rgba(255, 255, 255, 0.9);
89
- border-radius: 8px;
90
- padding: 10px;
91
- min-width: 160px;
92
- display: flex;
93
- align-items: flex-start;
94
- gap: 10px;
95
- }
96
-
97
- /* Type Logo Container */
98
- .type-logo-container {
99
- flex-shrink: 0;
100
- margin-top: 2px;
101
- }
102
-
103
- .type-logo {
104
- width: 35px;
105
- height: 35px;
106
- object-fit: contain;
107
- border-radius: 6px;
108
- background: rgba(255, 255, 255, 0.1);
109
- backdrop-filter: blur(4px);
110
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
111
- }
112
-
113
- /* Content Area */
114
- .content-area {
115
- flex: 1;
116
- min-width: 0; /* Allows flex item to shrink below content size */
117
- }
118
-
119
- /* Header Row */
120
- .header-row {
121
- display: flex;
122
- align-items: baseline;
123
- justify-content: space-between;
124
- margin-bottom: 6px;
125
- gap: 8px;
126
- }
127
-
128
- .piclet-name {
129
- font-weight: 600;
130
- font-size: 14px;
131
- color: #1a1a1a;
132
- flex: 1;
133
- min-width: 0;
134
- overflow: hidden;
135
- text-overflow: ellipsis;
136
- white-space: nowrap;
137
- }
138
-
139
- .level-text {
140
- font-size: 11px;
141
- font-weight: 600;
142
- color: #666;
143
- flex-shrink: 0;
144
- }
145
-
146
- /* Stat Rows */
147
- .stat-row {
148
- display: flex;
149
- align-items: center;
150
- gap: 6px;
151
- margin-bottom: 3px;
152
- }
153
-
154
- .stat-row:last-child {
155
- margin-bottom: 0;
156
- }
157
-
158
- .stat-label {
159
- font-size: 9px;
160
- font-weight: 600;
161
- color: #666;
162
- text-transform: uppercase;
163
- letter-spacing: 0.3px;
164
- width: 16px;
165
- flex-shrink: 0;
166
- }
167
-
168
- /* HP Bar */
169
- .hp-bar {
170
- height: 6px;
171
- background: #e0e0e0;
172
- border-radius: 3px;
173
- overflow: hidden;
174
- flex: 1;
175
- min-width: 80px;
176
- }
177
-
178
- .hp-fill {
179
- height: 100%;
180
- transition: width 0.5s ease, background-color 0.3s ease;
181
- }
182
-
183
- /* XP Bar */
184
- .xp-bar {
185
- height: 5px;
186
- background: #e0e0e0;
187
- border-radius: 2.5px;
188
- overflow: hidden;
189
- flex: 1;
190
- min-width: 80px;
191
- }
192
-
193
- .xp-fill {
194
- height: 100%;
195
- background: #2196f3;
196
- transition: width 1.2s ease-out;
197
- }
198
-
199
-
200
- /* Triangle Pointer */
201
- .triangle-pointer {
202
- width: 0;
203
- height: 0;
204
- border-style: solid;
205
- }
206
-
207
- .player-pointer {
208
- border-width: 8px 16px 8px 0;
209
- border-color: transparent rgba(255, 255, 255, 0.9) transparent transparent;
210
- margin-right: -1px;
211
- }
212
-
213
- .enemy-pointer {
214
- border-width: 8px 0 8px 16px;
215
- border-color: transparent transparent transparent rgba(255, 255, 255, 0.9);
216
- margin-left: -1px;
217
- }
218
-
219
- @media (max-width: 768px) {
220
- .piclet-info {
221
- min-width: 140px;
222
- padding: 8px;
223
- gap: 8px;
224
- }
225
-
226
- .type-logo {
227
- width: 28px;
228
- height: 28px;
229
- }
230
-
231
- .piclet-name {
232
- font-size: 12px;
233
- max-width: 87px;
234
- }
235
-
236
- .level-text {
237
- font-size: 10px;
238
- }
239
-
240
- .hp-bar, .xp-bar {
241
- min-width: 60px;
242
- }
243
- }
244
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/StatusEffectIndicator.svelte DELETED
@@ -1,89 +0,0 @@
1
- <script lang="ts">
2
- import type { StatusEffect } from '$lib/battle-engine/types';
3
-
4
- export let statusEffects: { type: StatusEffect; turnsLeft?: number }[] = [];
5
-
6
- function getStatusColor(status: StatusEffect): string {
7
- switch (status) {
8
- case 'burn': return '#ff6b6b';
9
- case 'freeze': return '#74c0fc';
10
- case 'paralyze': return '#ffd43b';
11
- case 'poison': return '#9775fa';
12
- case 'sleep': return '#868e96';
13
- case 'confuse': return '#ff8cc8';
14
- default: return '#495057';
15
- }
16
- }
17
-
18
- function getStatusIcon(status: StatusEffect): string {
19
- switch (status) {
20
- case 'burn': return '🔥';
21
- case 'freeze': return '❄️';
22
- case 'paralyze': return '⚡';
23
- case 'poison': return '☠️';
24
- case 'sleep': return '💤';
25
- case 'confuse': return '😵';
26
- default: return '?';
27
- }
28
- }
29
- </script>
30
-
31
- {#if statusEffects.length > 0}
32
- <div class="status-effects">
33
- {#each statusEffects as effect}
34
- <div
35
- class="status-effect"
36
- style="background-color: {getStatusColor(effect.type)}"
37
- title="{effect.type}{effect.turnsLeft ? ` (${effect.turnsLeft} turns)` : ''}"
38
- >
39
- <span class="status-icon">{getStatusIcon(effect.type)}</span>
40
- <span class="status-name">{effect.type.toUpperCase()}</span>
41
- {#if effect.turnsLeft}
42
- <span class="turns-left">{effect.turnsLeft}</span>
43
- {/if}
44
- </div>
45
- {/each}
46
- </div>
47
- {/if}
48
-
49
- <style>
50
- .status-effects {
51
- display: flex;
52
- gap: 4px;
53
- flex-wrap: wrap;
54
- margin: 4px 0;
55
- }
56
-
57
- .status-effect {
58
- display: flex;
59
- align-items: center;
60
- gap: 2px;
61
- padding: 2px 6px;
62
- border-radius: 12px;
63
- color: white;
64
- font-size: 0.7rem;
65
- font-weight: bold;
66
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
67
- }
68
-
69
- .status-icon {
70
- font-size: 0.8rem;
71
- }
72
-
73
- .status-name {
74
- font-size: 0.6rem;
75
- }
76
-
77
- .turns-left {
78
- background: rgba(255, 255, 255, 0.3);
79
- border-radius: 8px;
80
- padding: 1px 4px;
81
- font-size: 0.6rem;
82
- margin-left: 2px;
83
- }
84
-
85
- @keyframes pulse {
86
- 0%, 100% { opacity: 1; }
87
- 50% { opacity: 0.7; }
88
- }
89
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/TypewriterText.svelte DELETED
@@ -1,49 +0,0 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte';
3
-
4
- export let text: string = '';
5
- export let speed: number = 30; // milliseconds per character
6
-
7
- let displayedText = '';
8
- let currentIndex = 0;
9
- let intervalId: number | null = null;
10
-
11
- function startTyping() {
12
- // Reset when text changes
13
- displayedText = '';
14
- currentIndex = 0;
15
-
16
- // Clear any existing interval
17
- if (intervalId) {
18
- clearInterval(intervalId);
19
- }
20
-
21
- // Start typing effect
22
- intervalId = setInterval(() => {
23
- if (currentIndex < text.length) {
24
- displayedText += text[currentIndex];
25
- currentIndex++;
26
- } else {
27
- if (intervalId) {
28
- clearInterval(intervalId);
29
- intervalId = null;
30
- }
31
- }
32
- }, speed);
33
- }
34
-
35
- // Watch for text changes
36
- $: if (text) {
37
- startTyping();
38
- }
39
-
40
- onMount(() => {
41
- return () => {
42
- if (intervalId) {
43
- clearInterval(intervalId);
44
- }
45
- };
46
- });
47
- </script>
48
-
49
- <span>{displayedText}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Layout/ProgressBar.svelte CHANGED
@@ -5,27 +5,30 @@
5
 
6
  let gameState: GameState | null = $state(null);
7
 
8
- // Progress bar color based on progress
9
- const progressColor = $derived(getProgressColor(gameState?.progressPoints || 0));
10
-
11
- function getProgressColor(points: number): string {
12
- const progress = points / 1000;
13
- if (progress < 0.2) return '#4caf50'; // green
14
- if (progress < 0.4) return '#ffc107'; // yellow
15
- if (progress < 0.6) return '#ff9800'; // orange
16
- if (progress < 0.8) return '#f44336'; // red
17
- return '#9c27b0'; // purple
18
  }
19
 
20
- onMount(async () => {
21
  // Load game state
22
- gameState = await getOrCreateGameState();
23
-
 
 
24
  // Refresh game state periodically
25
- const interval = setInterval(async () => {
26
- gameState = await getOrCreateGameState();
 
 
27
  }, 5000); // Update every 5 seconds
28
-
29
  return () => clearInterval(interval);
30
  });
31
  </script>
@@ -33,9 +36,9 @@
33
  {#if gameState}
34
  <div class="progress-container">
35
  <div class="progress-bar">
36
- <div class="progress-fill" style="width: {(gameState.progressPoints / 1000) * 100}%; background-color: {progressColor}"></div>
37
  </div>
38
- <span class="progress-stats">👥 {gameState.trainersDefeated} 📷 {gameState.picletsCapured}</span>
39
  </div>
40
  {/if}
41
 
 
5
 
6
  let gameState: GameState | null = $state(null);
7
 
8
+ // Progress bar color based on rarity score
9
+ const progressColor = $derived(getProgressColor(gameState?.rarityScore || 0));
10
+
11
+ function getProgressColor(score: number): string {
12
+ if (score < 100) return '#4caf50'; // green - beginner
13
+ if (score < 500) return '#ffc107'; // yellow - intermediate
14
+ if (score < 1000) return '#ff9800'; // orange - advanced
15
+ if (score < 2000) return '#f44336'; // red - expert
16
+ return '#9c27b0'; // purple - master
 
17
  }
18
 
19
+ onMount(() => {
20
  // Load game state
21
+ getOrCreateGameState().then(state => {
22
+ gameState = state;
23
+ });
24
+
25
  // Refresh game state periodically
26
+ const interval = setInterval(() => {
27
+ getOrCreateGameState().then(state => {
28
+ gameState = state;
29
+ });
30
  }, 5000); // Update every 5 seconds
31
+
32
  return () => clearInterval(interval);
33
  });
34
  </script>
 
36
  {#if gameState}
37
  <div class="progress-container">
38
  <div class="progress-bar">
39
+ <div class="progress-fill" style="width: {Math.min((gameState.rarityScore / 2000) * 100, 100)}%; background-color: {progressColor}"></div>
40
  </div>
41
+ <span class="progress-stats">🔍 {gameState.uniqueDiscoveries} {gameState.rarityScore}</span>
42
  </div>
43
  {/if}
44
 
src/lib/components/Layout/TabBar.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- export type TabId = 'scanner' | 'encounters' | 'pictuary';
3
 
4
  interface Tab {
5
  id: TabId;
@@ -16,7 +16,7 @@
16
 
17
  const tabs: Tab[] = [
18
  { id: 'scanner', label: 'Scanner', icon: 'https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/snap_logo.png' },
19
- { id: 'encounters', label: 'Encounters', icon: 'https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/encounters_logo.png' },
20
  { id: 'pictuary', label: 'Pictuary', icon: 'https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/pictuary_logo.png' }
21
  ];
22
 
 
1
  <script lang="ts">
2
+ export type TabId = 'scanner' | 'activity' | 'pictuary';
3
 
4
  interface Tab {
5
  id: TabId;
 
16
 
17
  const tabs: Tab[] = [
18
  { id: 'scanner', label: 'Scanner', icon: 'https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/snap_logo.png' },
19
+ { id: 'activity', label: 'Activity', icon: 'https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/encounters_logo.png' },
20
  { id: 'pictuary', label: 'Pictuary', icon: 'https://huggingface.co/spaces/Fraser/piclets/resolve/main/assets/pictuary_logo.png' }
21
  ];
22
 
src/lib/components/Pages/Activity.svelte ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { fade, fly } from 'svelte/transition';
4
+ import type { ActivityEntry, PicletInstance, LeaderboardEntry } from '$lib/db/schema';
5
+ import { db } from '$lib/db';
6
+ import PullToRefresh from '../UI/PullToRefresh.svelte';
7
+ import { CanonicalService } from '$lib/services/canonicalService';
8
+
9
+ let activities: ActivityEntry[] = $state([]);
10
+ let leaderboard: LeaderboardEntry[] = $state([]);
11
+ let isLoading = $state(true);
12
+ let isRefreshing = $state(false);
13
+ let activeView: 'recent' | 'leaderboard' = $state('recent');
14
+
15
+ // Mock data for development (will be replaced with server data)
16
+ const mockActivities: ActivityEntry[] = [
17
+ {
18
+ id: 1,
19
+ type: 'discovery',
20
+ title: 'New Discovery!',
21
+ description: 'Fraser discovered the first "Pillow" Piclet',
22
+ picletTypeId: 'pillow_001',
23
+ discovererName: 'Fraser',
24
+ rarity: 'legendary',
25
+ createdAt: new Date(Date.now() - 1000 * 60 * 5) // 5 minutes ago
26
+ },
27
+ {
28
+ id: 2,
29
+ type: 'variation',
30
+ title: 'Variation Found',
31
+ description: 'Alex found a "Velvet Pillow" variation',
32
+ picletTypeId: 'pillow_002',
33
+ discovererName: 'Alex',
34
+ rarity: 'rare',
35
+ createdAt: new Date(Date.now() - 1000 * 60 * 30) // 30 minutes ago
36
+ },
37
+ {
38
+ id: 3,
39
+ type: 'milestone',
40
+ title: 'Milestone Reached!',
41
+ description: 'Sam collected 100 unique Piclets!',
42
+ picletTypeId: 'trophy',
43
+ discovererName: 'Sam',
44
+ rarity: 'epic',
45
+ createdAt: new Date(Date.now() - 1000 * 60 * 60) // 1 hour ago
46
+ }
47
+ ];
48
+
49
+ const mockLeaderboard: LeaderboardEntry[] = [
50
+ { username: 'Fraser', totalDiscoveries: 156, uniqueDiscoveries: 45, rarityScore: 2340, rank: 1 },
51
+ { username: 'Alex', totalDiscoveries: 134, uniqueDiscoveries: 38, rarityScore: 1890, rank: 2 },
52
+ { username: 'Sam', totalDiscoveries: 98, uniqueDiscoveries: 31, rarityScore: 1560, rank: 3 },
53
+ { username: 'Jordan', totalDiscoveries: 87, uniqueDiscoveries: 28, rarityScore: 1230, rank: 4 },
54
+ { username: 'Casey', totalDiscoveries: 76, uniqueDiscoveries: 22, rarityScore: 980, rank: 5 }
55
+ ];
56
+
57
+ onMount(async () => {
58
+ await loadActivityData();
59
+ });
60
+
61
+ async function loadActivityData() {
62
+ isLoading = true;
63
+ try {
64
+ // Check if player has any discoveries
65
+ const playerPiclets = await db.picletInstances.toArray();
66
+
67
+ if (playerPiclets.length === 0) {
68
+ // No discoveries yet - show empty state
69
+ activities = [];
70
+ leaderboard = [];
71
+ } else {
72
+ // TODO: Fetch from server once backend is ready
73
+ // For now, use mock data
74
+ activities = mockActivities;
75
+ leaderboard = mockLeaderboard;
76
+
77
+ // Load recent local discoveries
78
+ const recentLocal = playerPiclets
79
+ .filter(p => p.collectedAt)
80
+ .sort((a, b) => (b.collectedAt?.getTime() || 0) - (a.collectedAt?.getTime() || 0))
81
+ .slice(0, 5)
82
+ .map((p, index) => ({
83
+ id: 1000 + index,
84
+ type: 'discovery' as const,
85
+ title: p.isCanonical ? 'New Discovery!' : 'Variation Found',
86
+ description: `You ${p.isCanonical ? 'discovered' : 'found a variation of'} "${p.objectName || p.typeId}"`,
87
+ picletTypeId: p.typeId,
88
+ discovererName: 'You',
89
+ rarity: CanonicalService.calculateRarity(p.scanCount) as any,
90
+ createdAt: p.collectedAt || new Date()
91
+ }));
92
+
93
+ // Merge with mock activities
94
+ activities = [...recentLocal, ...activities].slice(0, 10);
95
+ }
96
+ } catch (error) {
97
+ console.error('Error loading activity data:', error);
98
+ }
99
+ isLoading = false;
100
+ }
101
+
102
+ async function handleRefresh() {
103
+ isRefreshing = true;
104
+ try {
105
+ await loadActivityData();
106
+ } catch (error) {
107
+ console.error('Error refreshing activity:', error);
108
+ }
109
+ isRefreshing = false;
110
+ }
111
+
112
+ function formatTime(date: Date): string {
113
+ const now = Date.now();
114
+ const diff = now - date.getTime();
115
+
116
+ const minutes = Math.floor(diff / (1000 * 60));
117
+ const hours = Math.floor(diff / (1000 * 60 * 60));
118
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
119
+
120
+ if (minutes < 1) return 'just now';
121
+ if (minutes < 60) return `${minutes}m ago`;
122
+ if (hours < 24) return `${hours}h ago`;
123
+ if (days < 7) return `${days}d ago`;
124
+
125
+ return date.toLocaleDateString();
126
+ }
127
+
128
+ function getRarityColor(rarity: string): string {
129
+ switch (rarity) {
130
+ case 'legendary': return '#FFD700';
131
+ case 'epic': return '#9B59B6';
132
+ case 'rare': return '#3498DB';
133
+ case 'uncommon': return '#2ECC71';
134
+ default: return '#95A5A6';
135
+ }
136
+ }
137
+
138
+ function getActivityIcon(type: string): string {
139
+ switch (type) {
140
+ case 'discovery': return '✨';
141
+ case 'variation': return '🔄';
142
+ case 'milestone': return '🏆';
143
+ default: return '📍';
144
+ }
145
+ }
146
+ </script>
147
+
148
+ <div class="activity-page">
149
+ <div class="view-selector">
150
+ <button
151
+ class="view-tab"
152
+ class:active={activeView === 'recent'}
153
+ onclick={() => activeView = 'recent'}
154
+ >
155
+ Recent Activity
156
+ </button>
157
+ <button
158
+ class="view-tab"
159
+ class:active={activeView === 'leaderboard'}
160
+ onclick={() => activeView = 'leaderboard'}
161
+ >
162
+ Leaderboard
163
+ </button>
164
+ </div>
165
+
166
+ <PullToRefresh onRefresh={handleRefresh}>
167
+ {#if isLoading}
168
+ <div class="loading">
169
+ <div class="spinner"></div>
170
+ <p>Loading activity...</p>
171
+ </div>
172
+ {:else if activeView === 'recent'}
173
+ {#if activities.length === 0}
174
+ <div class="empty-state">
175
+ <div class="empty-icon">🔍</div>
176
+ <h2>No Discoveries Yet</h2>
177
+ <p>Start scanning objects to discover Piclets!</p>
178
+ <p class="hint">Every object in the world has a unique Piclet waiting to be discovered.</p>
179
+ </div>
180
+ {:else}
181
+ <div class="activity-list">
182
+ {#each activities as activity, index (activity.id)}
183
+ <div
184
+ class="activity-card"
185
+ in:fly={{ y: 20, delay: index * 50 }}
186
+ >
187
+ <div class="activity-icon">
188
+ <span class="type-icon">{getActivityIcon(activity.type)}</span>
189
+ </div>
190
+
191
+ <div class="activity-info">
192
+ <h3>{activity.title}</h3>
193
+ <p>{activity.description}</p>
194
+ <div class="activity-meta">
195
+ <span
196
+ class="rarity-badge"
197
+ style="background-color: {getRarityColor(activity.rarity)}"
198
+ >
199
+ {activity.rarity}
200
+ </span>
201
+ <span class="time">{formatTime(activity.createdAt)}</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ {/each}
206
+ </div>
207
+ {/if}
208
+ {:else}
209
+ {#if leaderboard.length === 0}
210
+ <div class="empty-state">
211
+ <div class="empty-icon">🏆</div>
212
+ <h2>No Rankings Yet</h2>
213
+ <p>Be the first to discover Piclets!</p>
214
+ </div>
215
+ {:else}
216
+ <div class="leaderboard-list">
217
+ {#each leaderboard as entry, index (entry.username)}
218
+ <div
219
+ class="leaderboard-card"
220
+ in:fly={{ y: 20, delay: index * 50 }}
221
+ >
222
+ <div class="rank-badge" class:gold={entry.rank === 1} class:silver={entry.rank === 2} class:bronze={entry.rank === 3}>
223
+ #{entry.rank}
224
+ </div>
225
+
226
+ <div class="player-info">
227
+ <h3>{entry.username}</h3>
228
+ <div class="stats">
229
+ <span>🔍 {entry.uniqueDiscoveries} unique</span>
230
+ <span>📊 {entry.totalDiscoveries} total</span>
231
+ <span>⭐ {entry.rarityScore} points</span>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ {/each}
236
+ </div>
237
+ {/if}
238
+ {/if}
239
+ </PullToRefresh>
240
+ </div>
241
+
242
+ <style>
243
+ .activity-page {
244
+ height: 100%;
245
+ overflow: hidden;
246
+ display: flex;
247
+ flex-direction: column;
248
+ }
249
+
250
+ .view-selector {
251
+ display: flex;
252
+ gap: 0.5rem;
253
+ padding: 1rem;
254
+ background: #fff;
255
+ border-bottom: 1px solid #e0e0e0;
256
+ }
257
+
258
+ .view-tab {
259
+ flex: 1;
260
+ padding: 0.75rem;
261
+ border: 1px solid #e0e0e0;
262
+ border-radius: 8px;
263
+ background: #f5f5f5;
264
+ color: #666;
265
+ font-weight: 500;
266
+ cursor: pointer;
267
+ transition: all 0.2s;
268
+ }
269
+
270
+ .view-tab.active {
271
+ background: #007bff;
272
+ color: white;
273
+ border-color: #007bff;
274
+ }
275
+
276
+ .loading, .empty-state {
277
+ display: flex;
278
+ flex-direction: column;
279
+ align-items: center;
280
+ justify-content: center;
281
+ height: 60vh;
282
+ text-align: center;
283
+ padding: 1rem;
284
+ }
285
+
286
+ .spinner {
287
+ width: 48px;
288
+ height: 48px;
289
+ border: 4px solid #f0f0f0;
290
+ border-top-color: #007bff;
291
+ border-radius: 50%;
292
+ animation: spin 1s linear infinite;
293
+ margin-bottom: 1rem;
294
+ }
295
+
296
+ @keyframes spin {
297
+ to { transform: rotate(360deg); }
298
+ }
299
+
300
+ .empty-icon {
301
+ font-size: 4rem;
302
+ margin-bottom: 1rem;
303
+ }
304
+
305
+ .empty-state h2 {
306
+ margin: 0 0 0.5rem;
307
+ font-size: 1.25rem;
308
+ color: #333;
309
+ }
310
+
311
+ .empty-state p {
312
+ color: #666;
313
+ font-size: 0.9rem;
314
+ margin: 0.25rem 0;
315
+ }
316
+
317
+ .hint {
318
+ font-style: italic;
319
+ color: #999;
320
+ margin-top: 1rem;
321
+ }
322
+
323
+ .activity-list, .leaderboard-list {
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: 1rem;
327
+ padding: 1rem;
328
+ padding-bottom: 5rem;
329
+ }
330
+
331
+ .activity-card, .leaderboard-card {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 1rem;
335
+ background: #fff;
336
+ border: 1px solid #e0e0e0;
337
+ border-radius: 12px;
338
+ padding: 1rem;
339
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
340
+ transition: all 0.2s ease;
341
+ }
342
+
343
+ .activity-card:hover, .leaderboard-card:hover {
344
+ transform: translateY(-2px);
345
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
346
+ }
347
+
348
+ .activity-icon {
349
+ width: 48px;
350
+ height: 48px;
351
+ flex-shrink: 0;
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ background: #f0f7ff;
356
+ border-radius: 50%;
357
+ }
358
+
359
+ .type-icon {
360
+ font-size: 1.5rem;
361
+ }
362
+
363
+ .activity-info, .player-info {
364
+ flex: 1;
365
+ }
366
+
367
+ .activity-info h3, .player-info h3 {
368
+ margin: 0 0 0.25rem;
369
+ font-size: 1rem;
370
+ font-weight: 600;
371
+ color: #1a1a1a;
372
+ }
373
+
374
+ .activity-info p {
375
+ margin: 0 0 0.5rem;
376
+ font-size: 0.875rem;
377
+ color: #666;
378
+ }
379
+
380
+ .activity-meta {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 0.5rem;
384
+ }
385
+
386
+ .rarity-badge {
387
+ padding: 0.125rem 0.5rem;
388
+ border-radius: 12px;
389
+ font-size: 0.75rem;
390
+ font-weight: 600;
391
+ color: white;
392
+ text-transform: uppercase;
393
+ }
394
+
395
+ .time {
396
+ font-size: 0.75rem;
397
+ color: #999;
398
+ }
399
+
400
+ .rank-badge {
401
+ width: 48px;
402
+ height: 48px;
403
+ display: flex;
404
+ align-items: center;
405
+ justify-content: center;
406
+ border-radius: 50%;
407
+ background: #f0f0f0;
408
+ font-weight: bold;
409
+ font-size: 1.25rem;
410
+ flex-shrink: 0;
411
+ }
412
+
413
+ .rank-badge.gold {
414
+ background: linear-gradient(135deg, #FFD700, #FFA500);
415
+ color: white;
416
+ }
417
+
418
+ .rank-badge.silver {
419
+ background: linear-gradient(135deg, #C0C0C0, #808080);
420
+ color: white;
421
+ }
422
+
423
+ .rank-badge.bronze {
424
+ background: linear-gradient(135deg, #CD7F32, #8B4513);
425
+ color: white;
426
+ }
427
+
428
+ .stats {
429
+ display: flex;
430
+ gap: 1rem;
431
+ margin-top: 0.5rem;
432
+ font-size: 0.875rem;
433
+ color: #666;
434
+ }
435
+
436
+ .stats span {
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 0.25rem;
440
+ }
441
+ </style>
src/lib/components/Pages/Battle.svelte DELETED
@@ -1,177 +0,0 @@
1
- <script lang="ts">
2
- import { fade } from 'svelte/transition';
3
- import type { PicletInstance } from '$lib/db/schema';
4
- import LLMBattleEngine from '../Battle/LLMBattleEngine.svelte';
5
- import type { GradioClient } from '$lib/types';
6
-
7
- interface Props {
8
- playerPiclet: PicletInstance;
9
- enemyPiclet: PicletInstance;
10
- isWildBattle: boolean;
11
- rosterPiclets?: PicletInstance[];
12
- onBattleEnd: (result: { winner: 'player' | 'enemy'; capturedPiclet?: PicletInstance }) => void;
13
- commandClient: GradioClient;
14
- }
15
-
16
- let { playerPiclet, enemyPiclet, isWildBattle, rosterPiclets, onBattleEnd, commandClient }: Props = $props();
17
-
18
- // Simple battle state
19
- let battleEnded = $state(false);
20
- let battleResult: { winner: 'player' | 'enemy'; capturedPiclet?: PicletInstance } | null = $state(null);
21
-
22
- // Handle battle end
23
- function handleBattleEnd(winner: 'player' | 'enemy') {
24
- battleEnded = true;
25
-
26
- // Simplified: no capture mechanics since Piclets are auto-captured when scanned
27
- battleResult = { winner };
28
-
29
- // Give time for final battle messages, then call parent handler
30
- setTimeout(() => {
31
- onBattleEnd(battleResult!);
32
- }, 2000);
33
- }
34
- </script>
35
-
36
- <div class="battle-page" transition:fade>
37
- <!-- Header -->
38
- <div class="battle-header">
39
- <h2>{isWildBattle ? 'Wild Battle' : 'Trainer Battle'}</h2>
40
- <div class="battle-participants">
41
- <div class="participant player">
42
- <img src={playerPiclet.imageUrl} alt={playerPiclet.typeId} />
43
- <h3>{playerPiclet.typeId}</h3>
44
- <span class="tier tier-{playerPiclet.tier}">{playerPiclet.tier}</span>
45
- </div>
46
-
47
- <div class="vs-indicator">
48
- <span>VS</span>
49
- </div>
50
-
51
- <div class="participant enemy">
52
- <img src={enemyPiclet.imageUrl} alt={enemyPiclet.typeId} />
53
- <h3>{enemyPiclet.typeId}</h3>
54
- <span class="tier tier-{enemyPiclet.tier}">{enemyPiclet.tier}</span>
55
- </div>
56
- </div>
57
- </div>
58
-
59
- <!-- Main Battle Area -->
60
- <div class="battle-main">
61
- {#if !battleEnded}
62
- <LLMBattleEngine
63
- {playerPiclet}
64
- {enemyPiclet}
65
- {rosterPiclets}
66
- {commandClient}
67
- onBattleEnd={handleBattleEnd}
68
- />
69
- {:else}
70
- <div class="battle-results">
71
- <h2>{battleResult?.winner === 'player' ? '🎉 Victory!' : '💀 Defeat!'}</h2>
72
-
73
- {#if battleResult?.winner === 'player'}
74
- <p>You defeated {enemyPiclet.typeId}!</p>
75
- {:else}
76
- <p>{playerPiclet.typeId} was defeated by {enemyPiclet.typeId}...</p>
77
- <p>Better luck next time!</p>
78
- {/if}
79
- </div>
80
- {/if}
81
- </div>
82
- </div>
83
-
84
- <style>
85
- .battle-page {
86
- height: 100vh;
87
- display: flex;
88
- flex-direction: column;
89
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
90
- color: white;
91
- }
92
-
93
- .battle-header {
94
- padding: 1rem;
95
- text-align: center;
96
- background: rgba(0, 0, 0, 0.1);
97
- backdrop-filter: blur(10px);
98
- }
99
-
100
- .battle-participants {
101
- display: flex;
102
- align-items: center;
103
- justify-content: center;
104
- gap: 2rem;
105
- margin-top: 1rem;
106
- }
107
-
108
- .participant {
109
- display: flex;
110
- flex-direction: column;
111
- align-items: center;
112
- gap: 0.5rem;
113
- }
114
-
115
- .participant img {
116
- width: 80px;
117
- height: 80px;
118
- object-fit: cover;
119
- border-radius: 50%;
120
- border: 3px solid rgba(255, 255, 255, 0.3);
121
- }
122
-
123
- .participant.player img {
124
- border-color: #007bff;
125
- }
126
-
127
- .participant.enemy img {
128
- border-color: #dc3545;
129
- }
130
-
131
- .tier {
132
- padding: 0.25rem 0.5rem;
133
- border-radius: 12px;
134
- font-size: 0.8rem;
135
- font-weight: bold;
136
- text-transform: uppercase;
137
- }
138
-
139
- .tier-low { background: #6c757d; }
140
- .tier-medium { background: #28a745; }
141
- .tier-high { background: #fd7e14; }
142
- .tier-legendary { background: #dc3545; }
143
-
144
- .vs-indicator {
145
- font-size: 2rem;
146
- font-weight: bold;
147
- color: #ffd700;
148
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
149
- }
150
-
151
- .battle-main {
152
- flex: 1;
153
- display: flex;
154
- flex-direction: column;
155
- background: white;
156
- color: #333;
157
- border-radius: 20px 20px 0 0;
158
- overflow: hidden;
159
- }
160
-
161
- .battle-results {
162
- flex: 1;
163
- display: flex;
164
- flex-direction: column;
165
- align-items: center;
166
- justify-content: center;
167
- text-align: center;
168
- padding: 2rem;
169
- gap: 1rem;
170
- }
171
-
172
- .battle-results h2 {
173
- font-size: 2.5rem;
174
- margin: 0;
175
- }
176
-
177
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Pages/Encounters.svelte DELETED
@@ -1,586 +0,0 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import { fade, fly } from 'svelte/transition';
4
- import type { Encounter, GameState, PicletInstance } 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
- 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;
16
- let isRefreshing = false;
17
- let monsterImages: Map<string, string> = new Map();
18
-
19
- // Battle state
20
- let showBattle = false;
21
- let battlePlayerPiclet: PicletInstance | null = null;
22
- let battleEnemyPiclet: PicletInstance | null = null;
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
- });
33
-
34
- async function loadEncounters() {
35
- isLoading = true;
36
- try {
37
- // Check if we have any piclet instances
38
- const playerPiclets = await db.picletInstances.toArray();
39
-
40
- if (playerPiclets.length === 0) {
41
- // No piclets discovered/caught - show empty state
42
- encounters = [];
43
- isLoading = false;
44
- return;
45
- }
46
-
47
- // Player has piclets - always generate fresh encounters with wild piclets
48
- console.log('Player has piclets - generating fresh encounters with wild piclets');
49
- await EncounterService.forceEncounterRefresh();
50
- encounters = await EncounterService.generateEncounters();
51
-
52
- console.log('Final encounters:', encounters.map(e => ({ type: e.type, title: e.title })));
53
-
54
- // Load piclet images for wild encounters
55
- await loadPicletImages();
56
- } catch (error) {
57
- console.error('Error loading encounters:', error);
58
- }
59
- isLoading = false;
60
- }
61
-
62
- async function loadPicletImages() {
63
- const picletEncounters = encounters.filter(e =>
64
- (e.type === EncounterType.WILD_PICLET || e.type === EncounterType.FIRST_PICLET) && e.picletTypeId
65
- );
66
-
67
- for (const encounter of picletEncounters) {
68
- if (!encounter.picletTypeId) continue;
69
-
70
- // Find a piclet instance with this typeId
71
- const piclet = await db.picletInstances
72
- .where('typeId')
73
- .equals(encounter.picletTypeId)
74
- .first();
75
-
76
- if (piclet && piclet.imageData) {
77
- monsterImages.set(encounter.picletTypeId, piclet.imageData);
78
- }
79
- }
80
- // Trigger reactive update
81
- monsterImages = monsterImages;
82
- }
83
-
84
- async function handleRefresh() {
85
- isRefreshing = true;
86
- try {
87
- // Force refresh encounters
88
- console.log('Force refreshing encounters...');
89
- encounters = await EncounterService.generateEncounters();
90
-
91
- // Load piclet images for new encounters
92
- await loadPicletImages();
93
-
94
- // Update game state with new refresh time
95
- const gameState = await getOrCreateGameState();
96
- await db.gameState.update(gameState.id!, {
97
- lastEncounterRefresh: new Date()
98
- });
99
- } catch (error) {
100
- console.error('Error refreshing encounters:', error);
101
- }
102
- isRefreshing = false;
103
- }
104
-
105
- async function handleEncounterTap(encounter: Encounter) {
106
- if (encounter.type === EncounterType.FIRST_PICLET) {
107
- // First piclet encounter - direct catch
108
- await handleFirstPicletEncounter(encounter);
109
- } else if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
110
- // Regular wild encounter - start battle
111
- await startBattle(encounter);
112
- }
113
- }
114
-
115
-
116
- async function handleFirstPicletEncounter(encounter: Encounter) {
117
- try {
118
- if (!encounter.picletInstanceId) {
119
- throw new Error('No piclet instance specified for first piclet encounter');
120
- }
121
-
122
- // Get the specific piclet instance
123
- const picletInstance = await db.picletInstances.get(encounter.picletInstanceId);
124
- if (!picletInstance) {
125
- throw new Error('Piclet instance not found');
126
- }
127
-
128
- // Mark the piclet as caught and set it as the first roster piclet
129
- await db.picletInstances.update(encounter.picletInstanceId, {
130
- caught: true,
131
- caughtAt: new Date(),
132
- isInRoster: true,
133
- rosterPosition: 0,
134
- level: 3
135
- });
136
-
137
- // Get the updated piclet instance
138
- const caughtPiclet = await db.picletInstances.get(encounter.picletInstanceId);
139
-
140
- // Show the newly caught piclet detail page
141
- newlyCaughtPiclet = caughtPiclet!;
142
- showNewlyCaught = true;
143
-
144
- // Update counters and progress
145
- incrementCounter('picletsCapured');
146
- addProgressPoints(100);
147
-
148
- // Force refresh encounters to remove the first piclet encounter
149
- await forceEncounterRefresh();
150
- } catch (error) {
151
- console.error('Error handling first piclet encounter:', error);
152
- alert('Something went wrong. Please try again.');
153
- }
154
- }
155
-
156
- async function forceEncounterRefresh() {
157
- isRefreshing = true;
158
- try {
159
- await EncounterService.forceEncounterRefresh();
160
- encounters = await EncounterService.generateEncounters();
161
- await loadPicletImages();
162
- } catch (error) {
163
- console.error('Error refreshing encounters:', error);
164
- }
165
- isRefreshing = false;
166
- }
167
-
168
- function getEncounterIcon(encounter: Encounter): string {
169
- switch (encounter.type) {
170
- case EncounterType.FIRST_PICLET:
171
- return '✨';
172
- case EncounterType.WILD_PICLET:
173
- default:
174
- return '⚔️';
175
- }
176
- }
177
-
178
- function getEncounterColor(encounter: Encounter): string {
179
- switch (encounter.type) {
180
- case EncounterType.WILD_PICLET:
181
- return '#4caf50';
182
- case EncounterType.FIRST_PICLET:
183
- return '#ffd700';
184
- default:
185
- return '#607d8b';
186
- }
187
- }
188
-
189
- async function startBattle(encounter: Encounter) {
190
- try {
191
- // Get all piclet instances
192
- const allPiclets = await db.picletInstances.toArray();
193
-
194
- // Filter piclets that have a roster position (0-5)
195
- const rosterPiclets = allPiclets.filter(p =>
196
- p.rosterPosition !== undefined &&
197
- p.rosterPosition !== null &&
198
- p.rosterPosition >= 0 &&
199
- p.rosterPosition <= 5
200
- );
201
-
202
- // Sort by roster position
203
- rosterPiclets.sort((a, b) => (a.rosterPosition ?? 0) - (b.rosterPosition ?? 0));
204
-
205
- if (rosterPiclets.length === 0) {
206
- alert('You need at least one piclet in your roster to battle!');
207
- return;
208
- }
209
-
210
- // Check if there's at least one piclet in position 0
211
- const hasPosition0 = rosterPiclets.some(p => p.rosterPosition === 0);
212
- if (!hasPosition0) {
213
- alert('You need a piclet in the first roster slot (position 0) to battle!');
214
- return;
215
- }
216
-
217
- // Generate enemy piclet for battle
218
- const enemyPiclet = await generateEnemyPiclet(encounter);
219
- if (!enemyPiclet) return;
220
-
221
- // Set up battle
222
- battlePlayerPiclet = rosterPiclets[0];
223
- battleEnemyPiclet = enemyPiclet;
224
- battleIsWild = true;
225
- battleRosterPiclets = rosterPiclets; // Pass all roster piclets
226
- showBattle = true;
227
- uiStore.enterBattle();
228
- } catch (error) {
229
- console.error('Error starting battle:', error);
230
- }
231
- }
232
-
233
- async function generateEnemyPiclet(encounter: Encounter): Promise<PicletInstance | null> {
234
- if (!encounter.picletTypeId || !encounter.enemyLevel) return null;
235
-
236
- // Get a piclet instance with this typeId to use as a template
237
- const templatePiclet = await db.picletInstances
238
- .where('typeId')
239
- .equals(encounter.picletTypeId)
240
- .first();
241
-
242
- if (!templatePiclet) {
243
- console.error('Piclet template not found for typeId:', encounter.picletTypeId);
244
- return null;
245
- }
246
-
247
- // Calculate stats based on template's base stats and encounter level
248
- const level = encounter.enemyLevel;
249
-
250
- // Create enemy piclet instance based on template (simplified)
251
- const enemyPiclet: PicletInstance = {
252
- ...templatePiclet,
253
- id: -1, // Temporary ID for enemy
254
-
255
- isInRoster: false,
256
- caughtAt: new Date()
257
- };
258
-
259
- return enemyPiclet;
260
- }
261
-
262
- async function addCapturedPicletToRoster(capturedPiclet: PicletInstance) {
263
- try {
264
- // Get all roster piclets to find the next available position
265
- const allPiclets = await db.picletInstances.toArray();
266
- const rosterPiclets = allPiclets.filter(p =>
267
- p.rosterPosition !== undefined &&
268
- p.rosterPosition !== null &&
269
- p.rosterPosition >= 0 &&
270
- p.rosterPosition <= 5
271
- );
272
-
273
- // Find next available roster position (0-5)
274
- let nextPosition = 0;
275
- const occupiedPositions = new Set(rosterPiclets.map(p => p.rosterPosition));
276
- while (occupiedPositions.has(nextPosition) && nextPosition <= 5) {
277
- nextPosition++;
278
- }
279
-
280
- if (nextPosition > 5) {
281
- // Roster is full - for now just add to position 5 (could implement storage system later)
282
- console.warn('Roster is full, overriding position 5');
283
- nextPosition = 5;
284
- }
285
-
286
- console.log('About to create captured Piclet in database:', {
287
- id: capturedPiclet.id,
288
- nickname: capturedPiclet.nickname,
289
- typeId: capturedPiclet.typeId,
290
- level: capturedPiclet.level
291
- });
292
-
293
- // Create a new database record for the captured Piclet (enemy has temporary ID -1)
294
- const newPicletData = {
295
- ...capturedPiclet,
296
- // Remove the temporary ID so database auto-generates a real one
297
- id: undefined as any,
298
- caught: true,
299
- caughtAt: new Date(),
300
- isInRoster: true,
301
- rosterPosition: nextPosition
302
- };
303
-
304
- // Add the captured piclet to the database
305
- const newPicletId = await db.picletInstances.add(newPicletData);
306
- console.log('Created new database record with ID:', newPicletId);
307
-
308
- // Get the newly created piclet instance and show detail page
309
- const createdPiclet = await db.picletInstances.get(newPicletId);
310
- console.log('Retrieved newly created piclet from database:', createdPiclet);
311
-
312
- if (createdPiclet) {
313
- console.log('Setting newlyCaughtPiclet and showing detail page:', createdPiclet.nickname);
314
- newlyCaughtPiclet = createdPiclet;
315
- showNewlyCaught = true;
316
- console.log('showNewlyCaught is now:', showNewlyCaught);
317
- console.log(`Added captured Piclet ${createdPiclet.nickname} to roster position ${nextPosition}`);
318
- } else {
319
- console.error('Could not retrieve newly created piclet from database');
320
- // Use fallback data
321
- const fallbackPiclet = {
322
- ...capturedPiclet,
323
- id: newPicletId
324
- };
325
- newlyCaughtPiclet = fallbackPiclet;
326
- showNewlyCaught = true;
327
- }
328
- } catch (error) {
329
- console.error('Error adding captured Piclet to roster:', error);
330
- }
331
- }
332
-
333
- async function handleBattleEnd(result: any) {
334
- showBattle = false;
335
- uiStore.exitBattle();
336
-
337
- if (result === true) {
338
- // Victory
339
- console.log('Battle won!');
340
- // Force refresh encounters after battle
341
- forceEncounterRefresh();
342
- } else if (result === false) {
343
- // Defeat or ran away
344
- console.log('Battle lost or fled');
345
- // Force refresh encounters after battle
346
- forceEncounterRefresh();
347
- } else if (result && result.id) {
348
- // Caught a piclet - add to roster
349
- console.log('Piclet caught!', result);
350
- await addCapturedPicletToRoster(result);
351
- incrementCounter('picletsCapured');
352
- addProgressPoints(100);
353
- // Don't refresh encounters immediately - let user view the caught piclet first
354
- // Refresh will happen when they close the detail dialog
355
- }
356
- }
357
- </script>
358
-
359
- {#if showBattle && battlePlayerPiclet && battleEnemyPiclet}
360
- <Battle
361
- playerPiclet={battlePlayerPiclet}
362
- enemyPiclet={battleEnemyPiclet}
363
- isWildBattle={battleIsWild}
364
- rosterPiclets={battleRosterPiclets}
365
- onBattleEnd={handleBattleEnd}
366
- />
367
- {:else}
368
- <div class="encounters-page">
369
- <PullToRefresh onRefresh={handleRefresh}>
370
- {#if isLoading}
371
- <div class="loading">
372
- <div class="spinner"></div>
373
- <p>Loading encounters...</p>
374
- </div>
375
- {:else if encounters.length === 0}
376
- <div class="empty-state">
377
- <div class="empty-icon">📸</div>
378
- <h2>No Piclets Discovered</h2>
379
- <p>To start your adventure, select the Snap logo image:</p>
380
- <div class="logo-instruction">
381
- <img src="/assets/snap_logo.png" alt="Snap Logo" class="snap-logo-preview" />
382
- <p class="instruction-text">↑ Select this image in the scanner</p>
383
- </div>
384
- </div>
385
- {:else}
386
- <div class="encounters-list">
387
- {#each encounters as encounter, index (encounter.id)}
388
- <button
389
- class="encounter-card"
390
- style="border-color: {getEncounterColor(encounter)}30"
391
- on:click={() => handleEncounterTap(encounter)}
392
- in:fly={{ y: 20, delay: index * 50 }}
393
- disabled={isRefreshing}
394
- >
395
- <div class="encounter-icon">
396
- {#if (encounter.type === EncounterType.WILD_PICLET || encounter.type === EncounterType.FIRST_PICLET) && encounter.picletTypeId}
397
- {#if monsterImages.has(encounter.picletTypeId)}
398
- <img
399
- src={monsterImages.get(encounter.picletTypeId)}
400
- alt="Piclet"
401
- />
402
- {:else}
403
- <div class="fallback-icon">{getEncounterIcon(encounter)}</div>
404
- {/if}
405
- {:else}
406
- <span class="type-icon">{getEncounterIcon(encounter)}</span>
407
- {/if}
408
- </div>
409
-
410
- <div class="encounter-info">
411
- <h3>{encounter.title}</h3>
412
- <p>{encounter.description}</p>
413
- </div>
414
-
415
- <div class="encounter-arrow">›</div>
416
- </button>
417
- {/each}
418
- </div>
419
- {/if}
420
- </PullToRefresh>
421
- </div>
422
- {/if}
423
-
424
- <!-- Newly Caught Piclet Dialog -->
425
- {#if showNewlyCaught && newlyCaughtPiclet}
426
- <NewlyCaughtPicletDetail
427
- instance={newlyCaughtPiclet}
428
- onClose={() => {
429
- showNewlyCaught = false;
430
- newlyCaughtPiclet = null;
431
- // Refresh encounters after user closes the detail dialog
432
- forceEncounterRefresh();
433
- }}
434
- />
435
- {/if}
436
-
437
- <style>
438
- .encounters-page {
439
- height: 100%;
440
- overflow: hidden; /* PullToRefresh handles scrolling */
441
- }
442
-
443
- .loading, .empty-state {
444
- display: flex;
445
- flex-direction: column;
446
- align-items: center;
447
- justify-content: center;
448
- height: 60vh;
449
- text-align: center;
450
- padding: 1rem;
451
- }
452
-
453
- .spinner {
454
- width: 48px;
455
- height: 48px;
456
- border: 4px solid #f0f0f0;
457
- border-top-color: #4caf50;
458
- border-radius: 50%;
459
- animation: spin 1s linear infinite;
460
- margin-bottom: 1rem;
461
- }
462
-
463
- @keyframes spin {
464
- to { transform: rotate(360deg); }
465
- }
466
-
467
- .empty-icon {
468
- font-size: 4rem;
469
- margin-bottom: 1rem;
470
- }
471
-
472
- .empty-state h2 {
473
- margin: 0 0 0.5rem;
474
- font-size: 1.25rem;
475
- color: #333;
476
- }
477
-
478
- .empty-state p {
479
- color: #666;
480
- font-size: 0.9rem;
481
- }
482
-
483
- .encounters-list {
484
- display: flex;
485
- flex-direction: column;
486
- gap: 1rem;
487
- padding: 1rem;
488
- padding-bottom: 5rem;
489
- }
490
-
491
- .encounter-card {
492
- display: flex;
493
- align-items: center;
494
- gap: 1rem;
495
- background: #fff;
496
- border: 2px solid;
497
- border-radius: 12px;
498
- padding: 1rem;
499
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
500
- transition: all 0.2s ease;
501
- cursor: pointer;
502
- width: 100%;
503
- text-align: left;
504
- }
505
-
506
- .encounter-card:hover:not(:disabled) {
507
- transform: translateY(-2px);
508
- box-shadow: 0 4px 12px rgba(0,0,0,0.12);
509
- }
510
-
511
- .encounter-card:disabled {
512
- opacity: 0.6;
513
- cursor: not-allowed;
514
- }
515
-
516
- .encounter-icon {
517
- width: 60px;
518
- height: 60px;
519
- flex-shrink: 0;
520
- display: flex;
521
- align-items: center;
522
- justify-content: center;
523
- }
524
-
525
- .encounter-icon img {
526
- width: 100%;
527
- height: 100%;
528
- object-fit: cover;
529
- border-radius: 8px;
530
- }
531
-
532
-
533
-
534
-
535
- .type-icon, .fallback-icon {
536
- font-size: 2rem;
537
- }
538
-
539
- .encounter-info {
540
- flex: 1;
541
- }
542
-
543
- .encounter-info h3 {
544
- margin: 0 0 0.25rem;
545
- font-size: 1.1rem;
546
- font-weight: 600;
547
- color: #1a1a1a;
548
- }
549
-
550
- .encounter-info p {
551
- margin: 0;
552
- font-size: 0.875rem;
553
- color: #666;
554
- }
555
-
556
- .encounter-arrow {
557
- font-size: 1.5rem;
558
- color: #999;
559
- }
560
-
561
- .logo-instruction {
562
- margin-top: 1.5rem;
563
- display: flex;
564
- flex-direction: column;
565
- align-items: center;
566
- gap: 0.5rem;
567
- }
568
-
569
- .snap-logo-preview {
570
- width: 120px;
571
- height: 120px;
572
- object-fit: contain;
573
- border: 2px dashed #007bff;
574
- border-radius: 12px;
575
- padding: 1rem;
576
- background: #f0f7ff;
577
- }
578
-
579
- .instruction-text {
580
- font-size: 0.875rem;
581
- color: #007bff;
582
- font-weight: 500;
583
- margin: 0;
584
- }
585
-
586
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Pages/Pictuary.svelte CHANGED
@@ -1,576 +1,374 @@
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
- import { getCaughtPiclets, getRosterPiclets, moveToRoster, swapRosterPositions, moveToStorage, getUncaughtPiclets } from '$lib/db/piclets';
4
  import type { PicletInstance } from '$lib/db/schema';
5
  import { db } from '$lib/db';
6
  import PicletCard from '../Piclets/PicletCard.svelte';
7
- import EmptySlotCard from '../Piclets/EmptySlotCard.svelte';
8
- import DraggablePicletCard from '../Piclets/DraggablePicletCard.svelte';
9
- import RosterSlot from '../Piclets/RosterSlot.svelte';
10
  import PicletDetail from '../Piclets/PicletDetail.svelte';
11
- import AddToRosterDialog from '../Piclets/AddToRosterDialog.svelte';
12
- import ViewAll from './ViewAll.svelte';
13
- import { PicletType } from '$lib/types/picletTypes';
14
-
15
- let rosterPiclets: PicletInstance[] = $state([]);
16
- let storagePiclets: PicletInstance[] = $state([]);
17
- let discoveredPiclets: PicletInstance[] = $state([]);
18
  let isLoading = $state(true);
19
- let currentlyDragging: PicletInstance | null = $state(null);
20
  let selectedPiclet: PicletInstance | null = $state(null);
21
- let addToRosterPosition: number | null = $state(null);
22
- let viewAllMode: 'storage' | 'discovered' | null = $state(null);
23
-
24
- // Map roster positions for easy access
25
- let rosterMap = $derived(() => {
26
- const map = new Map<number, PicletInstance>();
27
- rosterPiclets.forEach(piclet => {
28
- if (piclet.rosterPosition !== undefined) {
29
- map.set(piclet.rosterPosition, piclet);
30
- }
31
- });
32
- return map;
33
- });
34
 
35
- // Get the most common type in the roster for background theming
36
- let dominantType = $derived(() => {
37
- if (rosterPiclets.length === 0) {
38
- return PicletType.BEAST; // Default fallback
 
 
 
 
 
39
  }
40
 
41
- // Count type occurrences
42
- const typeCounts = new Map<PicletType, number>();
43
- rosterPiclets.forEach(piclet => {
44
- if (piclet.primaryType) {
45
- const count = typeCounts.get(piclet.primaryType) || 0;
46
- typeCounts.set(piclet.primaryType, count + 1);
47
- }
48
- });
 
 
 
 
49
 
50
- // Find the most common type
51
- let maxCount = 0;
52
- let mostCommonType = PicletType.BEAST;
53
- typeCounts.forEach((count, type) => {
54
- if (count > maxCount) {
55
- maxCount = count;
56
- mostCommonType = type;
57
- }
 
58
  });
59
 
60
- return mostCommonType;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  });
62
 
63
- // Get background image path for the dominant type
64
- let backgroundImagePath = $derived(`/classes/${dominantType}.png`);
65
-
66
  async function loadPiclets() {
 
67
  try {
68
- // Run type migration first time to fix any invalid types
69
-
70
- const allInstances = await getCaughtPiclets();
71
-
72
- // Filter based on rosterPosition instead of isInRoster
73
- rosterPiclets = allInstances.filter(p =>
74
- p.rosterPosition !== undefined &&
75
- p.rosterPosition !== null &&
76
- p.rosterPosition >= 0 &&
77
- p.rosterPosition <= 5
78
- );
79
- storagePiclets = allInstances.filter(p =>
80
- p.rosterPosition === undefined ||
81
- p.rosterPosition === null ||
82
- p.rosterPosition < 0 ||
83
- p.rosterPosition > 5
84
  );
85
-
86
- // Get all uncaught piclets (discovered but not caught)
87
- discoveredPiclets = await getUncaughtPiclets();
88
- } catch (err) {
89
- console.error('Failed to load piclets:', err);
90
  } finally {
91
  isLoading = false;
92
  }
93
  }
94
-
95
- async function handleBulkDownload() {
96
- try {
97
- console.log('Starting bulk download of all Piclets...');
98
-
99
- // Get all piclets (roster + storage + discovered)
100
- const allPiclets = [...rosterPiclets, ...storagePiclets, ...discoveredPiclets];
101
-
102
- if (allPiclets.length === 0) {
103
- console.log('No Piclets to export');
104
- return;
105
- }
106
-
107
- // Get trainer scan data for all piclets
108
- const trainerScanData = await db.trainerScanProgress
109
- .where('status').equals('completed')
110
- .toArray();
111
-
112
- // Create a map of piclet ID to trainer data
113
- const trainerDataMap = new Map();
114
- trainerScanData.forEach(scan => {
115
- if (scan.picletInstanceId) {
116
- trainerDataMap.set(scan.picletInstanceId, {
117
- trainerName: scan.trainerName,
118
- imagePath: scan.imagePath,
119
- imageIndex: scan.imageIndex,
120
- completedAt: scan.completedAt,
121
- remoteUrl: scan.remoteUrl
122
- });
123
- }
124
- });
125
-
126
- // Create export data for each piclet
127
- const exportData = {
128
- exportInfo: {
129
- totalPiclets: allPiclets.length,
130
- exportedAt: new Date().toISOString(),
131
- exportSource: "Pictuary Game - Bulk Export",
132
- version: "1.0"
133
- },
134
- piclets: allPiclets.map(piclet => {
135
- const trainerInfo = trainerDataMap.get(piclet.id!);
136
-
137
- return {
138
- // Core piclet data (complete dataset)
139
- id: piclet.id,
140
- typeId: piclet.typeId,
141
- nickname: piclet.nickname,
142
- primaryType: piclet.primaryType,
143
-
144
- // Current Stats
145
- currentHp: piclet.currentHp,
146
- maxHp: piclet.maxHp,
147
- level: piclet.level,
148
- xp: piclet.xp,
149
- attack: piclet.attack,
150
- defense: piclet.defense,
151
- fieldAttack: piclet.fieldAttack,
152
- fieldDefense: piclet.fieldDefense,
153
- speed: piclet.speed,
154
-
155
- // Base Stats
156
- baseHp: piclet.baseHp,
157
- baseAttack: piclet.baseAttack,
158
- baseDefense: piclet.baseDefense,
159
- baseFieldAttack: piclet.baseFieldAttack,
160
- baseFieldDefense: piclet.baseFieldDefense,
161
- baseSpeed: piclet.baseSpeed,
162
-
163
- // Battle data
164
- moves: piclet.moves,
165
- nature: piclet.nature,
166
- specialAbility: piclet.specialAbility,
167
- specialAbilityUnlockLevel: piclet.specialAbilityUnlockLevel,
168
-
169
- // Roster info
170
- isInRoster: piclet.isInRoster,
171
- rosterPosition: piclet.rosterPosition,
172
-
173
- // Metadata
174
- caught: piclet.caught,
175
- caughtAt: piclet.caughtAt,
176
- bst: piclet.bst,
177
- tier: piclet.tier,
178
- role: piclet.role,
179
- variance: piclet.variance,
180
-
181
- // Original generation data
182
- imageUrl: piclet.imageUrl,
183
- imageData: piclet.imageData,
184
- imageCaption: piclet.imageCaption,
185
- concept: piclet.concept,
186
- description: piclet.description,
187
- imagePrompt: piclet.imagePrompt,
188
-
189
- // Trainer scanner data (if available)
190
- trainerInfo: trainerInfo || null
191
- };
192
- })
193
- };
194
-
195
- // Create and download JSON file
196
- const jsonString = JSON.stringify(exportData, null, 2);
197
- const blob = new Blob([jsonString], { type: 'application/json' });
198
- const url = URL.createObjectURL(blob);
199
-
200
- // Create temporary download link
201
- const link = document.createElement('a');
202
- link.href = url;
203
- link.download = `pictuary-collection-${Date.now()}.json`;
204
-
205
- // Trigger download
206
- document.body.appendChild(link);
207
- link.click();
208
- document.body.removeChild(link);
209
-
210
- // Clean up the blob URL
211
- URL.revokeObjectURL(url);
212
-
213
- console.log(`Successfully exported ${allPiclets.length} Piclets with trainer data`);
214
- } catch (error) {
215
- console.error('Failed to export Piclet collection:', error);
216
- }
217
- }
218
-
219
- onMount(() => {
220
- loadPiclets();
221
- });
222
-
223
- function handleRosterClick(position: number) {
224
- const piclet = rosterMap().get(position);
225
- if (piclet) {
226
- selectedPiclet = piclet;
227
- } else {
228
- addToRosterPosition = position;
229
- }
230
- }
231
-
232
- function handleStorageClick(piclet: PicletInstance) {
233
  selectedPiclet = piclet;
234
  }
235
-
236
-
237
- function handleDragStart(instance: PicletInstance) {
238
- currentlyDragging = instance;
239
- }
240
-
241
- function handleDragEnd() {
242
- currentlyDragging = null;
243
  }
244
-
245
- async function handleRosterDrop(position: number, dragData: any) {
246
- if (!dragData.instanceId) return;
247
-
248
- try {
249
- const draggedPiclet = [...rosterPiclets, ...storagePiclets].find(p => p.id === dragData.instanceId);
250
- if (!draggedPiclet) return;
251
-
252
- const targetPiclet = rosterMap().get(position);
253
-
254
- if (dragData.fromRoster && targetPiclet) {
255
- // Swap two roster positions
256
- await swapRosterPositions(
257
- dragData.instanceId,
258
- dragData.fromPosition,
259
- targetPiclet.id!,
260
- position
261
- );
262
- } else {
263
- // Move to roster (possibly replacing existing)
264
- await moveToRoster(dragData.instanceId, position);
265
- }
266
-
267
- await loadPiclets();
268
- } catch (err) {
269
- console.error('Failed to handle drop:', err);
270
- }
271
  }
272
  </script>
273
 
274
- {#if viewAllMode === 'storage'}
275
- <ViewAll
276
- title="Storage"
277
- type="storage"
278
- items={storagePiclets}
279
- onBack={() => viewAllMode = null}
280
- onItemsChanged={loadPiclets}
281
- onDragStart={handleDragStart}
282
- onDragEnd={handleDragEnd}
283
- />
284
- {:else if viewAllMode === 'discovered'}
285
- <ViewAll
286
- title="Discovered"
287
- type="discovered"
288
- items={discoveredPiclets}
289
- onBack={() => viewAllMode = null}
290
- />
291
- {:else}
292
- <div class="pictuary-page" style="--bg-image: url('{backgroundImagePath}')">
293
- {#if isLoading}
294
- <div class="loading-state">
295
- <div class="spinner"></div>
296
- <p>Loading collection...</p>
 
 
297
  </div>
298
- {:else if rosterPiclets.length === 0 && storagePiclets.length === 0 && discoveredPiclets.length === 0}
299
- <div class="empty-state">
300
- <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
301
- <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
302
- <circle cx="12" cy="13" r="4"></circle>
303
- </svg>
304
- <h3>No Piclets Yet</h3>
305
- <p>Take photos to discover new Piclets!</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  </div>
307
- {:else}
308
- <div class="content">
309
- <!-- Roster Section -->
310
- <section class="roster-section">
311
- <h2>Roster</h2>
312
- <div class="roster-grid">
313
- {#each Array(6) as _, position}
314
- <RosterSlot
315
- {position}
316
- piclet={rosterMap().get(position)}
317
- size={110}
318
- onDrop={handleRosterDrop}
319
- onPicletClick={(piclet) => handleRosterClick(position)}
320
- onEmptyClick={handleRosterClick}
321
- onDragStart={handleDragStart}
322
- onDragEnd={handleDragEnd}
323
- />
324
- {/each}
325
- </div>
326
- </section>
327
-
328
- <!-- Storage Section -->
329
- {#if storagePiclets.length > 0}
330
- <section class="storage-section">
331
- <div class="section-header">
332
- <h2>Storage ({storagePiclets.length})</h2>
333
- {#if storagePiclets.length > 10}
334
- <button class="view-all-btn">View All</button>
 
335
  {/if}
336
  </div>
337
- <div class="horizontal-scroll">
338
- {#each storagePiclets.slice(0, 10) as piclet}
339
- <DraggablePicletCard
340
- instance={piclet}
341
- size={110}
342
- onClick={() => handleStorageClick(piclet)}
343
- onDragStart={handleDragStart}
344
- onDragEnd={handleDragEnd}
345
- />
346
- {/each}
347
- </div>
348
- </section>
349
- {/if}
350
-
351
- <!-- Discovered Section -->
352
- {#if discoveredPiclets.length > 0}
353
- <section class="discovered-section">
354
- <div class="section-header">
355
- <h2>Discovered ({discoveredPiclets.length})</h2>
356
- <button
357
- class="download-button"
358
- onclick={handleBulkDownload}
359
- title="Download all Piclets with trainer data"
360
- >
361
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
362
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
363
- <polyline points="7,10 12,15 17,10"></polyline>
364
- <line x1="12" y1="15" x2="12" y2="3"></line>
365
- </svg>
366
- Download
367
- </button>
368
- </div>
369
- <div class="discovered-grid">
370
- {#each discoveredPiclets as piclet}
371
- <PicletCard
372
- piclet={piclet}
373
- size={100}
374
- onClick={() => selectedPiclet = piclet}
375
- />
376
- {/each}
377
- </div>
378
- </section>
379
- {/if}
380
- </div>
381
- {/if}
382
-
383
- {#if selectedPiclet}
384
- <PicletDetail
385
- instance={selectedPiclet}
386
- onClose={() => selectedPiclet = null}
387
- onDeleted={loadPiclets}
388
- />
389
- {/if}
390
-
391
- {#if addToRosterPosition !== null}
392
- <AddToRosterDialog
393
- position={addToRosterPosition}
394
- availablePiclets={storagePiclets}
395
- onClose={() => addToRosterPosition = null}
396
- onAdded={loadPiclets}
397
- />
398
- {/if}
399
  </div>
 
 
 
 
 
 
 
 
 
400
  {/if}
401
 
402
  <style>
403
  .pictuary-page {
404
  height: 100%;
405
  overflow-y: auto;
406
- -webkit-overflow-scrolling: touch;
407
- background: white;
408
- position: relative;
409
  }
410
-
411
- .pictuary-page::before {
412
- content: '';
413
- position: fixed;
414
- top: 0;
415
- left: 0;
416
- right: 0;
417
- bottom: 0;
418
- background-image: var(--bg-image);
419
- background-size: 300px 300px;
420
- background-repeat: no-repeat;
421
- background-position: center bottom;
422
- opacity: 0.03;
423
- pointer-events: none;
424
- z-index: 0;
425
  }
426
-
427
-
428
- .loading-state,
429
- .empty-state {
 
 
 
 
 
 
 
 
 
430
  display: flex;
431
  flex-direction: column;
432
  align-items: center;
433
- justify-content: center;
434
- height: calc(100% - 100px);
435
- padding: 2rem;
436
- text-align: center;
437
- position: relative;
438
- z-index: 1;
439
- }
440
-
441
- .spinner {
442
- width: 40px;
443
- height: 40px;
444
- border: 3px solid #f3f3f3;
445
- border-top: 3px solid #007bff;
446
- border-radius: 50%;
447
- animation: spin 1s linear infinite;
448
- margin-bottom: 1rem;
449
  }
450
-
451
- .empty-state svg {
452
- color: #8e8e93;
453
- margin-bottom: 1rem;
454
- }
455
-
456
- .empty-state h3 {
457
- margin: 0 0 0.5rem;
458
- font-size: 1.25rem;
459
- font-weight: 600;
460
- color: #333;
461
  }
462
-
463
- .empty-state p {
464
- margin: 0;
465
  color: #666;
 
466
  }
467
-
468
- .content {
469
- padding: 0 1rem 100px;
470
- position: relative;
471
- z-index: 1;
472
- }
473
-
474
- section {
475
- margin-bottom: 2rem;
476
  }
477
-
478
- section h2 {
479
- font-size: 1.5rem;
480
- font-weight: bold;
481
- color: #8e8e93;
482
- margin: 0 0 0.75rem;
 
483
  }
484
-
485
- .section-header {
486
  display: flex;
487
- justify-content: space-between;
488
- align-items: center;
489
- margin-bottom: 0.75rem;
490
- }
491
-
492
- .section-header h2 {
493
- margin: 0;
494
  }
495
-
496
- .view-all-btn {
497
- background: none;
498
- border: none;
499
- color: #007bff;
500
- font-size: 1rem;
 
 
501
  cursor: pointer;
502
- padding: 0;
503
  }
504
-
505
- .roster-grid {
506
- display: grid;
507
- grid-template-columns: repeat(3, 1fr);
508
- grid-template-rows: repeat(2, 1fr);
509
- gap: 12px;
 
 
 
 
510
  }
511
-
512
- .horizontal-scroll {
513
  display: flex;
514
- gap: 8px;
515
- overflow-x: auto;
516
- -webkit-overflow-scrolling: touch;
517
- padding-bottom: 8px;
 
518
  }
519
-
520
- .horizontal-scroll::-webkit-scrollbar {
521
- height: 4px;
 
 
 
 
 
522
  }
523
-
524
- .horizontal-scroll::-webkit-scrollbar-track {
525
- background: #f1f1f1;
526
- border-radius: 2px;
527
  }
528
-
529
- .horizontal-scroll::-webkit-scrollbar-thumb {
530
- background: #888;
531
- border-radius: 2px;
 
532
  }
533
-
534
- .discovered-grid {
535
  display: grid;
536
- grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
537
- gap: 12px;
538
  }
539
-
540
- .download-button {
541
- display: flex;
542
- align-items: center;
543
- gap: 0.5rem;
544
- background: linear-gradient(135deg, #007bff, #0056b3);
545
- border: none;
546
- color: white;
547
- padding: 0.5rem 1rem;
548
- border-radius: 8px;
549
- font-size: 0.9rem;
550
- font-weight: 500;
551
  cursor: pointer;
552
- transition: all 0.2s ease;
553
- box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
554
  }
555
-
556
- .download-button:hover {
557
- background: linear-gradient(135deg, #0056b3, #004085);
558
- transform: translateY(-1px);
559
- box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
560
- }
561
-
562
- .download-button:active {
563
- transform: translateY(0);
564
- box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
565
  }
566
-
567
- .download-button svg {
568
- width: 16px;
569
- height: 16px;
570
- stroke-width: 2.5;
 
 
 
 
 
 
 
 
571
  }
572
-
573
- @keyframes spin {
574
- to { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
575
  }
576
  </style>
 
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
+ import { getCollectedPiclets, getCanonicalPiclets } from '$lib/db/piclets';
4
  import type { PicletInstance } from '$lib/db/schema';
5
  import { db } from '$lib/db';
6
  import PicletCard from '../Piclets/PicletCard.svelte';
 
 
 
7
  import PicletDetail from '../Piclets/PicletDetail.svelte';
8
+ import { CanonicalService } from '$lib/services/canonicalService';
9
+
10
+ let collectedPiclets: PicletInstance[] = $state([]);
11
+ let canonicalPiclets: PicletInstance[] = $state([]);
 
 
 
12
  let isLoading = $state(true);
 
13
  let selectedPiclet: PicletInstance | null = $state(null);
14
+ let viewMode: 'all' | 'canonical' | 'variations' = $state('all');
15
+ let searchQuery = $state('');
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ // Filter piclets based on view mode and search
18
+ let filteredPiclets = $derived(() => {
19
+ let piclets = collectedPiclets;
20
+
21
+ // Filter by view mode
22
+ if (viewMode === 'canonical') {
23
+ piclets = piclets.filter(p => p.isCanonical);
24
+ } else if (viewMode === 'variations') {
25
+ piclets = piclets.filter(p => !p.isCanonical && p.canonicalId);
26
  }
27
 
28
+ // Filter by search query
29
+ if (searchQuery.trim()) {
30
+ const query = searchQuery.toLowerCase();
31
+ piclets = piclets.filter(p =>
32
+ p.objectName?.toLowerCase().includes(query) ||
33
+ p.nickname?.toLowerCase().includes(query) ||
34
+ p.typeId.toLowerCase().includes(query)
35
+ );
36
+ }
37
+
38
+ return piclets;
39
+ });
40
 
41
+ // Group piclets by object for display
42
+ let groupedPiclets = $derived(() => {
43
+ const groups = new Map<string, PicletInstance[]>();
44
+
45
+ filteredPiclets.forEach(piclet => {
46
+ const key = piclet.objectName || 'unknown';
47
+ const group = groups.get(key) || [];
48
+ group.push(piclet);
49
+ groups.set(key, group);
50
  });
51
 
52
+ return Array.from(groups.entries()).sort((a, b) =>
53
+ a[0].localeCompare(b[0])
54
+ );
55
+ });
56
+
57
+ // Stats calculation
58
+ let stats = $derived(() => {
59
+ const totalDiscovered = collectedPiclets.length;
60
+ const uniqueObjects = new Set(collectedPiclets.map(p => p.objectName)).size;
61
+ const canonicalCount = collectedPiclets.filter(p => p.isCanonical).length;
62
+ const variationCount = collectedPiclets.filter(p => !p.isCanonical).length;
63
+
64
+ // Calculate total rarity score
65
+ const rarityScore = collectedPiclets.reduce((sum, p) => {
66
+ const points = CanonicalService.calculateRarity(p.scanCount);
67
+ const multiplier = points === 'legendary' ? 100 : points === 'epic' ? 50 :
68
+ points === 'rare' ? 20 : points === 'uncommon' ? 10 : 5;
69
+ return sum + multiplier;
70
+ }, 0);
71
+
72
+ return {
73
+ totalDiscovered,
74
+ uniqueObjects,
75
+ canonicalCount,
76
+ variationCount,
77
+ rarityScore
78
+ };
79
+ });
80
+
81
+ onMount(async () => {
82
+ await loadPiclets();
83
  });
84
 
 
 
 
85
  async function loadPiclets() {
86
+ isLoading = true;
87
  try {
88
+ collectedPiclets = await getCollectedPiclets();
89
+ canonicalPiclets = await getCanonicalPiclets();
90
+
91
+ // Sort by discovery date (most recent first)
92
+ collectedPiclets.sort((a, b) =>
93
+ (b.discoveredAt?.getTime() || 0) - (a.discoveredAt?.getTime() || 0)
 
 
 
 
 
 
 
 
 
 
94
  );
95
+ } catch (error) {
96
+ console.error('Failed to load piclets:', error);
 
 
 
97
  } finally {
98
  isLoading = false;
99
  }
100
  }
101
+
102
+ function handlePicletClick(piclet: PicletInstance) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  selectedPiclet = piclet;
104
  }
105
+
106
+ function handleDetailClose() {
107
+ selectedPiclet = null;
 
 
 
 
 
108
  }
109
+
110
+ async function handlePicletDeleted() {
111
+ await loadPiclets();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
  </script>
114
 
115
+ <div class="pictuary-page">
116
+ <!-- Header with Stats -->
117
+ <div class="header">
118
+ <h1>Pictuary</h1>
119
+ <div class="stats">
120
+ <div class="stat-item">
121
+ <span class="stat-value">{stats.totalDiscovered}</span>
122
+ <span class="stat-label">Total</span>
123
+ </div>
124
+ <div class="stat-item">
125
+ <span class="stat-value">{stats.uniqueObjects}</span>
126
+ <span class="stat-label">Objects</span>
127
+ </div>
128
+ <div class="stat-item">
129
+ <span class="stat-value">{stats.canonicalCount}</span>
130
+ <span class="stat-label">Canonical</span>
131
+ </div>
132
+ <div class="stat-item">
133
+ <span class="stat-value">{stats.variationCount}</span>
134
+ <span class="stat-label">Variations</span>
135
+ </div>
136
+ <div class="stat-item">
137
+ <span class="stat-value">⭐{stats.rarityScore}</span>
138
+ <span class="stat-label">Score</span>
139
+ </div>
140
  </div>
141
+ </div>
142
+
143
+ <!-- Search and Filters -->
144
+ <div class="controls">
145
+ <input
146
+ type="text"
147
+ placeholder="Search piclets..."
148
+ bind:value={searchQuery}
149
+ class="search-input"
150
+ />
151
+ <div class="view-selector">
152
+ <button
153
+ class="view-btn"
154
+ class:active={viewMode === 'all'}
155
+ onclick={() => viewMode = 'all'}
156
+ >
157
+ All
158
+ </button>
159
+ <button
160
+ class="view-btn"
161
+ class:active={viewMode === 'canonical'}
162
+ onclick={() => viewMode = 'canonical'}
163
+ >
164
+ Canonical
165
+ </button>
166
+ <button
167
+ class="view-btn"
168
+ class:active={viewMode === 'variations'}
169
+ onclick={() => viewMode = 'variations'}
170
+ >
171
+ Variations
172
+ </button>
173
  </div>
174
+ </div>
175
+
176
+ <!-- Collection Grid -->
177
+ <div class="collection-container">
178
+ {#if isLoading}
179
+ <div class="loading">
180
+ <div class="spinner"></div>
181
+ <p>Loading collection...</p>
182
+ </div>
183
+ {:else if filteredPiclets.length === 0}
184
+ <div class="empty-state">
185
+ {#if collectedPiclets.length === 0}
186
+ <h2>No Piclets Discovered</h2>
187
+ <p>Start scanning objects to build your collection!</p>
188
+ {:else}
189
+ <h2>No Results</h2>
190
+ <p>Try adjusting your filters or search query</p>
191
+ {/if}
192
+ </div>
193
+ {:else}
194
+ <div class="piclet-grid">
195
+ {#each filteredPiclets as piclet (piclet.id)}
196
+ <div class="piclet-item" onclick={() => handlePicletClick(piclet)}>
197
+ <PicletCard instance={piclet} />
198
+ {#if piclet.isCanonical}
199
+ <div class="canonical-badge">✨</div>
200
+ {/if}
201
+ {#if piclet.scanCount > 1}
202
+ <div class="scan-badge">{piclet.scanCount}×</div>
203
  {/if}
204
  </div>
205
+ {/each}
206
+ </div>
207
+ {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  </div>
209
+ </div>
210
+
211
+ <!-- Detail View -->
212
+ {#if selectedPiclet}
213
+ <PicletDetail
214
+ instance={selectedPiclet}
215
+ onClose={handleDetailClose}
216
+ onDeleted={handlePicletDeleted}
217
+ />
218
  {/if}
219
 
220
  <style>
221
  .pictuary-page {
222
  height: 100%;
223
  overflow-y: auto;
224
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
 
225
  }
226
+
227
+ .header {
228
+ padding: 1.5rem;
229
+ background: rgba(255, 255, 255, 0.95);
230
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
 
 
 
 
231
  }
232
+
233
+ .header h1 {
234
+ margin: 0 0 1rem;
235
+ color: #333;
236
+ }
237
+
238
+ .stats {
239
+ display: flex;
240
+ justify-content: space-around;
241
+ gap: 1rem;
242
+ }
243
+
244
+ .stat-item {
245
  display: flex;
246
  flex-direction: column;
247
  align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  }
249
+
250
+ .stat-value {
251
+ font-size: 1.5rem;
252
+ font-weight: bold;
253
+ color: #667eea;
 
 
 
 
 
 
254
  }
255
+
256
+ .stat-label {
257
+ font-size: 0.75rem;
258
  color: #666;
259
+ text-transform: uppercase;
260
  }
261
+
262
+ .controls {
263
+ padding: 1rem;
264
+ background: rgba(255, 255, 255, 0.9);
265
+ display: flex;
266
+ flex-direction: column;
267
+ gap: 1rem;
 
 
268
  }
269
+
270
+ .search-input {
271
+ width: 100%;
272
+ padding: 0.75rem;
273
+ border: 2px solid #e0e0e0;
274
+ border-radius: 8px;
275
+ font-size: 1rem;
276
  }
277
+
278
+ .view-selector {
279
  display: flex;
280
+ gap: 0.5rem;
 
 
 
 
 
 
281
  }
282
+
283
+ .view-btn {
284
+ flex: 1;
285
+ padding: 0.5rem;
286
+ border: 1px solid #e0e0e0;
287
+ border-radius: 6px;
288
+ background: white;
289
+ color: #666;
290
  cursor: pointer;
291
+ transition: all 0.2s;
292
  }
293
+
294
+ .view-btn.active {
295
+ background: #667eea;
296
+ color: white;
297
+ border-color: #667eea;
298
+ }
299
+
300
+ .collection-container {
301
+ padding: 1rem;
302
+ min-height: 400px;
303
  }
304
+
305
+ .loading {
306
  display: flex;
307
+ flex-direction: column;
308
+ align-items: center;
309
+ justify-content: center;
310
+ padding: 3rem;
311
+ color: white;
312
  }
313
+
314
+ .spinner {
315
+ width: 48px;
316
+ height: 48px;
317
+ border: 4px solid rgba(255, 255, 255, 0.3);
318
+ border-top-color: white;
319
+ border-radius: 50%;
320
+ animation: spin 1s linear infinite;
321
  }
322
+
323
+ @keyframes spin {
324
+ to { transform: rotate(360deg); }
 
325
  }
326
+
327
+ .empty-state {
328
+ text-align: center;
329
+ padding: 3rem;
330
+ color: white;
331
  }
332
+
333
+ .piclet-grid {
334
  display: grid;
335
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
336
+ gap: 1rem;
337
  }
338
+
339
+ .piclet-item {
340
+ position: relative;
 
 
 
 
 
 
 
 
 
341
  cursor: pointer;
342
+ transition: transform 0.2s;
 
343
  }
344
+
345
+ .piclet-item:hover {
346
+ transform: scale(1.05);
 
 
 
 
 
 
 
347
  }
348
+
349
+ .canonical-badge {
350
+ position: absolute;
351
+ top: 0.5rem;
352
+ right: 0.5rem;
353
+ background: gold;
354
+ border-radius: 50%;
355
+ width: 24px;
356
+ height: 24px;
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ font-size: 0.75rem;
361
  }
362
+
363
+ .scan-badge {
364
+ position: absolute;
365
+ bottom: 0.5rem;
366
+ right: 0.5rem;
367
+ background: rgba(0, 0, 0, 0.7);
368
+ color: white;
369
+ padding: 0.125rem 0.5rem;
370
+ border-radius: 12px;
371
+ font-size: 0.75rem;
372
+ font-weight: bold;
373
  }
374
  </style>
src/lib/components/Pages/ViewAll.svelte DELETED
@@ -1,140 +0,0 @@
1
- <script lang="ts">
2
- import type { PicletInstance } from '$lib/db/schema';
3
- import PicletCard from '../Piclets/PicletCard.svelte';
4
- import DraggablePicletCard from '../Piclets/DraggablePicletCard.svelte';
5
- import PicletDetail from '../Piclets/PicletDetail.svelte';
6
-
7
- interface Props {
8
- title: string;
9
- type: 'storage' | 'discovered';
10
- items: PicletInstance[];
11
- onBack: () => void;
12
- onItemsChanged?: () => void;
13
- onDragStart?: (instance: PicletInstance) => void;
14
- onDragEnd?: () => void;
15
- }
16
-
17
- let { title, type, items, onBack, onItemsChanged, onDragStart, onDragEnd }: Props = $props();
18
- let selectedPiclet: PicletInstance | null = $state(null);
19
-
20
- function handleItemClick(item: PicletInstance) {
21
- selectedPiclet = item;
22
- }
23
- </script>
24
-
25
- <div class="view-all-page">
26
- <header class="page-header">
27
- <button class="back-btn" onclick={onBack} aria-label="Back">
28
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
29
- <path d="M19 12H5m0 0l7 7m-7-7l7-7"></path>
30
- </svg>
31
- </button>
32
- <h1>{title}</h1>
33
- <div class="header-spacer"></div>
34
- </header>
35
-
36
- <div class="content">
37
- {#if items.length === 0}
38
- <div class="empty-state">
39
- <p>No {type === 'storage' ? 'piclets in storage' : 'discovered piclets'} yet.</p>
40
- </div>
41
- {:else}
42
- <div class="items-grid">
43
- {#each items as item}
44
- {#if type === 'storage'}
45
- <DraggablePicletCard
46
- instance={item}
47
- size={100}
48
- onClick={() => handleItemClick(item)}
49
- onDragStart={onDragStart}
50
- onDragEnd={onDragEnd}
51
- />
52
- {:else if type === 'discovered'}
53
- <PicletCard
54
- piclet={item}
55
- size={100}
56
- onClick={() => handleItemClick(item)}
57
- />
58
- {/if}
59
- {/each}
60
- </div>
61
- {/if}
62
- </div>
63
-
64
- {#if selectedPiclet}
65
- <PicletDetail
66
- instance={selectedPiclet}
67
- onClose={() => selectedPiclet = null}
68
- onDeleted={() => {
69
- selectedPiclet = null;
70
- onItemsChanged?.();
71
- }}
72
- />
73
- {/if}
74
- </div>
75
-
76
- <style>
77
- .view-all-page {
78
- height: 100%;
79
- display: flex;
80
- flex-direction: column;
81
- background: white;
82
- }
83
-
84
- .page-header {
85
- display: flex;
86
- align-items: center;
87
- justify-content: space-between;
88
- padding: 0.5rem 1rem;
89
- background: white;
90
- position: sticky;
91
- top: 0;
92
- z-index: 10;
93
- border-bottom: 1px solid #e5e5ea;
94
- }
95
-
96
- .page-header h1 {
97
- margin: 0;
98
- font-size: 1.5rem;
99
- font-weight: bold;
100
- color: #333;
101
- }
102
-
103
- .back-btn {
104
- background: none;
105
- border: none;
106
- padding: 0.5rem;
107
- cursor: pointer;
108
- color: #007bff;
109
- display: flex;
110
- align-items: center;
111
- justify-content: center;
112
- }
113
-
114
- .header-spacer {
115
- width: 40px;
116
- }
117
-
118
- .content {
119
- flex: 1;
120
- overflow-y: auto;
121
- padding: 1rem;
122
- padding-bottom: 100px;
123
- }
124
-
125
- .empty-state {
126
- display: flex;
127
- align-items: center;
128
- justify-content: center;
129
- height: 200px;
130
- color: #666;
131
- text-align: center;
132
- }
133
-
134
- .items-grid {
135
- display: grid;
136
- grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
137
- gap: 1rem;
138
- justify-items: center;
139
- }
140
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/PicletGenerator/PicletGenerator.svelte CHANGED
@@ -1,6 +1,6 @@
1
  <script lang="ts">
2
  import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
3
- import type { PicletInstance } from '$lib/db/schema';
4
  import UploadStep from './UploadStep.svelte';
5
  import WorkflowProgress from './WorkflowProgress.svelte';
6
  import PicletResult from './PicletResult.svelte';
@@ -8,7 +8,8 @@
8
  import { extractPicletMetadata } from '$lib/services/picletMetadata';
9
  import { savePicletInstance, generatedDataToPicletInstance } from '$lib/db/piclets';
10
  import { PicletType, TYPE_DATA } from '$lib/types/picletTypes';
11
- import { EncounterService } from '$lib/db/encounterService';
 
12
  // import { withQwenTimeout } from '$lib/utils/qwenTimeout'; // Unused since qwen is disabled
13
 
14
  interface Props extends PicletGeneratorProps {
@@ -70,7 +71,7 @@
70
  const result = await client.predict("/model_chat", [
71
  prompt, // user message
72
  [], // chat history (empty for new conversation)
73
- "You are a helpful assistant that creates Pokemon-style monster concepts based on images.", // system prompt
74
  0.7, // temperature
75
  2000 // max_tokens
76
  ]);
@@ -87,7 +88,13 @@
87
  imagePrompt: null,
88
  picletImage: null,
89
  error: null,
90
- isProcessing: false
 
 
 
 
 
 
91
  });
92
 
93
  // Queue state for multi-image processing
@@ -292,66 +299,107 @@ Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit
292
 
293
  async function captionImage() {
294
  workflowState.currentStep = 'captioning';
295
-
296
  if (!joyCaptionClient || !workflowState.userImage) {
297
  throw new Error('Caption service not available or no image provided');
298
  }
299
-
300
  try {
301
- const output = await joyCaptionClient.predict("/stream_chat", [
302
- workflowState.userImage, // input_image
303
- "Descriptive", // caption_type
304
- "long", // caption_length
305
- [], // extra_options
306
- "", // name_input
307
- "" // custom_prompt (empty for default descriptive captioning)
308
- ]);
309
-
310
- const [, caption] = output.data;
311
-
312
- // Store the detailed object description
313
- workflowState.imageCaption = caption;
314
- console.log('Detailed object description generated:', caption);
 
 
 
 
315
  } catch (error) {
316
  handleAPIError(error);
317
  }
318
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
  async function generateConcept() {
321
  workflowState.currentStep = 'conceptualizing';
322
-
 
 
 
 
 
 
 
323
  const activeClient = getActiveTextClient();
324
- if (!activeClient || !workflowState.imageCaption) {
325
- throw new Error(`${currentTextClient} service not available or no image caption provided`);
326
  }
327
-
328
- const conceptPrompt = `Based on this detailed object description, create a Pokémon-style monster that transforms the object into an imaginative creature. The monster should clearly be inspired by the object's appearance but reimagined as a living monster.
329
 
330
- Object description: "${workflowState.imageCaption}"
 
 
 
 
 
331
 
332
- Guidelines:
333
- - Take the object's key visual elements (colors, shapes, materials) incorporating all of them into a single creature design
334
- - Add eyes (can be glowing, mechanical, multiple, etc.) positioned where they make sense
335
- - Include limbs (legs, arms, wings, tentacles) that grow from or replace parts of the object
336
- - Add a mouth, beak, or feeding apparatus if appropriate
337
- - Add creature elements like tail, fins, claws, horns, etc where fitting
338
 
339
  Format your response exactly as follows:
340
  \`\`\`md
341
  # Object Rarity
342
- {Assess how rare the object is based on real-world availability and value. Rare objects give strong monsters while common objects give weak ones. Use: common, uncommon, rare, or legendary}
343
 
344
  # Monster Name
345
- {Creative name that hints at the original object, 11 letters max}
346
 
347
  # Primary Type
348
- {Based on the object, choose the most fitting primary type: beast, bug, aquatic, flora, mineral, space, machina, structure, culture, or cuisine}
349
 
350
  # Monster Description
351
- {Detailed physical description showing how the object becomes a creature. Ensure the creature uses all the unique attributes of the object. Include colors, shapes, materials, eyes, limbs, mouth, and distinctive features. This section will be used for battle narratives and lore.}
352
 
353
  # Monster Image Prompt
354
- {Extensive visual description of the Pokémon style monster for image generation. Focus on key visual elements: body shape, colors, distinctive features, pose. Keep this optimized for AI image generation.}
355
  \`\`\``;
356
 
357
  try {
 
1
  <script lang="ts">
2
  import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
3
+ import type { PicletInstance, DiscoveryStatus } from '$lib/db/schema';
4
  import UploadStep from './UploadStep.svelte';
5
  import WorkflowProgress from './WorkflowProgress.svelte';
6
  import PicletResult from './PicletResult.svelte';
 
8
  import { extractPicletMetadata } from '$lib/services/picletMetadata';
9
  import { savePicletInstance, generatedDataToPicletInstance } from '$lib/db/piclets';
10
  import { PicletType, TYPE_DATA } from '$lib/types/picletTypes';
11
+ import { EnhancedCaptionService } from '$lib/services/enhancedCaption';
12
+ import { CanonicalService } from '$lib/services/canonicalService';
13
  // import { withQwenTimeout } from '$lib/utils/qwenTimeout'; // Unused since qwen is disabled
14
 
15
  interface Props extends PicletGeneratorProps {
 
71
  const result = await client.predict("/model_chat", [
72
  prompt, // user message
73
  [], // chat history (empty for new conversation)
74
+ "You are a helpful assistant that creates Pokemon-style monster concepts based on real-world objects.", // system prompt
75
  0.7, // temperature
76
  2000 // max_tokens
77
  ]);
 
88
  imagePrompt: null,
89
  picletImage: null,
90
  error: null,
91
+ isProcessing: false,
92
+ // Discovery-specific state
93
+ objectName: null,
94
+ objectAttributes: [],
95
+ visualDetails: null,
96
+ discoveryStatus: null,
97
+ canonicalPiclet: null
98
  });
99
 
100
  // Queue state for multi-image processing
 
299
 
300
  async function captionImage() {
301
  workflowState.currentStep = 'captioning';
302
+
303
  if (!joyCaptionClient || !workflowState.userImage) {
304
  throw new Error('Caption service not available or no image provided');
305
  }
306
+
307
  try {
308
+ // Use enhanced captioning for object identification
309
+ const captionResult = await EnhancedCaptionService.generateEnhancedCaption(
310
+ joyCaptionClient,
311
+ workflowState.userImage
312
+ );
313
+
314
+ // Store caption results
315
+ workflowState.imageCaption = captionResult.fullCaption;
316
+ workflowState.objectName = captionResult.extractedObject || 'object';
317
+ workflowState.objectAttributes = captionResult.extractedAttributes || [];
318
+ workflowState.visualDetails = captionResult.visualDetails;
319
+
320
+ console.log('Object identified:', workflowState.objectName);
321
+ console.log('Attributes:', workflowState.objectAttributes);
322
+ console.log('Visual details:', workflowState.visualDetails);
323
+
324
+ // Check canonical database
325
+ await checkCanonical();
326
  } catch (error) {
327
  handleAPIError(error);
328
  }
329
  }
330
+
331
+ async function checkCanonical() {
332
+ workflowState.currentStep = 'checking';
333
+
334
+ try {
335
+ // Search for existing canonical or variations
336
+ const searchResult = await CanonicalService.searchCanonical(
337
+ workflowState.objectName!,
338
+ workflowState.objectAttributes || []
339
+ );
340
+
341
+ if (searchResult) {
342
+ workflowState.discoveryStatus = searchResult.status;
343
+ workflowState.canonicalPiclet = searchResult.piclet;
344
+
345
+ console.log('Discovery status:', searchResult.status);
346
+
347
+ if (searchResult.status === 'existing') {
348
+ // Existing exact match - increment scan count
349
+ await CanonicalService.incrementScanCount(searchResult.piclet.typeId);
350
+ console.log('Found existing Piclet, scan count incremented');
351
+ }
352
+ } else {
353
+ // No server response or new discovery
354
+ workflowState.discoveryStatus = 'new';
355
+ console.log('New discovery - will create canonical Piclet');
356
+ }
357
+ } catch (error) {
358
+ console.error('Canonical check failed, treating as new discovery:', error);
359
+ workflowState.discoveryStatus = 'new';
360
+ }
361
+ }
362
 
363
  async function generateConcept() {
364
  workflowState.currentStep = 'conceptualizing';
365
+
366
+ // Skip if we have an existing canonical Piclet
367
+ if (workflowState.discoveryStatus === 'existing' && workflowState.canonicalPiclet) {
368
+ workflowState.picletConcept = workflowState.canonicalPiclet.concept;
369
+ console.log('Using existing canonical concept');
370
+ return;
371
+ }
372
+
373
  const activeClient = getActiveTextClient();
374
+ if (!activeClient || !workflowState.objectName) {
375
+ throw new Error(`${currentTextClient} service not available or no object identified`);
376
  }
 
 
377
 
378
+ // Create monster prompt using object and visual details
379
+ const monsterPrompt = EnhancedCaptionService.createMonsterPrompt(
380
+ workflowState.objectName,
381
+ workflowState.visualDetails || '',
382
+ workflowState.objectAttributes || []
383
+ );
384
 
385
+ const conceptPrompt = `${monsterPrompt}
 
 
 
 
 
386
 
387
  Format your response exactly as follows:
388
  \`\`\`md
389
  # Object Rarity
390
+ {Assess rarity based on the ${workflowState.objectName}. Use: common, uncommon, rare, epic, or legendary}
391
 
392
  # Monster Name
393
+ {Creative name related to ${workflowState.objectName}, 11 letters max}
394
 
395
  # Primary Type
396
+ {Choose the most fitting type based on ${workflowState.objectName}: beast, bug, aquatic, flora, mineral, space, machina, structure, culture, or cuisine}
397
 
398
  # Monster Description
399
+ {Detailed physical description of the ${workflowState.objectName}-based creature. Include how the object's features become creature features. Focus on eyes, limbs, mouth, and distinctive elements.}
400
 
401
  # Monster Image Prompt
402
+ {Visual description of the ${workflowState.objectName} monster for image generation. Include body shape, colors, pose, and key features.}
403
  \`\`\``;
404
 
405
  try {
src/lib/components/PicletGenerator/PicletResult.svelte CHANGED
@@ -33,8 +33,13 @@
33
  createdAt: new Date()
34
  };
35
 
36
- // Create piclet instance asynchronously
37
- generatedDataToPicletInstance(picletData).then(instance => {
 
 
 
 
 
38
  picletInstance = { ...instance, id: 0 }; // Add temporary id for display
39
  }).catch(error => {
40
  console.error('Failed to create piclet instance:', error);
@@ -69,10 +74,12 @@
69
  <div class="success-icon">✨</div>
70
  <h2>Success!</h2>
71
  <p class="success-message">
72
- {#if picletInstance?.caught}
73
- <strong>{picletInstance?.nickname || 'This creature'}</strong> has been caught and added to your roster!
 
 
74
  {:else}
75
- You can now encounter <strong>{picletInstance?.nickname || 'this creature'}</strong>!
76
  {/if}
77
  </p>
78
  </div>
 
33
  createdAt: new Date()
34
  };
35
 
36
+ // Create piclet instance asynchronously with discovery data
37
+ generatedDataToPicletInstance(
38
+ picletData,
39
+ workflowState.objectName || undefined,
40
+ workflowState.objectAttributes || undefined,
41
+ workflowState.visualDetails || undefined
42
+ ).then(instance => {
43
  picletInstance = { ...instance, id: 0 }; // Add temporary id for display
44
  }).catch(error => {
45
  console.error('Failed to create piclet instance:', error);
 
74
  <div class="success-icon">✨</div>
75
  <h2>Success!</h2>
76
  <p class="success-message">
77
+ {#if picletInstance?.isCanonical}
78
+ You discovered the first <strong>{picletInstance?.objectName || 'creature'}</strong> Piclet!
79
+ {:else if picletInstance?.canonicalId}
80
+ You found a variation of <strong>{picletInstance?.objectName || 'creature'}</strong>!
81
  {:else}
82
+ <strong>{picletInstance?.nickname || 'This creature'}</strong> has been discovered!
83
  {/if}
84
  </p>
85
  </div>
src/lib/components/Piclets/AddToRosterDialog.svelte DELETED
@@ -1,169 +0,0 @@
1
- <script lang="ts">
2
- import type { PicletInstance } from '$lib/db/schema';
3
- import PicletCard from './PicletCard.svelte';
4
- import { moveToRoster } from '$lib/db/piclets';
5
-
6
- interface Props {
7
- position: number;
8
- availablePiclets: PicletInstance[];
9
- onClose: () => void;
10
- onAdded?: () => void;
11
- }
12
-
13
- let { position, availablePiclets, onClose, onAdded }: Props = $props();
14
- let isAdding = $state(false);
15
-
16
- async function handleAddToRoster(piclet: PicletInstance) {
17
- if (!piclet.id || isAdding) return;
18
-
19
- isAdding = true;
20
- try {
21
- await moveToRoster(piclet.id, position);
22
- onAdded?.();
23
- onClose();
24
- } catch (err) {
25
- console.error('Failed to add to roster:', err);
26
- } finally {
27
- isAdding = false;
28
- }
29
- }
30
- </script>
31
-
32
- <div class="dialog-overlay" onclick={(e) => e.target === e.currentTarget && onClose()} onkeydown={(e) => e.key === 'Escape' && onClose()} role="button" tabindex="0" aria-label="Close dialog">
33
- <div class="dialog-content">
34
- <header class="dialog-header">
35
- <h2>Add to Roster</h2>
36
- <button class="close-btn" onclick={onClose} aria-label="Close">
37
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
38
- <path d="M6 18L18 6M6 6l12 12"></path>
39
- </svg>
40
- </button>
41
- </header>
42
-
43
- <div class="dialog-body">
44
- {#if availablePiclets.length === 0}
45
- <div class="empty-state">
46
- <p>No piclets available in storage.</p>
47
- <p class="hint">Scan objects to discover new piclets!</p>
48
- </div>
49
- {:else}
50
- <p class="instruction">Select a piclet to add to position {position + 1}:</p>
51
- <div class="piclets-grid">
52
- {#each availablePiclets as piclet}
53
- <button
54
- class="piclet-option"
55
- onclick={() => handleAddToRoster(piclet)}
56
- disabled={isAdding}
57
- >
58
- <PicletCard piclet={piclet} size={100} />
59
- </button>
60
- {/each}
61
- </div>
62
- {/if}
63
- </div>
64
- </div>
65
- </div>
66
-
67
- <style>
68
- .dialog-overlay {
69
- position: fixed;
70
- inset: 0;
71
- background: rgba(0, 0, 0, 0.5);
72
- display: flex;
73
- align-items: center;
74
- justify-content: center;
75
- z-index: 1000;
76
- padding: 1rem;
77
- }
78
-
79
- .dialog-content {
80
- background: white;
81
- border-radius: 16px;
82
- width: 100%;
83
- max-width: 600px;
84
- max-height: 80vh;
85
- overflow: hidden;
86
- display: flex;
87
- flex-direction: column;
88
- }
89
-
90
- .dialog-header {
91
- padding: 1rem;
92
- border-bottom: 1px solid #e5e5ea;
93
- position: relative;
94
- }
95
-
96
- .dialog-header h2 {
97
- margin: 0;
98
- text-align: center;
99
- font-size: 1.25rem;
100
- }
101
-
102
- .close-btn {
103
- position: absolute;
104
- top: 1rem;
105
- right: 1rem;
106
- background: none;
107
- border: none;
108
- padding: 0;
109
- width: 24px;
110
- height: 24px;
111
- cursor: pointer;
112
- color: #8e8e93;
113
- }
114
-
115
- .dialog-body {
116
- flex: 1;
117
- overflow-y: auto;
118
- padding: 1rem;
119
- }
120
-
121
- .empty-state {
122
- text-align: center;
123
- padding: 3rem 1rem;
124
- color: #666;
125
- }
126
-
127
- .empty-state p {
128
- margin: 0 0 0.5rem;
129
- }
130
-
131
- .hint {
132
- font-size: 0.875rem;
133
- color: #8e8e93;
134
- }
135
-
136
- .instruction {
137
- margin: 0 0 1rem;
138
- color: #666;
139
- text-align: center;
140
- }
141
-
142
- .piclets-grid {
143
- display: grid;
144
- grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
145
- gap: 1rem;
146
- justify-items: center;
147
- }
148
-
149
- .piclet-option {
150
- background: none;
151
- border: none;
152
- padding: 0;
153
- cursor: pointer;
154
- transition: transform 0.2s;
155
- }
156
-
157
- .piclet-option:not(:disabled):hover {
158
- transform: scale(1.05);
159
- }
160
-
161
- .piclet-option:not(:disabled):active {
162
- transform: scale(0.95);
163
- }
164
-
165
- .piclet-option:disabled {
166
- opacity: 0.6;
167
- cursor: not-allowed;
168
- }
169
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Piclets/DraggablePicletCard.svelte DELETED
@@ -1,70 +0,0 @@
1
- <script lang="ts">
2
- import type { PicletInstance } from '$lib/db/schema';
3
- import PicletCard from './PicletCard.svelte';
4
-
5
- interface Props {
6
- instance: PicletInstance;
7
- size?: number;
8
- onDragStart?: (instance: PicletInstance) => void;
9
- onDragEnd?: () => void;
10
- onClick?: () => void;
11
- }
12
-
13
- let { instance, size = 100, onDragStart, onDragEnd, onClick }: Props = $props();
14
-
15
- let isDragging = $state(false);
16
-
17
- function handleDragStart(e: DragEvent) {
18
- isDragging = true;
19
-
20
- // Set drag data
21
- e.dataTransfer!.effectAllowed = 'move';
22
- e.dataTransfer!.setData('application/json', JSON.stringify({
23
- instanceId: instance.id,
24
- fromRoster: instance.isInRoster,
25
- fromPosition: instance.rosterPosition
26
- }));
27
-
28
- // Create a custom drag image
29
- const dragImage = e.currentTarget as HTMLElement;
30
- const clone = dragImage.cloneNode(true) as HTMLElement;
31
- clone.style.transform = 'scale(1.1)';
32
- clone.style.opacity = '0.8';
33
- document.body.appendChild(clone);
34
- e.dataTransfer!.setDragImage(clone, size / 2, size / 2);
35
- setTimeout(() => document.body.removeChild(clone), 0);
36
-
37
- onDragStart?.(instance);
38
- }
39
-
40
- function handleDragEnd() {
41
- isDragging = false;
42
- onDragEnd?.();
43
- }
44
- </script>
45
-
46
- <div
47
- draggable="true"
48
- ondragstart={handleDragStart}
49
- ondragend={handleDragEnd}
50
- class="draggable-wrapper"
51
- class:dragging={isDragging}
52
- role="button"
53
- tabindex="0"
54
- >
55
- <PicletCard piclet={instance} {size} {onClick} />
56
- </div>
57
-
58
- <style>
59
- .draggable-wrapper {
60
- cursor: move;
61
- }
62
-
63
- .draggable-wrapper.dragging {
64
- opacity: 0.5;
65
- }
66
-
67
- .draggable-wrapper.dragging :global(.piclet-card) {
68
- transform: scale(0.9);
69
- }
70
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Piclets/EmptySlotCard.svelte DELETED
@@ -1,61 +0,0 @@
1
- <script lang="ts">
2
- interface Props {
3
- size?: number;
4
- isHighlighted?: boolean;
5
- onClick?: () => void;
6
- }
7
-
8
- let { size = 100, isHighlighted = false, onClick }: Props = $props();
9
- </script>
10
-
11
- <button
12
- class="empty-slot"
13
- class:highlighted={isHighlighted}
14
- style="width: {size}px; height: {size + 30}px;"
15
- onclick={onClick}
16
- type="button"
17
- >
18
- <svg
19
- width="32"
20
- height="32"
21
- viewBox="0 0 24 24"
22
- fill="none"
23
- stroke="currentColor"
24
- stroke-width="2"
25
- >
26
- {#if isHighlighted}
27
- <circle cx="12" cy="12" r="10" />
28
- <path d="M12 8v8m-4-4h8" />
29
- {:else}
30
- <path d="M12 5v14m-7-7h14" />
31
- {/if}
32
- </svg>
33
- </button>
34
-
35
- <style>
36
- .empty-slot {
37
- background: #f5f5f5;
38
- border: 2px dashed #d1d1d6;
39
- border-radius: 12px;
40
- cursor: pointer;
41
- display: flex;
42
- align-items: center;
43
- justify-content: center;
44
- color: #8e8e93;
45
- transition: all 0.2s;
46
- }
47
-
48
- .empty-slot:hover {
49
- background: #e5e5ea;
50
- }
51
-
52
- .empty-slot:active {
53
- transform: scale(0.95);
54
- }
55
-
56
- .empty-slot.highlighted {
57
- background: rgba(0, 123, 255, 0.1);
58
- border-color: #007bff;
59
- color: #007bff;
60
- }
61
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Piclets/NewlyCaughtPicletDetail.svelte DELETED
@@ -1,644 +0,0 @@
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
-
6
- interface Props {
7
- instance: PicletInstance;
8
- onClose: () => void;
9
- }
10
-
11
- let { instance, onClose }: Props = $props();
12
- let selectedTab = $state<'about' | 'abilities'>('about');
13
- let showCelebration = $state(true);
14
-
15
- // Ensure stats are up-to-date with current level and nature
16
- const updatedInstance = $derived(recalculatePicletStats(instance));
17
-
18
- // Convert to battle definition to get enhanced ability data
19
- const battleDefinition = $derived(picletInstanceToBattleDefinition(updatedInstance));
20
-
21
- // XP and level calculations
22
- const xpProgress = $derived(getXpProgress(updatedInstance.level, updatedInstance.xp));
23
- const xpToNext = $derived(getXpTowardsNextLevel(updatedInstance.level, updatedInstance.xp));
24
- const isMaxLevel = $derived(updatedInstance.level >= 100);
25
-
26
- // Calculate BST and handle type data
27
- const typeData = $derived(TYPE_DATA[updatedInstance.primaryType]);
28
- const tier = $derived(() => {
29
- const bst = updatedInstance.bst;
30
- if (bst >= 600) return { name: 'Legendary', color: '#FFD700', bg: 'linear-gradient(135deg, #FFD700, #FFA500)' };
31
- if (bst >= 530) return { name: 'Elite', color: '#9932CC', bg: 'linear-gradient(135deg, #9932CC, #8A2BE2)' };
32
- if (bst >= 480) return { name: 'Advanced', color: '#1E90FF', bg: 'linear-gradient(135deg, #1E90FF, #0066CC)' };
33
- if (bst >= 420) return { name: 'Standard', color: '#32CD32', bg: 'linear-gradient(135deg, #32CD32, #228B22)' };
34
- return { name: 'Basic', color: '#808080', bg: 'linear-gradient(135deg, #808080, #696969)' };
35
- });
36
-
37
- let celebrationTimeout: NodeJS.Timeout;
38
-
39
- onMount(() => {
40
- // Auto-hide celebration after 3 seconds
41
- celebrationTimeout = setTimeout(() => {
42
- showCelebration = false;
43
- }, 3000);
44
-
45
- return () => {
46
- if (celebrationTimeout) clearTimeout(celebrationTimeout);
47
- };
48
- });
49
-
50
- function dismissCelebration() {
51
- showCelebration = false;
52
- if (celebrationTimeout) clearTimeout(celebrationTimeout);
53
- }
54
-
55
- function handleContainerClick(event: MouseEvent) {
56
- event.stopPropagation();
57
- }
58
- </script>
59
-
60
- <div class="detail-overlay" role="dialog" aria-modal="true" tabindex="-1" onclick={onClose} onkeydown={(e) => e.key === 'Escape' ? onClose() : null}>
61
- <div class="detail-container newly-caught" role="document" onclick={handleContainerClick}>
62
-
63
- <!-- Celebration overlay -->
64
- {#if showCelebration}
65
- <div class="celebration-overlay" role="button" tabindex="0" onclick={dismissCelebration} onkeydown={(e) => e.key === 'Enter' || e.key === ' ' ? dismissCelebration() : null}>
66
- <div class="celebration-content">
67
- <div class="celebration-sparkles">✨</div>
68
- <h1 class="celebration-title">Welcome to your team!</h1>
69
- <div class="celebration-piclet-name">{updatedInstance.nickname}</div>
70
- <p class="celebration-subtitle">Your first Piclet has been caught!</p>
71
- <div class="celebration-sparkles">🎉</div>
72
- <div class="tap-to-continue">Tap to continue</div>
73
- </div>
74
- </div>
75
- {/if}
76
-
77
- <!-- Header with special "newly caught" styling -->
78
- <div class="detail-header" style="background: {tier().bg}">
79
- <button class="close-button" onclick={onClose}>×</button>
80
- <div class="newly-caught-badge">
81
- <span class="badge-text">✨ NEWLY CAUGHT ✨</span>
82
- </div>
83
- <div class="header-content">
84
- <div class="piclet-image-container">
85
- <img
86
- src={updatedInstance.imageUrl}
87
- alt={updatedInstance.nickname}
88
- class="piclet-image"
89
- />
90
- <div class="golden-glow"></div>
91
- </div>
92
- <div class="piclet-info">
93
- <h1 class="piclet-name">{updatedInstance.nickname}</h1>
94
- <div class="piclet-meta">
95
- <div class="type-badge" style="background: {typeData.color}">
96
- {typeData.icon} {updatedInstance.primaryType}
97
- </div>
98
- <div class="tier-badge" style="color: {tier().color}">
99
- {tier().name}
100
- </div>
101
- <div class="level-badge">Lv. {updatedInstance.level}</div>
102
- </div>
103
- </div>
104
- </div>
105
- </div>
106
-
107
- <!-- Navigation tabs -->
108
- <div class="tab-navigation">
109
- <button
110
- class="tab-button"
111
- class:active={selectedTab === 'about'}
112
- onclick={() => selectedTab = 'about'}
113
- >
114
- About
115
- </button>
116
- <button
117
- class="tab-button"
118
- class:active={selectedTab === 'abilities'}
119
- onclick={() => selectedTab = 'abilities'}
120
- >
121
- Abilities
122
- </button>
123
- </div>
124
-
125
- <!-- Tab content -->
126
- <div class="detail-content">
127
- {#if selectedTab === 'about'}
128
- <div class="about-tab">
129
- <div class="description-section">
130
- <h3>Description</h3>
131
- <p class="description-text">{updatedInstance.description}</p>
132
- </div>
133
-
134
- <div class="stats-section">
135
- <h3>Combat Stats</h3>
136
- <div class="stats-grid">
137
- <div class="stat-item">
138
- <span class="stat-label">HP</span>
139
- <span class="stat-value">{updatedInstance.maxHp}</span>
140
- </div>
141
- <div class="stat-item">
142
- <span class="stat-label">Attack</span>
143
- <span class="stat-value">{updatedInstance.attack}</span>
144
- </div>
145
- <div class="stat-item">
146
- <span class="stat-label">Defense</span>
147
- <span class="stat-value">{updatedInstance.defense}</span>
148
- </div>
149
- <div class="stat-item">
150
- <span class="stat-label">Speed</span>
151
- <span class="stat-value">{updatedInstance.speed}</span>
152
- </div>
153
- </div>
154
- <div class="bst-display">
155
- <span class="bst-label">Base Stat Total:</span>
156
- <span class="bst-value" style="color: {tier().color}">{updatedInstance.bst}</span>
157
- </div>
158
- </div>
159
-
160
- <div class="xp-section">
161
- <h3>Experience</h3>
162
- <div class="level-xp-section">
163
- <div class="xp-bar-container">
164
- <div class="xp-bar">
165
- <div class="xp-fill" style="width: {xpProgress}%"></div>
166
- </div>
167
- <div class="xp-text">
168
- {#if isMaxLevel}
169
- <span>MAX LEVEL</span>
170
- {:else}
171
- <span>{xpToNext} XP to level {updatedInstance.level + 1}</span>
172
- {/if}
173
- </div>
174
- </div>
175
- </div>
176
- </div>
177
- </div>
178
- {:else if selectedTab === 'abilities'}
179
- <div class="abilities-tab">
180
- <div class="special-ability-section">
181
- <h3>Special Ability</h3>
182
- {#if isSpecialAbilityUnlocked(updatedInstance)}
183
- <AbilityDisplay ability={battleDefinition.specialAbility} />
184
- {:else}
185
- <div class="locked-ability">
186
- <span class="lock-icon">🔒</span>
187
- <span class="locked-text">Unlocks at level {updatedInstance.specialAbilityUnlockLevel}</span>
188
- </div>
189
- {/if}
190
- </div>
191
-
192
- <div class="moves-section">
193
- <h3>Moves</h3>
194
- <div class="moves-grid">
195
- {#each updatedInstance.moves as move}
196
- <MoveDisplay {move} picletLevel={updatedInstance.level} />
197
- {/each}
198
- </div>
199
- </div>
200
- </div>
201
- {/if}
202
- </div>
203
- </div>
204
- </div>
205
-
206
- <style>
207
- .detail-overlay {
208
- position: fixed;
209
- inset: 0;
210
- background: rgba(0, 0, 0, 0.8);
211
- backdrop-filter: blur(4px);
212
- z-index: 2000;
213
- display: flex;
214
- align-items: center;
215
- justify-content: center;
216
- padding: 1rem;
217
- animation: fadeIn 0.3s ease-out;
218
- }
219
-
220
- .detail-container.newly-caught {
221
- background: white;
222
- border-radius: 24px;
223
- max-width: 500px;
224
- width: 100%;
225
- max-height: 90vh;
226
- overflow: hidden;
227
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
228
- animation: slideInUp 0.4s ease-out;
229
- position: relative;
230
- }
231
-
232
- /* Celebration overlay */
233
- .celebration-overlay {
234
- position: absolute;
235
- inset: 0;
236
- background: radial-gradient(circle, rgba(255, 215, 0, 0.95) 0%, rgba(255, 140, 0, 0.9) 100%);
237
- z-index: 100;
238
- display: flex;
239
- align-items: center;
240
- justify-content: center;
241
- cursor: pointer;
242
- animation: celebrationPulse 2s ease-in-out infinite;
243
- }
244
-
245
- .celebration-content {
246
- text-align: center;
247
- color: white;
248
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
249
- }
250
-
251
- .celebration-sparkles {
252
- font-size: 3rem;
253
- animation: sparkle 1.5s ease-in-out infinite;
254
- margin: 0.5rem 0;
255
- }
256
-
257
- .celebration-title {
258
- font-size: 2.5rem;
259
- font-weight: 800;
260
- margin: 1rem 0 0.5rem;
261
- animation: titleGlow 2s ease-in-out infinite;
262
- }
263
-
264
- .celebration-piclet-name {
265
- font-size: 2rem;
266
- font-weight: 700;
267
- margin: 0.5rem 0;
268
- text-transform: uppercase;
269
- letter-spacing: 2px;
270
- }
271
-
272
- .celebration-subtitle {
273
- font-size: 1.2rem;
274
- margin: 1rem 0;
275
- opacity: 0.9;
276
- }
277
-
278
- .tap-to-continue {
279
- font-size: 1rem;
280
- margin-top: 2rem;
281
- opacity: 0.8;
282
- animation: pulse 1.5s ease-in-out infinite;
283
- }
284
-
285
- /* Header styling */
286
- .detail-header {
287
- position: relative;
288
- padding: 2rem 1.5rem 1.5rem;
289
- color: white;
290
- text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
291
- }
292
-
293
- .newly-caught-badge {
294
- position: absolute;
295
- top: 1rem;
296
- left: 50%;
297
- transform: translateX(-50%);
298
- background: rgba(255, 255, 255, 0.2);
299
- backdrop-filter: blur(10px);
300
- border: 2px solid rgba(255, 255, 255, 0.3);
301
- border-radius: 20px;
302
- padding: 0.5rem 1rem;
303
- animation: badgeGlow 2s ease-in-out infinite;
304
- }
305
-
306
- .badge-text {
307
- font-weight: 700;
308
- font-size: 0.9rem;
309
- letter-spacing: 1px;
310
- }
311
-
312
- .close-button {
313
- position: absolute;
314
- top: 1rem;
315
- right: 1rem;
316
- background: rgba(255, 255, 255, 0.2);
317
- border: none;
318
- border-radius: 50%;
319
- width: 40px;
320
- height: 40px;
321
- color: white;
322
- font-size: 1.5rem;
323
- cursor: pointer;
324
- display: flex;
325
- align-items: center;
326
- justify-content: center;
327
- transition: all 0.2s ease;
328
- z-index: 10;
329
- }
330
-
331
- .close-button:hover {
332
- background: rgba(255, 255, 255, 0.3);
333
- transform: scale(1.1);
334
- }
335
-
336
- .header-content {
337
- display: flex;
338
- align-items: center;
339
- gap: 1.5rem;
340
- margin-top: 2rem;
341
- }
342
-
343
- .piclet-image-container {
344
- position: relative;
345
- flex-shrink: 0;
346
- }
347
-
348
- .piclet-image {
349
- width: 100px;
350
- height: 100px;
351
- border-radius: 16px;
352
- object-fit: cover;
353
- border: 3px solid rgba(255, 255, 255, 0.3);
354
- position: relative;
355
- z-index: 2;
356
- }
357
-
358
- .golden-glow {
359
- position: absolute;
360
- inset: -10px;
361
- background: radial-gradient(circle, rgba(255, 215, 0, 0.6), transparent 70%);
362
- border-radius: 50%;
363
- animation: goldenGlow 2s ease-in-out infinite;
364
- z-index: 1;
365
- }
366
-
367
- .piclet-info {
368
- flex: 1;
369
- min-width: 0;
370
- }
371
-
372
- .piclet-name {
373
- font-size: 1.8rem;
374
- font-weight: 700;
375
- margin: 0 0 0.5rem;
376
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
377
- }
378
-
379
- .piclet-meta {
380
- display: flex;
381
- flex-wrap: wrap;
382
- gap: 0.5rem;
383
- }
384
-
385
- .type-badge, .tier-badge, .level-badge {
386
- padding: 0.25rem 0.75rem;
387
- border-radius: 20px;
388
- font-size: 0.8rem;
389
- font-weight: 600;
390
- text-transform: uppercase;
391
- letter-spacing: 0.5px;
392
- }
393
-
394
- .type-badge {
395
- color: white;
396
- text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
397
- }
398
-
399
- .tier-badge {
400
- background: rgba(255, 255, 255, 0.2);
401
- backdrop-filter: blur(10px);
402
- }
403
-
404
- .level-badge {
405
- background: rgba(255, 255, 255, 0.3);
406
- color: white;
407
- }
408
-
409
- /* Tab navigation */
410
- .tab-navigation {
411
- display: flex;
412
- border-bottom: 1px solid #e0e0e0;
413
- background: #f8f9fa;
414
- }
415
-
416
- .tab-button {
417
- flex: 1;
418
- padding: 1rem;
419
- border: none;
420
- background: none;
421
- font-weight: 600;
422
- color: #666;
423
- cursor: pointer;
424
- transition: all 0.2s ease;
425
- position: relative;
426
- }
427
-
428
- .tab-button.active {
429
- color: #007bff;
430
- }
431
-
432
- .tab-button.active::after {
433
- content: '';
434
- position: absolute;
435
- bottom: 0;
436
- left: 0;
437
- right: 0;
438
- height: 3px;
439
- background: #007bff;
440
- }
441
-
442
- /* Content styling */
443
- .detail-content {
444
- max-height: 60vh;
445
- overflow-y: auto;
446
- padding: 1.5rem;
447
- }
448
-
449
- .about-tab, .abilities-tab {
450
- display: flex;
451
- flex-direction: column;
452
- gap: 1.5rem;
453
- }
454
-
455
- .description-section h3,
456
- .stats-section h3,
457
- .xp-section h3,
458
- .special-ability-section h3,
459
- .moves-section h3 {
460
- margin: 0 0 1rem;
461
- font-size: 1.2rem;
462
- font-weight: 600;
463
- color: #333;
464
- }
465
-
466
- .description-text {
467
- color: #666;
468
- line-height: 1.6;
469
- margin: 0;
470
- }
471
-
472
- .stats-grid {
473
- display: grid;
474
- grid-template-columns: repeat(2, 1fr);
475
- gap: 1rem;
476
- margin-bottom: 1rem;
477
- }
478
-
479
- .stat-item {
480
- display: flex;
481
- justify-content: space-between;
482
- align-items: center;
483
- padding: 0.75rem;
484
- background: #f8f9fa;
485
- border-radius: 8px;
486
- }
487
-
488
- .stat-label {
489
- font-weight: 600;
490
- color: #666;
491
- }
492
-
493
- .stat-value {
494
- font-weight: 700;
495
- color: #333;
496
- font-size: 1.1rem;
497
- }
498
-
499
- .bst-display {
500
- display: flex;
501
- justify-content: space-between;
502
- align-items: center;
503
- padding: 1rem;
504
- background: linear-gradient(135deg, #f8f9fa, #e9ecef);
505
- border-radius: 12px;
506
- border: 2px solid #dee2e6;
507
- }
508
-
509
- .bst-label {
510
- font-weight: 600;
511
- color: #666;
512
- }
513
-
514
- .bst-value {
515
- font-weight: 700;
516
- font-size: 1.3rem;
517
- }
518
-
519
- .level-xp-section {
520
- display: flex;
521
- flex-direction: column;
522
- gap: 0.5rem;
523
- }
524
-
525
- .xp-bar-container {
526
- display: flex;
527
- flex-direction: column;
528
- gap: 0.5rem;
529
- }
530
-
531
- .xp-bar {
532
- height: 8px;
533
- background: #e9ecef;
534
- border-radius: 4px;
535
- overflow: hidden;
536
- }
537
-
538
- .xp-fill {
539
- height: 100%;
540
- background: linear-gradient(90deg, #28a745, #20c997);
541
- transition: width 0.3s ease;
542
- }
543
-
544
- .xp-text {
545
- text-align: center;
546
- font-size: 0.9rem;
547
- color: #666;
548
- }
549
-
550
- .locked-ability {
551
- display: flex;
552
- align-items: center;
553
- gap: 0.5rem;
554
- padding: 1rem;
555
- background: #f8f9fa;
556
- border: 2px dashed #dee2e6;
557
- border-radius: 12px;
558
- color: #666;
559
- font-style: italic;
560
- }
561
-
562
- .lock-icon {
563
- font-size: 1.2rem;
564
- }
565
-
566
- .moves-grid {
567
- display: flex;
568
- flex-direction: column;
569
- gap: 0.75rem;
570
- }
571
-
572
- /* Animations */
573
- @keyframes fadeIn {
574
- from { opacity: 0; }
575
- to { opacity: 1; }
576
- }
577
-
578
- @keyframes slideInUp {
579
- from {
580
- opacity: 0;
581
- transform: translateY(100px) scale(0.9);
582
- }
583
- to {
584
- opacity: 1;
585
- transform: translateY(0) scale(1);
586
- }
587
- }
588
-
589
- @keyframes celebrationPulse {
590
- 0%, 100% { transform: scale(1); }
591
- 50% { transform: scale(1.02); }
592
- }
593
-
594
- @keyframes sparkle {
595
- 0%, 100% { transform: rotate(0deg) scale(1); }
596
- 25% { transform: rotate(-5deg) scale(1.1); }
597
- 75% { transform: rotate(5deg) scale(1.1); }
598
- }
599
-
600
- @keyframes titleGlow {
601
- 0%, 100% { text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); }
602
- 50% { text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5), 0 0 20px rgba(255, 255, 255, 0.8); }
603
- }
604
-
605
- @keyframes pulse {
606
- 0%, 100% { opacity: 0.8; }
607
- 50% { opacity: 1; }
608
- }
609
-
610
- @keyframes badgeGlow {
611
- 0%, 100% { box-shadow: 0 0 10px rgba(255, 255, 255, 0.3); }
612
- 50% { box-shadow: 0 0 20px rgba(255, 255, 255, 0.6), 0 0 30px rgba(255, 255, 255, 0.4); }
613
- }
614
-
615
- @keyframes goldenGlow {
616
- 0%, 100% { opacity: 0.6; }
617
- 50% { opacity: 0.9; }
618
- }
619
-
620
- @media (max-width: 768px) {
621
- .detail-container {
622
- margin: 0.5rem;
623
- max-height: 95vh;
624
- }
625
-
626
- .celebration-title {
627
- font-size: 2rem;
628
- }
629
-
630
- .celebration-piclet-name {
631
- font-size: 1.5rem;
632
- }
633
-
634
- .header-content {
635
- flex-direction: column;
636
- text-align: center;
637
- gap: 1rem;
638
- }
639
-
640
- .stats-grid {
641
- grid-template-columns: 1fr;
642
- }
643
- }
644
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Piclets/PicletDetail.svelte CHANGED
@@ -64,21 +64,35 @@
64
 
65
  <div class="metadata">
66
  <div class="meta-item">
67
- <strong>Caught:</strong>
68
- {instance.caught ? 'Yes' : 'No'}
69
  </div>
70
- {#if instance.caughtAt}
71
  <div class="meta-item">
72
- <strong>Caught on:</strong>
73
- {new Date(instance.caughtAt).toLocaleDateString()}
74
  </div>
75
  {/if}
76
- {#if instance.isInRoster}
77
  <div class="meta-item">
78
- <strong>Status:</strong>
79
- <span class="roster-badge">In Battle Roster</span>
80
  </div>
81
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  </div>
83
  </div>
84
  </div>
 
64
 
65
  <div class="metadata">
66
  <div class="meta-item">
67
+ <strong>Object:</strong>
68
+ {instance.objectName}
69
  </div>
70
+ {#if instance.isCanonical}
71
  <div class="meta-item">
72
+ <strong>Status:</strong>
73
+ <span class="canonical-badge">Canonical</span>
74
  </div>
75
  {/if}
76
+ {#if instance.variationAttributes && instance.variationAttributes.length > 0}
77
  <div class="meta-item">
78
+ <strong>Variation:</strong>
79
+ {instance.variationAttributes.join(', ')}
80
  </div>
81
  {/if}
82
+ {#if instance.discoveredAt}
83
+ <div class="meta-item">
84
+ <strong>Discovered:</strong>
85
+ {new Date(instance.discoveredAt).toLocaleDateString()}
86
+ </div>
87
+ {/if}
88
+ <div class="meta-item">
89
+ <strong>Scan Count:</strong>
90
+ {instance.scanCount || 1}
91
+ </div>
92
+ <div class="meta-item">
93
+ <strong>Rarity:</strong>
94
+ <span class="rarity-{instance.tier}">{instance.tier}</span>
95
+ </div>
96
  </div>
97
  </div>
98
  </div>
src/lib/components/Piclets/RosterSlot.svelte DELETED
@@ -1,109 +0,0 @@
1
- <script lang="ts">
2
- import type { PicletInstance } from '$lib/db/schema';
3
- import DraggablePicletCard from './DraggablePicletCard.svelte';
4
- import EmptySlotCard from './EmptySlotCard.svelte';
5
-
6
- interface Props {
7
- position: number;
8
- piclet?: PicletInstance;
9
- size?: number;
10
- onDrop?: (position: number, dragData: any) => void;
11
- onPicletClick?: (piclet: PicletInstance) => void;
12
- onEmptyClick?: (position: number) => void;
13
- onDragStart?: (instance: PicletInstance) => void;
14
- onDragEnd?: () => void;
15
- }
16
-
17
- let {
18
- position,
19
- piclet,
20
- size = 100,
21
- onDrop,
22
- onPicletClick,
23
- onEmptyClick,
24
- onDragStart,
25
- onDragEnd
26
- }: Props = $props();
27
-
28
- let isDragOver = $state(false);
29
- let canAcceptDrop = $state(false);
30
-
31
- function handleDragOver(e: DragEvent) {
32
- e.preventDefault();
33
-
34
- // Check if we can accept the drop
35
- const data = e.dataTransfer?.getData('application/json');
36
- if (data) {
37
- isDragOver = true;
38
- canAcceptDrop = true;
39
- e.dataTransfer!.dropEffect = 'move';
40
- }
41
- }
42
-
43
- function handleDragLeave() {
44
- isDragOver = false;
45
- canAcceptDrop = false;
46
- }
47
-
48
- function handleDrop(e: DragEvent) {
49
- e.preventDefault();
50
- isDragOver = false;
51
- canAcceptDrop = false;
52
-
53
- const data = e.dataTransfer?.getData('application/json');
54
- if (data) {
55
- const dragData = JSON.parse(data);
56
- onDrop?.(position, dragData);
57
- }
58
- }
59
-
60
- function handleClick() {
61
- if (piclet) {
62
- onPicletClick?.(piclet);
63
- } else {
64
- onEmptyClick?.(position);
65
- }
66
- }
67
- </script>
68
-
69
- <div
70
- class="roster-slot"
71
- ondragover={handleDragOver}
72
- ondragleave={handleDragLeave}
73
- ondrop={handleDrop}
74
- class:drag-over={isDragOver}
75
- role="region"
76
- aria-label="Roster slot {position + 1}"
77
- >
78
- {#if piclet}
79
- <DraggablePicletCard
80
- instance={piclet}
81
- {size}
82
- onClick={handleClick}
83
- {onDragStart}
84
- {onDragEnd}
85
- />
86
- {:else}
87
- <EmptySlotCard
88
- {size}
89
- isHighlighted={isDragOver && canAcceptDrop}
90
- onClick={handleClick}
91
- />
92
- {/if}
93
- </div>
94
-
95
- <style>
96
- .roster-slot {
97
- position: relative;
98
- }
99
-
100
- .roster-slot.drag-over::after {
101
- content: '';
102
- position: absolute;
103
- inset: -4px;
104
- border: 2px solid #007bff;
105
- border-radius: 16px;
106
- background: rgba(0, 123, 255, 0.1);
107
- pointer-events: none;
108
- }
109
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/db/encounterService.ts DELETED
@@ -1,115 +0,0 @@
1
- import { db } from './index';
2
- import type { Encounter, PicletInstance } from './schema';
3
- import { EncounterType } from './schema';
4
- import { getOrCreateGameState, markEncountersRefreshed } from './gameState';
5
- import { getCaughtPiclets } from './piclets';
6
-
7
- // Configuration
8
- const ENCOUNTER_REFRESH_HOURS = 2;
9
- const MIN_WILD_ENCOUNTERS = 2;
10
- const MAX_WILD_ENCOUNTERS = 4;
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 - simplified to only wild battles
40
- static async generateEncounters(): Promise<Encounter[]> {
41
- const encounters: Omit<Encounter, 'id'>[] = [];
42
-
43
- // Check if player has any Piclets
44
- const caughtPiclets = await getCaughtPiclets();
45
-
46
- if (caughtPiclets.length === 0) {
47
- // No Piclets yet - show empty state, they need to scan first
48
- console.log('Player has no caught piclets - returning empty encounters');
49
- await db.encounters.clear();
50
- await markEncountersRefreshed();
51
- return [];
52
- }
53
-
54
- // Player has Piclets - generate wild battle encounters
55
- console.log('Generating wild battle encounters');
56
- const wildEncounters = await this.generateWildBattleEncounters();
57
- encounters.push(...wildEncounters);
58
-
59
- // Clear existing encounters and add new ones
60
- await db.encounters.clear();
61
- for (const encounter of encounters) {
62
- await db.encounters.add(encounter);
63
- }
64
-
65
- await markEncountersRefreshed();
66
- return await this.getCurrentEncounters();
67
- }
68
-
69
- // Generate wild battle encounters using existing caught Piclets
70
- private static async generateWildBattleEncounters(): Promise<Omit<Encounter, 'id'>[]> {
71
- const encounters: Omit<Encounter, 'id'>[] = [];
72
-
73
- // Get all caught piclets to use as potential opponents
74
- const caughtPiclets = await getCaughtPiclets();
75
-
76
- if (caughtPiclets.length === 0) {
77
- return encounters;
78
- }
79
-
80
- // Generate 2-4 random wild encounters
81
- const numEncounters = Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1)) + MIN_WILD_ENCOUNTERS;
82
-
83
- for (let i = 0; i < numEncounters; i++) {
84
- // Pick a random Piclet from caught ones to use as opponent
85
- const randomPiclet = caughtPiclets[Math.floor(Math.random() * caughtPiclets.length)];
86
-
87
- // Use the piclet's name for display
88
- const displayName = randomPiclet.typeId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
89
-
90
- const encounter: Omit<Encounter, 'id'> = {
91
- type: EncounterType.WILD_PICLET,
92
- title: `Wild ${displayName}`,
93
- description: `A wild ${displayName} appears! Ready for battle?`,
94
- picletInstanceId: randomPiclet.id,
95
- picletTypeId: randomPiclet.typeId,
96
- enemyLevel: 5, // Simplified - no level variance needed
97
- createdAt: new Date()
98
- };
99
-
100
- encounters.push(encounter);
101
- }
102
-
103
- return encounters;
104
- }
105
-
106
- // Get a specific encounter by ID
107
- static async getEncounter(id: number): Promise<Encounter | undefined> {
108
- return await db.encounters.get(id);
109
- }
110
-
111
- // Delete an encounter (after it's been completed)
112
- static async deleteEncounter(id: number): Promise<void> {
113
- await db.encounters.delete(id);
114
- }
115
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/db/gameState.ts CHANGED
@@ -1,26 +1,27 @@
1
  import { db } from './index';
2
- import type { GameState, Encounter, EncounterType } from './schema';
3
 
4
  // Initialize or get game state
5
  export async function getOrCreateGameState(): Promise<GameState> {
6
  const states = await db.gameState.toArray();
7
-
8
  if (states.length > 0) {
9
  // Update last played
10
  await db.gameState.update(states[0].id!, { lastPlayed: new Date() });
11
  return states[0];
12
  }
13
-
14
  // Create initial game state
15
  const newState: Omit<GameState, 'id'> = {
16
- lastEncounterRefresh: new Date(),
17
  lastPlayed: new Date(),
18
- progressPoints: 0,
19
- trainersDefeated: 0,
20
- picletsCapured: 0,
21
- battlesLost: 0
 
22
  };
23
-
24
  const id = await db.gameState.add(newState);
25
  return { ...newState, id };
26
  }
@@ -31,59 +32,67 @@ export async function updateProgress(updates: Partial<GameState>): Promise<void>
31
  await db.gameState.update(state.id!, updates);
32
  }
33
 
34
- // Increment specific counters
35
- export async function incrementCounter(counter: 'trainersDefeated' | 'picletsCapured' | 'battlesLost'): Promise<void> {
36
  const state = await getOrCreateGameState();
37
  await db.gameState.update(state.id!, {
38
- [counter]: state[counter] + 1
39
  });
40
  }
41
 
42
- // Add progress points
43
- export async function addProgressPoints(points: number): Promise<void> {
44
  const state = await getOrCreateGameState();
45
- const newPoints = Math.min(1000, state.progressPoints + points);
46
  await db.gameState.update(state.id!, {
47
- progressPoints: newPoints
48
  });
49
  }
50
 
51
- // Create a new encounter
52
- export async function createEncounter(encounter: Omit<Encounter, 'id' | 'createdAt'>): Promise<number> {
53
- return await db.encounters.add({
54
- ...encounter,
55
  createdAt: new Date()
56
  });
57
  }
58
 
59
- // Get recent encounters
60
- export async function getRecentEncounters(limit: number = 10): Promise<Encounter[]> {
61
- return await db.encounters
62
  .orderBy('createdAt')
63
  .reverse()
64
  .limit(limit)
65
  .toArray();
66
  }
67
 
68
- // Get encounters by type
69
- export async function getEncountersByType(type: EncounterType): Promise<Encounter[]> {
70
- return await db.encounters
71
- .where('type')
72
- .equals(type)
73
- .toArray();
74
  }
75
 
76
- // Check if should refresh encounters (e.g., every hour)
77
- export async function shouldRefreshEncounters(): Promise<boolean> {
78
  const state = await getOrCreateGameState();
79
- const hoursSinceRefresh = (Date.now() - state.lastEncounterRefresh.getTime()) / (1000 * 60 * 60);
80
- return hoursSinceRefresh >= 1;
 
81
  }
82
 
83
- // Mark encounters as refreshed
84
- export async function markEncountersRefreshed(): Promise<void> {
85
  const state = await getOrCreateGameState();
86
  await db.gameState.update(state.id!, {
87
- lastEncounterRefresh: new Date()
88
  });
 
 
 
 
 
 
 
 
 
89
  }
 
1
  import { db } from './index';
2
+ import type { GameState, ActivityEntry } from './schema';
3
 
4
  // Initialize or get game state
5
  export async function getOrCreateGameState(): Promise<GameState> {
6
  const states = await db.gameState.toArray();
7
+
8
  if (states.length > 0) {
9
  // Update last played
10
  await db.gameState.update(states[0].id!, { lastPlayed: new Date() });
11
  return states[0];
12
  }
13
+
14
  // Create initial game state
15
  const newState: Omit<GameState, 'id'> = {
16
+ lastActivityRefresh: new Date(),
17
  lastPlayed: new Date(),
18
+ totalDiscoveries: 0,
19
+ uniqueDiscoveries: 0,
20
+ variationsFound: 0,
21
+ rarityScore: 0,
22
+ currentStreak: 0
23
  };
24
+
25
  const id = await db.gameState.add(newState);
26
  return { ...newState, id };
27
  }
 
32
  await db.gameState.update(state.id!, updates);
33
  }
34
 
35
+ // Increment discovery counters
36
+ export async function incrementDiscoveryCounter(counter: 'totalDiscoveries' | 'uniqueDiscoveries' | 'variationsFound'): Promise<void> {
37
  const state = await getOrCreateGameState();
38
  await db.gameState.update(state.id!, {
39
+ [counter]: (state[counter] || 0) + 1
40
  });
41
  }
42
 
43
+ // Add rarity score points
44
+ export async function addRarityScore(points: number): Promise<void> {
45
  const state = await getOrCreateGameState();
 
46
  await db.gameState.update(state.id!, {
47
+ rarityScore: (state.rarityScore || 0) + points
48
  });
49
  }
50
 
51
+ // Create a new activity entry
52
+ export async function createActivityEntry(entry: Omit<ActivityEntry, 'id' | 'createdAt'>): Promise<number> {
53
+ return await db.activityEntries.add({
54
+ ...entry,
55
  createdAt: new Date()
56
  });
57
  }
58
 
59
+ // Get recent activity
60
+ export async function getRecentActivity(limit: number = 10): Promise<ActivityEntry[]> {
61
+ return await db.activityEntries
62
  .orderBy('createdAt')
63
  .reverse()
64
  .limit(limit)
65
  .toArray();
66
  }
67
 
68
+ // Check if should refresh activity (e.g., every 30 minutes)
69
+ export async function shouldRefreshActivity(): Promise<boolean> {
70
+ const state = await getOrCreateGameState();
71
+ const minutesSinceRefresh = (Date.now() - state.lastActivityRefresh.getTime()) / (1000 * 60);
72
+ return minutesSinceRefresh >= 30;
 
73
  }
74
 
75
+ // Mark activity as refreshed
76
+ export async function markActivityRefreshed(): Promise<void> {
77
  const state = await getOrCreateGameState();
78
+ await db.gameState.update(state.id!, {
79
+ lastActivityRefresh: new Date()
80
+ });
81
  }
82
 
83
+ // Update current streak
84
+ export async function updateStreak(daysActive: number): Promise<void> {
85
  const state = await getOrCreateGameState();
86
  await db.gameState.update(state.id!, {
87
+ currentStreak: daysActive
88
  });
89
+ }
90
+
91
+ // Calculate rarity points for a discovery
92
+ export function calculateRarityPoints(scanCount: number): number {
93
+ if (scanCount <= 5) return 100; // Legendary
94
+ if (scanCount <= 20) return 50; // Epic
95
+ if (scanCount <= 50) return 20; // Rare
96
+ if (scanCount <= 100) return 10; // Uncommon
97
+ return 5; // Common
98
  }
src/lib/db/index.ts CHANGED
@@ -1,10 +1,10 @@
1
  import Dexie, { type Table } from 'dexie';
2
- import type { PicletInstance, Encounter, GameState, TrainerScanProgress } from './schema';
3
 
4
  export class PicletDatabase extends Dexie {
5
  // Game tables
6
  picletInstances!: Table<PicletInstance>;
7
- encounters!: Table<Encounter>;
8
  gameState!: Table<GameState>;
9
  trainerScanProgress!: Table<TrainerScanProgress>;
10
 
@@ -48,6 +48,30 @@ export class PicletDatabase extends Dexie {
48
  gameState: '++id, lastPlayed',
49
  trainerScanProgress: 'imagePath, trainerName, status, completedAt'
50
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
  }
53
 
 
1
  import Dexie, { type Table } from 'dexie';
2
+ import type { PicletInstance, ActivityEntry, GameState, TrainerScanProgress } from './schema';
3
 
4
  export class PicletDatabase extends Dexie {
5
  // Game tables
6
  picletInstances!: Table<PicletInstance>;
7
+ activityEntries!: Table<ActivityEntry>;
8
  gameState!: Table<GameState>;
9
  trainerScanProgress!: Table<TrainerScanProgress>;
10
 
 
48
  gameState: '++id, lastPlayed',
49
  trainerScanProgress: 'imagePath, trainerName, status, completedAt'
50
  });
51
+
52
+ // Version 8: Discovery system - replace encounters with activity, update piclet fields
53
+ this.version(8).stores({
54
+ picletInstances: '++id, typeId, objectName, isCanonical, canonicalId, isInCollection, collectedAt, tier',
55
+ activityEntries: '++id, type, createdAt, picletTypeId, discovererName, rarity',
56
+ gameState: '++id, lastPlayed, lastActivityRefresh',
57
+ trainerScanProgress: 'imagePath, trainerName, status, completedAt'
58
+ }).upgrade(tx => {
59
+ // Migrate existing data
60
+ return tx.table('picletInstances').toCollection().modify(piclet => {
61
+ // Map old fields to new
62
+ piclet.objectName = piclet.typeId || 'unknown';
63
+ piclet.isCanonical = false;
64
+ piclet.isInCollection = piclet.caught || false;
65
+ piclet.collectedAt = piclet.caughtAt || new Date();
66
+ piclet.scanCount = 1;
67
+ piclet.discoveredAt = piclet.caughtAt || new Date();
68
+ // Remove old fields
69
+ delete piclet.caught;
70
+ delete piclet.caughtAt;
71
+ delete piclet.isInRoster;
72
+ delete piclet.rosterPosition;
73
+ });
74
+ });
75
  }
76
  }
77
 
src/lib/db/piclets.ts CHANGED
@@ -16,39 +16,49 @@ interface GeneratedPicletData {
16
  }
17
 
18
  // Convert generated piclet data to a PicletInstance
19
- export async function generatedDataToPicletInstance(data: GeneratedPicletData): Promise<Omit<PicletInstance, 'id'>> {
 
 
 
 
 
20
  if (!data.stats) {
21
  throw new Error('Generated data must have stats to create PicletInstance');
22
  }
23
 
24
  const stats = data.stats as PicletStats;
25
-
26
- // Map tier from stats
27
- let tier: string = stats.tier || 'medium';
28
-
29
- // Check if this is the first piclet (no existing piclets in database)
30
- const existingPiclets = await db.picletInstances.count();
31
- const isFirstPiclet = existingPiclets === 0;
32
-
33
  return {
34
  // Basic Info
35
- typeId: stats.name || data.name,
 
36
  nickname: stats.name || data.name,
37
  primaryType: stats.primaryType as PicletType,
38
  tier: tier,
39
-
40
- // Roster Management
41
- isInRoster: isFirstPiclet,
42
- rosterPosition: isFirstPiclet ? 0 : undefined,
43
-
44
- // Metadata
45
- caught: isFirstPiclet, // First piclet is automatically caught
46
- caughtAt: isFirstPiclet ? new Date() : undefined,
47
-
48
- // Original generation data
 
 
 
 
49
  imageUrl: data.imageUrl,
50
  imageData: data.imageData,
51
  imageCaption: data.imageCaption,
 
52
  concept: data.concept,
53
  description: stats.description,
54
  imagePrompt: data.imagePrompt
@@ -81,39 +91,32 @@ export async function getPicletInstance(id: number): Promise<PicletInstance | un
81
  return await db.picletInstances.get(id);
82
  }
83
 
84
- // Get roster piclets (those currently in battle roster)
85
- export async function getRosterPiclets(): Promise<PicletInstance[]> {
86
- return await db.picletInstances.where('isInRoster').equals(1).toArray();
87
- }
88
-
89
- // Get caught piclets (those that have been captured)
90
- export async function getCaughtPiclets(): Promise<PicletInstance[]> {
91
- return await db.picletInstances.where('caught').equals(1).toArray();
92
  }
93
 
94
- // Move a piclet to roster (simplified version)
95
- export async function moveToRoster(picletId: number, position: number): Promise<void> {
96
- await db.picletInstances.update(picletId, {
97
- isInRoster: true,
98
- rosterPosition: position
99
- });
100
  }
101
 
102
- // Get uncaught piclets
103
- export async function getUncaughtPiclets(): Promise<PicletInstance[]> {
104
- return await db.picletInstances.where('caught').equals(0).toArray();
105
  }
106
 
107
- // Swap roster positions (simplified version)
108
- export async function swapRosterPositions(id1: number, position1: number, id2: number, position2: number): Promise<void> {
109
- await db.picletInstances.update(id1, { rosterPosition: position2 });
110
- await db.picletInstances.update(id2, { rosterPosition: position1 });
111
  }
112
 
113
- // Move to storage (simplified version)
114
- export async function moveToStorage(picletId: number): Promise<void> {
115
- await db.picletInstances.update(picletId, {
116
- isInRoster: false,
117
- rosterPosition: undefined
118
- });
 
 
119
  }
 
16
  }
17
 
18
  // Convert generated piclet data to a PicletInstance
19
+ export async function generatedDataToPicletInstance(
20
+ data: GeneratedPicletData,
21
+ objectName?: string,
22
+ attributes?: string[],
23
+ visualDetails?: string
24
+ ): Promise<Omit<PicletInstance, 'id'>> {
25
  if (!data.stats) {
26
  throw new Error('Generated data must have stats to create PicletInstance');
27
  }
28
 
29
  const stats = data.stats as PicletStats;
30
+
31
+ // Map tier from stats to discovery rarity
32
+ let tier: string = stats.tier || 'common';
33
+
34
+ // Generate unique typeId
35
+ const typeId = `${objectName || stats.name}_${Date.now()}`;
36
+
 
37
  return {
38
  // Basic Info
39
+ typeId: typeId,
40
+ objectName: objectName || stats.name || data.name,
41
  nickname: stats.name || data.name,
42
  primaryType: stats.primaryType as PicletType,
43
  tier: tier,
44
+
45
+ // Discovery Metadata
46
+ isCanonical: false, // Will be set by server
47
+ canonicalId: undefined,
48
+ variationAttributes: attributes || [],
49
+ discoveredBy: 'Player', // Will be set by server
50
+ discoveredAt: new Date(),
51
+ scanCount: 1,
52
+
53
+ // Collection Management
54
+ isInCollection: true, // Auto-collected when scanned
55
+ collectedAt: new Date(),
56
+
57
+ // Visual Data
58
  imageUrl: data.imageUrl,
59
  imageData: data.imageData,
60
  imageCaption: data.imageCaption,
61
+ visualDetails: visualDetails || '',
62
  concept: data.concept,
63
  description: stats.description,
64
  imagePrompt: data.imagePrompt
 
91
  return await db.picletInstances.get(id);
92
  }
93
 
94
+ // Get collected piclets (those that have been discovered)
95
+ export async function getCollectedPiclets(): Promise<PicletInstance[]> {
96
+ return await db.picletInstances.where('isInCollection').equals(1).toArray();
 
 
 
 
 
97
  }
98
 
99
+ // Get canonical piclets
100
+ export async function getCanonicalPiclets(): Promise<PicletInstance[]> {
101
+ return await db.picletInstances.where('isCanonical').equals(1).toArray();
 
 
 
102
  }
103
 
104
+ // Get piclets by object name
105
+ export async function getPicletsByObjectName(objectName: string): Promise<PicletInstance[]> {
106
+ return await db.picletInstances.where('objectName').equals(objectName).toArray();
107
  }
108
 
109
+ // Get variations of a canonical piclet
110
+ export async function getVariations(canonicalId: string): Promise<PicletInstance[]> {
111
+ return await db.picletInstances.where('canonicalId').equals(canonicalId).toArray();
 
112
  }
113
 
114
+ // Update scan count
115
+ export async function updateScanCount(picletId: number): Promise<void> {
116
+ const piclet = await db.picletInstances.get(picletId);
117
+ if (piclet) {
118
+ await db.picletInstances.update(picletId, {
119
+ scanCount: (piclet.scanCount || 0) + 1
120
+ });
121
+ }
122
  }
src/lib/db/schema.ts CHANGED
@@ -1,70 +1,91 @@
1
  import type { PicletType } from '../types/picletTypes';
2
 
3
- // Enums
4
- export enum EncounterType {
5
- WILD_PICLET = 'wildPiclet',
6
- FIRST_PICLET = 'firstPiclet'
 
7
  }
8
 
9
 
10
- // PicletInstance - Individual monster instances owned by the player
11
  export interface PicletInstance {
12
  id?: number;
13
-
14
  // Basic Info
15
- typeId: string;
 
16
  nickname?: string;
17
  primaryType: PicletType;
18
- tier: string; // 'low' | 'medium' | 'high' | 'legendary'
19
-
20
- // Roster Management
21
- isInRoster: boolean;
22
- rosterPosition?: number; // 0-5 when in roster
23
-
24
- // Metadata
25
- caught: boolean; // Whether this Piclet has been caught by the player
26
- caughtAt?: Date; // When this Piclet was caught (undefined if not caught)
27
-
28
- // Original generation data
 
 
 
 
29
  imageUrl: string;
30
  imageData?: string; // Base64 encoded image with transparency
31
- imageCaption: string;
32
- concept: string;
 
33
  description: string; // Generated monster description
34
- imagePrompt: string;
35
  }
36
 
37
- // Encounter - Game encounters
38
- export interface Encounter {
39
  id?: number;
40
-
41
  // Type
42
- type: EncounterType;
43
-
44
  // Details
45
  title: string;
46
  description: string;
47
- picletTypeId?: string; // For wild piclet encounters
48
- picletInstanceId?: number; // For first piclet encounters - specific Piclet to catch
49
- enemyLevel?: number;
50
-
51
  // Timing
52
  createdAt: Date;
53
  }
54
 
55
- // GameState - Overall game progress
56
  export interface GameState {
57
  id?: number;
58
-
59
  // Timing
60
- lastEncounterRefresh: Date;
61
  lastPlayed: Date;
62
-
63
- // Progress (0-1000)
64
- progressPoints: number;
65
- trainersDefeated: number;
66
- picletsCapured: number;
67
- battlesLost: number;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
 
 
1
  import type { PicletType } from '../types/picletTypes';
2
 
3
+ // Discovery Status
4
+ export enum DiscoveryStatus {
5
+ NEW = 'new',
6
+ VARIATION = 'variation',
7
+ EXISTING = 'existing'
8
  }
9
 
10
 
11
+ // PicletInstance - Individual monster instances discovered by players
12
  export interface PicletInstance {
13
  id?: number;
14
+
15
  // Basic Info
16
+ typeId: string; // Unique identifier for this specific Piclet
17
+ objectName: string; // Canonical object name (e.g., "pillow", "pyramid")
18
  nickname?: string;
19
  primaryType: PicletType;
20
+ tier: string; // 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'
21
+
22
+ // Discovery Metadata
23
+ isCanonical: boolean; // True if this is the canonical version
24
+ canonicalId?: string; // Reference to canonical Piclet if this is a variation
25
+ variationAttributes?: string[]; // e.g., ["velvet", "blue"] for variations
26
+ discoveredBy?: string; // Username of first discoverer
27
+ discoveredAt: Date; // When first discovered
28
+ scanCount: number; // Total times this Piclet has been scanned
29
+
30
+ // Collection Management
31
+ isInCollection: boolean; // Whether player has discovered this
32
+ collectedAt?: Date; // When player discovered it
33
+
34
+ // Visual Data
35
  imageUrl: string;
36
  imageData?: string; // Base64 encoded image with transparency
37
+ imageCaption: string; // Original caption from image
38
+ visualDetails?: string; // Extra visual details for monster generation
39
+ concept: string; // Generated monster concept
40
  description: string; // Generated monster description
41
+ imagePrompt: string; // Prompt used for image generation
42
  }
43
 
44
+ // ActivityEntry - Recent discoveries and leaderboard
45
+ export interface ActivityEntry {
46
  id?: number;
47
+
48
  // Type
49
+ type: 'discovery' | 'variation' | 'milestone';
50
+
51
  // Details
52
  title: string;
53
  description: string;
54
+ picletTypeId: string;
55
+ discovererName: string;
56
+ rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
57
+
58
  // Timing
59
  createdAt: Date;
60
  }
61
 
62
+ // GameState - Discovery progress and stats
63
  export interface GameState {
64
  id?: number;
65
+
66
  // Timing
67
+ lastActivityRefresh: Date;
68
  lastPlayed: Date;
69
+
70
+ // Discovery Stats
71
+ totalDiscoveries: number;
72
+ uniqueDiscoveries: number; // Canonical Piclets found
73
+ variationsFound: number;
74
+ rarityScore: number; // Sum of rarity points
75
+ currentStreak: number; // Days in a row with discoveries
76
+
77
+ // Cached server data
78
+ lastServerSync?: Date;
79
+ cachedLeaderboard?: LeaderboardEntry[];
80
+ }
81
+
82
+ // LeaderboardEntry - For activity feed
83
+ export interface LeaderboardEntry {
84
+ username: string;
85
+ totalDiscoveries: number;
86
+ uniqueDiscoveries: number;
87
+ rarityScore: number;
88
+ rank: number;
89
  }
90
 
91
 
src/lib/services/canonicalService.ts ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { PicletInstance, DiscoveryStatus } from '$lib/db/schema';
2
+
3
+ const SERVER_URL = import.meta.env.DEV
4
+ ? 'http://localhost:3000'
5
+ : 'https://piclets-server.herokuapp.com';
6
+
7
+ export interface CanonicalSearchResult {
8
+ status: DiscoveryStatus;
9
+ piclet: PicletInstance;
10
+ canonicalId?: string;
11
+ matchedAttributes?: string[];
12
+ suggestedVariation?: string[];
13
+ }
14
+
15
+ export interface ObjectExtractionResult {
16
+ primaryObject: string;
17
+ attributes: string[];
18
+ visualDetails: string;
19
+ }
20
+
21
+ export class CanonicalService {
22
+ /**
23
+ * Extract object and attributes from image caption
24
+ * Focuses on identifying the primary object and its variations
25
+ */
26
+ static extractObjectFromCaption(caption: string): ObjectExtractionResult {
27
+ // Clean and normalize caption
28
+ const normalized = caption.toLowerCase().trim();
29
+
30
+ // Common patterns to identify the main object
31
+ // Priority: noun after article (a/an/the), first noun, or key noun phrases
32
+ const objectPatterns = [
33
+ /(?:a|an|the)\s+(\w+(?:\s+\w+)?)\s+(?:is|sits|stands|lies|rests)/i,
34
+ /(?:a|an|the)\s+([\w\s]+?)(?:\s+with|\s+that|\s+in|\s+on|,|\.|$)/i,
35
+ /^([\w\s]+?)(?:\s+with|\s+that|\s+in|\s+on|,|\.|$)/i,
36
+ ];
37
+
38
+ let primaryObject = '';
39
+ for (const pattern of objectPatterns) {
40
+ const match = caption.match(pattern);
41
+ if (match && match[1]) {
42
+ // Clean up the captured object
43
+ primaryObject = match[1]
44
+ .trim()
45
+ .replace(/\s+/g, ' ')
46
+ .split(' ')
47
+ .filter(word => !['very', 'quite', 'rather', 'extremely'].includes(word))
48
+ .pop() || ''; // Get the last word as the core object
49
+
50
+ if (primaryObject) break;
51
+ }
52
+ }
53
+
54
+ // Fallback: take first noun-like word
55
+ if (!primaryObject) {
56
+ const words = normalized.split(/\s+/);
57
+ primaryObject = words.find(w => w.length > 3 && !['with', 'that', 'this', 'from'].includes(w)) || 'object';
58
+ }
59
+
60
+ // Extract descriptive attributes (limit to 2-3 most relevant)
61
+ const attributeWords = [
62
+ // Materials
63
+ 'wooden', 'metal', 'plastic', 'glass', 'leather', 'velvet', 'silk', 'cotton', 'stone', 'marble',
64
+ 'gold', 'silver', 'bronze', 'copper', 'steel', 'iron', 'aluminum', 'ceramic', 'porcelain',
65
+ // Styles
66
+ 'modern', 'vintage', 'antique', 'rustic', 'minimalist', 'ornate', 'gothic', 'art deco', 'retro',
67
+ // Colors (basic only)
68
+ 'red', 'blue', 'green', 'yellow', 'purple', 'orange', 'black', 'white', 'gray', 'brown',
69
+ // Patterns
70
+ 'striped', 'polka dot', 'floral', 'geometric', 'plaid', 'checkered',
71
+ // Conditions
72
+ 'old', 'new', 'worn', 'shiny', 'matte', 'glossy', 'rough', 'smooth'
73
+ ];
74
+
75
+ const attributes: string[] = [];
76
+ const lowerCaption = caption.toLowerCase();
77
+
78
+ for (const attr of attributeWords) {
79
+ if (lowerCaption.includes(attr) && attributes.length < 3) {
80
+ attributes.push(attr);
81
+ }
82
+ }
83
+
84
+ // Extract visual details for monster generation (everything else interesting)
85
+ const visualDetails = caption
86
+ .replace(new RegExp(primaryObject, 'gi'), '')
87
+ .replace(new RegExp(attributes.join('|'), 'gi'), '')
88
+ .replace(/(?:a|an|the)\s+/gi, '')
89
+ .replace(/\s+/g, ' ')
90
+ .trim();
91
+
92
+ return {
93
+ primaryObject: primaryObject.toLowerCase(),
94
+ attributes,
95
+ visualDetails
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Search for canonical Piclet or variations
101
+ */
102
+ static async searchCanonical(
103
+ objectName: string,
104
+ attributes: string[]
105
+ ): Promise<CanonicalSearchResult | null> {
106
+ try {
107
+ const response = await fetch(`${SERVER_URL}/api/piclets/search`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ object: objectName, attributes })
111
+ });
112
+
113
+ if (!response.ok) {
114
+ console.error('Server search failed:', response.status);
115
+ return null;
116
+ }
117
+
118
+ return await response.json();
119
+ } catch (error) {
120
+ console.error('Failed to search canonical:', error);
121
+ return null;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Create a new canonical Piclet
127
+ */
128
+ static async createCanonical(
129
+ piclet: Partial<PicletInstance>,
130
+ discovererName: string
131
+ ): Promise<PicletInstance | null> {
132
+ try {
133
+ const response = await fetch(`${SERVER_URL}/api/piclets/canonical`, {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify({
137
+ ...piclet,
138
+ discoveredBy: discovererName,
139
+ discoveredAt: new Date(),
140
+ isCanonical: true,
141
+ scanCount: 1
142
+ })
143
+ });
144
+
145
+ if (!response.ok) {
146
+ console.error('Failed to create canonical:', response.status);
147
+ return null;
148
+ }
149
+
150
+ return await response.json();
151
+ } catch (error) {
152
+ console.error('Failed to create canonical:', error);
153
+ return null;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Create a variation of existing canonical Piclet
159
+ */
160
+ static async createVariation(
161
+ canonicalId: string,
162
+ variation: Partial<PicletInstance>,
163
+ discovererName: string
164
+ ): Promise<PicletInstance | null> {
165
+ try {
166
+ const response = await fetch(`${SERVER_URL}/api/piclets/variation`, {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({
170
+ canonicalId,
171
+ ...variation,
172
+ discoveredBy: discovererName,
173
+ discoveredAt: new Date(),
174
+ isCanonical: false,
175
+ scanCount: 1
176
+ })
177
+ });
178
+
179
+ if (!response.ok) {
180
+ console.error('Failed to create variation:', response.status);
181
+ return null;
182
+ }
183
+
184
+ return await response.json();
185
+ } catch (error) {
186
+ console.error('Failed to create variation:', error);
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Increment scan count for existing Piclet
193
+ */
194
+ static async incrementScanCount(picletId: string): Promise<void> {
195
+ try {
196
+ await fetch(`${SERVER_URL}/api/piclets/${picletId}/scan`, {
197
+ method: 'POST'
198
+ });
199
+ } catch (error) {
200
+ console.error('Failed to increment scan count:', error);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Calculate rarity based on scan count
206
+ */
207
+ static calculateRarity(scanCount: number): string {
208
+ if (scanCount <= 5) return 'legendary';
209
+ if (scanCount <= 20) return 'epic';
210
+ if (scanCount <= 50) return 'rare';
211
+ if (scanCount <= 100) return 'uncommon';
212
+ return 'common';
213
+ }
214
+ }
src/lib/services/enhancedCaption.ts ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { GradioClient } from '$lib/types';
2
+
3
+ export interface EnhancedCaptionResult {
4
+ objectCaption: string; // Main object identification
5
+ visualDetails: string; // Additional visual details for monster generation
6
+ fullCaption: string; // Complete original caption
7
+ extractedObject?: string; // Parsed primary object
8
+ extractedAttributes?: string[]; // Parsed attributes
9
+ }
10
+
11
+ export class EnhancedCaptionService {
12
+ /**
13
+ * Generate multiple captions to extract object and visual details
14
+ */
15
+ static async generateEnhancedCaption(
16
+ client: GradioClient,
17
+ image: Blob | File
18
+ ): Promise<EnhancedCaptionResult> {
19
+ try {
20
+ // First caption: Focus on object identification
21
+ const objectPrompt = "Identify the main object in this image. Start with 'This is a/an' followed by the object name and up to 2-3 key attributes (material, color, or style). Be concise and focus only on WHAT the object is, not where it is or what surrounds it.";
22
+
23
+ const objectResult = await client.predict("/stream_chat", [
24
+ image,
25
+ "Descriptive", // caption type
26
+ "short", // length - short for object identification
27
+ [], // extra_options
28
+ "", // name_input
29
+ objectPrompt // custom_prompt for object focus
30
+ ]);
31
+
32
+ const objectCaption = objectResult.data[1] as string;
33
+
34
+ // Second caption: Get visual details for monster generation
35
+ const detailsPrompt = "Describe the unique visual characteristics, textures, patterns, and interesting details of this object that would make it distinctive as a creature. Focus on surface details, decorative elements, and any unusual features. Do not repeat the object name.";
36
+
37
+ const detailsResult = await client.predict("/stream_chat", [
38
+ image,
39
+ "Descriptive",
40
+ "medium-length", // More details for visual generation
41
+ [],
42
+ "",
43
+ detailsPrompt
44
+ ]);
45
+
46
+ const visualDetails = detailsResult.data[1] as string;
47
+
48
+ // Third caption: Full descriptive caption as backup
49
+ const fullResult = await client.predict("/stream_chat", [
50
+ image,
51
+ "Descriptive",
52
+ "long",
53
+ [],
54
+ "",
55
+ "" // No custom prompt for natural full description
56
+ ]);
57
+
58
+ const fullCaption = fullResult.data[1] as string;
59
+
60
+ // Extract structured data from object caption
61
+ const extraction = this.parseObjectCaption(objectCaption);
62
+
63
+ return {
64
+ objectCaption,
65
+ visualDetails,
66
+ fullCaption,
67
+ extractedObject: extraction.object,
68
+ extractedAttributes: extraction.attributes
69
+ };
70
+ } catch (error) {
71
+ console.error('Enhanced caption generation failed:', error);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Parse the object-focused caption to extract structured data
78
+ */
79
+ private static parseObjectCaption(caption: string): {
80
+ object: string;
81
+ attributes: string[];
82
+ } {
83
+ // Remove "This is a/an" prefix
84
+ let cleaned = caption
85
+ .replace(/^(This is|It'?s|That'?s)\s+(a|an|the)?\s*/i, '')
86
+ .trim();
87
+
88
+ // Common attribute words to extract
89
+ const attributePatterns = [
90
+ // Materials
91
+ /\b(wooden|metal|plastic|glass|leather|velvet|silk|cotton|stone|marble|ceramic|porcelain)\b/gi,
92
+ // Colors
93
+ /\b(red|blue|green|yellow|purple|orange|black|white|gray|brown|pink|gold|silver)\b/gi,
94
+ // Styles
95
+ /\b(modern|vintage|antique|rustic|minimalist|ornate|gothic|retro|classic)\b/gi,
96
+ // Patterns
97
+ /\b(striped|checkered|floral|geometric|polka-dot|plaid)\b/gi
98
+ ];
99
+
100
+ const attributes: string[] = [];
101
+ let objectText = cleaned;
102
+
103
+ // Extract attributes from the caption
104
+ for (const pattern of attributePatterns) {
105
+ const matches = cleaned.match(pattern);
106
+ if (matches) {
107
+ attributes.push(...matches.map(m => m.toLowerCase()));
108
+ // Remove matched attributes from object text
109
+ objectText = objectText.replace(pattern, '').trim();
110
+ }
111
+ }
112
+
113
+ // Clean up object text - get the core noun
114
+ const words = objectText.split(/\s+/).filter(w => w.length > 0);
115
+
116
+ // Remove common descriptive words that aren't the object
117
+ const filterWords = ['with', 'that', 'which', 'having', 'featuring', 'very', 'quite', 'rather'];
118
+ const objectWords = words.filter(w => !filterWords.includes(w.toLowerCase()));
119
+
120
+ // The object is typically the first significant noun
121
+ let object = objectWords[0] || 'object';
122
+
123
+ // Handle compound objects (e.g., "coffee mug" -> "mug", "throw pillow" -> "pillow")
124
+ const compoundMappings: Record<string, string> = {
125
+ 'coffee': 'mug',
126
+ 'throw': 'pillow',
127
+ 'picture': 'frame',
128
+ 'water': 'bottle',
129
+ 'wine': 'glass',
130
+ 'flower': 'vase',
131
+ 'table': 'lamp',
132
+ 'desk': 'lamp',
133
+ 'floor': 'lamp'
134
+ };
135
+
136
+ if (compoundMappings[object.toLowerCase()] && objectWords.length > 1) {
137
+ object = objectWords[1];
138
+ }
139
+
140
+ // Limit attributes to top 3 most relevant
141
+ const uniqueAttributes = [...new Set(attributes)].slice(0, 3);
142
+
143
+ return {
144
+ object: object.toLowerCase(),
145
+ attributes: uniqueAttributes
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Generate a combined prompt for monster generation
151
+ */
152
+ static createMonsterPrompt(
153
+ objectName: string,
154
+ visualDetails: string,
155
+ attributes: string[]
156
+ ): string {
157
+ const attributeText = attributes.length > 0
158
+ ? ` with ${attributes.join(', ')} characteristics`
159
+ : '';
160
+
161
+ return `Create a Pokemon-style creature based on a ${objectName}${attributeText}.
162
+
163
+ Visual inspiration: ${visualDetails}
164
+
165
+ The creature should embody the essence of a ${objectName} while incorporating these visual elements into its design. Make it cute but distinctive, with clear ${objectName}-inspired features.`;
166
+ }
167
+ }
src/lib/services/picletMetadata.ts CHANGED
@@ -4,7 +4,7 @@ const METADATA_KEY = 'snaplings-piclet-v1';
4
 
5
  interface PicletMetadata {
6
  version: 1;
7
- data: Omit<PicletInstance, 'id' | 'rosterPosition' | 'isInRoster' | 'caughtAt'>;
8
  checksum?: string;
9
  }
10
 
@@ -44,9 +44,8 @@ export async function extractPicletMetadata(file: File): Promise<PicletInstance
44
  // Create PicletInstance from metadata
45
  const piclet: PicletInstance = {
46
  ...metadata.data,
47
- caughtAt: new Date(), // Use current date for import
48
- isInRoster: false,
49
- rosterPosition: undefined
50
  };
51
 
52
  return piclet;
@@ -68,33 +67,24 @@ export async function embedPicletMetadata(imageBlob: Blob, piclet: PicletInstanc
68
  version: 1,
69
  data: {
70
  typeId: piclet.typeId,
 
71
  nickname: piclet.nickname,
72
  primaryType: piclet.primaryType,
73
- currentHp: piclet.maxHp, // Reset to full HP for sharing
74
- maxHp: piclet.maxHp,
75
- level: piclet.level,
76
- xp: piclet.xp,
77
- attack: piclet.attack,
78
- defense: piclet.defense,
79
- fieldAttack: piclet.fieldAttack,
80
- fieldDefense: piclet.fieldDefense,
81
- speed: piclet.speed,
82
- baseHp: piclet.baseHp,
83
- baseAttack: piclet.baseAttack,
84
- baseDefense: piclet.baseDefense,
85
- baseFieldAttack: piclet.baseFieldAttack,
86
- baseFieldDefense: piclet.baseFieldDefense,
87
- baseSpeed: piclet.baseSpeed,
88
- moves: piclet.moves,
89
- nature: piclet.nature,
90
- bst: piclet.bst,
91
  tier: piclet.tier,
92
- role: piclet.role,
93
- variance: piclet.variance,
 
 
 
 
 
 
94
  imageUrl: piclet.imageUrl,
95
  imageData: piclet.imageData,
96
  imageCaption: piclet.imageCaption,
 
97
  concept: piclet.concept,
 
98
  imagePrompt: piclet.imagePrompt
99
  }
100
  };
 
4
 
5
  interface PicletMetadata {
6
  version: 1;
7
+ data: Omit<PicletInstance, 'id'>;
8
  checksum?: string;
9
  }
10
 
 
44
  // Create PicletInstance from metadata
45
  const piclet: PicletInstance = {
46
  ...metadata.data,
47
+ collectedAt: new Date(), // Use current date for import
48
+ isInCollection: true
 
49
  };
50
 
51
  return piclet;
 
67
  version: 1,
68
  data: {
69
  typeId: piclet.typeId,
70
+ objectName: piclet.objectName,
71
  nickname: piclet.nickname,
72
  primaryType: piclet.primaryType,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  tier: piclet.tier,
74
+ isCanonical: piclet.isCanonical,
75
+ canonicalId: piclet.canonicalId,
76
+ variationAttributes: piclet.variationAttributes,
77
+ discoveredBy: piclet.discoveredBy,
78
+ discoveredAt: piclet.discoveredAt,
79
+ scanCount: piclet.scanCount,
80
+ isInCollection: piclet.isInCollection,
81
+ collectedAt: piclet.collectedAt,
82
  imageUrl: piclet.imageUrl,
83
  imageData: piclet.imageData,
84
  imageCaption: piclet.imageCaption,
85
+ visualDetails: piclet.visualDetails,
86
  concept: piclet.concept,
87
+ description: piclet.description,
88
  imagePrompt: piclet.imagePrompt
89
  }
90
  };
src/lib/types/index.ts CHANGED
@@ -75,13 +75,14 @@ export interface GradioLibs {
75
  }
76
 
77
  // Piclet Generator Types
78
- export type PicletWorkflowStep =
79
- | 'upload'
80
- | 'captioning'
 
81
  | 'conceptualizing'
82
- | 'statsGenerating'
83
- | 'promptCrafting'
84
- | 'generating'
85
  | 'complete';
86
 
87
  export interface PicletWorkflowState {
@@ -94,6 +95,12 @@ export interface PicletWorkflowState {
94
  picletImage: FluxGenerationResult | null;
95
  error: string | null;
96
  isProcessing: boolean;
 
 
 
 
 
 
97
  }
98
 
99
  export interface PicletGeneratorProps {
 
75
  }
76
 
77
  // Piclet Generator Types
78
+ export type PicletWorkflowStep =
79
+ | 'upload'
80
+ | 'captioning'
81
+ | 'checking'
82
  | 'conceptualizing'
83
+ | 'statsGenerating'
84
+ | 'promptCrafting'
85
+ | 'generating'
86
  | 'complete';
87
 
88
  export interface PicletWorkflowState {
 
95
  picletImage: FluxGenerationResult | null;
96
  error: string | null;
97
  isProcessing: boolean;
98
+ // Discovery-specific state
99
+ objectName: string | null;
100
+ objectAttributes: string[];
101
+ visualDetails: string | null;
102
+ discoveryStatus: 'new' | 'variation' | 'existing' | null;
103
+ canonicalPiclet: any | null; // Will be PicletInstance but avoiding circular dep
104
  }
105
 
106
  export interface PicletGeneratorProps {