RESET TO MONSTER DISCOVERY SYSTEM
Browse files- CLAUDE.md +86 -28
- pokemon_emerald_docs/DEFINING_ATTACKS.md +0 -277
- pokemon_emerald_docs/battle_system_architecture.md +0 -363
- pokemon_emerald_docs/battle_system_mechanics.md +0 -475
- src/App.svelte +4 -4
- src/lib/components/Battle/ActionButtons.svelte +0 -113
- src/lib/components/Battle/ActionViewSelector.svelte +0 -508
- src/lib/components/Battle/FieldEffectIndicator.svelte +0 -117
- src/lib/components/Battle/LLMBattleEngine.svelte +0 -453
- src/lib/components/Battle/PicletInfo.svelte +0 -244
- src/lib/components/Battle/StatusEffectIndicator.svelte +0 -89
- src/lib/components/Battle/TypewriterText.svelte +0 -49
- src/lib/components/Layout/ProgressBar.svelte +21 -18
- src/lib/components/Layout/TabBar.svelte +2 -2
- src/lib/components/Pages/Activity.svelte +441 -0
- src/lib/components/Pages/Battle.svelte +0 -177
- src/lib/components/Pages/Encounters.svelte +0 -586
- src/lib/components/Pages/Pictuary.svelte +305 -507
- src/lib/components/Pages/ViewAll.svelte +0 -140
- src/lib/components/PicletGenerator/PicletGenerator.svelte +85 -37
- src/lib/components/PicletGenerator/PicletResult.svelte +12 -5
- src/lib/components/Piclets/AddToRosterDialog.svelte +0 -169
- src/lib/components/Piclets/DraggablePicletCard.svelte +0 -70
- src/lib/components/Piclets/EmptySlotCard.svelte +0 -61
- src/lib/components/Piclets/NewlyCaughtPicletDetail.svelte +0 -644
- src/lib/components/Piclets/PicletDetail.svelte +22 -8
- src/lib/components/Piclets/RosterSlot.svelte +0 -109
- src/lib/db/encounterService.ts +0 -115
- src/lib/db/gameState.ts +45 -36
- src/lib/db/index.ts +26 -2
- src/lib/db/piclets.ts +50 -47
- src/lib/db/schema.ts +60 -39
- src/lib/services/canonicalService.ts +214 -0
- src/lib/services/enhancedCaption.ts +167 -0
- src/lib/services/picletMetadata.ts +14 -24
- 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
|
8 |
|
9 |
### Main Features
|
10 |
-
- **Monster
|
11 |
-
- **
|
12 |
-
- **Collection Management**:
|
13 |
-
- **
|
14 |
-
- **
|
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` (
|
51 |
-
- **Monster
|
52 |
-
- **
|
53 |
-
- **Piclet Management**: `PicletCard.svelte`, `PicletDetail.svelte
|
54 |
-
- **Database**: IndexedDB with `schema.ts`
|
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
|
|
|
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 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
###
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
###
|
110 |
-
- **
|
111 |
-
- **
|
112 |
-
- **
|
|
|
113 |
|
114 |
### Performance
|
115 |
- **Large image files**: Consider image compression before upload
|
116 |
-
- **
|
117 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
-
|
|
|
|
|
|
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
|
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 |
-
|
36 |
pictuary: 'Pictuary'
|
37 |
};
|
38 |
|
@@ -175,8 +175,8 @@
|
|
175 |
{joyCaptionClient}
|
176 |
{qwenClient}
|
177 |
/>
|
178 |
-
{:else if activeTab === '
|
179 |
-
<
|
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
|
9 |
-
const progressColor = $derived(getProgressColor(gameState?.
|
10 |
-
|
11 |
-
function getProgressColor(
|
12 |
-
|
13 |
-
if (
|
14 |
-
if (
|
15 |
-
if (
|
16 |
-
|
17 |
-
return '#9c27b0'; // purple
|
18 |
}
|
19 |
|
20 |
-
onMount(
|
21 |
// Load game state
|
22 |
-
|
23 |
-
|
|
|
|
|
24 |
// Refresh game state periodically
|
25 |
-
const interval = setInterval(
|
26 |
-
|
|
|
|
|
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.
|
37 |
</div>
|
38 |
-
<span class="progress-stats"
|
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' | '
|
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: '
|
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 {
|
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
|
12 |
-
|
13 |
-
|
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
|
22 |
-
let
|
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 |
-
//
|
36 |
-
let
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
39 |
}
|
40 |
|
41 |
-
//
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
49 |
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
58 |
});
|
59 |
|
60 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
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 |
-
|
87 |
-
discoveredPiclets = await getUncaughtPiclets();
|
88 |
-
} catch (err) {
|
89 |
-
console.error('Failed to load piclets:', err);
|
90 |
} finally {
|
91 |
isLoading = false;
|
92 |
}
|
93 |
}
|
94 |
-
|
95 |
-
|
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 |
-
|
238 |
-
currentlyDragging = instance;
|
239 |
-
}
|
240 |
-
|
241 |
-
function handleDragEnd() {
|
242 |
-
currentlyDragging = null;
|
243 |
}
|
244 |
-
|
245 |
-
async function
|
246 |
-
|
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 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
{
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
<div class="
|
296 |
-
|
|
|
|
|
297 |
</div>
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
</div>
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
{
|
334 |
-
|
|
|
335 |
{/if}
|
336 |
</div>
|
337 |
-
|
338 |
-
|
339 |
-
|
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 |
-
|
407 |
-
background: white;
|
408 |
-
position: relative;
|
409 |
}
|
410 |
-
|
411 |
-
.
|
412 |
-
|
413 |
-
|
414 |
-
|
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 |
-
|
429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
.
|
452 |
-
|
453 |
-
|
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 |
-
.
|
464 |
-
|
465 |
color: #666;
|
|
|
466 |
}
|
467 |
-
|
468 |
-
.
|
469 |
-
padding:
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
section {
|
475 |
-
margin-bottom: 2rem;
|
476 |
}
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
|
|
483 |
}
|
484 |
-
|
485 |
-
.
|
486 |
display: flex;
|
487 |
-
|
488 |
-
align-items: center;
|
489 |
-
margin-bottom: 0.75rem;
|
490 |
-
}
|
491 |
-
|
492 |
-
.section-header h2 {
|
493 |
-
margin: 0;
|
494 |
}
|
495 |
-
|
496 |
-
.view-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
|
|
|
|
501 |
cursor: pointer;
|
502 |
-
|
503 |
}
|
504 |
-
|
505 |
-
.
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
|
|
|
|
|
|
|
|
510 |
}
|
511 |
-
|
512 |
-
.
|
513 |
display: flex;
|
514 |
-
|
515 |
-
|
516 |
-
-
|
517 |
-
padding
|
|
|
518 |
}
|
519 |
-
|
520 |
-
.
|
521 |
-
|
|
|
|
|
|
|
|
|
|
|
522 |
}
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
border-radius: 2px;
|
527 |
}
|
528 |
-
|
529 |
-
.
|
530 |
-
|
531 |
-
|
|
|
532 |
}
|
533 |
-
|
534 |
-
.
|
535 |
display: grid;
|
536 |
-
grid-template-columns: repeat(auto-fill, minmax(
|
537 |
-
gap:
|
538 |
}
|
539 |
-
|
540 |
-
.
|
541 |
-
|
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:
|
553 |
-
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
|
554 |
}
|
555 |
-
|
556 |
-
.
|
557 |
-
|
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 |
-
.
|
568 |
-
|
569 |
-
|
570 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
571 |
}
|
572 |
-
|
573 |
-
|
574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
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
|
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 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
console.log('
|
|
|
|
|
|
|
|
|
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.
|
325 |
-
throw new Error(`${currentTextClient} service not available or no
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
331 |
|
332 |
-
|
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
|
343 |
|
344 |
# Monster Name
|
345 |
-
{Creative name
|
346 |
|
347 |
# Primary Type
|
348 |
-
{
|
349 |
|
350 |
# Monster Description
|
351 |
-
{Detailed physical description
|
352 |
|
353 |
# Monster Image Prompt
|
354 |
-
{
|
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(
|
|
|
|
|
|
|
|
|
|
|
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?.
|
73 |
-
<strong>{picletInstance?.
|
|
|
|
|
74 |
{:else}
|
75 |
-
|
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>
|
68 |
-
{instance.
|
69 |
</div>
|
70 |
-
{#if instance.
|
71 |
<div class="meta-item">
|
72 |
-
<strong>
|
73 |
-
|
74 |
</div>
|
75 |
{/if}
|
76 |
-
{#if instance.
|
77 |
<div class="meta-item">
|
78 |
-
<strong>
|
79 |
-
|
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,
|
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 |
-
|
17 |
lastPlayed: new Date(),
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
|
|
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
|
35 |
-
export async function
|
36 |
const state = await getOrCreateGameState();
|
37 |
await db.gameState.update(state.id!, {
|
38 |
-
[counter]: state[counter] + 1
|
39 |
});
|
40 |
}
|
41 |
|
42 |
-
// Add
|
43 |
-
export async function
|
44 |
const state = await getOrCreateGameState();
|
45 |
-
const newPoints = Math.min(1000, state.progressPoints + points);
|
46 |
await db.gameState.update(state.id!, {
|
47 |
-
|
48 |
});
|
49 |
}
|
50 |
|
51 |
-
// Create a new
|
52 |
-
export async function
|
53 |
-
return await db.
|
54 |
-
...
|
55 |
createdAt: new Date()
|
56 |
});
|
57 |
}
|
58 |
|
59 |
-
// Get recent
|
60 |
-
export async function
|
61 |
-
return await db.
|
62 |
.orderBy('createdAt')
|
63 |
.reverse()
|
64 |
.limit(limit)
|
65 |
.toArray();
|
66 |
}
|
67 |
|
68 |
-
//
|
69 |
-
export async function
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
.toArray();
|
74 |
}
|
75 |
|
76 |
-
//
|
77 |
-
export async function
|
78 |
const state = await getOrCreateGameState();
|
79 |
-
|
80 |
-
|
|
|
81 |
}
|
82 |
|
83 |
-
//
|
84 |
-
export async function
|
85 |
const state = await getOrCreateGameState();
|
86 |
await db.gameState.update(state.id!, {
|
87 |
-
|
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,
|
3 |
|
4 |
export class PicletDatabase extends Dexie {
|
5 |
// Game tables
|
6 |
picletInstances!: Table<PicletInstance>;
|
7 |
-
|
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(
|
|
|
|
|
|
|
|
|
|
|
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 || '
|
28 |
-
|
29 |
-
//
|
30 |
-
const
|
31 |
-
|
32 |
-
|
33 |
return {
|
34 |
// Basic Info
|
35 |
-
typeId:
|
|
|
36 |
nickname: stats.name || data.name,
|
37 |
primaryType: stats.primaryType as PicletType,
|
38 |
tier: tier,
|
39 |
-
|
40 |
-
//
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
//
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
//
|
|
|
|
|
|
|
|
|
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
|
85 |
-
export async function
|
86 |
-
return await db.picletInstances.where('
|
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 |
-
//
|
95 |
-
export async function
|
96 |
-
await db.picletInstances.
|
97 |
-
isInRoster: true,
|
98 |
-
rosterPosition: position
|
99 |
-
});
|
100 |
}
|
101 |
|
102 |
-
// Get
|
103 |
-
export async function
|
104 |
-
return await db.picletInstances.where('
|
105 |
}
|
106 |
|
107 |
-
//
|
108 |
-
export async function
|
109 |
-
await db.picletInstances.
|
110 |
-
await db.picletInstances.update(id2, { rosterPosition: position1 });
|
111 |
}
|
112 |
|
113 |
-
//
|
114 |
-
export async function
|
115 |
-
await db.picletInstances.
|
116 |
-
|
117 |
-
|
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 |
-
//
|
4 |
-
export enum
|
5 |
-
|
6 |
-
|
|
|
7 |
}
|
8 |
|
9 |
|
10 |
-
// PicletInstance - Individual monster instances
|
11 |
export interface PicletInstance {
|
12 |
id?: number;
|
13 |
-
|
14 |
// Basic Info
|
15 |
-
typeId: string;
|
|
|
16 |
nickname?: string;
|
17 |
primaryType: PicletType;
|
18 |
-
tier: string; // '
|
19 |
-
|
20 |
-
//
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
//
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
//
|
|
|
|
|
|
|
|
|
29 |
imageUrl: string;
|
30 |
imageData?: string; // Base64 encoded image with transparency
|
31 |
-
imageCaption: string;
|
32 |
-
|
|
|
33 |
description: string; // Generated monster description
|
34 |
-
imagePrompt: string;
|
35 |
}
|
36 |
|
37 |
-
//
|
38 |
-
export interface
|
39 |
id?: number;
|
40 |
-
|
41 |
// Type
|
42 |
-
type:
|
43 |
-
|
44 |
// Details
|
45 |
title: string;
|
46 |
description: string;
|
47 |
-
picletTypeId
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
// Timing
|
52 |
createdAt: Date;
|
53 |
}
|
54 |
|
55 |
-
// GameState -
|
56 |
export interface GameState {
|
57 |
id?: number;
|
58 |
-
|
59 |
// Timing
|
60 |
-
|
61 |
lastPlayed: Date;
|
62 |
-
|
63 |
-
//
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'
|
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 |
-
|
48 |
-
|
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 |
-
|
93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|