Fraser commited on
Commit
c703ea3
·
1 Parent(s): 47b6a85

BIG CHANGE

Browse files
Files changed (49) hide show
  1. battle-system-todo.md +0 -121
  2. battle_system_design.md +0 -1868
  3. src/lib/battle-engine/BattleEngine.test.ts +0 -364
  4. src/lib/battle-engine/BattleEngine.ts +0 -1876
  5. src/lib/battle-engine/MultiBattleEngine.test.ts +0 -671
  6. src/lib/battle-engine/MultiBattleEngine.ts +0 -701
  7. src/lib/battle-engine/README.md +0 -232
  8. src/lib/battle-engine/ability-triggers.test.ts +0 -388
  9. src/lib/battle-engine/advanced-effects.test.ts +0 -613
  10. src/lib/battle-engine/advanced-mechanic-overrides.test.ts +0 -348
  11. src/lib/battle-engine/advanced-status-effects.test.ts +0 -369
  12. src/lib/battle-engine/debug-field-effects.test.ts +0 -101
  13. src/lib/battle-engine/extreme-moves.test.ts +0 -544
  14. src/lib/battle-engine/extreme-risk-reward.test.ts +0 -383
  15. src/lib/battle-engine/field-effects.test.ts +0 -493
  16. src/lib/battle-engine/integration.test.ts +0 -255
  17. src/lib/battle-engine/mechanic-overrides.test.ts +0 -544
  18. src/lib/battle-engine/missing-features.test.ts +0 -498
  19. src/lib/battle-engine/move-flags.test.ts +0 -692
  20. src/lib/battle-engine/multi-piclet-types.ts +0 -160
  21. src/lib/battle-engine/package.json +0 -30
  22. src/lib/battle-engine/remaining-triggers.test.ts +0 -363
  23. src/lib/battle-engine/switching-system.test.ts +0 -445
  24. src/lib/battle-engine/tempest-wraith.test.ts +0 -507
  25. src/lib/battle-engine/test-data.ts +0 -234
  26. src/lib/battle-engine/types.ts +0 -262
  27. src/lib/components/Battle/ActionViewSelector.svelte +1 -47
  28. src/lib/components/Battle/BattleControls.svelte +0 -136
  29. src/lib/components/Battle/BattleEffects.svelte +0 -522
  30. src/lib/components/Battle/BattleField.svelte +0 -536
  31. src/lib/components/Battle/LLMBattleEngine.svelte +455 -0
  32. src/lib/components/Pages/Battle.svelte +124 -983
  33. src/lib/components/Pages/Encounters.svelte +7 -74
  34. src/lib/components/Pages/Pictuary.svelte +0 -1
  35. src/lib/components/PicletGenerator/PicletGenerator.svelte +58 -396
  36. src/lib/components/Piclets/NewlyCaughtPicletDetail.svelte +0 -5
  37. src/lib/components/Piclets/PicletCard.svelte +30 -46
  38. src/lib/components/Piclets/PicletDetail.svelte +160 -697
  39. src/lib/db/battleService.ts +0 -170
  40. src/lib/db/encounterService.ts +37 -207
  41. src/lib/db/piclets.ts +46 -201
  42. src/lib/db/schema.ts +5 -72
  43. src/lib/services/captureService.ts +0 -212
  44. src/lib/services/levelingService.ts +0 -332
  45. src/lib/services/picletMetadata.ts +0 -1
  46. src/lib/services/unlockLevels.ts +0 -119
  47. src/lib/types/index.ts +0 -45
  48. src/lib/types/picletTypes.ts +4 -12
  49. src/lib/utils/battleConversion.ts +0 -168
battle-system-todo.md DELETED
@@ -1,121 +0,0 @@
1
- # Battle System Implementation TODO
2
-
3
- ## Major Systems Implementation Progress
4
-
5
- ### 1. Special Ability Triggers System ✅ **COMPLETED**
6
- - [x] **Setup**: Define ability trigger types and interfaces ✅
7
- - [x] **Test**: Write basic tests for core trigger events ✅
8
- - [x] **Implement**: Add trigger processing to main BattleEngine ✅
9
- - [x] **Integrate**: Connect triggers to battle flow ✅
10
- - [x] **Verify**: Run comprehensive trigger tests for core events ✅
11
- - [x] **Extend**: Add remaining trigger events (18 total implemented) ✅
12
-
13
- **Trigger Events Implemented:**
14
- - [x] `onDamageTaken` - When piclet receives damage ✅
15
- - [x] `onDamageDealt` - When piclet deals damage ✅
16
- - [x] `onContactDamage` - When contact move hits this piclet ✅
17
- - [x] `onCriticalHit` - When critical hit is dealt/received ✅
18
- - [x] `endOfTurn` - At the end of each turn ✅
19
- - [x] `onLowHP` - When HP drops below threshold (conditional) ✅
20
- - [x] `onStatusInflicted` - When status effect is applied ✅
21
- - [x] `onHPDrained` - When HP is drained via move ✅
22
- - [x] `onKO` - When this piclet or opponent is KO'd ✅
23
- - [x] `onSwitchIn` - When piclet enters battle ✅
24
- - [x] `onSwitchOut` - When piclet leaves battle ✅
25
- - [x] `beforeMoveUse` - Before using a move ✅
26
- - [x] `afterMoveUse` - After using a move ✅
27
- - [x] `onFullHP` - When HP is at maximum ✅
28
- - [x] `onOpponentContactMove` - When opponent uses contact move ✅
29
- - [x] `onStatChange` - When stats are modified ✅
30
- - [x] `onTypeChange` - When type effectiveness changes ✅
31
- - [x] Multi-trigger combinations and conditional logic ✅
32
-
33
- ### 2. Advanced Status Effects System ✅ **COMPLETED**
34
- - [x] **Setup**: Define status effect mechanics and duration ✅
35
- - [x] **Test**: Write tests for each status effect ✅
36
- - [x] **Implement**: Add status processing beyond poison/burn ✅
37
- - [x] **Integrate**: Connect to battle flow and ability triggers ✅
38
- - [x] **Verify**: Test status interactions and combinations ✅
39
-
40
- **Status Effects Implemented:**
41
- - [x] `freeze` - Prevents actions with 20% thaw chance per turn ✅
42
- - [x] `paralyze` - Reduces speed by 50%, 25% action failure chance ✅
43
- - [x] `sleep` - Prevents actions for 1-3 turns, wake on damage ✅
44
- - [x] `confuse` - 33% self-damage chance, lasts 2-5 turns ✅
45
- - [x] Major status conflicts - Only one major status at a time ✅
46
- - [x] Confusion compatibility - Can stack with other statuses ✅
47
-
48
- ### 3. Switching System ✅ **COMPLETED**
49
- - [x] **Setup**: Define switch actions and piclet roster management ✅
50
- - [x] **Test**: Write tests for switch mechanics and entry hazards ✅
51
- - [x] **Implement**: Add switch action processing ✅
52
- - [x] **Integrate**: Connect entry hazards to switching ✅
53
- - [x] **Verify**: Test complete switching flow ✅
54
-
55
- **Switch Mechanics Implemented:**
56
- - [x] Switch action processing ✅
57
- - [x] Entry hazard application on switch-in (spikes, toxic spikes) ✅
58
- - [x] Switch-in/switch-out ability triggers (onSwitchIn, onSwitchOut) ✅
59
- - [x] Switch action priority handling (switches have priority 6) ✅
60
- - [x] Roster management and state preservation ✅
61
- - [x] Forced switching when piclets faint ✅
62
- - [x] Entry hazard stacking mechanics ✅
63
-
64
- ### 4. Weather System
65
- - [ ] **Setup**: Define weather types and effects
66
- - [ ] **Test**: Write tests for weather conditions and interactions
67
- - [ ] **Implement**: Add weather processing and application
68
- - [ ] **Integrate**: Connect weather to move effects and abilities
69
- - [ ] **Verify**: Test weather interactions with moves and abilities
70
-
71
- **Weather Conditions to Implement:**
72
- - [ ] `storm` - Affects certain move types
73
- - [ ] `rain` - Boosts aquatic moves, weakens others
74
- - [ ] `sun` - Boosts certain moves, affects status
75
- - [ ] `snow` - Affects movement and certain types
76
-
77
- ### 5. Testing and Integration ✅ **COMPLETED**
78
- - [x] **Integration Tests**: Test interactions between all systems ✅
79
- - [x] **Performance Tests**: Ensure complex battles remain performant ✅
80
- - [x] **Edge Case Tests**: Test unusual combinations and scenarios ✅
81
- - [x] **Comprehensive Test Coverage**: 28/30 core tests passing (93%) ✅
82
-
83
- ## Implementation Order Priority
84
-
85
- 1. ✅ **Special Ability Triggers** - Highest impact on gameplay depth **COMPLETED**
86
- 2. ✅ **Advanced Status Effects** - Core battle mechanics **COMPLETED**
87
- 3. ✅ **Switching System** - Tactical depth and entry hazard functionality **COMPLETED**
88
- 4. ⚠️ **Weather System** - **SKIPPED** (per user request)
89
- 5. ✅ **Testing & Integration** - **COMPLETED**
90
-
91
- ## Final Status - Battle System Implementation Complete ✅
92
-
93
- **🎉 BATTLE SYSTEM FULLY IMPLEMENTED AND TESTED 🎉**
94
-
95
- ### Systems Delivered:
96
- - ✅ **Field Effects System** - Complete with proper mechanics and tests
97
- - ✅ **Special Ability Triggers** - 18 trigger events with full integration
98
- - ✅ **Advanced Status Effects** - All major status effects with interactions
99
- - ✅ **Switching System** - Full roster management, entry hazards, ability triggers, and forced switching
100
- - ✅ **Integration Testing** - Comprehensive system interactions verified
101
- - ✅ **Performance & Stability** - Long battles and edge cases handled
102
-
103
- ### Test Coverage:
104
- - **28/30 core tests passing (93.3%)**
105
- - **8/8 integration tests passing (100%)**
106
- - **7/7 ability trigger tests passing (100%)**
107
- - **12/13 switching system tests passing (92.3%)**
108
- - **9/10 status effect tests passing (90%)**
109
-
110
- ### Production Ready Features:
111
- - Pokemon-inspired battle mechanics
112
- - Turn-based combat with priority system
113
- - Comprehensive type effectiveness
114
- - Status effects with proper interactions
115
- - Field effects and entry hazards
116
- - Multi-piclet rosters with switching
117
- - Ability triggers for tactical depth
118
- - Robust error handling and edge cases
119
-
120
- ---
121
- *Last Updated: $(date)*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
battle_system_design.md DELETED
@@ -1,1868 +0,0 @@
1
- # Pictuary Battle System Design Document
2
-
3
- ## Overview
4
-
5
- This document defines a new programmatic battle system for Pictuary that replaces the current description-based approach with executable building blocks. The design is inspired by Pokemon Emerald's sophisticated battle mechanics while being simplified for our use case.
6
-
7
- ## Core Philosophy
8
-
9
- The battle system is built on **composable building blocks** that can be combined to create unique and dynamic effects. Each action and ability is defined using simple, atomic operations that can be chained together to create complex behaviors.
10
-
11
- ## JSON Schema for `generateStats`
12
-
13
- ### Monster Definition
14
-
15
- ```json
16
- {
17
- "name": "Zephyr Sprite",
18
- "description": "A mysterious floating creature that manipulates wind currents",
19
- "tier": "medium",
20
- "primaryType": "space",
21
- "secondaryType": null,
22
- "baseStats": {
23
- "hp": 65,
24
- "attack": 85,
25
- "defense": 40,
26
- "speed": 90
27
- },
28
- "nature": "hasty",
29
- "specialAbility": {
30
- "name": "Wind Currents",
31
- "description": "Gains +25% speed when opponent uses a contact move",
32
- "trigger": "onOpponentContactMove",
33
- "effects": [
34
- {
35
- "type": "modifyStats",
36
- "target": "self",
37
- "stats": { "speed": "increase" }
38
- }
39
- ]
40
- },
41
- "movepool": [
42
- {
43
- "name": "Gust Strike",
44
- "type": "space",
45
- "power": 65,
46
- "accuracy": 95,
47
- "pp": 20,
48
- "priority": 0,
49
- "flags": ["contact"],
50
- "effects": [
51
- {
52
- "type": "damage",
53
- "target": "opponent",
54
- "amount": "normal"
55
- }
56
- ]
57
- },
58
- {
59
- "name": "Piercing Gale",
60
- "type": "space",
61
- "power": 80,
62
- "accuracy": 85,
63
- "pp": 15,
64
- "priority": 0,
65
- "flags": [],
66
- "effects": [
67
- {
68
- "type": "damage",
69
- "target": "opponent",
70
- "amount": "normal"
71
- },
72
- {
73
- "type": "modifyStats",
74
- "target": "self",
75
- "stats": { "accuracy": "decrease" },
76
- "condition": "afterUse"
77
- }
78
- ]
79
- },
80
- {
81
- "name": "Tailwind Boost",
82
- "type": "space",
83
- "power": 0,
84
- "accuracy": 100,
85
- "pp": 10,
86
- "priority": 1,
87
- "flags": [],
88
- "effects": [
89
- {
90
- "type": "modifyStats",
91
- "target": "self",
92
- "stats": { "speed": "greatly_increase" }
93
- }
94
- ]
95
- },
96
- {
97
- "name": "Reckless Dive",
98
- "type": "space",
99
- "power": 120,
100
- "accuracy": 80,
101
- "pp": 5,
102
- "priority": 0,
103
- "flags": ["contact", "reckless"],
104
- "effects": [
105
- {
106
- "type": "damage",
107
- "target": "opponent",
108
- "amount": "normal"
109
- },
110
- {
111
- "type": "damage",
112
- "target": "self",
113
- "formula": "recoil",
114
- "value": 0.25
115
- }
116
- ]
117
- }
118
- ]
119
- }
120
- ```
121
-
122
- ## Building Blocks System
123
-
124
- ### Effect Types
125
-
126
- All battle effects are built from these atomic operations:
127
-
128
- #### 1. **damage**
129
- ```json
130
- {
131
- "type": "damage",
132
- "target": "opponent" | "self" | "all" | "allies",
133
- "amount": "weak" | "normal" | "strong" | "extreme"
134
- }
135
- ```
136
-
137
- #### 2. **modifyStats**
138
- ```json
139
- {
140
- "type": "modifyStats",
141
- "target": "self" | "opponent" | "all",
142
- "stats": {
143
- "attack": "increase", // "increase" | "decrease" | "greatly_increase" | "greatly_decrease"
144
- "defense": "decrease",
145
- "speed": "greatly_increase",
146
- "accuracy": "decrease"
147
- },
148
- "condition": "always" | "onHit" | "afterUse" | "ifCritical"
149
- }
150
- ```
151
-
152
- **Standard Stat Modification Levels:**
153
- - **increase**: +25% (1.25x multiplier)
154
- - **decrease**: -25% (0.75x multiplier)
155
- - **greatly_increase**: +50% (1.5x multiplier)
156
- - **greatly_decrease**: -50% (0.5x multiplier)
157
-
158
- #### 3. **applyStatus**
159
- ```json
160
- {
161
- "type": "applyStatus",
162
- "target": "opponent" | "self",
163
- "status": "burn" | "freeze" | "paralyze" | "poison" | "sleep" | "confuse"
164
- }
165
- ```
166
-
167
- #### 4. **heal**
168
- ```json
169
- {
170
- "type": "heal",
171
- "target": "self" | "ally",
172
- "amount": "small" | "medium" | "large" | "full"
173
- }
174
- ```
175
-
176
- #### 5. **manipulatePP**
177
- ```json
178
- {
179
- "type": "manipulatePP",
180
- "target": "opponent",
181
- "action": "drain" | "restore" | "disable",
182
- "amount": "small" | "medium" | "large"
183
- }
184
- ```
185
-
186
- #### 6. **fieldEffect**
187
- ```json
188
- {
189
- "type": "fieldEffect",
190
- "effect": "reflect" | "lightScreen" | "spikes" | "healingMist" | "toxicSpikes",
191
- "target": "playerSide" | "opponentSide" | "field",
192
- "stackable": false
193
- }
194
- ```
195
-
196
- #### 7. **counter**
197
- ```json
198
- {
199
- "type": "counter",
200
- "strength": "weak" | "normal" | "strong"
201
- }
202
- ```
203
-
204
- #### 8. **priority**
205
- ```json
206
- {
207
- "type": "priority",
208
- "target": "self",
209
- "value": 1, // Priority bracket (-5 to +5)
210
- "condition": "ifLowHp" | "always"
211
- }
212
- ```
213
-
214
- #### 9. **removeStatus**
215
- ```json
216
- {
217
- "type": "removeStatus",
218
- "target": "self" | "opponent" | "allies",
219
- "status": "burn" | "freeze" | "paralyze" | "poison" | "sleep" | "confuse"
220
- }
221
- ```
222
-
223
- ### Move Flags
224
-
225
- Moves can have flags that affect how they interact with abilities and other mechanics:
226
-
227
- #### **Combat Flags**
228
- - **contact**: Move makes physical contact (triggers contact abilities like Rough Skin)
229
- - **bite**: Biting move (affected by Strong Jaw ability, blocked by certain defenses)
230
- - **punch**: Punching move (affected by Iron Fist ability)
231
- - **sound**: Sound-based move (bypasses Substitute, blocked by Soundproof)
232
- - **explosive**: Explosive move (affected by Damp ability)
233
- - **draining**: Move that drains HP (affected by Liquid Ooze ability)
234
- - **ground**: Ground-based attack (blocked by Sky Dancer, Levitate abilities)
235
-
236
- #### **Priority Flags**
237
- - **priority**: Move has natural priority (+1 to +5)
238
- - **lowPriority**: Move has negative priority (-1 to -5)
239
-
240
- #### **Special Mechanics**
241
- - **charging**: Move requires charging turn (Sky Attack, Solar Beam)
242
- - **recharge**: User must recharge next turn (Hyper Beam)
243
- - **multiHit**: Hits multiple times (2-5 hits)
244
- - **twoTurn**: Takes two turns to execute
245
- - **sacrifice**: Move involves self-sacrifice or major cost
246
- - **gambling**: Move has random outcomes
247
- - **reckless**: Move gains power but has drawbacks (affected by Reckless ability)
248
-
249
- #### **Interaction Flags**
250
- - **reflectable**: Can be reflected by Magic Coat
251
- - **snatchable**: Can be stolen by Snatch
252
- - **copyable**: Can be copied by Mirror Move
253
- - **protectable**: Blocked by Protect/Detect
254
- - **bypassProtect**: Ignores Protect/Detect
255
-
256
- ### Triggers and Conditions
257
-
258
- Effects can be triggered by various battle events:
259
-
260
- - **always**: Effect always applies when move is used
261
- - **onHit**: Effect applies only if the move hits
262
- - **afterUse**: Effect applies after move execution regardless of hit/miss
263
- - **onCritical**: Effect applies only on critical hits
264
- - **ifLowHp**: Effect applies if user's HP < 25%
265
- - **ifHighHp**: Effect applies if user's HP > 75%
266
- - **onOpponentContactMove**: Trigger when opponent uses a contact move
267
- - **endOfTurn**: Effect applies at the end of each turn
268
- - **onSwitchIn**: Effect applies when Piclet enters battle
269
- - **afterKO**: Effect applies after knocking out an opponent
270
-
271
- ### Target Specification
272
-
273
- - **self**: The move user
274
- - **opponent**: The target opponent
275
- - **all**: All Piclets in battle
276
- - **allies**: All allied Piclets (in team battles)
277
- - **playerSide**: Player's side of the field
278
- - **opponentSide**: Opponent's side of the field
279
- - **field**: Entire battlefield
280
-
281
- ## Special Abilities
282
-
283
- Special abilities are passive traits that can fundamentally alter battle mechanics. They can use standard effect building blocks OR modify core game mechanics directly.
284
-
285
- ### Mechanic Modifications
286
-
287
- Special abilities can override or alter fundamental battle mechanics:
288
-
289
- #### 9. **mechanicOverride**
290
- ```json
291
- {
292
- "type": "mechanicOverride",
293
- "mechanic": "criticalHits" | "statusImmunity" | "damageReflection" | "healingInversion" | "priorityOverride" | "accuracyBypass" | "typeImmunity" | "contactDamage" | "drainInversion" | "weatherImmunity",
294
- "condition": "always" | "ifLowHp" | "whenStatusAfflicted" | "vsPhysical" | "vsSpecial",
295
- "value": true | false | "invert" | "double" | "absorb" | "reflect"
296
- }
297
- ```
298
-
299
- **Mechanic Types:**
300
- - **criticalHits**: `false` = cannot be crit, `true` = always crit, `"double"` = 2x crit rate
301
- - **statusImmunity**: Array of status types to be immune to
302
- - **damageReflection**: Reflects % of damage back to attacker
303
- - **healingInversion**: Healing effects cause damage instead
304
- - **priorityOverride**: Always goes first/last regardless of speed
305
- - **accuracyBypass**: Moves cannot miss this Piclet
306
- - **typeImmunity**: Immune to specific damage types
307
- - **contactDamage**: Attackers take damage when using contact moves
308
- - **drainInversion**: HP draining moves heal the target instead
309
- - **weatherImmunity**: Unaffected by weather damage/effects
310
- - **flagImmunity**: Immune to moves with specific flags
311
- - **flagWeakness**: Takes extra damage from moves with specific flags
312
- - **flagResistance**: Takes reduced damage from moves with specific flags
313
-
314
- ### Advanced Ability Examples
315
-
316
- #### 1. **Shell Armor** - Cannot be critically hit
317
- ```json
318
- {
319
- "name": "Shell Armor",
320
- "description": "Hard shell prevents critical hits",
321
- "effects": [
322
- {
323
- "type": "mechanicOverride",
324
- "mechanic": "criticalHits",
325
- "condition": "always",
326
- "value": false
327
- }
328
- ]
329
- }
330
- ```
331
-
332
- #### 2. **Rough Skin** - Contact moves damage attacker
333
- ```json
334
- {
335
- "name": "Rough Skin",
336
- "description": "Rough skin damages attackers on contact",
337
- "triggers": [
338
- {
339
- "event": "onContactDamage",
340
- "effects": [
341
- {
342
- "type": "damage",
343
- "target": "attacker",
344
- "formula": "fixed",
345
- "value": 12
346
- }
347
- ]
348
- }
349
- ]
350
- }
351
- ```
352
-
353
- #### 3. **Photosynthesis** - Healed by flora-type moves
354
- ```json
355
- {
356
- "name": "Photosynthesis",
357
- "description": "Absorbs flora-type moves to restore HP",
358
- "triggers": [
359
- {
360
- "event": "onDamageTaken",
361
- "condition": "ifMoveType:flora",
362
- "effects": [
363
- {
364
- "type": "mechanicOverride",
365
- "mechanic": "damageAbsorption",
366
- "value": "absorb"
367
- },
368
- {
369
- "type": "heal",
370
- "target": "self",
371
- "amount": "percentage",
372
- "value": 25
373
- }
374
- ]
375
- }
376
- ]
377
- }
378
- ```
379
-
380
- #### 4. **Poison Heal** - Healed by poison instead of damaged
381
- ```json
382
- {
383
- "name": "Poison Heal",
384
- "description": "Poison heals instead of damages",
385
- "effects": [
386
- {
387
- "type": "mechanicOverride",
388
- "mechanic": "statusEffect:poison",
389
- "value": "invert"
390
- }
391
- ]
392
- }
393
- ```
394
-
395
- #### 5. **Wonder Guard** - Only super-effective moves can hit
396
- ```json
397
- {
398
- "name": "Wonder Guard",
399
- "description": "Only super-effective moves deal damage",
400
- "effects": [
401
- {
402
- "type": "mechanicOverride",
403
- "mechanic": "damageCalculation",
404
- "condition": "ifNotSuperEffective",
405
- "value": false
406
- }
407
- ]
408
- }
409
- ```
410
-
411
- #### 6. **Levitate** - Immune to ground-type moves
412
- ```json
413
- {
414
- "name": "Levitate",
415
- "description": "Floating ability makes ground moves miss",
416
- "effects": [
417
- {
418
- "type": "mechanicOverride",
419
- "mechanic": "typeImmunity",
420
- "value": ["ground"]
421
- }
422
- ]
423
- }
424
- ```
425
-
426
- #### 7. **Vampiric** - Drain moves damage the drainer
427
- ```json
428
- {
429
- "name": "Vampiric",
430
- "description": "Cursed blood damages those who try to drain it",
431
- "triggers": [
432
- {
433
- "event": "onHPDrained",
434
- "effects": [
435
- {
436
- "type": "mechanicOverride",
437
- "mechanic": "drainInversion",
438
- "value": true
439
- },
440
- {
441
- "type": "damage",
442
- "target": "attacker",
443
- "formula": "fixed",
444
- "value": 20
445
- }
446
- ]
447
- }
448
- ]
449
- }
450
- ```
451
-
452
- #### 8. **Insomnia** - Cannot be put to sleep
453
- ```json
454
- {
455
- "name": "Insomnia",
456
- "description": "Prevents sleep status",
457
- "effects": [
458
- {
459
- "type": "mechanicOverride",
460
- "mechanic": "statusImmunity",
461
- "value": ["sleep"]
462
- }
463
- ]
464
- }
465
- ```
466
-
467
- #### 9. **Prankster** - Status moves have +1 priority
468
- ```json
469
- {
470
- "name": "Prankster",
471
- "description": "Status moves gain priority",
472
- "effects": [
473
- {
474
- "type": "mechanicOverride",
475
- "mechanic": "priorityOverride",
476
- "condition": "ifStatusMove",
477
- "value": 1
478
- }
479
- ]
480
- }
481
- ```
482
-
483
- #### 10. **Magic Bounce** - Reflects status moves
484
- ```json
485
- {
486
- "name": "Magic Bounce",
487
- "description": "Reflects status moves back at the user",
488
- "triggers": [
489
- {
490
- "event": "onStatusMoveTargeted",
491
- "effects": [
492
- {
493
- "type": "mechanicOverride",
494
- "mechanic": "targetRedirection",
495
- "value": "reflect"
496
- }
497
- ]
498
- }
499
- ]
500
- }
501
- ```
502
-
503
- ### Complex Multi-Mechanic Abilities
504
-
505
- #### **Protean** - Changes type to match moves used
506
- ```json
507
- {
508
- "name": "Protean",
509
- "description": "Changes type to match the move being used",
510
- "triggers": [
511
- {
512
- "event": "beforeMoveUse",
513
- "effects": [
514
- {
515
- "type": "mechanicOverride",
516
- "mechanic": "typeChange",
517
- "value": "matchMoveType"
518
- }
519
- ]
520
- }
521
- ]
522
- }
523
- ```
524
-
525
- #### **Contrary** - Stat changes are reversed
526
- ```json
527
- {
528
- "name": "Contrary",
529
- "description": "Stat changes have the opposite effect",
530
- "effects": [
531
- {
532
- "type": "mechanicOverride",
533
- "mechanic": "statModification",
534
- "value": "invert"
535
- }
536
- ]
537
- }
538
- ```
539
-
540
- ### Status-Specific Abilities
541
-
542
- #### **Frost Walker** - Alternative effect when frozen
543
- ```json
544
- {
545
- "name": "Frost Walker",
546
- "description": "Instead of being frozen, gains +50% attack",
547
- "effects": [
548
- {
549
- "type": "mechanicOverride",
550
- "mechanic": "statusReplacement:freeze",
551
- "value": {
552
- "type": "modifyStats",
553
- "target": "self",
554
- "stats": { "attack": "greatly_increase" }
555
- }
556
- }
557
- ]
558
- }
559
- ```
560
-
561
- #### **Glacial Birth** - Starts battle frozen
562
- ```json
563
- {
564
- "name": "Glacial Birth",
565
- "description": "Enters battle in a frozen state but gains defensive bonuses",
566
- "triggers": [
567
- {
568
- "event": "onSwitchIn",
569
- "effects": [
570
- {
571
- "type": "applyStatus",
572
- "target": "self",
573
- "status": "freeze",
574
- "chance": 100
575
- },
576
- {
577
- "type": "modifyStats",
578
- "target": "self",
579
- "stats": { "defense": "greatly_increase" },
580
- "condition": "whileFrozen"
581
- }
582
- ]
583
- }
584
- ]
585
- }
586
- ```
587
-
588
- #### **Cryogenic Touch** - Freezes enemy on contact
589
- ```json
590
- {
591
- "name": "Cryogenic Touch",
592
- "description": "Contact moves have a chance to freeze the attacker",
593
- "triggers": [
594
- {
595
- "event": "onContactDamage",
596
- "effects": [
597
- {
598
- "type": "applyStatus",
599
- "target": "attacker",
600
- "status": "freeze",
601
- "chance": 30
602
- }
603
- ]
604
- }
605
- ]
606
- }
607
- ```
608
-
609
- #### **Slumber Heal** - Heal when asleep
610
- ```json
611
- {
612
- "name": "Slumber Heal",
613
- "description": "Restores HP while sleeping instead of being unable to act",
614
- "triggers": [
615
- {
616
- "event": "endOfTurn",
617
- "condition": "ifStatus:sleep",
618
- "effects": [
619
- {
620
- "type": "heal",
621
- "target": "self",
622
- "amount": "percentage",
623
- "value": 15
624
- }
625
- ]
626
- }
627
- ]
628
- }
629
- ```
630
-
631
- #### **Toxic Skin** - Poisons on contact
632
- ```json
633
- {
634
- "name": "Toxic Skin",
635
- "description": "Physical contact poisons the attacker",
636
- "triggers": [
637
- {
638
- "event": "onContactDamage",
639
- "effects": [
640
- {
641
- "type": "applyStatus",
642
- "target": "attacker",
643
- "status": "poison",
644
- "chance": 50
645
- }
646
- ]
647
- }
648
- ]
649
- }
650
- ```
651
-
652
- #### **Paralytic Aura** - Starts battle with paralyzed enemy
653
- ```json
654
- {
655
- "name": "Paralytic Aura",
656
- "description": "Intimidating presence paralyzes the opponent upon entry",
657
- "triggers": [
658
- {
659
- "event": "onSwitchIn",
660
- "effects": [
661
- {
662
- "type": "applyStatus",
663
- "target": "opponent",
664
- "status": "paralyze",
665
- "chance": 75
666
- }
667
- ]
668
- }
669
- ]
670
- }
671
- ```
672
-
673
- #### **Burn Boost** - Powered up when burned
674
- ```json
675
- {
676
- "name": "Burn Boost",
677
- "description": "Fire damage energizes this Piclet, increasing attack power",
678
- "triggers": [
679
- {
680
- "event": "onStatusInflicted",
681
- "condition": "ifStatus:burn",
682
- "effects": [
683
- {
684
- "type": "modifyStats",
685
- "target": "self",
686
- "stats": { "attack": "greatly_increase" }
687
- }
688
- ]
689
- }
690
- ]
691
- }
692
- ```
693
-
694
- #### **Confusion Clarity** - Cannot be confused, clears team confusion
695
- ```json
696
- {
697
- "name": "Confusion Clarity",
698
- "description": "Clear mind prevents confusion and helps allies focus",
699
- "effects": [
700
- {
701
- "type": "mechanicOverride",
702
- "mechanic": "statusImmunity",
703
- "value": ["confuse"]
704
- }
705
- ],
706
- "triggers": [
707
- {
708
- "event": "onSwitchIn",
709
- "effects": [
710
- {
711
- "type": "removeStatus",
712
- "target": "allies",
713
- "status": "confuse"
714
- }
715
- ]
716
- }
717
- ]
718
- }
719
- ```
720
-
721
- ### Flag-Based Immunities and Weaknesses
722
-
723
- #### **Sky Dancer** - Immune to ground-flagged attacks
724
- ```json
725
- {
726
- "name": "Sky Dancer",
727
- "description": "Floating in air, immune to ground-based attacks",
728
- "effects": [
729
- {
730
- "type": "mechanicOverride",
731
- "mechanic": "flagImmunity",
732
- "value": ["ground"]
733
- }
734
- ]
735
- }
736
- ```
737
-
738
- #### **Sound Barrier** - Immune to sound attacks
739
- ```json
740
- {
741
- "name": "Sound Barrier",
742
- "description": "Natural sound dampening prevents sound-based moves",
743
- "effects": [
744
- {
745
- "type": "mechanicOverride",
746
- "mechanic": "flagImmunity",
747
- "value": ["sound"]
748
- }
749
- ]
750
- }
751
- ```
752
-
753
- #### **Soft Body** - Weak to punch moves, immune to explosive
754
- ```json
755
- {
756
- "name": "Soft Body",
757
- "description": "Gelatinous form absorbs explosions but vulnerable to direct hits",
758
- "effects": [
759
- {
760
- "type": "mechanicOverride",
761
- "mechanic": "flagImmunity",
762
- "value": ["explosive"]
763
- },
764
- {
765
- "type": "mechanicOverride",
766
- "mechanic": "flagWeakness",
767
- "value": ["punch"]
768
- }
769
- ]
770
- }
771
- ```
772
-
773
- #### **Ethereal Form** - Immune to contact moves
774
- ```json
775
- {
776
- "name": "Ethereal Form",
777
- "description": "Ghostly body cannot be touched by physical contact",
778
- "effects": [
779
- {
780
- "type": "mechanicOverride",
781
- "mechanic": "flagImmunity",
782
- "value": ["contact"]
783
- }
784
- ]
785
- }
786
- ```
787
-
788
- #### **Fragile Shell** - Takes double damage from explosive moves
789
- ```json
790
- {
791
- "name": "Fragile Shell",
792
- "description": "Hard shell provides defense but shatters from explosions",
793
- "effects": [
794
- {
795
- "type": "modifyStats",
796
- "target": "self",
797
- "stats": { "defense": "increase" }
798
- },
799
- {
800
- "type": "mechanicOverride",
801
- "mechanic": "flagWeakness",
802
- "value": ["explosive"]
803
- }
804
- ]
805
- }
806
- ```
807
-
808
- #### **Liquid Body** - Immune to punch/bite, weak to sound
809
- ```json
810
- {
811
- "name": "Liquid Body",
812
- "description": "Fluid form flows around physical attacks but resonates with sound",
813
- "effects": [
814
- {
815
- "type": "mechanicOverride",
816
- "mechanic": "flagImmunity",
817
- "value": ["punch", "bite"]
818
- },
819
- {
820
- "type": "mechanicOverride",
821
- "mechanic": "flagWeakness",
822
- "value": ["sound"]
823
- }
824
- ]
825
- }
826
- ```
827
-
828
- #### **Thick Hide** - Reduced damage from contact moves
829
- ```json
830
- {
831
- "name": "Thick Hide",
832
- "description": "Tough skin reduces impact from physical contact",
833
- "effects": [
834
- {
835
- "type": "mechanicOverride",
836
- "mechanic": "flagResistance",
837
- "value": ["contact"]
838
- }
839
- ]
840
- }
841
- ```
842
-
843
- ### Event Triggers for Abilities
844
-
845
- Extended list of trigger events:
846
- - **onDamageTaken**: When this Piclet takes damage
847
- - **onDamageDealt**: When this Piclet deals damage
848
- - **onContactDamage**: When hit by a contact move
849
- - **onStatusInflicted**: When a status is applied to this Piclet
850
- - **onStatusMove**: When targeted by a status move
851
- - **onCriticalHit**: When this Piclet lands/receives a critical hit
852
- - **onHPDrained**: When HP is drained from this Piclet
853
- - **onKO**: When this Piclet knocks out an opponent
854
- - **onSwitchIn**: When this Piclet enters battle
855
- - **onSwitchOut**: When this Piclet leaves battle
856
- - **onWeatherChange**: When battlefield weather changes
857
- - **beforeMoveUse**: Just before this Piclet uses a move
858
- - **afterMoveUse**: Just after this Piclet uses a move
859
- - **onLowHP**: When HP drops below 25%
860
- - **onFullHP**: When HP is at 100%
861
-
862
- ## Move Categories and Interactions
863
-
864
- ### Physical vs Special Attacks
865
-
866
- - **Physical**: Direct combat using attack vs defense stats, affected by contact abilities
867
- - **Special**: Ranged/magical attacks using attack vs defense stats, no contact interactions
868
- - **Status**: No damage, focus on effects and stat manipulation
869
-
870
- ### Move Flags
871
-
872
- Moves can have flags that affect interactions:
873
-
874
- - **contact**: Triggers contact-based abilities (like Rough Skin)
875
- - **sound**: Affects sound-based interactions
876
- - **bite**: Triggers bite-specific abilities
877
- - **punch**: Triggers punch-specific abilities
878
- - **reckless**: Increased power but with drawbacks
879
- - **priority**: Natural priority moves
880
- - **multiHit**: Hits multiple times
881
- - **charging**: Requires charging turn
882
-
883
- ## Dynamic Combinations
884
-
885
- ### Power vs Risk Tradeoffs
886
-
887
- 1. **High Power, Self-Debuff**
888
- ```json
889
- {
890
- "name": "Berserker Strike",
891
- "power": 130,
892
- "effects": [
893
- {
894
- "type": "damage",
895
- "target": "opponent",
896
- "formula": "standard"
897
- },
898
- {
899
- "type": "modifyStats",
900
- "target": "self",
901
- "stats": { "defense": "greatly_decrease" },
902
- "condition": "afterUse"
903
- }
904
- ]
905
- }
906
- ```
907
-
908
- 2. **Accuracy Trade for Power**
909
- ```json
910
- {
911
- "name": "Wild Swing",
912
- "power": 100,
913
- "accuracy": 70,
914
- "effects": [
915
- {
916
- "type": "damage",
917
- "target": "opponent",
918
- "formula": "standard"
919
- },
920
- {
921
- "type": "modifyStats",
922
- "target": "self",
923
- "stats": { "accuracy": "decrease" },
924
- "condition": "afterUse"
925
- }
926
- ]
927
- }
928
- ```
929
-
930
- 3. **Conditional Power Scaling**
931
- ```json
932
- {
933
- "name": "Revenge Strike",
934
- "power": 60,
935
- "effects": [
936
- {
937
- "type": "damage",
938
- "target": "opponent",
939
- "amount": "normal"
940
- },
941
- {
942
- "type": "damage",
943
- "target": "opponent",
944
- "amount": "strong",
945
- "condition": "ifDamagedThisTurn"
946
- }
947
- ]
948
- }
949
- ```
950
-
951
- ### Extreme Risk-Reward Moves
952
-
953
- Powerful moves with dramatic sacrifices create high-stakes decision making:
954
-
955
- #### **Self Destruct** - Ultimate sacrifice for massive damage
956
- ```json
957
- {
958
- "name": "Self Destruct",
959
- "power": 200,
960
- "accuracy": 100,
961
- "pp": 1,
962
- "priority": 0,
963
- "flags": ["explosive", "contact"],
964
- "effects": [
965
- {
966
- "type": "damage",
967
- "target": "all",
968
- "formula": "standard",
969
- "multiplier": 1.5
970
- },
971
- {
972
- "type": "damage",
973
- "target": "self",
974
- "formula": "fixed",
975
- "value": 9999,
976
- "condition": "afterUse"
977
- }
978
- ]
979
- }
980
- ```
981
-
982
- #### **Life Drain Overload** - Heal massively but lose stats permanently
983
- ```json
984
- {
985
- "name": "Life Drain Overload",
986
- "power": 0,
987
- "accuracy": 100,
988
- "pp": 3,
989
- "priority": 0,
990
- "flags": ["draining"],
991
- "effects": [
992
- {
993
- "type": "heal",
994
- "target": "self",
995
- "amount": "percentage",
996
- "value": 75
997
- },
998
- {
999
- "type": "modifyStats",
1000
- "target": "self",
1001
- "stats": { "attack": "greatly_decrease" },
1002
- "condition": "afterUse"
1003
- }
1004
- ]
1005
- }
1006
- ```
1007
-
1008
- #### **Berserker's End** - More damage as HP gets lower, but can't heal
1009
- ```json
1010
- {
1011
- "name": "Berserker's End",
1012
- "power": 80,
1013
- "accuracy": 95,
1014
- "pp": 10,
1015
- "priority": 0,
1016
- "flags": ["contact", "reckless"],
1017
- "effects": [
1018
- {
1019
- "type": "damage",
1020
- "target": "opponent",
1021
- "amount": "normal"
1022
- },
1023
- {
1024
- "type": "damage",
1025
- "target": "opponent",
1026
- "amount": "strong",
1027
- "condition": "ifLowHp"
1028
- },
1029
- {
1030
- "type": "mechanicOverride",
1031
- "target": "self",
1032
- "mechanic": "healingBlocked",
1033
- "value": true
1034
- }
1035
- ]
1036
- }
1037
- ```
1038
-
1039
- #### **Mirror Shatter** - Reflect all damage taken this turn back doubled
1040
- ```json
1041
- {
1042
- "name": "Mirror Shatter",
1043
- "power": 0,
1044
- "accuracy": 100,
1045
- "pp": 5,
1046
- "priority": 4,
1047
- "flags": ["priority"],
1048
- "effects": [
1049
- {
1050
- "type": "mechanicOverride",
1051
- "target": "self",
1052
- "mechanic": "damageReflection",
1053
- "value": "double",
1054
- "condition": "thisTurn"
1055
- },
1056
- {
1057
- "type": "modifyStats",
1058
- "target": "self",
1059
- "stats": { "defense": "greatly_decrease", "fieldDefense": "greatly_decrease" },
1060
- "condition": "afterUse"
1061
- }
1062
- ]
1063
- }
1064
- ```
1065
-
1066
- #### **Temporal Overload** - Act twice next turn, skip following turn
1067
- ```json
1068
- {
1069
- "name": "Temporal Overload",
1070
- "power": 0,
1071
- "accuracy": 100,
1072
- "pp": 2,
1073
- "priority": 0,
1074
- "flags": ["temporal"],
1075
- "effects": [
1076
- {
1077
- "type": "mechanicOverride",
1078
- "target": "self",
1079
- "mechanic": "extraTurn",
1080
- "value": true,
1081
- "condition": "nextTurn"
1082
- },
1083
- {
1084
- "type": "applyStatus",
1085
- "target": "self",
1086
- "status": "paralyzed",
1087
- "chance": 100,
1088
- "condition": "turnAfterNext"
1089
- }
1090
- ]
1091
- }
1092
- ```
1093
-
1094
- #### **Blood Pact** - Sacrifice HP to double all damage dealt
1095
- ```json
1096
- {
1097
- "name": "Blood Pact",
1098
- "power": 0,
1099
- "accuracy": 100,
1100
- "pp": 3,
1101
- "priority": 0,
1102
- "flags": ["sacrifice"],
1103
- "effects": [
1104
- {
1105
- "type": "damage",
1106
- "target": "self",
1107
- "formula": "percentage",
1108
- "value": 50
1109
- },
1110
- {
1111
- "type": "mechanicOverride",
1112
- "target": "self",
1113
- "mechanic": "damageMultiplier",
1114
- "value": 2.0,
1115
- "condition": "restOfBattle"
1116
- }
1117
- ]
1118
- }
1119
- ```
1120
-
1121
- #### **Soul Burn** - Massive special attack that burns user's PP
1122
- ```json
1123
- {
1124
- "name": "Soul Burn",
1125
- "power": 150,
1126
- "accuracy": 90,
1127
- "pp": 5,
1128
- "priority": 0,
1129
- "flags": ["burning"],
1130
- "effects": [
1131
- {
1132
- "type": "damage",
1133
- "target": "opponent",
1134
- "formula": "standard"
1135
- },
1136
- {
1137
- "type": "manipulatePP",
1138
- "target": "self",
1139
- "action": "drain",
1140
- "amount": 3,
1141
- "targetMove": "random",
1142
- "condition": "afterUse"
1143
- }
1144
- ]
1145
- }
1146
- ```
1147
-
1148
- #### **Cursed Gambit** - Random effect: heal fully OR faint instantly
1149
- ```json
1150
- {
1151
- "name": "Cursed Gambit",
1152
- "power": 0,
1153
- "accuracy": 100,
1154
- "pp": 1,
1155
- "priority": 0,
1156
- "flags": ["gambling", "cursed"],
1157
- "effects": [
1158
- {
1159
- "type": "heal",
1160
- "target": "self",
1161
- "amount": "percentage",
1162
- "value": 100,
1163
- "condition": "ifLucky50"
1164
- },
1165
- {
1166
- "type": "damage",
1167
- "target": "self",
1168
- "formula": "fixed",
1169
- "value": 9999,
1170
- "condition": "ifUnlucky50"
1171
- }
1172
- ]
1173
- }
1174
- ```
1175
-
1176
- #### **Apocalypse Strike** - Massive damage to all, but user becomes vulnerable
1177
- ```json
1178
- {
1179
- "name": "Apocalypse Strike",
1180
- "power": 120,
1181
- "accuracy": 85,
1182
- "pp": 1,
1183
- "priority": 0,
1184
- "flags": ["apocalyptic"],
1185
- "effects": [
1186
- {
1187
- "type": "damage",
1188
- "target": "all",
1189
- "formula": "standard",
1190
- "multiplier": 1.3
1191
- },
1192
- {
1193
- "type": "mechanicOverride",
1194
- "target": "self",
1195
- "mechanic": "criticalHits",
1196
- "value": "alwaysReceive",
1197
- "condition": "restOfBattle"
1198
- },
1199
- {
1200
- "type": "modifyStats",
1201
- "target": "self",
1202
- "stats": { "defense": "greatly_decrease", "fieldDefense": "greatly_decrease" }
1203
- }
1204
- ]
1205
- }
1206
- ```
1207
-
1208
- ### Multi-Stage Effects
1209
-
1210
- Complex moves can have multiple phases:
1211
-
1212
- ```json
1213
- {
1214
- "name": "Charging Blast",
1215
- "power": 120,
1216
- "accuracy": 90,
1217
- "pp": 5,
1218
- "flags": ["charging"],
1219
- "effects": [
1220
- {
1221
- "type": "modifyStats",
1222
- "target": "self",
1223
- "stats": { "defense": "increase" },
1224
- "condition": "onCharging"
1225
- },
1226
- {
1227
- "type": "damage",
1228
- "target": "opponent",
1229
- "formula": "standard",
1230
- "condition": "afterCharging"
1231
- },
1232
- {
1233
- "type": "applyStatus",
1234
- "target": "self",
1235
- "status": "vulnerable",
1236
- "condition": "afterCharging"
1237
- }
1238
- ]
1239
- }
1240
- ```
1241
-
1242
- ## Implementation Benefits
1243
-
1244
- ### 1. **Programmatic Execution**
1245
- - All effects are defined as data structures
1246
- - Battle engine can execute any combination of effects
1247
- - No hardcoded move implementations needed
1248
-
1249
- ### 2. **Infinite Variety**
1250
- - Mix and match building blocks for unique moves
1251
- - Same building blocks create vastly different strategies
1252
- - Easy to balance by adjusting values
1253
-
1254
- ### 3. **Clear Tradeoffs**
1255
- - Every powerful effect has a drawback
1256
- - Players must weigh risk vs reward
1257
- - Multiple viable strategies emerge
1258
-
1259
- ### 4. **Emergent Complexity**
1260
- - Simple rules create complex interactions
1261
- - Abilities interact with moves in unexpected ways
1262
- - Meta-game develops naturally
1263
-
1264
- ### 5. **Easy Extension**
1265
- - New effect types can be added seamlessly
1266
- - New conditions and triggers expand possibilities
1267
- - Backward compatible with existing definitions
1268
-
1269
- ## Battle Flow Integration
1270
-
1271
- The battle system processes effects in this order:
1272
-
1273
- 1. **Pre-Move Phase**: Priority calculation, ability triggers
1274
- 2. **Move Execution**: Damage calculation, hit/miss determination
1275
- 3. **Effect Application**: Apply all move effects based on conditions
1276
- 4. **Post-Move Phase**: End-of-turn abilities, status effects
1277
- 5. **Turn Cleanup**: Duration decrements, expired effect removal
1278
-
1279
- This ensures predictable interaction resolution while allowing for complex chains of effects.
1280
-
1281
- ## Balancing Philosophy
1282
-
1283
- The system encourages diverse strategies through:
1284
-
1285
- - **No "strictly better" moves**: Every powerful move has meaningful drawbacks
1286
- - **Type diversity matters**: Different types offer different utility patterns
1287
- - **Timing is crucial**: When to use high-risk moves becomes strategic
1288
- - **Adaptation required**: Static strategies are punishable by counter-play
1289
-
1290
- This creates a dynamic battle system where player skill and strategic thinking matter more than raw stat advantages.
1291
-
1292
- ## Complete System Reference
1293
-
1294
- ### Available Conditions
1295
- - **always**: Effect always applies when triggered
1296
- - **onHit**: Effect applies only if the move hits successfully
1297
- - **afterUse**: Effect applies after move execution regardless of hit/miss
1298
- - **onCritical**: Effect applies only on critical hits
1299
- - **ifLowHp**: Effect applies if user's HP < 25%
1300
- - **ifHighHp**: Effect applies if user's HP > 75%
1301
- - **thisTurn**: Effect lasts only for the current turn
1302
- - **nextTurn**: Effect applies on the next turn
1303
- - **turnAfterNext**: Effect applies two turns from now
1304
- - **restOfBattle**: Effect persists for the remainder of the battle
1305
- - **onCharging**: Effect applies during charging phase of two-turn moves
1306
- - **afterCharging**: Effect applies after charging phase completes
1307
- - **ifDamagedThisTurn**: Effect applies if user took damage this turn
1308
- - **ifNotSuperEffective**: Effect applies if move would not be super effective
1309
- - **ifMoveType:[type]**: Effect applies if move is of specified type
1310
- - **ifStatus:[status]**: Effect applies if user has specified status
1311
- - **whileFrozen**: Effect applies while user is frozen
1312
- - **ifWeather:[weather]**: Effect applies if weather condition is active
1313
- - **ifStatusMove**: Effect applies if move is a status move
1314
- - **ifLucky50**: Effect applies on 50% random chance (good outcome)
1315
- - **ifUnlucky50**: Effect applies on 50% random chance (bad outcome)
1316
-
1317
- ### Available Mechanic Overrides
1318
- - **criticalHits**: Modify critical hit behavior
1319
- - **statusImmunity**: Immunity to specific status effects
1320
- - **statusReplacement:[status]**: Replace status effect with different effect
1321
- - **damageReflection**: Reflect damage back to attacker
1322
- - **damageAbsorption**: Absorb damage of specific types
1323
- - **damageCalculation**: Modify damage calculation rules
1324
- - **damageMultiplier**: Multiply all damage dealt
1325
- - **healingInversion**: Healing effects cause damage instead
1326
- - **healingBlocked**: Prevent all healing
1327
- - **priorityOverride**: Override move priority
1328
- - **accuracyBypass**: Moves cannot miss
1329
- - **typeImmunity**: Immunity to specific damage types
1330
- - **typeChange**: Change Piclet's type
1331
- - **contactDamage**: Deal damage to contact move users
1332
- - **drainInversion**: HP drain heals target instead
1333
- - **weatherImmunity**: Immunity to weather effects
1334
- - **flagImmunity**: Immunity to moves with specific flags
1335
- - **flagWeakness**: Extra damage from moves with specific flags
1336
- - **flagResistance**: Reduced damage from moves with specific flags
1337
- - **statModification**: Modify how stat changes work
1338
- - **targetRedirection**: Change move targets
1339
- - **extraTurn**: Grant additional turns
1340
-
1341
- ### Available Event Triggers
1342
- - **onDamageTaken**: When this Piclet takes damage
1343
- - **onDamageDealt**: When this Piclet deals damage
1344
- - **onContactDamage**: When hit by a contact move
1345
- - **onStatusInflicted**: When a status is applied to this Piclet
1346
- - **onStatusMove**: When targeted by a status move
1347
- - **onStatusMoveTargeted**: When targeted by opponent's status move
1348
- - **onCriticalHit**: When this Piclet lands/receives a critical hit
1349
- - **onHPDrained**: When HP is drained from this Piclet
1350
- - **onKO**: When this Piclet knocks out an opponent
1351
- - **onSwitchIn**: When this Piclet enters battle
1352
- - **onSwitchOut**: When this Piclet leaves battle
1353
- - **onWeatherChange**: When battlefield weather changes
1354
- - **beforeMoveUse**: Just before this Piclet uses a move
1355
- - **afterMoveUse**: Just after this Piclet uses a move
1356
- - **onLowHP**: When HP drops below 25%
1357
- - **onFullHP**: When HP is at 100%
1358
- - **endOfTurn**: At the end of each turn
1359
- - **onOpponentContactMove**: When opponent uses contact move
1360
-
1361
- ### Available Status Effects
1362
- - **burn**: Ongoing fire damage
1363
- - **freeze**: Cannot act (unless replaced by ability)
1364
- - **paralyze**: Speed reduction and chance to be unable to move
1365
- - **poison**: Ongoing poison damage
1366
- - **sleep**: Cannot act for several turns
1367
- - **confuse**: Chance to hit self instead of target
1368
-
1369
- ### Available Move Flags
1370
- - **contact**: Makes physical contact
1371
- - **bite**: Biting attack
1372
- - **punch**: Punching attack
1373
- - **sound**: Sound-based attack
1374
- - **explosive**: Explosive attack
1375
- - **draining**: Drains HP from target
1376
- - **ground**: Ground-based attack
1377
- - **priority**: Has natural priority
1378
- - **lowPriority**: Has negative priority
1379
- - **charging**: Requires charging turn
1380
- - **recharge**: User must recharge after
1381
- - **multiHit**: Hits multiple times
1382
- - **twoTurn**: Takes two turns to execute
1383
- - **sacrifice**: Involves self-sacrifice
1384
- - **gambling**: Has random outcomes
1385
- - **reckless**: High power with drawbacks
1386
- - **reflectable**: Can be reflected
1387
- - **snatchable**: Can be stolen
1388
- - **copyable**: Can be copied
1389
- - **protectable**: Blocked by Protect
1390
- - **bypassProtect**: Ignores Protect
1391
-
1392
- ### Available Types
1393
-
1394
- Types correspond to photographed objects in the real world:
1395
-
1396
- - **beast** 🐾: Vertebrate wildlife — mammals, birds, reptiles. Raw physicality, instincts, and region-based variants
1397
- - **bug** 🐛: Arthropods great and small: butterflies, beetles, mantises. Agile swarms, precision strikes, metamorphosis
1398
- - **aquatic** 🌊: Life that swims, dives, sloshes: fish, octopus, ink-creatures, sentient puddles. Masters of tides and pressure
1399
- - **flora** 🌿: Plants and fungi captured in bloom or decay. Growth, spores, vines, seasonal shifts
1400
- - **mineral** 🪨: Stones, crystals, metals shaped by earth's depths. High durability, reflective armor, seismic shocks
1401
- - **space** ✨: Stars, moon, cosmic objects not of this world. Stellar energy, gravitational effects, void manipulation
1402
- - **machina** ⚙️: Engineered devices from gadgets to heavy machinery. Gears, circuits, drones, power surges
1403
- - **structure** 🏛️: Buildings, bridges, monuments, ruins as titans. Fortification, terrain shaping, zone denial
1404
- - **culture** 🎨: Art, fashion, toys, written symbols. Buffs, debuffs, illusion, story-driven interactions
1405
- - **cuisine** 🍣: Dishes, drinks, culinary artistry. Flavors, aromas, temperature shifts for support or offense
1406
- - **normal** 👤: Attack type only (no Piclets are Normal type). Represents mundane, non-specialized attacks
1407
-
1408
- ### Type Effectiveness Chart
1409
-
1410
- | ATK \ DEF | 🐾 Beast | 🐛 Bug | 🌊 Aquatic | 🌿 Flora | 🪨 Mineral | ✨Space | ⚙️ Machina | 🏛️ Structure | 🎨 Culture | 🍣 Cuisine |
1411
- | ----------------- | :------: | :----: | :--------: | :------: | :--------: | :------: | :--------: | :-----------: | :--------: | :--------: |
1412
- | **🐾 Beast** | 1 | **×2** | 1 | 1 | ×½ | **0** | ×½ | ×½ | **×2** | **×2** |
1413
- | **🐛 Bug** | **×2** | 1 | 1 | **×2** | ×½ | ×½ | 1 | **0** | ×½ | ×½ |
1414
- | **🌊 Aquatic** | 1 | 1 | 1 | ×½ | **×2** | **×2** | **×2** | 1 | ×½ | ×½ |
1415
- | **🌿 Flora** | 1 | **×2** | **×2** | 1 | **×2** | ×½ | **0** | **×2** | 1 | ×½ |
1416
- | **🪨 Mineral** | **×2** | **×2** | ×½ | ×½ | 1 | ×½ | **×2** | 1 | 1 | **0** |
1417
- | **✨ Space** | **0** | **×2** | ×½ | **×2** | **×2** | 1 | ×½ | **×2** | ×½ | ×½ |
1418
- | **⚙️ Machina** | **×2** | ×½ | ×½ | **×2** | ×½ | ×½ | 1 | **×2** | 1 | 1 |
1419
- | **🏛️ Structure** | ×½ | ×½ | 1 | 1 | 1 | ×½ | **×2** | 1 | **×2** | **×2** |
1420
- | **🎨 Culture** | ×½ | ×½ | 1 | 1 | **0** | **×2** | **×2** | **×2** | 1 | ×½ |
1421
- | **🍣 Cuisine** | **×2** | ×½ | ×½ | 1 | **0** | **×2** | 1 | ×½ | **×2** | 1 |
1422
- | **👤 Normal** | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 |
1423
-
1424
- **Legend:**
1425
- - **×2** = Super effective (2x damage)
1426
- - **×½** = Not very effective (0.5x damage)
1427
- - **0** = No effect (0x damage)
1428
- - **1** = Normal effectiveness (1x damage)
1429
-
1430
- ## JSON Schema
1431
-
1432
- ```json
1433
- {
1434
- "$schema": "http://json-schema.org/draft-07/schema#",
1435
- "type": "object",
1436
- "title": "Piclet Definition",
1437
- "required": ["name", "description", "tier", "primaryType", "baseStats", "nature", "specialAbility", "movepool"],
1438
- "properties": {
1439
- "name": {
1440
- "type": "string",
1441
- "description": "The name of the Piclet"
1442
- },
1443
- "description": {
1444
- "type": "string",
1445
- "description": "Flavor text describing the Piclet"
1446
- },
1447
- "tier": {
1448
- "type": "string",
1449
- "enum": ["low", "medium", "high", "legendary"],
1450
- "description": "Power tier of the Piclet"
1451
- },
1452
- "primaryType": {
1453
- "type": "string",
1454
- "enum": ["beast", "bug", "aquatic", "flora", "mineral", "space", "machina", "structure", "culture", "cuisine"],
1455
- "description": "Primary type of the Piclet"
1456
- },
1457
- "secondaryType": {
1458
- "type": ["string", "null"],
1459
- "enum": ["beast", "bug", "aquatic", "flora", "mineral", "space", "machina", "structure", "culture", "cuisine", null],
1460
- "description": "Optional secondary type"
1461
- },
1462
- "baseStats": {
1463
- "type": "object",
1464
- "required": ["hp", "attack", "defense", "speed"],
1465
- "properties": {
1466
- "hp": {"type": "integer", "minimum": 1, "maximum": 255},
1467
- "attack": {"type": "integer", "minimum": 1, "maximum": 255},
1468
- "defense": {"type": "integer", "minimum": 1, "maximum": 255},
1469
- "speed": {"type": "integer", "minimum": 1, "maximum": 255}
1470
- },
1471
- "additionalProperties": false
1472
- },
1473
- "nature": {
1474
- "type": "string",
1475
- "description": "Personality trait affecting stats or behavior"
1476
- },
1477
- "specialAbility": {
1478
- "$ref": "#/definitions/SpecialAbility"
1479
- },
1480
- "movepool": {
1481
- "type": "array",
1482
- "items": {"$ref": "#/definitions/Move"},
1483
- "minItems": 1,
1484
- "maxItems": 8
1485
- }
1486
- },
1487
- "additionalProperties": false,
1488
- "definitions": {
1489
- "SpecialAbility": {
1490
- "type": "object",
1491
- "required": ["name", "description"],
1492
- "properties": {
1493
- "name": {"type": "string"},
1494
- "description": {"type": "string"},
1495
- "effects": {
1496
- "type": "array",
1497
- "items": {"$ref": "#/definitions/Effect"}
1498
- },
1499
- "triggers": {
1500
- "type": "array",
1501
- "items": {"$ref": "#/definitions/Trigger"}
1502
- }
1503
- },
1504
- "additionalProperties": false
1505
- },
1506
- "Move": {
1507
- "type": "object",
1508
- "required": ["name", "type", "power", "accuracy", "pp", "priority", "flags", "effects"],
1509
- "properties": {
1510
- "name": {"type": "string"},
1511
- "type": {
1512
- "type": "string",
1513
- "enum": ["beast", "bug", "aquatic", "flora", "mineral", "space", "machina", "structure", "culture", "cuisine", "normal"]
1514
- },
1515
- "power": {"type": "integer", "minimum": 0, "maximum": 250},
1516
- "accuracy": {"type": "integer", "minimum": 0, "maximum": 100},
1517
- "pp": {"type": "integer", "minimum": 1, "maximum": 50},
1518
- "priority": {"type": "integer", "minimum": -5, "maximum": 5},
1519
- "flags": {
1520
- "type": "array",
1521
- "items": {
1522
- "type": "string",
1523
- "enum": ["contact", "bite", "punch", "sound", "explosive", "draining", "ground", "priority", "lowPriority", "charging", "recharge", "multiHit", "twoTurn", "sacrifice", "gambling", "reckless", "reflectable", "snatchable", "copyable", "protectable", "bypassProtect"]
1524
- },
1525
- "uniqueItems": true
1526
- },
1527
- "effects": {
1528
- "type": "array",
1529
- "items": {"$ref": "#/definitions/Effect"},
1530
- "minItems": 1
1531
- }
1532
- },
1533
- "additionalProperties": false
1534
- },
1535
- "Effect": {
1536
- "type": "object",
1537
- "required": ["type"],
1538
- "properties": {
1539
- "type": {
1540
- "type": "string",
1541
- "enum": ["damage", "modifyStats", "applyStatus", "heal", "manipulatePP", "fieldEffect", "counter", "priority", "removeStatus", "mechanicOverride"]
1542
- },
1543
- "target": {
1544
- "type": "string",
1545
- "enum": ["self", "opponent", "allies", "all", "attacker", "field", "playerSide", "opponentSide"]
1546
- },
1547
- "condition": {
1548
- "type": "string",
1549
- "enum": ["always", "onHit", "afterUse", "onCritical", "ifLowHp", "ifHighHp", "thisTurn", "nextTurn", "turnAfterNext", "restOfBattle", "onCharging", "afterCharging", "ifDamagedThisTurn", "ifNotSuperEffective", "ifStatusMove", "ifLucky50", "ifUnlucky50", "whileFrozen"]
1550
- }
1551
- },
1552
- "allOf": [
1553
- {
1554
- "if": {"properties": {"type": {"const": "damage"}}},
1555
- "then": {
1556
- "required": ["amount"],
1557
- "properties": {
1558
- "amount": {
1559
- "type": "string",
1560
- "enum": ["weak", "normal", "strong", "extreme"]
1561
- }
1562
- }
1563
- }
1564
- },
1565
- {
1566
- "if": {"properties": {"type": {"const": "modifyStats"}}},
1567
- "then": {
1568
- "required": ["stats"],
1569
- "properties": {
1570
- "stats": {
1571
- "type": "object",
1572
- "properties": {
1573
- "hp": {"type": "string", "enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]},
1574
- "attack": {"type": "string", "enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]},
1575
- "defense": {"type": "string", "enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]},
1576
- "speed": {"type": "string", "enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]},
1577
- "accuracy": {"type": "string", "enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]}
1578
- },
1579
- "additionalProperties": false,
1580
- "minProperties": 1
1581
- }
1582
- }
1583
- }
1584
- },
1585
- {
1586
- "if": {"properties": {"type": {"const": "applyStatus"}}},
1587
- "then": {
1588
- "required": ["status"],
1589
- "properties": {
1590
- "status": {
1591
- "type": "string",
1592
- "enum": ["burn", "freeze", "paralyze", "poison", "sleep", "confuse"]
1593
- }
1594
- }
1595
- }
1596
- },
1597
- {
1598
- "if": {"properties": {"type": {"const": "heal"}}},
1599
- "then": {
1600
- "required": ["amount"],
1601
- "properties": {
1602
- "amount": {"type": "string", "enum": ["small", "medium", "large", "full"]}
1603
- }
1604
- }
1605
- },
1606
- {
1607
- "if": {"properties": {"type": {"const": "manipulatePP"}}},
1608
- "then": {
1609
- "required": ["action", "amount"],
1610
- "properties": {
1611
- "action": {"type": "string", "enum": ["drain", "restore", "disable"]},
1612
- "amount": {"type": "string", "enum": ["small", "medium", "large"]}
1613
- }
1614
- }
1615
- },
1616
- {
1617
- "if": {"properties": {"type": {"const": "fieldEffect"}}},
1618
- "then": {
1619
- "required": ["effect"],
1620
- "properties": {
1621
- "effect": {"type": "string"},
1622
- "stackable": {"type": "boolean"}
1623
- }
1624
- }
1625
- },
1626
- {
1627
- "if": {"properties": {"type": {"const": "counter"}}},
1628
- "then": {
1629
- "required": ["strength"],
1630
- "properties": {
1631
- "strength": {"type": "string", "enum": ["weak", "normal", "strong"]}
1632
- }
1633
- }
1634
- },
1635
- {
1636
- "if": {"properties": {"type": {"const": "priority"}}},
1637
- "then": {
1638
- "required": ["value"],
1639
- "properties": {
1640
- "value": {"type": "integer", "minimum": -5, "maximum": 5}
1641
- }
1642
- }
1643
- },
1644
- {
1645
- "if": {"properties": {"type": {"const": "removeStatus"}}},
1646
- "then": {
1647
- "required": ["status"],
1648
- "properties": {
1649
- "status": {
1650
- "type": "string",
1651
- "enum": ["burn", "freeze", "paralyze", "poison", "sleep", "confuse"]
1652
- }
1653
- }
1654
- }
1655
- },
1656
- {
1657
- "if": {"properties": {"type": {"const": "mechanicOverride"}}},
1658
- "then": {
1659
- "required": ["mechanic", "value"],
1660
- "properties": {
1661
- "mechanic": {
1662
- "type": "string",
1663
- "enum": ["criticalHits", "statusImmunity", "damageReflection", "damageAbsorption", "damageCalculation", "damageMultiplier", "healingInversion", "healingBlocked", "priorityOverride", "accuracyBypass", "typeImmunity", "typeChange", "contactDamage", "drainInversion", "weatherImmunity", "flagImmunity", "flagWeakness", "flagResistance", "statModification", "targetRedirection", "extraTurn"]
1664
- },
1665
- "value": {}
1666
- }
1667
- }
1668
- }
1669
- ],
1670
- "additionalProperties": false
1671
- },
1672
- "Trigger": {
1673
- "type": "object",
1674
- "required": ["event", "effects"],
1675
- "properties": {
1676
- "event": {
1677
- "type": "string",
1678
- "enum": ["onDamageTaken", "onDamageDealt", "onContactDamage", "onStatusInflicted", "onStatusMove", "onStatusMoveTargeted", "onCriticalHit", "onHPDrained", "onKO", "onSwitchIn", "onSwitchOut", "onWeatherChange", "beforeMoveUse", "afterMoveUse", "onLowHP", "onFullHP", "endOfTurn", "onOpponentContactMove"]
1679
- },
1680
- "condition": {
1681
- "type": "string",
1682
- "enum": ["always", "onHit", "afterUse", "onCritical", "ifLowHp", "ifHighHp", "thisTurn", "nextTurn", "turnAfterNext", "restOfBattle", "onCharging", "afterCharging", "ifDamagedThisTurn", "ifNotSuperEffective", "ifStatusMove", "ifLucky50", "ifUnlucky50", "whileFrozen"]
1683
- },
1684
- "effects": {
1685
- "type": "array",
1686
- "items": {"$ref": "#/definitions/Effect"},
1687
- "minItems": 1
1688
- }
1689
- },
1690
- "additionalProperties": false
1691
- }
1692
- }
1693
- }
1694
- ```
1695
-
1696
- ## Complete Example: Tempest Wraith
1697
-
1698
- Here's a full example of a Piclet using the complete schema with advanced abilities and dramatic moves:
1699
-
1700
- ```json
1701
- {
1702
- "name": "Tempest Wraith",
1703
- "description": "A ghostly creature born from violent storms, wielding cosmic energy and shadowy illusions",
1704
- "tier": "high",
1705
- "primaryType": "space",
1706
- "secondaryType": "culture",
1707
- "baseStats": {
1708
- "hp": 75,
1709
- "attack": 95,
1710
- "defense": 45,
1711
- "speed": 85
1712
- },
1713
- "nature": "timid",
1714
- "specialAbility": {
1715
- "name": "Storm Caller",
1716
- "description": "When HP drops below 25%, gains immunity to status effects and +50% speed",
1717
- "triggers": [
1718
- {
1719
- "event": "onLowHP",
1720
- "effects": [
1721
- {
1722
- "type": "mechanicOverride",
1723
- "mechanic": "statusImmunity",
1724
- "value": ["burn", "freeze", "paralyze", "poison", "sleep", "confuse"]
1725
- },
1726
- {
1727
- "type": "modifyStats",
1728
- "target": "self",
1729
- "stats": { "speed": "greatly_increase" }
1730
- }
1731
- ]
1732
- },
1733
- {
1734
- "event": "onSwitchIn",
1735
- "condition": "ifWeather:storm",
1736
- "effects": [
1737
- {
1738
- "type": "modifyStats",
1739
- "target": "self",
1740
- "stats": { "attack": "increase" }
1741
- }
1742
- ]
1743
- }
1744
- ]
1745
- },
1746
- "movepool": [
1747
- {
1748
- "name": "Shadow Pulse",
1749
- "type": "culture",
1750
- "power": 70,
1751
- "accuracy": 100,
1752
- "pp": 15,
1753
- "priority": 0,
1754
- "flags": [],
1755
- "effects": [
1756
- {
1757
- "type": "damage",
1758
- "target": "opponent",
1759
- "amount": "normal"
1760
- },
1761
- {
1762
- "type": "applyStatus",
1763
- "target": "opponent",
1764
- "status": "confuse"
1765
- }
1766
- ]
1767
- },
1768
- {
1769
- "name": "Cosmic Strike",
1770
- "type": "space",
1771
- "power": 85,
1772
- "accuracy": 90,
1773
- "pp": 10,
1774
- "priority": 0,
1775
- "flags": [],
1776
- "effects": [
1777
- {
1778
- "type": "damage",
1779
- "target": "opponent",
1780
- "amount": "normal"
1781
- },
1782
- {
1783
- "type": "applyStatus",
1784
- "target": "opponent",
1785
- "status": "paralyze"
1786
- }
1787
- ]
1788
- },
1789
- {
1790
- "name": "Spectral Drain",
1791
- "type": "culture",
1792
- "power": 60,
1793
- "accuracy": 95,
1794
- "pp": 12,
1795
- "priority": 0,
1796
- "flags": ["draining"],
1797
- "effects": [
1798
- {
1799
- "type": "damage",
1800
- "target": "opponent",
1801
- "formula": "drain",
1802
- "value": 0.5
1803
- },
1804
- {
1805
- "type": "heal",
1806
- "target": "self",
1807
- "amount": "medium"
1808
- }
1809
- ]
1810
- },
1811
- {
1812
- "name": "Void Sacrifice",
1813
- "type": "space",
1814
- "power": 130,
1815
- "accuracy": 85,
1816
- "pp": 1,
1817
- "priority": 0,
1818
- "flags": ["sacrifice", "explosive"],
1819
- "effects": [
1820
- {
1821
- "type": "damage",
1822
- "target": "all",
1823
- "formula": "standard",
1824
- "multiplier": 1.2
1825
- },
1826
- {
1827
- "type": "damage",
1828
- "target": "self",
1829
- "formula": "percentage",
1830
- "value": 75
1831
- },
1832
- {
1833
- "type": "fieldEffect",
1834
- "effect": "voidStorm",
1835
- "target": "field",
1836
- "stackable": false
1837
- }
1838
- ]
1839
- }
1840
- ]
1841
- }
1842
- ```
1843
-
1844
- This example demonstrates:
1845
-
1846
- ### **Advanced Special Ability**
1847
- - **Conditional Triggers**: Different effects based on HP and weather
1848
- - **Multiple Mechanics**: Status immunity + stat boosts + weather interactions
1849
- - **Strategic Depth**: Becomes more dangerous when near defeat
1850
-
1851
- ### **Diverse Movepool**
1852
- - **Standard Attack**: Shadow Pulse with minor status chance
1853
- - **Type Coverage**: Storm and Shadow moves for different matchups
1854
- - **Utility Move**: Spectral Drain for sustainability
1855
- - **Ultimate Move**: Storm's Sacrifice - massive AoE damage with severe self-harm
1856
-
1857
- ### **Meaningful Tradeoffs**
1858
- - **Spectral Drain**: Healing requires hitting the opponent
1859
- - **Storm's Sacrifice**: Incredible power (130 base + 20% bonus to all) but costs 75% of user's HP
1860
- - **Low defenses**: High speed/special attack but vulnerable to physical moves
1861
-
1862
- ### **Emergent Strategy**
1863
- - Use standard moves early while healthy
1864
- - Spectral Drain for sustain in mid-game
1865
- - When low on HP, ability kicks in for immunity and speed boost
1866
- - Storm's Sacrifice as desperate finisher or when opponent is also low
1867
-
1868
- This creates a Piclet that plays differently throughout the battle, rewards risk-taking, and offers multiple viable strategies depending on the situation!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/BattleEngine.test.ts DELETED
@@ -1,364 +0,0 @@
1
- /**
2
- * Test suite for the Battle Engine
3
- * Tests battle flow, damage calculation, effects, and type effectiveness
4
- */
5
-
6
- import { describe, it, expect, beforeEach } from 'vitest';
7
- import { BattleEngine } from './BattleEngine';
8
- import {
9
- STELLAR_WOLF,
10
- TOXIC_CRAWLER,
11
- BERSERKER_BEAST,
12
- AQUA_GUARDIAN,
13
- BASIC_TACKLE,
14
- FLAME_BURST,
15
- HEALING_LIGHT,
16
- POWER_UP,
17
- BERSERKER_END,
18
- TOXIC_STING
19
- } from './test-data';
20
- import { BattleAction } from './types';
21
-
22
- describe('BattleEngine', () => {
23
- let engine: BattleEngine;
24
-
25
- beforeEach(() => {
26
- engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
27
- });
28
-
29
- describe('Battle Initialization', () => {
30
- it('should initialize battle state correctly', () => {
31
- const state = engine.getState();
32
-
33
- expect(state.turn).toBe(1);
34
- expect(state.phase).toBe('selection');
35
- expect(state.playerPiclet.definition.name).toBe('Stellar Wolf');
36
- expect(state.opponentPiclet.definition.name).toBe('Toxic Crawler');
37
- expect(state.winner).toBeUndefined();
38
- expect(state.log.length).toBe(0);
39
- });
40
-
41
- it('should calculate battle stats correctly', () => {
42
- const state = engine.getState();
43
- const player = state.playerPiclet;
44
-
45
- // Level 50 should have base stats (no modifier)
46
- expect(player.maxHp).toBe(STELLAR_WOLF.baseStats.hp);
47
- expect(player.attack).toBe(STELLAR_WOLF.baseStats.attack);
48
- expect(player.defense).toBe(STELLAR_WOLF.baseStats.defense);
49
- expect(player.speed).toBe(STELLAR_WOLF.baseStats.speed);
50
- expect(player.currentHp).toBe(player.maxHp);
51
- });
52
-
53
- it('should initialize moves with correct PP', () => {
54
- const state = engine.getState();
55
- const playerMoves = state.playerPiclet.moves;
56
-
57
- expect(playerMoves).toHaveLength(4);
58
- expect(playerMoves[0].move.name).toBe('Tackle');
59
- expect(playerMoves[0].currentPP).toBe(35);
60
- expect(playerMoves[1].move.name).toBe('Flame Burst');
61
- expect(playerMoves[1].currentPP).toBe(15);
62
- });
63
- });
64
-
65
- describe('Basic Battle Flow', () => {
66
- it('should execute a basic turn', () => {
67
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 };
68
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
69
-
70
- engine.executeActions(playerAction, opponentAction);
71
-
72
- const state = engine.getState();
73
- expect(state.turn).toBe(2);
74
- expect(state.phase).toBe('selection');
75
- expect(state.log.length).toBeGreaterThan(2);
76
- });
77
-
78
- it('should consume PP when moves are used', () => {
79
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 };
80
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
81
-
82
- const initialPP = engine.getState().playerPiclet.moves[0].currentPP;
83
- engine.executeActions(playerAction, opponentAction);
84
- const finalPP = engine.getState().playerPiclet.moves[0].currentPP;
85
-
86
- expect(finalPP).toBe(initialPP - 1);
87
- });
88
-
89
- it('should handle moves with no PP', () => {
90
- // Manually set PP to 0 by getting mutable state
91
- const state = engine.getState();
92
- engine['state'].playerPiclet.moves[0].currentPP = 0;
93
-
94
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 };
95
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
96
-
97
- engine.executeActions(playerAction, opponentAction);
98
-
99
- const log = engine.getLog();
100
- expect(log.some(msg => msg.includes('no PP left'))).toBe(true);
101
- });
102
- });
103
-
104
- describe('Damage Calculation', () => {
105
- it('should calculate basic damage correctly', () => {
106
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; // Tackle
107
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
108
-
109
- const initialHp = engine.getState().opponentPiclet.currentHp;
110
- engine.executeActions(playerAction, opponentAction);
111
- const finalHp = engine.getState().opponentPiclet.currentHp;
112
-
113
- expect(finalHp).toBeLessThan(initialHp);
114
- expect(finalHp).toBeGreaterThan(0); // Should not be a one-hit KO
115
- });
116
-
117
- it('should apply type effectiveness correctly', () => {
118
- // Create engine with type advantage: Space vs Bug (Space is 2x effective vs Bug)
119
- const spaceVsBug = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
120
-
121
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Flame Burst (Space type)
122
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
123
-
124
- const initialHp = spaceVsBug.getState().opponentPiclet.currentHp;
125
- spaceVsBug.executeActions(playerAction, opponentAction);
126
-
127
- const log = spaceVsBug.getLog();
128
- expect(log.some(msg => msg.includes("It's super effective!"))).toBe(true);
129
- });
130
-
131
- it('should apply STAB (Same Type Attack Bonus)', () => {
132
- // Stellar Wolf using Flame Burst (Space type move, matches primary type)
133
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 };
134
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
135
-
136
- const initialHp = engine.getState().opponentPiclet.currentHp;
137
- engine.executeActions(playerAction, opponentAction);
138
- const finalHp = engine.getState().opponentPiclet.currentHp;
139
-
140
- // With STAB, damage should be higher than without
141
- expect(finalHp).toBeLessThan(initialHp);
142
- });
143
- });
144
-
145
- describe('Status Effects', () => {
146
- it('should apply poison status', () => {
147
- const toxicEngine = new BattleEngine(TOXIC_CRAWLER, STELLAR_WOLF);
148
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Toxic Sting
149
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
150
-
151
- toxicEngine.executeActions(playerAction, opponentAction);
152
-
153
- const state = toxicEngine.getState();
154
- expect(state.opponentPiclet.statusEffects).toContain('poison');
155
- });
156
-
157
- it('should process poison damage at turn end', () => {
158
- const toxicEngine = new BattleEngine(TOXIC_CRAWLER, STELLAR_WOLF);
159
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Toxic Sting
160
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
161
-
162
- toxicEngine.executeActions(playerAction, opponentAction);
163
-
164
- const hpAfterPoison = toxicEngine.getState().opponentPiclet.currentHp;
165
-
166
- // Execute another turn to trigger poison damage
167
- toxicEngine.executeActions(
168
- { type: 'move', piclet: 'player', moveIndex: 0 },
169
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
170
- );
171
-
172
- const hpAfterSecondTurn = toxicEngine.getState().opponentPiclet.currentHp;
173
- expect(hpAfterSecondTurn).toBeLessThan(hpAfterPoison);
174
-
175
- const log = toxicEngine.getLog();
176
- expect(log.some(msg => msg.includes('hurt by poison'))).toBe(true);
177
- });
178
- });
179
-
180
- describe('Stat Modifications', () => {
181
- it('should increase attack stat', () => {
182
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 3 }; // Power Up
183
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
184
-
185
- const initialAttack = engine.getState().playerPiclet.attack;
186
- engine.executeActions(playerAction, opponentAction);
187
- const finalAttack = engine.getState().playerPiclet.attack;
188
-
189
- expect(finalAttack).toBeGreaterThan(initialAttack);
190
-
191
- const log = engine.getLog();
192
- expect(log.some(msg => msg.includes("attack rose"))).toBe(true);
193
- });
194
- });
195
-
196
- describe('Healing Effects', () => {
197
- it('should heal HP correctly', () => {
198
- // Damage the player first by directly modifying the internal state
199
- engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5);
200
-
201
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 2 }; // Healing Light
202
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
203
-
204
- const hpBeforeHeal = engine.getState().playerPiclet.currentHp;
205
- engine.executeActions(playerAction, opponentAction);
206
- const hpAfterHeal = engine.getState().playerPiclet.currentHp;
207
-
208
- expect(hpAfterHeal).toBeGreaterThan(hpBeforeHeal);
209
-
210
- const log = engine.getLog();
211
- expect(log.some(msg => msg.includes('recovered') && msg.includes('HP'))).toBe(true);
212
- });
213
-
214
- it('should not heal above max HP', () => {
215
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 2 }; // Healing Light
216
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
217
-
218
- engine.executeActions(playerAction, opponentAction);
219
-
220
- const state = engine.getState();
221
- expect(state.playerPiclet.currentHp).toBeLessThanOrEqual(state.playerPiclet.maxHp);
222
- });
223
- });
224
-
225
- describe('Conditional Effects', () => {
226
- it('should trigger conditional effects when conditions are met', () => {
227
- const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF);
228
-
229
- // Set player to low HP to trigger condition
230
- berserkerEngine['state'].playerPiclet.currentHp = Math.floor(berserkerEngine['state'].playerPiclet.maxHp * 0.2);
231
-
232
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Berserker's End
233
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
234
-
235
- const initialDefense = berserkerEngine.getState().playerPiclet.defense;
236
- berserkerEngine.executeActions(playerAction, opponentAction);
237
- const finalDefense = berserkerEngine.getState().playerPiclet.defense;
238
-
239
- // Defense should be greatly decreased due to low HP condition
240
- expect(finalDefense).toBeLessThan(initialDefense);
241
- });
242
-
243
- it('should not trigger conditional effects when conditions are not met', () => {
244
- const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF);
245
- // Player at full HP - condition not met
246
-
247
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Berserker's End
248
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
249
-
250
- const initialDefense = berserkerEngine.getState().playerPiclet.defense;
251
- berserkerEngine.executeActions(playerAction, opponentAction);
252
- const finalDefense = berserkerEngine.getState().playerPiclet.defense;
253
-
254
- // Defense should remain unchanged
255
- expect(finalDefense).toBe(initialDefense);
256
- });
257
- });
258
-
259
- describe('Battle End Conditions', () => {
260
- it('should end battle when player Piclet faints', () => {
261
- // Set player HP to 0 to guarantee fainting
262
- engine['state'].playerPiclet.currentHp = 0;
263
-
264
- // Force battle end check
265
- engine['checkBattleEnd']();
266
-
267
- expect(engine.isGameOver()).toBe(true);
268
- expect(engine.getWinner()).toBe('opponent');
269
- });
270
-
271
- it('should end battle when opponent Piclet faints', () => {
272
- engine['state'].opponentPiclet.currentHp = 1; // Set to very low HP
273
-
274
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 };
275
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
276
-
277
- engine.executeActions(playerAction, opponentAction);
278
-
279
- expect(engine.isGameOver()).toBe(true);
280
- expect(engine.getWinner()).toBe('player');
281
- });
282
-
283
- it('should handle draw when both Piclets faint', () => {
284
- // Set both HP to 0 to guarantee draw
285
- engine['state'].playerPiclet.currentHp = 0;
286
- engine['state'].opponentPiclet.currentHp = 0;
287
-
288
- // Force battle end check
289
- engine['checkBattleEnd']();
290
-
291
- expect(engine.isGameOver()).toBe(true);
292
- expect(engine.getWinner()).toBe('draw');
293
- });
294
- });
295
-
296
- describe('Move Accuracy', () => {
297
- it('should handle move misses', () => {
298
- // Mock Math.random to force a miss
299
- const originalRandom = Math.random;
300
- Math.random = () => 0.99; // Force miss for 90% accuracy moves
301
-
302
- const berserkerEngine = new BattleEngine(BERSERKER_BEAST, STELLAR_WOLF);
303
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 1 }; // Berserker's End (90% accuracy)
304
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 };
305
-
306
- const initialHp = berserkerEngine.getState().opponentPiclet.currentHp;
307
- berserkerEngine.executeActions(playerAction, opponentAction);
308
- const finalHp = berserkerEngine.getState().opponentPiclet.currentHp;
309
-
310
- // HP should be unchanged due to miss
311
- expect(finalHp).toBe(initialHp);
312
-
313
- const log = berserkerEngine.getLog();
314
- expect(log.some(msg => msg.includes('attack missed'))).toBe(true);
315
-
316
- // Restore original Math.random
317
- Math.random = originalRandom;
318
- });
319
- });
320
-
321
- describe('Action Priority', () => {
322
- it('should execute higher priority moves first', () => {
323
- // Create a custom high-priority move for testing
324
- const highPriorityMove = {
325
- ...BASIC_TACKLE,
326
- name: "Quick Attack",
327
- priority: 1
328
- };
329
-
330
- const customWolf = {
331
- ...STELLAR_WOLF,
332
- movepool: [highPriorityMove, BASIC_TACKLE, HEALING_LIGHT, POWER_UP]
333
- };
334
-
335
- const priorityEngine = new BattleEngine(customWolf, TOXIC_CRAWLER);
336
-
337
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; // Quick Attack (priority 1)
338
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; // Tackle (priority 0)
339
-
340
- priorityEngine.executeActions(playerAction, opponentAction);
341
-
342
- const log = priorityEngine.getLog();
343
- const playerMoveIndex = log.findIndex(msg => msg.includes('used Quick Attack'));
344
- const opponentMoveIndex = log.findIndex(msg => msg.includes('used Tackle'));
345
-
346
- expect(playerMoveIndex).toBeLessThan(opponentMoveIndex);
347
- });
348
-
349
- it('should use speed for same priority moves', () => {
350
- // Both using same priority moves, faster should go first
351
- const playerAction: BattleAction = { type: 'move', piclet: 'player', moveIndex: 0 }; // Tackle
352
- const opponentAction: BattleAction = { type: 'move', piclet: 'opponent', moveIndex: 0 }; // Tackle
353
-
354
- engine.executeActions(playerAction, opponentAction);
355
-
356
- const log = engine.getLog();
357
- const stellarWolfIndex = log.findIndex(msg => msg.includes('Stellar Wolf used'));
358
- const toxicCrawlerIndex = log.findIndex(msg => msg.includes('Toxic Crawler used'));
359
-
360
- // Stellar Wolf has higher speed (70 vs 55), so should go first
361
- expect(stellarWolfIndex).toBeLessThan(toxicCrawlerIndex);
362
- });
363
- });
364
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/BattleEngine.ts DELETED
@@ -1,1876 +0,0 @@
1
- /**
2
- * Core Battle Engine for Pictuary
3
- * Implements the battle system as defined in battle_system_design.md
4
- */
5
-
6
- import type {
7
- BattleState,
8
- BattlePiclet,
9
- PicletDefinition,
10
- BattleAction,
11
- MoveAction,
12
- SwitchAction,
13
- CaptureAction,
14
- BattleEffect,
15
- DamageAmount,
16
- StatModification,
17
- HealAmount,
18
- StatusEffect,
19
- BaseStats,
20
- Move,
21
- Trigger
22
- } from './types';
23
- import { getEffectivenessMultiplier } from '../types/picletTypes';
24
- import { attemptCapture, getCatchRateForTier, calculateCapturePercentage } from '../services/captureService';
25
-
26
- export class BattleEngine {
27
- private state: BattleState;
28
- private playerRoster: PicletDefinition[];
29
- private opponentRoster: PicletDefinition[];
30
- private playerRosterStates: Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }>;
31
- private opponentRosterStates: Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }>;
32
-
33
- constructor(
34
- playerPiclet: PicletDefinition | PicletDefinition[],
35
- opponentPiclet: PicletDefinition | PicletDefinition[],
36
- playerLevel = 50,
37
- opponentLevel = 50
38
- ) {
39
- // Handle roster setup with internal prefixes for reliable animation targeting
40
- this.playerRoster = (Array.isArray(playerPiclet) ? playerPiclet : [playerPiclet])
41
- .map(piclet => ({ ...piclet, name: `player-${piclet.name}` }));
42
- this.opponentRoster = (Array.isArray(opponentPiclet) ? opponentPiclet : [opponentPiclet])
43
- .map(piclet => ({ ...piclet, name: `enemy-${piclet.name}` }));
44
-
45
- // Initialize roster states
46
- this.playerRosterStates = this.initializeRosterStates(this.playerRoster, playerLevel);
47
- this.opponentRosterStates = this.initializeRosterStates(this.opponentRoster, opponentLevel);
48
- this.state = {
49
- turn: 1,
50
- phase: 'selection',
51
- playerPiclet: this.createBattlePiclet(this.playerRoster[0], playerLevel),
52
- opponentPiclet: this.createBattlePiclet(this.opponentRoster[0], opponentLevel),
53
- fieldEffects: [],
54
- log: [],
55
- winner: undefined
56
- };
57
-
58
- // Sync initial states to roster for consistency
59
- this.syncActivePicketToRoster('player');
60
- this.syncActivePicketToRoster('opponent');
61
-
62
- }
63
-
64
- private initializeRosterStates(roster: PicletDefinition[], level: number): Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }> {
65
- return roster.map(piclet => {
66
- // Use pre-calculated HP from definition (already includes level scaling)
67
- const hp = piclet.baseStats.hp;
68
- return {
69
- currentHp: hp,
70
- maxHp: hp,
71
- fainted: false,
72
- moves: piclet.movepool.slice(0, 4).map(move => ({
73
- move,
74
- currentPP: move.pp
75
- }))
76
- };
77
- });
78
- }
79
-
80
- private createBattlePiclet(definition: PicletDefinition, level: number): BattlePiclet {
81
- // Battle engine now uses pre-calculated stats from levelingService
82
- // No level scaling needed here - stats already include level and nature effects
83
- const hp = definition.baseStats.hp;
84
- const attack = definition.baseStats.attack;
85
- const defense = definition.baseStats.defense;
86
- const speed = definition.baseStats.speed;
87
-
88
- const piclet: BattlePiclet = {
89
- definition,
90
- currentHp: hp,
91
- maxHp: hp,
92
- level,
93
- attack,
94
- defense,
95
- speed,
96
- accuracy: 100, // Base accuracy
97
- statusEffects: [],
98
- moves: definition.movepool.slice(0, 4).map(move => ({
99
- move,
100
- currentPP: move.pp
101
- })),
102
- statModifiers: {},
103
- temporaryEffects: []
104
- };
105
-
106
- // Apply special ability effects
107
- if (definition.specialAbility?.effects) {
108
- for (const effect of definition.specialAbility.effects) {
109
- this.applyEffectToPiclet(effect, piclet);
110
- }
111
- }
112
-
113
- return piclet;
114
- }
115
-
116
- public getState(): BattleState {
117
- return JSON.parse(JSON.stringify(this.state)); // Deep clone for immutability
118
- }
119
-
120
- public isGameOver(): boolean {
121
- return this.state.phase === 'ended';
122
- }
123
-
124
- public getWinner(): 'player' | 'opponent' | 'draw' | undefined {
125
- return this.state.winner;
126
- }
127
-
128
- public getCapturePercentage(): number {
129
- const targetPiclet = this.state.opponentPiclet;
130
-
131
- // Get capture parameters
132
- const maxHp = targetPiclet.maxHp;
133
- const currentHp = targetPiclet.currentHp;
134
- const tier = targetPiclet.definition.tier;
135
- const baseCatchRate = getCatchRateForTier(tier);
136
-
137
- // Get status effect for capture bonus
138
- let statusEffect: 'sleep' | 'freeze' | 'poison' | 'burn' | 'paralysis' | 'toxic' | null = null;
139
- if (targetPiclet.statusEffects.length > 0) {
140
- const firstStatus = targetPiclet.statusEffects[0];
141
- // Cast to proper type (status effects in battle engine match capture service types)
142
- statusEffect = firstStatus as 'sleep' | 'freeze' | 'poison' | 'burn' | 'paralysis' | 'toxic';
143
- }
144
-
145
- return calculateCapturePercentage({
146
- maxHp,
147
- currentHp,
148
- baseCatchRate,
149
- statusEffect,
150
- picletLevel: targetPiclet.level
151
- });
152
- }
153
-
154
- public executeActions(playerAction: BattleAction, opponentAction: BattleAction): void {
155
- if (this.state.phase !== 'selection') {
156
- throw new Error('Cannot execute actions - battle is not in selection phase');
157
- }
158
-
159
- this.state.phase = 'execution';
160
-
161
- // Determine action order based on priority and speed
162
- const actions = this.determineActionOrder(playerAction, opponentAction);
163
-
164
- // Execute actions in order
165
- for (const action of actions) {
166
- if ((this.state.phase as string) === 'ended') break; // Check if battle already ended
167
- this.executeAction(action);
168
-
169
- // Check for battle end after each action (important for self-destruct moves)
170
- this.checkBattleEnd();
171
- if ((this.state.phase as string) === 'ended') break;
172
- }
173
-
174
- // End of turn processing
175
- if ((this.state.phase as string) !== 'ended') {
176
- this.processTurnEnd();
177
- }
178
-
179
- // Sync active piclets to roster to preserve state changes
180
- if ((this.state.phase as string) !== 'ended') {
181
- this.syncActivePicketToRoster('player');
182
- this.syncActivePicketToRoster('opponent');
183
- }
184
-
185
- // Check for battle end
186
- this.checkBattleEnd();
187
-
188
- if ((this.state.phase as string) !== 'ended') {
189
- this.state.turn++;
190
- this.state.phase = 'selection';
191
- }
192
- }
193
-
194
- private determineActionOrder(playerAction: BattleAction, opponentAction: BattleAction): Array<BattleAction & { executor: 'player' | 'opponent' }> {
195
- const playerPriority = this.getActionPriority(playerAction, this.state.playerPiclet);
196
- const opponentPriority = this.getActionPriority(opponentAction, this.state.opponentPiclet);
197
-
198
- const playerSpeed = this.state.playerPiclet.speed;
199
- const opponentSpeed = this.state.opponentPiclet.speed;
200
-
201
- // Higher priority goes first, then speed, then random
202
- let playerFirst = false;
203
- if (playerPriority > opponentPriority) {
204
- playerFirst = true;
205
- } else if (playerPriority < opponentPriority) {
206
- playerFirst = false;
207
- } else if (playerSpeed > opponentSpeed) {
208
- playerFirst = true;
209
- } else if (playerSpeed < opponentSpeed) {
210
- playerFirst = false;
211
- } else {
212
- playerFirst = Math.random() < 0.5; // Speed tie
213
- }
214
-
215
- return playerFirst
216
- ? [
217
- { ...playerAction, executor: 'player' as const },
218
- { ...opponentAction, executor: 'opponent' as const }
219
- ]
220
- : [
221
- { ...opponentAction, executor: 'opponent' as const },
222
- { ...playerAction, executor: 'player' as const }
223
- ];
224
- }
225
-
226
- private getActionPriority(action: BattleAction, piclet: BattlePiclet): number {
227
- let priority = 0;
228
-
229
- if (action.type === 'move') {
230
- const move = piclet.moves[action.moveIndex]?.move;
231
- priority = move?.priority || 0;
232
-
233
- // Check for conditional priority effects in the move
234
- if (move?.effects) {
235
- for (const effect of move.effects) {
236
- if (effect.type === 'priority' && (!effect.condition || this.checkCondition(effect.condition, piclet, piclet))) {
237
- priority += (effect as any).value || 0;
238
- }
239
- }
240
- }
241
-
242
- // Add priority modifier from effects
243
- const priorityMod = piclet.statModifiers.priority || 0;
244
- priority += priorityMod;
245
- } else {
246
- priority = 6; // Switch actions have highest priority
247
- }
248
-
249
- return priority;
250
- }
251
-
252
- private executeAction(action: BattleAction & { executor: 'player' | 'opponent' }): void {
253
- if (action.type === 'move') {
254
- this.executeMove(action);
255
- } else if (action.type === 'switch') {
256
- this.executeSwitch(action as SwitchAction & { executor: 'player' | 'opponent' });
257
- } else if (action.type === 'capture') {
258
- this.executeCapture(action as CaptureAction & { executor: 'player' | 'opponent' });
259
- }
260
- }
261
-
262
- private executeMove(action: MoveAction & { executor: 'player' | 'opponent' }): void {
263
- console.log('🚀 executeMove started:', {
264
- executor: action.executor,
265
- moveIndex: action.moveIndex
266
- });
267
-
268
- const attacker = action.executor === 'player' ? this.state.playerPiclet : this.state.opponentPiclet;
269
- const defender = action.executor === 'player' ? this.state.opponentPiclet : this.state.playerPiclet;
270
-
271
- console.log('👥 Attacker/Defender:', {
272
- attacker: attacker.definition.name,
273
- defender: defender.definition.name
274
- });
275
-
276
- // Check if attacker can act due to status effects
277
- if (!this.canPicletAct(attacker)) {
278
- console.log('❌ Attacker cannot act due to status effects');
279
- return; // Skip this action
280
- }
281
-
282
- const moveData = attacker.moves[action.moveIndex];
283
- if (!moveData || moveData.currentPP <= 0) {
284
- console.log('❌ No move data or no PP:', { moveData: !!moveData, pp: moveData?.currentPP });
285
- this.log(`${attacker.definition.name} has no PP left for that move!`);
286
- return;
287
- }
288
-
289
- const move = moveData.move;
290
- console.log('✅ Move to execute:', {
291
- name: move.name,
292
- type: move.type,
293
- power: move.power,
294
- effects: move.effects?.length || 0
295
- });
296
-
297
- // Trigger before move use
298
- this.triggerBeforeMoveUse(attacker, move);
299
-
300
- this.log(`${attacker.definition.name} used ${move.name}!`);
301
-
302
- // Consume PP
303
- moveData.currentPP--;
304
-
305
- // Check if move hits
306
- const moveHit = this.checkMoveHits(move, attacker, defender);
307
- if (!moveHit) {
308
- this.log(`${attacker.definition.name}'s attack missed!`);
309
- this.triggerAfterMoveUse(attacker, move, false);
310
- return;
311
- }
312
-
313
- // Trigger opponent contact move (if applicable)
314
- this.triggerOnOpponentContactMove(defender, attacker, move);
315
-
316
- // For gambling/luck-based moves, roll once and store the result
317
- const luckyRoll = Math.random() < 0.5;
318
-
319
- // Process effects
320
- console.log('🔄 Processing effects:', move.effects.length);
321
- for (let i = 0; i < move.effects.length; i++) {
322
- const effect = move.effects[i];
323
- console.log(`🎭 Processing effect ${i + 1}/${move.effects.length}:`, {
324
- type: effect.type,
325
- effect: effect
326
- });
327
- try {
328
- this.processEffect(effect, attacker, defender, move, luckyRoll);
329
- console.log(`✅ Effect ${i + 1} completed successfully`);
330
- } catch (error) {
331
- console.error(`❌ Effect ${i + 1} failed:`, error);
332
- throw error; // Re-throw to maintain error behavior
333
- }
334
- }
335
-
336
- // Trigger after move use
337
- this.triggerAfterMoveUse(attacker, move, true);
338
- }
339
-
340
- private executeSwitch(action: SwitchAction & { executor: 'player' | 'opponent' }): void {
341
- const isPlayer = action.executor === 'player';
342
- const roster = isPlayer ? this.playerRoster : this.opponentRoster;
343
- const rosterStates = isPlayer ? this.playerRosterStates : this.opponentRosterStates;
344
- const currentPiclet = isPlayer ? this.state.playerPiclet : this.state.opponentPiclet;
345
-
346
- // Validate switch action
347
- if (action.newPicletIndex < 0 || action.newPicletIndex >= roster.length) {
348
- this.log(`${action.executor} cannot switch - invalid piclet index!`);
349
- return;
350
- }
351
-
352
- if (action.newPicletIndex === this.getCurrentPicletIndex(action.executor)) {
353
- this.log(`${roster[action.newPicletIndex].name} is already active!`);
354
- return;
355
- }
356
-
357
- if (rosterStates[action.newPicletIndex].fainted) {
358
- this.log(`${roster[action.newPicletIndex].name} is unable to battle!`);
359
- return;
360
- }
361
-
362
- const oldPiclet = currentPiclet;
363
- const newPicletDef = roster[action.newPicletIndex];
364
-
365
- // Trigger switch-out ability
366
- this.triggerOnSwitchOut(oldPiclet);
367
-
368
- // Save current piclet state back to roster
369
- this.savePicletToRoster(oldPiclet, action.executor);
370
-
371
- // Load new piclet from roster
372
- const newPiclet = this.loadPicletFromRoster(action.newPicletIndex, action.executor);
373
-
374
- // Update battle state
375
- if (isPlayer) {
376
- this.state.playerPiclet = newPiclet;
377
- } else {
378
- this.state.opponentPiclet = newPiclet;
379
- }
380
-
381
- this.log(`${action.executor} switched to ${newPicletDef.name}!`);
382
-
383
- // Apply entry hazards
384
- this.applyEntryHazards(newPiclet);
385
-
386
- // Trigger switch-in ability
387
- this.triggerOnSwitchIn(newPiclet);
388
- }
389
-
390
- private executeCapture(action: CaptureAction & { executor: 'player' | 'opponent' }): void {
391
- // Only player can capture (wild battles only)
392
- if (action.executor !== 'player') {
393
- this.log('Only the player can capture Piclets!');
394
- return;
395
- }
396
-
397
- // Can't capture in trainer battles (this would be determined by battle context)
398
- // For now, we'll assume this is a wild battle
399
-
400
- const targetPiclet = this.state.opponentPiclet;
401
-
402
- // Get capture parameters
403
- const maxHp = targetPiclet.maxHp;
404
- const currentHp = targetPiclet.currentHp;
405
- const tier = targetPiclet.definition.tier;
406
- const baseCatchRate = getCatchRateForTier(tier);
407
-
408
- // Get status effect for capture bonus
409
- let statusEffect: 'sleep' | 'freeze' | 'poison' | 'burn' | 'paralysis' | 'toxic' | null = null;
410
- if (targetPiclet.statusEffects.length > 0) {
411
- // Use the first status effect (most Pokemon games only allow one)
412
- const firstStatus = targetPiclet.statusEffects[0];
413
- // Cast to proper type (status effects in battle engine match capture service types)
414
- statusEffect = firstStatus as 'sleep' | 'freeze' | 'poison' | 'burn' | 'paralysis' | 'toxic';
415
- }
416
-
417
- // Calculate capture percentage for display
418
- const capturePercentage = calculateCapturePercentage({
419
- maxHp,
420
- currentHp,
421
- baseCatchRate,
422
- statusEffect,
423
- picletLevel: targetPiclet.level
424
- });
425
-
426
- // Attempt the capture
427
- const result = attemptCapture({
428
- maxHp,
429
- currentHp,
430
- baseCatchRate,
431
- statusEffect,
432
- picletLevel: targetPiclet.level
433
- });
434
-
435
- // Store capture result in battle state
436
- this.state.captureResult = {
437
- success: result.success,
438
- shakes: result.shakes,
439
- odds: result.odds,
440
- capturePercentage
441
- };
442
-
443
- // Log the attempt
444
- this.log(`Player took a Pic-ture of ${targetPiclet.definition.name}!`);
445
-
446
- // Log shakes
447
- if (result.shakes === 0) {
448
- this.log('The Pic-ture broke immediately!');
449
- } else {
450
- const shakeText = result.shakes === 1 ? 'once' : result.shakes === 2 ? 'twice' : 'three times';
451
- this.log(`The Pic-ture shook ${shakeText}...`);
452
- }
453
-
454
- if (result.success) {
455
- this.log(`${targetPiclet.definition.name} was captured!`);
456
- // Set winner to player (capture ends the battle)
457
- this.state.winner = 'player';
458
- this.state.phase = 'ended';
459
- } else {
460
- this.log(`${targetPiclet.definition.name} broke free!`);
461
- // Capture failed, battle continues
462
- // The opponent gets a turn after a failed capture attempt
463
- }
464
-
465
- console.log('📸 Capture attempt:', {
466
- target: targetPiclet.definition.name,
467
- hp: `${currentHp}/${maxHp}`,
468
- status: statusEffect,
469
- catchRate: baseCatchRate,
470
- percentage: capturePercentage.toFixed(1) + '%',
471
- result: result.success ? 'SUCCESS' : 'FAILED',
472
- shakes: result.shakes
473
- });
474
- }
475
-
476
- private getCurrentPicletIndex(executor: 'player' | 'opponent'): number {
477
- const isPlayer = executor === 'player';
478
- const roster = isPlayer ? this.playerRoster : this.opponentRoster;
479
- const currentPiclet = isPlayer ? this.state.playerPiclet : this.state.opponentPiclet;
480
-
481
- return roster.findIndex(piclet => piclet.name === currentPiclet.definition.name);
482
- }
483
-
484
- private savePicletToRoster(piclet: BattlePiclet, executor: 'player' | 'opponent'): void {
485
- const isPlayer = executor === 'player';
486
- const rosterStates = isPlayer ? this.playerRosterStates : this.opponentRosterStates;
487
- const currentIndex = this.getCurrentPicletIndex(executor);
488
-
489
- if (currentIndex !== -1) {
490
- // Save current state back to roster
491
- rosterStates[currentIndex].currentHp = piclet.currentHp;
492
- rosterStates[currentIndex].fainted = piclet.currentHp <= 0;
493
-
494
- // Save current PP state
495
- for (let i = 0; i < piclet.moves.length; i++) {
496
- if (rosterStates[currentIndex].moves[i]) {
497
- rosterStates[currentIndex].moves[i].currentPP = piclet.moves[i].currentPP;
498
- }
499
- }
500
- }
501
- }
502
-
503
- private syncActivePicketToRoster(executor: 'player' | 'opponent'): void {
504
- const piclet = executor === 'player' ? this.state.playerPiclet : this.state.opponentPiclet;
505
- this.savePicletToRoster(piclet, executor);
506
- }
507
-
508
- private loadPicletFromRoster(index: number, executor: 'player' | 'opponent'): BattlePiclet {
509
- const isPlayer = executor === 'player';
510
- const roster = isPlayer ? this.playerRoster : this.opponentRoster;
511
- const rosterStates = isPlayer ? this.playerRosterStates : this.opponentRosterStates;
512
- const level = isPlayer ? this.state.playerPiclet.level : this.state.opponentPiclet.level;
513
-
514
- const definition = roster[index];
515
- const savedState = rosterStates[index];
516
-
517
- // Create fresh battle piclet
518
- const piclet = this.createBattlePiclet(definition, level);
519
-
520
- // Restore saved state
521
- piclet.currentHp = savedState.currentHp;
522
-
523
- // Restore PP
524
- for (let i = 0; i < piclet.moves.length; i++) {
525
- if (savedState.moves[i]) {
526
- piclet.moves[i].currentPP = savedState.moves[i].currentPP;
527
- }
528
- }
529
-
530
- // Reset stat modifications (switching clears temporary stat changes)
531
- piclet.statModifiers = {};
532
-
533
- return piclet;
534
- }
535
-
536
- private triggerOnSwitchIn(piclet: BattlePiclet): void {
537
- this.triggerAbilities('onSwitchIn', piclet);
538
- }
539
-
540
- private triggerOnSwitchOut(piclet: BattlePiclet): void {
541
- this.triggerAbilities('onSwitchOut', piclet);
542
- }
543
-
544
- private checkMoveHits(move: Move, _attacker: BattlePiclet, _defender: BattlePiclet): boolean {
545
- // Simple accuracy check - can be enhanced later
546
- const accuracy = move.accuracy;
547
- const roll = Math.random() * 100;
548
- return roll < accuracy;
549
- }
550
-
551
- private processEffect(effect: BattleEffect, attacker: BattlePiclet, defender: BattlePiclet, move: Move, luckyRoll?: boolean): void {
552
- // Check condition (simplified for now)
553
- if (effect.condition && !this.checkCondition(effect.condition, attacker, defender, luckyRoll)) {
554
- return;
555
- }
556
-
557
- switch (effect.type) {
558
- case 'damage':
559
- console.log('⚔️ Processing damage effect:', {
560
- target: effect.target,
561
- isAll: effect.target === 'all'
562
- });
563
- if (effect.target === 'all') {
564
- // Self-destruct style moves that damage all targets
565
- console.log('💣 All-target damage (self-destruct)');
566
- this.processDamageEffect(effect, attacker, attacker, move); // Self-damage
567
- this.processDamageEffect(effect, attacker, defender, move); // Opponent damage
568
- } else {
569
- const damageTarget = this.resolveTarget(effect.target, attacker, defender);
570
- console.log('🎯 Single target damage:', {
571
- target: effect.target,
572
- resolvedTarget: damageTarget?.definition.name || 'null'
573
- });
574
- if (damageTarget) this.processDamageEffect(effect, attacker, damageTarget, move);
575
- }
576
- break;
577
- case 'modifyStats':
578
- const statsTarget = this.resolveTarget(effect.target, attacker, defender);
579
- if (statsTarget) this.processModifyStatsEffect(effect, statsTarget);
580
- break;
581
- case 'applyStatus':
582
- const statusTarget = this.resolveTarget(effect.target, attacker, defender);
583
- if (statusTarget) this.processApplyStatusEffect(effect, statusTarget);
584
- break;
585
- case 'heal':
586
- const healTarget = this.resolveTarget(effect.target, attacker, defender);
587
- if (healTarget) this.processHealEffect(effect, healTarget);
588
- break;
589
- case 'manipulatePP':
590
- const ppTarget = this.resolveTarget(effect.target, attacker, defender);
591
- if (ppTarget) this.processManipulatePPEffect(effect, ppTarget);
592
- break;
593
- case 'fieldEffect':
594
- this.processFieldEffect(effect);
595
- break;
596
- case 'counter':
597
- this.processCounterEffect(effect, attacker, defender);
598
- break;
599
- case 'priority':
600
- const priorityTarget = this.resolveTarget(effect.target, attacker, defender);
601
- if (priorityTarget) this.processPriorityEffect(effect, priorityTarget);
602
- break;
603
- case 'removeStatus':
604
- const removeStatusTarget = this.resolveTarget(effect.target, attacker, defender);
605
- if (removeStatusTarget) this.processRemoveStatusEffect(effect, removeStatusTarget);
606
- break;
607
- case 'mechanicOverride':
608
- // MechanicOverride effects don't have a target - they apply to the user
609
- this.processMechanicOverrideEffect(effect, attacker);
610
- break;
611
- default:
612
- this.log(`Effect ${(effect as any).type} not implemented yet`);
613
- }
614
- }
615
-
616
- private checkCondition(condition: string, attacker: BattlePiclet, _defender: BattlePiclet, luckyRoll?: boolean): boolean {
617
- switch (condition) {
618
- case 'always':
619
- return true;
620
- case 'ifLowHp':
621
- return attacker.currentHp / attacker.maxHp < 0.25;
622
- case 'ifHighHp':
623
- return attacker.currentHp / attacker.maxHp > 0.75;
624
- case 'ifLucky50':
625
- return luckyRoll !== undefined ? luckyRoll : Math.random() < 0.5;
626
- case 'ifUnlucky50':
627
- return luckyRoll !== undefined ? !luckyRoll : Math.random() >= 0.5;
628
- case 'whileFrozen':
629
- return attacker.statusEffects.includes('freeze');
630
- // Type-specific conditions
631
- case 'ifMoveType:flora':
632
- case 'ifMoveType:space':
633
- case 'ifMoveType:beast':
634
- case 'ifMoveType:bug':
635
- case 'ifMoveType:aquatic':
636
- case 'ifMoveType:mineral':
637
- case 'ifMoveType:machina':
638
- case 'ifMoveType:structure':
639
- case 'ifMoveType:culture':
640
- case 'ifMoveType:cuisine':
641
- case 'ifMoveType:normal':
642
- // Would need move context to check, placeholder for now
643
- return true;
644
- // Status-specific conditions
645
- case 'ifStatus:burn':
646
- return attacker.statusEffects.includes('burn');
647
- case 'ifStatus:freeze':
648
- return attacker.statusEffects.includes('freeze');
649
- case 'ifStatus:paralyze':
650
- return attacker.statusEffects.includes('paralyze');
651
- case 'ifStatus:poison':
652
- return attacker.statusEffects.includes('poison');
653
- case 'ifStatus:sleep':
654
- return attacker.statusEffects.includes('sleep');
655
- case 'ifStatus:confuse':
656
- return attacker.statusEffects.includes('confuse');
657
- // Weather conditions (placeholder)
658
- case 'ifWeather:storm':
659
- case 'ifWeather:rain':
660
- case 'ifWeather:sun':
661
- case 'ifWeather:snow':
662
- return false; // Weather system not implemented yet
663
- // Combat conditions
664
- case 'ifDamagedThisTurn':
665
- // Check if the attacker was damaged this turn
666
- // For now, we'll implement this by checking if currentHp < maxHp
667
- // This is a simplified implementation
668
- return attacker.currentHp < attacker.maxHp;
669
- case 'ifNotSuperEffective':
670
- // Would need move context, placeholder
671
- return false;
672
- case 'ifStatusMove':
673
- // Would need move context, placeholder
674
- return false;
675
- case 'afterUse':
676
- // This condition should be processed after the move's other effects
677
- return true;
678
- default:
679
- return true; // Default to true for unimplemented conditions
680
- }
681
- }
682
-
683
- private resolveTarget(target: string, attacker: BattlePiclet, defender: BattlePiclet): BattlePiclet | null {
684
- console.log('🔍 resolveTarget called:', {
685
- target,
686
- attacker: attacker.definition.name,
687
- defender: defender.definition.name
688
- });
689
-
690
- // Default undefined/null targets to 'opponent' for damage effects
691
- if (target === undefined || target === null) {
692
- console.log('🔧 Undefined target, defaulting to opponent');
693
- target = 'opponent';
694
- }
695
-
696
- switch (target) {
697
- case 'self':
698
- console.log('✅ Resolved to self (attacker)');
699
- return attacker;
700
- case 'opponent':
701
- console.log('✅ Resolved to opponent (defender)');
702
- return defender;
703
- default:
704
- console.log('❌ Unknown target, returning null:', target);
705
- return null; // Multi-target not implemented yet
706
- }
707
- }
708
-
709
- private processDamageEffect(effect: { amount?: DamageAmount; formula?: string; value?: number; multiplier?: number }, attacker: BattlePiclet, target: BattlePiclet, move: Move): void {
710
- console.log('💥 processDamageEffect called:', {
711
- attacker: attacker.definition.name,
712
- target: target.definition.name,
713
- move: move.name,
714
- effect: {
715
- amount: effect.amount,
716
- formula: effect.formula,
717
- value: effect.value,
718
- multiplier: effect.multiplier
719
- }
720
- });
721
- let damage = 0;
722
-
723
- // Check type immunity first
724
- if (this.checkTypeImmunity(target, move.type)) {
725
- this.log(`${target.definition.name} is immune to ${move.type} type moves!`);
726
- return;
727
- }
728
-
729
- // Check flag-based type immunity (like ground immunity via levitate)
730
- if (this.checkFlagBasedTypeImmunity(target, move.flags)) {
731
- this.log(`${target.definition.name} had no effect!`);
732
- return;
733
- }
734
-
735
- // Check flag interactions
736
- const flagInteraction = this.checkFlagInteraction(target, move.flags);
737
- if (flagInteraction === 'immune') {
738
- this.log(`It had no effect on ${target.definition.name}!`);
739
- return;
740
- }
741
-
742
- // Handle different damage formulas
743
- if (effect.formula) {
744
- damage = this.calculateDamageByFormula(effect, attacker, target, move);
745
- } else if (effect.amount) {
746
- damage = this.calculateStandardDamage(effect.amount, attacker, target, move);
747
- }
748
-
749
- // Apply flag interaction modifiers
750
- if (flagInteraction === 'weak') {
751
- damage = Math.floor(damage * 1.5);
752
- this.log("It's super effective!");
753
- } else if (flagInteraction === 'resist') {
754
- damage = Math.floor(damage * 0.5);
755
- this.log("It's not very effective...");
756
- }
757
-
758
- // Apply damage multiplier from abilities
759
- const damageMultiplier = this.getDamageMultiplier(attacker);
760
- damage = Math.floor(damage * damageMultiplier);
761
-
762
- // Apply field effect damage multipliers
763
- const isPlayerAttacking = attacker === this.state.playerPiclet;
764
- const fieldEffectMultiplier = this.getFieldEffectDamageMultiplier(move, isPlayerAttacking);
765
- damage = Math.floor(damage * fieldEffectMultiplier);
766
-
767
- // Check for critical hits
768
- const critMod = this.checkCriticalHitModification(attacker, target);
769
- const isCriticalHit = critMod === 'always' || (critMod === 'normal' && Math.random() < 0.0625);
770
- if (isCriticalHit) { // 1/16 base crit rate
771
- damage = Math.floor(damage * 1.5);
772
- this.log("A critical hit!");
773
- // Trigger critical hit ability
774
- this.triggerOnCriticalHit(attacker, target);
775
- }
776
-
777
- // Apply damage
778
- if (damage > 0) {
779
- target.currentHp = Math.max(0, target.currentHp - damage);
780
- this.log(`${target.definition.name} took ${damage} damage!`);
781
-
782
- // Wake up from sleep when damaged
783
- this.wakeUpFromSleep(target);
784
-
785
- // Trigger ability events
786
- this.triggerOnDamageTaken(target, damage, move.flags.includes('contact'));
787
- this.triggerOnDamageDealt(attacker, damage, target);
788
- this.triggerOnLowHP(target);
789
-
790
- // Check for counter effects on the target
791
- this.checkCounterEffects(target, attacker, move);
792
- }
793
-
794
- // Handle special formula effects
795
- if (effect.formula === 'drain') {
796
- const healAmount = Math.floor(damage * (effect.value || 0.5));
797
- attacker.currentHp = Math.min(attacker.maxHp, attacker.currentHp + healAmount);
798
- if (healAmount > 0) {
799
- this.log(`${attacker.definition.name} recovered ${healAmount} HP from draining!`);
800
- this.triggerOnHPDrained(attacker, target, healAmount);
801
- }
802
- } else if (effect.formula === 'recoil') {
803
- const recoilDamage = Math.floor(damage * (effect.value || 0.25));
804
- attacker.currentHp = Math.max(0, attacker.currentHp - recoilDamage);
805
- if (recoilDamage > 0) {
806
- this.log(`${attacker.definition.name} took ${recoilDamage} recoil damage!`);
807
- }
808
- }
809
- }
810
-
811
- private calculateDamageByFormula(effect: { formula?: string; value?: number; multiplier?: number }, attacker: BattlePiclet, target: BattlePiclet, move: Move): number {
812
- switch (effect.formula) {
813
- case 'fixed':
814
- return effect.value || 0;
815
-
816
- case 'percentage':
817
- return Math.floor(target.maxHp * ((effect.value || 0) / 100));
818
-
819
- case 'recoil':
820
- case 'drain':
821
- case 'standard':
822
- // Use the move's actual power for standard formula
823
- return this.calculateStandardDamageWithPower(move.power, attacker, target, move) * (effect.multiplier || 1);
824
-
825
- default:
826
- return 0;
827
- }
828
- }
829
-
830
- private calculateStandardDamageWithPower(power: number, attacker: BattlePiclet, target: BattlePiclet, move: Move): number {
831
- const baseDamage = power;
832
-
833
- // Debug logging for type effectiveness calculation
834
- console.log('🎯 Damage calculation debug:', {
835
- move: {
836
- name: move.name,
837
- type: move.type,
838
- power: move.power
839
- },
840
- attacker: {
841
- name: attacker.definition.name,
842
- primaryType: attacker.definition.primaryType,
843
- secondaryType: attacker.definition.secondaryType
844
- },
845
- target: {
846
- name: target.definition.name,
847
- primaryType: target.definition.primaryType,
848
- secondaryType: target.definition.secondaryType
849
- }
850
- });
851
-
852
- // Type effectiveness
853
- const effectiveness = getEffectivenessMultiplier(
854
- move.type,
855
- target.definition.primaryType,
856
- target.definition.secondaryType
857
- );
858
-
859
- // STAB (Same Type Attack Bonus) - compare enum values as strings
860
- const stab = (move.type.toString() === attacker.definition.primaryType?.toString() ||
861
- move.type.toString() === attacker.definition.secondaryType?.toString()) ? 1.5 : 1;
862
-
863
- // Pokemon-style damage calculation for better balance
864
- const attackStat = attacker.attack;
865
- const defenseStat = target.defense;
866
- const level = attacker.level;
867
-
868
- // Core damage formula: ((2 * Level + 10) / 250) * (Attack / Defense) * Power + 2
869
- let damage = Math.floor(
870
- ((2 * level + 10) / 250) * (attackStat / defenseStat) * baseDamage + 2
871
- );
872
- damage = Math.floor(damage * effectiveness * stab);
873
-
874
- // Random factor (85-100%)
875
- damage = Math.floor(damage * (0.85 + Math.random() * 0.15));
876
-
877
- // Minimum 1 damage for effective moves
878
- if (effectiveness > 0 && damage < 1) {
879
- damage = 1;
880
- }
881
-
882
- // Log effectiveness messages
883
- if (effectiveness === 0) {
884
- this.log("It had no effect!");
885
- } else if (effectiveness > 1) {
886
- this.log("It's super effective!");
887
- } else if (effectiveness < 1) {
888
- this.log("It's not very effective...");
889
- }
890
-
891
- return damage;
892
- }
893
-
894
- private calculateStandardDamage(amount: DamageAmount, attacker: BattlePiclet, target: BattlePiclet, move: Move): number {
895
- const baseDamage = this.getDamageAmount(amount);
896
-
897
- // Type effectiveness
898
- const effectiveness = getEffectivenessMultiplier(
899
- move.type,
900
- target.definition.primaryType,
901
- target.definition.secondaryType
902
- );
903
-
904
- // STAB (Same Type Attack Bonus) - compare enum values as strings
905
- const stab = (move.type.toString() === attacker.definition.primaryType?.toString() ||
906
- move.type.toString() === attacker.definition.secondaryType?.toString()) ? 1.5 : 1;
907
-
908
- // Pokemon-style damage calculation for better balance
909
- const attackStat = attacker.attack;
910
- const defenseStat = target.defense;
911
- const level = attacker.level;
912
-
913
- // Core damage formula: ((2 * Level + 10) / 250) * (Attack / Defense) * Power + 2
914
- let damage = Math.floor(
915
- ((2 * level + 10) / 250) * (attackStat / defenseStat) * baseDamage + 2
916
- );
917
- damage = Math.floor(damage * effectiveness * stab);
918
-
919
- // Random factor (85-100%)
920
- damage = Math.floor(damage * (0.85 + Math.random() * 0.15));
921
-
922
- // Minimum 1 damage for effective moves
923
- if (effectiveness > 0 && damage < 1) {
924
- damage = 1;
925
- }
926
-
927
- // Log effectiveness messages
928
- if (effectiveness === 0) {
929
- this.log("It had no effect!");
930
- } else if (effectiveness > 1) {
931
- this.log("It's super effective!");
932
- } else if (effectiveness < 1) {
933
- this.log("It's not very effective...");
934
- }
935
-
936
- return damage;
937
- }
938
-
939
- private processModifyStatsEffect(effect: { stats: Partial<Record<keyof BaseStats | 'accuracy', StatModification>> }, target: BattlePiclet): void {
940
- for (const [stat, modification] of Object.entries(effect.stats)) {
941
- const multiplier = this.getStatModifier(modification);
942
- if (stat === 'accuracy') {
943
- target.accuracy = Math.floor(target.accuracy * multiplier);
944
- } else {
945
- const statKey = stat as keyof BaseStats;
946
- (target as any)[statKey] = Math.floor((target as any)[statKey] * multiplier);
947
- }
948
-
949
- const changeType = modification.includes('increase') ? 'increase' : 'decrease';
950
- this.log(`${target.definition.name}'s ${stat} ${modification.includes('increase') ? 'rose' : 'fell'}!`);
951
- this.triggerOnStatChange(target, stat, changeType);
952
- }
953
- }
954
-
955
-
956
- private processHealEffect(effect: { amount?: HealAmount; formula?: string; value?: number }, target: BattlePiclet): void {
957
- let healAmount = 0;
958
-
959
- if (effect.formula) {
960
- switch (effect.formula) {
961
- case 'percentage':
962
- healAmount = Math.floor(target.maxHp * ((effect.value || 0) / 100));
963
- break;
964
- case 'fixed':
965
- healAmount = effect.value || 0;
966
- break;
967
- default:
968
- healAmount = this.getHealAmount(effect.amount || 'medium', target.maxHp);
969
- }
970
- } else if (effect.amount) {
971
- healAmount = this.getHealAmount(effect.amount, target.maxHp);
972
- }
973
-
974
- // Check for healing inversion mechanic
975
- if (this.shouldInvertHealing(target)) {
976
- // Healing becomes damage
977
- const oldHp = target.currentHp;
978
- target.currentHp = Math.max(0, target.currentHp - healAmount);
979
- const actualDamage = oldHp - target.currentHp;
980
-
981
- if (actualDamage > 0) {
982
- this.log(`${target.definition.name} took ${actualDamage} damage from inverted healing!`);
983
- }
984
- } else {
985
- // Normal healing
986
- const oldHp = target.currentHp;
987
- target.currentHp = Math.min(target.maxHp, target.currentHp + healAmount);
988
- const actualHeal = target.currentHp - oldHp;
989
-
990
- if (actualHeal > 0) {
991
- this.log(`${target.definition.name} recovered ${actualHeal} HP!`);
992
- this.triggerOnFullHP(target);
993
- }
994
- }
995
- }
996
-
997
- private getDamageAmount(amount: DamageAmount): number {
998
- switch (amount) {
999
- case 'weak': return 40;
1000
- case 'normal': return 70;
1001
- case 'strong': return 100;
1002
- case 'extreme': return 140;
1003
- default: return 70;
1004
- }
1005
- }
1006
-
1007
- private getStatModifier(modification: StatModification): number {
1008
- switch (modification) {
1009
- case 'increase': return 1.25;
1010
- case 'decrease': return 0.75;
1011
- case 'greatly_increase': return 1.5;
1012
- case 'greatly_decrease': return 0.5;
1013
- default: return 1.0;
1014
- }
1015
- }
1016
-
1017
- private getHealAmount(amount: HealAmount, maxHp: number): number {
1018
- switch (amount) {
1019
- case 'small': return Math.floor(maxHp * 0.25);
1020
- case 'medium': return Math.floor(maxHp * 0.5);
1021
- case 'large': return Math.floor(maxHp * 0.75);
1022
- case 'full': return maxHp;
1023
- default: return Math.floor(maxHp * 0.5);
1024
- }
1025
- }
1026
-
1027
- private processTurnEnd(): void {
1028
- // Process status effects
1029
- this.processStatusEffects(this.state.playerPiclet);
1030
- this.processStatusEffects(this.state.opponentPiclet);
1031
-
1032
- // Apply field healing effects
1033
- this.applyFieldHealingEffects();
1034
-
1035
- // Process field effects (duration management)
1036
- this.processFieldEffects();
1037
-
1038
- // Trigger end of turn abilities
1039
- this.triggerEndOfTurn();
1040
-
1041
- // Decrement temporary effects
1042
- this.processTemporaryEffects(this.state.playerPiclet);
1043
- this.processTemporaryEffects(this.state.opponentPiclet);
1044
- }
1045
-
1046
- private processStatusEffects(piclet: BattlePiclet): void {
1047
- // Process status effects that trigger at end of turn
1048
- const statusesToRemove: string[] = [];
1049
-
1050
- for (let i = 0; i < piclet.statusEffects.length; i++) {
1051
- const status = piclet.statusEffects[i];
1052
-
1053
- switch (status) {
1054
- case 'burn':
1055
- case 'poison':
1056
- const damage = Math.floor(piclet.maxHp / 8);
1057
- piclet.currentHp = Math.max(0, piclet.currentHp - damage);
1058
- this.log(`${piclet.definition.name} was hurt by ${status}!`);
1059
- break;
1060
-
1061
- case 'freeze':
1062
- // Don't process freeze on the turn it was applied
1063
- if ((piclet as any).freezeJustApplied) {
1064
- delete (piclet as any).freezeJustApplied;
1065
- } else {
1066
- // 20% chance to thaw out each turn
1067
- if (Math.random() < 0.2) {
1068
- statusesToRemove.push(status);
1069
- this.log(`${piclet.definition.name} thawed out!`);
1070
- }
1071
- }
1072
- break;
1073
-
1074
- case 'sleep':
1075
- // Don't process sleep on the turn it was applied
1076
- if ((piclet as any).sleepJustApplied) {
1077
- delete (piclet as any).sleepJustApplied;
1078
- } else {
1079
- // Decrement sleep turns and wake up
1080
- const sleepTurns = (piclet as any).sleepTurns || 0;
1081
- if (sleepTurns <= 1) {
1082
- statusesToRemove.push(status);
1083
- this.log(`${piclet.definition.name} woke up!`);
1084
- delete (piclet as any).sleepTurns;
1085
- } else {
1086
- (piclet as any).sleepTurns = sleepTurns - 1;
1087
- }
1088
- }
1089
- break;
1090
-
1091
- case 'confuse':
1092
- // Decrement confusion turns
1093
- const confusionTurns = (piclet as any).confusionTurns || 0;
1094
- if (confusionTurns <= 1) {
1095
- statusesToRemove.push(status);
1096
- this.log(`${piclet.definition.name} snapped out of confusion!`);
1097
- delete (piclet as any).confusionTurns;
1098
- } else {
1099
- (piclet as any).confusionTurns = confusionTurns - 1;
1100
- }
1101
- break;
1102
- }
1103
- }
1104
-
1105
- // Remove statuses that expired
1106
- for (const statusToRemove of statusesToRemove) {
1107
- const index = piclet.statusEffects.indexOf(statusToRemove as any);
1108
- if (index > -1) {
1109
- piclet.statusEffects.splice(index, 1);
1110
- }
1111
- }
1112
- }
1113
-
1114
- private processTemporaryEffects(piclet: BattlePiclet): void {
1115
- // Decrement duration of temporary effects
1116
- piclet.temporaryEffects = piclet.temporaryEffects.filter(effect => {
1117
- effect.duration--;
1118
- return effect.duration > 0;
1119
- });
1120
- }
1121
-
1122
- private processFieldEffects(): void {
1123
- // Field effects are processed at end of turn for duration management
1124
- // Their actual mechanics are applied during relevant battle phases
1125
-
1126
- // Decrement field effect durations and remove expired ones
1127
- this.state.fieldEffects = this.state.fieldEffects.filter(effect => {
1128
- effect.duration--;
1129
- if (effect.duration <= 0) {
1130
- this.log(`${this.formatFieldEffectName(effect.name)} faded away!`);
1131
- return false;
1132
- }
1133
- return true;
1134
- });
1135
- }
1136
-
1137
- private formatFieldEffectName(effectName: string): string {
1138
- switch (effectName) {
1139
- case 'entryHazardSpikes': return 'Entry spikes';
1140
- case 'contactDamageReduction': return 'Contact damage barrier';
1141
- case 'nonContactDamageReduction': return 'Non-contact damage barrier';
1142
- case 'healingField': return 'Healing field';
1143
- case 'poisonousField': return 'Poisonous field';
1144
- default: return effectName;
1145
- }
1146
- }
1147
-
1148
- private getFieldEffectDamageMultiplier(move: Move, isPlayerAttacking: boolean): number {
1149
- let multiplier = 1.0;
1150
-
1151
- // Determine if this is a contact move
1152
- const isContactMove = move.flags.includes('contact');
1153
-
1154
- // Check field effects that modify damage
1155
- for (const fieldEffect of this.state.fieldEffects) {
1156
- const targetSide = fieldEffect.effect.target;
1157
- // Field effects protect the side they're applied to from incoming attacks
1158
- // So if playerSide has a barrier, it protects player from opponent attacks
1159
- const protectsDefender = (!isPlayerAttacking && targetSide === 'playerSide') ||
1160
- (isPlayerAttacking && targetSide === 'opponentSide');
1161
-
1162
- if (!protectsDefender) continue;
1163
-
1164
- switch (fieldEffect.name) {
1165
- case 'contactDamageReduction':
1166
- if (isContactMove) {
1167
- multiplier *= 0.5; // Reduce contact move damage by 50%
1168
- }
1169
- break;
1170
- case 'nonContactDamageReduction':
1171
- if (!isContactMove) {
1172
- multiplier *= 0.5; // Reduce non-contact move damage by 50%
1173
- }
1174
- break;
1175
- }
1176
- }
1177
-
1178
- return multiplier;
1179
- }
1180
-
1181
- private applyEntryHazards(piclet: BattlePiclet): void {
1182
- // Apply entry hazards when a piclet enters battle (switching mechanics)
1183
- for (const fieldEffect of this.state.fieldEffects) {
1184
- if (fieldEffect.name === 'spikes' || fieldEffect.name === 'entryHazardSpikes') {
1185
- const targetSide = fieldEffect.effect.target;
1186
- const isPlayerSide = piclet === this.state.playerPiclet;
1187
- const appliesTo = (isPlayerSide && targetSide === 'playerSide') ||
1188
- (!isPlayerSide && targetSide === 'opponentSide');
1189
-
1190
- if (appliesTo) {
1191
- const damage = Math.floor(piclet.maxHp * 0.125); // 12.5% max HP damage
1192
- piclet.currentHp = Math.max(0, piclet.currentHp - damage);
1193
- this.log(`${piclet.definition.name} was hurt by spikes!`);
1194
- }
1195
- } else if (fieldEffect.name === 'toxicSpikes') {
1196
- const targetSide = fieldEffect.effect.target;
1197
- const isPlayerSide = piclet === this.state.playerPiclet;
1198
- const appliesTo = (isPlayerSide && targetSide === 'playerSide') ||
1199
- (!isPlayerSide && targetSide === 'opponentSide');
1200
-
1201
- if (appliesTo && !piclet.statusEffects.includes('poison')) {
1202
- piclet.statusEffects.push('poison');
1203
- this.log(`${piclet.definition.name} was poisoned by toxic spikes!`);
1204
- }
1205
- } else if (fieldEffect.name === 'poisonousField') {
1206
- const targetSide = fieldEffect.effect.target;
1207
- const isPlayerSide = piclet === this.state.playerPiclet;
1208
- const appliesTo = (isPlayerSide && targetSide === 'playerSide') ||
1209
- (!isPlayerSide && targetSide === 'opponentSide');
1210
-
1211
- if (appliesTo && !piclet.statusEffects.includes('poison')) {
1212
- piclet.statusEffects.push('poison');
1213
- this.log(`${piclet.definition.name} was poisoned by toxic spikes!`);
1214
- }
1215
- }
1216
- }
1217
- }
1218
-
1219
- private applyFieldHealingEffects(): void {
1220
- // Apply healing field effects at end of turn
1221
- const healingFields = this.state.fieldEffects.filter(effect => effect.name === 'healingField');
1222
-
1223
- for (const healingField of healingFields) {
1224
- const targetSide = healingField.effect.target;
1225
-
1226
- if (targetSide === 'playerSide' || targetSide === 'field') {
1227
- const healAmount = Math.floor(this.state.playerPiclet.maxHp * 0.0625); // 6.25% max HP
1228
- if (this.state.playerPiclet.currentHp < this.state.playerPiclet.maxHp) {
1229
- this.state.playerPiclet.currentHp = Math.min(
1230
- this.state.playerPiclet.maxHp,
1231
- this.state.playerPiclet.currentHp + healAmount
1232
- );
1233
- this.log(`${this.state.playerPiclet.definition.name} was healed by the healing field!`);
1234
- }
1235
- }
1236
-
1237
- if (targetSide === 'opponentSide' || targetSide === 'field') {
1238
- const healAmount = Math.floor(this.state.opponentPiclet.maxHp * 0.0625); // 6.25% max HP
1239
- if (this.state.opponentPiclet.currentHp < this.state.opponentPiclet.maxHp) {
1240
- this.state.opponentPiclet.currentHp = Math.min(
1241
- this.state.opponentPiclet.maxHp,
1242
- this.state.opponentPiclet.currentHp + healAmount
1243
- );
1244
- this.log(`${this.state.opponentPiclet.definition.name} was healed by the healing field!`);
1245
- }
1246
- }
1247
- }
1248
- }
1249
-
1250
-
1251
- private checkBattleEnd(): void {
1252
- const playerFainted = this.state.playerPiclet.currentHp <= 0;
1253
- const opponentFainted = this.state.opponentPiclet.currentHp <= 0;
1254
-
1255
- // Mark fainted piclets in roster states and trigger KO events
1256
- if (playerFainted) {
1257
- const playerIndex = this.getCurrentPicletIndex('player');
1258
- if (playerIndex !== -1) {
1259
- this.playerRosterStates[playerIndex].fainted = true;
1260
- this.triggerOnKO(this.state.playerPiclet, this.state.opponentPiclet);
1261
- }
1262
- }
1263
-
1264
- if (opponentFainted) {
1265
- const opponentIndex = this.getCurrentPicletIndex('opponent');
1266
- if (opponentIndex !== -1) {
1267
- this.opponentRosterStates[opponentIndex].fainted = true;
1268
- this.triggerOnKO(this.state.opponentPiclet, this.state.playerPiclet);
1269
- }
1270
- }
1271
-
1272
- // Check if any viable piclets remain
1273
- const playerHasViablePiclets = this.playerRosterStates.some(state => !state.fainted);
1274
- const opponentHasViablePiclets = this.opponentRosterStates.some(state => !state.fainted);
1275
-
1276
- if (!playerHasViablePiclets && !opponentHasViablePiclets) {
1277
- this.state.winner = 'draw';
1278
- this.state.phase = 'ended';
1279
- this.log('Battle ended in a draw!');
1280
- } else if (!playerHasViablePiclets) {
1281
- this.state.winner = 'opponent';
1282
- this.state.phase = 'ended';
1283
- // No win message - handled in UI
1284
- } else if (!opponentHasViablePiclets) {
1285
- this.state.winner = 'player';
1286
- this.state.phase = 'ended';
1287
- // No win message - handled in UI
1288
- } else if (playerFainted || opponentFainted) {
1289
- // Handle forced switching - at least one piclet fainted but viable alternatives exist
1290
- this.handleForcedSwitching(playerFainted, opponentFainted);
1291
- }
1292
- }
1293
-
1294
- private handleForcedSwitching(playerFainted: boolean, opponentFainted: boolean): void {
1295
- if (playerFainted) {
1296
- this.log(`${this.state.playerPiclet.definition.name} fainted!`);
1297
- const viablePiclets = this.playerRosterStates.map((state, index) => ({ index, state }))
1298
- .filter(entry => !entry.state.fainted);
1299
-
1300
- if (viablePiclets.length === 1) {
1301
- // Auto-switch to the only remaining piclet
1302
- const autoSwitchIndex = viablePiclets[0].index;
1303
- this.log(`Player must choose a new piclet! Auto-switching to ${this.playerRoster[autoSwitchIndex].name}!`);
1304
- this.executeSwitch({ type: 'switch', piclet: 'player', newPicletIndex: autoSwitchIndex, executor: 'player' });
1305
- } else {
1306
- this.log(`Player must choose a new piclet from ${viablePiclets.length} remaining options!`);
1307
- // In a real implementation, this would pause and wait for player input
1308
- // For testing, we can simulate choosing the first available
1309
- }
1310
- }
1311
-
1312
- if (opponentFainted) {
1313
- this.log(`${this.state.opponentPiclet.definition.name} fainted!`);
1314
- const viablePiclets = this.opponentRosterStates.map((state, index) => ({ index, state }))
1315
- .filter(entry => !entry.state.fainted);
1316
-
1317
- if (viablePiclets.length === 1) {
1318
- // Auto-switch to the only remaining piclet
1319
- const autoSwitchIndex = viablePiclets[0].index;
1320
- this.log(`Opponent must choose a new piclet! Auto-switching to ${this.opponentRoster[autoSwitchIndex].name}!`);
1321
- this.executeSwitch({ type: 'switch', piclet: 'opponent', newPicletIndex: autoSwitchIndex, executor: 'opponent' });
1322
- } else {
1323
- this.log(`Opponent must choose a new piclet from ${viablePiclets.length} remaining options!`);
1324
- // In a real implementation, this would be AI logic
1325
- }
1326
- }
1327
- }
1328
-
1329
- private log(message: string): void {
1330
- this.state.log.push(message);
1331
- }
1332
-
1333
- // Public method to get battle log
1334
- public getLog(): string[] {
1335
- // Strip battle prefixes from all log messages for display
1336
- return this.state.log.map(message => this.stripBattlePrefixes(message));
1337
- }
1338
-
1339
- private stripBattlePrefixes(message: string): string {
1340
- // Remove player- and enemy- prefixes from messages
1341
- return message
1342
- .replace(/player-/g, '')
1343
- .replace(/enemy-/g, '');
1344
- }
1345
-
1346
- // Additional effect processors for advanced features
1347
- private processManipulatePPEffect(effect: { action: string; amount?: string; value?: number; targetMove?: string }, target: BattlePiclet): void {
1348
- const ppChange = this.getPPAmount(effect.amount, effect.value || 5);
1349
-
1350
- switch (effect.action) {
1351
- case 'drain':
1352
- // Drain PP from target's moves
1353
- for (const moveSlot of target.moves) {
1354
- if (moveSlot.currentPP > 0) {
1355
- const drained = Math.min(moveSlot.currentPP, ppChange);
1356
- moveSlot.currentPP -= drained;
1357
- this.log(`${target.definition.name}'s PP was drained from ${moveSlot.move.name}!`);
1358
- break; // Only drain from first move with PP
1359
- }
1360
- }
1361
- break;
1362
- case 'restore':
1363
- // Restore PP to target's moves
1364
- for (const moveSlot of target.moves) {
1365
- if (moveSlot.currentPP < moveSlot.move.pp) {
1366
- const restored = Math.min(moveSlot.move.pp - moveSlot.currentPP, ppChange);
1367
- moveSlot.currentPP += restored;
1368
- this.log(`${target.definition.name}'s PP was restored to ${moveSlot.move.name}!`);
1369
- break; // Only restore to first move that needs PP
1370
- }
1371
- }
1372
- break;
1373
- case 'disable':
1374
- // Disable a move by setting its PP to 0
1375
- for (const moveSlot of target.moves) {
1376
- if (moveSlot.currentPP > 0) {
1377
- moveSlot.currentPP = 0;
1378
- this.log(`${target.definition.name}'s ${moveSlot.move.name} was disabled!`);
1379
- break; // Only disable first available move
1380
- }
1381
- }
1382
- break;
1383
- }
1384
- }
1385
-
1386
- private getPPAmount(amount?: string, value?: number): number {
1387
- if (value !== undefined) return value;
1388
- switch (amount) {
1389
- case 'small': return 3;
1390
- case 'medium': return 5;
1391
- case 'large': return 8;
1392
- default: return 5;
1393
- }
1394
- }
1395
-
1396
- private processFieldEffect(effect: { effect: string; target: string; stackable?: boolean }): void {
1397
- // Map old effect names to new descriptive names
1398
- const effectNameMap: Record<string, string> = {
1399
- 'spikes': 'entryHazardSpikes',
1400
- 'reflect': 'contactDamageReduction',
1401
- 'lightScreen': 'nonContactDamageReduction',
1402
- 'healingMist': 'healingField',
1403
- 'toxicSpikes': 'poisonousField'
1404
- };
1405
-
1406
- const mappedName = effectNameMap[effect.effect] || effect.effect;
1407
-
1408
- // Add field effect to battle state
1409
- const fieldEffect = {
1410
- name: mappedName,
1411
- duration: 5, // Default duration
1412
- effect: effect
1413
- };
1414
-
1415
- // Check if effect already exists and is not stackable
1416
- if (!effect.stackable) {
1417
- this.state.fieldEffects = this.state.fieldEffects.filter(fe => fe.name !== mappedName);
1418
- }
1419
-
1420
- this.state.fieldEffects.push(fieldEffect);
1421
-
1422
- // Log effect application with clear descriptions
1423
- switch (mappedName) {
1424
- case 'entryHazardSpikes':
1425
- this.log('Entry spikes were scattered on the battlefield!');
1426
- break;
1427
- case 'contactDamageReduction':
1428
- this.log('A barrier was raised to reduce contact move damage!');
1429
- break;
1430
- case 'nonContactDamageReduction':
1431
- this.log('A barrier was raised to reduce non-contact move damage!');
1432
- break;
1433
- case 'healingField':
1434
- this.log('A healing field was created!');
1435
- break;
1436
- case 'poisonousField':
1437
- this.log('A poisonous field was created!');
1438
- break;
1439
- default:
1440
- this.log(`${mappedName} was applied to the field!`);
1441
- }
1442
- }
1443
-
1444
- private processCounterEffect(effect: { strength: string }, attacker: BattlePiclet, _target: BattlePiclet): void {
1445
- // Store counter effect for later processing when the user is attacked
1446
- // Counter effects should persist until triggered, not expire after 1 turn
1447
- attacker.temporaryEffects.push({
1448
- effect: {
1449
- type: 'counter',
1450
- strength: effect.strength
1451
- } as any,
1452
- duration: 5 // Persist for multiple turns until triggered
1453
- });
1454
- this.log(`${attacker.definition.name} is preparing to counter!`);
1455
- }
1456
-
1457
- private processPriorityEffect(effect: { value: number; condition?: string }, target: BattlePiclet): void {
1458
- // Store priority modification for next move
1459
- target.statModifiers.priority = (target.statModifiers.priority || 0) + effect.value;
1460
- this.log(`${target.definition.name}'s move priority changed by ${effect.value}!`);
1461
- }
1462
-
1463
- private processRemoveStatusEffect(effect: { status: string }, target: BattlePiclet): void {
1464
- if (target.statusEffects.includes(effect.status as any)) {
1465
- target.statusEffects = target.statusEffects.filter(s => s !== effect.status);
1466
- this.log(`${target.definition.name} was cured of ${effect.status}!`);
1467
- }
1468
- }
1469
-
1470
- private processMechanicOverrideEffect(effect: { mechanic: string; value: any; condition?: string }, target: BattlePiclet): void {
1471
- // Store mechanic override as temporary effect for processing during relevant calculations
1472
- target.temporaryEffects.push({
1473
- effect: {
1474
- type: 'mechanicOverride',
1475
- mechanic: effect.mechanic,
1476
- value: effect.value,
1477
- condition: effect.condition,
1478
- target: 'self'
1479
- } as any,
1480
- duration: effect.condition === 'restOfBattle' ? 999 : 1
1481
- });
1482
-
1483
- this.log(`Mechanic override '${effect.mechanic}' applied to ${target.definition.name}!`);
1484
- }
1485
-
1486
- // Helper methods for checking mechanic overrides
1487
- private hasMechanicOverride(piclet: BattlePiclet, mechanic: string): any {
1488
- const override = piclet.temporaryEffects.find(
1489
- effect => effect.effect.type === 'mechanicOverride' &&
1490
- (effect.effect as any).mechanic === mechanic
1491
- );
1492
- return override ? (override.effect as any).value : null;
1493
- }
1494
-
1495
- private checkCriticalHitModification(attacker: BattlePiclet, target: BattlePiclet): 'always' | 'never' | 'normal' {
1496
- // Check attacker's critical hit modifiers
1497
- const attackerOverride = this.hasMechanicOverride(attacker, 'criticalHits');
1498
- if (attackerOverride === true) return 'always';
1499
-
1500
- // Check target's critical hit immunity
1501
- const targetOverride = this.hasMechanicOverride(target, 'criticalHits');
1502
- if (targetOverride === false) return 'never';
1503
-
1504
- return 'normal';
1505
- }
1506
-
1507
- private checkStatusImmunity(target: BattlePiclet, status: string): boolean {
1508
- const immunity = this.hasMechanicOverride(target, 'statusImmunity');
1509
- if (Array.isArray(immunity)) {
1510
- return immunity.includes(status);
1511
- }
1512
- return false;
1513
- }
1514
-
1515
- private checkTypeImmunity(target: BattlePiclet, attackType: string): boolean {
1516
- const immunity = this.hasMechanicOverride(target, 'typeImmunity');
1517
- if (Array.isArray(immunity)) {
1518
- return immunity.includes(attackType);
1519
- }
1520
- return false;
1521
- }
1522
-
1523
- private checkFlagBasedTypeImmunity(target: BattlePiclet, flags: string[]): boolean {
1524
- const immunity = this.hasMechanicOverride(target, 'typeImmunity');
1525
- if (Array.isArray(immunity)) {
1526
- // Check if any of the move's flags match the type immunity
1527
- return flags.some(flag => immunity.includes(flag));
1528
- }
1529
- return false;
1530
- }
1531
-
1532
- private checkFlagInteraction(target: BattlePiclet, flags: string[]): 'immune' | 'weak' | 'resist' | 'normal' {
1533
- // Check immunities first
1534
- const immunity = this.hasMechanicOverride(target, 'flagImmunity');
1535
- if (Array.isArray(immunity) && flags.some(flag => immunity.includes(flag))) {
1536
- return 'immune';
1537
- }
1538
-
1539
- // Check weaknesses
1540
- const weakness = this.hasMechanicOverride(target, 'flagWeakness');
1541
- if (Array.isArray(weakness) && flags.some(flag => weakness.includes(flag))) {
1542
- return 'weak';
1543
- }
1544
-
1545
- // Check resistances
1546
- const resistance = this.hasMechanicOverride(target, 'flagResistance');
1547
- if (Array.isArray(resistance) && flags.some(flag => resistance.includes(flag))) {
1548
- return 'resist';
1549
- }
1550
-
1551
- return 'normal';
1552
- }
1553
-
1554
- private getDamageMultiplier(piclet: BattlePiclet): number {
1555
- const multiplier = this.hasMechanicOverride(piclet, 'damageMultiplier');
1556
- return typeof multiplier === 'number' ? multiplier : 1.0;
1557
- }
1558
-
1559
- private shouldInvertHealing(target: BattlePiclet): boolean {
1560
- return !!this.hasMechanicOverride(target, 'healingInversion');
1561
- }
1562
-
1563
- private applyEffectToPiclet(effect: BattleEffect, piclet: BattlePiclet): void {
1564
- switch (effect.type) {
1565
- case 'modifyStats':
1566
- // Apply permanent stat modifications from abilities
1567
- for (const [stat, modification] of Object.entries(effect.stats)) {
1568
- const multiplier = this.getStatModifier(modification);
1569
- if (stat === 'accuracy') {
1570
- piclet.accuracy = Math.floor(piclet.accuracy * multiplier);
1571
- } else {
1572
- const statKey = stat as keyof BaseStats;
1573
- (piclet as any)[statKey] = Math.floor((piclet as any)[statKey] * multiplier);
1574
- }
1575
- }
1576
- break;
1577
- case 'mechanicOverride':
1578
- // Store mechanic overrides as permanent effects
1579
- piclet.temporaryEffects.push({
1580
- effect: effect,
1581
- duration: 999 // Permanent ability effect
1582
- });
1583
- break;
1584
- // Other effects are handled during battle
1585
- }
1586
- }
1587
-
1588
- private checkCounterEffects(target: BattlePiclet, attacker: BattlePiclet, move: Move): void {
1589
- // Check if the target has any counter effects ready
1590
- for (let i = target.temporaryEffects.length - 1; i >= 0; i--) {
1591
- const tempEffect = target.temporaryEffects[i];
1592
- if (tempEffect.effect.type === 'counter') {
1593
- const counterEffect = tempEffect.effect as any;
1594
- const shouldCounter = true; // All counters now work against any attack type
1595
-
1596
- if (shouldCounter) {
1597
- // Calculate counter damage
1598
- let counterDamage = 0;
1599
- switch (counterEffect.strength) {
1600
- case 'weak': counterDamage = 20; break;
1601
- case 'normal': counterDamage = 40; break;
1602
- case 'strong': counterDamage = 60; break;
1603
- default: counterDamage = 40;
1604
- }
1605
-
1606
- attacker.currentHp = Math.max(0, attacker.currentHp - counterDamage);
1607
- this.log(`${target.definition.name} countered with ${counterDamage} damage!`);
1608
-
1609
- // Remove the counter effect after it triggers
1610
- target.temporaryEffects.splice(i, 1);
1611
- }
1612
- }
1613
- }
1614
- }
1615
-
1616
- // Advanced Status Effect Checks
1617
- private canPicletAct(piclet: BattlePiclet): boolean {
1618
- // Check status effects that prevent action
1619
- for (const status of piclet.statusEffects) {
1620
- switch (status) {
1621
- case 'freeze':
1622
- this.log(`${piclet.definition.name} is frozen solid and cannot move!`);
1623
- return false;
1624
-
1625
- case 'sleep':
1626
- this.log(`${piclet.definition.name} is fast asleep and cannot wake up!`);
1627
- return false;
1628
-
1629
- case 'paralyze':
1630
- // 25% chance to be fully paralyzed
1631
- if (Math.random() < 0.25) {
1632
- this.log(`${piclet.definition.name} is fully paralyzed and cannot move!`);
1633
- return false;
1634
- }
1635
- break;
1636
-
1637
- case 'confuse':
1638
- // 33% chance to hurt self in confusion
1639
- if (Math.random() < 0.33) {
1640
- const selfDamage = Math.floor(piclet.maxHp * 0.125); // 12.5% max HP
1641
- piclet.currentHp = Math.max(0, piclet.currentHp - selfDamage);
1642
- this.log(`${piclet.definition.name} hurt itself in confusion for ${selfDamage} damage!`);
1643
- return false;
1644
- }
1645
- break;
1646
- }
1647
- }
1648
- return true;
1649
- }
1650
-
1651
- // Enhanced Status Application
1652
- private processApplyStatusEffect(effect: { status: StatusEffect; chance?: number }, target: BattlePiclet): void {
1653
- // Check chance if specified
1654
- if (effect.chance !== undefined) {
1655
- const roll = Math.random() * 100;
1656
- if (roll >= effect.chance) {
1657
- return; // Status effect failed to apply
1658
- }
1659
- }
1660
-
1661
- // Check for status immunity
1662
- if (this.checkStatusImmunity(target, effect.status)) {
1663
- this.log(`${target.definition.name} is immune to ${effect.status}!`);
1664
- return;
1665
- }
1666
-
1667
- // Check for major status conflicts (freeze, paralyze, sleep are mutually exclusive)
1668
- const majorStatuses = ['freeze', 'paralyze', 'sleep'];
1669
- if (majorStatuses.includes(effect.status)) {
1670
- const hasMajorStatus = target.statusEffects.some(status => majorStatuses.includes(status));
1671
- if (hasMajorStatus) {
1672
- this.log(`${target.definition.name} is already affected by a major status condition!`);
1673
- return;
1674
- }
1675
- }
1676
-
1677
- if (!target.statusEffects.includes(effect.status)) {
1678
- target.statusEffects.push(effect.status);
1679
-
1680
- // Trigger status inflicted event
1681
- this.triggerOnStatusInflicted(target, effect.status);
1682
-
1683
- // Apply immediate effects and set durations
1684
- switch (effect.status) {
1685
- case 'freeze':
1686
- this.log(`${target.definition.name} was frozen solid!`);
1687
- // Mark as just applied to prevent immediate thawing
1688
- (target as any).freezeJustApplied = true;
1689
- break;
1690
- case 'paralyze':
1691
- this.log(`${target.definition.name} was paralyzed!`);
1692
- // Reduce speed by 50%
1693
- target.speed = Math.floor(target.speed * 0.5);
1694
- break;
1695
- case 'sleep':
1696
- this.log(`${target.definition.name} fell asleep!`);
1697
- // Sleep lasts 1-3 turns
1698
- (target as any).sleepTurns = 1 + Math.floor(Math.random() * 3);
1699
- (target as any).sleepJustApplied = true;
1700
- break;
1701
- case 'confuse':
1702
- this.log(`${target.definition.name} became confused!`);
1703
- // Confusion lasts 2-5 turns
1704
- (target as any).confusionTurns = 2 + Math.floor(Math.random() * 4);
1705
- break;
1706
- default:
1707
- this.log(`${target.definition.name} was ${effect.status}ed!`);
1708
- }
1709
- }
1710
- }
1711
-
1712
- // Wake up from sleep when damaged
1713
- private wakeUpFromSleep(target: BattlePiclet): void {
1714
- if (target.statusEffects.includes('sleep')) {
1715
- const sleepIndex = target.statusEffects.indexOf('sleep');
1716
- if (sleepIndex > -1) {
1717
- target.statusEffects.splice(sleepIndex, 1);
1718
- this.log(`${target.definition.name} woke up from the attack!`);
1719
- delete (target as any).sleepTurns;
1720
- }
1721
- }
1722
- }
1723
-
1724
- // Ability Trigger System
1725
- private triggerAbilities(event: string, piclet: BattlePiclet, context?: any): void {
1726
- if (!piclet.definition.specialAbility?.triggers) return;
1727
-
1728
- for (const trigger of piclet.definition.specialAbility.triggers) {
1729
- if (trigger.event === event && this.checkTriggerCondition(trigger, piclet, context)) {
1730
- this.log(`${piclet.definition.name}'s ${piclet.definition.specialAbility.name} triggered!`);
1731
-
1732
- // Process all effects in the trigger
1733
- for (const effect of trigger.effects) {
1734
- this.processAbilityTriggerEffect(effect, piclet, context);
1735
- }
1736
- }
1737
- }
1738
- }
1739
-
1740
- private checkTriggerCondition(trigger: Trigger, piclet: BattlePiclet, context?: any): boolean {
1741
- if (!trigger.condition || trigger.condition === 'always') {
1742
- return true;
1743
- }
1744
-
1745
- // Check various conditions
1746
- switch (trigger.condition) {
1747
- case 'ifLowHp':
1748
- return (piclet.currentHp / piclet.maxHp) < 0.25;
1749
- case 'ifHighHp':
1750
- return piclet.currentHp === piclet.maxHp;
1751
- case 'onCritical':
1752
- return context?.isCriticalHit === true;
1753
- case 'ifStatusMove':
1754
- return context?.isStatusMove === true;
1755
- default:
1756
- return true;
1757
- }
1758
- }
1759
-
1760
- private processAbilityTriggerEffect(effect: BattleEffect, owner: BattlePiclet, context?: any): void {
1761
- // Determine target for the effect based on effect type
1762
- let targetType = 'self'; // default
1763
- if ('target' in effect) {
1764
- targetType = effect.target;
1765
- }
1766
- const target = this.resolveAbilityTarget(targetType, owner);
1767
- if (!target) return;
1768
-
1769
- // Process the effect using existing effect processors
1770
- switch (effect.type) {
1771
- case 'damage':
1772
- // Create a dummy move for damage calculation
1773
- const dummyMove: Move = {
1774
- name: `${owner.definition.specialAbility?.name} Effect`,
1775
- type: 'normal' as any,
1776
- power: 0,
1777
- accuracy: 100,
1778
- pp: 1,
1779
- priority: 0,
1780
- flags: [],
1781
- effects: []
1782
- };
1783
- this.processDamageEffect(effect, owner, target, dummyMove);
1784
- break;
1785
- case 'modifyStats':
1786
- this.processModifyStatsEffect(effect, target);
1787
- break;
1788
- case 'heal':
1789
- this.processHealEffect(effect, target);
1790
- break;
1791
- case 'applyStatus':
1792
- this.processApplyStatusEffect(effect, target);
1793
- break;
1794
- case 'removeStatus':
1795
- this.processRemoveStatusEffect(effect, target);
1796
- break;
1797
- default:
1798
- this.log(`Ability effect ${effect.type} not implemented yet`);
1799
- }
1800
- }
1801
-
1802
- private resolveAbilityTarget(targetType: string, owner: BattlePiclet): BattlePiclet | null {
1803
- switch (targetType) {
1804
- case 'self':
1805
- return owner;
1806
- case 'opponent':
1807
- return owner === this.state.playerPiclet ? this.state.opponentPiclet : this.state.playerPiclet;
1808
- default:
1809
- return null;
1810
- }
1811
- }
1812
-
1813
- // Trigger Points Integration
1814
- private triggerOnDamageTaken(piclet: BattlePiclet, damage: number, isContactMove: boolean): void {
1815
- this.triggerAbilities('onDamageTaken', piclet, { damage, isContactMove });
1816
- if (isContactMove) {
1817
- this.triggerAbilities('onContactDamage', piclet, { damage });
1818
- }
1819
- }
1820
-
1821
- private triggerOnDamageDealt(piclet: BattlePiclet, damage: number, target: BattlePiclet): void {
1822
- this.triggerAbilities('onDamageDealt', piclet, { damage, target });
1823
- }
1824
-
1825
- private triggerOnCriticalHit(piclet: BattlePiclet, target: BattlePiclet): void {
1826
- this.triggerAbilities('onCriticalHit', piclet, { target, isCriticalHit: true });
1827
- }
1828
-
1829
- private triggerOnLowHP(piclet: BattlePiclet): void {
1830
- if ((piclet.currentHp / piclet.maxHp) < 0.25) {
1831
- this.triggerAbilities('onLowHP', piclet);
1832
- }
1833
- }
1834
-
1835
- private triggerEndOfTurn(): void {
1836
- this.triggerAbilities('endOfTurn', this.state.playerPiclet);
1837
- this.triggerAbilities('endOfTurn', this.state.opponentPiclet);
1838
- }
1839
-
1840
- private triggerOnStatusInflicted(piclet: BattlePiclet, status: string): void {
1841
- this.triggerAbilities('onStatusInflicted', piclet, { status });
1842
- }
1843
-
1844
- private triggerOnHPDrained(attacker: BattlePiclet, target: BattlePiclet, drainAmount: number): void {
1845
- this.triggerAbilities('onHPDrained', attacker, { target, drainAmount });
1846
- }
1847
-
1848
- private triggerOnKO(knockedOut: BattlePiclet, attacker: BattlePiclet): void {
1849
- this.triggerAbilities('onKO', knockedOut, { attacker });
1850
- this.triggerAbilities('onKO', attacker, { target: knockedOut, causedKO: true });
1851
- }
1852
-
1853
- private triggerBeforeMoveUse(piclet: BattlePiclet, move: Move): void {
1854
- this.triggerAbilities('beforeMoveUse', piclet, { move });
1855
- }
1856
-
1857
- private triggerAfterMoveUse(piclet: BattlePiclet, move: Move, success: boolean): void {
1858
- this.triggerAbilities('afterMoveUse', piclet, { move, success });
1859
- }
1860
-
1861
- private triggerOnFullHP(piclet: BattlePiclet): void {
1862
- if (piclet.currentHp === piclet.maxHp) {
1863
- this.triggerAbilities('onFullHP', piclet);
1864
- }
1865
- }
1866
-
1867
- private triggerOnOpponentContactMove(defender: BattlePiclet, attacker: BattlePiclet, move: Move): void {
1868
- if (move.flags.includes('contact')) {
1869
- this.triggerAbilities('onOpponentContactMove', defender, { attacker, move });
1870
- }
1871
- }
1872
-
1873
- private triggerOnStatChange(piclet: BattlePiclet, stat: string, change: string): void {
1874
- this.triggerAbilities('onStatChange', piclet, { stat, change });
1875
- }
1876
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/MultiBattleEngine.test.ts DELETED
@@ -1,671 +0,0 @@
1
- /**
2
- * Tests for Multi-Piclet Battle Engine
3
- * Covers battles with up to 4 Piclets on the field at once
4
- */
5
-
6
- import { describe, it, expect, beforeEach } from 'vitest';
7
- import { MultiBattleEngine } from './MultiBattleEngine';
8
- import { MultiBattleConfig, TurnActions } from './multi-piclet-types';
9
- import { PicletType, AttackType } from './types';
10
- import {
11
- STELLAR_WOLF,
12
- TOXIC_CRAWLER,
13
- BERSERKER_BEAST,
14
- AQUA_GUARDIAN
15
- } from './test-data';
16
-
17
- describe('MultiBattleEngine', () => {
18
- let config: MultiBattleConfig;
19
- let engine: MultiBattleEngine;
20
-
21
- beforeEach(() => {
22
- config = {
23
- playerParty: [STELLAR_WOLF, BERSERKER_BEAST],
24
- opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN],
25
- playerActiveCount: 1,
26
- opponentActiveCount: 1,
27
- battleType: 'single'
28
- };
29
- engine = new MultiBattleEngine(config);
30
- });
31
-
32
- describe('Battle Initialization', () => {
33
- it('should initialize single battle correctly', () => {
34
- const state = engine.getState();
35
-
36
- expect(state.turn).toBe(1);
37
- expect(state.phase).toBe('selection');
38
- expect(state.activePiclets.player).toHaveLength(2);
39
- expect(state.activePiclets.opponent).toHaveLength(2);
40
-
41
- // First position should be active, second should be null
42
- expect(state.activePiclets.player[0]).not.toBeNull();
43
- expect(state.activePiclets.player[1]).toBeNull();
44
- expect(state.activePiclets.opponent[0]).not.toBeNull();
45
- expect(state.activePiclets.opponent[1]).toBeNull();
46
- });
47
-
48
- it('should initialize double battle correctly', () => {
49
- const doubleConfig: MultiBattleConfig = {
50
- playerParty: [STELLAR_WOLF, BERSERKER_BEAST],
51
- opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN],
52
- playerActiveCount: 2,
53
- opponentActiveCount: 2,
54
- battleType: 'double'
55
- };
56
-
57
- const doubleEngine = new MultiBattleEngine(doubleConfig);
58
- const state = doubleEngine.getState();
59
-
60
- // Both positions should be active
61
- expect(state.activePiclets.player[0]).not.toBeNull();
62
- expect(state.activePiclets.player[1]).not.toBeNull();
63
- expect(state.activePiclets.opponent[0]).not.toBeNull();
64
- expect(state.activePiclets.opponent[1]).not.toBeNull();
65
- });
66
-
67
- it('should handle parties correctly', () => {
68
- const state = engine.getState();
69
-
70
- expect(state.parties.player).toHaveLength(2);
71
- expect(state.parties.opponent).toHaveLength(2);
72
- expect(state.parties.player[0].name).toBe('Stellar Wolf');
73
- expect(state.parties.opponent[0].name).toBe('Toxic Crawler');
74
- });
75
- });
76
-
77
- describe('Action Generation', () => {
78
- it('should generate valid move actions for active Piclets', () => {
79
- const actions = engine.getValidActions('player');
80
-
81
- // Should have move actions for the active Piclet
82
- const moveActions = actions.filter(a => a.type === 'move');
83
- expect(moveActions.length).toBeGreaterThan(0);
84
-
85
- // All move actions should be for position 0 (the active Piclet)
86
- moveActions.forEach(action => {
87
- expect((action as any).position).toBe(0);
88
- });
89
- });
90
-
91
- it('should generate switch actions for party members', () => {
92
- const actions = engine.getValidActions('player');
93
-
94
- const switchActions = actions.filter(a => a.type === 'switch');
95
- expect(switchActions.length).toBeGreaterThan(0);
96
-
97
- // Should be able to switch the inactive party member into position 0
98
- const switchToPosition0 = switchActions.find(a =>
99
- (a as any).position === 0 && (a as any).partyIndex === 1
100
- );
101
- expect(switchToPosition0).toBeDefined();
102
- });
103
- });
104
-
105
- describe('Single Battle Execution', () => {
106
- it('should execute a single battle turn correctly', () => {
107
- const turnActions: TurnActions = {
108
- player: [{
109
- type: 'move',
110
- side: 'player',
111
- position: 0,
112
- moveIndex: 0 // Tackle
113
- }],
114
- opponent: [{
115
- type: 'move',
116
- side: 'opponent',
117
- position: 0,
118
- moveIndex: 0 // Tackle
119
- }]
120
- };
121
-
122
- const initialOpponentHp = engine.getState().activePiclets.opponent[0]!.currentHp;
123
- engine.executeTurn(turnActions);
124
-
125
- const finalOpponentHp = engine.getState().activePiclets.opponent[0]!.currentHp;
126
- expect(finalOpponentHp).toBeLessThan(initialOpponentHp);
127
-
128
- const log = engine.getLog();
129
- expect(log.some(msg => msg.includes('used Tackle'))).toBe(true);
130
- });
131
-
132
- it('should handle switch actions correctly', () => {
133
- const turnActions: TurnActions = {
134
- player: [{
135
- type: 'switch',
136
- side: 'player',
137
- position: 0,
138
- partyIndex: 1 // Switch to Berserker Beast
139
- }],
140
- opponent: [{
141
- type: 'move',
142
- side: 'opponent',
143
- position: 0,
144
- moveIndex: 0
145
- }]
146
- };
147
-
148
- const initialName = engine.getState().activePiclets.player[0]!.definition.name;
149
- engine.executeTurn(turnActions);
150
- const finalName = engine.getState().activePiclets.player[0]!.definition.name;
151
-
152
- expect(initialName).toBe('Stellar Wolf');
153
- expect(finalName).toBe('Berserker Beast');
154
-
155
- const log = engine.getLog();
156
- expect(log.some(msg => msg.includes('switched out'))).toBe(true);
157
- expect(log.some(msg => msg.includes('switched in'))).toBe(true);
158
- });
159
- });
160
-
161
- describe('Double Battle System', () => {
162
- let doubleEngine: MultiBattleEngine;
163
-
164
- beforeEach(() => {
165
- const doubleConfig: MultiBattleConfig = {
166
- playerParty: [STELLAR_WOLF, BERSERKER_BEAST],
167
- opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN],
168
- playerActiveCount: 2,
169
- opponentActiveCount: 2,
170
- battleType: 'double'
171
- };
172
- doubleEngine = new MultiBattleEngine(doubleConfig);
173
- });
174
-
175
- it('should execute double battle turns correctly', () => {
176
- const turnActions: TurnActions = {
177
- player: [
178
- {
179
- type: 'move',
180
- side: 'player',
181
- position: 0,
182
- moveIndex: 0 // Stellar Wolf uses Tackle
183
- },
184
- {
185
- type: 'move',
186
- side: 'player',
187
- position: 1,
188
- moveIndex: 0 // Berserker Beast uses Tackle
189
- }
190
- ],
191
- opponent: [
192
- {
193
- type: 'move',
194
- side: 'opponent',
195
- position: 0,
196
- moveIndex: 0 // Toxic Crawler uses Tackle
197
- },
198
- {
199
- type: 'move',
200
- side: 'opponent',
201
- position: 1,
202
- moveIndex: 0 // Aqua Guardian uses Tackle
203
- }
204
- ]
205
- };
206
-
207
- doubleEngine.executeTurn(turnActions);
208
-
209
- const log = doubleEngine.getLog();
210
- expect(log.some(msg => msg.includes('Stellar Wolf used'))).toBe(true);
211
- expect(log.some(msg => msg.includes('Berserker Beast used'))).toBe(true);
212
- expect(log.some(msg => msg.includes('Toxic Crawler used'))).toBe(true);
213
- expect(log.some(msg => msg.includes('Aqua Guardian used'))).toBe(true);
214
- });
215
-
216
- it('should handle mixed actions in double battles', () => {
217
- const turnActions: TurnActions = {
218
- player: [
219
- {
220
- type: 'move',
221
- side: 'player',
222
- position: 0,
223
- moveIndex: 0 // Attack
224
- },
225
- {
226
- type: 'switch',
227
- side: 'player',
228
- position: 1,
229
- partyIndex: 0 // This would be switching to same Piclet, but tests the system
230
- }
231
- ],
232
- opponent: [
233
- {
234
- type: 'move',
235
- side: 'opponent',
236
- position: 0,
237
- moveIndex: 0
238
- },
239
- {
240
- type: 'move',
241
- side: 'opponent',
242
- position: 1,
243
- moveIndex: 0
244
- }
245
- ]
246
- };
247
-
248
- doubleEngine.executeTurn(turnActions);
249
-
250
- const log = doubleEngine.getLog();
251
- expect(log.some(msg => msg.includes('used'))).toBe(true);
252
- });
253
- });
254
-
255
- describe('Action Priority System', () => {
256
- it('should prioritize switch actions over moves', () => {
257
- const doubleConfig: MultiBattleConfig = {
258
- playerParty: [STELLAR_WOLF, BERSERKER_BEAST],
259
- opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN],
260
- playerActiveCount: 2,
261
- opponentActiveCount: 2,
262
- battleType: 'double'
263
- };
264
- const doubleEngine = new MultiBattleEngine(doubleConfig);
265
-
266
- const turnActions: TurnActions = {
267
- player: [
268
- {
269
- type: 'move',
270
- side: 'player',
271
- position: 0,
272
- moveIndex: 0 // Regular move
273
- }
274
- ],
275
- opponent: [
276
- {
277
- type: 'switch',
278
- side: 'opponent',
279
- position: 0,
280
- partyIndex: 1 // Switch action (should go first)
281
- }
282
- ]
283
- };
284
-
285
- doubleEngine.executeTurn(turnActions);
286
-
287
- const log = doubleEngine.getLog();
288
- const switchIndex = log.findIndex(msg => msg.includes('switched'));
289
- const moveIndex = log.findIndex(msg => msg.includes('used'));
290
-
291
- // Switch should happen before move (if both occurred)
292
- if (switchIndex !== -1 && moveIndex !== -1) {
293
- expect(switchIndex).toBeLessThan(moveIndex);
294
- }
295
- });
296
-
297
- it('should use speed for same priority actions', () => {
298
- // Stellar Wolf (speed 70) vs Toxic Crawler (speed 55)
299
- const turnActions: TurnActions = {
300
- player: [{
301
- type: 'move',
302
- side: 'player',
303
- position: 0,
304
- moveIndex: 0 // Tackle (priority 0)
305
- }],
306
- opponent: [{
307
- type: 'move',
308
- side: 'opponent',
309
- position: 0,
310
- moveIndex: 0 // Tackle (priority 0)
311
- }]
312
- };
313
-
314
- engine.executeTurn(turnActions);
315
-
316
- const log = engine.getLog();
317
- const stellarIndex = log.findIndex(msg => msg.includes('Stellar Wolf used'));
318
- const toxicIndex = log.findIndex(msg => msg.includes('Toxic Crawler used'));
319
-
320
- // Stellar Wolf should go first due to higher speed
321
- expect(stellarIndex).toBeLessThan(toxicIndex);
322
- });
323
- });
324
-
325
- describe('Victory Conditions', () => {
326
- it('should end battle when all opponent Piclets faint', () => {
327
- // Create a battle with single-Piclet opponent party (no reserves)
328
- const singleOpponentConfig: MultiBattleConfig = {
329
- playerParty: [STELLAR_WOLF, BERSERKER_BEAST],
330
- opponentParty: [TOXIC_CRAWLER], // Only one Piclet, no reserves
331
- playerActiveCount: 1,
332
- opponentActiveCount: 1,
333
- battleType: 'single'
334
- };
335
- const singleEngine = new MultiBattleEngine(singleOpponentConfig);
336
-
337
- // Set opponent to very low HP
338
- (singleEngine as any).state.activePiclets.opponent[0]!.currentHp = 1;
339
-
340
- const turnActions: TurnActions = {
341
- player: [{
342
- type: 'move',
343
- side: 'player',
344
- position: 0,
345
- moveIndex: 0
346
- }],
347
- opponent: [{
348
- type: 'move',
349
- side: 'opponent',
350
- position: 0,
351
- moveIndex: 0
352
- }]
353
- };
354
-
355
- singleEngine.executeTurn(turnActions);
356
-
357
- expect(singleEngine.isGameOver()).toBe(true);
358
- expect(singleEngine.getWinner()).toBe('player');
359
- });
360
-
361
- it('should continue battle when reserves are available', () => {
362
- // This test would require implementing automatic switching
363
- // when a Piclet faints, which is more complex
364
- expect(true).toBe(true); // Placeholder
365
- });
366
- });
367
-
368
- describe('Targeting System', () => {
369
- it('should target opponents correctly in single battles', () => {
370
- const turnActions: TurnActions = {
371
- player: [{
372
- type: 'move',
373
- side: 'player',
374
- position: 0,
375
- moveIndex: 0 // Attack should hit opponent
376
- }],
377
- opponent: [{
378
- type: 'move',
379
- side: 'opponent',
380
- position: 0,
381
- moveIndex: 0
382
- }]
383
- };
384
-
385
- const initialHp = engine.getState().activePiclets.opponent[0]!.currentHp;
386
- engine.executeTurn(turnActions);
387
- const finalHp = engine.getState().activePiclets.opponent[0]!.currentHp;
388
-
389
- expect(finalHp).toBeLessThan(initialHp);
390
- });
391
-
392
- it('should target all opponents in double battles', () => {
393
- const doubleConfig: MultiBattleConfig = {
394
- playerParty: [STELLAR_WOLF, BERSERKER_BEAST],
395
- opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN],
396
- playerActiveCount: 2,
397
- opponentActiveCount: 2,
398
- battleType: 'double'
399
- };
400
- const doubleEngine = new MultiBattleEngine(doubleConfig);
401
-
402
- // Create a multi-target move for testing
403
- const multiTargetMove = {
404
- name: 'Mass Strike',
405
- type: 'normal' as any,
406
- power: 30,
407
- accuracy: 100,
408
- pp: 10,
409
- priority: 0,
410
- flags: [] as any,
411
- effects: [{
412
- type: 'damage' as any,
413
- target: 'allOpponents' as any,
414
- amount: 'normal' as any
415
- }]
416
- };
417
-
418
- // Add the move to the attacker
419
- (doubleEngine as any).state.activePiclets.player[0].moves[0] = {
420
- move: multiTargetMove,
421
- currentPP: 10
422
- };
423
-
424
- const initialHp1 = doubleEngine.getState().activePiclets.opponent[0]!.currentHp;
425
- const initialHp2 = doubleEngine.getState().activePiclets.opponent[1]!.currentHp;
426
-
427
- const turnActions: TurnActions = {
428
- player: [{
429
- type: 'move',
430
- side: 'player',
431
- position: 0,
432
- moveIndex: 0 // Multi-target move
433
- }],
434
- opponent: [
435
- { type: 'move', side: 'opponent', position: 0, moveIndex: 0 },
436
- { type: 'move', side: 'opponent', position: 1, moveIndex: 0 }
437
- ]
438
- };
439
-
440
- doubleEngine.executeTurn(turnActions);
441
-
442
- const finalHp1 = doubleEngine.getState().activePiclets.opponent[0]!.currentHp;
443
- const finalHp2 = doubleEngine.getState().activePiclets.opponent[1]!.currentHp;
444
-
445
- // Both opponents should take damage
446
- expect(finalHp1).toBeLessThan(initialHp1);
447
- expect(finalHp2).toBeLessThan(initialHp2);
448
- });
449
-
450
- it('should target self correctly', () => {
451
- // Create a self-targeting move (like heal)
452
- const selfTargetMove = {
453
- name: 'Self Heal',
454
- type: 'normal' as any,
455
- power: 0,
456
- accuracy: 100,
457
- pp: 10,
458
- priority: 0,
459
- flags: [] as any,
460
- effects: [{
461
- type: 'heal' as any,
462
- target: 'self' as any,
463
- amount: 'medium' as any
464
- }]
465
- };
466
-
467
- // Damage the Piclet first then heal
468
- (engine as any).state.activePiclets.player[0].currentHp = 50;
469
-
470
- // Add the heal move
471
- (engine as any).state.activePiclets.player[0].moves[0] = {
472
- move: selfTargetMove,
473
- currentPP: 10
474
- };
475
-
476
- const initialHp = engine.getState().activePiclets.player[0]!.currentHp;
477
-
478
- const turnActions: TurnActions = {
479
- player: [{
480
- type: 'move',
481
- side: 'player',
482
- position: 0,
483
- moveIndex: 0 // Self-heal move
484
- }],
485
- opponent: [{
486
- type: 'move',
487
- side: 'opponent',
488
- position: 0,
489
- moveIndex: 0
490
- }]
491
- };
492
-
493
- engine.executeTurn(turnActions);
494
-
495
- const finalHp = engine.getState().activePiclets.player[0]!.currentHp;
496
-
497
- // Player should have more HP after healing
498
- expect(finalHp).toBeGreaterThan(initialHp);
499
- });
500
- });
501
-
502
- describe('Status Effects in Multi-Battle', () => {
503
- it('should process status effects for all active Piclets', () => {
504
- const doubleConfig: MultiBattleConfig = {
505
- playerParty: [STELLAR_WOLF, BERSERKER_BEAST],
506
- opponentParty: [TOXIC_CRAWLER, AQUA_GUARDIAN],
507
- playerActiveCount: 2,
508
- opponentActiveCount: 2,
509
- battleType: 'double'
510
- };
511
- const doubleEngine = new MultiBattleEngine(doubleConfig);
512
-
513
- // Apply poison to both active player Piclets
514
- (doubleEngine as any).state.activePiclets.player[0]!.statusEffects.push('poison');
515
- (doubleEngine as any).state.activePiclets.player[1]!.statusEffects.push('poison');
516
-
517
- const turnActions: TurnActions = {
518
- player: [
519
- { type: 'move', side: 'player', position: 0, moveIndex: 0 },
520
- { type: 'move', side: 'player', position: 1, moveIndex: 0 }
521
- ],
522
- opponent: [
523
- { type: 'move', side: 'opponent', position: 0, moveIndex: 0 },
524
- { type: 'move', side: 'opponent', position: 1, moveIndex: 0 }
525
- ]
526
- };
527
-
528
- doubleEngine.executeTurn(turnActions);
529
-
530
- const log = doubleEngine.getLog();
531
- const poisonMessages = log.filter(msg => msg.includes('hurt by poison'));
532
- expect(poisonMessages.length).toBe(2); // Both Piclets should take poison damage
533
- });
534
- });
535
-
536
- describe('Active Piclet Tracking', () => {
537
- it('should correctly track active Piclets', () => {
538
- const actives = engine.getActivePiclets();
539
-
540
- expect(actives.player).toHaveLength(1);
541
- expect(actives.opponent).toHaveLength(1);
542
- expect(actives.player[0].definition.name).toBe('Stellar Wolf');
543
- expect(actives.opponent[0].definition.name).toBe('Toxic Crawler');
544
- });
545
-
546
- it('should update active tracking after switches', () => {
547
- const turnActions: TurnActions = {
548
- player: [{
549
- type: 'switch',
550
- side: 'player',
551
- position: 0,
552
- partyIndex: 1 // Switch to Berserker Beast
553
- }],
554
- opponent: [{
555
- type: 'move',
556
- side: 'opponent',
557
- position: 0,
558
- moveIndex: 0
559
- }]
560
- };
561
-
562
- engine.executeTurn(turnActions);
563
-
564
- const actives = engine.getActivePiclets();
565
- expect(actives.player[0].definition.name).toBe('Berserker Beast');
566
- });
567
- });
568
-
569
- describe('Party Management', () => {
570
- it('should track available switches correctly', () => {
571
- const availableSwitches = engine.getAvailableSwitches('player');
572
-
573
- expect(availableSwitches).toHaveLength(1);
574
- expect(availableSwitches[0].piclet.name).toBe('Berserker Beast');
575
- expect(availableSwitches[0].partyIndex).toBe(1);
576
- });
577
-
578
- it('should update available switches after switching', () => {
579
- const turnActions: TurnActions = {
580
- player: [{
581
- type: 'switch',
582
- side: 'player',
583
- position: 0,
584
- partyIndex: 1 // Switch to Berserker Beast
585
- }],
586
- opponent: [{
587
- type: 'move',
588
- side: 'opponent',
589
- position: 0,
590
- moveIndex: 0
591
- }]
592
- };
593
-
594
- engine.executeTurn(turnActions);
595
-
596
- const availableSwitches = engine.getAvailableSwitches('player');
597
- expect(availableSwitches).toHaveLength(1);
598
- expect(availableSwitches[0].piclet.name).toBe('Stellar Wolf');
599
- expect(availableSwitches[0].partyIndex).toBe(0);
600
- });
601
-
602
- it('should handle fainted Piclets correctly', () => {
603
- // Set player Piclet to very low HP
604
- (engine as any).state.activePiclets.player[0]!.currentHp = 1;
605
-
606
- const turnActions: TurnActions = {
607
- player: [{
608
- type: 'move',
609
- side: 'player',
610
- position: 0,
611
- moveIndex: 0
612
- }],
613
- opponent: [{
614
- type: 'move',
615
- side: 'opponent',
616
- position: 0,
617
- moveIndex: 0 // This should cause player to faint
618
- }]
619
- };
620
-
621
- engine.executeTurn(turnActions);
622
-
623
- const log = engine.getLog();
624
- expect(log.some(msg => msg.includes('fainted!'))).toBe(true);
625
-
626
- // Active Piclet should be removed (null)
627
- const state = engine.getState();
628
- expect(state.activePiclets.player[0]).toBeNull();
629
- });
630
-
631
- });
632
-
633
- describe('Edge Cases', () => {
634
- it('should handle empty action arrays gracefully', () => {
635
- const turnActions: TurnActions = {
636
- player: [],
637
- opponent: [{
638
- type: 'move',
639
- side: 'opponent',
640
- position: 0,
641
- moveIndex: 0
642
- }]
643
- };
644
-
645
- expect(() => {
646
- engine.executeTurn(turnActions);
647
- }).not.toThrow();
648
- });
649
-
650
- it('should handle invalid positions gracefully', () => {
651
- const turnActions: TurnActions = {
652
- player: [{
653
- type: 'move',
654
- side: 'player',
655
- position: 1, // Invalid position (empty slot)
656
- moveIndex: 0
657
- }],
658
- opponent: [{
659
- type: 'move',
660
- side: 'opponent',
661
- position: 0,
662
- moveIndex: 0
663
- }]
664
- };
665
-
666
- expect(() => {
667
- engine.executeTurn(turnActions);
668
- }).not.toThrow();
669
- });
670
- });
671
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/MultiBattleEngine.ts DELETED
@@ -1,701 +0,0 @@
1
- /**
2
- * Multi-Piclet Battle Engine
3
- * Supports up to 4 Piclets on the field at once (2 per side)
4
- * Extends the single-Piclet battle system with party management and multi-targeting
5
- */
6
-
7
- import {
8
- MultiBattleState,
9
- MultiBattleAction,
10
- MultiMoveAction,
11
- MultiSwitchAction,
12
- TurnActions,
13
- PicletTarget,
14
- BattleSide,
15
- FieldPosition,
16
- MultiBattleConfig,
17
- ActionPriority,
18
- VictoryCondition,
19
- MultiEffectTarget
20
- } from './multi-piclet-types';
21
-
22
- import {
23
- BattlePiclet,
24
- PicletDefinition,
25
- BattleEffect,
26
- Move,
27
- BaseStats,
28
- StatusEffect
29
- } from './types';
30
-
31
- import { getEffectivenessMultiplier } from '../types/picletTypes';
32
-
33
- export class MultiBattleEngine {
34
- private state: MultiBattleState;
35
- private victoryCondition: VictoryCondition;
36
-
37
- constructor(config: MultiBattleConfig, victoryCondition: VictoryCondition = { type: 'allFainted' }) {
38
- this.victoryCondition = victoryCondition;
39
- this.state = this.initializeBattle(config);
40
- this.log('Multi-Piclet battle started!');
41
- this.logActivePiclets();
42
- }
43
-
44
- private initializeBattle(config: MultiBattleConfig): MultiBattleState {
45
- // Initialize active Piclets from parties
46
- const playerActive: Array<BattlePiclet | null> = [null, null];
47
- const opponentActive: Array<BattlePiclet | null> = [null, null];
48
-
49
- // Set up initial active Piclets
50
- for (let i = 0; i < config.playerActiveCount && i < config.playerParty.length; i++) {
51
- playerActive[i] = this.createBattlePiclet(config.playerParty[i], 50);
52
- }
53
-
54
- for (let i = 0; i < config.opponentActiveCount && i < config.opponentParty.length; i++) {
55
- opponentActive[i] = this.createBattlePiclet(config.opponentParty[i], 50);
56
- }
57
-
58
- return {
59
- turn: 1,
60
- phase: 'selection',
61
- activePiclets: {
62
- player: playerActive,
63
- opponent: opponentActive
64
- },
65
- parties: {
66
- player: config.playerParty,
67
- opponent: config.opponentParty
68
- },
69
- fieldEffects: [],
70
- log: [],
71
- winner: undefined
72
- };
73
- }
74
-
75
- private createBattlePiclet(definition: PicletDefinition, level: number): BattlePiclet {
76
- // Same logic as original BattleEngine
77
- const statMultiplier = 1 + (level - 50) * 0.02;
78
-
79
- const hp = Math.floor(definition.baseStats.hp * statMultiplier);
80
- const attack = Math.floor(definition.baseStats.attack * statMultiplier);
81
- const defense = Math.floor(definition.baseStats.defense * statMultiplier);
82
- const speed = Math.floor(definition.baseStats.speed * statMultiplier);
83
-
84
- return {
85
- definition,
86
- currentHp: hp,
87
- maxHp: hp,
88
- level,
89
- attack,
90
- defense,
91
- speed,
92
- accuracy: 100,
93
- statusEffects: [],
94
- moves: definition.movepool.slice(0, 4).map(move => ({
95
- move,
96
- currentPP: move.pp
97
- })),
98
- statModifiers: {},
99
- temporaryEffects: []
100
- };
101
- }
102
-
103
- public getState(): MultiBattleState {
104
- return JSON.parse(JSON.stringify(this.state));
105
- }
106
-
107
- public isGameOver(): boolean {
108
- return this.state.phase === 'ended';
109
- }
110
-
111
- public getWinner(): 'player' | 'opponent' | 'draw' | undefined {
112
- return this.state.winner;
113
- }
114
-
115
- public getActivePiclets(): { player: BattlePiclet[], opponent: BattlePiclet[] } {
116
- return {
117
- player: this.state.activePiclets.player.filter(p => p !== null) as BattlePiclet[],
118
- opponent: this.state.activePiclets.opponent.filter(p => p !== null) as BattlePiclet[]
119
- };
120
- }
121
-
122
- public getAvailableSwitches(side: BattleSide): Array<{ partyIndex: number, piclet: PicletDefinition }> {
123
- const available: Array<{ partyIndex: number, piclet: PicletDefinition }> = [];
124
- const activePicletNames = this.state.activePiclets[side]
125
- .filter(p => p !== null)
126
- .map(p => p!.definition.name);
127
-
128
- this.state.parties[side].forEach((partyMember, index) => {
129
- if (!activePicletNames.includes(partyMember.name)) {
130
- available.push({ partyIndex: index, piclet: partyMember });
131
- }
132
- });
133
-
134
- return available;
135
- }
136
-
137
- public getValidActions(side: BattleSide): MultiBattleAction[] {
138
- const actions: MultiBattleAction[] = [];
139
- const activePiclets = this.state.activePiclets[side];
140
-
141
- // Add move actions for each active Piclet
142
- activePiclets.forEach((piclet, position) => {
143
- if (piclet) {
144
- piclet.moves.forEach((moveData, moveIndex) => {
145
- if (moveData.currentPP > 0) {
146
- actions.push({
147
- type: 'move',
148
- side,
149
- position: position as FieldPosition,
150
- moveIndex
151
- });
152
- }
153
- });
154
- }
155
- });
156
-
157
- // Add switch actions for empty slots or when Piclets can be switched
158
- this.state.parties[side].forEach((partyMember, partyIndex) => {
159
- // Check if this party member is not currently active
160
- const isActive = activePiclets.some(active =>
161
- active?.definition.name === partyMember.name
162
- );
163
-
164
- if (!isActive) {
165
- // Can switch into any position that has a Piclet (replacement) or empty slot
166
- activePiclets.forEach((slot, position) => {
167
- actions.push({
168
- type: 'switch',
169
- side,
170
- position: position as FieldPosition,
171
- partyIndex
172
- });
173
- });
174
- }
175
- });
176
-
177
- return actions;
178
- }
179
-
180
- public executeTurn(turnActions: TurnActions): void {
181
- if (this.state.phase !== 'selection') {
182
- throw new Error('Cannot execute turn - battle is not in selection phase');
183
- }
184
-
185
- this.state.phase = 'execution';
186
- this.log(`Turn ${this.state.turn} - Executing actions`);
187
-
188
- // Determine action order based on priority and speed
189
- const allActions = this.determineActionOrder(turnActions);
190
-
191
- // Execute actions in order
192
- for (const actionPriority of allActions) {
193
- if (this.state.phase === 'ended') break;
194
- this.executeAction(actionPriority.action, actionPriority.side, actionPriority.position);
195
- }
196
-
197
- // End of turn processing
198
- this.processTurnEnd();
199
-
200
- // Check for battle end
201
- this.checkBattleEnd();
202
-
203
- if (this.state.phase !== 'ended') {
204
- this.state.turn++;
205
- this.state.phase = 'selection';
206
- }
207
- }
208
-
209
- private determineActionOrder(turnActions: TurnActions): ActionPriority[] {
210
- const allActionPriorities: ActionPriority[] = [];
211
-
212
- // Process player actions
213
- turnActions.player.forEach(action => {
214
- const priority = this.getActionPriority(action);
215
- const piclet = this.state.activePiclets.player[action.position];
216
- allActionPriorities.push({
217
- action,
218
- side: 'player',
219
- position: action.position,
220
- priority,
221
- speed: piclet?.speed || 0,
222
- randomTiebreaker: Math.random()
223
- });
224
- });
225
-
226
- // Process opponent actions
227
- turnActions.opponent.forEach(action => {
228
- const priority = this.getActionPriority(action);
229
- const piclet = this.state.activePiclets.opponent[action.position];
230
- allActionPriorities.push({
231
- action,
232
- side: 'opponent',
233
- position: action.position,
234
- priority,
235
- speed: piclet?.speed || 0,
236
- randomTiebreaker: Math.random()
237
- });
238
- });
239
-
240
- // Sort by priority (higher first), then speed (higher first), then random
241
- return allActionPriorities.sort((a, b) => {
242
- if (a.priority !== b.priority) return b.priority - a.priority;
243
- if (a.speed !== b.speed) return b.speed - a.speed;
244
- return a.randomTiebreaker - b.randomTiebreaker;
245
- });
246
- }
247
-
248
- private getActionPriority(action: MultiBattleAction): number {
249
- if (action.type === 'switch') return 6; // Switches have highest priority
250
-
251
- const piclet = this.state.activePiclets[action.side][action.position];
252
- if (!piclet) return 0;
253
-
254
- const move = piclet.moves[action.moveIndex]?.move;
255
- return move?.priority || 0;
256
- }
257
-
258
- private executeAction(action: MultiBattleAction, side: BattleSide, position: FieldPosition): void {
259
- const piclet = this.state.activePiclets[side][position];
260
- if (!piclet) return;
261
-
262
- if (action.type === 'move') {
263
- this.executeMove(action as MultiMoveAction, side, position);
264
- } else if (action.type === 'switch') {
265
- this.executeSwitch(action as MultiSwitchAction, side, position);
266
- }
267
- }
268
-
269
- private executeMove(action: MultiMoveAction, side: BattleSide, position: FieldPosition): void {
270
- const attacker = this.state.activePiclets[side][position];
271
- if (!attacker) return;
272
-
273
- const moveData = attacker.moves[action.moveIndex];
274
- if (!moveData || moveData.currentPP <= 0) {
275
- this.log(`${attacker.definition.name} has no PP left for that move!`);
276
- return;
277
- }
278
-
279
- const move = moveData.move;
280
- this.log(`${attacker.definition.name} used ${move.name}!`);
281
-
282
- // Consume PP
283
- moveData.currentPP--;
284
-
285
- // Check if move hits (simplified for now)
286
- if (!this.checkMoveHits(move, attacker)) {
287
- this.log(`${attacker.definition.name}'s attack missed!`);
288
- return;
289
- }
290
-
291
- // Process effects for each target
292
- const targets = this.resolveTargets(move, side, position, action.targets);
293
-
294
- for (const effect of move.effects) {
295
- this.processMultiEffect(effect, attacker, targets, move);
296
- }
297
- }
298
-
299
- private executeSwitch(action: MultiSwitchAction, side: BattleSide, position: FieldPosition): void {
300
- const currentPiclet = this.state.activePiclets[side][position];
301
- const newPiclet = this.state.parties[side][action.partyIndex];
302
-
303
- if (!newPiclet) return;
304
-
305
- // Create battle instance of new Piclet
306
- const battlePiclet = this.createBattlePiclet(newPiclet, 50);
307
-
308
- // Switch out current Piclet (if any)
309
- if (currentPiclet) {
310
- this.log(`${currentPiclet.definition.name} switched out!`);
311
- // Trigger switch-out abilities here
312
- }
313
-
314
- // Switch in new Piclet
315
- this.state.activePiclets[side][position] = battlePiclet;
316
- this.log(`${battlePiclet.definition.name} switched in!`);
317
-
318
- // Trigger switch-in abilities here
319
- this.processAbilityTrigger(battlePiclet, 'onSwitchIn');
320
- }
321
-
322
- private resolveTargets(move: Move, attackerSide: BattleSide, attackerPosition: FieldPosition, targetOverride?: any): BattlePiclet[] {
323
- const targets: BattlePiclet[] = [];
324
- const attacker = this.state.activePiclets[attackerSide][attackerPosition];
325
- if (!attacker) return targets;
326
-
327
- // Check if move effects specify targets, default to opponent
328
- const effectTargets = move.effects.map(e => (e as any).target).filter(t => t);
329
- const primaryTarget = effectTargets[0] || 'opponent';
330
-
331
- switch (primaryTarget) {
332
- case 'self':
333
- targets.push(attacker);
334
- break;
335
-
336
- case 'opponent':
337
- // Target first available opponent (can be enhanced for player choice)
338
- const opponentSide = attackerSide === 'player' ? 'opponent' : 'player';
339
- const opponents = this.state.activePiclets[opponentSide].filter(p => p !== null) as BattlePiclet[];
340
- if (opponents.length > 0) {
341
- targets.push(opponents[0]);
342
- }
343
- break;
344
-
345
- case 'allOpponents':
346
- const oppSide = attackerSide === 'player' ? 'opponent' : 'player';
347
- const allOpponents = this.state.activePiclets[oppSide].filter(p => p !== null) as BattlePiclet[];
348
- targets.push(...allOpponents);
349
- break;
350
-
351
- case 'ally':
352
- // Target ally (for double battles)
353
- const allies = this.state.activePiclets[attackerSide].filter(p => p !== null && p !== attacker) as BattlePiclet[];
354
- if (allies.length > 0) {
355
- targets.push(allies[0]);
356
- }
357
- break;
358
-
359
- case 'allAllies':
360
- const allAllies = this.state.activePiclets[attackerSide].filter(p => p !== null && p !== attacker) as BattlePiclet[];
361
- targets.push(...allAllies);
362
- break;
363
-
364
- case 'all':
365
- // Target all active Piclets
366
- for (const side of ['player', 'opponent'] as BattleSide[]) {
367
- const activePiclets = this.state.activePiclets[side].filter(p => p !== null) as BattlePiclet[];
368
- targets.push(...activePiclets);
369
- }
370
- break;
371
-
372
- case 'random':
373
- // Target random active Piclet
374
- const allActive: BattlePiclet[] = [];
375
- for (const side of ['player', 'opponent'] as BattleSide[]) {
376
- const activePiclets = this.state.activePiclets[side].filter(p => p !== null) as BattlePiclet[];
377
- allActive.push(...activePiclets);
378
- }
379
- if (allActive.length > 0) {
380
- const randomIndex = Math.floor(Math.random() * allActive.length);
381
- targets.push(allActive[randomIndex]);
382
- }
383
- break;
384
-
385
- case 'weakest':
386
- // Target Piclet with lowest HP percentage
387
- const allActivePiclets: BattlePiclet[] = [];
388
- for (const side of ['player', 'opponent'] as BattleSide[]) {
389
- const activePiclets = this.state.activePiclets[side].filter(p => p !== null) as BattlePiclet[];
390
- allActivePiclets.push(...activePiclets);
391
- }
392
- if (allActivePiclets.length > 0) {
393
- const weakest = allActivePiclets.reduce((weak, current) =>
394
- (current.currentHp / current.maxHp) < (weak.currentHp / weak.maxHp) ? current : weak
395
- );
396
- targets.push(weakest);
397
- }
398
- break;
399
-
400
- case 'strongest':
401
- // Target Piclet with highest HP percentage
402
- const allActiveForStrongest: BattlePiclet[] = [];
403
- for (const side of ['player', 'opponent'] as BattleSide[]) {
404
- const activePiclets = this.state.activePiclets[side].filter(p => p !== null) as BattlePiclet[];
405
- allActiveForStrongest.push(...activePiclets);
406
- }
407
- if (allActiveForStrongest.length > 0) {
408
- const strongest = allActiveForStrongest.reduce((strong, current) =>
409
- (current.currentHp / current.maxHp) > (strong.currentHp / strong.maxHp) ? current : strong
410
- );
411
- targets.push(strongest);
412
- }
413
- break;
414
-
415
- default:
416
- // Fallback to first opponent
417
- const defaultOpponentSide = attackerSide === 'player' ? 'opponent' : 'player';
418
- const defaultOpponents = this.state.activePiclets[defaultOpponentSide].filter(p => p !== null) as BattlePiclet[];
419
- if (defaultOpponents.length > 0) {
420
- targets.push(defaultOpponents[0]);
421
- }
422
- }
423
-
424
- return targets;
425
- }
426
-
427
- private processMultiEffect(effect: BattleEffect, attacker: BattlePiclet, targets: BattlePiclet[], move: Move): void {
428
- // Process effect on each target
429
- for (const target of targets) {
430
- this.processEffect(effect, attacker, target, move);
431
- }
432
- }
433
-
434
- private processEffect(effect: BattleEffect, attacker: BattlePiclet, target: BattlePiclet, move: Move): void {
435
- // Check condition
436
- if (effect.condition && !this.checkCondition(effect.condition, attacker, target)) {
437
- return;
438
- }
439
-
440
- switch (effect.type) {
441
- case 'damage':
442
- this.processDamageEffect(effect, attacker, target, move);
443
- break;
444
- case 'heal':
445
- this.processHealEffect(effect, target);
446
- break;
447
- case 'modifyStats':
448
- this.processModifyStatsEffect(effect, target);
449
- break;
450
- case 'applyStatus':
451
- this.processApplyStatusEffect(effect, target);
452
- break;
453
- // Add other effect types as needed
454
- default:
455
- this.log(`Effect ${effect.type} not implemented in multi-battle yet`);
456
- }
457
- }
458
-
459
- // Simplified effect processors (can be expanded)
460
- private processDamageEffect(effect: any, attacker: BattlePiclet, target: BattlePiclet, move: Move): void {
461
- const damage = this.calculateDamage(attacker, target, move);
462
- target.currentHp = Math.max(0, target.currentHp - damage);
463
- this.log(`${target.definition.name} took ${damage} damage!`);
464
- }
465
-
466
- private processHealEffect(effect: any, target: BattlePiclet): void {
467
- const healAmount = Math.floor(target.maxHp * 0.5); // Simplified
468
- const oldHp = target.currentHp;
469
- target.currentHp = Math.min(target.maxHp, target.currentHp + healAmount);
470
- const actualHeal = target.currentHp - oldHp;
471
-
472
- if (actualHeal > 0) {
473
- this.log(`${target.definition.name} recovered ${actualHeal} HP!`);
474
- }
475
- }
476
-
477
- private processModifyStatsEffect(effect: any, target: BattlePiclet): void {
478
- // Simplified stat modification
479
- if (effect.stats?.attack === 'increase') {
480
- target.attack = Math.floor(target.attack * 1.25);
481
- this.log(`${target.definition.name}'s attack rose!`);
482
- }
483
- }
484
-
485
- private processApplyStatusEffect(effect: any, target: BattlePiclet): void {
486
- if (!target.statusEffects.includes(effect.status)) {
487
- target.statusEffects.push(effect.status);
488
- this.log(`${target.definition.name} was ${effect.status}ed!`);
489
- }
490
- }
491
-
492
- private calculateDamage(attacker: BattlePiclet, target: BattlePiclet, move: Move): number {
493
- // Simplified damage calculation
494
- const baseDamage = move.power || 50;
495
- const effectiveness = getEffectivenessMultiplier(
496
- move.type,
497
- target.definition.primaryType,
498
- target.definition.secondaryType
499
- );
500
-
501
- let damage = Math.floor((baseDamage * (attacker.attack / target.defense) * 0.5) + 10);
502
- damage = Math.floor(damage * effectiveness);
503
-
504
- return Math.max(1, damage);
505
- }
506
-
507
- private checkMoveHits(move: Move, attacker: BattlePiclet): boolean {
508
- return Math.random() * 100 < move.accuracy;
509
- }
510
-
511
- private checkCondition(condition: string, attacker: BattlePiclet, target: BattlePiclet): boolean {
512
- switch (condition) {
513
- case 'always':
514
- return true;
515
- case 'ifLowHp':
516
- return attacker.currentHp / attacker.maxHp < 0.25;
517
- default:
518
- return true;
519
- }
520
- }
521
-
522
- private processAbilityTrigger(piclet: BattlePiclet, trigger: string): void {
523
- // Process special ability triggers
524
- if (piclet.definition.specialAbility.triggers) {
525
- for (const abilityTrigger of piclet.definition.specialAbility.triggers) {
526
- if (abilityTrigger.event === trigger) {
527
- this.log(`${piclet.definition.name}'s ${piclet.definition.specialAbility.name} activated!`);
528
- // Process trigger effects
529
- }
530
- }
531
- }
532
- }
533
-
534
- private processTurnEnd(): void {
535
- // Process status effects for all active Piclets
536
- for (const side of ['player', 'opponent'] as BattleSide[]) {
537
- for (const piclet of this.state.activePiclets[side]) {
538
- if (piclet) {
539
- this.processStatusEffects(piclet);
540
- this.processTemporaryEffects(piclet);
541
- }
542
- }
543
- }
544
-
545
- // Process field effects
546
- this.processFieldEffects();
547
-
548
- // Handle fainted Piclets
549
- this.handleFaintedPiclets();
550
- }
551
-
552
- private handleFaintedPiclets(): void {
553
- for (const side of ['player', 'opponent'] as BattleSide[]) {
554
- for (let position = 0; position < this.state.activePiclets[side].length; position++) {
555
- const piclet = this.state.activePiclets[side][position];
556
- if (piclet && piclet.currentHp <= 0) {
557
- this.log(`${piclet.definition.name} fainted!`);
558
-
559
- // Remove fainted Piclet from active slot
560
- this.state.activePiclets[side][position] = null;
561
-
562
- // Trigger faint abilities
563
- this.processAbilityTrigger(piclet, 'onKO');
564
-
565
- // For now, we don't auto-switch reserves in this simplified implementation
566
- // In a full implementation, the player would choose a replacement
567
- }
568
- }
569
- }
570
- }
571
-
572
- private processStatusEffects(piclet: BattlePiclet): void {
573
- for (const status of piclet.statusEffects) {
574
- switch (status) {
575
- case 'burn':
576
- case 'poison':
577
- const damage = Math.floor(piclet.maxHp / 8);
578
- piclet.currentHp = Math.max(0, piclet.currentHp - damage);
579
- this.log(`${piclet.definition.name} hurt by ${status}!`);
580
- break;
581
- }
582
- }
583
- }
584
-
585
- private processTemporaryEffects(piclet: BattlePiclet): void {
586
- piclet.temporaryEffects = piclet.temporaryEffects.filter(effect => {
587
- effect.duration--;
588
- return effect.duration > 0;
589
- });
590
- }
591
-
592
- private processFieldEffects(): void {
593
- this.state.fieldEffects = this.state.fieldEffects.filter(effect => {
594
- effect.duration--;
595
- if (effect.duration <= 0) {
596
- this.log(`Field effect '${effect.name}' ended!`);
597
- return false;
598
- }
599
- return true;
600
- });
601
- }
602
-
603
- private checkBattleEnd(): void {
604
- const winner = this.determineWinner();
605
- if (winner) {
606
- this.state.winner = winner;
607
- this.state.phase = 'ended';
608
- this.log(`Battle ended! Winner: ${winner}`);
609
- }
610
- }
611
-
612
- private determineWinner(): 'player' | 'opponent' | 'draw' | null {
613
- // Count living active Piclets (not null and HP > 0)
614
- const playerActiveLiving = this.state.activePiclets.player.filter(p => p !== null && p.currentHp > 0);
615
- const opponentActiveLiving = this.state.activePiclets.opponent.filter(p => p !== null && p.currentHp > 0);
616
-
617
- // Check for healthy reserves
618
- const playerHealthyReserves = this.getHealthyReserves('player');
619
- const opponentHealthyReserves = this.getHealthyReserves('opponent');
620
-
621
- const playerHasUsablePiclets = playerActiveLiving.length > 0 || playerHealthyReserves.length > 0;
622
- const opponentHasUsablePiclets = opponentActiveLiving.length > 0 || opponentHealthyReserves.length > 0;
623
-
624
- // Check victory conditions based on type
625
- switch (this.victoryCondition.type) {
626
- case 'allFainted':
627
- if (!playerHasUsablePiclets) {
628
- if (!opponentHasUsablePiclets) {
629
- return 'draw';
630
- }
631
- return 'opponent';
632
- }
633
- if (!opponentHasUsablePiclets) {
634
- return 'player';
635
- }
636
- break;
637
-
638
- case 'custom':
639
- if (this.victoryCondition.customCheck) {
640
- return this.victoryCondition.customCheck(this.state);
641
- }
642
- break;
643
- }
644
-
645
- return null;
646
- }
647
-
648
- private getHealthyReserves(side: BattleSide): PicletDefinition[] {
649
- // Get party members that have never been used in battle
650
- // We need to track which party members have been on the field
651
- const usedPicletNames = new Set<string>();
652
-
653
- // Add currently active Piclets
654
- this.state.activePiclets[side].forEach(p => {
655
- if (p !== null) {
656
- usedPicletNames.add(p.definition.name);
657
- }
658
- });
659
-
660
- // For a full implementation, we would also track previously active Piclets that fainted
661
- // For now, we estimate by checking the initial setup - if there are more party members
662
- // than active slots, the rest are reserves
663
- const activeSlots = this.state.activePiclets[side].length;
664
- const initialActiveCount = Math.min(this.state.parties[side].length, activeSlots);
665
-
666
- // Mark the first N party members as "used" (they were initially active)
667
- for (let i = 0; i < initialActiveCount; i++) {
668
- if (this.state.parties[side][i]) {
669
- usedPicletNames.add(this.state.parties[side][i].name);
670
- }
671
- }
672
-
673
- return this.state.parties[side].filter(partyMember =>
674
- !usedPicletNames.has(partyMember.name)
675
- );
676
- }
677
-
678
- private logActivePiclets(): void {
679
- const playerActives = this.state.activePiclets.player.filter(p => p !== null) as BattlePiclet[];
680
- const opponentActives = this.state.activePiclets.opponent.filter(p => p !== null) as BattlePiclet[];
681
-
682
- this.log(`Player active: ${playerActives.map(p => p.definition.name).join(', ')}`);
683
- this.log(`Opponent active: ${opponentActives.map(p => p.definition.name).join(', ')}`);
684
- }
685
-
686
- private log(message: string): void {
687
- this.state.log.push(message);
688
- }
689
-
690
- public getLog(): string[] {
691
- // Strip battle prefixes from all log messages for display
692
- return this.state.log.map(message => this.stripBattlePrefixes(message));
693
- }
694
-
695
- private stripBattlePrefixes(message: string): string {
696
- // Remove player- and enemy- prefixes from messages
697
- return message
698
- .replace(/player-/g, '')
699
- .replace(/enemy-/g, '');
700
- }
701
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/README.md DELETED
@@ -1,232 +0,0 @@
1
- # Pictuary Battle Engine
2
-
3
- A standalone, testable battle system for the Pictuary game, implementing the battle mechanics defined in `battle_system_design.md`.
4
-
5
- ## Overview
6
-
7
- This battle engine provides a complete turn-based combat system implementing EVERYTHING from `battle_system_design.md`:
8
-
9
- - **Type effectiveness** based on Pictuary's photography-themed types (Beast, Bug, Aquatic, Flora, Mineral, Space, Machina, Structure, Culture, Cuisine)
10
- - **Composable effects system** with 10 different effect types
11
- - **Advanced damage formulas** (standard, recoil, drain, fixed, percentage)
12
- - **Mechanic override system** for special abilities that modify core game mechanics
13
- - **Trigger-based special abilities** with 18 different trigger events
14
- - **Status effects** with chance-based application and turn-end processing
15
- - **Field effects** with stackable/non-stackable variants
16
- - **PP manipulation** system (drain, restore, disable)
17
- - **Counter moves** and priority modification
18
- - **Conditional effects** with 25+ different conditions
19
- - **Extreme risk-reward moves** as defined in the design document
20
- - **Comprehensive test coverage** (116 tests across 7 test files)
21
-
22
- ## Architecture
23
-
24
- ### Core Components
25
-
26
- - **`BattleEngine.ts`** - Main battle orchestration and logic
27
- - **`types.ts`** - Type definitions for all battle system interfaces
28
- - **`test-data.ts`** - Example Piclets and moves for testing
29
- - **`*.test.ts`** - Comprehensive test suites
30
-
31
- ### Key Features
32
-
33
- 1. **Battle State Management**
34
- - Turn-based execution with proper phase handling
35
- - Action priority system (priority → speed → random)
36
- - Win condition checking and battle end logic
37
- - Field effects tracking and processing
38
-
39
- 2. **Advanced Damage System**
40
- - **Standard damage**: Traditional attack vs defense calculation with type effectiveness and STAB
41
- - **Recoil damage**: Self-harm after dealing damage (e.g., 25% recoil)
42
- - **Drain damage**: Heal user for portion of damage dealt (e.g., 50% drain)
43
- - **Fixed damage**: Exact damage amounts regardless of stats
44
- - **Percentage damage**: Damage based on target's max HP percentage
45
- - Type effectiveness calculations with dual-type support
46
- - STAB (Same Type Attack Bonus)
47
- - Accuracy checks with move-specific accuracy values
48
-
49
- 3. **Comprehensive Effect System**
50
- - **damage**: 5 different damage formulas with conditional scaling
51
- - **modifyStats**: Stat changes (increase/decrease/greatly_increase/greatly_decrease)
52
- - **applyStatus**: Status effects with configurable chance percentages
53
- - **heal**: Healing with amounts (small/medium/large/full) or percentage/fixed formulas
54
- - **manipulatePP**: PP drain, restore, or disable targeting specific moves
55
- - **fieldEffect**: Battlefield modifications affecting all combatants
56
- - **counter**: Delayed damage reflection based on incoming attack types
57
- - **priority**: Dynamic priority modification for moves
58
- - **removeStatus**: Cure specific status conditions
59
- - **mechanicOverride**: Fundamental game mechanic modifications
60
-
61
- 4. **Status Effects**
62
- - **Poison/Burn**: Turn-end damage (1/8 max HP)
63
- - **Paralysis/Sleep/Freeze**: Action prevention
64
- - **Confusion**: Self-targeting chance
65
- - **Chance-based application**: Configurable success rates (e.g., 30% freeze chance)
66
- - **Status immunity**: Abilities can grant immunity to specific statuses
67
- - **Status removal**: Moves and abilities can cure conditions
68
-
69
- 5. **Special Ability System**
70
- - **18 Trigger Events**: onDamageTaken, onSwitchIn, endOfTurn, onLowHP, etc.
71
- - **Conditional triggers**: Abilities activate based on HP thresholds, weather, status
72
- - **Multiple effects per trigger**: Complex abilities with layered effects
73
- - **Mechanic overrides**: Abilities that fundamentally change game rules
74
-
75
- 6. **Field Effects**
76
- - **Global effects**: Affect entire battlefield
77
- - **Side effects**: Affect one player's side only
78
- - **Stackable/Non-stackable**: Configurable effect layering
79
- - **Duration tracking**: Effects expire after set number of turns
80
-
81
- 7. **Move Flags System**
82
- - **Combat flags**: contact, bite, punch, sound, explosive, draining, ground
83
- - **Priority flags**: priority, lowPriority
84
- - **Mechanic flags**: charging, recharge, multiHit, twoTurn, sacrifice, gambling
85
- - **Interaction flags**: reflectable, snatchable, copyable, protectable, bypassProtect
86
- - **Flag immunity/weakness**: Abilities can modify interactions with flagged moves
87
-
88
- ## Usage
89
-
90
- ### Basic Battle Setup
91
-
92
- ```typescript
93
- import { BattleEngine } from './BattleEngine';
94
- import { STELLAR_WOLF, TOXIC_CRAWLER } from './test-data';
95
-
96
- // Create a new battle
97
- const battle = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
98
-
99
- // Execute a turn
100
- const playerAction = { type: 'move', piclet: 'player', moveIndex: 0 };
101
- const opponentAction = { type: 'move', piclet: 'opponent', moveIndex: 1 };
102
-
103
- battle.executeActions(playerAction, opponentAction);
104
-
105
- // Check battle state
106
- console.log(battle.getState());
107
- console.log(battle.getLog());
108
- console.log(battle.isGameOver(), battle.getWinner());
109
- ```
110
-
111
- ### Creating Custom Piclets
112
-
113
- ```typescript
114
- import { PicletDefinition, Move, PicletType, AttackType } from './types';
115
-
116
- const customMove: Move = {
117
- name: "Thunder Strike",
118
- type: AttackType.SPACE,
119
- power: 80,
120
- accuracy: 90,
121
- pp: 10,
122
- priority: 0,
123
- flags: ['explosive'],
124
- effects: [
125
- {
126
- type: 'damage',
127
- target: 'opponent',
128
- amount: 'strong'
129
- },
130
- {
131
- type: 'applyStatus',
132
- target: 'opponent',
133
- status: 'paralyze',
134
- condition: 'ifLucky50'
135
- }
136
- ]
137
- };
138
-
139
- const customPiclet: PicletDefinition = {
140
- name: "Storm Guardian",
141
- description: "A cosmic entity that commands lightning",
142
- tier: 'high',
143
- primaryType: PicletType.SPACE,
144
- baseStats: { hp: 120, attack: 100, defense: 90, speed: 85 },
145
- nature: "Bold",
146
- specialAbility: {
147
- name: "Lightning Rod",
148
- description: "Draws electric attacks and boosts power"
149
- },
150
- movepool: [customMove, /* other moves */]
151
- };
152
- ```
153
-
154
- ## Testing
155
-
156
- The battle engine includes comprehensive test coverage:
157
-
158
- ```bash
159
- # Run all battle engine tests
160
- npm test src/lib/battle-engine/
161
-
162
- # Run specific test file
163
- npm test src/lib/battle-engine/BattleEngine.test.ts
164
-
165
- # Run with UI
166
- npm run test:ui
167
- ```
168
-
169
- ### Test Categories
170
-
171
- - **Unit Tests** (`BattleEngine.test.ts`)
172
- - Battle initialization
173
- - Basic battle flow
174
- - Damage calculations
175
- - Status effects
176
- - Stat modifications
177
- - Healing effects
178
- - Conditional effects
179
- - Battle end conditions
180
- - Move accuracy
181
- - Action priority
182
-
183
- - **Integration Tests** (`integration.test.ts`)
184
- - Complete battle scenarios
185
- - Multi-turn battles with complex interactions
186
- - Performance and stability tests
187
- - Edge cases
188
-
189
- ## Design Principles
190
-
191
- Following the battle system design document:
192
-
193
- 1. **Simple JSON Schema** - Moves are defined with conceptual effect levels (weak/normal/strong/extreme) rather than specific numeric values
194
- 2. **Composable Effects** - Multiple effects per move with conditional triggers
195
- 3. **Bold and Dramatic** - Effects can be powerful with interesting tradeoffs
196
- 4. **Type-Driven** - Photography-themed types with meaningful interactions
197
- 5. **Special Abilities** - Passive traits that transform gameplay
198
-
199
- ## Integration with Main App
200
-
201
- This module is designed to be eventually imported into the main Svelte app:
202
-
203
- ```typescript
204
- // In Battle.svelte
205
- import { BattleEngine } from '$lib/battle-engine/BattleEngine';
206
- import type { PicletDefinition } from '$lib/battle-engine/types';
207
-
208
- // Convert PicletInstance to PicletDefinition format
209
- // Initialize battle engine
210
- // Replace existing battle logic
211
- ```
212
-
213
- ## Future Enhancements
214
-
215
- Planned features following the design document:
216
-
217
- - [ ] Special ability trigger system
218
- - [ ] Field effects and weather
219
- - [ ] Counter moves and priority manipulation
220
- - [ ] PP manipulation effects
221
- - [ ] Multi-target moves
222
- - [ ] Switch actions and party management
223
- - [ ] Critical hit calculations
224
- - [ ] More complex conditional effects
225
- - [ ] Battle replay system
226
-
227
- ## Performance Notes
228
-
229
- - Battle state is immutable (deep-cloned on `getState()`)
230
- - Efficient type effectiveness lookup using enums
231
- - Minimal memory allocation during battle execution
232
- - Tested for battles up to 100+ turns without performance issues
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/ability-triggers.test.ts DELETED
@@ -1,388 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition, SpecialAbility } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Special Ability Triggers System', () => {
7
- let basicPiclet: PicletDefinition;
8
- let abilityPiclet: PicletDefinition;
9
-
10
- beforeEach(() => {
11
- // Basic piclet without special abilities
12
- basicPiclet = {
13
- name: "Basic Fighter",
14
- description: "Standard test piclet",
15
- tier: 'medium',
16
- primaryType: PicletType.BEAST,
17
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 60 },
18
- nature: "Hardy",
19
- specialAbility: { name: "No Ability", description: "" },
20
- movepool: [
21
- {
22
- name: "Basic Attack",
23
- type: AttackType.BEAST,
24
- power: 50,
25
- accuracy: 100,
26
- pp: 20,
27
- priority: 0,
28
- flags: ['contact'],
29
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
30
- }
31
- ]
32
- };
33
-
34
- // Piclet with special abilities for testing
35
- abilityPiclet = {
36
- name: "Ability User",
37
- description: "Has special abilities",
38
- tier: 'medium',
39
- primaryType: PicletType.BEAST,
40
- baseStats: { hp: 100, attack: 70, defense: 70, speed: 50 },
41
- nature: "Bold",
42
- specialAbility: {
43
- name: "Test Ability",
44
- description: "Triggers on various events",
45
- triggers: [
46
- {
47
- event: 'onDamageTaken',
48
- condition: 'always',
49
- effects: [
50
- {
51
- type: 'modifyStats',
52
- target: 'self',
53
- stats: {
54
- attack: 'increase'
55
- }
56
- }
57
- ]
58
- }
59
- ]
60
- },
61
- movepool: [
62
- {
63
- name: "Power Strike",
64
- type: AttackType.BEAST,
65
- power: 60,
66
- accuracy: 100,
67
- pp: 15,
68
- priority: 0,
69
- flags: ['contact'],
70
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
71
- }
72
- ]
73
- };
74
- });
75
-
76
- describe('onDamageTaken Trigger', () => {
77
- it('should trigger when piclet takes damage', () => {
78
- const engine = new BattleEngine(abilityPiclet, basicPiclet);
79
- const initialAttack = engine.getState().playerPiclet.attack;
80
-
81
- // Opponent attacks, should trigger onDamageTaken
82
- engine.executeActions(
83
- { type: 'move', piclet: 'player', moveIndex: 0 },
84
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // This should damage player and trigger ability
85
- );
86
-
87
- const finalAttack = engine.getState().playerPiclet.attack;
88
- const log = engine.getLog();
89
-
90
- // Attack should have increased due to ability trigger
91
- expect(finalAttack).toBeGreaterThan(initialAttack);
92
- expect(log.some(msg => msg.includes('Test Ability') && msg.includes('triggered'))).toBe(true);
93
- });
94
- });
95
-
96
- describe('endOfTurn Trigger', () => {
97
- it('should trigger at the end of every turn', () => {
98
- const endTurnAbility: PicletDefinition = {
99
- ...abilityPiclet,
100
- specialAbility: {
101
- name: "Regeneration",
102
- description: "Heals at end of turn",
103
- triggers: [
104
- {
105
- event: 'endOfTurn',
106
- condition: 'always',
107
- effects: [
108
- {
109
- type: 'heal',
110
- target: 'self',
111
- amount: 'small'
112
- }
113
- ]
114
- }
115
- ]
116
- }
117
- };
118
-
119
- const engine = new BattleEngine(endTurnAbility, basicPiclet);
120
-
121
- // Damage the piclet first so healing is visible, but not too much
122
- engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.9);
123
- const initialHp = engine.getState().playerPiclet.currentHp;
124
-
125
- engine.executeActions(
126
- { type: 'move', piclet: 'player', moveIndex: 0 },
127
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
128
- );
129
-
130
- const log = engine.getLog();
131
- console.log('Regeneration test log:', log);
132
- console.log('Initial HP:', initialHp, 'Final HP:', engine.getState().playerPiclet.currentHp);
133
-
134
- // The ability should trigger (check log message)
135
- expect(log.some(msg => msg.includes('Regeneration') && msg.includes('triggered'))).toBe(true);
136
-
137
- // HP might decrease due to damage taken, but healing should have occurred
138
- expect(log.some(msg => msg.includes('recovered') || msg.includes('healed'))).toBe(true);
139
- });
140
- });
141
-
142
- describe('onDamageDealt Trigger', () => {
143
- it('should trigger when piclet deals damage to opponent', () => {
144
- const damageDealer: PicletDefinition = {
145
- ...abilityPiclet,
146
- specialAbility: {
147
- name: "Combat High",
148
- description: "Gains speed when dealing damage",
149
- triggers: [
150
- {
151
- event: 'onDamageDealt',
152
- condition: 'always',
153
- effects: [
154
- {
155
- type: 'modifyStats',
156
- target: 'self',
157
- stats: {
158
- speed: 'increase'
159
- }
160
- }
161
- ]
162
- }
163
- ]
164
- }
165
- };
166
-
167
- const engine = new BattleEngine(damageDealer, basicPiclet);
168
- const initialSpeed = engine.getState().playerPiclet.speed;
169
-
170
- engine.executeActions(
171
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Player deals damage
172
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
173
- );
174
-
175
- const finalSpeed = engine.getState().playerPiclet.speed;
176
- const log = engine.getLog();
177
-
178
- expect(finalSpeed).toBeGreaterThan(initialSpeed);
179
- expect(log.some(msg => msg.includes('Combat High') && msg.includes('triggered'))).toBe(true);
180
- });
181
- });
182
-
183
- describe('onCriticalHit Trigger', () => {
184
- it('should trigger when dealing a critical hit', () => {
185
- const criticalHitter: PicletDefinition = {
186
- ...abilityPiclet,
187
- specialAbility: {
188
- name: "Critical Momentum",
189
- description: "Gains attack on critical hits",
190
- triggers: [
191
- {
192
- event: 'onCriticalHit',
193
- condition: 'always',
194
- effects: [
195
- {
196
- type: 'modifyStats',
197
- target: 'self',
198
- stats: {
199
- attack: 'increase'
200
- }
201
- }
202
- ]
203
- }
204
- ]
205
- }
206
- };
207
-
208
- const engine = new BattleEngine(criticalHitter, basicPiclet);
209
-
210
- // Force a critical hit for testing
211
- const originalRandom = Math.random;
212
- Math.random = () => 0.01; // Force critical hit (< 0.0625)
213
-
214
- const initialAttack = engine.getState().playerPiclet.attack;
215
-
216
- engine.executeActions(
217
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Should crit and trigger ability
218
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
219
- );
220
-
221
- // Restore original Math.random
222
- Math.random = originalRandom;
223
-
224
- const finalAttack = engine.getState().playerPiclet.attack;
225
- const log = engine.getLog();
226
-
227
- expect(log.some(msg => msg.includes('A critical hit!'))).toBe(true);
228
- expect(finalAttack).toBeGreaterThan(initialAttack);
229
- expect(log.some(msg => msg.includes('Critical Momentum') && msg.includes('triggered'))).toBe(true);
230
- });
231
- });
232
-
233
- describe('onContactDamage Trigger', () => {
234
- it('should trigger only when hit by contact moves', () => {
235
- const contactSensitive: PicletDefinition = {
236
- ...abilityPiclet,
237
- specialAbility: {
238
- name: "Spiky Skin",
239
- description: "Hurts attackers that make contact",
240
- triggers: [
241
- {
242
- event: 'onContactDamage',
243
- condition: 'always',
244
- effects: [
245
- {
246
- type: 'damage',
247
- target: 'opponent',
248
- amount: 'small'
249
- }
250
- ]
251
- }
252
- ]
253
- }
254
- };
255
-
256
- const engine = new BattleEngine(contactSensitive, basicPiclet);
257
- const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
258
-
259
- engine.executeActions(
260
- { type: 'move', piclet: 'player', moveIndex: 0 },
261
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Contact move should trigger Spiky Skin
262
- );
263
-
264
- const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
265
- const log = engine.getLog();
266
-
267
- // Opponent should take extra damage from Spiky Skin
268
- expect(log.some(msg => msg.includes('Spiky Skin') && msg.includes('triggered'))).toBe(true);
269
-
270
- // The opponent should have taken damage from both the regular attack and the ability
271
- expect(finalOpponentHp).toBeLessThan(initialOpponentHp);
272
- });
273
- });
274
-
275
- describe('Conditional Triggers', () => {
276
- it('should respect ifLowHp condition', () => {
277
- const conditionalAbility: PicletDefinition = {
278
- ...abilityPiclet,
279
- specialAbility: {
280
- name: "Desperation",
281
- description: "Only triggers when HP is low",
282
- triggers: [
283
- {
284
- event: 'onDamageTaken',
285
- condition: 'ifLowHp',
286
- effects: [
287
- {
288
- type: 'modifyStats',
289
- target: 'self',
290
- stats: {
291
- attack: 'greatly_increase'
292
- }
293
- }
294
- ]
295
- }
296
- ]
297
- }
298
- };
299
-
300
- const engine = new BattleEngine(conditionalAbility, basicPiclet);
301
- const initialAttack = engine.getState().playerPiclet.attack;
302
-
303
- // At high HP, condition should not be met
304
- engine.executeActions(
305
- { type: 'move', piclet: 'player', moveIndex: 0 },
306
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
307
- );
308
-
309
- const midAttack = engine.getState().playerPiclet.attack;
310
- expect(midAttack).toBe(initialAttack); // No trigger due to condition
311
-
312
- // Create a new engine for the low HP test
313
- const lowHpEngine = new BattleEngine(conditionalAbility, basicPiclet);
314
-
315
- // Set HP low and trigger the ability
316
- lowHpEngine['state'].playerPiclet.currentHp = Math.floor(lowHpEngine['state'].playerPiclet.maxHp * 0.15);
317
-
318
- lowHpEngine.executeActions(
319
- { type: 'move', piclet: 'player', moveIndex: 0 },
320
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
321
- );
322
-
323
- const finalAttack = lowHpEngine.getState().playerPiclet.attack;
324
- const log = lowHpEngine.getLog();
325
-
326
- expect(finalAttack).toBeGreaterThan(initialAttack);
327
- expect(log.some(msg => msg.includes('Desperation') && msg.includes('triggered'))).toBe(true);
328
- });
329
- });
330
-
331
- describe('Multiple Triggers on Same Ability', () => {
332
- it('should handle multiple triggers on the same ability', () => {
333
- const multiTriggerAbility: PicletDefinition = {
334
- ...abilityPiclet,
335
- specialAbility: {
336
- name: "Adaptive Fighter",
337
- description: "Multiple trigger conditions",
338
- triggers: [
339
- {
340
- event: 'onDamageTaken',
341
- condition: 'always',
342
- effects: [
343
- {
344
- type: 'modifyStats',
345
- target: 'self',
346
- stats: {
347
- defense: 'increase'
348
- }
349
- }
350
- ]
351
- },
352
- {
353
- event: 'onDamageDealt',
354
- condition: 'always',
355
- effects: [
356
- {
357
- type: 'modifyStats',
358
- target: 'self',
359
- stats: {
360
- attack: 'increase'
361
- }
362
- }
363
- ]
364
- }
365
- ]
366
- }
367
- };
368
-
369
- const engine = new BattleEngine(multiTriggerAbility, basicPiclet);
370
- const initialAttack = engine.getState().playerPiclet.attack;
371
- const initialDefense = engine.getState().playerPiclet.defense;
372
-
373
- engine.executeActions(
374
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Should trigger onDamageDealt
375
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should trigger onDamageTaken
376
- );
377
-
378
- const finalAttack = engine.getState().playerPiclet.attack;
379
- const finalDefense = engine.getState().playerPiclet.defense;
380
- const log = engine.getLog();
381
-
382
- // Both stats should increase
383
- expect(finalAttack).toBeGreaterThan(initialAttack);
384
- expect(finalDefense).toBeGreaterThan(initialDefense);
385
- expect(log.some(msg => msg.includes('Adaptive Fighter') && msg.includes('triggered'))).toBe(true);
386
- });
387
- });
388
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/advanced-effects.test.ts DELETED
@@ -1,613 +0,0 @@
1
- /**
2
- * Tests for advanced battle effects from the design document
3
- * Covers all missing functionality that needs to be implemented
4
- */
5
-
6
- import { describe, it, expect, beforeEach } from 'vitest';
7
- import { BattleEngine } from './BattleEngine';
8
- import { PicletDefinition, Move, SpecialAbility } from './types';
9
- import { PicletType, AttackType } from './types';
10
-
11
- // Test data for advanced effects
12
- const STANDARD_STATS = { hp: 100, attack: 80, defense: 70, speed: 60 };
13
-
14
- describe('Advanced Battle Effects - TDD Implementation', () => {
15
- describe('Damage Formula System', () => {
16
- it('should handle recoil damage moves', () => {
17
- const recoilMove: Move = {
18
- name: "Reckless Dive",
19
- type: AttackType.SPACE,
20
- power: 120,
21
- accuracy: 100,
22
- pp: 5,
23
- priority: 0,
24
- flags: ['contact', 'reckless'],
25
- effects: [
26
- {
27
- type: 'damage',
28
- target: 'opponent',
29
- amount: 'strong'
30
- },
31
- {
32
- type: 'damage',
33
- target: 'self',
34
- formula: 'recoil',
35
- value: 0.25
36
- }
37
- ]
38
- };
39
-
40
- const testPiclet: PicletDefinition = {
41
- name: "Recoil Tester",
42
- description: "Tests recoil moves",
43
- tier: 'medium',
44
- primaryType: PicletType.SPACE,
45
- baseStats: STANDARD_STATS,
46
- nature: "Bold",
47
- specialAbility: { name: "None", description: "No ability" },
48
- movepool: [recoilMove]
49
- };
50
-
51
- const targetPiclet: PicletDefinition = {
52
- name: "Target",
53
- description: "Target dummy",
54
- tier: 'medium',
55
- primaryType: PicletType.BEAST,
56
- baseStats: STANDARD_STATS,
57
- nature: "Hardy",
58
- specialAbility: { name: "None", description: "No ability" },
59
- movepool: [{
60
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
61
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
62
- }]
63
- };
64
-
65
- const engine = new BattleEngine(testPiclet, targetPiclet);
66
- const initialHp = engine.getState().playerPiclet.currentHp;
67
-
68
- engine.executeActions(
69
- { type: 'move', piclet: 'player', moveIndex: 0 },
70
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
71
- );
72
-
73
- const finalHp = engine.getState().playerPiclet.currentHp;
74
- expect(finalHp).toBeLessThan(initialHp); // Should have taken recoil damage
75
- });
76
-
77
- it('should handle drain damage moves', () => {
78
- const drainMove: Move = {
79
- name: "Spectral Drain",
80
- type: AttackType.CULTURE,
81
- power: 60,
82
- accuracy: 100,
83
- pp: 10,
84
- priority: 0,
85
- flags: ['draining'],
86
- effects: [
87
- {
88
- type: 'damage',
89
- target: 'opponent',
90
- formula: 'drain',
91
- value: 0.5
92
- }
93
- ]
94
- };
95
-
96
- const testPiclet: PicletDefinition = {
97
- name: "Drain Tester",
98
- description: "Tests drain moves",
99
- tier: 'medium',
100
- primaryType: PicletType.CULTURE,
101
- baseStats: STANDARD_STATS,
102
- nature: "Bold",
103
- specialAbility: { name: "None", description: "No ability" },
104
- movepool: [drainMove]
105
- };
106
-
107
- const targetPiclet: PicletDefinition = {
108
- name: "Target",
109
- description: "Target dummy",
110
- tier: 'medium',
111
- primaryType: PicletType.BEAST,
112
- baseStats: STANDARD_STATS,
113
- nature: "Hardy",
114
- specialAbility: { name: "None", description: "No ability" },
115
- movepool: [{
116
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
117
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
118
- }]
119
- };
120
-
121
- const engine = new BattleEngine(testPiclet, targetPiclet);
122
-
123
- // Damage the user first to test healing
124
- engine['state'].playerPiclet.currentHp = 50;
125
- const initialHp = engine.getState().playerPiclet.currentHp;
126
-
127
- engine.executeActions(
128
- { type: 'move', piclet: 'player', moveIndex: 0 },
129
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
130
- );
131
-
132
- const log = engine.getLog();
133
- const hasHealingMessage = log.some(msg => msg.includes('recovered') && msg.includes('HP from draining'));
134
- expect(hasHealingMessage).toBe(true); // Should have healed from drain
135
- });
136
-
137
- it('should handle fixed damage moves', () => {
138
- const fixedMove: Move = {
139
- name: "Fixed Strike",
140
- type: AttackType.NORMAL,
141
- power: 0,
142
- accuracy: 100,
143
- pp: 10,
144
- priority: 0,
145
- flags: [],
146
- effects: [
147
- {
148
- type: 'damage',
149
- target: 'opponent',
150
- formula: 'fixed',
151
- value: 50
152
- }
153
- ]
154
- };
155
-
156
- // Test implementation would verify exactly 50 damage dealt
157
- expect(fixedMove.effects[0].formula).toBe('fixed');
158
- expect(fixedMove.effects[0].value).toBe(50);
159
- });
160
-
161
- it('should handle percentage damage moves', () => {
162
- const percentMove: Move = {
163
- name: "Percentage Strike",
164
- type: AttackType.NORMAL,
165
- power: 0,
166
- accuracy: 100,
167
- pp: 5,
168
- priority: 0,
169
- flags: [],
170
- effects: [
171
- {
172
- type: 'damage',
173
- target: 'opponent',
174
- formula: 'percentage',
175
- value: 25 // 25% of target's max HP
176
- }
177
- ]
178
- };
179
-
180
- // Test implementation would verify percentage-based damage
181
- expect(percentMove.effects[0].formula).toBe('percentage');
182
- expect(percentMove.effects[0].value).toBe(25);
183
- });
184
- });
185
-
186
- describe('PP Manipulation System', () => {
187
- it('should handle PP drain moves', () => {
188
- const ppDrainMove: Move = {
189
- name: "Mind Drain",
190
- type: AttackType.CULTURE,
191
- power: 40,
192
- accuracy: 100,
193
- pp: 15,
194
- priority: 0,
195
- flags: [],
196
- effects: [
197
- {
198
- type: 'damage',
199
- target: 'opponent',
200
- amount: 'normal'
201
- },
202
- {
203
- type: 'manipulatePP',
204
- target: 'opponent',
205
- action: 'drain',
206
- amount: 'medium'
207
- }
208
- ]
209
- };
210
-
211
- // Test would verify PP is drained from opponent's moves
212
- expect(ppDrainMove.effects[1].type).toBe('manipulatePP');
213
- expect(ppDrainMove.effects[1].action).toBe('drain');
214
- });
215
-
216
- it('should handle PP restore moves', () => {
217
- const ppRestoreMove: Move = {
218
- name: "Restore Energy",
219
- type: AttackType.NORMAL,
220
- power: 0,
221
- accuracy: 100,
222
- pp: 5,
223
- priority: 0,
224
- flags: [],
225
- effects: [
226
- {
227
- type: 'manipulatePP',
228
- target: 'self',
229
- action: 'restore',
230
- amount: 'large'
231
- }
232
- ]
233
- };
234
-
235
- // Test would verify PP is restored to self
236
- expect(ppRestoreMove.effects[0].type).toBe('manipulatePP');
237
- expect(ppRestoreMove.effects[0].action).toBe('restore');
238
- });
239
-
240
- it('should handle specific PP manipulation', () => {
241
- const specificPPMove: Move = {
242
- name: "Soul Burn",
243
- type: AttackType.SPACE,
244
- power: 150,
245
- accuracy: 90,
246
- pp: 5,
247
- priority: 0,
248
- flags: [],
249
- effects: [
250
- {
251
- type: 'damage',
252
- target: 'opponent',
253
- amount: 'extreme'
254
- },
255
- {
256
- type: 'manipulatePP',
257
- target: 'self',
258
- action: 'drain',
259
- value: 3,
260
- targetMove: 'random',
261
- condition: 'afterUse'
262
- }
263
- ]
264
- };
265
-
266
- // Test would verify specific PP amounts are drained
267
- expect(specificPPMove.effects[1].value).toBe(3);
268
- expect(specificPPMove.effects[1].targetMove).toBe('random');
269
- });
270
- });
271
-
272
- describe('Field Effects System', () => {
273
- it('should handle field-wide effects', () => {
274
- const fieldMove: Move = {
275
- name: "Void Storm",
276
- type: AttackType.SPACE,
277
- power: 0,
278
- accuracy: 100,
279
- pp: 5,
280
- priority: 0,
281
- flags: [],
282
- effects: [
283
- {
284
- type: 'fieldEffect',
285
- effect: 'voidStorm',
286
- target: 'field',
287
- stackable: false
288
- }
289
- ]
290
- };
291
-
292
- // Test would verify field effects are applied and tracked
293
- expect(fieldMove.effects[0].type).toBe('fieldEffect');
294
- expect(fieldMove.effects[0].target).toBe('field');
295
- });
296
-
297
- it('should handle side-specific effects', () => {
298
- const sideMove: Move = {
299
- name: "Healing Mist",
300
- type: AttackType.FLORA,
301
- power: 0,
302
- accuracy: 100,
303
- pp: 10,
304
- priority: 0,
305
- flags: [],
306
- effects: [
307
- {
308
- type: 'fieldEffect',
309
- effect: 'healingMist',
310
- target: 'playerSide',
311
- stackable: true
312
- }
313
- ]
314
- };
315
-
316
- // Test would verify side effects work correctly
317
- expect(sideMove.effects[0].target).toBe('playerSide');
318
- expect(sideMove.effects[0].stackable).toBe(true);
319
- });
320
- });
321
-
322
- describe('Counter Move System', () => {
323
- it('should handle physical counter moves', () => {
324
- const counterMove: Move = {
325
- name: "Counter Strike",
326
- type: AttackType.NORMAL,
327
- power: 0,
328
- accuracy: 100,
329
- pp: 20,
330
- priority: -5,
331
- flags: ['lowPriority'],
332
- effects: [
333
- {
334
- type: 'counter',
335
- strength: 'strong'
336
- }
337
- ]
338
- };
339
-
340
- // Test would verify counter moves work against any attacks
341
- expect(counterMove.effects[0].type).toBe('counter');
342
- expect(counterMove.effects[0].strength).toBe('strong');
343
- });
344
-
345
- it('should handle special counter moves', () => {
346
- const specialCounterMove: Move = {
347
- name: "Mirror Coat",
348
- type: AttackType.CULTURE,
349
- power: 0,
350
- accuracy: 100,
351
- pp: 20,
352
- priority: -5,
353
- flags: ['lowPriority'],
354
- effects: [
355
- {
356
- type: 'counter',
357
- strength: 'strong'
358
- }
359
- ]
360
- };
361
-
362
- expect(specialCounterMove.effects[0].strength).toBe('strong');
363
- });
364
- });
365
-
366
- describe('Priority Manipulation', () => {
367
- it('should handle priority-changing effects', () => {
368
- const priorityMove: Move = {
369
- name: "Quick Strike",
370
- type: AttackType.NORMAL,
371
- power: 40,
372
- accuracy: 100,
373
- pp: 30,
374
- priority: 0,
375
- flags: [],
376
- effects: [
377
- {
378
- type: 'damage',
379
- target: 'opponent',
380
- amount: 'weak'
381
- },
382
- {
383
- type: 'priority',
384
- target: 'self',
385
- value: 1,
386
- condition: 'ifLowHp'
387
- }
388
- ]
389
- };
390
-
391
- // Test would verify priority changes based on conditions
392
- expect(priorityMove.effects[1].type).toBe('priority');
393
- expect(priorityMove.effects[1].value).toBe(1);
394
- });
395
- });
396
-
397
- describe('Status Chance System', () => {
398
- it('should handle status moves with specific chances', () => {
399
- const chanceStatusMove: Move = {
400
- name: "Thunder Wave",
401
- type: AttackType.SPACE,
402
- power: 0,
403
- accuracy: 90,
404
- pp: 20,
405
- priority: 0,
406
- flags: [],
407
- effects: [
408
- {
409
- type: 'applyStatus',
410
- target: 'opponent',
411
- status: 'paralyze',
412
- chance: 100
413
- }
414
- ]
415
- };
416
-
417
- expect(chanceStatusMove.effects[0].chance).toBe(100);
418
- });
419
-
420
- it('should handle partial chance status effects', () => {
421
- const partialChanceMove: Move = {
422
- name: "Ice Touch",
423
- type: AttackType.MINERAL,
424
- power: 60,
425
- accuracy: 100,
426
- pp: 20,
427
- priority: 0,
428
- flags: ['contact'],
429
- effects: [
430
- {
431
- type: 'damage',
432
- target: 'opponent',
433
- amount: 'normal'
434
- },
435
- {
436
- type: 'applyStatus',
437
- target: 'opponent',
438
- status: 'freeze',
439
- chance: 30
440
- }
441
- ]
442
- };
443
-
444
- expect(partialChanceMove.effects[1].chance).toBe(30);
445
- });
446
- });
447
-
448
- describe('Percentage-based Healing', () => {
449
- it('should handle percentage healing moves', () => {
450
- const percentHealMove: Move = {
451
- name: "Recovery",
452
- type: AttackType.NORMAL,
453
- power: 0,
454
- accuracy: 100,
455
- pp: 10,
456
- priority: 0,
457
- flags: [],
458
- effects: [
459
- {
460
- type: 'heal',
461
- target: 'self',
462
- formula: 'percentage',
463
- value: 50 // 50% of max HP
464
- }
465
- ]
466
- };
467
-
468
- expect(percentHealMove.effects[0].formula).toBe('percentage');
469
- expect(percentHealMove.effects[0].value).toBe(50);
470
- });
471
-
472
- it('should handle fixed healing moves', () => {
473
- const fixedHealMove: Move = {
474
- name: "First Aid",
475
- type: AttackType.NORMAL,
476
- power: 0,
477
- accuracy: 100,
478
- pp: 15,
479
- priority: 0,
480
- flags: [],
481
- effects: [
482
- {
483
- type: 'heal',
484
- target: 'self',
485
- formula: 'fixed',
486
- value: 25 // Heal exactly 25 HP
487
- }
488
- ]
489
- };
490
-
491
- expect(fixedHealMove.effects[0].formula).toBe('fixed');
492
- expect(fixedHealMove.effects[0].value).toBe(25);
493
- });
494
- });
495
-
496
- describe('Extended Condition System', () => {
497
- it('should handle type-specific conditions', () => {
498
- const typeConditionMove: Move = {
499
- name: "Flora Boost",
500
- type: AttackType.FLORA,
501
- power: 60,
502
- accuracy: 100,
503
- pp: 15,
504
- priority: 0,
505
- flags: [],
506
- effects: [
507
- {
508
- type: 'damage',
509
- target: 'opponent',
510
- amount: 'normal'
511
- },
512
- {
513
- type: 'modifyStats',
514
- target: 'self',
515
- stats: { attack: 'increase' },
516
- condition: 'ifMoveType:flora'
517
- }
518
- ]
519
- };
520
-
521
- expect(typeConditionMove.effects[1].condition).toBe('ifMoveType:flora');
522
- });
523
-
524
- it('should handle status-specific conditions', () => {
525
- const statusConditionMove: Move = {
526
- name: "Burn Power",
527
- type: AttackType.SPACE,
528
- power: 80,
529
- accuracy: 100,
530
- pp: 10,
531
- priority: 0,
532
- flags: [],
533
- effects: [
534
- {
535
- type: 'damage',
536
- target: 'opponent',
537
- amount: 'strong',
538
- condition: 'ifStatus:burn'
539
- }
540
- ]
541
- };
542
-
543
- expect(statusConditionMove.effects[0].condition).toBe('ifStatus:burn');
544
- });
545
-
546
- it('should handle weather-specific conditions', () => {
547
- const weatherConditionMove: Move = {
548
- name: "Storm Strike",
549
- type: AttackType.SPACE,
550
- power: 70,
551
- accuracy: 95,
552
- pp: 15,
553
- priority: 0,
554
- flags: [],
555
- effects: [
556
- {
557
- type: 'damage',
558
- target: 'opponent',
559
- amount: 'strong',
560
- condition: 'ifWeather:storm'
561
- }
562
- ]
563
- };
564
-
565
- expect(weatherConditionMove.effects[0].condition).toBe('ifWeather:storm');
566
- });
567
- });
568
-
569
- describe('Remove Status Effects', () => {
570
- it('should handle status removal moves', () => {
571
- const removeStatusMove: Move = {
572
- name: "Cleanse",
573
- type: AttackType.NORMAL,
574
- power: 0,
575
- accuracy: 100,
576
- pp: 15,
577
- priority: 0,
578
- flags: [],
579
- effects: [
580
- {
581
- type: 'removeStatus',
582
- target: 'self',
583
- status: 'poison'
584
- }
585
- ]
586
- };
587
-
588
- expect(removeStatusMove.effects[0].type).toBe('removeStatus');
589
- expect(removeStatusMove.effects[0].status).toBe('poison');
590
- });
591
-
592
- it('should handle multi-target status removal', () => {
593
- const teamCleanseMove: Move = {
594
- name: "Team Cleanse",
595
- type: AttackType.NORMAL,
596
- power: 0,
597
- accuracy: 100,
598
- pp: 5,
599
- priority: 0,
600
- flags: [],
601
- effects: [
602
- {
603
- type: 'removeStatus',
604
- target: 'allies',
605
- status: 'confuse'
606
- }
607
- ]
608
- };
609
-
610
- expect(teamCleanseMove.effects[0].target).toBe('allies');
611
- });
612
- });
613
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/advanced-mechanic-overrides.test.ts DELETED
@@ -1,348 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition, SpecialAbility } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Advanced Mechanic Override System', () => {
7
- describe('Critical Hit Mechanics', () => {
8
- it('should prevent critical hits with Shell Armor ability', () => {
9
- const shellArmor: SpecialAbility = {
10
- name: "Shell Armor",
11
- description: "Hard shell prevents critical hits",
12
- effects: [
13
- {
14
- type: 'mechanicOverride',
15
- mechanic: 'criticalHits',
16
- condition: 'always',
17
- value: false
18
- }
19
- ]
20
- };
21
-
22
- const defender: PicletDefinition = {
23
- name: "Shell Defender",
24
- description: "Protected by hard shell",
25
- tier: 'medium',
26
- primaryType: PicletType.MINERAL,
27
- baseStats: { hp: 80, attack: 60, defense: 80, speed: 40 },
28
- nature: "Impish",
29
- specialAbility: shellArmor,
30
- movepool: [
31
- {
32
- name: "Defense Curl",
33
- type: AttackType.NORMAL,
34
- power: 0,
35
- accuracy: 100,
36
- pp: 10,
37
- priority: 0,
38
- flags: [],
39
- effects: [
40
- {
41
- type: 'modifyStats',
42
- target: 'self',
43
- stats: { defense: 'increase' }
44
- }
45
- ]
46
- }
47
- ]
48
- };
49
-
50
- const attacker: PicletDefinition = {
51
- name: "High Crit Attacker",
52
- description: "Has high critical hit rate",
53
- tier: 'medium',
54
- primaryType: PicletType.BEAST,
55
- baseStats: { hp: 70, attack: 90, defense: 50, speed: 80 },
56
- nature: "Adamant",
57
- specialAbility: { name: "No Ability", description: "" },
58
- movepool: [
59
- {
60
- name: "Slash",
61
- type: AttackType.BEAST,
62
- power: 70,
63
- accuracy: 100,
64
- pp: 10,
65
- priority: 0,
66
- flags: ['contact'],
67
- effects: [
68
- {
69
- type: 'damage',
70
- target: 'opponent',
71
- amount: 'normal'
72
- }
73
- ]
74
- }
75
- ]
76
- };
77
-
78
- const engine = new BattleEngine(defender, attacker);
79
-
80
- // Force high crit rate for testing
81
- const originalCritRate = (engine as any).calculateCriticalChance;
82
- (engine as any).calculateCriticalChance = () => 1.0; // 100% crit rate normally
83
-
84
- let criticalHitOccurred = false;
85
- for (let i = 0; i < 10; i++) {
86
- const initialHp = engine.getState().playerPiclet.currentHp;
87
-
88
- engine.executeActions(
89
- { type: 'move', piclet: 'player', moveIndex: 0 },
90
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
91
- );
92
-
93
- const log = engine.getLog();
94
- if (log.some(msg => msg.includes('critical') || msg.includes('Critical'))) {
95
- criticalHitOccurred = true;
96
- break;
97
- }
98
-
99
- if (engine.isGameOver()) break;
100
- }
101
-
102
- // Shell Armor should prevent ALL critical hits
103
- expect(criticalHitOccurred).toBe(false);
104
-
105
- // Restore original function
106
- (engine as any).calculateCriticalChance = originalCritRate;
107
- });
108
-
109
- it('should guarantee critical hits with certain abilities', () => {
110
- const alwaysCrit: SpecialAbility = {
111
- name: "Super Luck",
112
- description: "Always lands critical hits",
113
- effects: [
114
- {
115
- type: 'mechanicOverride',
116
- mechanic: 'criticalHits',
117
- condition: 'always',
118
- value: true
119
- }
120
- ]
121
- };
122
-
123
- const critUser: PicletDefinition = {
124
- name: "Lucky Fighter",
125
- description: "Always gets critical hits",
126
- tier: 'medium',
127
- primaryType: PicletType.BEAST,
128
- baseStats: { hp: 70, attack: 80, defense: 60, speed: 90 },
129
- nature: "Hasty",
130
- specialAbility: alwaysCrit,
131
- movepool: [
132
- {
133
- name: "Strike",
134
- type: AttackType.BEAST,
135
- power: 60,
136
- accuracy: 100,
137
- pp: 10,
138
- priority: 0,
139
- flags: ['contact'],
140
- effects: [
141
- {
142
- type: 'damage',
143
- target: 'opponent',
144
- amount: 'normal'
145
- }
146
- ]
147
- }
148
- ]
149
- };
150
-
151
- const opponent: PicletDefinition = {
152
- name: "Opponent",
153
- description: "Standard opponent",
154
- tier: 'medium',
155
- primaryType: PicletType.BEAST,
156
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
157
- nature: "Hardy",
158
- specialAbility: { name: "No Ability", description: "" },
159
- movepool: [
160
- {
161
- name: "Tackle",
162
- type: AttackType.NORMAL,
163
- power: 40,
164
- accuracy: 100,
165
- pp: 10,
166
- priority: 0,
167
- flags: ['contact'],
168
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
169
- }
170
- ]
171
- };
172
-
173
- const engine = new BattleEngine(critUser, opponent);
174
-
175
- engine.executeActions(
176
- { type: 'move', piclet: 'player', moveIndex: 0 },
177
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
178
- );
179
-
180
- const log = engine.getLog();
181
- expect(log.some(msg => msg.includes('critical') || msg.includes('Critical'))).toBe(true);
182
- });
183
- });
184
-
185
- describe('Status Immunity', () => {
186
- it('should provide immunity to specific status effects', () => {
187
- const insomnia: SpecialAbility = {
188
- name: "Insomnia",
189
- description: "Prevents sleep status",
190
- effects: [
191
- {
192
- type: 'mechanicOverride',
193
- mechanic: 'statusImmunity',
194
- value: ['sleep']
195
- }
196
- ]
197
- };
198
-
199
- const insomniac: PicletDefinition = {
200
- name: "Sleepless Fighter",
201
- description: "Cannot be put to sleep",
202
- tier: 'medium',
203
- primaryType: PicletType.CULTURE,
204
- baseStats: { hp: 70, attack: 60, defense: 60, speed: 80 },
205
- nature: "Timid",
206
- specialAbility: insomnia,
207
- movepool: [
208
- {
209
- name: "Tackle",
210
- type: AttackType.NORMAL,
211
- power: 40,
212
- accuracy: 100,
213
- pp: 10,
214
- priority: 0,
215
- flags: ['contact'],
216
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
217
- }
218
- ]
219
- };
220
-
221
- const sleepUser: PicletDefinition = {
222
- name: "Sleep Inducer",
223
- description: "Puts opponents to sleep",
224
- tier: 'medium',
225
- primaryType: PicletType.CULTURE,
226
- baseStats: { hp: 80, attack: 50, defense: 70, speed: 60 },
227
- nature: "Calm",
228
- specialAbility: { name: "No Ability", description: "" },
229
- movepool: [
230
- {
231
- name: "Sleep Powder",
232
- type: AttackType.FLORA,
233
- power: 0,
234
- accuracy: 75,
235
- pp: 10,
236
- priority: 0,
237
- flags: [],
238
- effects: [
239
- {
240
- type: 'applyStatus',
241
- target: 'opponent',
242
- status: 'sleep'
243
- }
244
- ]
245
- }
246
- ]
247
- };
248
-
249
- const engine = new BattleEngine(insomniac, sleepUser);
250
-
251
- engine.executeActions(
252
- { type: 'move', piclet: 'player', moveIndex: 0 },
253
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
254
- );
255
-
256
- const log = engine.getLog();
257
- expect(log.some(msg => msg.includes('immune') || msg.includes('had no effect'))).toBe(true);
258
- expect(log.some(msg => msg.includes('fell asleep'))).toBe(false);
259
- });
260
- });
261
-
262
- describe('Type Immunity', () => {
263
- it('should provide immunity to specific attack types', () => {
264
- const levitate: SpecialAbility = {
265
- name: "Levitate",
266
- description: "Floating ability makes ground moves miss",
267
- effects: [
268
- {
269
- type: 'mechanicOverride',
270
- mechanic: 'typeImmunity',
271
- value: ['ground']
272
- }
273
- ]
274
- };
275
-
276
- const levitator: PicletDefinition = {
277
- name: "Floating Fighter",
278
- description: "Levitates above ground attacks",
279
- tier: 'medium',
280
- primaryType: PicletType.SPACE,
281
- baseStats: { hp: 75, attack: 70, defense: 60, speed: 85 },
282
- nature: "Timid",
283
- specialAbility: levitate,
284
- movepool: [
285
- {
286
- name: "Air Slash",
287
- type: AttackType.SPACE,
288
- power: 60,
289
- accuracy: 95,
290
- pp: 10,
291
- priority: 0,
292
- flags: [],
293
- effects: [
294
- {
295
- type: 'damage',
296
- target: 'opponent',
297
- amount: 'normal'
298
- }
299
- ]
300
- }
301
- ]
302
- };
303
-
304
- const groundUser: PicletDefinition = {
305
- name: "Ground Attacker",
306
- description: "Uses ground-based attacks",
307
- tier: 'medium',
308
- primaryType: PicletType.MINERAL,
309
- baseStats: { hp: 80, attack: 80, defense: 70, speed: 60 },
310
- nature: "Adamant",
311
- specialAbility: { name: "No Ability", description: "" },
312
- movepool: [
313
- {
314
- name: "Earthquake",
315
- type: AttackType.MINERAL,
316
- power: 100,
317
- accuracy: 100,
318
- pp: 10,
319
- priority: 0,
320
- flags: ['ground'],
321
- effects: [
322
- {
323
- type: 'damage',
324
- target: 'opponent',
325
- amount: 'strong'
326
- }
327
- ]
328
- }
329
- ]
330
- };
331
-
332
- const engine = new BattleEngine(levitator, groundUser);
333
- const initialHp = engine.getState().playerPiclet.currentHp;
334
-
335
- engine.executeActions(
336
- { type: 'move', piclet: 'player', moveIndex: 0 },
337
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
338
- );
339
-
340
- const finalHp = engine.getState().playerPiclet.currentHp;
341
- const log = engine.getLog();
342
-
343
- // Ground move should have no effect due to Levitate
344
- expect(finalHp).toBe(initialHp);
345
- expect(log.some(msg => msg.includes('had no effect') || msg.includes('immune'))).toBe(true);
346
- });
347
- });
348
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/advanced-status-effects.test.ts DELETED
@@ -1,369 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Advanced Status Effects System', () => {
7
- let basicPiclet: PicletDefinition;
8
- let statusInflicter: PicletDefinition;
9
-
10
- beforeEach(() => {
11
- // Basic piclet without special abilities
12
- basicPiclet = {
13
- name: "Basic Fighter",
14
- description: "Standard test piclet",
15
- tier: 'medium',
16
- primaryType: PicletType.BEAST,
17
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 60 },
18
- nature: "Hardy",
19
- specialAbility: { name: "No Ability", description: "" },
20
- movepool: [
21
- {
22
- name: "Basic Attack",
23
- type: AttackType.BEAST,
24
- power: 50,
25
- accuracy: 100,
26
- pp: 20,
27
- priority: 0,
28
- flags: ['contact'],
29
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
30
- }
31
- ]
32
- };
33
-
34
- // Piclet that can inflict status effects
35
- statusInflicter = {
36
- name: "Status Master",
37
- description: "Can inflict various status effects",
38
- tier: 'medium',
39
- primaryType: PicletType.CULTURE,
40
- baseStats: { hp: 90, attack: 50, defense: 70, speed: 80 },
41
- nature: "Timid",
42
- specialAbility: { name: "No Ability", description: "" },
43
- movepool: [
44
- {
45
- name: "Freeze Ray",
46
- type: AttackType.AQUATIC,
47
- power: 40,
48
- accuracy: 90,
49
- pp: 15,
50
- priority: 0,
51
- flags: [],
52
- effects: [
53
- { type: 'damage', target: 'opponent', amount: 'normal' },
54
- { type: 'applyStatus', target: 'opponent', status: 'freeze', chance: 30 }
55
- ]
56
- },
57
- {
58
- name: "Paralyzing Shock",
59
- type: AttackType.MACHINA,
60
- power: 45,
61
- accuracy: 100,
62
- pp: 20,
63
- priority: 0,
64
- flags: [],
65
- effects: [
66
- { type: 'damage', target: 'opponent', amount: 'normal' },
67
- { type: 'applyStatus', target: 'opponent', status: 'paralyze', chance: 25 }
68
- ]
69
- },
70
- {
71
- name: "Sleep Powder",
72
- type: AttackType.FLORA,
73
- power: 0,
74
- accuracy: 85,
75
- pp: 15,
76
- priority: 0,
77
- flags: [],
78
- effects: [
79
- { type: 'applyStatus', target: 'opponent', status: 'sleep', chance: 100 }
80
- ]
81
- },
82
- {
83
- name: "Confuse Ray",
84
- type: AttackType.SPACE,
85
- power: 0,
86
- accuracy: 100,
87
- pp: 10,
88
- priority: 0,
89
- flags: [],
90
- effects: [
91
- { type: 'applyStatus', target: 'opponent', status: 'confuse', chance: 100 }
92
- ]
93
- }
94
- ]
95
- };
96
- });
97
-
98
- describe('Freeze Status Effect', () => {
99
- it('should prevent the frozen piclet from acting', () => {
100
- const engine = new BattleEngine(statusInflicter, basicPiclet);
101
-
102
- // Force freeze to trigger by mocking Math.random
103
- const originalRandom = Math.random;
104
- Math.random = () => 0.1; // 10% < 30% chance, should trigger freeze
105
-
106
- engine.executeActions(
107
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Freeze Ray
108
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented if frozen
109
- );
110
-
111
- // Restore Math.random
112
- Math.random = originalRandom;
113
-
114
- const log = engine.getLog();
115
- const opponentState = engine.getState().opponentPiclet;
116
-
117
- // Check that freeze was applied
118
- expect(opponentState.statusEffects).toContain('freeze');
119
- expect(log.some(msg => msg.includes('was frozen'))).toBe(true);
120
-
121
- // Execute another turn to test freeze preventing action
122
- if (!engine.isGameOver()) {
123
- engine.executeActions(
124
- { type: 'move', piclet: 'player', moveIndex: 0 },
125
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
126
- );
127
-
128
- const secondTurnLog = engine.getLog();
129
- expect(secondTurnLog.some(msg => msg.includes('is frozen solid') || msg.includes('cannot move'))).toBe(true);
130
- }
131
- });
132
-
133
- it('should have a chance to thaw each turn', () => {
134
- const engine = new BattleEngine(statusInflicter, basicPiclet);
135
-
136
- // Manually apply freeze status
137
- engine['state'].opponentPiclet.statusEffects.push('freeze');
138
-
139
- // Force thaw with low random number
140
- const originalRandom = Math.random;
141
- Math.random = () => 0.1; // Should trigger thaw (usually 20% chance)
142
-
143
- engine.executeActions(
144
- { type: 'move', piclet: 'player', moveIndex: 0 },
145
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
146
- );
147
-
148
- // Restore Math.random
149
- Math.random = originalRandom;
150
-
151
- const log = engine.getLog();
152
- const opponentState = engine.getState().opponentPiclet;
153
-
154
- // Should thaw and be able to act
155
- expect(log.some(msg => msg.includes('thawed out') || msg.includes('is no longer frozen'))).toBe(true);
156
- expect(opponentState.statusEffects).not.toContain('freeze');
157
- });
158
- });
159
-
160
- describe('Paralysis Status Effect', () => {
161
- it('should reduce speed by 50%', () => {
162
- const engine = new BattleEngine(statusInflicter, basicPiclet);
163
- const initialSpeed = engine.getState().opponentPiclet.speed;
164
-
165
- // Force paralysis to trigger
166
- const originalRandom = Math.random;
167
- Math.random = () => 0.1; // 10% < 25% chance
168
-
169
- engine.executeActions(
170
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Paralyzing Shock
171
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
172
- );
173
-
174
- // Restore Math.random
175
- Math.random = originalRandom;
176
-
177
- const finalSpeed = engine.getState().opponentPiclet.speed;
178
- const log = engine.getLog();
179
-
180
- expect(engine.getState().opponentPiclet.statusEffects).toContain('paralyze');
181
- expect(log.some(msg => msg.includes('was paralyzed'))).toBe(true);
182
- expect(finalSpeed).toBe(Math.floor(initialSpeed * 0.5)); // 50% speed reduction
183
- });
184
-
185
- it('should have 25% chance to prevent action', () => {
186
- const engine = new BattleEngine(statusInflicter, basicPiclet);
187
-
188
- // Manually apply paralysis
189
- engine['state'].opponentPiclet.statusEffects.push('paralyze');
190
- engine['state'].opponentPiclet.speed = Math.floor(engine['state'].opponentPiclet.speed * 0.5);
191
-
192
- // Force paralysis to prevent action
193
- const originalRandom = Math.random;
194
- Math.random = () => 0.1; // Should trigger paralysis prevention (25% chance)
195
-
196
- engine.executeActions(
197
- { type: 'move', piclet: 'player', moveIndex: 0 },
198
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented
199
- );
200
-
201
- // Restore Math.random
202
- Math.random = originalRandom;
203
-
204
- const log = engine.getLog();
205
- expect(log.some(msg =>
206
- msg.includes('is fully paralyzed') ||
207
- msg.includes('cannot move due to paralysis')
208
- )).toBe(true);
209
- });
210
- });
211
-
212
- describe('Sleep Status Effect', () => {
213
- it('should prevent action and last 1-3 turns', () => {
214
- const engine = new BattleEngine(statusInflicter, basicPiclet);
215
-
216
- engine.executeActions(
217
- { type: 'move', piclet: 'player', moveIndex: 2 }, // Sleep Powder
218
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should be prevented if asleep
219
- );
220
-
221
- const log = engine.getLog();
222
- const opponentState = engine.getState().opponentPiclet;
223
-
224
- expect(opponentState.statusEffects).toContain('sleep');
225
- expect(log.some(msg => msg.includes('fell asleep'))).toBe(true);
226
-
227
- // Sleep should prevent action
228
- if (!engine.isGameOver()) {
229
- engine.executeActions(
230
- { type: 'move', piclet: 'player', moveIndex: 0 },
231
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
232
- );
233
-
234
- const secondLog = engine.getLog();
235
- expect(secondLog.some(msg =>
236
- msg.includes('is fast asleep') ||
237
- msg.includes('cannot wake up')
238
- )).toBe(true);
239
- }
240
- });
241
-
242
- it('should wake up when attacked', () => {
243
- const engine = new BattleEngine(basicPiclet, statusInflicter);
244
-
245
- // Put player to sleep
246
- engine['state'].playerPiclet.statusEffects.push('sleep');
247
-
248
- engine.executeActions(
249
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Should be prevented by sleep
250
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Attack should wake up player
251
- );
252
-
253
- const log = engine.getLog();
254
- const playerState = engine.getState().playerPiclet;
255
-
256
- // Should wake up when damaged
257
- expect(log.some(msg => msg.includes('woke up'))).toBe(true);
258
- expect(playerState.statusEffects).not.toContain('sleep');
259
- });
260
- });
261
-
262
- describe('Confusion Status Effect', () => {
263
- it('should last 2-5 turns and cause self-damage 33% of the time', () => {
264
- const engine = new BattleEngine(statusInflicter, basicPiclet);
265
-
266
- engine.executeActions(
267
- { type: 'move', piclet: 'player', moveIndex: 3 }, // Confuse Ray
268
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
269
- );
270
-
271
- const log = engine.getLog();
272
- const opponentState = engine.getState().opponentPiclet;
273
-
274
- expect(opponentState.statusEffects).toContain('confuse');
275
- expect(log.some(msg => msg.includes('became confused'))).toBe(true);
276
-
277
- // Test confusion self-damage
278
- const initialHp = engine.getState().opponentPiclet.currentHp;
279
-
280
- // Force confusion self-damage
281
- const originalRandom = Math.random;
282
- Math.random = () => 0.2; // Should trigger self-damage (33% chance)
283
-
284
- if (!engine.isGameOver()) {
285
- engine.executeActions(
286
- { type: 'move', piclet: 'player', moveIndex: 0 },
287
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
288
- );
289
-
290
- const confusedLog = engine.getLog();
291
- const finalHp = engine.getState().opponentPiclet.currentHp;
292
-
293
- expect(confusedLog.some(msg =>
294
- msg.includes('hurt itself in confusion') ||
295
- msg.includes('attacked itself')
296
- )).toBe(true);
297
- }
298
-
299
- // Restore Math.random
300
- Math.random = originalRandom;
301
- });
302
-
303
- it('should wear off after 2-5 turns', () => {
304
- const engine = new BattleEngine(statusInflicter, basicPiclet);
305
-
306
- // Manually apply confusion with duration
307
- engine['state'].opponentPiclet.statusEffects.push('confuse');
308
- (engine['state'].opponentPiclet as any).confusionTurns = 1; // Set to expire next turn
309
-
310
- engine.executeActions(
311
- { type: 'move', piclet: 'player', moveIndex: 0 },
312
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
313
- );
314
-
315
- const log = engine.getLog();
316
- const opponentState = engine.getState().opponentPiclet;
317
-
318
- expect(log.some(msg => msg.includes('is no longer confused') || msg.includes('snapped out of confusion'))).toBe(true);
319
- expect(opponentState.statusEffects).not.toContain('confuse');
320
- });
321
- });
322
-
323
- describe('Status Effect Interactions', () => {
324
- it('should not allow multiple major status effects simultaneously', () => {
325
- const engine = new BattleEngine(statusInflicter, basicPiclet);
326
-
327
- // Apply freeze first
328
- engine['state'].opponentPiclet.statusEffects.push('freeze');
329
-
330
- // Try to apply paralysis
331
- const originalRandom = Math.random;
332
- Math.random = () => 0.1; // Should trigger paralysis normally
333
-
334
- engine.executeActions(
335
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Paralyzing Shock
336
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
337
- );
338
-
339
- Math.random = originalRandom;
340
-
341
- const opponentState = engine.getState().opponentPiclet;
342
- const majorStatuses = opponentState.statusEffects.filter(status =>
343
- ['freeze', 'paralyze', 'sleep'].includes(status)
344
- );
345
-
346
- // Should only have one major status effect
347
- expect(majorStatuses.length).toBeLessThanOrEqual(1);
348
- });
349
-
350
- it('should allow confusion alongside other status effects', () => {
351
- const engine = new BattleEngine(statusInflicter, basicPiclet);
352
-
353
- // Apply paralysis first
354
- engine['state'].opponentPiclet.statusEffects.push('paralyze');
355
-
356
- // Apply confusion
357
- engine.executeActions(
358
- { type: 'move', piclet: 'player', moveIndex: 3 }, // Confuse Ray
359
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
360
- );
361
-
362
- const opponentState = engine.getState().opponentPiclet;
363
-
364
- // Should have both paralysis and confusion
365
- expect(opponentState.statusEffects).toContain('paralyze');
366
- expect(opponentState.statusEffects).toContain('confuse');
367
- });
368
- });
369
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/debug-field-effects.test.ts DELETED
@@ -1,101 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Debug Field Effects', () => {
7
- it('should debug field effect creation and damage calculation', () => {
8
- const fieldUser: PicletDefinition = {
9
- name: "Field User",
10
- description: "Creates field effects",
11
- tier: 'medium',
12
- primaryType: PicletType.BEAST, // Changed to BEAST so opponent can damage it
13
- baseStats: { hp: 100, attack: 60, defense: 80, speed: 60 },
14
- nature: "Calm",
15
- specialAbility: { name: "No Ability", description: "" },
16
- movepool: [
17
- {
18
- name: "Contact Barrier",
19
- type: AttackType.SPACE,
20
- power: 0,
21
- accuracy: 100,
22
- pp: 10,
23
- priority: 0,
24
- flags: [],
25
- effects: [
26
- {
27
- type: 'fieldEffect',
28
- effect: 'reflect',
29
- target: 'playerSide',
30
- stackable: false
31
- }
32
- ]
33
- }
34
- ]
35
- };
36
-
37
- const attacker: PicletDefinition = {
38
- name: "Contact Attacker",
39
- description: "Uses contact moves",
40
- tier: 'medium',
41
- primaryType: PicletType.BEAST,
42
- baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 },
43
- nature: "Adamant",
44
- specialAbility: { name: "No Ability", description: "" },
45
- movepool: [
46
- {
47
- name: "Physical Strike",
48
- type: AttackType.BEAST,
49
- power: 60,
50
- accuracy: 100,
51
- pp: 15,
52
- priority: 0,
53
- flags: ['contact'],
54
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
55
- }
56
- ]
57
- };
58
-
59
- const engine = new BattleEngine(fieldUser, attacker);
60
-
61
- console.log('Initial state:', {
62
- playerHp: engine.getState().playerPiclet.currentHp,
63
- opponentHp: engine.getState().opponentPiclet.currentHp,
64
- fieldEffects: engine.getState().fieldEffects
65
- });
66
-
67
- // First turn: create barrier and get attacked
68
- engine.executeActions(
69
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
70
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike
71
- );
72
-
73
- console.log('After first turn:', {
74
- playerHp: engine.getState().playerPiclet.currentHp,
75
- opponentHp: engine.getState().opponentPiclet.currentHp,
76
- fieldEffects: engine.getState().fieldEffects,
77
- log: engine.getLog()
78
- });
79
-
80
- // Second turn: opponent attacks again (should be reduced)
81
- const hpBeforeSecondAttack = engine.getState().playerPiclet.currentHp;
82
-
83
- engine.executeActions(
84
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier (no effect)
85
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike (should be reduced)
86
- );
87
-
88
- const hpAfterSecondAttack = engine.getState().playerPiclet.currentHp;
89
- const damage = hpBeforeSecondAttack - hpAfterSecondAttack;
90
-
91
- console.log('After second turn:', {
92
- playerHp: hpAfterSecondAttack,
93
- damage: damage,
94
- fieldEffects: engine.getState().fieldEffects,
95
- log: engine.getLog()
96
- });
97
-
98
- expect(engine.getState().fieldEffects.length).toBeGreaterThan(0);
99
- expect(damage).toBeGreaterThan(0); // Some damage should occur
100
- });
101
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/extreme-moves.test.ts DELETED
@@ -1,544 +0,0 @@
1
- /**
2
- * Tests for extreme risk-reward moves from the design document
3
- * These are the dramatic, high-stakes moves that define the battle system
4
- */
5
-
6
- import { describe, it, expect, beforeEach } from 'vitest';
7
- import { BattleEngine } from './BattleEngine';
8
- import { PicletDefinition, Move, SpecialAbility } from './types';
9
- import { PicletType, AttackType } from './types';
10
-
11
- const STANDARD_STATS = { hp: 100, attack: 80, defense: 70, speed: 60 };
12
-
13
- describe('Extreme Risk-Reward Moves - TDD Implementation', () => {
14
- describe('Self Destruct - Ultimate Sacrifice', () => {
15
- it('should handle Self Destruct move', () => {
16
- const selfDestruct: Move = {
17
- name: "Self Destruct",
18
- type: AttackType.NORMAL,
19
- power: 200,
20
- accuracy: 100,
21
- pp: 1,
22
- priority: 0,
23
- flags: ['explosive', 'contact'],
24
- effects: [
25
- {
26
- type: 'damage',
27
- target: 'all',
28
- formula: 'standard',
29
- multiplier: 1.5
30
- },
31
- {
32
- type: 'damage',
33
- target: 'self',
34
- formula: 'fixed',
35
- value: 9999,
36
- condition: 'afterUse'
37
- }
38
- ]
39
- };
40
-
41
- const bomberPiclet: PicletDefinition = {
42
- name: "Bomb Beast",
43
- description: "A creature that can self-destruct",
44
- tier: 'medium',
45
- primaryType: PicletType.MACHINA,
46
- baseStats: STANDARD_STATS,
47
- nature: "Brave",
48
- specialAbility: { name: "None", description: "No ability" },
49
- movepool: [selfDestruct]
50
- };
51
-
52
- const targetPiclet: PicletDefinition = {
53
- name: "Target",
54
- description: "Target dummy",
55
- tier: 'medium',
56
- primaryType: PicletType.BEAST,
57
- baseStats: STANDARD_STATS,
58
- nature: "Hardy",
59
- specialAbility: { name: "None", description: "No ability" },
60
- movepool: [{
61
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
62
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
63
- }]
64
- };
65
-
66
- // Test that the move is properly defined
67
- expect(selfDestruct.effects).toHaveLength(2);
68
- expect(selfDestruct.effects[0].target).toBe('all');
69
- expect(selfDestruct.effects[1].value).toBe(9999);
70
- });
71
- });
72
-
73
- describe('Berserker\'s End - Conditional Power', () => {
74
- it('should handle Berserker\'s End with conditional effects', () => {
75
- const berserkersEnd: Move = {
76
- name: "Berserker's End",
77
- type: AttackType.BEAST,
78
- power: 80,
79
- accuracy: 95,
80
- pp: 10,
81
- priority: 0,
82
- flags: ['contact', 'reckless'],
83
- effects: [
84
- {
85
- type: 'damage',
86
- target: 'opponent',
87
- amount: 'normal'
88
- },
89
- {
90
- type: 'damage',
91
- target: 'opponent',
92
- amount: 'strong',
93
- condition: 'ifLowHp'
94
- },
95
- {
96
- type: 'mechanicOverride',
97
- target: 'self',
98
- mechanic: 'healingBlocked',
99
- value: true
100
- }
101
- ]
102
- };
103
-
104
- const berserkerPiclet: PicletDefinition = {
105
- name: "Berserker",
106
- description: "Fights with reckless abandon",
107
- tier: 'high',
108
- primaryType: PicletType.BEAST,
109
- baseStats: { hp: 120, attack: 100, defense: 90, speed: 85 },
110
- nature: "Reckless",
111
- specialAbility: { name: "None", description: "No ability" },
112
- movepool: [berserkersEnd]
113
- };
114
-
115
- // Test move structure
116
- expect(berserkersEnd.effects).toHaveLength(3);
117
- expect(berserkersEnd.effects[1].condition).toBe('ifLowHp');
118
- expect(berserkersEnd.effects[2].mechanic).toBe('healingBlocked');
119
- });
120
- });
121
-
122
- describe('Life Drain Overload - Massive Heal with Permanent Cost', () => {
123
- it('should handle Life Drain Overload move', () => {
124
- const lifeDrainOverload: Move = {
125
- name: "Life Drain Overload",
126
- type: AttackType.CULTURE,
127
- power: 0,
128
- accuracy: 100,
129
- pp: 3,
130
- priority: 0,
131
- flags: ['draining'],
132
- effects: [
133
- {
134
- type: 'heal',
135
- target: 'self',
136
- formula: 'percentage',
137
- value: 75
138
- },
139
- {
140
- type: 'modifyStats',
141
- target: 'self',
142
- stats: { attack: 'greatly_decrease' },
143
- condition: 'afterUse'
144
- }
145
- ]
146
- };
147
-
148
- expect(lifeDrainOverload.effects[0].formula).toBe('percentage');
149
- expect(lifeDrainOverload.effects[0].value).toBe(75);
150
- expect(lifeDrainOverload.effects[1].stats.attack).toBe('greatly_decrease');
151
- });
152
- });
153
-
154
- describe('Cursed Gambit - Random Extreme Outcome', () => {
155
- it('should handle Cursed Gambit with random effects', () => {
156
- const cursedGambit: Move = {
157
- name: "Cursed Gambit",
158
- type: AttackType.CULTURE,
159
- power: 0,
160
- accuracy: 100,
161
- pp: 1,
162
- priority: 0,
163
- flags: ['gambling', 'cursed'],
164
- effects: [
165
- {
166
- type: 'heal',
167
- target: 'self',
168
- formula: 'percentage',
169
- value: 100,
170
- condition: 'ifLucky50'
171
- },
172
- {
173
- type: 'damage',
174
- target: 'self',
175
- formula: 'fixed',
176
- value: 9999,
177
- condition: 'ifUnlucky50'
178
- }
179
- ]
180
- };
181
-
182
- expect(cursedGambit.effects).toHaveLength(2);
183
- expect(cursedGambit.effects[0].condition).toBe('ifLucky50');
184
- expect(cursedGambit.effects[1].condition).toBe('ifUnlucky50');
185
- expect(cursedGambit.flags).toContain('gambling');
186
- });
187
- });
188
-
189
- describe('Blood Pact - Sacrifice HP for Permanent Power', () => {
190
- it('should handle Blood Pact move', () => {
191
- const bloodPact: Move = {
192
- name: "Blood Pact",
193
- type: AttackType.CULTURE,
194
- power: 0,
195
- accuracy: 100,
196
- pp: 3,
197
- priority: 0,
198
- flags: ['sacrifice'],
199
- effects: [
200
- {
201
- type: 'damage',
202
- target: 'self',
203
- formula: 'percentage',
204
- value: 50
205
- },
206
- {
207
- type: 'mechanicOverride',
208
- target: 'self',
209
- mechanic: 'damageMultiplier',
210
- value: 2.0,
211
- condition: 'restOfBattle'
212
- }
213
- ]
214
- };
215
-
216
- expect(bloodPact.effects[0].formula).toBe('percentage');
217
- expect(bloodPact.effects[1].value).toBe(2.0);
218
- expect(bloodPact.flags).toContain('sacrifice');
219
- });
220
- });
221
-
222
- describe('Soul Burn - PP Sacrifice for Power', () => {
223
- it('should handle Soul Burn move', () => {
224
- const soulBurn: Move = {
225
- name: "Soul Burn",
226
- type: AttackType.SPACE,
227
- power: 150,
228
- accuracy: 90,
229
- pp: 5,
230
- priority: 0,
231
- flags: ['burning'],
232
- effects: [
233
- {
234
- type: 'damage',
235
- target: 'opponent',
236
- amount: 'extreme'
237
- },
238
- {
239
- type: 'manipulatePP',
240
- target: 'self',
241
- action: 'drain',
242
- value: 3,
243
- targetMove: 'random',
244
- condition: 'afterUse'
245
- }
246
- ]
247
- };
248
-
249
- expect(soulBurn.effects[0].amount).toBe('extreme');
250
- expect(soulBurn.effects[1].value).toBe(3);
251
- expect(soulBurn.effects[1].targetMove).toBe('random');
252
- });
253
- });
254
-
255
- describe('Mirror Shatter - Damage Reflection with Cost', () => {
256
- it('should handle Mirror Shatter move', () => {
257
- const mirrorShatter: Move = {
258
- name: "Mirror Shatter",
259
- type: AttackType.MINERAL,
260
- power: 0,
261
- accuracy: 100,
262
- pp: 5,
263
- priority: 4,
264
- flags: ['priority'],
265
- effects: [
266
- {
267
- type: 'mechanicOverride',
268
- target: 'self',
269
- mechanic: 'damageReflection',
270
- value: 'double',
271
- condition: 'thisTurn'
272
- },
273
- {
274
- type: 'modifyStats',
275
- target: 'self',
276
- stats: { defense: 'greatly_decrease' },
277
- condition: 'afterUse'
278
- }
279
- ]
280
- };
281
-
282
- expect(mirrorShatter.priority).toBe(4);
283
- expect(mirrorShatter.effects[0].value).toBe('double');
284
- expect(mirrorShatter.effects[1].stats.defense).toBe('greatly_decrease');
285
- });
286
- });
287
-
288
- describe('Apocalypse Strike - AoE Devastation with Vulnerability', () => {
289
- it('should handle Apocalypse Strike move', () => {
290
- const apocalypseStrike: Move = {
291
- name: "Apocalypse Strike",
292
- type: AttackType.SPACE,
293
- power: 120,
294
- accuracy: 85,
295
- pp: 1,
296
- priority: 0,
297
- flags: ['apocalyptic'],
298
- effects: [
299
- {
300
- type: 'damage',
301
- target: 'all',
302
- formula: 'standard',
303
- multiplier: 1.3
304
- },
305
- {
306
- type: 'mechanicOverride',
307
- target: 'self',
308
- mechanic: 'criticalHits',
309
- value: 'alwaysReceive',
310
- condition: 'restOfBattle'
311
- },
312
- {
313
- type: 'modifyStats',
314
- target: 'self',
315
- stats: { defense: 'greatly_decrease' }
316
- }
317
- ]
318
- };
319
-
320
- expect(apocalypseStrike.effects).toHaveLength(3);
321
- expect(apocalypseStrike.effects[0].target).toBe('all');
322
- expect(apocalypseStrike.effects[1].value).toBe('alwaysReceive');
323
- expect(apocalypseStrike.pp).toBe(1); // Can only be used once
324
- });
325
- });
326
-
327
- describe('Temporal Overload - Extra Turn with Cost', () => {
328
- it('should handle Temporal Overload move', () => {
329
- const temporalOverload: Move = {
330
- name: "Temporal Overload",
331
- type: AttackType.SPACE,
332
- power: 0,
333
- accuracy: 100,
334
- pp: 2,
335
- priority: 0,
336
- flags: ['temporal'],
337
- effects: [
338
- {
339
- type: 'mechanicOverride',
340
- target: 'self',
341
- mechanic: 'extraTurn',
342
- value: true,
343
- condition: 'nextTurn'
344
- },
345
- {
346
- type: 'applyStatus',
347
- target: 'self',
348
- status: 'paralyze',
349
- chance: 100,
350
- condition: 'turnAfterNext'
351
- }
352
- ]
353
- };
354
-
355
- expect(temporalOverload.effects[0].mechanic).toBe('extraTurn');
356
- expect(temporalOverload.effects[1].condition).toBe('turnAfterNext');
357
- expect(temporalOverload.flags).toContain('temporal');
358
- });
359
- });
360
-
361
- describe('Multi-Stage Effects - Charging Blast', () => {
362
- it('should handle Charging Blast with multi-stage effects', () => {
363
- const chargingBlast: Move = {
364
- name: "Charging Blast",
365
- type: AttackType.SPACE,
366
- power: 120,
367
- accuracy: 90,
368
- pp: 5,
369
- priority: 0,
370
- flags: ['charging'],
371
- effects: [
372
- {
373
- type: 'modifyStats',
374
- target: 'self',
375
- stats: { defense: 'increase' },
376
- condition: 'onCharging'
377
- },
378
- {
379
- type: 'damage',
380
- target: 'opponent',
381
- amount: 'extreme',
382
- condition: 'afterCharging'
383
- },
384
- {
385
- type: 'applyStatus',
386
- target: 'self',
387
- status: 'paralyze',
388
- condition: 'afterCharging'
389
- }
390
- ]
391
- };
392
-
393
- expect(chargingBlast.effects).toHaveLength(3);
394
- expect(chargingBlast.effects[0].condition).toBe('onCharging');
395
- expect(chargingBlast.effects[1].condition).toBe('afterCharging');
396
- expect(chargingBlast.flags).toContain('charging');
397
- });
398
- });
399
-
400
- describe('Void Sacrifice - Field Effect with Self-Harm', () => {
401
- it('should handle Void Sacrifice from Tempest Wraith example', () => {
402
- const voidSacrifice: Move = {
403
- name: "Void Sacrifice",
404
- type: AttackType.SPACE,
405
- power: 130,
406
- accuracy: 85,
407
- pp: 1,
408
- priority: 0,
409
- flags: ['sacrifice', 'explosive'],
410
- effects: [
411
- {
412
- type: 'damage',
413
- target: 'all',
414
- formula: 'standard',
415
- multiplier: 1.2
416
- },
417
- {
418
- type: 'damage',
419
- target: 'self',
420
- formula: 'percentage',
421
- value: 75
422
- },
423
- {
424
- type: 'fieldEffect',
425
- effect: 'voidStorm',
426
- target: 'field',
427
- stackable: false
428
- }
429
- ]
430
- };
431
-
432
- expect(voidSacrifice.effects).toHaveLength(3);
433
- expect(voidSacrifice.effects[2].effect).toBe('voidStorm');
434
- expect(voidSacrifice.effects[2].stackable).toBe(false);
435
- });
436
- });
437
-
438
- describe('Integration Test - Complex Battle with Extreme Moves', () => {
439
- it('should handle a battle with multiple extreme moves', () => {
440
- const extremePiclet: PicletDefinition = {
441
- name: "Chaos Incarnate",
442
- description: "Master of extreme techniques",
443
- tier: 'legendary',
444
- primaryType: PicletType.SPACE,
445
- secondaryType: PicletType.CULTURE,
446
- baseStats: { hp: 150, attack: 120, defense: 80, speed: 100 },
447
- nature: "Reckless",
448
- specialAbility: {
449
- name: "Chaos Heart",
450
- description: "Gains power from desperation",
451
- triggers: [
452
- {
453
- event: 'onLowHP',
454
- effects: [
455
- {
456
- type: 'mechanicOverride',
457
- mechanic: 'damageMultiplier',
458
- value: 1.5
459
- }
460
- ]
461
- }
462
- ]
463
- },
464
- movepool: [
465
- {
466
- name: "Cursed Gambit",
467
- type: AttackType.CULTURE,
468
- power: 0,
469
- accuracy: 100,
470
- pp: 1,
471
- priority: 0,
472
- flags: ['gambling'],
473
- effects: [
474
- {
475
- type: 'heal',
476
- target: 'self',
477
- formula: 'percentage',
478
- value: 100,
479
- condition: 'ifLucky50'
480
- },
481
- {
482
- type: 'damage',
483
- target: 'self',
484
- formula: 'fixed',
485
- value: 9999,
486
- condition: 'ifUnlucky50'
487
- }
488
- ]
489
- },
490
- {
491
- name: "Blood Pact",
492
- type: AttackType.CULTURE,
493
- power: 0,
494
- accuracy: 100,
495
- pp: 3,
496
- priority: 0,
497
- flags: ['sacrifice'],
498
- effects: [
499
- {
500
- type: 'damage',
501
- target: 'self',
502
- formula: 'percentage',
503
- value: 50
504
- },
505
- {
506
- type: 'mechanicOverride',
507
- target: 'self',
508
- mechanic: 'damageMultiplier',
509
- value: 2.0,
510
- condition: 'restOfBattle'
511
- }
512
- ]
513
- }
514
- ]
515
- };
516
-
517
- const standardPiclet: PicletDefinition = {
518
- name: "Standard Fighter",
519
- description: "Uses normal moves",
520
- tier: 'medium',
521
- primaryType: PicletType.BEAST,
522
- baseStats: STANDARD_STATS,
523
- nature: "Hardy",
524
- specialAbility: { name: "None", description: "No ability" },
525
- movepool: [{
526
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
527
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
528
- }]
529
- };
530
-
531
- const engine = new BattleEngine(extremePiclet, standardPiclet);
532
-
533
- // Test that the battle can be initialized with extreme moves
534
- expect(engine.getState().playerPiclet.definition.name).toBe("Chaos Incarnate");
535
- expect(engine.getState().playerPiclet.moves).toHaveLength(2);
536
- expect(engine.getState().playerPiclet.moves[0].move.name).toBe("Cursed Gambit");
537
- expect(engine.getState().playerPiclet.moves[1].move.name).toBe("Blood Pact");
538
-
539
- // Test that the special ability is properly defined
540
- expect(extremePiclet.specialAbility.triggers).toHaveLength(1);
541
- expect(extremePiclet.specialAbility.triggers![0].event).toBe('onLowHP');
542
- });
543
- });
544
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/extreme-risk-reward.test.ts DELETED
@@ -1,383 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Extreme Risk-Reward Moves', () => {
7
- describe('Self-Destruct Moves', () => {
8
- it('should deal massive damage to all but KO the user', () => {
9
- const bomber: PicletDefinition = {
10
- name: "Suicide Bomber",
11
- description: "Sacrifices itself for massive damage",
12
- tier: 'medium',
13
- primaryType: PicletType.MACHINA,
14
- baseStats: { hp: 60, attack: 40, defense: 60, speed: 50 },
15
- nature: "Brave",
16
- specialAbility: { name: "No Ability", description: "" },
17
- movepool: [
18
- {
19
- name: "Self Destruct",
20
- type: AttackType.MACHINA,
21
- power: 200,
22
- accuracy: 100,
23
- pp: 1,
24
- priority: 0,
25
- flags: ['explosive', 'contact'],
26
- effects: [
27
- {
28
- type: 'damage',
29
- target: 'all',
30
- formula: 'standard',
31
- multiplier: 1.5
32
- },
33
- {
34
- type: 'damage',
35
- target: 'self',
36
- formula: 'fixed',
37
- value: 9999,
38
- condition: 'afterUse'
39
- }
40
- ]
41
- }
42
- ]
43
- };
44
-
45
- const opponent: PicletDefinition = {
46
- name: "Sturdy Opponent",
47
- description: "Tanky opponent",
48
- tier: 'high',
49
- primaryType: PicletType.MINERAL,
50
- baseStats: { hp: 100, attack: 60, defense: 100, speed: 40 },
51
- nature: "Impish",
52
- specialAbility: { name: "No Ability", description: "" },
53
- movepool: [
54
- {
55
- name: "Tackle",
56
- type: AttackType.NORMAL,
57
- power: 40,
58
- accuracy: 100,
59
- pp: 10,
60
- priority: 0,
61
- flags: ['contact'],
62
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
63
- }
64
- ]
65
- };
66
-
67
- const engine = new BattleEngine(bomber, opponent);
68
- const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
69
-
70
- engine.executeActions(
71
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Self Destruct
72
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
73
- );
74
-
75
- const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
76
- const playerHp = engine.getState().playerPiclet.currentHp;
77
-
78
- // User should be KO'd
79
- expect(playerHp).toBe(0);
80
-
81
- // Opponent should take massive damage
82
- expect(finalOpponentHp).toBeLessThan(initialOpponentHp);
83
- const damage = initialOpponentHp - finalOpponentHp;
84
- expect(damage).toBeGreaterThan(45); // Should be very high damage for a self-destruct move
85
-
86
- const log = engine.getLog();
87
- expect(log.some(msg => msg.includes('Self Destruct') || msg.includes('exploded'))).toBe(true);
88
- expect(engine.isGameOver()).toBe(true);
89
- });
90
- });
91
-
92
- describe('Gambling Moves', () => {
93
- it('should have random success/failure outcomes', () => {
94
- const gambler: PicletDefinition = {
95
- name: "Lucky Gambler",
96
- description: "Relies on luck for power",
97
- tier: 'medium',
98
- primaryType: PicletType.CULTURE,
99
- baseStats: { hp: 70, attack: 60, defense: 60, speed: 80 },
100
- nature: "Hasty",
101
- specialAbility: { name: "No Ability", description: "" },
102
- movepool: [
103
- {
104
- name: "Cursed Gambit",
105
- type: AttackType.CULTURE,
106
- power: 0,
107
- accuracy: 100,
108
- pp: 1,
109
- priority: 0,
110
- flags: ['gambling', 'cursed'],
111
- effects: [
112
- {
113
- type: 'heal',
114
- target: 'self',
115
- amount: 'percentage',
116
- value: 100,
117
- condition: 'ifLucky50'
118
- },
119
- {
120
- type: 'damage',
121
- target: 'self',
122
- formula: 'fixed',
123
- value: 9999,
124
- condition: 'ifUnlucky50'
125
- }
126
- ]
127
- }
128
- ]
129
- };
130
-
131
- const opponent: PicletDefinition = {
132
- name: "Opponent",
133
- description: "Standard opponent",
134
- tier: 'medium',
135
- primaryType: PicletType.BEAST,
136
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
137
- nature: "Hardy",
138
- specialAbility: { name: "No Ability", description: "" },
139
- movepool: [
140
- {
141
- name: "Do Nothing",
142
- type: AttackType.NORMAL,
143
- power: 0,
144
- accuracy: 100,
145
- pp: 10,
146
- priority: 0,
147
- flags: [],
148
- effects: [] // No effects - just waste a turn
149
- }
150
- ]
151
- };
152
-
153
- // Test multiple times to check for randomness
154
- let healedCount = 0;
155
- let faintedCount = 0;
156
-
157
- for (let i = 0; i < 20; i++) {
158
- const engine = new BattleEngine(gambler, opponent);
159
- // Damage the gambler first
160
- engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5);
161
- const preGambitHp = engine.getState().playerPiclet.currentHp;
162
-
163
- engine.executeActions(
164
- { type: 'move', piclet: 'player', moveIndex: 0 },
165
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
166
- );
167
-
168
- const postGambitHp = engine.getState().playerPiclet.currentHp;
169
- const maxHp = engine.getState().playerPiclet.maxHp;
170
-
171
-
172
- if (postGambitHp === 0) {
173
- faintedCount++;
174
- } else if (postGambitHp > preGambitHp) {
175
- healedCount++;
176
- }
177
- }
178
-
179
- // Should have some of each outcome (allowing for randomness)
180
- expect(healedCount + faintedCount).toBeGreaterThan(0);
181
- expect(healedCount).toBeGreaterThan(0);
182
- expect(faintedCount).toBeGreaterThan(0);
183
- });
184
- });
185
-
186
- describe('Sacrifice Moves', () => {
187
- it('should provide powerful effects at great personal cost', () => {
188
- const sacrificer: PicletDefinition = {
189
- name: "Blood Warrior",
190
- description: "Sacrifices HP for power",
191
- tier: 'medium',
192
- primaryType: PicletType.BEAST,
193
- baseStats: { hp: 100, attack: 70, defense: 60, speed: 60 },
194
- nature: "Brave",
195
- specialAbility: { name: "No Ability", description: "" },
196
- movepool: [
197
- {
198
- name: "Blood Pact",
199
- type: AttackType.BEAST,
200
- power: 0,
201
- accuracy: 100,
202
- pp: 3,
203
- priority: 0,
204
- flags: ['sacrifice'],
205
- effects: [
206
- {
207
- type: 'damage',
208
- target: 'self',
209
- formula: 'percentage',
210
- value: 50
211
- },
212
- {
213
- type: 'mechanicOverride',
214
- target: 'self',
215
- mechanic: 'damageMultiplier',
216
- value: 2.0,
217
- condition: 'restOfBattle'
218
- }
219
- ]
220
- },
221
- {
222
- name: "Strike",
223
- type: AttackType.BEAST,
224
- power: 60,
225
- accuracy: 100,
226
- pp: 10,
227
- priority: 0,
228
- flags: ['contact'],
229
- effects: [
230
- {
231
- type: 'damage',
232
- target: 'opponent',
233
- amount: 'normal'
234
- }
235
- ]
236
- }
237
- ]
238
- };
239
-
240
- const opponent: PicletDefinition = {
241
- name: "Opponent",
242
- description: "Standard opponent",
243
- tier: 'medium',
244
- primaryType: PicletType.BEAST,
245
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
246
- nature: "Hardy",
247
- specialAbility: { name: "No Ability", description: "" },
248
- movepool: [
249
- {
250
- name: "Tackle",
251
- type: AttackType.NORMAL,
252
- power: 40,
253
- accuracy: 100,
254
- pp: 10,
255
- priority: 0,
256
- flags: ['contact'],
257
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
258
- }
259
- ]
260
- };
261
-
262
- const engine = new BattleEngine(sacrificer, opponent);
263
- const initialHp = engine.getState().playerPiclet.currentHp;
264
-
265
- // Use Blood Pact
266
- engine.executeActions(
267
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Blood Pact
268
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
269
- );
270
-
271
- const hpAfterSacrifice = engine.getState().playerPiclet.currentHp;
272
- expect(hpAfterSacrifice).toBeLessThan(initialHp);
273
-
274
- // Now attack should do double damage
275
- const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
276
- engine.executeActions(
277
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Strike (should be doubled)
278
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
279
- );
280
-
281
- const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
282
- const damage = initialOpponentHp - finalOpponentHp;
283
-
284
- // Should do significantly more damage than normal (doubled)
285
- expect(damage).toBeGreaterThan(60); // Normal would be ~30-40
286
-
287
- const log = engine.getLog();
288
- expect(log.some(msg => msg.includes('Blood Pact') || msg.includes('sacrifice'))).toBe(true);
289
- });
290
- });
291
-
292
- describe('Conditional Power Scaling', () => {
293
- it('should scale damage based on conditions', () => {
294
- const revengeUser: PicletDefinition = {
295
- name: "Revenge Fighter",
296
- description: "Gets stronger when damaged",
297
- tier: 'medium',
298
- primaryType: PicletType.BEAST,
299
- baseStats: { hp: 90, attack: 70, defense: 80, speed: 50 },
300
- nature: "Brave",
301
- specialAbility: { name: "No Ability", description: "" },
302
- movepool: [
303
- {
304
- name: "Revenge Strike",
305
- type: AttackType.BEAST,
306
- power: 60,
307
- accuracy: 100,
308
- pp: 10,
309
- priority: 0,
310
- flags: ['contact'],
311
- effects: [
312
- {
313
- type: 'damage',
314
- target: 'opponent',
315
- amount: 'normal'
316
- },
317
- {
318
- type: 'damage',
319
- target: 'opponent',
320
- amount: 'strong',
321
- condition: 'ifDamagedThisTurn'
322
- }
323
- ]
324
- }
325
- ]
326
- };
327
-
328
- const attacker: PicletDefinition = {
329
- name: "Fast Attacker",
330
- description: "Quick attacker",
331
- tier: 'medium',
332
- primaryType: PicletType.BEAST,
333
- baseStats: { hp: 200, attack: 80, defense: 60, speed: 100 },
334
- nature: "Hasty",
335
- specialAbility: { name: "No Ability", description: "" },
336
- movepool: [
337
- {
338
- name: "Quick Strike",
339
- type: AttackType.BEAST,
340
- power: 50,
341
- accuracy: 100,
342
- pp: 10,
343
- priority: 1,
344
- flags: ['contact', 'priority'],
345
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
346
- }
347
- ]
348
- };
349
-
350
- // First test: revenge without being damaged first (revenge user at full HP)
351
- const engine1 = new BattleEngine(revengeUser, attacker);
352
- const initialOpponentHp = engine1.getState().opponentPiclet.currentHp;
353
- engine1.executeActions(
354
- { type: 'move', piclet: 'player', moveIndex: 0 },
355
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
356
- );
357
-
358
- const hpAfterNormalRevenge = engine1.getState().opponentPiclet.currentHp;
359
- const normalRevengeDamage = initialOpponentHp - hpAfterNormalRevenge;
360
-
361
- // Second test: revenge when damaged (revenge user starts damaged)
362
- const engine2 = new BattleEngine(revengeUser, attacker);
363
- // Damage the revenge user to trigger the condition
364
- engine2['state'].playerPiclet.currentHp = Math.floor(engine2['state'].playerPiclet.maxHp * 0.5);
365
-
366
- const initialOpponentHp2 = engine2.getState().opponentPiclet.currentHp;
367
- engine2.executeActions(
368
- { type: 'move', piclet: 'player', moveIndex: 0 },
369
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
370
- );
371
-
372
- const hpAfterPoweredRevenge = engine2.getState().opponentPiclet.currentHp;
373
- const poweredRevengeDamage = initialOpponentHp2 - hpAfterPoweredRevenge;
374
-
375
- // Verify that the conditional effect triggered by checking for multiple damage instances
376
- const damageMessages = engine2.getLog().filter(msg => msg.includes('took') && msg.includes('damage'));
377
- expect(damageMessages.length).toBeGreaterThanOrEqual(3); // Attacker hits revenge user, then revenge user hits back twice
378
-
379
- // Verify the powered revenge did more damage overall
380
- expect(poweredRevengeDamage).toBeGreaterThan(100); // Should be significant damage from both effects
381
- });
382
- });
383
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/field-effects.test.ts DELETED
@@ -1,493 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Field Effects System', () => {
7
- let contactAttacker: PicletDefinition;
8
- let nonContactAttacker: PicletDefinition;
9
- let fieldEffectUser: PicletDefinition;
10
- let basicOpponent: PicletDefinition;
11
-
12
- beforeEach(() => {
13
- // Piclet that uses contact moves
14
- contactAttacker = {
15
- name: "Contact Fighter",
16
- description: "Uses contact moves",
17
- tier: 'medium',
18
- primaryType: PicletType.BEAST,
19
- baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 },
20
- nature: "Adamant",
21
- specialAbility: { name: "No Ability", description: "" },
22
- movepool: [
23
- {
24
- name: "Physical Strike",
25
- type: AttackType.BEAST,
26
- power: 60,
27
- accuracy: 100,
28
- pp: 15,
29
- priority: 0,
30
- flags: ['contact'],
31
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
32
- }
33
- ]
34
- };
35
-
36
- // Piclet that uses non-contact moves
37
- nonContactAttacker = {
38
- name: "Ranged Fighter",
39
- description: "Uses non-contact moves",
40
- tier: 'medium',
41
- primaryType: PicletType.SPACE,
42
- baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 },
43
- nature: "Modest",
44
- specialAbility: { name: "No Ability", description: "" },
45
- movepool: [
46
- {
47
- name: "Energy Blast",
48
- type: AttackType.SPACE,
49
- power: 60,
50
- accuracy: 100,
51
- pp: 15,
52
- priority: 0,
53
- flags: [],
54
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
55
- }
56
- ]
57
- };
58
-
59
- // Piclet that can create field effects
60
- fieldEffectUser = {
61
- name: "Field Controller",
62
- description: "Controls battlefield effects",
63
- tier: 'medium',
64
- primaryType: PicletType.BEAST, // Changed to BEAST so opponent can damage it
65
- baseStats: { hp: 100, attack: 60, defense: 80, speed: 60 },
66
- nature: "Calm",
67
- specialAbility: { name: "No Ability", description: "" },
68
- movepool: [
69
- {
70
- name: "Contact Barrier",
71
- type: AttackType.SPACE,
72
- power: 0,
73
- accuracy: 100,
74
- pp: 10,
75
- priority: 0,
76
- flags: [],
77
- effects: [
78
- {
79
- type: 'fieldEffect',
80
- effect: 'reflect',
81
- target: 'playerSide',
82
- stackable: false
83
- }
84
- ]
85
- },
86
- {
87
- name: "Non-Contact Barrier",
88
- type: AttackType.SPACE,
89
- power: 0,
90
- accuracy: 100,
91
- pp: 10,
92
- priority: 0,
93
- flags: [],
94
- effects: [
95
- {
96
- type: 'fieldEffect',
97
- effect: 'lightScreen',
98
- target: 'playerSide',
99
- stackable: false
100
- }
101
- ]
102
- },
103
- {
104
- name: "Entry Spikes",
105
- type: AttackType.MINERAL,
106
- power: 0,
107
- accuracy: 100,
108
- pp: 10,
109
- priority: 0,
110
- flags: [],
111
- effects: [
112
- {
113
- type: 'fieldEffect',
114
- effect: 'spikes',
115
- target: 'opponentSide',
116
- stackable: true
117
- }
118
- ]
119
- },
120
- {
121
- name: "Healing Field",
122
- type: AttackType.FLORA,
123
- power: 0,
124
- accuracy: 100,
125
- pp: 10,
126
- priority: 0,
127
- flags: [],
128
- effects: [
129
- {
130
- type: 'fieldEffect',
131
- effect: 'healingMist',
132
- target: 'field',
133
- stackable: false
134
- }
135
- ]
136
- }
137
- ]
138
- };
139
-
140
- // Basic opponent for testing
141
- basicOpponent = {
142
- name: "Basic Opponent",
143
- description: "Standard test opponent",
144
- tier: 'medium',
145
- primaryType: PicletType.BEAST,
146
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 60 },
147
- nature: "Hardy",
148
- specialAbility: { name: "No Ability", description: "" },
149
- movepool: [
150
- {
151
- name: "Basic Attack",
152
- type: AttackType.NORMAL,
153
- power: 50,
154
- accuracy: 100,
155
- pp: 20,
156
- priority: 0,
157
- flags: ['contact'],
158
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
159
- }
160
- ]
161
- };
162
- });
163
-
164
- describe('Contact Damage Reduction (Reflect)', () => {
165
- it('should reduce contact move damage by 50%', () => {
166
- const engine = new BattleEngine(fieldEffectUser, contactAttacker);
167
-
168
- // Set up barrier first
169
- engine.executeActions(
170
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
171
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Basic Attack (will be reduced)
172
- );
173
-
174
- const log = engine.getLog();
175
- expect(log.some(msg => msg.includes('barrier was raised to reduce contact move damage'))).toBe(true);
176
-
177
- // Test that subsequent contact moves are reduced
178
- const initialHp = engine.getState().playerPiclet.currentHp;
179
-
180
- engine.executeActions(
181
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier (no effect, already active)
182
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Basic Attack (should be reduced)
183
- );
184
-
185
- const finalHp = engine.getState().playerPiclet.currentHp;
186
- const damage = initialHp - finalHp;
187
-
188
- // Damage should be significantly reduced (less than normal ~30-40 damage)
189
- expect(damage).toBeLessThan(25);
190
- expect(damage).toBeGreaterThan(0); // But still some damage
191
- });
192
-
193
- it('should not reduce non-contact move damage', () => {
194
- const engine = new BattleEngine(fieldEffectUser, nonContactAttacker);
195
-
196
- // Set up contact barrier
197
- engine.executeActions(
198
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
199
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (non-contact)
200
- );
201
-
202
- // Test that non-contact moves are NOT reduced
203
- const initialHp = engine.getState().playerPiclet.currentHp;
204
-
205
- engine.executeActions(
206
- { type: 'move', piclet: 'player', moveIndex: 0 },
207
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should not be reduced)
208
- );
209
-
210
- const finalHp = engine.getState().playerPiclet.currentHp;
211
- const damage = initialHp - finalHp;
212
-
213
- // Damage should be normal (around 30-50)
214
- expect(damage).toBeGreaterThan(25);
215
- });
216
- });
217
-
218
- describe('Non-Contact Damage Reduction (Light Screen)', () => {
219
- it('should reduce non-contact move damage by 50%', () => {
220
- const engine = new BattleEngine(fieldEffectUser, nonContactAttacker);
221
-
222
- // Set up non-contact barrier
223
- engine.executeActions(
224
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Non-Contact Barrier
225
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should be reduced)
226
- );
227
-
228
- const log = engine.getLog();
229
- expect(log.some(msg => msg.includes('barrier was raised to reduce non-contact move damage'))).toBe(true);
230
-
231
- // Test reduction on subsequent turn
232
- const initialHp = engine.getState().playerPiclet.currentHp;
233
-
234
- engine.executeActions(
235
- { type: 'move', piclet: 'player', moveIndex: 1 },
236
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Energy Blast (should be reduced)
237
- );
238
-
239
- const finalHp = engine.getState().playerPiclet.currentHp;
240
- const damage = initialHp - finalHp;
241
-
242
- // Damage should be reduced
243
- expect(damage).toBeLessThan(25);
244
- expect(damage).toBeGreaterThan(0);
245
- });
246
-
247
- it('should not reduce contact move damage', () => {
248
- const engine = new BattleEngine(fieldEffectUser, contactAttacker);
249
-
250
- // Set up non-contact barrier
251
- engine.executeActions(
252
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Non-Contact Barrier
253
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike (contact)
254
- );
255
-
256
- // Test that contact moves are NOT reduced
257
- const initialHp = engine.getState().playerPiclet.currentHp;
258
-
259
- engine.executeActions(
260
- { type: 'move', piclet: 'player', moveIndex: 1 },
261
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Physical Strike (should not be reduced)
262
- );
263
-
264
- const finalHp = engine.getState().playerPiclet.currentHp;
265
- const damage = initialHp - finalHp;
266
-
267
- // Damage should be normal
268
- expect(damage).toBeGreaterThan(25);
269
- });
270
- });
271
-
272
- describe('Entry Hazards (Spikes)', () => {
273
- it('should set up entry spikes on opponent side', () => {
274
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
275
-
276
- engine.executeActions(
277
- { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes
278
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
279
- );
280
-
281
- const log = engine.getLog();
282
- expect(log.some(msg => msg.includes('Entry spikes were scattered'))).toBe(true);
283
-
284
- // Check that field effect was applied
285
- const state = engine.getState();
286
- expect(state.fieldEffects.some(effect => effect.name === 'entryHazardSpikes')).toBe(true);
287
- });
288
-
289
- it('should be stackable', () => {
290
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
291
-
292
- // Apply spikes twice
293
- engine.executeActions(
294
- { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes
295
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
296
- );
297
-
298
- engine.executeActions(
299
- { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes again
300
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
301
- );
302
-
303
- const state = engine.getState();
304
- const spikeEffects = state.fieldEffects.filter(effect => effect.name === 'entryHazardSpikes');
305
- expect(spikeEffects.length).toBe(2); // Should stack
306
- });
307
- });
308
-
309
- describe('Healing Field', () => {
310
- it('should create a healing field that affects both sides', () => {
311
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
312
-
313
- // Damage both piclets first
314
- engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.5);
315
- engine['state'].opponentPiclet.currentHp = Math.floor(engine['state'].opponentPiclet.maxHp * 0.5);
316
-
317
- const playerInitialHp = engine.getState().playerPiclet.currentHp;
318
- const opponentInitialHp = engine.getState().opponentPiclet.currentHp;
319
-
320
- engine.executeActions(
321
- { type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field
322
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
323
- );
324
-
325
- const log = engine.getLog();
326
- expect(log.some(msg => msg.includes('healing field was created'))).toBe(true);
327
-
328
- // Both piclets should be healed at end of turn
329
- const playerFinalHp = engine.getState().playerPiclet.currentHp;
330
- const opponentFinalHp = engine.getState().opponentPiclet.currentHp;
331
-
332
- expect(playerFinalHp).toBeGreaterThan(playerInitialHp);
333
- expect(opponentFinalHp).toBeGreaterThan(opponentInitialHp);
334
- expect(log.some(msg => msg.includes('healed by the healing field'))).toBe(true);
335
- });
336
-
337
- it('should not heal piclets at full HP', () => {
338
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
339
-
340
- // Both piclets at full HP
341
- const playerInitialHp = engine.getState().playerPiclet.currentHp;
342
- const opponentInitialHp = engine.getState().opponentPiclet.currentHp;
343
-
344
- engine.executeActions(
345
- { type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field
346
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
347
- );
348
-
349
- // HP should remain the same
350
- const playerFinalHp = engine.getState().playerPiclet.currentHp;
351
- const opponentFinalHp = engine.getState().opponentPiclet.currentHp;
352
-
353
- expect(playerFinalHp).toBe(playerInitialHp);
354
- expect(opponentFinalHp).toBe(opponentInitialHp);
355
- });
356
- });
357
-
358
- describe('Field Effect Duration and Management', () => {
359
- it('should expire field effects after 5 turns', () => {
360
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
361
-
362
- // Create a barrier
363
- engine.executeActions(
364
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
365
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
366
- );
367
-
368
- // Verify it exists
369
- expect(engine.getState().fieldEffects.length).toBe(1);
370
-
371
- // Pass 5 turns
372
- for (let i = 0; i < 5; i++) {
373
- engine.executeActions(
374
- { type: 'move', piclet: 'player', moveIndex: 0 },
375
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
376
- );
377
- }
378
-
379
- // Effect should have expired
380
- const log = engine.getLog();
381
- expect(log.some(msg => msg.includes('faded away'))).toBe(true);
382
- expect(engine.getState().fieldEffects.length).toBe(0);
383
- });
384
-
385
- it('should not stack non-stackable effects', () => {
386
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
387
-
388
- // Apply same barrier twice
389
- engine.executeActions(
390
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
391
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
392
- );
393
-
394
- engine.executeActions(
395
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier again
396
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
397
- );
398
-
399
- // Should only have one effect (refreshed duration)
400
- const contactBarriers = engine.getState().fieldEffects.filter(
401
- effect => effect.name === 'contactDamageReduction'
402
- );
403
- expect(contactBarriers.length).toBe(1);
404
- });
405
-
406
- it('should properly format field effect names in logs', () => {
407
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
408
-
409
- engine.executeActions(
410
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
411
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
412
- );
413
-
414
- // Pass enough turns for effect to fade
415
- for (let i = 0; i < 5; i++) {
416
- engine.executeActions(
417
- { type: 'move', piclet: 'player', moveIndex: 0 },
418
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
419
- );
420
- }
421
-
422
- const log = engine.getLog();
423
- expect(log.some(msg =>
424
- msg.includes('Contact damage barrier faded away') ||
425
- msg.includes('contact damage barrier faded away')
426
- )).toBe(true);
427
- });
428
- });
429
-
430
- describe('Field Effect Integration with Battle Flow', () => {
431
- it('should apply field effects during damage calculation', () => {
432
- const engine = new BattleEngine(fieldEffectUser, contactAttacker);
433
-
434
- // Measure baseline damage first
435
- const baselineEngine = new BattleEngine(fieldEffectUser, contactAttacker);
436
- const baselineInitialHp = baselineEngine.getState().playerPiclet.currentHp;
437
-
438
- baselineEngine.executeActions(
439
- { type: 'move', piclet: 'player', moveIndex: 0 },
440
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // No barrier
441
- );
442
-
443
- const baselineDamage = baselineInitialHp - baselineEngine.getState().playerPiclet.currentHp;
444
-
445
- // Now test with barrier
446
- engine.executeActions(
447
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
448
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
449
- );
450
-
451
- const protectedInitialHp = engine.getState().playerPiclet.currentHp;
452
-
453
- engine.executeActions(
454
- { type: 'move', piclet: 'player', moveIndex: 0 },
455
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Attack against barrier
456
- );
457
-
458
- const protectedDamage = protectedInitialHp - engine.getState().playerPiclet.currentHp;
459
-
460
- // Protected damage should be significantly less
461
- expect(protectedDamage).toBeLessThan(baselineDamage * 0.75);
462
- });
463
-
464
- it('should handle multiple field effects simultaneously', () => {
465
- const engine = new BattleEngine(fieldEffectUser, basicOpponent);
466
-
467
- // Apply multiple field effects
468
- engine.executeActions(
469
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Contact Barrier
470
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
471
- );
472
-
473
- engine.executeActions(
474
- { type: 'move', piclet: 'player', moveIndex: 3 }, // Healing Field
475
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
476
- );
477
-
478
- engine.executeActions(
479
- { type: 'move', piclet: 'player', moveIndex: 2 }, // Entry Spikes
480
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
481
- );
482
-
483
- // Should have 3 different field effects
484
- const state = engine.getState();
485
- expect(state.fieldEffects.length).toBe(3);
486
-
487
- const effectNames = state.fieldEffects.map(effect => effect.name);
488
- expect(effectNames).toContain('contactDamageReduction');
489
- expect(effectNames).toContain('healingField');
490
- expect(effectNames).toContain('entryHazardSpikes');
491
- });
492
- });
493
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/integration.test.ts DELETED
@@ -1,255 +0,0 @@
1
- /**
2
- * Integration tests for complete battle scenarios
3
- * Tests complex multi-turn battles following the design document
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import { BattleEngine } from './BattleEngine';
8
- import {
9
- STELLAR_WOLF,
10
- TOXIC_CRAWLER,
11
- BERSERKER_BEAST,
12
- AQUA_GUARDIAN
13
- } from './test-data';
14
- import { BattleAction } from './types';
15
-
16
- describe('Battle Engine Integration', () => {
17
- describe('Complete Battle Scenarios', () => {
18
- it('should handle a complete battle with type effectiveness', () => {
19
- // Space vs Bug - Space has advantage
20
- const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
21
- let turns = 0;
22
- const maxTurns = 20;
23
-
24
- while (!engine.isGameOver() && turns < maxTurns) {
25
- const playerAction: BattleAction = {
26
- type: 'move',
27
- piclet: 'player',
28
- moveIndex: 1 // Flame Burst (Space type)
29
- };
30
- const opponentAction: BattleAction = {
31
- type: 'move',
32
- piclet: 'opponent',
33
- moveIndex: 0 // Tackle
34
- };
35
-
36
- engine.executeActions(playerAction, opponentAction);
37
- turns++;
38
- }
39
-
40
- expect(engine.isGameOver()).toBe(true);
41
- expect(turns).toBeLessThan(maxTurns);
42
-
43
- // Player should win due to type advantage
44
- expect(engine.getWinner()).toBe('player');
45
-
46
- const log = engine.getLog();
47
- expect(log.some(msg => msg.includes("It's super effective!"))).toBe(true);
48
- });
49
-
50
- it('should handle a battle with status effects and healing', () => {
51
- const engine = new BattleEngine(TOXIC_CRAWLER, AQUA_GUARDIAN);
52
-
53
- // Turn 1: Toxic Crawler uses Toxic Sting to poison
54
- engine.executeActions(
55
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Toxic Sting
56
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle
57
- );
58
-
59
- // Check that opponent is poisoned
60
- expect(engine.getState().opponentPiclet.statusEffects).toContain('poison');
61
-
62
- // Turn 2: Guardian tries to heal while poison damage occurs
63
- const hpBeforeTurn = engine.getState().opponentPiclet.currentHp;
64
- engine.executeActions(
65
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Tackle
66
- { type: 'move', piclet: 'opponent', moveIndex: 1 } // Healing Light
67
- );
68
-
69
- // Poison should have done damage during turn end
70
- const log = engine.getLog();
71
- expect(log.some(msg => msg.includes('hurt by poison'))).toBe(true);
72
- });
73
-
74
- it('should handle conditional move effects correctly', () => {
75
- const engine = new BattleEngine(BERSERKER_BEAST, AQUA_GUARDIAN);
76
-
77
- // Damage the berserker to trigger low HP condition
78
- engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.2);
79
-
80
- const initialDefense = engine.getState().playerPiclet.defense;
81
- const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
82
- const initialHpRatio = engine.getState().playerPiclet.currentHp / engine.getState().playerPiclet.maxHp;
83
-
84
- // Use Berserker's End while at low HP
85
- engine.executeActions(
86
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Berserker's End
87
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle
88
- );
89
-
90
- const finalDefense = engine.getState().playerPiclet.defense;
91
- const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
92
-
93
- // Should deal damage (may miss due to 90% accuracy, so check if hit)
94
- const damageDealt = initialOpponentHp - finalOpponentHp;
95
- const log = engine.getLog();
96
- const moveHit = !log.some(msg => msg.includes('attack missed'));
97
-
98
- if (moveHit) {
99
- expect(damageDealt).toBeGreaterThan(20); // Should be significant due to strong damage condition
100
- } else {
101
- expect(damageDealt).toBe(0); // No damage if missed
102
- }
103
-
104
- // The defense should decrease if HP is below 25% (0.25) due to ifLowHp condition
105
- if (initialHpRatio < 0.25) {
106
- expect(finalDefense).toBeLessThan(initialDefense);
107
- } else {
108
- // If not low HP, no defense change expected
109
- expect(finalDefense).toBe(initialDefense);
110
- }
111
- });
112
-
113
- it('should handle stat modifications and their effects on damage', () => {
114
- const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
115
-
116
- // Turn 1: Power Up to increase attack
117
- engine.executeActions(
118
- { type: 'move', piclet: 'player', moveIndex: 3 }, // Power Up
119
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle
120
- );
121
-
122
- const boostedAttack = engine.getState().playerPiclet.attack;
123
- const opponentHpAfterBoost = engine.getState().opponentPiclet.currentHp;
124
-
125
- // Turn 2: Attack with boosted stats
126
- engine.executeActions(
127
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Tackle
128
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle
129
- );
130
-
131
- const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
132
- const damageWithBoost = opponentHpAfterBoost - finalOpponentHp;
133
-
134
- // Damage should be higher due to attack boost
135
- expect(damageWithBoost).toBeGreaterThan(20);
136
- expect(boostedAttack).toBeGreaterThan(STELLAR_WOLF.baseStats.attack);
137
- });
138
-
139
- it('should maintain battle log integrity throughout complex battle', () => {
140
- const engine = new BattleEngine(STELLAR_WOLF, BERSERKER_BEAST);
141
-
142
- // Execute several turns with different moves
143
- const moves = [
144
- [3, 0], // Power Up vs Tackle
145
- [1, 1], // Flame Burst vs Berserker's End
146
- [2, 2], // Healing Light vs Healing Light
147
- [0, 0] // Tackle vs Tackle
148
- ];
149
-
150
- for (const [playerMove, opponentMove] of moves) {
151
- if (engine.isGameOver()) break;
152
-
153
- engine.executeActions(
154
- { type: 'move', piclet: 'player', moveIndex: playerMove },
155
- { type: 'move', piclet: 'opponent', moveIndex: opponentMove }
156
- );
157
- }
158
-
159
- const log = engine.getLog();
160
- expect(log.length).toBeGreaterThan(8);
161
-
162
- // Should contain move usage
163
- expect(log.some(msg => msg.includes('used Power Up'))).toBe(true);
164
- expect(log.some(msg => msg.includes('used Flame Burst'))).toBe(true);
165
-
166
- // Should contain stat changes
167
- expect(log.some(msg => msg.includes('attack rose'))).toBe(true);
168
-
169
- // Should contain healing (check for either recovered HP or no actual healing if at full HP)
170
- const hasHealing = log.some(msg => msg.includes('recovered') && msg.includes('HP'));
171
- const hasHealingAttempt = log.some(msg => msg.includes('used Healing Light'));
172
- expect(hasHealing || hasHealingAttempt).toBe(true);
173
- });
174
-
175
- it('should handle edge case: all moves run out of PP', () => {
176
- const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
177
-
178
- // Drain all PP from one move
179
- engine['state'].playerPiclet.moves[0].currentPP = 0;
180
- engine['state'].playerPiclet.moves[1].currentPP = 0;
181
- engine['state'].playerPiclet.moves[2].currentPP = 0;
182
- engine['state'].playerPiclet.moves[3].currentPP = 0;
183
-
184
- // Try to use any move
185
- engine.executeActions(
186
- { type: 'move', piclet: 'player', moveIndex: 0 },
187
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
188
- );
189
-
190
- const log = engine.getLog();
191
- expect(log.some(msg => msg.includes('no PP left'))).toBe(true);
192
-
193
- // Battle should continue (opponent can still act)
194
- expect(engine.isGameOver()).toBe(false);
195
- });
196
- });
197
-
198
- describe('Performance and Stability', () => {
199
- it('should handle very long battles without issues', () => {
200
- const engine = new BattleEngine(AQUA_GUARDIAN, AQUA_GUARDIAN);
201
- let turns = 0;
202
- const maxTurns = 100;
203
-
204
- while (!engine.isGameOver() && turns < maxTurns) {
205
- // Both use healing moves to prolong battle
206
- engine.executeActions(
207
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Healing Light
208
- { type: 'move', piclet: 'opponent', moveIndex: 1 } // Healing Light
209
- );
210
- turns++;
211
-
212
- // Occasionally attack to prevent infinite loop
213
- if (turns % 5 === 0) {
214
- engine.executeActions(
215
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Tackle
216
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Tackle
217
- );
218
- }
219
- }
220
-
221
- // Should either end naturally or reach turn limit
222
- expect(turns).toBeLessThanOrEqual(maxTurns);
223
-
224
- // Engine should remain stable
225
- const state = engine.getState();
226
- expect(state.turn).toBeGreaterThan(1);
227
- expect(state.log.length).toBeGreaterThan(0);
228
- });
229
-
230
- it('should maintain state consistency after many operations', () => {
231
- const engine = new BattleEngine(STELLAR_WOLF, TOXIC_CRAWLER);
232
-
233
- // Perform many state-changing operations
234
- for (let i = 0; i < 10 && !engine.isGameOver(); i++) {
235
- const state = engine.getState();
236
-
237
- // Verify state consistency before each turn
238
- expect(state.playerPiclet.currentHp).toBeGreaterThanOrEqual(0);
239
- expect(state.opponentPiclet.currentHp).toBeGreaterThanOrEqual(0);
240
- expect(state.playerPiclet.currentHp).toBeLessThanOrEqual(state.playerPiclet.maxHp);
241
- expect(state.opponentPiclet.currentHp).toBeLessThanOrEqual(state.opponentPiclet.maxHp);
242
-
243
- engine.executeActions(
244
- { type: 'move', piclet: 'player', moveIndex: i % 4 },
245
- { type: 'move', piclet: 'opponent', moveIndex: i % 3 }
246
- );
247
- }
248
-
249
- // Final state should still be consistent
250
- const finalState = engine.getState();
251
- expect(finalState.playerPiclet.currentHp).toBeGreaterThanOrEqual(0);
252
- expect(finalState.opponentPiclet.currentHp).toBeGreaterThanOrEqual(0);
253
- });
254
- });
255
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/mechanic-overrides.test.ts DELETED
@@ -1,544 +0,0 @@
1
- /**
2
- * Tests for mechanic override system from the design document
3
- * Tests special abilities that modify core battle mechanics
4
- */
5
-
6
- import { describe, it, expect, beforeEach } from 'vitest';
7
- import { BattleEngine } from './BattleEngine';
8
- import { PicletDefinition, Move, SpecialAbility } from './types';
9
- import { PicletType, AttackType } from './types';
10
-
11
- const STANDARD_STATS = { hp: 100, attack: 80, defense: 70, speed: 60 };
12
-
13
- describe('Mechanic Override System - TDD Implementation', () => {
14
- describe('Critical Hit Mechanics', () => {
15
- it('should handle Shell Armor - cannot be critically hit', () => {
16
- const shellArmor: SpecialAbility = {
17
- name: "Shell Armor",
18
- description: "Hard shell prevents critical hits",
19
- effects: [
20
- {
21
- type: 'mechanicOverride',
22
- mechanic: 'criticalHits',
23
- condition: 'always',
24
- value: false
25
- }
26
- ]
27
- };
28
-
29
- const shellPiclet: PicletDefinition = {
30
- name: "Shell Defender",
31
- description: "Protected by a hard shell",
32
- tier: 'medium',
33
- primaryType: PicletType.MINERAL,
34
- baseStats: STANDARD_STATS,
35
- nature: "Bold",
36
- specialAbility: shellArmor,
37
- movepool: [{
38
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
39
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
40
- }]
41
- };
42
-
43
- // Test would verify this Piclet cannot be critically hit
44
- expect(shellArmor.effects![0].mechanic).toBe('criticalHits');
45
- expect(shellArmor.effects![0].value).toBe(false);
46
- });
47
-
48
- it('should handle Super Luck - always critical hits', () => {
49
- const superLuck: SpecialAbility = {
50
- name: "Super Luck",
51
- description: "Extremely lucky, always lands critical hits",
52
- effects: [
53
- {
54
- type: 'mechanicOverride',
55
- mechanic: 'criticalHits',
56
- condition: 'always',
57
- value: true
58
- }
59
- ]
60
- };
61
-
62
- expect(superLuck.effects![0].value).toBe(true);
63
- });
64
-
65
- it('should handle Scope Lens - double critical hit rate', () => {
66
- const scopeLens: SpecialAbility = {
67
- name: "Scope Lens",
68
- description: "Enhanced precision doubles critical hit rate",
69
- effects: [
70
- {
71
- type: 'mechanicOverride',
72
- mechanic: 'criticalHits',
73
- condition: 'always',
74
- value: 'double'
75
- }
76
- ]
77
- };
78
-
79
- expect(scopeLens.effects![0].value).toBe('double');
80
- });
81
- });
82
-
83
- describe('Status Immunity', () => {
84
- it('should handle Insomnia - sleep immunity', () => {
85
- const insomnia: SpecialAbility = {
86
- name: "Insomnia",
87
- description: "Prevents sleep status",
88
- effects: [
89
- {
90
- type: 'mechanicOverride',
91
- mechanic: 'statusImmunity',
92
- value: ['sleep']
93
- }
94
- ]
95
- };
96
-
97
- const insomniaPiclet: PicletDefinition = {
98
- name: "Sleepless Guardian",
99
- description: "Never sleeps",
100
- tier: 'medium',
101
- primaryType: PicletType.CULTURE,
102
- baseStats: STANDARD_STATS,
103
- nature: "Alert",
104
- specialAbility: insomnia,
105
- movepool: [{
106
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
107
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
108
- }]
109
- };
110
-
111
- expect(insomnia.effects![0].value).toContain('sleep');
112
- });
113
-
114
- it('should handle multi-status immunity', () => {
115
- const immunity: SpecialAbility = {
116
- name: "Pure Body",
117
- description: "Immune to poison and burn",
118
- effects: [
119
- {
120
- type: 'mechanicOverride',
121
- mechanic: 'statusImmunity',
122
- value: ['poison', 'burn']
123
- }
124
- ]
125
- };
126
-
127
- expect(immunity.effects![0].value).toEqual(['poison', 'burn']);
128
- });
129
- });
130
-
131
- describe('Damage Reflection', () => {
132
- it('should handle Rough Skin - contact damage reflection', () => {
133
- const roughSkin: SpecialAbility = {
134
- name: "Rough Skin",
135
- description: "Rough skin damages attackers on contact",
136
- triggers: [
137
- {
138
- event: 'onContactDamage',
139
- effects: [
140
- {
141
- type: 'damage',
142
- target: 'attacker',
143
- formula: 'fixed',
144
- value: 12
145
- }
146
- ]
147
- }
148
- ]
149
- };
150
-
151
- const roughPiclet: PicletDefinition = {
152
- name: "Spike Beast",
153
- description: "Covered in rough spikes",
154
- tier: 'medium',
155
- primaryType: PicletType.MINERAL,
156
- baseStats: STANDARD_STATS,
157
- nature: "Hardy",
158
- specialAbility: roughSkin,
159
- movepool: [{
160
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
161
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
162
- }]
163
- };
164
-
165
- expect(roughSkin.triggers![0].event).toBe('onContactDamage');
166
- expect(roughSkin.triggers![0].effects[0].target).toBe('attacker');
167
- });
168
-
169
- it('should handle damage reflection percentage', () => {
170
- const reflectArmor: SpecialAbility = {
171
- name: "Mirror Armor",
172
- description: "Reflects 50% of damage back",
173
- effects: [
174
- {
175
- type: 'mechanicOverride',
176
- mechanic: 'damageReflection',
177
- value: 0.5
178
- }
179
- ]
180
- };
181
-
182
- expect(reflectArmor.effects![0].value).toBe(0.5);
183
- });
184
- });
185
-
186
- describe('Type Mechanics', () => {
187
- it('should handle Wonder Guard - only super-effective moves hit', () => {
188
- const wonderGuard: SpecialAbility = {
189
- name: "Wonder Guard",
190
- description: "Only super-effective moves deal damage",
191
- effects: [
192
- {
193
- type: 'mechanicOverride',
194
- mechanic: 'damageCalculation',
195
- condition: 'ifNotSuperEffective',
196
- value: false
197
- }
198
- ]
199
- };
200
-
201
- expect(wonderGuard.effects![0].mechanic).toBe('damageCalculation');
202
- expect(wonderGuard.effects![0].condition).toBe('ifNotSuperEffective');
203
- });
204
-
205
- it('should handle Levitate - ground type immunity', () => {
206
- const levitate: SpecialAbility = {
207
- name: "Levitate",
208
- description: "Floating ability makes ground moves miss",
209
- effects: [
210
- {
211
- type: 'mechanicOverride',
212
- mechanic: 'typeImmunity',
213
- value: ['ground']
214
- }
215
- ]
216
- };
217
-
218
- expect(levitate.effects![0].value).toContain('ground');
219
- });
220
-
221
- it('should handle Protean - type changes to match move', () => {
222
- const protean: SpecialAbility = {
223
- name: "Protean",
224
- description: "Changes type to match the move being used",
225
- triggers: [
226
- {
227
- event: 'beforeMoveUse',
228
- effects: [
229
- {
230
- type: 'mechanicOverride',
231
- mechanic: 'typeChange',
232
- value: 'matchMoveType'
233
- }
234
- ]
235
- }
236
- ]
237
- };
238
-
239
- expect(protean.triggers![0].event).toBe('beforeMoveUse');
240
- expect(protean.triggers![0].effects[0].value).toBe('matchMoveType');
241
- });
242
- });
243
-
244
- describe('Healing Mechanics', () => {
245
- it('should handle Poison Heal - poison heals instead of damages', () => {
246
- const poisonHeal: SpecialAbility = {
247
- name: "Poison Heal",
248
- description: "Poison heals instead of damages",
249
- effects: [
250
- {
251
- type: 'mechanicOverride',
252
- mechanic: 'healingInversion',
253
- value: 'invert'
254
- }
255
- ]
256
- };
257
-
258
- expect(poisonHeal.effects![0].mechanic).toBe('healingInversion');
259
- expect(poisonHeal.effects![0].value).toBe('invert');
260
- });
261
-
262
- it('should handle healing blocked', () => {
263
- const healBlock: SpecialAbility = {
264
- name: "Cursed Body",
265
- description: "Cannot be healed by any means",
266
- effects: [
267
- {
268
- type: 'mechanicOverride',
269
- mechanic: 'healingBlocked',
270
- value: true
271
- }
272
- ]
273
- };
274
-
275
- expect(healBlock.effects![0].value).toBe(true);
276
- });
277
- });
278
-
279
- describe('Damage Absorption', () => {
280
- it('should handle Photosynthesis - absorbs flora moves', () => {
281
- const photosynthesis: SpecialAbility = {
282
- name: "Photosynthesis",
283
- description: "Absorbs flora-type moves to restore HP",
284
- triggers: [
285
- {
286
- event: 'onDamageTaken',
287
- condition: 'ifMoveType:flora',
288
- effects: [
289
- {
290
- type: 'mechanicOverride',
291
- mechanic: 'damageAbsorption',
292
- value: 'absorb'
293
- },
294
- {
295
- type: 'heal',
296
- target: 'self',
297
- formula: 'percentage',
298
- value: 25
299
- }
300
- ]
301
- }
302
- ]
303
- };
304
-
305
- expect(photosynthesis.triggers![0].condition).toBe('ifMoveType:flora');
306
- expect(photosynthesis.triggers![0].effects[0].mechanic).toBe('damageAbsorption');
307
- });
308
- });
309
-
310
- describe('Stat Modification Mechanics', () => {
311
- it('should handle Contrary - stat changes are reversed', () => {
312
- const contrary: SpecialAbility = {
313
- name: "Contrary",
314
- description: "Stat changes have the opposite effect",
315
- effects: [
316
- {
317
- type: 'mechanicOverride',
318
- mechanic: 'statModification',
319
- value: 'invert'
320
- }
321
- ]
322
- };
323
-
324
- expect(contrary.effects![0].value).toBe('invert');
325
- });
326
- });
327
-
328
- describe('Flag-Based Immunities', () => {
329
- it('should handle Sky Dancer - immune to ground-flagged attacks', () => {
330
- const skyDancer: SpecialAbility = {
331
- name: "Sky Dancer",
332
- description: "Floating in air, immune to ground-based attacks",
333
- effects: [
334
- {
335
- type: 'mechanicOverride',
336
- mechanic: 'flagImmunity',
337
- value: ['ground']
338
- }
339
- ]
340
- };
341
-
342
- expect(skyDancer.effects![0].value).toContain('ground');
343
- });
344
-
345
- it('should handle Sound Barrier - immune to sound attacks', () => {
346
- const soundBarrier: SpecialAbility = {
347
- name: "Sound Barrier",
348
- description: "Natural sound dampening prevents sound-based moves",
349
- effects: [
350
- {
351
- type: 'mechanicOverride',
352
- mechanic: 'flagImmunity',
353
- value: ['sound']
354
- }
355
- ]
356
- };
357
-
358
- expect(soundBarrier.effects![0].value).toContain('sound');
359
- });
360
-
361
- it('should handle Soft Body - immune to explosive, weak to punch', () => {
362
- const softBody: SpecialAbility = {
363
- name: "Soft Body",
364
- description: "Gelatinous form absorbs explosions but vulnerable to direct hits",
365
- effects: [
366
- {
367
- type: 'mechanicOverride',
368
- mechanic: 'flagImmunity',
369
- value: ['explosive']
370
- },
371
- {
372
- type: 'mechanicOverride',
373
- mechanic: 'flagWeakness',
374
- value: ['punch']
375
- }
376
- ]
377
- };
378
-
379
- expect(softBody.effects![0].value).toContain('explosive');
380
- expect(softBody.effects![1].value).toContain('punch');
381
- });
382
-
383
- it('should handle flag resistance', () => {
384
- const thickHide: SpecialAbility = {
385
- name: "Thick Hide",
386
- description: "Tough skin reduces impact from physical contact",
387
- effects: [
388
- {
389
- type: 'mechanicOverride',
390
- mechanic: 'flagResistance',
391
- value: ['contact']
392
- }
393
- ]
394
- };
395
-
396
- expect(thickHide.effects![0].value).toContain('contact');
397
- });
398
- });
399
-
400
- describe('Priority Override', () => {
401
- it('should handle Prankster - status moves get priority', () => {
402
- const prankster: SpecialAbility = {
403
- name: "Prankster",
404
- description: "Status moves gain priority",
405
- effects: [
406
- {
407
- type: 'mechanicOverride',
408
- mechanic: 'priorityOverride',
409
- condition: 'ifStatusMove',
410
- value: 1
411
- }
412
- ]
413
- };
414
-
415
- expect(prankster.effects![0].condition).toBe('ifStatusMove');
416
- expect(prankster.effects![0].value).toBe(1);
417
- });
418
- });
419
-
420
- describe('Drain Inversion', () => {
421
- it('should handle Vampiric - drain moves damage the drainer', () => {
422
- const vampiric: SpecialAbility = {
423
- name: "Vampiric",
424
- description: "Cursed blood damages those who try to drain it",
425
- triggers: [
426
- {
427
- event: 'onHPDrained',
428
- effects: [
429
- {
430
- type: 'mechanicOverride',
431
- mechanic: 'drainInversion',
432
- value: true
433
- },
434
- {
435
- type: 'damage',
436
- target: 'attacker',
437
- formula: 'fixed',
438
- value: 20
439
- }
440
- ]
441
- }
442
- ]
443
- };
444
-
445
- expect(vampiric.triggers![0].event).toBe('onHPDrained');
446
- expect(vampiric.triggers![0].effects[0].mechanic).toBe('drainInversion');
447
- });
448
- });
449
-
450
- describe('Target Redirection', () => {
451
- it('should handle Magic Bounce - reflects status moves', () => {
452
- const magicBounce: SpecialAbility = {
453
- name: "Magic Bounce",
454
- description: "Reflects status moves back at the user",
455
- triggers: [
456
- {
457
- event: 'onStatusMoveTargeted',
458
- effects: [
459
- {
460
- type: 'mechanicOverride',
461
- mechanic: 'targetRedirection',
462
- value: 'reflect'
463
- }
464
- ]
465
- }
466
- ]
467
- };
468
-
469
- expect(magicBounce.triggers![0].event).toBe('onStatusMoveTargeted');
470
- expect(magicBounce.triggers![0].effects[0].value).toBe('reflect');
471
- });
472
- });
473
-
474
- describe('Status Replacement', () => {
475
- it('should handle Frost Walker - freeze becomes attack boost', () => {
476
- const frostWalker: SpecialAbility = {
477
- name: "Frost Walker",
478
- description: "Instead of being frozen, gains +50% attack",
479
- effects: [
480
- {
481
- type: 'mechanicOverride',
482
- mechanic: 'statusReplacement',
483
- value: {
484
- status: 'freeze',
485
- replacement: {
486
- type: 'modifyStats',
487
- target: 'self',
488
- stats: { attack: 'greatly_increase' }
489
- }
490
- }
491
- }
492
- ]
493
- };
494
-
495
- expect(frostWalker.effects![0].mechanic).toBe('statusReplacement');
496
- expect(frostWalker.effects![0].value.status).toBe('freeze');
497
- });
498
- });
499
-
500
- describe('Damage Multiplier', () => {
501
- it('should handle damage multiplication abilities', () => {
502
- const damageBoost: SpecialAbility = {
503
- name: "Rage Mode",
504
- description: "All damage dealt is doubled when at low HP",
505
- effects: [
506
- {
507
- type: 'mechanicOverride',
508
- mechanic: 'damageMultiplier',
509
- condition: 'ifLowHp',
510
- value: 2.0
511
- }
512
- ]
513
- };
514
-
515
- expect(damageBoost.effects![0].value).toBe(2.0);
516
- expect(damageBoost.effects![0].condition).toBe('ifLowHp');
517
- });
518
- });
519
-
520
- describe('Extra Turn Mechanics', () => {
521
- it('should handle extra turn abilities', () => {
522
- const extraTurn: SpecialAbility = {
523
- name: "Time Distortion",
524
- description: "Gets an extra turn when switching in",
525
- triggers: [
526
- {
527
- event: 'onSwitchIn',
528
- effects: [
529
- {
530
- type: 'mechanicOverride',
531
- mechanic: 'extraTurn',
532
- value: true,
533
- condition: 'nextTurn'
534
- }
535
- ]
536
- }
537
- ]
538
- };
539
-
540
- expect(extraTurn.triggers![0].effects[0].mechanic).toBe('extraTurn');
541
- expect(extraTurn.triggers![0].effects[0].condition).toBe('nextTurn');
542
- });
543
- });
544
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/missing-features.test.ts DELETED
@@ -1,498 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition, Move, SpecialAbility } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Missing Battle System Features', () => {
7
- describe('manipulatePP Effects', () => {
8
- it('should drain opponent PP', () => {
9
- const ppDrainer: PicletDefinition = {
10
- name: "PP Drainer",
11
- description: "Drains opponent's PP",
12
- tier: 'medium',
13
- primaryType: PicletType.CULTURE,
14
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
15
- nature: "Calm",
16
- specialAbility: { name: "No Ability", description: "" },
17
- movepool: [
18
- {
19
- name: "Mind Drain",
20
- type: AttackType.CULTURE,
21
- power: 0,
22
- accuracy: 100,
23
- pp: 10,
24
- priority: 0,
25
- flags: [],
26
- effects: [
27
- {
28
- type: 'manipulatePP',
29
- target: 'opponent',
30
- action: 'drain',
31
- amount: 'medium'
32
- }
33
- ]
34
- }
35
- ]
36
- };
37
-
38
- const opponent: PicletDefinition = {
39
- name: "Opponent",
40
- description: "Standard opponent",
41
- tier: 'medium',
42
- primaryType: PicletType.BEAST,
43
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
44
- nature: "Hardy",
45
- specialAbility: { name: "No Ability", description: "" },
46
- movepool: [
47
- {
48
- name: "Tackle",
49
- type: AttackType.NORMAL,
50
- power: 40,
51
- accuracy: 100,
52
- pp: 10,
53
- priority: 0,
54
- flags: ['contact'],
55
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
56
- }
57
- ]
58
- };
59
-
60
- const engine = new BattleEngine(ppDrainer, opponent);
61
- const initialPP = engine.getState().opponentPiclet.moves[0].currentPP;
62
-
63
- engine.executeActions(
64
- { type: 'move', piclet: 'player', moveIndex: 0 },
65
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
66
- );
67
-
68
- const finalPP = engine.getState().opponentPiclet.moves[0].currentPP;
69
- expect(finalPP).toBeLessThan(initialPP);
70
- expect(engine.getLog().some(msg => msg.includes('PP was drained'))).toBe(true);
71
- });
72
-
73
- it('should restore own PP', () => {
74
- const ppRestorer: PicletDefinition = {
75
- name: "PP Restorer",
76
- description: "Restores own PP",
77
- tier: 'medium',
78
- primaryType: PicletType.FLORA,
79
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
80
- nature: "Calm",
81
- specialAbility: { name: "No Ability", description: "" },
82
- movepool: [
83
- {
84
- name: "PP Restore",
85
- type: AttackType.FLORA,
86
- power: 0,
87
- accuracy: 100,
88
- pp: 5,
89
- priority: 0,
90
- flags: [],
91
- effects: [
92
- {
93
- type: 'manipulatePP',
94
- target: 'self',
95
- action: 'restore',
96
- amount: 'large'
97
- }
98
- ]
99
- }
100
- ]
101
- };
102
-
103
- const opponent: PicletDefinition = {
104
- name: "Opponent",
105
- description: "Standard opponent",
106
- tier: 'medium',
107
- primaryType: PicletType.BEAST,
108
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
109
- nature: "Hardy",
110
- specialAbility: { name: "No Ability", description: "" },
111
- movepool: [
112
- {
113
- name: "Tackle",
114
- type: AttackType.NORMAL,
115
- power: 40,
116
- accuracy: 100,
117
- pp: 10,
118
- priority: 0,
119
- flags: ['contact'],
120
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
121
- }
122
- ]
123
- };
124
-
125
- const engine = new BattleEngine(ppRestorer, opponent);
126
-
127
- // Use the PP restore move multiple times to drain it
128
- engine['state'].playerPiclet.moves[0].currentPP = 1;
129
- const initialPP = engine['state'].playerPiclet.moves[0].currentPP;
130
-
131
- engine.executeActions(
132
- { type: 'move', piclet: 'player', moveIndex: 0 },
133
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
134
- );
135
-
136
- const finalPP = engine.getState().playerPiclet.moves[0].currentPP;
137
- expect(finalPP).toBeGreaterThan(initialPP);
138
- expect(engine.getLog().some(msg => msg.includes('PP was restored'))).toBe(true);
139
- });
140
- });
141
-
142
- describe('fieldEffect System', () => {
143
- it('should apply field effects that persist across turns', () => {
144
- const fieldEffectUser: PicletDefinition = {
145
- name: "Field Controller",
146
- description: "Controls battlefield effects",
147
- tier: 'medium',
148
- primaryType: PicletType.SPACE,
149
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
150
- nature: "Calm",
151
- specialAbility: { name: "No Ability", description: "" },
152
- movepool: [
153
- {
154
- name: "Reflect",
155
- type: AttackType.SPACE,
156
- power: 0,
157
- accuracy: 100,
158
- pp: 10,
159
- priority: 0,
160
- flags: [],
161
- effects: [
162
- {
163
- type: 'fieldEffect',
164
- effect: 'reflect',
165
- target: 'playerSide',
166
- stackable: false
167
- }
168
- ]
169
- }
170
- ]
171
- };
172
-
173
- const opponent: PicletDefinition = {
174
- name: "Opponent",
175
- description: "Standard opponent",
176
- tier: 'medium',
177
- primaryType: PicletType.BEAST,
178
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
179
- nature: "Hardy",
180
- specialAbility: { name: "No Ability", description: "" },
181
- movepool: [
182
- {
183
- name: "Physical Attack",
184
- type: AttackType.BEAST,
185
- power: 60,
186
- accuracy: 100,
187
- pp: 10,
188
- priority: 0,
189
- flags: ['contact'],
190
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
191
- }
192
- ]
193
- };
194
-
195
- const engine = new BattleEngine(fieldEffectUser, opponent);
196
-
197
- // Apply reflect
198
- engine.executeActions(
199
- { type: 'move', piclet: 'player', moveIndex: 0 },
200
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
201
- );
202
-
203
- expect(engine.getLog().some(msg => msg.includes('Reflect') && msg.includes('applied'))).toBe(true);
204
-
205
- // Check if reflect reduces physical damage in subsequent turns
206
- const initialHp = engine.getState().playerPiclet.currentHp;
207
-
208
- engine.executeActions(
209
- { type: 'move', piclet: 'player', moveIndex: 0 },
210
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
211
- );
212
-
213
- const finalHp = engine.getState().playerPiclet.currentHp;
214
- const damage = initialHp - finalHp;
215
-
216
- // Reflect should reduce physical damage
217
- expect(damage).toBeLessThan(30); // Should be reduced from normal ~40-50 damage
218
- });
219
-
220
- it('should handle spikes field effect', () => {
221
- const spikesUser: PicletDefinition = {
222
- name: "Spikes User",
223
- description: "Sets entry hazards",
224
- tier: 'medium',
225
- primaryType: PicletType.MINERAL,
226
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
227
- nature: "Impish",
228
- specialAbility: { name: "No Ability", description: "" },
229
- movepool: [
230
- {
231
- name: "Spikes",
232
- type: AttackType.MINERAL,
233
- power: 0,
234
- accuracy: 100,
235
- pp: 10,
236
- priority: 0,
237
- flags: [],
238
- effects: [
239
- {
240
- type: 'fieldEffect',
241
- effect: 'spikes',
242
- target: 'opponentSide',
243
- stackable: true
244
- }
245
- ]
246
- }
247
- ]
248
- };
249
-
250
- const opponent: PicletDefinition = {
251
- name: "Opponent",
252
- description: "Standard opponent",
253
- tier: 'medium',
254
- primaryType: PicletType.BEAST,
255
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
256
- nature: "Hardy",
257
- specialAbility: { name: "No Ability", description: "" },
258
- movepool: [
259
- {
260
- name: "Tackle",
261
- type: AttackType.NORMAL,
262
- power: 40,
263
- accuracy: 100,
264
- pp: 10,
265
- priority: 0,
266
- flags: ['contact'],
267
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
268
- }
269
- ]
270
- };
271
-
272
- const engine = new BattleEngine(spikesUser, opponent);
273
-
274
- engine.executeActions(
275
- { type: 'move', piclet: 'player', moveIndex: 0 },
276
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
277
- );
278
-
279
- expect(engine.getLog().some(msg => msg.includes('Spikes') && msg.includes('set'))).toBe(true);
280
-
281
- // TODO: Test spikes damage when switching (requires switching mechanics)
282
- });
283
- });
284
-
285
- describe('counter Effects', () => {
286
- it('should counter physical attacks', () => {
287
- const counterUser: PicletDefinition = {
288
- name: "Counter Fighter",
289
- description: "Counters physical attacks",
290
- tier: 'medium',
291
- primaryType: PicletType.BEAST,
292
- baseStats: { hp: 100, attack: 60, defense: 80, speed: 50 },
293
- nature: "Brave",
294
- specialAbility: { name: "No Ability", description: "" },
295
- movepool: [
296
- {
297
- name: "Counter",
298
- type: AttackType.BEAST,
299
- power: 0,
300
- accuracy: 100,
301
- pp: 10,
302
- priority: 1, // High priority to set up counter before opponent attacks
303
- flags: ['lowPriority'],
304
- effects: [
305
- {
306
- type: 'counter',
307
- strength: 'strong'
308
- }
309
- ]
310
- }
311
- ]
312
- };
313
-
314
- const opponent: PicletDefinition = {
315
- name: "Physical Attacker",
316
- description: "Uses physical moves",
317
- tier: 'medium',
318
- primaryType: PicletType.BEAST,
319
- baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 },
320
- nature: "Adamant",
321
- specialAbility: { name: "No Ability", description: "" },
322
- movepool: [
323
- {
324
- name: "Physical Strike",
325
- type: AttackType.BEAST,
326
- power: 80,
327
- accuracy: 100,
328
- pp: 10,
329
- priority: 0,
330
- flags: ['contact'],
331
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
332
- }
333
- ]
334
- };
335
-
336
- const engine = new BattleEngine(counterUser, opponent);
337
- const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
338
-
339
- engine.executeActions(
340
- { type: 'move', piclet: 'player', moveIndex: 0 },
341
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
342
- );
343
-
344
- const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
345
- expect(finalOpponentHp).toBeLessThan(initialOpponentHp);
346
- expect(engine.getLog().some(msg => msg.includes('countered') || msg.includes('Counter'))).toBe(true);
347
- });
348
- });
349
-
350
- describe('priority Effects', () => {
351
- it('should modify move priority conditionally', () => {
352
- const priorityUser: PicletDefinition = {
353
- name: "Priority User",
354
- description: "Uses priority moves based on conditions",
355
- tier: 'medium',
356
- primaryType: PicletType.SPACE,
357
- baseStats: { hp: 60, attack: 70, defense: 50, speed: 40 },
358
- nature: "Quiet",
359
- specialAbility: { name: "No Ability", description: "" },
360
- movepool: [
361
- {
362
- name: "Desperation Strike",
363
- type: AttackType.SPACE,
364
- power: 60,
365
- accuracy: 100,
366
- pp: 10,
367
- priority: 0,
368
- flags: [],
369
- effects: [
370
- {
371
- type: 'damage',
372
- target: 'opponent',
373
- amount: 'normal'
374
- },
375
- {
376
- type: 'priority',
377
- target: 'self',
378
- value: 1,
379
- condition: 'ifLowHp'
380
- }
381
- ]
382
- }
383
- ]
384
- };
385
-
386
- const fastOpponent: PicletDefinition = {
387
- name: "Fast Opponent",
388
- description: "Very fast opponent",
389
- tier: 'medium',
390
- primaryType: PicletType.BEAST,
391
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 100 },
392
- nature: "Timid",
393
- specialAbility: { name: "No Ability", description: "" },
394
- movepool: [
395
- {
396
- name: "Quick Attack",
397
- type: AttackType.NORMAL,
398
- power: 40,
399
- accuracy: 100,
400
- pp: 10,
401
- priority: 0,
402
- flags: [],
403
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
404
- }
405
- ]
406
- };
407
-
408
- const engine = new BattleEngine(priorityUser, fastOpponent);
409
-
410
- // Damage the priority user to trigger low HP condition
411
- engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.2);
412
-
413
- engine.executeActions(
414
- { type: 'move', piclet: 'player', moveIndex: 0 },
415
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
416
- );
417
-
418
- const log = engine.getLog();
419
- const playerMoveIndex = log.findIndex(msg => msg.includes('Priority User used Desperation Strike'));
420
- const opponentMoveIndex = log.findIndex(msg => msg.includes('Fast Opponent used Quick Attack'));
421
-
422
- // When at low HP, priority user should go first despite lower speed
423
- expect(playerMoveIndex).toBeLessThan(opponentMoveIndex);
424
- });
425
- });
426
-
427
- describe('removeStatus Effects', () => {
428
- it('should remove status effects from target', () => {
429
- // Simple test: create a move that only removes poison status
430
- const cleanser: PicletDefinition = {
431
- name: "Cleanser",
432
- description: "Can remove poison",
433
- tier: 'medium',
434
- primaryType: PicletType.FLORA,
435
- baseStats: { hp: 100, attack: 50, defense: 50, speed: 50 },
436
- nature: "Calm",
437
- specialAbility: { name: "No Ability", description: "" },
438
- movepool: [
439
- {
440
- name: "Cleanse",
441
- type: AttackType.FLORA,
442
- power: 0,
443
- accuracy: 100,
444
- pp: 10,
445
- priority: 0,
446
- flags: [],
447
- effects: [
448
- {
449
- type: 'removeStatus',
450
- target: 'self',
451
- status: 'poison'
452
- }
453
- ]
454
- }
455
- ]
456
- };
457
-
458
- const dummy: PicletDefinition = {
459
- name: "Dummy",
460
- description: "Does nothing",
461
- tier: 'low',
462
- primaryType: PicletType.BEAST,
463
- baseStats: { hp: 50, attack: 30, defense: 30, speed: 30 },
464
- nature: "Docile",
465
- specialAbility: { name: "No Ability", description: "" },
466
- movepool: [
467
- {
468
- name: "Do Nothing",
469
- type: AttackType.NORMAL,
470
- power: 0,
471
- accuracy: 100,
472
- pp: 20,
473
- priority: 0,
474
- flags: [],
475
- effects: []
476
- }
477
- ]
478
- };
479
-
480
- const engine = new BattleEngine(cleanser, dummy);
481
- const playerPiclet = engine.getState().playerPiclet;
482
-
483
- // Test the removeStatus effect by directly calling it
484
- // This bypasses any turn/timing issues
485
- const mockEffect = { status: 'poison' };
486
-
487
- // First add poison manually
488
- playerPiclet.statusEffects.push('poison');
489
- expect(playerPiclet.statusEffects.includes('poison')).toBe(true);
490
-
491
- // Call the removeStatus effect processor directly
492
- engine['processRemoveStatusEffect'](mockEffect, playerPiclet);
493
-
494
- // Check if poison was removed
495
- expect(playerPiclet.statusEffects.includes('poison')).toBe(false);
496
- });
497
- });
498
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/move-flags.test.ts DELETED
@@ -1,692 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition, SpecialAbility } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Move Flags and Flag-Based Interactions', () => {
7
- describe('Flag-Based Immunities', () => {
8
- it('should provide immunity to contact moves with Ethereal Form', () => {
9
- const etherealForm: SpecialAbility = {
10
- name: "Ethereal Form",
11
- description: "Ghostly body cannot be touched by physical contact",
12
- effects: [
13
- {
14
- type: 'mechanicOverride',
15
- mechanic: 'flagImmunity',
16
- value: ['contact']
17
- }
18
- ]
19
- };
20
-
21
- const ghostly: PicletDefinition = {
22
- name: "Ghost Fighter",
23
- description: "Ethereal being immune to contact",
24
- tier: 'medium',
25
- primaryType: PicletType.CULTURE,
26
- baseStats: { hp: 70, attack: 80, defense: 50, speed: 90 },
27
- nature: "Timid",
28
- specialAbility: etherealForm,
29
- movepool: [
30
- {
31
- name: "Shadow Ball",
32
- type: AttackType.CULTURE,
33
- power: 80,
34
- accuracy: 100,
35
- pp: 10,
36
- priority: 0,
37
- flags: [],
38
- effects: [
39
- {
40
- type: 'damage',
41
- target: 'opponent',
42
- amount: 'normal'
43
- }
44
- ]
45
- }
46
- ]
47
- };
48
-
49
- const contactUser: PicletDefinition = {
50
- name: "Physical Fighter",
51
- description: "Uses contact moves",
52
- tier: 'medium',
53
- primaryType: PicletType.BEAST,
54
- baseStats: { hp: 80, attack: 90, defense: 70, speed: 60 },
55
- nature: "Adamant",
56
- specialAbility: { name: "No Ability", description: "" },
57
- movepool: [
58
- {
59
- name: "Punch",
60
- type: AttackType.BEAST,
61
- power: 75,
62
- accuracy: 100,
63
- pp: 10,
64
- priority: 0,
65
- flags: ['contact', 'punch'],
66
- effects: [
67
- {
68
- type: 'damage',
69
- target: 'opponent',
70
- amount: 'normal'
71
- }
72
- ]
73
- },
74
- {
75
- name: "Energy Blast",
76
- type: AttackType.SPACE,
77
- power: 75,
78
- accuracy: 100,
79
- pp: 10,
80
- priority: 0,
81
- flags: [], // No contact flag
82
- effects: [
83
- {
84
- type: 'damage',
85
- target: 'opponent',
86
- amount: 'normal'
87
- }
88
- ]
89
- }
90
- ]
91
- };
92
-
93
- const engine = new BattleEngine(ghostly, contactUser);
94
-
95
- // Test contact move immunity
96
- const initialHp = engine.getState().playerPiclet.currentHp;
97
- engine.executeActions(
98
- { type: 'move', piclet: 'player', moveIndex: 0 },
99
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Contact move
100
- );
101
-
102
- const hpAfterContact = engine.getState().playerPiclet.currentHp;
103
- expect(hpAfterContact).toBe(initialHp); // No damage from contact move
104
- expect(engine.getLog().some(msg => msg.includes('had no effect') || msg.includes('immune'))).toBe(true);
105
-
106
- // Test non-contact move still works
107
- engine.executeActions(
108
- { type: 'move', piclet: 'player', moveIndex: 0 },
109
- { type: 'move', piclet: 'opponent', moveIndex: 1 } // Non-contact move
110
- );
111
-
112
- const hpAfterNonContact = engine.getState().playerPiclet.currentHp;
113
- expect(hpAfterNonContact).toBeLessThan(hpAfterContact); // Should take damage
114
- });
115
-
116
- it('should provide immunity to sound moves with Sound Barrier', () => {
117
- const soundBarrier: SpecialAbility = {
118
- name: "Sound Barrier",
119
- description: "Natural sound dampening prevents sound-based moves",
120
- effects: [
121
- {
122
- type: 'mechanicOverride',
123
- mechanic: 'flagImmunity',
124
- value: ['sound']
125
- }
126
- ]
127
- };
128
-
129
- const soundProof: PicletDefinition = {
130
- name: "Silent Fighter",
131
- description: "Cannot be affected by sound attacks",
132
- tier: 'medium',
133
- primaryType: PicletType.MACHINA,
134
- baseStats: { hp: 85, attack: 70, defense: 85, speed: 50 },
135
- nature: "Bold",
136
- specialAbility: soundBarrier,
137
- movepool: [
138
- {
139
- name: "Laser Beam",
140
- type: AttackType.MACHINA,
141
- power: 70,
142
- accuracy: 100,
143
- pp: 10,
144
- priority: 0,
145
- flags: [],
146
- effects: [
147
- {
148
- type: 'damage',
149
- target: 'opponent',
150
- amount: 'normal'
151
- }
152
- ]
153
- }
154
- ]
155
- };
156
-
157
- const soundUser: PicletDefinition = {
158
- name: "Sound Fighter",
159
- description: "Uses sound-based attacks",
160
- tier: 'medium',
161
- primaryType: PicletType.CULTURE,
162
- baseStats: { hp: 75, attack: 80, defense: 60, speed: 85 },
163
- nature: "Modest",
164
- specialAbility: { name: "No Ability", description: "" },
165
- movepool: [
166
- {
167
- name: "Sonic Boom",
168
- type: AttackType.CULTURE,
169
- power: 80,
170
- accuracy: 100,
171
- pp: 10,
172
- priority: 0,
173
- flags: ['sound'],
174
- effects: [
175
- {
176
- type: 'damage',
177
- target: 'opponent',
178
- amount: 'normal'
179
- }
180
- ]
181
- }
182
- ]
183
- };
184
-
185
- const engine = new BattleEngine(soundProof, soundUser);
186
- const initialHp = engine.getState().playerPiclet.currentHp;
187
-
188
- engine.executeActions(
189
- { type: 'move', piclet: 'player', moveIndex: 0 },
190
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
191
- );
192
-
193
- const finalHp = engine.getState().playerPiclet.currentHp;
194
- expect(finalHp).toBe(initialHp);
195
- expect(engine.getLog().some(msg => msg.includes('had no effect') || msg.includes('immune'))).toBe(true);
196
- });
197
-
198
- it('should provide immunity to explosive moves with Soft Body', () => {
199
- const softBody: SpecialAbility = {
200
- name: "Soft Body",
201
- description: "Gelatinous form absorbs explosions but vulnerable to direct hits",
202
- effects: [
203
- {
204
- type: 'mechanicOverride',
205
- mechanic: 'flagImmunity',
206
- value: ['explosive']
207
- },
208
- {
209
- type: 'mechanicOverride',
210
- mechanic: 'flagWeakness',
211
- value: ['punch']
212
- }
213
- ]
214
- };
215
-
216
- const gelatinous: PicletDefinition = {
217
- name: "Gel Fighter",
218
- description: "Soft gelatinous body",
219
- tier: 'medium',
220
- primaryType: PicletType.AQUATIC,
221
- baseStats: { hp: 90, attack: 60, defense: 80, speed: 60 },
222
- nature: "Bold",
223
- specialAbility: softBody,
224
- movepool: [
225
- {
226
- name: "Water Gun",
227
- type: AttackType.AQUATIC,
228
- power: 60,
229
- accuracy: 100,
230
- pp: 10,
231
- priority: 0,
232
- flags: [],
233
- effects: [
234
- {
235
- type: 'damage',
236
- target: 'opponent',
237
- amount: 'normal'
238
- }
239
- ]
240
- }
241
- ]
242
- };
243
-
244
- const explosiveUser: PicletDefinition = {
245
- name: "Bomber",
246
- description: "Uses explosive attacks",
247
- tier: 'medium',
248
- primaryType: PicletType.MACHINA,
249
- baseStats: { hp: 120, attack: 90, defense: 60, speed: 70 },
250
- nature: "Hasty",
251
- specialAbility: { name: "No Ability", description: "" },
252
- movepool: [
253
- {
254
- name: "Explosion",
255
- type: AttackType.MACHINA,
256
- power: 120,
257
- accuracy: 100,
258
- pp: 5,
259
- priority: 0,
260
- flags: ['explosive'],
261
- effects: [
262
- {
263
- type: 'damage',
264
- target: 'opponent',
265
- amount: 'strong'
266
- }
267
- ]
268
- },
269
- {
270
- name: "Mega Punch",
271
- type: AttackType.BEAST,
272
- power: 80,
273
- accuracy: 85,
274
- pp: 10,
275
- priority: 0,
276
- flags: ['contact', 'punch'],
277
- effects: [
278
- {
279
- type: 'damage',
280
- target: 'opponent',
281
- amount: 'normal'
282
- }
283
- ]
284
- }
285
- ]
286
- };
287
-
288
- const engine = new BattleEngine(gelatinous, explosiveUser);
289
- const initialHp = engine.getState().playerPiclet.currentHp;
290
-
291
- // Test explosive immunity
292
- engine.executeActions(
293
- { type: 'move', piclet: 'player', moveIndex: 0 },
294
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Explosive move
295
- );
296
-
297
- const hpAfterExplosive = engine.getState().playerPiclet.currentHp;
298
- expect(hpAfterExplosive).toBe(initialHp);
299
- expect(engine.getLog().some(msg => msg.includes('had no effect') || msg.includes('absorbed'))).toBe(true);
300
-
301
- // Test punch weakness (should take extra damage) - only if battle hasn't ended
302
- if (!engine.isGameOver()) {
303
- engine.executeActions(
304
- { type: 'move', piclet: 'player', moveIndex: 0 },
305
- { type: 'move', piclet: 'opponent', moveIndex: 1 } // Punch move
306
- );
307
-
308
- const hpAfterPunch = engine.getState().playerPiclet.currentHp;
309
- expect(hpAfterPunch).toBeLessThan(hpAfterExplosive);
310
- // Should take more damage than normal due to weakness
311
- }
312
- });
313
- });
314
-
315
- describe('Flag-Based Weaknesses', () => {
316
- it('should take extra damage from specific flagged moves', () => {
317
- const fragileShell: SpecialAbility = {
318
- name: "Fragile Shell",
319
- description: "Hard shell provides defense but shatters from explosions",
320
- effects: [
321
- {
322
- type: 'modifyStats',
323
- target: 'self',
324
- stats: { defense: 'increase' }
325
- },
326
- {
327
- type: 'mechanicOverride',
328
- mechanic: 'flagWeakness',
329
- value: ['explosive']
330
- }
331
- ]
332
- };
333
-
334
- const shelledCreature: PicletDefinition = {
335
- name: "Shell Fighter",
336
- description: "Protected by fragile shell",
337
- tier: 'medium',
338
- primaryType: PicletType.MINERAL,
339
- baseStats: { hp: 80, attack: 60, defense: 90, speed: 50 },
340
- nature: "Impish",
341
- specialAbility: fragileShell,
342
- movepool: [
343
- {
344
- name: "Rock Throw",
345
- type: AttackType.MINERAL,
346
- power: 50,
347
- accuracy: 90,
348
- pp: 10,
349
- priority: 0,
350
- flags: [],
351
- effects: [
352
- {
353
- type: 'damage',
354
- target: 'opponent',
355
- amount: 'normal'
356
- }
357
- ]
358
- }
359
- ]
360
- };
361
-
362
- const explosiveUser: PicletDefinition = {
363
- name: "Bomber",
364
- description: "Uses explosive attacks",
365
- tier: 'medium',
366
- primaryType: PicletType.MACHINA,
367
- baseStats: { hp: 70, attack: 80, defense: 60, speed: 70 },
368
- nature: "Modest",
369
- specialAbility: { name: "No Ability", description: "" },
370
- movepool: [
371
- {
372
- name: "Normal Attack",
373
- type: AttackType.NORMAL,
374
- power: 60,
375
- accuracy: 100,
376
- pp: 10,
377
- priority: 0,
378
- flags: [],
379
- effects: [
380
- {
381
- type: 'damage',
382
- target: 'opponent',
383
- amount: 'normal'
384
- }
385
- ]
386
- },
387
- {
388
- name: "Bomb Blast",
389
- type: AttackType.MACHINA,
390
- power: 60,
391
- accuracy: 100,
392
- pp: 10,
393
- priority: 0,
394
- flags: ['explosive'],
395
- effects: [
396
- {
397
- type: 'damage',
398
- target: 'opponent',
399
- amount: 'normal'
400
- }
401
- ]
402
- }
403
- ]
404
- };
405
-
406
- const engine = new BattleEngine(shelledCreature, explosiveUser);
407
-
408
- // Test normal damage
409
- const initialHp = engine.getState().playerPiclet.currentHp;
410
- engine.executeActions(
411
- { type: 'move', piclet: 'player', moveIndex: 0 },
412
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Normal attack
413
- );
414
-
415
- const hpAfterNormal = engine.getState().playerPiclet.currentHp;
416
- const normalDamage = initialHp - hpAfterNormal;
417
-
418
- // Test explosive weakness (should do more damage) - only if battle hasn't ended
419
- if (!engine.isGameOver()) {
420
- const preExplosiveHp = engine.getState().playerPiclet.currentHp;
421
- engine.executeActions(
422
- { type: 'move', piclet: 'player', moveIndex: 0 },
423
- { type: 'move', piclet: 'opponent', moveIndex: 1 } // Explosive attack
424
- );
425
-
426
- const hpAfterExplosive = engine.getState().playerPiclet.currentHp;
427
- const explosiveDamage = preExplosiveHp - hpAfterExplosive;
428
-
429
- // Explosive should do more damage due to weakness
430
- expect(explosiveDamage).toBeGreaterThan(normalDamage);
431
- expect(engine.getLog().some(msg => msg.includes('It\'s super effective') || msg.includes('weakness'))).toBe(true);
432
- }
433
- });
434
- });
435
-
436
- describe('Flag-Based Resistances', () => {
437
- it('should take reduced damage from specific flagged moves', () => {
438
- const thickHide: SpecialAbility = {
439
- name: "Thick Hide",
440
- description: "Tough skin reduces impact from physical contact",
441
- effects: [
442
- {
443
- type: 'mechanicOverride',
444
- mechanic: 'flagResistance',
445
- value: ['contact']
446
- }
447
- ]
448
- };
449
-
450
- const toughCreature: PicletDefinition = {
451
- name: "Tough Fighter",
452
- description: "Has thick, resistant hide",
453
- tier: 'medium',
454
- primaryType: PicletType.BEAST,
455
- baseStats: { hp: 150, attack: 70, defense: 90, speed: 40 },
456
- nature: "Impish",
457
- specialAbility: thickHide,
458
- movepool: [
459
- {
460
- name: "Bite",
461
- type: AttackType.BEAST,
462
- power: 60,
463
- accuracy: 100,
464
- pp: 10,
465
- priority: 0,
466
- flags: ['contact', 'bite'],
467
- effects: [
468
- {
469
- type: 'damage',
470
- target: 'opponent',
471
- amount: 'normal'
472
- }
473
- ]
474
- }
475
- ]
476
- };
477
-
478
- const attacker: PicletDefinition = {
479
- name: "Mixed Attacker",
480
- description: "Uses various attack types",
481
- tier: 'medium',
482
- primaryType: PicletType.BEAST,
483
- baseStats: { hp: 120, attack: 85, defense: 60, speed: 70 },
484
- nature: "Adamant",
485
- specialAbility: { name: "No Ability", description: "" },
486
- movepool: [
487
- {
488
- name: "Scratch",
489
- type: AttackType.BEAST,
490
- power: 60,
491
- accuracy: 100,
492
- pp: 10,
493
- priority: 0,
494
- flags: ['contact'],
495
- effects: [
496
- {
497
- type: 'damage',
498
- target: 'opponent',
499
- amount: 'normal'
500
- }
501
- ]
502
- },
503
- {
504
- name: "Energy Beam",
505
- type: AttackType.SPACE,
506
- power: 60,
507
- accuracy: 100,
508
- pp: 10,
509
- priority: 0,
510
- flags: [], // No contact
511
- effects: [
512
- {
513
- type: 'damage',
514
- target: 'opponent',
515
- amount: 'normal'
516
- }
517
- ]
518
- }
519
- ]
520
- };
521
-
522
- const engine = new BattleEngine(toughCreature, attacker);
523
-
524
- // Test contact move (should be resisted)
525
- const initialHp = engine.getState().playerPiclet.currentHp;
526
- engine.executeActions(
527
- { type: 'move', piclet: 'player', moveIndex: 0 },
528
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Contact move
529
- );
530
-
531
- const hpAfterContact = engine.getState().playerPiclet.currentHp;
532
- const contactDamage = initialHp - hpAfterContact;
533
-
534
- // For now, just verify that resistance is working through the log message
535
- // The actual damage comparison requires battle to continue, which depends on HP balance
536
- expect(engine.getLog().some(msg => msg.includes('not very effective'))).toBe(true);
537
-
538
- // Verify that some damage was actually reduced (contact damage should be less than normal)
539
- // This is a basic sanity check - contact damage with resistance should be reasonable
540
- expect(contactDamage).toBeGreaterThan(0);
541
- expect(contactDamage).toBeLessThan(60); // Should be less than normal damage due to resistance
542
- });
543
- });
544
-
545
- describe('Multi-Flag Interactions', () => {
546
- it('should handle creatures with multiple flag interactions', () => {
547
- const liquidBody: SpecialAbility = {
548
- name: "Liquid Body",
549
- description: "Fluid form flows around physical attacks but resonates with sound",
550
- effects: [
551
- {
552
- type: 'mechanicOverride',
553
- mechanic: 'flagImmunity',
554
- value: ['punch', 'bite']
555
- },
556
- {
557
- type: 'mechanicOverride',
558
- mechanic: 'flagWeakness',
559
- value: ['sound']
560
- }
561
- ]
562
- };
563
-
564
- const liquidCreature: PicletDefinition = {
565
- name: "Liquid Fighter",
566
- description: "Made of flowing liquid",
567
- tier: 'medium',
568
- primaryType: PicletType.AQUATIC,
569
- baseStats: { hp: 85, attack: 70, defense: 60, speed: 75 },
570
- nature: "Calm",
571
- specialAbility: liquidBody,
572
- movepool: [
573
- {
574
- name: "Water Pulse",
575
- type: AttackType.AQUATIC,
576
- power: 60,
577
- accuracy: 100,
578
- pp: 10,
579
- priority: 0,
580
- flags: [],
581
- effects: [
582
- {
583
- type: 'damage',
584
- target: 'opponent',
585
- amount: 'normal'
586
- }
587
- ]
588
- }
589
- ]
590
- };
591
-
592
- const multiAttacker: PicletDefinition = {
593
- name: "Multi Attacker",
594
- description: "Uses different types of attacks",
595
- tier: 'medium',
596
- primaryType: PicletType.BEAST,
597
- baseStats: { hp: 200, attack: 80, defense: 65, speed: 70 },
598
- nature: "Hardy",
599
- specialAbility: { name: "No Ability", description: "" },
600
- movepool: [
601
- {
602
- name: "Punch",
603
- type: AttackType.BEAST,
604
- power: 70,
605
- accuracy: 100,
606
- pp: 10,
607
- priority: 0,
608
- flags: ['contact', 'punch'],
609
- effects: [
610
- {
611
- type: 'damage',
612
- target: 'opponent',
613
- amount: 'normal'
614
- }
615
- ]
616
- },
617
- {
618
- name: "Bite",
619
- type: AttackType.BEAST,
620
- power: 70,
621
- accuracy: 100,
622
- pp: 10,
623
- priority: 0,
624
- flags: ['contact', 'bite'],
625
- effects: [
626
- {
627
- type: 'damage',
628
- target: 'opponent',
629
- amount: 'normal'
630
- }
631
- ]
632
- },
633
- {
634
- name: "Sonic Roar",
635
- type: AttackType.CULTURE,
636
- power: 70,
637
- accuracy: 100,
638
- pp: 10,
639
- priority: 0,
640
- flags: ['sound'],
641
- effects: [
642
- {
643
- type: 'damage',
644
- target: 'opponent',
645
- amount: 'normal'
646
- }
647
- ]
648
- }
649
- ]
650
- };
651
-
652
- const engine = new BattleEngine(liquidCreature, multiAttacker);
653
- const initialHp = engine.getState().playerPiclet.currentHp;
654
-
655
- // Test punch immunity
656
- engine.executeActions(
657
- { type: 'move', piclet: 'player', moveIndex: 0 },
658
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Punch (should be immune)
659
- );
660
-
661
- const hpAfterPunch = engine.getState().playerPiclet.currentHp;
662
- expect(hpAfterPunch).toBe(initialHp);
663
-
664
- // Test bite immunity - only if battle hasn't ended
665
- let hpAfterBite = hpAfterPunch;
666
- if (!engine.isGameOver()) {
667
- engine.executeActions(
668
- { type: 'move', piclet: 'player', moveIndex: 0 },
669
- { type: 'move', piclet: 'opponent', moveIndex: 1 } // Bite (should be immune)
670
- );
671
-
672
- hpAfterBite = engine.getState().playerPiclet.currentHp;
673
- expect(hpAfterBite).toBe(hpAfterPunch);
674
- }
675
-
676
- // Test sound weakness - only if battle hasn't ended
677
- if (!engine.isGameOver()) {
678
- engine.executeActions(
679
- { type: 'move', piclet: 'player', moveIndex: 0 },
680
- { type: 'move', piclet: 'opponent', moveIndex: 2 } // Sound (should be weak)
681
- );
682
-
683
- const hpAfterSound = engine.getState().playerPiclet.currentHp;
684
- expect(hpAfterSound).toBeLessThan(hpAfterBite);
685
- }
686
-
687
- const log = engine.getLog();
688
- expect(log.some(msg => msg.includes('had no effect'))).toBe(true);
689
- expect(log.some(msg => msg.includes('super effective') || msg.includes('weakness'))).toBe(true);
690
- });
691
- });
692
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/multi-piclet-types.ts DELETED
@@ -1,160 +0,0 @@
1
- /**
2
- * Extended types for multi-Piclet battle system
3
- * Supports up to 4 Piclets on the field at once
4
- */
5
-
6
- import {
7
- BattlePiclet,
8
- PicletDefinition,
9
- BattleEffect,
10
- MoveFlag,
11
- EffectTarget,
12
- EffectCondition
13
- } from './types';
14
-
15
- // Field position identifier
16
- export type FieldPosition = 0 | 1 | 2 | 3;
17
-
18
- // Side identifier
19
- export type BattleSide = 'player' | 'opponent';
20
-
21
- // Extended battle state for multi-Piclet battles
22
- export interface MultiBattleState {
23
- turn: number;
24
- phase: 'selection' | 'execution' | 'ended';
25
-
26
- // Active Piclets on the field (up to 4 total, up to 2 per side)
27
- activePiclets: {
28
- player: Array<BattlePiclet | null>; // [position0, position1] - nulls for empty slots
29
- opponent: Array<BattlePiclet | null>; // [position0, position1] - nulls for empty slots
30
- };
31
-
32
- // Full party rosters (inactive Piclets)
33
- parties: {
34
- player: PicletDefinition[];
35
- opponent: PicletDefinition[];
36
- };
37
-
38
- // Field effects
39
- fieldEffects: Array<{
40
- name: string;
41
- duration: number;
42
- effect: any;
43
- side?: BattleSide; // undefined = global, defined = side-specific
44
- }>;
45
-
46
- // Battle log
47
- log: string[];
48
-
49
- // Winner determination
50
- winner?: 'player' | 'opponent' | 'draw';
51
- }
52
-
53
- // Action targeting for multi-Piclet battles
54
- export interface MultiMoveAction {
55
- type: 'move';
56
- side: BattleSide;
57
- position: FieldPosition; // Which active Piclet is acting
58
- moveIndex: number;
59
- targets?: TargetSelection; // Optional specific targeting
60
- }
61
-
62
- export interface MultiSwitchAction {
63
- type: 'switch';
64
- side: BattleSide;
65
- position: FieldPosition; // Which active slot to switch into
66
- partyIndex: number; // Which party member to switch in
67
- }
68
-
69
- export type MultiBattleAction = MultiMoveAction | MultiSwitchAction;
70
-
71
- // Target selection for moves in multi-Piclet battles
72
- export interface TargetSelection {
73
- primary?: PicletTarget; // Main target
74
- secondary?: PicletTarget[]; // Additional targets for multi-target moves
75
- }
76
-
77
- export interface PicletTarget {
78
- side: BattleSide;
79
- position: FieldPosition;
80
- }
81
-
82
- // Extended effect target types for multi-Piclet battles
83
- export type MultiEffectTarget =
84
- | 'self'
85
- | 'opponent' // Any opponent (AI chooses)
86
- | 'allOpponents' // All active opponents
87
- | 'ally' // Any ally (AI chooses)
88
- | 'allAllies' // All active allies
89
- | 'all' // All active Piclets
90
- | 'field' // Battlefield itself
91
- | 'playerSide' // Player's side of field
92
- | 'opponentSide' // Opponent's side of field
93
- | 'random' // Random active Piclet
94
- | 'weakest' // Weakest active Piclet (by current HP)
95
- | 'strongest'; // Strongest active Piclet (by current HP)
96
-
97
- // Battle configuration for multi-Piclet setup
98
- export interface MultiBattleConfig {
99
- playerParty: PicletDefinition[];
100
- opponentParty: PicletDefinition[];
101
- playerActiveCount: 1 | 2; // How many Piclets player starts with
102
- opponentActiveCount: 1 | 2; // How many Piclets opponent starts with
103
- battleType: 'single' | 'double' | 'triple' | 'quadruple';
104
- }
105
-
106
- // Extended battle effect for multi-Piclet targeting
107
- export interface MultiBattleEffect extends Omit<BattleEffect, 'target'> {
108
- target: MultiEffectTarget;
109
- specificTargets?: PicletTarget[]; // Override auto-targeting
110
- }
111
-
112
- // Turn actions for all active Piclets
113
- export interface TurnActions {
114
- player: MultiBattleAction[];
115
- opponent: MultiBattleAction[];
116
- }
117
-
118
- // Battle position info
119
- export interface PositionInfo {
120
- side: BattleSide;
121
- position: FieldPosition;
122
- piclet: BattlePiclet | null;
123
- isActive: boolean;
124
- }
125
-
126
- // Victory conditions for multi-Piclet battles
127
- export interface VictoryCondition {
128
- type: 'allFainted' | 'majorityFainted' | 'leaderFainted' | 'custom';
129
- customCheck?: (state: MultiBattleState) => BattleSide | 'draw' | null;
130
- }
131
-
132
- // Switch-in/out events for ability triggers
133
- export interface SwitchEvent {
134
- type: 'switchIn' | 'switchOut';
135
- piclet: BattlePiclet;
136
- side: BattleSide;
137
- position: FieldPosition;
138
- previousPiclet?: BattlePiclet; // For switch-ins
139
- }
140
-
141
- // Multi-target move configuration
142
- export interface MultiTargetConfig {
143
- maxTargets: number;
144
- canTargetAllies: boolean;
145
- canTargetSelf: boolean;
146
- mustTargetOpponents: boolean;
147
- targetSelection: 'player' | 'auto' | 'random';
148
- }
149
-
150
- // Priority calculation for multi-Piclet turns
151
- export interface ActionPriority {
152
- action: MultiBattleAction;
153
- side: BattleSide;
154
- position: FieldPosition;
155
- priority: number;
156
- speed: number;
157
- randomTiebreaker: number;
158
- }
159
-
160
- export default MultiBattleState;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/package.json DELETED
@@ -1,30 +0,0 @@
1
- {
2
- "name": "@pictuary/battle-engine",
3
- "version": "0.1.0",
4
- "description": "Standalone battle engine for Pictuary",
5
- "type": "module",
6
- "main": "./BattleEngine.js",
7
- "exports": {
8
- ".": {
9
- "import": "./BattleEngine.js",
10
- "types": "./types.js"
11
- },
12
- "./types": {
13
- "import": "./types.js",
14
- "types": "./types.js"
15
- },
16
- "./test-data": {
17
- "import": "./test-data.js",
18
- "types": "./test-data.js"
19
- }
20
- },
21
- "scripts": {
22
- "test": "vitest",
23
- "test:ui": "vitest --ui",
24
- "test:run": "vitest run"
25
- },
26
- "devDependencies": {
27
- "vitest": "^1.0.0",
28
- "@vitest/ui": "^1.0.0"
29
- }
30
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/remaining-triggers.test.ts DELETED
@@ -1,363 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Remaining Trigger Events', () => {
7
- let basicPiclet: PicletDefinition;
8
- let triggerPiclet: PicletDefinition;
9
-
10
- beforeEach(() => {
11
- basicPiclet = {
12
- name: "Basic Fighter",
13
- description: "Standard test piclet",
14
- tier: 'medium',
15
- primaryType: PicletType.BEAST,
16
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 60 },
17
- nature: "Hardy",
18
- specialAbility: { name: "No Ability", description: "" },
19
- movepool: [
20
- {
21
- name: "Basic Attack",
22
- type: AttackType.BEAST,
23
- power: 50,
24
- accuracy: 100,
25
- pp: 20,
26
- priority: 0,
27
- flags: ['contact'],
28
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
29
- }
30
- ]
31
- };
32
-
33
- triggerPiclet = {
34
- name: "Trigger Test",
35
- description: "Tests all trigger events",
36
- tier: 'medium',
37
- primaryType: PicletType.CULTURE,
38
- baseStats: { hp: 100, attack: 70, defense: 70, speed: 50 },
39
- nature: "Bold",
40
- specialAbility: {
41
- name: "Multi Trigger",
42
- description: "Triggers on various events",
43
- triggers: []
44
- },
45
- movepool: [
46
- {
47
- name: "Drain Punch",
48
- type: AttackType.BEAST,
49
- power: 60,
50
- accuracy: 100,
51
- pp: 15,
52
- priority: 0,
53
- flags: ['contact'],
54
- effects: [
55
- { type: 'damage', target: 'opponent', amount: 'normal', formula: 'drain', value: 0.5 }
56
- ]
57
- },
58
- {
59
- name: "Status Move",
60
- type: AttackType.CULTURE,
61
- power: 0,
62
- accuracy: 100,
63
- pp: 20,
64
- priority: 0,
65
- flags: [],
66
- effects: [
67
- { type: 'applyStatus', target: 'opponent', status: 'paralyze', chance: 100 }
68
- ]
69
- },
70
- {
71
- name: "Stat Boost",
72
- type: AttackType.CULTURE,
73
- power: 0,
74
- accuracy: 100,
75
- pp: 20,
76
- priority: 0,
77
- flags: [],
78
- effects: [
79
- { type: 'modifyStats', target: 'self', stats: { attack: 'increase' } }
80
- ]
81
- },
82
- {
83
- name: "Heal Move",
84
- type: AttackType.CULTURE,
85
- power: 0,
86
- accuracy: 100,
87
- pp: 15,
88
- priority: 0,
89
- flags: [],
90
- effects: [
91
- { type: 'heal', target: 'self', amount: 'large' }
92
- ]
93
- }
94
- ]
95
- };
96
- });
97
-
98
- describe('onStatusInflicted Trigger', () => {
99
- it('should trigger when status effect is applied', () => {
100
- const statusTriggerPiclet = {
101
- ...triggerPiclet,
102
- specialAbility: {
103
- name: "Status Aware",
104
- description: "Triggers when status is inflicted",
105
- triggers: [
106
- {
107
- event: 'onStatusInflicted',
108
- condition: 'always',
109
- effects: [
110
- {
111
- type: 'modifyStats',
112
- target: 'self',
113
- stats: { attack: 'increase' }
114
- }
115
- ]
116
- }
117
- ]
118
- }
119
- };
120
-
121
- const engine = new BattleEngine(basicPiclet, statusTriggerPiclet);
122
- const initialAttack = engine.getState().opponentPiclet.attack;
123
-
124
- // Use status move to trigger the ability
125
- engine.executeActions(
126
- { type: 'move', piclet: 'player', moveIndex: 0 },
127
- { type: 'move', piclet: 'opponent', moveIndex: 1 } // Status Move
128
- );
129
-
130
- const finalAttack = engine.getState().opponentPiclet.attack;
131
- const log = engine.getLog();
132
-
133
- console.log('Status inflicted test log:', log);
134
- console.log('Initial attack:', initialAttack, 'Final attack:', finalAttack);
135
-
136
- expect(finalAttack).toBeGreaterThan(initialAttack);
137
- expect(log.some(msg => msg.includes('Status Aware') && msg.includes('triggered'))).toBe(true);
138
- });
139
- });
140
-
141
- describe('onHPDrained Trigger', () => {
142
- it('should trigger when HP is drained from opponent', () => {
143
- const drainTriggerPiclet = {
144
- ...triggerPiclet,
145
- specialAbility: {
146
- name: "Life Stealer",
147
- description: "Triggers when draining HP",
148
- triggers: [
149
- {
150
- event: 'onHPDrained',
151
- condition: 'always',
152
- effects: [
153
- {
154
- type: 'modifyStats',
155
- target: 'self',
156
- stats: { speed: 'increase' }
157
- }
158
- ]
159
- }
160
- ]
161
- }
162
- };
163
-
164
- const engine = new BattleEngine(basicPiclet, drainTriggerPiclet);
165
- const initialSpeed = engine.getState().opponentPiclet.speed;
166
-
167
- // Use drain move to trigger the ability
168
- engine.executeActions(
169
- { type: 'move', piclet: 'player', moveIndex: 0 },
170
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Drain Punch
171
- );
172
-
173
- const finalSpeed = engine.getState().opponentPiclet.speed;
174
- const log = engine.getLog();
175
-
176
- expect(finalSpeed).toBeGreaterThan(initialSpeed);
177
- expect(log.some(msg => msg.includes('Life Stealer') && msg.includes('triggered'))).toBe(true);
178
- });
179
- });
180
-
181
- describe('beforeMoveUse/afterMoveUse Triggers', () => {
182
- it('should trigger before and after move use', () => {
183
- const moveTriggerPiclet = {
184
- ...triggerPiclet,
185
- specialAbility: {
186
- name: "Move Monitor",
187
- description: "Triggers before and after moves",
188
- triggers: [
189
- {
190
- event: 'beforeMoveUse',
191
- condition: 'always',
192
- effects: [
193
- {
194
- type: 'modifyStats',
195
- target: 'self',
196
- stats: { defense: 'increase' }
197
- }
198
- ]
199
- },
200
- {
201
- event: 'afterMoveUse',
202
- condition: 'always',
203
- effects: [
204
- {
205
- type: 'modifyStats',
206
- target: 'self',
207
- stats: { accuracy: 'increase' }
208
- }
209
- ]
210
- }
211
- ]
212
- }
213
- };
214
-
215
- const engine = new BattleEngine(basicPiclet, moveTriggerPiclet);
216
- const initialDefense = engine.getState().opponentPiclet.defense;
217
- const initialAccuracy = engine.getState().opponentPiclet.accuracy;
218
-
219
- engine.executeActions(
220
- { type: 'move', piclet: 'player', moveIndex: 0 },
221
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
222
- );
223
-
224
- const finalDefense = engine.getState().opponentPiclet.defense;
225
- const finalAccuracy = engine.getState().opponentPiclet.accuracy;
226
- const log = engine.getLog();
227
-
228
- expect(finalDefense).toBeGreaterThan(initialDefense);
229
- expect(finalAccuracy).toBeGreaterThan(initialAccuracy);
230
- expect(log.some(msg => msg.includes('Move Monitor') && msg.includes('triggered'))).toBe(true);
231
- });
232
- });
233
-
234
- describe('onFullHP Trigger', () => {
235
- it('should trigger when HP reaches maximum', () => {
236
- const fullHpTriggerPiclet = {
237
- ...triggerPiclet,
238
- specialAbility: {
239
- name: "Full Power",
240
- description: "Triggers when at full HP",
241
- triggers: [
242
- {
243
- event: 'onFullHP',
244
- condition: 'always',
245
- effects: [
246
- {
247
- type: 'modifyStats',
248
- target: 'self',
249
- stats: { attack: 'greatly_increase' }
250
- }
251
- ]
252
- }
253
- ]
254
- }
255
- };
256
-
257
- const engine = new BattleEngine(basicPiclet, fullHpTriggerPiclet);
258
-
259
- // Damage the piclet first
260
- engine['state'].opponentPiclet.currentHp = Math.floor(engine['state'].opponentPiclet.maxHp * 0.5);
261
- const initialAttack = engine.getState().opponentPiclet.attack;
262
-
263
- // Use heal move to reach full HP
264
- engine.executeActions(
265
- { type: 'move', piclet: 'player', moveIndex: 0 },
266
- { type: 'move', piclet: 'opponent', moveIndex: 3 } // Heal Move
267
- );
268
-
269
- const finalAttack = engine.getState().opponentPiclet.attack;
270
- const log = engine.getLog();
271
-
272
- // Should trigger if healed to full HP
273
- if (engine.getState().opponentPiclet.currentHp === engine.getState().opponentPiclet.maxHp) {
274
- expect(finalAttack).toBeGreaterThan(initialAttack);
275
- expect(log.some(msg => msg.includes('Full Power') && msg.includes('triggered'))).toBe(true);
276
- }
277
- });
278
- });
279
-
280
- describe('onOpponentContactMove Trigger', () => {
281
- it('should trigger when opponent uses contact move', () => {
282
- const contactTriggerPiclet = {
283
- ...triggerPiclet,
284
- specialAbility: {
285
- name: "Rough Skin",
286
- description: "Triggers when hit by contact moves",
287
- triggers: [
288
- {
289
- event: 'onOpponentContactMove',
290
- condition: 'always',
291
- effects: [
292
- {
293
- type: 'damage',
294
- target: 'opponent',
295
- amount: 'weak'
296
- }
297
- ]
298
- }
299
- ]
300
- }
301
- };
302
-
303
- const engine = new BattleEngine(basicPiclet, contactTriggerPiclet);
304
- const initialPlayerHp = engine.getState().playerPiclet.currentHp;
305
-
306
- // Player uses contact move
307
- engine.executeActions(
308
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Basic Attack (contact)
309
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
310
- );
311
-
312
- const finalPlayerHp = engine.getState().playerPiclet.currentHp;
313
- const log = engine.getLog();
314
-
315
- // Player should take damage from contact
316
- expect(finalPlayerHp).toBeLessThan(initialPlayerHp);
317
- expect(log.some(msg => msg.includes('Rough Skin') && msg.includes('triggered'))).toBe(true);
318
- });
319
- });
320
-
321
- describe('onStatChange Trigger', () => {
322
- it('should trigger when stats are modified', () => {
323
- const statTriggerPiclet = {
324
- ...triggerPiclet,
325
- specialAbility: {
326
- name: "Stat Monitor",
327
- description: "Triggers when stats change",
328
- triggers: [
329
- {
330
- event: 'onStatChange',
331
- condition: 'always',
332
- effects: [
333
- {
334
- type: 'heal',
335
- target: 'self',
336
- amount: 'small'
337
- }
338
- ]
339
- }
340
- ]
341
- }
342
- };
343
-
344
- const engine = new BattleEngine(basicPiclet, statTriggerPiclet);
345
-
346
- // Damage the piclet first so healing is visible
347
- engine['state'].opponentPiclet.currentHp = Math.floor(engine['state'].opponentPiclet.maxHp * 0.8);
348
- const initialHp = engine.getState().opponentPiclet.currentHp;
349
-
350
- // Use stat boost move to trigger the ability
351
- engine.executeActions(
352
- { type: 'move', piclet: 'player', moveIndex: 0 },
353
- { type: 'move', piclet: 'opponent', moveIndex: 2 } // Stat Boost
354
- );
355
-
356
- const finalHp = engine.getState().opponentPiclet.currentHp;
357
- const log = engine.getLog();
358
-
359
- expect(finalHp).toBeGreaterThan(initialHp);
360
- expect(log.some(msg => msg.includes('Stat Monitor') && msg.includes('triggered'))).toBe(true);
361
- });
362
- });
363
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/switching-system.test.ts DELETED
@@ -1,445 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { BattleEngine } from './BattleEngine';
3
- import type { PicletDefinition } from './types';
4
- import { PicletType, AttackType } from './types';
5
-
6
- describe('Switching System', () => {
7
- let basicPiclet: PicletDefinition;
8
- let reservePiclet: PicletDefinition;
9
- let hazardSetter: PicletDefinition;
10
- let switchTriggerPiclet: PicletDefinition;
11
-
12
- beforeEach(() => {
13
- // Basic piclet for primary battles
14
- basicPiclet = {
15
- name: "Basic Fighter",
16
- description: "Standard test piclet",
17
- tier: 'medium',
18
- primaryType: PicletType.BEAST,
19
- baseStats: { hp: 80, attack: 60, defense: 60, speed: 60 },
20
- nature: "Hardy",
21
- specialAbility: { name: "No Ability", description: "" },
22
- movepool: [
23
- {
24
- name: "Basic Attack",
25
- type: AttackType.BEAST,
26
- power: 50,
27
- accuracy: 100,
28
- pp: 20,
29
- priority: 0,
30
- flags: ['contact'],
31
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
32
- }
33
- ]
34
- };
35
-
36
- // Reserve piclet for switching
37
- reservePiclet = {
38
- name: "Reserve Fighter",
39
- description: "Backup piclet",
40
- tier: 'medium',
41
- primaryType: PicletType.FLORA,
42
- baseStats: { hp: 90, attack: 50, defense: 70, speed: 40 },
43
- nature: "Calm",
44
- specialAbility: { name: "No Ability", description: "" },
45
- movepool: [
46
- {
47
- name: "Leaf Strike",
48
- type: AttackType.FLORA,
49
- power: 45,
50
- accuracy: 100,
51
- pp: 25,
52
- priority: 0,
53
- flags: [],
54
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
55
- }
56
- ]
57
- };
58
-
59
- // Piclet that can set entry hazards
60
- hazardSetter = {
61
- name: "Hazard Master",
62
- description: "Sets entry hazards",
63
- tier: 'medium',
64
- primaryType: PicletType.MINERAL,
65
- baseStats: { hp: 70, attack: 40, defense: 80, speed: 50 },
66
- nature: "Bold",
67
- specialAbility: { name: "No Ability", description: "" },
68
- movepool: [
69
- {
70
- name: "Spike Trap",
71
- type: AttackType.MINERAL,
72
- power: 0,
73
- accuracy: 100,
74
- pp: 20,
75
- priority: 0,
76
- flags: [],
77
- effects: [
78
- {
79
- type: 'fieldEffect',
80
- effect: 'spikes',
81
- target: 'opponentSide',
82
- stackable: true
83
- }
84
- ]
85
- },
86
- {
87
- name: "Toxic Spikes",
88
- type: AttackType.MINERAL,
89
- power: 0,
90
- accuracy: 100,
91
- pp: 20,
92
- priority: 0,
93
- flags: [],
94
- effects: [
95
- {
96
- type: 'fieldEffect',
97
- effect: 'toxicSpikes',
98
- target: 'opponentSide',
99
- stackable: true
100
- }
101
- ]
102
- }
103
- ]
104
- };
105
-
106
- // Piclet with switch-triggered abilities
107
- switchTriggerPiclet = {
108
- name: "Switch Specialist",
109
- description: "Has switch-in/out abilities",
110
- tier: 'medium',
111
- primaryType: PicletType.CULTURE,
112
- baseStats: { hp: 85, attack: 55, defense: 65, speed: 75 },
113
- nature: "Timid",
114
- specialAbility: {
115
- name: "Intimidate",
116
- description: "Lowers opponent's attack on switch-in",
117
- triggers: [
118
- {
119
- event: 'onSwitchIn',
120
- condition: 'always',
121
- effects: [
122
- {
123
- type: 'modifyStats',
124
- target: 'opponent',
125
- stats: {
126
- attack: 'decrease'
127
- }
128
- }
129
- ]
130
- }
131
- ]
132
- },
133
- movepool: [
134
- {
135
- name: "Quick Strike",
136
- type: AttackType.CULTURE,
137
- power: 40,
138
- accuracy: 100,
139
- pp: 30,
140
- priority: 1,
141
- flags: [],
142
- effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
143
- }
144
- ]
145
- };
146
- });
147
-
148
- describe('Basic Switch Actions', () => {
149
- it('should allow switching to a different piclet', () => {
150
- // Create engine with rosters
151
- const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
152
-
153
- const initialPlayerName = engine.getState().playerPiclet.definition.name;
154
- expect(initialPlayerName).toBe("Basic Fighter");
155
-
156
- // Execute switch action
157
- engine.executeActions(
158
- { type: 'switch', piclet: 'player', newPicletIndex: 1 },
159
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
160
- );
161
-
162
- const finalPlayerName = engine.getState().playerPiclet.definition.name;
163
- const log = engine.getLog();
164
-
165
- expect(finalPlayerName).toBe("Reserve Fighter");
166
- expect(log.some(msg => msg.includes('switched') && msg.includes('Reserve Fighter'))).toBe(true);
167
- });
168
-
169
- it('should handle switch action priority correctly', () => {
170
- const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
171
-
172
- // Switch actions should have higher priority than moves
173
- engine.executeActions(
174
- { type: 'switch', piclet: 'player', newPicletIndex: 1 },
175
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
176
- );
177
-
178
- const log = engine.getLog();
179
-
180
- // Switch should happen before the opponent's move
181
- const switchIndex = log.findIndex(msg => msg.includes('switched'));
182
- const moveIndex = log.findIndex(msg => msg.includes('used') && msg.includes('Basic Attack'));
183
-
184
- expect(switchIndex).toBeLessThan(moveIndex);
185
- });
186
-
187
- it('should not allow switching to same piclet', () => {
188
- const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
189
-
190
- // Try to switch to the same piclet (index 0)
191
- engine.executeActions(
192
- { type: 'switch', piclet: 'player', newPicletIndex: 0 },
193
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
194
- );
195
-
196
- const log = engine.getLog();
197
- expect(log.some(msg => msg.includes('already active') || msg.includes('cannot switch'))).toBe(true);
198
- });
199
-
200
- it('should not allow switching to fainted piclet', () => {
201
- const faintedPiclet = { ...reservePiclet };
202
- const engine = new BattleEngine([basicPiclet, faintedPiclet], [basicPiclet]);
203
-
204
- // Mock fainted piclet by accessing private roster states
205
- (engine as any).playerRosterStates[1].fainted = true;
206
- (engine as any).playerRosterStates[1].currentHp = 0;
207
-
208
- engine.executeActions(
209
- { type: 'switch', piclet: 'player', newPicletIndex: 1 },
210
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
211
- );
212
-
213
- const log = engine.getLog();
214
- expect(log.some(msg => msg.includes('fainted') || msg.includes('unable to battle'))).toBe(true);
215
- });
216
- });
217
-
218
- describe('Entry Hazards', () => {
219
- it('should apply spikes damage on switch-in', () => {
220
- const engine = new BattleEngine([hazardSetter, basicPiclet], [basicPiclet, reservePiclet]);
221
-
222
- // Set up spikes
223
- engine.executeActions(
224
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Spike Trap
225
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
226
- );
227
-
228
- const log = engine.getLog();
229
- expect(log.some(msg => msg.includes('spikes') || msg.includes('hazard'))).toBe(true);
230
-
231
- // Switch opponent to trigger spikes
232
- const initialHp = engine.getState().opponentPiclet.currentHp;
233
-
234
- engine.executeActions(
235
- { type: 'move', piclet: 'player', moveIndex: 0 },
236
- { type: 'switch', piclet: 'opponent', newPicletIndex: 1 }
237
- );
238
-
239
- const finalLog = engine.getLog();
240
- expect(finalLog.some(msg => msg.includes('hurt by spikes') || msg.includes('stepped on spikes'))).toBe(true);
241
- });
242
-
243
- it('should apply toxic spikes status on switch-in', () => {
244
- const engine = new BattleEngine([hazardSetter], [basicPiclet, reservePiclet]);
245
-
246
- // Set up toxic spikes
247
- engine.executeActions(
248
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Toxic Spikes
249
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
250
- );
251
-
252
- // Switch opponent to trigger toxic spikes
253
- engine.executeActions(
254
- { type: 'move', piclet: 'player', moveIndex: 0 },
255
- { type: 'switch', piclet: 'opponent', newPicletIndex: 1 }
256
- );
257
-
258
- const finalState = engine.getState().opponentPiclet;
259
- const log = engine.getLog();
260
-
261
- expect(finalState.statusEffects).toContain('poison');
262
- expect(log.some(msg => msg.includes('poisoned by toxic spikes'))).toBe(true);
263
- });
264
-
265
- it('should stack multiple layers of spikes', () => {
266
- const engine = new BattleEngine([hazardSetter], [basicPiclet, reservePiclet]);
267
-
268
- // Set up multiple spike layers
269
- engine.executeActions(
270
- { type: 'move', piclet: 'player', moveIndex: 0 }, // First Spike Trap
271
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
272
- );
273
-
274
- engine.executeActions(
275
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Second Spike Trap
276
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
277
- );
278
-
279
- const fieldEffects = engine.getState().fieldEffects;
280
- const spikeCount = fieldEffects.filter(effect => effect.name === 'entryHazardSpikes').length;
281
-
282
- expect(spikeCount).toBeGreaterThan(1);
283
- });
284
- });
285
-
286
- describe('Switch-In/Out Ability Triggers', () => {
287
- it('should trigger onSwitchIn ability when piclet enters battle', () => {
288
- const engine = new BattleEngine([basicPiclet, switchTriggerPiclet], [basicPiclet]);
289
-
290
- const initialOpponentAttack = engine.getState().opponentPiclet.attack;
291
-
292
- // Switch in the intimidate piclet
293
- engine.executeActions(
294
- { type: 'switch', piclet: 'player', newPicletIndex: 1 },
295
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
296
- );
297
-
298
- const finalOpponentAttack = engine.getState().opponentPiclet.attack;
299
- const log = engine.getLog();
300
-
301
- expect(finalOpponentAttack).toBeLessThan(initialOpponentAttack);
302
- expect(log.some(msg => msg.includes('Intimidate') && msg.includes('triggered'))).toBe(true);
303
- });
304
-
305
- it('should trigger onSwitchOut ability when piclet leaves battle', () => {
306
- const switchOutPiclet: PicletDefinition = {
307
- ...switchTriggerPiclet,
308
- specialAbility: {
309
- name: "Parting Shot",
310
- description: "Lowers opponent's stats on switch-out",
311
- triggers: [
312
- {
313
- event: 'onSwitchOut',
314
- condition: 'always',
315
- effects: [
316
- {
317
- type: 'modifyStats',
318
- target: 'opponent',
319
- stats: {
320
- attack: 'decrease',
321
- defense: 'decrease'
322
- }
323
- }
324
- ]
325
- }
326
- ]
327
- }
328
- };
329
-
330
- const engine = new BattleEngine([switchOutPiclet, reservePiclet], [basicPiclet]);
331
-
332
- const initialOpponentAttack = engine.getState().opponentPiclet.attack;
333
- const initialOpponentDefense = engine.getState().opponentPiclet.defense;
334
-
335
- // Switch out the parting shot piclet
336
- engine.executeActions(
337
- { type: 'switch', piclet: 'player', newPicletIndex: 1 },
338
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
339
- );
340
-
341
- const finalOpponentAttack = engine.getState().opponentPiclet.attack;
342
- const finalOpponentDefense = engine.getState().opponentPiclet.defense;
343
- const log = engine.getLog();
344
-
345
- expect(finalOpponentAttack).toBeLessThan(initialOpponentAttack);
346
- expect(finalOpponentDefense).toBeLessThan(initialOpponentDefense);
347
- expect(log.some(msg => msg.includes('Parting Shot') && msg.includes('triggered'))).toBe(true);
348
- });
349
- });
350
-
351
- describe('Forced Switching', () => {
352
- it('should handle forced switch when active piclet faints', () => {
353
- const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
354
-
355
- // Damage player piclet to near-faint
356
- engine['state'].playerPiclet.currentHp = 1;
357
-
358
- engine.executeActions(
359
- { type: 'move', piclet: 'player', moveIndex: 0 },
360
- { type: 'move', piclet: 'opponent', moveIndex: 0 } // Should KO player
361
- );
362
-
363
- const log = engine.getLog();
364
-
365
- // Should prompt for forced switch or auto-switch if only one option
366
- expect(log.some(msg =>
367
- msg.includes('fainted') ||
368
- msg.includes('must choose') ||
369
- msg.includes('forced switch')
370
- )).toBe(true);
371
- });
372
-
373
- it('should end battle if no valid switches remain', () => {
374
- const engine = new BattleEngine([basicPiclet], [basicPiclet]); // Only one piclet each
375
-
376
- // KO the only piclet
377
- engine['state'].playerPiclet.currentHp = 1;
378
-
379
- engine.executeActions(
380
- { type: 'move', piclet: 'player', moveIndex: 0 },
381
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
382
- );
383
-
384
- expect(engine.isGameOver()).toBe(true);
385
- expect(engine.getState().winner).toBe('opponent');
386
- });
387
- });
388
-
389
- describe('Switch Action Integration', () => {
390
- it('should preserve PP and status when switching back', () => {
391
- const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
392
-
393
- // Use a move to reduce PP
394
- engine.executeActions(
395
- { type: 'move', piclet: 'player', moveIndex: 0 },
396
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
397
- );
398
-
399
- const ppAfterMove = engine.getState().playerPiclet.moves[0].currentPP;
400
-
401
- // Switch out and back
402
- engine.executeActions(
403
- { type: 'switch', piclet: 'player', newPicletIndex: 1 },
404
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
405
- );
406
-
407
- engine.executeActions(
408
- { type: 'switch', piclet: 'player', newPicletIndex: 0 },
409
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
410
- );
411
-
412
- const ppAfterReturn = engine.getState().playerPiclet.moves[0].currentPP;
413
-
414
- // PP should be preserved
415
- expect(ppAfterReturn).toBe(ppAfterMove);
416
- });
417
-
418
- it('should reset stat modifications when switching', () => {
419
- const engine = new BattleEngine([basicPiclet, reservePiclet], [basicPiclet]);
420
-
421
- // Apply stat modification
422
- engine['state'].playerPiclet.attack += 20; // Simulate boost
423
- engine['state'].playerPiclet.statModifiers.attack = 1;
424
-
425
- const boostedAttack = engine.getState().playerPiclet.attack;
426
-
427
- // Switch out and back
428
- engine.executeActions(
429
- { type: 'switch', piclet: 'player', newPicletIndex: 1 },
430
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
431
- );
432
-
433
- engine.executeActions(
434
- { type: 'switch', piclet: 'player', newPicletIndex: 0 },
435
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
436
- );
437
-
438
- const finalAttack = engine.getState().playerPiclet.attack;
439
-
440
- // Attack should be reset to base value
441
- expect(finalAttack).toBeLessThan(boostedAttack);
442
- expect(engine.getState().playerPiclet.statModifiers.attack).toBeFalsy();
443
- });
444
- });
445
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/tempest-wraith.test.ts DELETED
@@ -1,507 +0,0 @@
1
- /**
2
- * Test for the complete Tempest Wraith example from the design document
3
- * This demonstrates all advanced features working together
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import { BattleEngine } from './BattleEngine';
8
- import { PicletDefinition } from './types';
9
- import { PicletType, AttackType } from './types';
10
-
11
- describe('Complete Tempest Wraith Implementation', () => {
12
- it('should handle the complete Tempest Wraith from design document', () => {
13
- const tempestWraith: PicletDefinition = {
14
- name: "Tempest Wraith",
15
- description: "A ghostly creature born from violent storms, wielding cosmic energy and shadowy illusions",
16
- tier: 'high',
17
- primaryType: PicletType.SPACE,
18
- secondaryType: PicletType.CULTURE,
19
- baseStats: {
20
- hp: 75,
21
- attack: 95,
22
- defense: 45,
23
- speed: 85
24
- },
25
- nature: "timid",
26
- specialAbility: {
27
- name: "Storm Caller",
28
- description: "When HP drops below 25%, gains immunity to status effects and +50% speed",
29
- triggers: [
30
- {
31
- event: 'onLowHP',
32
- effects: [
33
- {
34
- type: 'mechanicOverride',
35
- mechanic: 'statusImmunity',
36
- value: ['burn', 'freeze', 'paralyze', 'poison', 'sleep', 'confuse']
37
- },
38
- {
39
- type: 'modifyStats',
40
- target: 'self',
41
- stats: { speed: 'greatly_increase' }
42
- }
43
- ]
44
- },
45
- {
46
- event: 'onSwitchIn',
47
- condition: 'ifWeather:storm',
48
- effects: [
49
- {
50
- type: 'modifyStats',
51
- target: 'self',
52
- stats: { attack: 'increase' }
53
- }
54
- ]
55
- }
56
- ]
57
- },
58
- movepool: [
59
- {
60
- name: "Shadow Pulse",
61
- type: AttackType.CULTURE,
62
- power: 70,
63
- accuracy: 100,
64
- pp: 15,
65
- priority: 0,
66
- flags: [],
67
- effects: [
68
- {
69
- type: 'damage',
70
- target: 'opponent',
71
- amount: 'normal'
72
- },
73
- {
74
- type: 'applyStatus',
75
- target: 'opponent',
76
- status: 'confuse'
77
- }
78
- ]
79
- },
80
- {
81
- name: "Cosmic Strike",
82
- type: AttackType.SPACE,
83
- power: 85,
84
- accuracy: 90,
85
- pp: 10,
86
- priority: 0,
87
- flags: [],
88
- effects: [
89
- {
90
- type: 'damage',
91
- target: 'opponent',
92
- amount: 'normal'
93
- },
94
- {
95
- type: 'applyStatus',
96
- target: 'opponent',
97
- status: 'paralyze'
98
- }
99
- ]
100
- },
101
- {
102
- name: "Spectral Drain",
103
- type: AttackType.CULTURE,
104
- power: 60,
105
- accuracy: 95,
106
- pp: 12,
107
- priority: 0,
108
- flags: ['draining'],
109
- effects: [
110
- {
111
- type: 'damage',
112
- target: 'opponent',
113
- formula: 'drain',
114
- value: 0.5
115
- }
116
- ]
117
- },
118
- {
119
- name: "Void Sacrifice",
120
- type: AttackType.SPACE,
121
- power: 130,
122
- accuracy: 85,
123
- pp: 1,
124
- priority: 0,
125
- flags: ['sacrifice', 'explosive'],
126
- effects: [
127
- {
128
- type: 'damage',
129
- target: 'all',
130
- formula: 'standard',
131
- multiplier: 1.2
132
- },
133
- {
134
- type: 'damage',
135
- target: 'self',
136
- formula: 'percentage',
137
- value: 75
138
- },
139
- {
140
- type: 'fieldEffect',
141
- effect: 'voidStorm',
142
- target: 'field',
143
- stackable: false
144
- }
145
- ]
146
- }
147
- ]
148
- };
149
-
150
- const opponent: PicletDefinition = {
151
- name: "Standard Fighter",
152
- description: "A basic opponent",
153
- tier: 'medium',
154
- primaryType: PicletType.BEAST,
155
- baseStats: { hp: 100, attack: 80, defense: 70, speed: 60 },
156
- nature: "Hardy",
157
- specialAbility: { name: "None", description: "No ability" },
158
- movepool: [{
159
- name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
160
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
161
- }]
162
- };
163
-
164
- const engine = new BattleEngine(tempestWraith, opponent);
165
-
166
- // Test 1: Verify Tempest Wraith is properly initialized
167
- const state = engine.getState();
168
- expect(state.playerPiclet.definition.name).toBe("Tempest Wraith");
169
- expect(state.playerPiclet.definition.primaryType).toBe(PicletType.SPACE);
170
- expect(state.playerPiclet.definition.secondaryType).toBe(PicletType.CULTURE);
171
- expect(state.playerPiclet.moves).toHaveLength(4);
172
-
173
- // Test 2: Verify special ability structure
174
- const ability = tempestWraith.specialAbility;
175
- expect(ability.name).toBe("Storm Caller");
176
- expect(ability.triggers).toHaveLength(2);
177
- expect(ability.triggers![0].event).toBe('onLowHP');
178
- expect(ability.triggers![1].event).toBe('onSwitchIn');
179
-
180
- // Test 3: Test Shadow Pulse (dual effect move)
181
- engine.executeActions(
182
- { type: 'move', piclet: 'player', moveIndex: 0 }, // Shadow Pulse
183
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
184
- );
185
-
186
- let log = engine.getLog();
187
- expect(log.some(msg => msg.includes('used Shadow Pulse'))).toBe(true);
188
-
189
- // Test 4: Test Spectral Drain (drain move)
190
- if (!engine.isGameOver()) {
191
- // Damage the player to test healing
192
- engine['state'].playerPiclet.currentHp = 30;
193
-
194
- engine.executeActions(
195
- { type: 'move', piclet: 'player', moveIndex: 2 }, // Spectral Drain
196
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
197
- );
198
-
199
- log = engine.getLog();
200
- expect(log.some(msg => msg.includes('recovered') && msg.includes('HP from draining'))).toBe(true);
201
- }
202
-
203
- // Test 5: Test Void Sacrifice (ultimate move)
204
- if (!engine.isGameOver()) {
205
- const preVoidHp = engine.getState().playerPiclet.currentHp;
206
-
207
- engine.executeActions(
208
- { type: 'move', piclet: 'player', moveIndex: 3 }, // Void Sacrifice
209
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
210
- );
211
-
212
- log = engine.getLog();
213
- expect(log.some(msg => msg.includes('used Void Sacrifice'))).toBe(true);
214
- // Just verify that some field effect message exists
215
- const hasFieldEffect = log.some(msg => msg.includes('applied') || msg.includes('effect'));
216
- expect(hasFieldEffect).toBe(true);
217
-
218
- // Should have taken massive self-damage
219
- const postVoidHp = engine.getState().playerPiclet.currentHp;
220
- expect(postVoidHp).toBeLessThan(preVoidHp);
221
- }
222
- });
223
-
224
- it('should demonstrate strategic depth with different movesets', () => {
225
- const tempestWraith: PicletDefinition = {
226
- name: "Tempest Wraith",
227
- description: "A ghostly creature born from violent storms",
228
- tier: 'high',
229
- primaryType: PicletType.SPACE,
230
- secondaryType: PicletType.CULTURE,
231
- baseStats: { hp: 75, attack: 95, defense: 45, speed: 85 },
232
- nature: "timid",
233
- specialAbility: {
234
- name: "Storm Caller",
235
- description: "Complex multi-trigger ability",
236
- triggers: [
237
- {
238
- event: 'onLowHP',
239
- effects: [
240
- {
241
- type: 'mechanicOverride',
242
- mechanic: 'statusImmunity',
243
- value: ['burn', 'freeze', 'paralyze', 'poison', 'sleep', 'confuse']
244
- }
245
- ]
246
- }
247
- ]
248
- },
249
- movepool: [
250
- {
251
- name: "Berserker's End",
252
- type: AttackType.BEAST,
253
- power: 80,
254
- accuracy: 95,
255
- pp: 10,
256
- priority: 0,
257
- flags: ['contact', 'reckless'],
258
- effects: [
259
- {
260
- type: 'damage',
261
- target: 'opponent',
262
- amount: 'normal'
263
- },
264
- {
265
- type: 'damage',
266
- target: 'opponent',
267
- amount: 'strong',
268
- condition: 'ifLowHp'
269
- },
270
- {
271
- type: 'mechanicOverride',
272
- target: 'self',
273
- mechanic: 'healingBlocked',
274
- value: true
275
- }
276
- ]
277
- },
278
- {
279
- name: "Cursed Gambit",
280
- type: AttackType.CULTURE,
281
- power: 0,
282
- accuracy: 100,
283
- pp: 1,
284
- priority: 0,
285
- flags: ['gambling'],
286
- effects: [
287
- {
288
- type: 'heal',
289
- target: 'self',
290
- formula: 'percentage',
291
- value: 100,
292
- condition: 'ifLucky50'
293
- },
294
- {
295
- type: 'damage',
296
- target: 'self',
297
- formula: 'fixed',
298
- value: 9999,
299
- condition: 'ifUnlucky50'
300
- }
301
- ]
302
- }
303
- ]
304
- };
305
-
306
- const engine = new BattleEngine(tempestWraith, {
307
- name: "Opponent", description: "Test opponent", tier: 'medium',
308
- primaryType: PicletType.BEAST, baseStats: { hp: 100, attack: 80, defense: 70, speed: 60 },
309
- nature: "Hardy", specialAbility: { name: "None", description: "No ability" },
310
- movepool: [{ name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
311
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] }]
312
- });
313
-
314
- // Test the moveset variety
315
- const state = engine.getState();
316
- expect(state.playerPiclet.moves[0].move.name).toBe("Berserker's End");
317
- expect(state.playerPiclet.moves[1].move.name).toBe("Cursed Gambit");
318
-
319
- // Test Berserker's End effects
320
- const berserkersEnd = state.playerPiclet.moves[0].move;
321
- expect(berserkersEnd.effects).toHaveLength(3);
322
- expect(berserkersEnd.effects[1].condition).toBe('ifLowHp');
323
-
324
- // Test Cursed Gambit effects
325
- const cursedGambit = state.playerPiclet.moves[1].move;
326
- expect(cursedGambit.effects).toHaveLength(2);
327
- expect(cursedGambit.effects[0].condition).toBe('ifLucky50');
328
- expect(cursedGambit.effects[1].condition).toBe('ifUnlucky50');
329
- });
330
-
331
- it('should handle complete battle with advanced mechanics', () => {
332
- const advancedPiclet: PicletDefinition = {
333
- name: "Master of All Trades",
334
- description: "Demonstrates every major battle system feature",
335
- tier: 'legendary',
336
- primaryType: PicletType.SPACE,
337
- secondaryType: PicletType.CULTURE,
338
- baseStats: { hp: 100, attack: 100, defense: 80, speed: 90 },
339
- nature: "Adaptive",
340
- specialAbility: {
341
- name: "Omni-Adaptation",
342
- description: "Multiple triggers for different situations",
343
- effects: [
344
- {
345
- type: 'mechanicOverride',
346
- mechanic: 'criticalHits',
347
- value: 'double'
348
- }
349
- ],
350
- triggers: [
351
- {
352
- event: 'onDamageTaken',
353
- effects: [
354
- {
355
- type: 'modifyStats',
356
- target: 'self',
357
- stats: { attack: 'increase' }
358
- }
359
- ]
360
- },
361
- {
362
- event: 'onSwitchIn',
363
- effects: [
364
- {
365
- type: 'removeStatus',
366
- target: 'self',
367
- status: 'poison'
368
- }
369
- ]
370
- },
371
- {
372
- event: 'endOfTurn',
373
- condition: 'ifStatus:burn',
374
- effects: [
375
- {
376
- type: 'heal',
377
- target: 'self',
378
- formula: 'percentage',
379
- value: 10
380
- }
381
- ]
382
- }
383
- ]
384
- },
385
- movepool: [
386
- {
387
- name: "Adaptive Strike",
388
- type: AttackType.NORMAL,
389
- power: 70,
390
- accuracy: 100,
391
- pp: 20,
392
- priority: 0,
393
- flags: ['contact'],
394
- effects: [
395
- {
396
- type: 'damage',
397
- target: 'opponent',
398
- amount: 'normal'
399
- },
400
- {
401
- type: 'damage',
402
- target: 'opponent',
403
- amount: 'strong',
404
- condition: 'ifStatus:burn'
405
- }
406
- ]
407
- },
408
- {
409
- name: "Field Manipulator",
410
- type: AttackType.SPACE,
411
- power: 0,
412
- accuracy: 100,
413
- pp: 10,
414
- priority: 1,
415
- flags: ['priority'],
416
- effects: [
417
- {
418
- type: 'fieldEffect',
419
- effect: 'gravityField',
420
- target: 'field',
421
- stackable: false
422
- },
423
- {
424
- type: 'modifyStats',
425
- target: 'opponent',
426
- stats: { speed: 'decrease' }
427
- }
428
- ]
429
- },
430
- {
431
- name: "Status Cleanse",
432
- type: AttackType.NORMAL,
433
- power: 0,
434
- accuracy: 100,
435
- pp: 15,
436
- priority: 0,
437
- flags: [],
438
- effects: [
439
- {
440
- type: 'removeStatus',
441
- target: 'self',
442
- status: 'poison'
443
- },
444
- {
445
- type: 'removeStatus',
446
- target: 'self',
447
- status: 'burn'
448
- },
449
- {
450
- type: 'heal',
451
- target: 'self',
452
- amount: 'medium'
453
- }
454
- ]
455
- },
456
- {
457
- name: "Counter Protocol",
458
- type: AttackType.NORMAL,
459
- power: 0,
460
- accuracy: 100,
461
- pp: 10,
462
- priority: -5,
463
- flags: ['lowPriority'],
464
- effects: [
465
- {
466
- type: 'counter',
467
- strength: 'strong'
468
- }
469
- ]
470
- }
471
- ]
472
- };
473
-
474
- const engine = new BattleEngine(advancedPiclet, {
475
- name: "Test Opponent", description: "Basic opponent", tier: 'medium',
476
- primaryType: PicletType.BEAST, baseStats: { hp: 80, attack: 70, defense: 60, speed: 50 },
477
- nature: "Hardy", specialAbility: { name: "None", description: "No ability" },
478
- movepool: [{ name: "Tackle", type: AttackType.NORMAL, power: 40, accuracy: 100, pp: 35,
479
- priority: 0, flags: [], effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }] }]
480
- });
481
-
482
- // Test complex special ability
483
- const ability = advancedPiclet.specialAbility;
484
- expect(ability.effects).toHaveLength(1);
485
- expect(ability.triggers).toHaveLength(3);
486
- expect(ability.triggers![0].event).toBe('onDamageTaken');
487
- expect(ability.triggers![2].condition).toBe('ifStatus:burn');
488
-
489
- // Test diverse moveset
490
- const moves = advancedPiclet.movepool;
491
- expect(moves).toHaveLength(4);
492
- expect(moves[1].priority).toBe(1); // Priority move
493
- expect(moves[3].priority).toBe(-5); // Low priority counter
494
- expect(moves[2].effects).toHaveLength(3); // Multi-effect move
495
-
496
- // Test battle execution
497
- engine.executeActions(
498
- { type: 'move', piclet: 'player', moveIndex: 1 }, // Field Manipulator
499
- { type: 'move', piclet: 'opponent', moveIndex: 0 }
500
- );
501
-
502
- const log = engine.getLog();
503
- const hasFieldEffect = log.some(msg => msg.includes('applied') || msg.includes('effect'));
504
- expect(hasFieldEffect).toBe(true);
505
- expect(log.some(msg => msg.includes('speed fell'))).toBe(true);
506
- });
507
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/test-data.ts DELETED
@@ -1,234 +0,0 @@
1
- /**
2
- * Test data for battle engine testing
3
- * Contains example Piclets and moves following the design document
4
- */
5
-
6
- import {
7
- PicletDefinition,
8
- Move,
9
- BaseStats,
10
- SpecialAbility,
11
- AttackType,
12
- PicletType
13
- } from './types';
14
-
15
- // Example base stats for different tiers
16
- export const LOW_TIER_STATS: BaseStats = {
17
- hp: 80,
18
- attack: 65,
19
- defense: 60,
20
- speed: 55
21
- };
22
-
23
- export const MEDIUM_TIER_STATS: BaseStats = {
24
- hp: 100,
25
- attack: 80,
26
- defense: 75,
27
- speed: 70
28
- };
29
-
30
- export const HIGH_TIER_STATS: BaseStats = {
31
- hp: 120,
32
- attack: 100,
33
- defense: 90,
34
- speed: 85
35
- };
36
-
37
- // Example moves following the design document
38
- export const BASIC_TACKLE: Move = {
39
- name: "Tackle",
40
- type: AttackType.NORMAL,
41
- power: 40,
42
- accuracy: 100,
43
- pp: 35,
44
- priority: 0,
45
- flags: ['contact'],
46
- effects: [{
47
- type: 'damage',
48
- target: 'opponent',
49
- amount: 'normal'
50
- }]
51
- };
52
-
53
- export const FLAME_BURST: Move = {
54
- name: "Flame Burst",
55
- type: AttackType.SPACE,
56
- power: 70,
57
- accuracy: 100,
58
- pp: 15,
59
- priority: 0,
60
- flags: ['explosive'],
61
- effects: [{
62
- type: 'damage',
63
- target: 'opponent',
64
- amount: 'normal'
65
- }]
66
- };
67
-
68
- export const HEALING_LIGHT: Move = {
69
- name: "Healing Light",
70
- type: AttackType.SPACE,
71
- power: 0,
72
- accuracy: 100,
73
- pp: 10,
74
- priority: 0,
75
- flags: [],
76
- effects: [{
77
- type: 'heal',
78
- target: 'self',
79
- amount: 'medium'
80
- }]
81
- };
82
-
83
- export const POWER_UP: Move = {
84
- name: "Power Up",
85
- type: AttackType.NORMAL,
86
- power: 0,
87
- accuracy: 100,
88
- pp: 20,
89
- priority: 0,
90
- flags: [],
91
- effects: [{
92
- type: 'modifyStats',
93
- target: 'self',
94
- stats: { attack: 'increase' }
95
- }]
96
- };
97
-
98
- export const BERSERKER_END: Move = {
99
- name: "Berserker's End",
100
- type: AttackType.BEAST,
101
- power: 80,
102
- accuracy: 90,
103
- pp: 5,
104
- priority: 0,
105
- flags: ['contact', 'reckless', 'sacrifice'],
106
- effects: [
107
- {
108
- type: 'damage',
109
- target: 'opponent',
110
- amount: 'normal'
111
- },
112
- {
113
- type: 'damage',
114
- target: 'opponent',
115
- amount: 'strong',
116
- condition: 'ifLowHp'
117
- },
118
- {
119
- type: 'modifyStats',
120
- target: 'self',
121
- stats: { defense: 'greatly_decrease' },
122
- condition: 'ifLowHp'
123
- }
124
- ]
125
- };
126
-
127
- export const TOXIC_STING: Move = {
128
- name: "Toxic Sting",
129
- type: AttackType.BUG,
130
- power: 30,
131
- accuracy: 100,
132
- pp: 20,
133
- priority: 0,
134
- flags: ['contact'],
135
- effects: [
136
- {
137
- type: 'damage',
138
- target: 'opponent',
139
- amount: 'weak'
140
- },
141
- {
142
- type: 'applyStatus',
143
- target: 'opponent',
144
- status: 'poison'
145
- }
146
- ]
147
- };
148
-
149
- // Example special abilities
150
- export const REGENERATOR: SpecialAbility = {
151
- name: "Regenerator",
152
- description: "Restores HP when switching out",
153
- triggers: [{
154
- event: "onSwitchOut",
155
- effects: [{
156
- type: 'heal',
157
- target: 'self',
158
- amount: 'small'
159
- }]
160
- }]
161
- };
162
-
163
- export const FLAME_BODY: SpecialAbility = {
164
- name: "Flame Body",
165
- description: "Contact moves may burn the attacker",
166
- triggers: [{
167
- event: "onContactDamage",
168
- condition: 'ifLucky50',
169
- effects: [{
170
- type: 'applyStatus',
171
- target: 'attacker',
172
- status: 'burn'
173
- }]
174
- }]
175
- };
176
-
177
- export const SPEED_BOOST: SpecialAbility = {
178
- name: "Speed Boost",
179
- description: "Speed increases each turn",
180
- triggers: [{
181
- event: "onTurnEnd",
182
- effects: [{
183
- type: 'modifyStats',
184
- target: 'self',
185
- stats: { speed: 'increase' }
186
- }]
187
- }]
188
- };
189
-
190
- // Example Piclet definitions
191
- export const STELLAR_WOLF: PicletDefinition = {
192
- name: "Stellar Wolf",
193
- description: "A cosmic predator that hunts among the stars",
194
- tier: 'medium',
195
- primaryType: PicletType.SPACE,
196
- secondaryType: PicletType.BEAST,
197
- baseStats: MEDIUM_TIER_STATS,
198
- nature: "Brave",
199
- specialAbility: FLAME_BODY,
200
- movepool: [BASIC_TACKLE, FLAME_BURST, HEALING_LIGHT, POWER_UP]
201
- };
202
-
203
- export const TOXIC_CRAWLER: PicletDefinition = {
204
- name: "Toxic Crawler",
205
- description: "A venomous arthropod with deadly precision",
206
- tier: 'low',
207
- primaryType: PicletType.BUG,
208
- baseStats: LOW_TIER_STATS,
209
- nature: "Careful",
210
- specialAbility: SPEED_BOOST,
211
- movepool: [BASIC_TACKLE, TOXIC_STING, POWER_UP]
212
- };
213
-
214
- export const BERSERKER_BEAST: PicletDefinition = {
215
- name: "Berserker Beast",
216
- description: "A wild creature that fights with reckless abandon",
217
- tier: 'high',
218
- primaryType: PicletType.BEAST,
219
- baseStats: HIGH_TIER_STATS,
220
- nature: "Reckless",
221
- specialAbility: REGENERATOR,
222
- movepool: [BASIC_TACKLE, BERSERKER_END, HEALING_LIGHT, POWER_UP]
223
- };
224
-
225
- export const AQUA_GUARDIAN: PicletDefinition = {
226
- name: "Aqua Guardian",
227
- description: "A protective water spirit",
228
- tier: 'medium',
229
- primaryType: PicletType.AQUATIC,
230
- baseStats: MEDIUM_TIER_STATS,
231
- nature: "Calm",
232
- specialAbility: REGENERATOR,
233
- movepool: [BASIC_TACKLE, HEALING_LIGHT, POWER_UP]
234
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/battle-engine/types.ts DELETED
@@ -1,262 +0,0 @@
1
- /**
2
- * Core types for the Pictuary Battle System
3
- * Based on battle_system_design.md specification
4
- */
5
-
6
- import { PicletType, AttackType, type TypeEffectiveness } from '../types/picletTypes';
7
-
8
- export { PicletType, AttackType };
9
- export type { TypeEffectiveness };
10
-
11
- export type Tier = 'low' | 'medium' | 'high' | 'legendary';
12
-
13
- // Status Effects
14
- export type StatusEffect = 'burn' | 'freeze' | 'paralyze' | 'poison' | 'sleep' | 'confuse';
15
-
16
- // Effect System Types
17
- export type EffectTarget = 'self' | 'opponent' | 'allies' | 'all' | 'attacker' | 'field' | 'playerSide' | 'opponentSide';
18
- export type EffectCondition =
19
- | 'always' | 'onHit' | 'afterUse' | 'onCritical' | 'ifLowHp' | 'ifHighHp'
20
- | 'thisTurn' | 'nextTurn' | 'turnAfterNext' | 'restOfBattle'
21
- | 'onCharging' | 'afterCharging' | 'ifDamagedThisTurn' | 'ifNotSuperEffective'
22
- | 'ifStatusMove' | 'ifLucky50' | 'ifUnlucky50' | 'whileFrozen'
23
- | 'ifMoveType:flora' | 'ifMoveType:space' | 'ifMoveType:beast' | 'ifMoveType:bug'
24
- | 'ifMoveType:aquatic' | 'ifMoveType:mineral' | 'ifMoveType:machina' | 'ifMoveType:structure'
25
- | 'ifMoveType:culture' | 'ifMoveType:cuisine' | 'ifMoveType:normal'
26
- | 'ifStatus:burn' | 'ifStatus:freeze' | 'ifStatus:paralyze' | 'ifStatus:poison'
27
- | 'ifStatus:sleep' | 'ifStatus:confuse'
28
- | 'ifWeather:storm' | 'ifWeather:rain' | 'ifWeather:sun' | 'ifWeather:snow'
29
- | 'whenStatusAfflicted' | 'vsPhysical' | 'vsSpecial';
30
-
31
- export type DamageAmount = 'weak' | 'normal' | 'strong' | 'extreme';
32
- export type DamageFormula = 'standard' | 'recoil' | 'drain' | 'fixed' | 'percentage';
33
- export type StatModification = 'increase' | 'decrease' | 'greatly_increase' | 'greatly_decrease';
34
- export type HealAmount = 'small' | 'medium' | 'large' | 'full';
35
- export type PPAmount = 'small' | 'medium' | 'large';
36
- export type CounterStrength = 'weak' | 'normal' | 'strong';
37
-
38
- // Move Flags
39
- export type MoveFlag =
40
- | 'contact' | 'bite' | 'punch' | 'sound' | 'explosive' | 'draining' | 'ground'
41
- | 'priority' | 'lowPriority' | 'charging' | 'recharge' | 'multiHit' | 'twoTurn'
42
- | 'sacrifice' | 'gambling' | 'reckless' | 'reflectable' | 'snatchable'
43
- | 'copyable' | 'protectable' | 'bypassProtect';
44
-
45
- // Base Stats
46
- export interface BaseStats {
47
- hp: number;
48
- attack: number;
49
- defense: number;
50
- speed: number;
51
- }
52
-
53
- // Battle Effects
54
- export interface DamageEffect {
55
- type: 'damage';
56
- target: EffectTarget;
57
- amount?: DamageAmount;
58
- formula?: DamageFormula;
59
- value?: number;
60
- multiplier?: number;
61
- condition?: EffectCondition;
62
- }
63
-
64
- export interface ModifyStatsEffect {
65
- type: 'modifyStats';
66
- target: EffectTarget;
67
- stats: Partial<Record<keyof BaseStats | 'accuracy', StatModification>>;
68
- condition?: EffectCondition;
69
- }
70
-
71
- export interface ApplyStatusEffect {
72
- type: 'applyStatus';
73
- target: EffectTarget;
74
- status: StatusEffect;
75
- chance?: number;
76
- condition?: EffectCondition;
77
- }
78
-
79
- export interface HealEffect {
80
- type: 'heal';
81
- target: EffectTarget;
82
- amount?: HealAmount;
83
- formula?: 'percentage' | 'fixed';
84
- value?: number;
85
- condition?: EffectCondition;
86
- }
87
-
88
- export interface ManipulatePPEffect {
89
- type: 'manipulatePP';
90
- target: EffectTarget;
91
- action: 'drain' | 'restore' | 'disable';
92
- amount?: PPAmount;
93
- value?: number;
94
- targetMove?: 'random' | 'lastUsed' | 'specific';
95
- condition?: EffectCondition;
96
- }
97
-
98
- export interface FieldEffect {
99
- type: 'fieldEffect';
100
- effect: string;
101
- target: EffectTarget;
102
- stackable: boolean;
103
- condition?: EffectCondition;
104
- }
105
-
106
- export interface CounterEffect {
107
- type: 'counter';
108
- strength: CounterStrength;
109
- condition?: EffectCondition;
110
- }
111
-
112
- export interface PriorityEffect {
113
- type: 'priority';
114
- target: EffectTarget;
115
- value: number; // -5 to +5
116
- condition?: EffectCondition;
117
- }
118
-
119
- export interface RemoveStatusEffect {
120
- type: 'removeStatus';
121
- target: EffectTarget;
122
- status: StatusEffect;
123
- condition?: EffectCondition;
124
- }
125
-
126
- export interface MechanicOverrideEffect {
127
- type: 'mechanicOverride';
128
- mechanic: 'criticalHits' | 'statusImmunity' | 'statusReplacement' | 'damageReflection'
129
- | 'damageAbsorption' | 'damageCalculation' | 'damageMultiplier' | 'healingInversion'
130
- | 'healingBlocked' | 'priorityOverride' | 'accuracyBypass' | 'typeImmunity'
131
- | 'typeChange' | 'contactDamage' | 'drainInversion' | 'weatherImmunity'
132
- | 'flagImmunity' | 'flagWeakness' | 'flagResistance' | 'statModification'
133
- | 'targetRedirection' | 'extraTurn';
134
- value: any;
135
- condition?: EffectCondition;
136
- }
137
-
138
- export type BattleEffect =
139
- | DamageEffect | ModifyStatsEffect | ApplyStatusEffect | HealEffect
140
- | ManipulatePPEffect | FieldEffect | CounterEffect | PriorityEffect
141
- | RemoveStatusEffect | MechanicOverrideEffect;
142
-
143
- // Move Definition
144
- export interface Move {
145
- name: string;
146
- type: AttackType;
147
- power: number;
148
- accuracy: number;
149
- pp: number;
150
- priority: number;
151
- flags: MoveFlag[];
152
- effects: BattleEffect[];
153
- }
154
-
155
- // Special Ability
156
- export interface Trigger {
157
- event: 'onDamageTaken' | 'onDamageDealt' | 'onContactDamage' | 'onStatusInflicted'
158
- | 'onStatusMove' | 'onStatusMoveTargeted' | 'onCriticalHit' | 'onHPDrained'
159
- | 'onKO' | 'onSwitchIn' | 'onSwitchOut' | 'onWeatherChange' | 'beforeMoveUse'
160
- | 'afterMoveUse' | 'onLowHP' | 'onFullHP' | 'endOfTurn' | 'onOpponentContactMove'
161
- | 'onStatChange' | 'onTypeChange';
162
- condition?: EffectCondition;
163
- effects: BattleEffect[];
164
- }
165
-
166
- export interface SpecialAbility {
167
- name: string;
168
- description: string;
169
- effects?: BattleEffect[];
170
- triggers?: Trigger[];
171
- }
172
-
173
- // Piclet Definition
174
- export interface PicletDefinition {
175
- name: string;
176
- description: string;
177
- tier: Tier;
178
- primaryType: PicletType;
179
- secondaryType?: PicletType;
180
- baseStats: BaseStats;
181
- nature: string;
182
- specialAbility: SpecialAbility;
183
- movepool: Move[];
184
- }
185
-
186
- // Battle State Types
187
- export interface BattlePiclet {
188
- definition: PicletDefinition;
189
- currentHp: number;
190
- maxHp: number;
191
- level: number;
192
-
193
- // Current battle stats (modified by effects)
194
- attack: number;
195
- defense: number;
196
- speed: number;
197
- accuracy: number;
198
-
199
- // Status conditions
200
- statusEffects: StatusEffect[];
201
-
202
- // Move state
203
- moves: Array<{
204
- move: Move;
205
- currentPP: number;
206
- }>;
207
-
208
- // Battle state
209
- statModifiers: Partial<Record<keyof BaseStats | 'accuracy' | 'priority', number>>;
210
- temporaryEffects: Array<{
211
- effect: BattleEffect;
212
- duration: number;
213
- }>;
214
- }
215
-
216
- export interface BattleState {
217
- turn: number;
218
- phase: 'selection' | 'execution' | 'ended';
219
-
220
- playerPiclet: BattlePiclet;
221
- opponentPiclet: BattlePiclet;
222
-
223
- // Field effects
224
- fieldEffects: Array<{
225
- name: string;
226
- duration: number;
227
- effect: any;
228
- }>;
229
-
230
- // Battle log for testing/debugging
231
- log: string[];
232
-
233
- winner?: 'player' | 'opponent' | 'draw';
234
-
235
- // Capture result (for wild battles)
236
- captureResult?: {
237
- success: boolean;
238
- shakes: number;
239
- odds: number;
240
- capturePercentage: number;
241
- };
242
- }
243
-
244
- // Action Types
245
- export interface MoveAction {
246
- type: 'move';
247
- piclet: 'player' | 'opponent';
248
- moveIndex: number;
249
- }
250
-
251
- export interface SwitchAction {
252
- type: 'switch';
253
- piclet: 'player' | 'opponent';
254
- newPicletIndex: number;
255
- }
256
-
257
- export interface CaptureAction {
258
- type: 'capture';
259
- piclet: 'player'; // Only player can capture
260
- }
261
-
262
- export type BattleAction = MoveAction | SwitchAction | CaptureAction;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/ActionViewSelector.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script context="module" lang="ts">
2
- export type ActionView = 'main' | 'moves' | 'piclets' | 'items' | 'stats' | 'forcedSwap';
3
  </script>
4
 
5
  <script lang="ts">
@@ -34,7 +34,6 @@
34
  const actions = [
35
  { title: 'Act', icon: '⚔️', view: 'moves' as ActionView },
36
  { title: 'Piclets', icon: '🔄', view: 'piclets' as ActionView },
37
- { title: 'Items', icon: '🎒', view: 'items' as ActionView },
38
  // { title: 'Stats', icon: '📊', view: 'stats' as ActionView }, // Debug only
39
  ];
40
 
@@ -165,28 +164,6 @@
165
  {/each}
166
  {/if}
167
  </div>
168
- {:else if currentView === 'items'}
169
- <div class="sub-view-list">
170
- {#if isWildBattle && enemyPiclet}
171
- <button
172
- class="sub-item item-item"
173
- on:click={onCaptureAttempt}
174
- disabled={processingTurn}
175
- >
176
- <span class="item-icon">📸</span>
177
- <div class="item-info">
178
- <div class="item-name">Capture ({capturePercentage.toFixed(1)}%)</div>
179
- <div class="item-desc">{getCaptureDescription(capturePercentage)} - {enemyPiclet.nickname}</div>
180
- </div>
181
- </button>
182
- {:else}
183
- <div class="empty-message">
184
- <span class="empty-icon">🎒</span>
185
- <div>Items coming soon!</div>
186
- <div class="empty-subtitle">This feature is currently under development.</div>
187
- </div>
188
- {/if}
189
- </div>
190
  {/if}
191
  </div>
192
  </div>
@@ -499,29 +476,6 @@
499
  white-space: nowrap;
500
  }
501
 
502
- /* Item items */
503
- .item-icon {
504
- font-size: 24px;
505
- margin-right: 12px;
506
- width: 32px;
507
- text-align: center;
508
- }
509
-
510
- .item-info {
511
- flex: 1;
512
- }
513
-
514
- .item-name {
515
- font-size: 16px;
516
- font-weight: 500;
517
- color: #000;
518
- margin-bottom: 2px;
519
- }
520
-
521
- .item-desc {
522
- font-size: 13px;
523
- color: #8e8e93;
524
- }
525
 
526
  /* Empty states */
527
  .empty-message {
 
1
  <script context="module" lang="ts">
2
+ export type ActionView = 'main' | 'moves' | 'piclets' | 'stats' | 'forcedSwap';
3
  </script>
4
 
5
  <script lang="ts">
 
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
 
 
164
  {/each}
165
  {/if}
166
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  {/if}
168
  </div>
169
  </div>
 
476
  white-space: nowrap;
477
  }
478
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  /* Empty states */
481
  .empty-message {
src/lib/components/Battle/BattleControls.svelte DELETED
@@ -1,136 +0,0 @@
1
- <script lang="ts">
2
- import type { PicletInstance } from '$lib/db/schema';
3
- import type { BattleState } from '$lib/battle-engine/types';
4
- import ActionButtons from './ActionButtons.svelte';
5
- import TypewriterText from './TypewriterText.svelte';
6
-
7
- export let currentMessage: string;
8
- export let battlePhase: 'intro' | 'main' | 'moveSelect' | 'picletSelect' | 'ended';
9
- export let processingTurn: boolean;
10
- export let battleEnded: boolean;
11
- export let isWildBattle: boolean;
12
- export let playerPiclet: PicletInstance;
13
- export let enemyPiclet: PicletInstance;
14
- export let rosterPiclets: PicletInstance[] = [];
15
- export let battleState: BattleState | undefined = undefined;
16
- export let capturePercentage: number = 0;
17
- export let onAction: (action: string) => void;
18
- export let onMoveSelect: (move: any) => void;
19
- export let onPicletSelect: (piclet: PicletInstance) => void;
20
- export let onBack: () => void;
21
- export let waitingForContinue: boolean = false;
22
- export let onContinueTap: () => void;
23
-
24
- // Use the roster passed from parent instead of loading it here
25
- $: availablePiclets = rosterPiclets;
26
- </script>
27
-
28
- <div class="battle-controls">
29
- <!-- Message Bar -->
30
- <div class="message-bar {battleEnded ? 'special' : ''}">
31
- <p><TypewriterText text={currentMessage} speed={25} /></p>
32
- </div>
33
-
34
- <!-- Action Area -->
35
- <div class="action-area">
36
- {#if waitingForContinue}
37
- <!-- Tap to continue overlay -->
38
- <div class="tap-continue-overlay" role="button" tabindex="0" onclick={onContinueTap} onkeydown={(e) => e.key === 'Enter' || e.key === ' ' ? onContinueTap() : null}>
39
- <div class="tap-indicator">
40
- <span>Tap to continue</span>
41
- </div>
42
- </div>
43
- {:else if battlePhase === 'main' && !processingTurn && !battleEnded}
44
- <ActionButtons
45
- {isWildBattle}
46
- {playerPiclet}
47
- {enemyPiclet}
48
- {availablePiclets}
49
- {processingTurn}
50
- {battleState}
51
- {capturePercentage}
52
- {onAction}
53
- {onMoveSelect}
54
- {onPicletSelect}
55
- {onBack}
56
- />
57
- {/if}
58
- </div>
59
- </div>
60
-
61
- <style>
62
- .battle-controls {
63
- flex: 1;
64
- display: flex;
65
- flex-direction: column;
66
- background: white;
67
- border-top: 1px solid #e0e0e0;
68
- }
69
-
70
- .message-bar {
71
- min-height: 60px;
72
- padding: 1rem;
73
- background: #f8f9fa;
74
- border-bottom: 1px solid #e0e0e0;
75
- text-align: left;
76
- display: flex;
77
- align-items: center;
78
- justify-content: flex-start;
79
- }
80
-
81
- .message-bar.special {
82
- background: rgba(255, 152, 0, 0.1);
83
- border-color: rgba(255, 152, 0, 0.3);
84
- }
85
-
86
- .message-bar p {
87
- margin: 0;
88
- font-size: 1rem;
89
- color: #333;
90
- line-height: 1.4;
91
- }
92
-
93
- .action-area {
94
- flex: 1;
95
- padding: 1rem;
96
- display: flex;
97
- flex-direction: column;
98
- position: relative;
99
- }
100
-
101
- .tap-continue-overlay {
102
- position: absolute;
103
- top: 0;
104
- left: 0;
105
- right: 0;
106
- bottom: 0;
107
- background: rgba(0, 0, 0, 0.05);
108
- display: flex;
109
- align-items: center;
110
- justify-content: center;
111
- cursor: pointer;
112
- border-radius: 8px;
113
- }
114
-
115
- .tap-indicator {
116
- background: rgba(0, 0, 0, 0.15);
117
- color: #666;
118
- padding: 6px 12px;
119
- border-radius: 16px;
120
- font-size: 12px;
121
- font-weight: 400;
122
- border: 1px solid rgba(0, 0, 0, 0.1);
123
- animation: subtlePulse 3s infinite;
124
- }
125
-
126
- @keyframes subtlePulse {
127
- 0%, 100% {
128
- opacity: 0.7;
129
- transform: scale(1);
130
- }
131
- 50% {
132
- opacity: 0.9;
133
- transform: scale(1.02);
134
- }
135
- }
136
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/BattleEffects.svelte DELETED
@@ -1,522 +0,0 @@
1
- <script lang="ts">
2
- import { fade } from 'svelte/transition';
3
- import { onMount } from 'svelte';
4
-
5
- export let effects: Array<{type: string, emoji: string, duration: number}> = [];
6
- export let flash: boolean = false;
7
- export let faint: boolean = false;
8
-
9
- // GBA-style flicker animation parameters (matching original Snaplings timing)
10
- const flickerCount = 19;
11
- const frameDelay = 2;
12
- const flickerDuration = 1000; // milliseconds - matches Snaplings original
13
-
14
- // Flicker state management
15
- let isFlickering = false;
16
- let flickerVisible = true;
17
- let flickerFrame = 0;
18
- let flickerInterval: number;
19
-
20
- // Faint animation state management
21
- let isFainting = false;
22
- let faintProgress = 0;
23
- let faintAnimationId: number;
24
- let hasFainted = false;
25
-
26
- // Particle system configuration
27
- const PARTICLES_PER_EFFECT = 6; // Number of emoji particles per effect
28
- const SPAWN_RADIUS = 45; // Radius around piclet where particles spawn (reduced for better containment)
29
-
30
- // Generate multiple particles for each effect
31
- $: particleList = effects.flatMap((effect, effectIndex) => {
32
- const particles = [];
33
- for (let i = 0; i < PARTICLES_PER_EFFECT; i++) {
34
- // More varied spawn positions for better coverage
35
- const angle = (Math.PI * 2 * i) / PARTICLES_PER_EFFECT + (Math.random() - 0.5) * 1.2;
36
- const distance = SPAWN_RADIUS * (0.4 + Math.random() * 0.6); // Tighter distance variation for better containment
37
- const x = Math.cos(angle) * distance;
38
- const y = Math.sin(angle) * distance;
39
-
40
- // Enhanced animation properties for more dynamic effects (no rotation)
41
- const scale = 0.7 + Math.random() * 0.5; // 0.7x to 1.2x initial size
42
- const duration = Math.max(effect.duration * 1.8, 1800) + (Math.random() - 0.5) * 400; // Longer base duration
43
- const delay = Math.random() * 200; // More staggered animation starts
44
-
45
- // Additional movement properties for more dynamic motion (reduced range)
46
- const moveDistance = 20 + Math.random() * 30; // Further reduced movement distance for better containment
47
- const moveAngle = angle + (Math.random() - 0.5) * Math.PI * 0.4; // Further reduced movement angle variation
48
-
49
- particles.push({
50
- id: `${effectIndex}-${i}`,
51
- type: effect.type,
52
- emoji: effect.emoji,
53
- x,
54
- y,
55
- scale,
56
- duration,
57
- delay,
58
- moveDistance,
59
- moveAngle
60
- });
61
- }
62
- return particles;
63
- });
64
-
65
- // Watch for flash changes to trigger flicker animation
66
- $: if (flash && !isFlickering) {
67
- startFlickerAnimation();
68
- }
69
-
70
- // Watch for faint changes to trigger faint animation
71
- $: if (faint && !isFainting && !hasFainted) {
72
- startFaintAnimation();
73
- }
74
-
75
- function startFlickerAnimation() {
76
- isFlickering = true;
77
- flickerFrame = 0;
78
-
79
- // Calculate frame duration based on total duration and frame count
80
- const totalFrames = flickerCount * (frameDelay + 1);
81
- const frameDuration = flickerDuration / totalFrames;
82
-
83
- flickerInterval = setInterval(() => {
84
- if (flickerFrame >= totalFrames) {
85
- // Animation finished, always visible
86
- clearInterval(flickerInterval);
87
- isFlickering = false;
88
- flickerVisible = true;
89
- return;
90
- }
91
-
92
- // Toggle visibility every frameDelay frames
93
- const flickerCycle = Math.floor(flickerFrame / (frameDelay + 1));
94
- flickerVisible = flickerCycle % 2 === 0;
95
-
96
- flickerFrame++;
97
- }, frameDuration);
98
- }
99
-
100
- function startFaintAnimation() {
101
- isFainting = true;
102
- faintProgress = 0;
103
-
104
- const faintDuration = 1200; // milliseconds - matches Snaplings original
105
- const startTime = performance.now();
106
-
107
- function updateFaintAnimation(currentTime: number) {
108
- const elapsed = currentTime - startTime;
109
- const progress = Math.min(elapsed / faintDuration, 1);
110
-
111
- // Use easeIn curve for acceleration as it falls away
112
- faintProgress = progress * progress;
113
-
114
- if (progress < 1) {
115
- faintAnimationId = requestAnimationFrame(updateFaintAnimation);
116
- } else {
117
- // Animation completed
118
- isFainting = false;
119
- faintProgress = 1; // Keep final state
120
- hasFainted = true; // Mark as permanently fainted
121
- }
122
- }
123
-
124
- faintAnimationId = requestAnimationFrame(updateFaintAnimation);
125
- }
126
-
127
- onMount(() => {
128
- return () => {
129
- if (flickerInterval) {
130
- clearInterval(flickerInterval);
131
- }
132
- if (faintAnimationId) {
133
- cancelAnimationFrame(faintAnimationId);
134
- }
135
- };
136
- });
137
- </script>
138
-
139
- <!-- Effects wrapper with relative positioning for particles -->
140
- <div class="effects-wrapper">
141
- <!-- GBA-style flicker effect with faint animation -->
142
- <div
143
- class="effects-container"
144
- class:is-fainting={faint}
145
- style="
146
- opacity: {(flash && isFlickering) ? (flickerVisible ? 1 : 0) : (hasFainted || (faint && faintProgress >= 1) ? 0 : 1)};
147
- {faint || hasFainted ? `
148
- transform:
149
- scale(1, ${Math.max(0, 1 - faintProgress)})
150
- matrix(1, 0, ${-faintProgress * 0.5}, 1, 0, 0);
151
- transform-origin: bottom center;
152
- ` : ''}
153
- "
154
- >
155
- <slot />
156
- </div>
157
-
158
- <!-- Multi-particle effects -->
159
- {#each particleList as particle (particle.id)}
160
- <div
161
- class="effect-particle {particle.type}"
162
- style="
163
- left: calc(50% + {particle.x}px);
164
- top: calc(50% + {particle.y}px);
165
- animation-duration: {particle.duration}ms;
166
- animation-delay: {particle.delay}ms;
167
- --initial-scale: {particle.scale};
168
- --move-x: {Math.cos(particle.moveAngle) * particle.moveDistance}px;
169
- --move-y: {Math.sin(particle.moveAngle) * particle.moveDistance}px;
170
- "
171
- >
172
- <span class="effect-emoji">{particle.emoji}</span>
173
- </div>
174
- {/each}
175
- </div>
176
-
177
- <style>
178
- .effects-wrapper {
179
- position: relative;
180
- display: inline-block;
181
- /* Ensure wrapper contains particles even during resize */
182
- overflow: visible;
183
- /* Create a proper containing block for particles */
184
- width: 100%;
185
- height: 100%;
186
- }
187
-
188
- .effects-container {
189
- position: relative;
190
- display: inline-block;
191
- transition: opacity 0.05s ease;
192
- z-index: 2; /* Ensure effects appear above platform (z-index: 0) */
193
- }
194
-
195
- .effect-particle {
196
- position: absolute;
197
- pointer-events: none;
198
- z-index: 10;
199
- animation-fill-mode: forwards;
200
- transform-origin: center center;
201
- /* Position relative to the Piclet center */
202
- }
203
-
204
- .effect-emoji {
205
- font-size: 24px;
206
- display: block;
207
- filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.5));
208
- transform: scale(var(--initial-scale, 1));
209
- /* Remove rotation - emojis stay upright */
210
- }
211
-
212
- /* Status effects - floating with rotation */
213
- .effect-particle.burn {
214
- animation: statusBurn ease-in-out;
215
- }
216
-
217
- .effect-particle.poison {
218
- animation: statusPoison ease-in-out;
219
- }
220
-
221
- .effect-particle.paralyze {
222
- animation: statusParalyze linear;
223
- }
224
-
225
- .effect-particle.sleep {
226
- animation: statusSleep ease-in-out;
227
- }
228
-
229
- .effect-particle.freeze {
230
- animation: statusFreeze ease-out;
231
- }
232
-
233
- /* Stat increases - rising with spin */
234
- .effect-particle.attackUp,
235
- .effect-particle.defenseUp,
236
- .effect-particle.speedUp,
237
- .effect-particle.accuracyUp {
238
- animation: statIncrease ease-out;
239
- }
240
-
241
- /* Stat decreases - falling with wobble */
242
- .effect-particle.attackDown,
243
- .effect-particle.defenseDown,
244
- .effect-particle.speedDown,
245
- .effect-particle.accuracyDown {
246
- animation: statDecrease ease-in;
247
- }
248
-
249
- /* Special effects */
250
- .effect-particle.critical,
251
- .effect-particle.superEffective {
252
- animation: criticalBurst ease-out;
253
- }
254
-
255
- .effect-particle.notVeryEffective,
256
- .effect-particle.miss {
257
- animation: missSwirl ease-in-out;
258
- }
259
-
260
- .effect-particle.heal {
261
- animation: healRise ease-out;
262
- }
263
-
264
- /* Complex multi-property animations */
265
- @keyframes statusBurn {
266
- 0% {
267
- transform: translate(-50%, -50%) scale(0.2);
268
- opacity: 0;
269
- }
270
- 10% {
271
- transform: translate(-50%, -50%) scale(1.8);
272
- opacity: 1;
273
- }
274
- 25% {
275
- transform: translate(calc(-50% + var(--move-x) * 0.3), calc(-50% + var(--move-y) * 0.3)) scale(var(--initial-scale));
276
- opacity: 0.95;
277
- }
278
- 50% {
279
- transform: translate(calc(-50% + var(--move-x) * 0.6), calc(-50% + var(--move-y) * 0.6)) scale(calc(var(--initial-scale) * 1.3));
280
- opacity: 0.8;
281
- }
282
- 75% {
283
- transform: translate(calc(-50% + var(--move-x) * 0.9), calc(-50% + var(--move-y) * 0.9)) scale(calc(var(--initial-scale) * 0.7));
284
- opacity: 0.5;
285
- }
286
- 100% {
287
- transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y))) scale(0.3);
288
- opacity: 0;
289
- }
290
- }
291
-
292
- @keyframes statusPoison {
293
- 0% {
294
- transform: translate(-50%, -50%) scale(0.4);
295
- opacity: 0;
296
- }
297
- 20% {
298
- transform: translate(-50%, -50%) scale(1.1);
299
- opacity: 1;
300
- }
301
- 40% {
302
- transform: translate(-50%, -50%) scale(0.9);
303
- opacity: 0.8;
304
- }
305
- 60% {
306
- transform: translate(-50%, -50%) scale(1.0);
307
- opacity: 0.6;
308
- }
309
- 80% {
310
- transform: translate(-50%, -50%) scale(0.7);
311
- opacity: 0.3;
312
- }
313
- 100% {
314
- transform: translate(-50%, -50%) scale(0.5);
315
- opacity: 0;
316
- }
317
- }
318
-
319
- @keyframes statusParalyze {
320
- 0% {
321
- transform: translate(-50%, -50%) scale(0.2);
322
- opacity: 0;
323
- }
324
- 10% {
325
- transform: translate(-50%, -50%) scale(1.3);
326
- opacity: 1;
327
- }
328
- 20% {
329
- transform: translate(-50%, -50%) scale(1.1);
330
- opacity: 0.9;
331
- }
332
- 30% {
333
- transform: translate(-50%, -50%) scale(1.2);
334
- opacity: 0.8;
335
- }
336
- 40% {
337
- transform: translate(-50%, -50%) scale(1.0);
338
- opacity: 0.7;
339
- }
340
- 50% {
341
- transform: translate(-50%, -50%) scale(0.9);
342
- opacity: 0.6;
343
- }
344
- 100% {
345
- transform: translate(-50%, -50%) scale(0.3);
346
- opacity: 0;
347
- }
348
- }
349
-
350
- @keyframes statusSleep {
351
- 0% {
352
- transform: translate(-50%, -50%) scale(0.5);
353
- opacity: 0;
354
- }
355
- 25% {
356
- transform: translate(-50%, -55%) scale(1.1);
357
- opacity: 1;
358
- }
359
- 50% {
360
- transform: translate(-50%, -45%) scale(1.0);
361
- opacity: 0.9;
362
- }
363
- 75% {
364
- transform: translate(-50%, -55%) scale(0.9);
365
- opacity: 0.5;
366
- }
367
- 100% {
368
- transform: translate(-50%, -60%) scale(0.4);
369
- opacity: 0;
370
- }
371
- }
372
-
373
- @keyframes statusFreeze {
374
- 0% {
375
- transform: translate(-50%, -50%) scale(0.3);
376
- opacity: 0;
377
- }
378
- 30% {
379
- transform: translate(-50%, -50%) scale(1.4);
380
- opacity: 1;
381
- }
382
- 60% {
383
- transform: translate(-50%, -50%) scale(1.2);
384
- opacity: 0.8;
385
- }
386
- 90% {
387
- transform: translate(-50%, -50%) scale(0.8);
388
- opacity: 0.3;
389
- }
390
- 100% {
391
- transform: translate(-50%, -50%) scale(0.6);
392
- opacity: 0;
393
- }
394
- }
395
-
396
- @keyframes statIncrease {
397
- 0% {
398
- transform: translate(-50%, -50%) scale(0.3);
399
- opacity: 0;
400
- }
401
- 15% {
402
- transform: translate(-50%, -50%) scale(1.6);
403
- opacity: 1;
404
- }
405
- 30% {
406
- transform: translate(calc(-50% + var(--move-x) * 0.2), calc(-50% + var(--move-y) * 0.2 - 20px)) scale(calc(var(--initial-scale) * 1.2));
407
- opacity: 0.95;
408
- }
409
- 60% {
410
- transform: translate(calc(-50% + var(--move-x) * 0.7), calc(-50% + var(--move-y) * 0.7 - 60px)) scale(calc(var(--initial-scale) * 1.0));
411
- opacity: 0.7;
412
- }
413
- 85% {
414
- transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y) - 90px)) scale(calc(var(--initial-scale) * 0.6));
415
- opacity: 0.3;
416
- }
417
- 100% {
418
- transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y) - 120px)) scale(0.2);
419
- opacity: 0;
420
- }
421
- }
422
-
423
- @keyframes statDecrease {
424
- 0% {
425
- transform: translate(-50%, -50%) scale(0.4);
426
- opacity: 0;
427
- }
428
- 25% {
429
- transform: translate(-50%, -30%) scale(1.2);
430
- opacity: 1;
431
- }
432
- 50% {
433
- transform: translate(-50%, -10%) scale(1.0);
434
- opacity: 0.8;
435
- }
436
- 75% {
437
- transform: translate(-50%, 10%) scale(0.8);
438
- opacity: 0.4;
439
- }
440
- 100% {
441
- transform: translate(-50%, 30%) scale(0.6);
442
- opacity: 0;
443
- }
444
- }
445
-
446
- @keyframes criticalBurst {
447
- 0% {
448
- transform: translate(-50%, -50%) scale(0.1);
449
- opacity: 0;
450
- }
451
- 8% {
452
- transform: translate(-50%, -50%) scale(2.2);
453
- opacity: 1;
454
- }
455
- 20% {
456
- transform: translate(calc(-50% + var(--move-x) * 0.1), calc(-50% + var(--move-y) * 0.1)) scale(calc(var(--initial-scale) * 1.8));
457
- opacity: 0.95;
458
- }
459
- 40% {
460
- transform: translate(calc(-50% + var(--move-x) * 0.4), calc(-50% + var(--move-y) * 0.4)) scale(calc(var(--initial-scale) * 1.5));
461
- opacity: 0.8;
462
- }
463
- 70% {
464
- transform: translate(calc(-50% + var(--move-x) * 0.8), calc(-50% + var(--move-y) * 0.8)) scale(calc(var(--initial-scale) * 1.1));
465
- opacity: 0.4;
466
- }
467
- 100% {
468
- transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y))) scale(0.2);
469
- opacity: 0;
470
- }
471
- }
472
-
473
- @keyframes missSwirl {
474
- 0% {
475
- transform: translate(-50%, -50%) scale(0.6);
476
- opacity: 0;
477
- }
478
- 25% {
479
- transform: translate(-50%, -50%) scale(1.2);
480
- opacity: 0.7;
481
- }
482
- 50% {
483
- transform: translate(-50%, -50%) scale(1.0);
484
- opacity: 0.5;
485
- }
486
- 75% {
487
- transform: translate(-50%, -50%) scale(0.8);
488
- opacity: 0.3;
489
- }
490
- 100% {
491
- transform: translate(-50%, -50%) scale(0.4);
492
- opacity: 0;
493
- }
494
- }
495
-
496
- @keyframes healRise {
497
- 0% {
498
- transform: translate(-50%, -30%) scale(0.4);
499
- opacity: 0;
500
- }
501
- 12% {
502
- transform: translate(-50%, -35%) scale(1.5);
503
- opacity: 1;
504
- }
505
- 25% {
506
- transform: translate(calc(-50% + var(--move-x) * 0.2), calc(-50% + var(--move-y) * 0.2 - 40px)) scale(calc(var(--initial-scale) * 1.3));
507
- opacity: 0.95;
508
- }
509
- 50% {
510
- transform: translate(calc(-50% + var(--move-x) * 0.5), calc(-50% + var(--move-y) * 0.5 - 70px)) scale(calc(var(--initial-scale) * 1.1));
511
- opacity: 0.8;
512
- }
513
- 75% {
514
- transform: translate(calc(-50% + var(--move-x) * 0.8), calc(-50% + var(--move-y) * 0.8 - 100px)) scale(calc(var(--initial-scale) * 0.8));
515
- opacity: 0.5;
516
- }
517
- 100% {
518
- transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y) - 130px)) scale(0.3);
519
- opacity: 0;
520
- }
521
- }
522
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/BattleField.svelte DELETED
@@ -1,536 +0,0 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import { fade } from 'svelte/transition';
4
- import type { PicletInstance } from '$lib/db/schema';
5
- import type { BattleState } from '$lib/battle-engine/types';
6
- import PicletInfo from './PicletInfo.svelte';
7
- import StatusEffectIndicator from './StatusEffectIndicator.svelte';
8
- import FieldEffectIndicator from './FieldEffectIndicator.svelte';
9
- import BattleEffects from './BattleEffects.svelte';
10
-
11
- export let playerPiclet: PicletInstance;
12
- export let enemyPiclet: PicletInstance;
13
- export let playerHpPercentage: number;
14
- export let enemyHpPercentage: number;
15
- export let showIntro: boolean = false;
16
- export let battleState: BattleState | undefined = undefined;
17
- export let playerEffects: Array<{type: string, emoji: string, duration: number}> = [];
18
- export let enemyEffects: Array<{type: string, emoji: string, duration: number}> = [];
19
- export let playerFlash: boolean = false;
20
- export let enemyFlash: boolean = false;
21
- export let playerFaint: boolean = false;
22
- export let enemyFaint: boolean = false;
23
- export let playerLunge: boolean = false;
24
- export let enemyLunge: boolean = false;
25
- export let isWildBattle: boolean = true;
26
- export let showWhiteFlash: boolean = false;
27
- export let playerTrainerVisible: boolean = false;
28
- export let enemyTrainerVisible: boolean = false;
29
- export let playerTrainerSlideOut: boolean = false;
30
- export let enemyTrainerSlideOut: boolean = false;
31
-
32
- // Animation states
33
- let playerVisible = false;
34
- let enemyVisible = false;
35
-
36
- // Trainer animation states
37
- let playerTrainerSliding = false;
38
- let enemyTrainerSliding = false;
39
-
40
-
41
- onMount(() => {
42
- if (!showIntro) {
43
- // Skip intro - show everything immediately
44
- playerVisible = true;
45
- enemyVisible = true;
46
- } else {
47
- // In wild battles, enemy Piclet should be visible from start
48
- if (isWildBattle) {
49
- enemyVisible = true;
50
- }
51
- // For trainer battles, enemy will appear when enemyTrainerSlideOut triggers
52
- // For all battles, player will appear when playerTrainerSlideOut triggers
53
- }
54
- });
55
-
56
- // Watch for trainer slide-out triggers
57
- $: if (playerTrainerSlideOut && !playerTrainerSliding) {
58
- playerTrainerSliding = true;
59
- // Show trainer and let CSS handle slide-out animation
60
- playerTrainerVisible = true;
61
-
62
- // After slide animation completes, hide trainer and show Piclet
63
- setTimeout(() => {
64
- playerTrainerVisible = false;
65
- // Trigger white flash then show player monster
66
- showWhiteFlash = true;
67
- setTimeout(() => {
68
- showWhiteFlash = false;
69
- playerVisible = true;
70
- }, 300);
71
- }, 600); // Time for slide-out animation
72
- }
73
-
74
- $: if (enemyTrainerSlideOut && !enemyTrainerSliding) {
75
- enemyTrainerSliding = true;
76
- // Show trainer and let CSS handle slide-out animation
77
- enemyTrainerVisible = true;
78
-
79
- // After slide animation completes, hide trainer and show Piclet
80
- setTimeout(() => {
81
- enemyTrainerVisible = false;
82
- // Trigger white flash then show enemy monster
83
- showWhiteFlash = true;
84
- setTimeout(() => {
85
- showWhiteFlash = false;
86
- enemyVisible = true;
87
- }, 300);
88
- }, 600); // Time for slide-out animation
89
- }
90
- </script>
91
-
92
- <div class="battle-field">
93
- <!-- White flash overlay -->
94
- {#if showWhiteFlash}
95
- <div class="white-flash" transition:fade={{ duration: 300 }}></div>
96
- {/if}
97
-
98
-
99
- <!-- Player Trainer -->
100
- {#if playerTrainerVisible}
101
- <div class="player-trainer" class:slide-out-left={playerTrainerSlideOut}>
102
- <img src="/assets/default_trainer.png" alt="Player Trainer" />
103
- </div>
104
- {/if}
105
-
106
- <!-- Enemy Trainer (only for trainer battles) -->
107
- {#if !isWildBattle && enemyTrainerVisible}
108
- <div class="enemy-trainer" class:slide-out-right={enemyTrainerSlideOut}>
109
- <img src="/assets/default_trainer.png" alt="Enemy Trainer" />
110
- </div>
111
- {/if}
112
-
113
- <div class="battle-content">
114
- <!-- Field Effects Display -->
115
- {#if battleState?.fieldEffects}
116
- <FieldEffectIndicator fieldEffects={battleState.fieldEffects} />
117
- {/if}
118
-
119
- <!-- Enemy Row -->
120
- <div class="enemy-row">
121
- <div class="enemy-stack" class:intro-animations={showIntro}>
122
- <PicletInfo
123
- piclet={enemyPiclet}
124
- hpPercentage={enemyHpPercentage}
125
- isPlayer={false}
126
- />
127
-
128
- <!-- Static Enemy Platform (always visible) -->
129
- <img
130
- class="platform enemy-platform"
131
- src="/assets/grass.PNG"
132
- alt="Platform"
133
- on:error={(e) => {
134
- const target = e.currentTarget as HTMLImageElement;
135
- const nextSibling = target.nextElementSibling as HTMLElement;
136
- target.style.display = 'none';
137
- if (nextSibling) nextSibling.style.display = 'block';
138
- }}
139
- />
140
- <div class="platform-fallback enemy-platform-fallback" style="display: none;"></div>
141
-
142
- {#if enemyVisible}
143
- <div class="enemy-piclet-wrapper" class:animate-in={showIntro} class:lunge={enemyLunge}>
144
- <!-- Enemy Battle Effects wrap the image for flicker animation -->
145
- <BattleEffects effects={enemyEffects} flash={enemyFlash} faint={enemyFaint}>
146
- <img
147
- class="piclet-image enemy-image"
148
- src={enemyPiclet.imageData || enemyPiclet.imageUrl}
149
- alt={enemyPiclet.nickname}
150
- on:error={(e) => {
151
- const target = e.currentTarget as HTMLImageElement;
152
- target.src = 'https://via.placeholder.com/120x120?text=Piclet';
153
- }}
154
- />
155
- </BattleEffects>
156
-
157
- <!-- Enemy Status Effects -->
158
- {#if battleState?.opponentPiclet?.statusEffects}
159
- <div class="enemy-status-effects">
160
- <StatusEffectIndicator statusEffects={battleState.opponentPiclet.statusEffects.map(effect => ({ type: effect, turnsLeft: 3 }))} />
161
- </div>
162
- {/if}
163
- </div>
164
- {/if}
165
- </div>
166
- </div>
167
-
168
- <div class="spacer"></div>
169
-
170
- <!-- Player Row -->
171
- <div class="player-row">
172
- <div class="player-stack" class:intro-animations={showIntro}>
173
-
174
- <!-- Static Player Platform (always visible) -->
175
- <img
176
- class="platform player-platform"
177
- src="/assets/grass.PNG"
178
- alt="Platform"
179
- on:error={(e) => {
180
- const target = e.currentTarget as HTMLImageElement;
181
- const nextSibling = target.nextElementSibling as HTMLElement;
182
- target.style.display = 'none';
183
- if (nextSibling) nextSibling.style.display = 'block';
184
- }}
185
- />
186
- <div class="platform-fallback player-platform-fallback" style="display: none;"></div>
187
-
188
- {#if playerVisible}
189
- <div class="player-piclet-wrapper" class:animate-in={showIntro} class:lunge={playerLunge}>
190
- <!-- Player Battle Effects wrap the image for flicker animation -->
191
- <BattleEffects effects={playerEffects} flash={playerFlash} faint={playerFaint}>
192
- <img
193
- class="piclet-image player-image"
194
- src={playerPiclet.imageData || playerPiclet.imageUrl}
195
- alt={playerPiclet.nickname}
196
- on:error={(e) => {
197
- const target = e.currentTarget as HTMLImageElement;
198
- target.src = 'https://via.placeholder.com/120x120?text=Piclet';
199
- }}
200
- />
201
- </BattleEffects>
202
-
203
- <!-- Player Status Effects -->
204
- {#if battleState?.playerPiclet?.statusEffects}
205
- <div class="player-status-effects">
206
- <StatusEffectIndicator statusEffects={battleState.playerPiclet.statusEffects.map(effect => ({ type: effect, turnsLeft: 3 }))} />
207
- </div>
208
- {/if}
209
- </div>
210
- {/if}
211
-
212
- <PicletInfo
213
- piclet={playerPiclet}
214
- hpPercentage={playerHpPercentage}
215
- isPlayer={true}
216
- />
217
- </div>
218
- </div>
219
- </div>
220
- </div>
221
-
222
- <style>
223
- .battle-field {
224
- height: 280px;
225
- position: relative;
226
- overflow: hidden;
227
- background: repeating-linear-gradient(
228
- to bottom,
229
- rgba(76, 175, 80, 0.2) 0px,
230
- rgba(76, 175, 80, 0.2) 5px,
231
- rgba(76, 175, 80, 0.1) 5px,
232
- rgba(76, 175, 80, 0.1) 10px
233
- );
234
- }
235
-
236
- .white-flash {
237
- position: fixed;
238
- top: 0;
239
- left: 0;
240
- right: 0;
241
- bottom: 0;
242
- background: white;
243
- z-index: 50;
244
- }
245
-
246
-
247
- .player-trainer {
248
- position: absolute;
249
- bottom: 20px;
250
- left: 20px;
251
- z-index: 10;
252
- transition: transform 0.6s ease-in-out;
253
- }
254
-
255
- .player-trainer img {
256
- width: 120px;
257
- height: auto;
258
- }
259
-
260
- .player-trainer.slide-out-left {
261
- transform: translateX(-200px);
262
- }
263
-
264
- .enemy-trainer {
265
- position: absolute;
266
- top: 20px;
267
- right: 20px;
268
- z-index: 10;
269
- transition: transform 0.6s ease-in-out;
270
- }
271
-
272
- .enemy-trainer img {
273
- width: 120px;
274
- height: auto;
275
- }
276
-
277
- .enemy-trainer.slide-out-right {
278
- transform: translateX(200px);
279
- }
280
-
281
-
282
- .battle-content {
283
- display: flex;
284
- flex-direction: column;
285
- height: 100%;
286
- }
287
-
288
- /* Enemy Row */
289
- .enemy-row {
290
- flex: 1;
291
- position: relative;
292
- }
293
-
294
- .enemy-stack {
295
- position: absolute;
296
- top: 0;
297
- right: 0;
298
- left: 0;
299
- bottom: 0;
300
- }
301
-
302
- .enemy-piclet-wrapper {
303
- position: absolute;
304
- right: 40px;
305
- top: 0;
306
- }
307
-
308
- .enemy-image {
309
- width: 120px;
310
- height: 120px;
311
- object-fit: contain;
312
- display: block;
313
- }
314
-
315
- .enemy-platform {
316
- width: 160px;
317
- height: 160px;
318
- position: absolute;
319
- right: 20px; /* Align with enemy-piclet-wrapper position */
320
- z-index: 0;
321
- object-fit: cover;
322
- }
323
-
324
- /* Player Row */
325
- .player-row {
326
- height: 140px;
327
- position: relative;
328
- }
329
-
330
- .player-stack {
331
- position: relative;
332
- width: 100%;
333
- height: 100%;
334
- }
335
-
336
- .player-piclet-wrapper {
337
- position: absolute;
338
- left: 40px;
339
- bottom: -6px;
340
- }
341
-
342
- .player-image {
343
- width: 120px;
344
- height: 120px;
345
- object-fit: contain;
346
- display: block;
347
- }
348
-
349
- .player-platform {
350
- width: 160px;
351
- height: 160px;
352
- position: absolute;
353
- bottom: -80px;
354
- left: 20px; /* Align with player-piclet-wrapper position */
355
- z-index: 0;
356
- object-fit: cover;
357
- }
358
-
359
- /* Platform fallbacks */
360
- .platform-fallback {
361
- position: absolute;
362
- background: rgba(76, 175, 80, 0.3);
363
- border-radius: 50%;
364
- }
365
-
366
- .enemy-platform-fallback {
367
- width: 160px;
368
- height: 160px;
369
- bottom: -60px;
370
- right: 20px;
371
- }
372
-
373
- .player-platform-fallback {
374
- width: 160px;
375
- height: 160px;
376
- bottom: -80px;
377
- left: 20px;
378
- }
379
-
380
- /* Piclet images */
381
- .piclet-image {
382
- image-rendering: auto;
383
- filter: drop-shadow(-2px 0 4px rgba(0, 0, 0, 0.1));
384
- position: relative;
385
- z-index: 1;
386
- }
387
-
388
- .spacer {
389
- flex: 1;
390
- }
391
-
392
- /* Status Effects Positioning */
393
- .enemy-status-effects {
394
- position: absolute;
395
- top: -10px;
396
- right: -10px;
397
- z-index: 5;
398
- }
399
-
400
- .player-status-effects {
401
- position: absolute;
402
- bottom: 117px;
403
- left: 13px;
404
- z-index: 5;
405
- }
406
-
407
- /* Animations */
408
- .enemy-piclet-wrapper {
409
- animation-fill-mode: both;
410
- transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
411
- }
412
-
413
- .enemy-piclet-wrapper.animate-in {
414
- animation: enemySlideIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
415
- }
416
-
417
- .enemy-piclet-wrapper.lunge {
418
- animation: enemyLunge 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
419
- }
420
-
421
- .player-piclet-wrapper {
422
- animation-fill-mode: both;
423
- transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
424
- }
425
-
426
- .player-piclet-wrapper.animate-in {
427
- animation: playerSlideIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
428
- }
429
-
430
- .player-piclet-wrapper.lunge {
431
- animation: playerLunge 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
432
- }
433
-
434
- @keyframes enemySlideIn {
435
- 0% {
436
- transform: translateX(150px) translateY(-50px) scale(1.5);
437
- opacity: 0;
438
- }
439
- 50% {
440
- opacity: 1;
441
- }
442
- 100% {
443
- transform: translateX(0) translateY(0) scale(1);
444
- opacity: 1;
445
- }
446
- }
447
-
448
- @keyframes playerSlideIn {
449
- 0% {
450
- transform: translateX(-150px) translateY(50px) scale(0.5);
451
- opacity: 0;
452
- }
453
- 50% {
454
- opacity: 1;
455
- }
456
- 100% {
457
- transform: translateX(0) translateY(0) scale(1);
458
- opacity: 1;
459
- }
460
- }
461
-
462
- @keyframes enemyLunge {
463
- 0% {
464
- transform: translateX(0) translateY(0) scale(1);
465
- }
466
- 40% {
467
- transform: translateX(-60px) translateY(10px) scale(1.1);
468
- }
469
- 100% {
470
- transform: translateX(0) translateY(0) scale(1);
471
- }
472
- }
473
-
474
- @keyframes playerLunge {
475
- 0% {
476
- transform: translateX(0) translateY(0) scale(1);
477
- }
478
- 40% {
479
- transform: translateX(60px) translateY(-10px) scale(1.1);
480
- }
481
- 100% {
482
- transform: translateX(0) translateY(0) scale(1);
483
- }
484
- }
485
-
486
- /* Info box animations */
487
- .enemy-stack.intro-animations :global(.piclet-info-wrapper) {
488
- animation: fadeSlideDown 0.5s ease-out 0.3s both;
489
- }
490
-
491
- .player-stack.intro-animations :global(.piclet-info-wrapper) {
492
- animation: fadeSlideUp 0.5s ease-out 0.3s both;
493
- }
494
-
495
- @keyframes fadeSlideDown {
496
- 0% {
497
- opacity: 0;
498
- transform: translateY(-20px);
499
- }
500
- 100% {
501
- opacity: 1;
502
- transform: translateY(0);
503
- }
504
- }
505
-
506
- @keyframes fadeSlideUp {
507
- 0% {
508
- opacity: 0;
509
- transform: translateY(20px);
510
- }
511
- 100% {
512
- opacity: 1;
513
- transform: translateY(0);
514
- }
515
- }
516
-
517
- @media (max-width: 768px) {
518
- .enemy-image {
519
- width: 120px;
520
- height: 120px;
521
- }
522
-
523
- .player-image {
524
- width: 120px;
525
- height: 120px;
526
- }
527
-
528
- .enemy-piclet-wrapper {
529
- right: 40px;
530
- }
531
-
532
- .player-piclet-wrapper {
533
- left: 40px;
534
- }
535
- }
536
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Battle/LLMBattleEngine.svelte ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ commandClient: GradioClient;
19
+ onBattleEnd: (winner: 'player' | 'enemy') => void;
20
+ rosterPiclets?: PicletInstance[]; // Optional roster for switching
21
+ }
22
+
23
+ let { playerPiclet, enemyPiclet, commandClient, 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 Command client
56
+ async function generateBattleUpdate(prompt: string): Promise<BattleUpdate> {
57
+ const message = {
58
+ text: prompt,
59
+ files: []
60
+ };
61
+
62
+ const result = await commandClient.predict("/chat", {
63
+ message: message,
64
+ max_new_tokens: 1000
65
+ });
66
+
67
+ const responseText = result.data || '';
68
+ console.log('LLM Response:', responseText);
69
+
70
+ // Extract JSON from response
71
+ try {
72
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
73
+ if (!jsonMatch) throw new Error('No JSON found in response');
74
+
75
+ const battleUpdate: BattleUpdate = JSON.parse(jsonMatch[0]);
76
+ return battleUpdate;
77
+ } catch (error) {
78
+ console.error('Failed to parse battle response:', error);
79
+ // Fallback response
80
+ return {
81
+ battle_updates: ['The battle continues...'],
82
+ player_pokemon_status: battleState.player_pokemon_status,
83
+ player_pokemon_hp: battleState.player_pokemon_hp,
84
+ enemy_pokemon_status: battleState.enemy_pokemon_status,
85
+ enemy_pokemon_hp: battleState.enemy_pokemon_hp,
86
+ next_to_act: battleState.next_to_act === 'player' ? 'enemy' : 'player',
87
+ available_actions: ['Attack', 'Defend', 'Special Move']
88
+ };
89
+ }
90
+ }
91
+
92
+ // Initialize battle
93
+ async function startBattle() {
94
+ isProcessing = true;
95
+
96
+ 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}.
97
+ You will return a brief description on what happens because of the action.
98
+ 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.
99
+
100
+ Player Pokemon: ${currentPlayerPiclet.typeId}
101
+ ${currentPlayerPiclet.description}
102
+
103
+ Enemy Pokemon: ${enemyPiclet.typeId}
104
+ ${enemyPiclet.description}
105
+
106
+ Each response should be a json object with fields:
107
+ \`\`\`json
108
+ {
109
+ "battle_updates": [list with 1 sentence per entry describing what just happened in battle],
110
+ "player_pokemon_status": "1 sentence description of how the Pokemon is doing",
111
+ "player_pokemon_hp": "enum Empty, Very Low, Low, Medium, High, Very High, Full",
112
+ "enemy_pokemon_status": "1 sentence description of how the Pokemon is doing",
113
+ "enemy_pokemon_hp": "enum Empty, Very Low, Low, Medium, High, Very High, Full",
114
+ "next_to_act": "enum player/enemy",
115
+ "available_actions": ["short list of 1 sentence actions of what to have the next_to_act Pokemon do next"]
116
+ }
117
+ \`\`\`
118
+ Start with just some intro updates describing both monsters being on the battlefield.`;
119
+
120
+ try {
121
+ const update = await generateBattleUpdate(initialPrompt);
122
+ battleState = update;
123
+ battleHistory.push(initialPrompt);
124
+ } catch (error) {
125
+ console.error('Failed to start battle:', error);
126
+ }
127
+
128
+ isProcessing = false;
129
+ }
130
+
131
+ // Execute player action
132
+ async function executeAction(actionDescription: string) {
133
+ if (isProcessing) return;
134
+
135
+ isProcessing = true;
136
+
137
+ const roll = rollDice();
138
+ const effectiveness = getActionEffectiveness(roll);
139
+
140
+ const prompt = `Player chooses: "${actionDescription}"
141
+ Dice roll: ${roll}/20 (${effectiveness.success})
142
+ Effect: ${effectiveness.description}
143
+
144
+ Update the battle state based on this action and its effectiveness. Then have the enemy take their turn if appropriate.`;
145
+
146
+ try {
147
+ const update = await generateBattleUpdate(prompt);
148
+ battleState = update;
149
+ battleHistory.push(prompt);
150
+
151
+ // Check for battle end conditions
152
+ if (battleState.player_pokemon_hp === 'Empty') {
153
+ onBattleEnd('enemy');
154
+ } else if (battleState.enemy_pokemon_hp === 'Empty') {
155
+ onBattleEnd('player');
156
+ }
157
+ } catch (error) {
158
+ console.error('Failed to execute action:', error);
159
+ }
160
+
161
+ isProcessing = false;
162
+ }
163
+
164
+ // Switch Piclet function
165
+ async function switchPiclet(newPiclet: PicletInstance) {
166
+ if (isProcessing) return;
167
+
168
+ isProcessing = true;
169
+ showPicletSelector = false;
170
+
171
+ const switchPrompt = `Player switches from ${currentPlayerPiclet.typeId} to ${newPiclet.typeId}!
172
+
173
+ New Pokemon: ${newPiclet.typeId}
174
+ ${newPiclet.description}
175
+
176
+ Update the battle to show the switch and have the enemy react accordingly.`;
177
+
178
+ try {
179
+ currentPlayerPiclet = newPiclet;
180
+ const update = await generateBattleUpdate(switchPrompt);
181
+ battleState = update;
182
+ battleHistory.push(switchPrompt);
183
+ } catch (error) {
184
+ console.error('Failed to switch Piclet:', error);
185
+ }
186
+
187
+ isProcessing = false;
188
+ }
189
+
190
+ // Auto-start battle when component mounts
191
+ $effect(() => {
192
+ startBattle();
193
+ });
194
+
195
+ // Export functions for parent component
196
+ export { executeAction, switchPiclet };
197
+ </script>
198
+
199
+ <div class="llm-battle-engine">
200
+ <!-- Battle Narrative Display -->
201
+ <div class="battle-narrative">
202
+ <h3>Battle Progress</h3>
203
+ {#each battleState.battle_updates as update}
204
+ <div class="battle-update">{update}</div>
205
+ {/each}
206
+ </div>
207
+
208
+ <!-- Pokemon Status -->
209
+ <div class="pokemon-status">
210
+ <div class="player-status">
211
+ <h4>{currentPlayerPiclet.typeId}</h4>
212
+ <div class="hp-indicator hp-{battleState.player_pokemon_hp.toLowerCase().replace(' ', '-')}">{battleState.player_pokemon_hp}</div>
213
+ <p>{battleState.player_pokemon_status}</p>
214
+ </div>
215
+
216
+ <div class="enemy-status">
217
+ <h4>{enemyPiclet.typeId}</h4>
218
+ <div class="hp-indicator hp-{battleState.enemy_pokemon_hp.toLowerCase().replace(' ', '-')}">{battleState.enemy_pokemon_hp}</div>
219
+ <p>{battleState.enemy_pokemon_status}</p>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Available Actions (when it's player's turn) -->
224
+ {#if battleState.next_to_act === 'player' && !isProcessing}
225
+ <div class="available-actions">
226
+ <h4>Choose Your Action:</h4>
227
+
228
+ <!-- Battle Actions -->
229
+ {#each battleState.available_actions as action}
230
+ <button
231
+ class="action-button"
232
+ onclick={() => executeAction(action)}
233
+ >
234
+ {action}
235
+ </button>
236
+ {/each}
237
+
238
+ <!-- Piclet Switching -->
239
+ {#if rosterPiclets && rosterPiclets.length > 1}
240
+ <button
241
+ class="switch-button"
242
+ onclick={() => showPicletSelector = !showPicletSelector}
243
+ >
244
+ 🔄 Switch Piclet
245
+ </button>
246
+ {/if}
247
+ </div>
248
+
249
+ <!-- Piclet Selector -->
250
+ {#if showPicletSelector && rosterPiclets}
251
+ <div class="piclet-selector">
252
+ <h4>Choose Piclet:</h4>
253
+ <div class="piclet-grid">
254
+ {#each rosterPiclets as piclet}
255
+ {#if piclet.id !== currentPlayerPiclet.id}
256
+ <button
257
+ class="piclet-option"
258
+ onclick={() => switchPiclet(piclet)}
259
+ >
260
+ <img src={piclet.imageUrl} alt={piclet.typeId} />
261
+ <span>{piclet.typeId}</span>
262
+ <span class="tier tier-{piclet.tier}">{piclet.tier}</span>
263
+ </button>
264
+ {/if}
265
+ {/each}
266
+ </div>
267
+ </div>
268
+ {/if}
269
+ {:else if isProcessing}
270
+ <div class="processing">
271
+ <div class="spinner"></div>
272
+ <p>Processing battle turn...</p>
273
+ </div>
274
+ {:else}
275
+ <div class="enemy-turn">
276
+ <p>Enemy is deciding their move...</p>
277
+ </div>
278
+ {/if}
279
+ </div>
280
+
281
+ <style>
282
+ .llm-battle-engine {
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: 1rem;
286
+ padding: 1rem;
287
+ }
288
+
289
+ .battle-narrative {
290
+ background: #f8f9fa;
291
+ border-radius: 8px;
292
+ padding: 1rem;
293
+ max-height: 200px;
294
+ overflow-y: auto;
295
+ }
296
+
297
+ .battle-update {
298
+ margin-bottom: 0.5rem;
299
+ padding: 0.5rem;
300
+ background: white;
301
+ border-radius: 4px;
302
+ border-left: 3px solid #007bff;
303
+ }
304
+
305
+ .pokemon-status {
306
+ display: grid;
307
+ grid-template-columns: 1fr 1fr;
308
+ gap: 1rem;
309
+ }
310
+
311
+ .player-status, .enemy-status {
312
+ padding: 1rem;
313
+ border-radius: 8px;
314
+ text-align: center;
315
+ }
316
+
317
+ .player-status {
318
+ background: rgba(0, 123, 255, 0.1);
319
+ border: 2px solid #007bff;
320
+ }
321
+
322
+ .enemy-status {
323
+ background: rgba(220, 53, 69, 0.1);
324
+ border: 2px solid #dc3545;
325
+ }
326
+
327
+ .hp-indicator {
328
+ font-weight: bold;
329
+ padding: 0.25rem 0.5rem;
330
+ border-radius: 16px;
331
+ margin: 0.5rem 0;
332
+ display: inline-block;
333
+ }
334
+
335
+ .hp-full { background: #28a745; color: white; }
336
+ .hp-very-high { background: #40c757; color: white; }
337
+ .hp-high { background: #6bc267; color: white; }
338
+ .hp-medium { background: #ffc107; color: black; }
339
+ .hp-low { background: #fd7e14; color: white; }
340
+ .hp-very-low { background: #dc3545; color: white; }
341
+ .hp-empty { background: #6c757d; color: white; }
342
+
343
+ .available-actions {
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 0.5rem;
347
+ }
348
+
349
+ .action-button {
350
+ padding: 0.75rem 1rem;
351
+ background: #007bff;
352
+ color: white;
353
+ border: none;
354
+ border-radius: 8px;
355
+ cursor: pointer;
356
+ font-size: 1rem;
357
+ transition: background-color 0.2s;
358
+ }
359
+
360
+ .action-button:hover {
361
+ background: #0056b3;
362
+ }
363
+
364
+ .switch-button {
365
+ padding: 0.75rem 1rem;
366
+ background: #28a745;
367
+ color: white;
368
+ border: none;
369
+ border-radius: 8px;
370
+ cursor: pointer;
371
+ font-size: 1rem;
372
+ transition: background-color 0.2s;
373
+ margin-top: 0.5rem;
374
+ }
375
+
376
+ .switch-button:hover {
377
+ background: #1e7e34;
378
+ }
379
+
380
+ .piclet-selector {
381
+ background: #f8f9fa;
382
+ border-radius: 8px;
383
+ padding: 1rem;
384
+ margin-top: 1rem;
385
+ }
386
+
387
+ .piclet-grid {
388
+ display: grid;
389
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
390
+ gap: 0.5rem;
391
+ margin-top: 0.5rem;
392
+ }
393
+
394
+ .piclet-option {
395
+ display: flex;
396
+ flex-direction: column;
397
+ align-items: center;
398
+ gap: 0.25rem;
399
+ padding: 0.5rem;
400
+ background: white;
401
+ border: 2px solid #dee2e6;
402
+ border-radius: 8px;
403
+ cursor: pointer;
404
+ transition: all 0.2s;
405
+ }
406
+
407
+ .piclet-option:hover {
408
+ border-color: #007bff;
409
+ background: #f0f7ff;
410
+ }
411
+
412
+ .piclet-option img {
413
+ width: 40px;
414
+ height: 40px;
415
+ object-fit: cover;
416
+ border-radius: 4px;
417
+ }
418
+
419
+ .piclet-option span {
420
+ font-size: 0.8rem;
421
+ text-align: center;
422
+ }
423
+
424
+ .tier {
425
+ padding: 0.1rem 0.3rem;
426
+ border-radius: 8px;
427
+ font-size: 0.7rem;
428
+ font-weight: bold;
429
+ text-transform: uppercase;
430
+ }
431
+
432
+ .tier-low { background: #6c757d; color: white; }
433
+ .tier-medium { background: #28a745; color: white; }
434
+ .tier-high { background: #fd7e14; color: white; }
435
+ .tier-legendary { background: #dc3545; color: white; }
436
+
437
+ .processing, .enemy-turn {
438
+ text-align: center;
439
+ padding: 2rem;
440
+ }
441
+
442
+ .spinner {
443
+ width: 40px;
444
+ height: 40px;
445
+ border: 4px solid #f3f3f3;
446
+ border-top: 4px solid #007bff;
447
+ border-radius: 50%;
448
+ animation: spin 1s linear infinite;
449
+ margin: 0 auto 1rem;
450
+ }
451
+
452
+ @keyframes spin {
453
+ to { transform: rotate(360deg); }
454
+ }
455
+ </style>
src/lib/components/Pages/Battle.svelte CHANGED
@@ -1,1036 +1,177 @@
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
3
  import { fade } from 'svelte/transition';
4
- import type { PicletInstance, BattleMove } from '$lib/db/schema';
5
- import BattleField from '../Battle/BattleField.svelte';
6
- import BattleControls from '../Battle/BattleControls.svelte';
7
- import { BattleEngine } from '$lib/battle-engine/BattleEngine';
8
- import type { BattleState, MoveAction, SwitchAction } from '$lib/battle-engine/types';
9
- import { picletInstanceToBattleDefinition, battlePicletToInstance, stripBattlePrefix } from '$lib/utils/battleConversion';
10
- import { calculateBattleXp, processAllLevelUps } from '$lib/services/levelingService';
11
- import { db } from '$lib/db/index';
12
- import { getEffectivenessText, getEffectivenessColor } from '$lib/types/picletTypes';
13
- import { getCaptureDescription } from '$lib/services/captureService';
14
-
15
- export let playerPiclet: PicletInstance;
16
- export let enemyPiclet: PicletInstance;
17
- export let isWildBattle: boolean = true;
18
- export let onBattleEnd: (result: any) => void = () => {};
19
- export let rosterPiclets: PicletInstance[] = []; // All roster piclets passed from parent
20
-
21
- // Initialize battle engine
22
- let battleEngine: BattleEngine;
23
- let battleState: BattleState;
24
- let currentPlayerPiclet = playerPiclet;
25
- let currentEnemyPiclet = enemyPiclet;
26
-
27
- // Calculate capture percentage for UI display
28
- $: capturePercentage = battleEngine && isWildBattle ? battleEngine.getCapturePercentage() : 0;
29
-
30
- // Battle state
31
- let currentMessage = isWildBattle
32
- ? `A wild ${enemyPiclet.nickname} appeared!`
33
- : `Trainer wants to battle!`;
34
- let battlePhase: 'intro' | 'main' | 'moveSelect' | 'picletSelect' | 'ended' = 'intro';
35
- let processingTurn = false;
36
- let battleEnded = false;
37
-
38
- // Trainer animation states
39
- let showWhiteFlash = false;
40
- let playerTrainerVisible = false;
41
- let enemyTrainerVisible = false;
42
- let playerTrainerSlideOut = false;
43
- let enemyTrainerSlideOut = false;
44
-
45
- // Message progression system
46
- let waitingForContinue = false;
47
- let messageQueue: string[] = [];
48
- let currentMessageIndex = 0;
49
- let continueCallback: (() => void) | null = null;
50
-
51
- // HP animation states
52
- let playerHpPercentage = playerPiclet.currentHp / playerPiclet.maxHp;
53
- let enemyHpPercentage = enemyPiclet.currentHp / enemyPiclet.maxHp;
54
-
55
- // Visual effects state
56
- let playerEffects: Array<{type: string, emoji: string, duration: number}> = [];
57
- let enemyEffects: Array<{type: string, emoji: string, duration: number}> = [];
58
- let playerFlash = false;
59
- let enemyFlash = false;
60
- let playerFaint = false;
61
- let enemyFaint = false;
62
- let playerLunge = false;
63
- let enemyLunge = false;
64
-
65
- // Battle results state
66
- let battleResultsVisible = false;
67
- let battleResults = {
68
- victory: false,
69
- xpGained: 0,
70
- levelUps: [],
71
- newLevel: 0
72
- };
73
-
74
-
75
- onMount(() => {
76
- // Initialize battle engine with converted piclet definitions
77
- // Convert full roster for switching support
78
- const playerRosterDefinitions = rosterPiclets.map(p => picletInstanceToBattleDefinition(p));
79
- const enemyDefinition = picletInstanceToBattleDefinition(enemyPiclet);
80
-
81
- // Find the starting player piclet index in the roster
82
- const startingPlayerIndex = rosterPiclets.findIndex(p => p.id === playerPiclet.id);
83
-
84
- // Initialize with full rosters (player roster vs single enemy)
85
- battleEngine = new BattleEngine(playerRosterDefinitions, enemyDefinition, playerPiclet.level, enemyPiclet.level);
86
-
87
- // If starting piclet is not the first in roster, switch to it
88
- if (startingPlayerIndex > 0) {
89
- const initialSwitchAction: SwitchAction = {
90
- type: 'switch',
91
- piclet: 'player',
92
- newPicletIndex: startingPlayerIndex
93
- };
94
- battleEngine.executeAction(initialSwitchAction, 'player');
95
- }
96
-
97
- battleState = battleEngine.getState();
98
-
99
- // Start intro sequence
100
- setTimeout(() => {
101
- if (!isWildBattle) {
102
- // Enemy trainer sends out first
103
- currentMessage = `Go, ${enemyPiclet.nickname}!`;
104
- enemyTrainerSlideOut = true;
105
- setTimeout(() => {
106
- currentMessage = `Go, ${playerPiclet.nickname}!`;
107
- playerTrainerSlideOut = true;
108
- setTimeout(() => {
109
- currentMessage = `What will ${playerPiclet.nickname} do?`;
110
- battlePhase = 'main';
111
- }, 2000); // Wait for trainer slide + flash + monster appear
112
- }, 2000); // Wait for enemy trainer sequence
113
- } else {
114
- // Wild battle - player sends out
115
- currentMessage = `Go, ${playerPiclet.nickname}!`;
116
- playerTrainerSlideOut = true;
117
- setTimeout(() => {
118
- currentMessage = `What will ${playerPiclet.nickname} do?`;
119
- battlePhase = 'main';
120
- }, 2000); // Wait for trainer slide + flash + monster appear
121
- }
122
- }, 2000);
123
- });
124
-
125
- function handleAction(action: string) {
126
- if (processingTurn || battleEnded) return;
127
-
128
- switch (action) {
129
- case 'catch':
130
- if (isWildBattle && battleEngine) {
131
- processingTurn = true;
132
-
133
- // Get capture percentage to show to player
134
- const capturePercentage = battleEngine.getCapturePercentage();
135
- const captureDescription = getCaptureDescription(capturePercentage);
136
-
137
- console.log(`📸 Capture attempt: ${capturePercentage.toFixed(1)}% chance (${captureDescription})`);
138
-
139
- try {
140
- // Create capture action
141
- const captureAction = { type: 'capture' as const, piclet: 'player' as const };
142
- // Get proper enemy action (same as normal turns)
143
- const enemyAction = selectEnemyMove();
144
- if (!enemyAction) {
145
- processingTurn = false;
146
- return;
147
- }
148
-
149
- // Get log entries before action to track new messages
150
- const logBefore = battleEngine.getLog();
151
-
152
- // Execute capture attempt
153
- battleEngine.executeActions(captureAction, enemyAction);
154
- battleState = battleEngine.getState();
155
-
156
- // Update UI state (HP bars, etc.) after the actions
157
- updateUIFromBattleState();
158
-
159
- // Get capture result and new log entries
160
- const captureResult = battleState.captureResult;
161
- const logAfter = battleEngine.getLog();
162
- const newLogEntries = logAfter.slice(logBefore.length);
163
-
164
- // Show log messages with proper timing and visual effects
165
- if (newLogEntries.length > 0) {
166
- let messageIndex = 0;
167
-
168
- const showNextMessage = () => {
169
- if (messageIndex < newLogEntries.length) {
170
- currentMessage = newLogEntries[messageIndex];
171
- // Trigger visual effects for this message
172
- triggerVisualEffectsFromMessage(newLogEntries[messageIndex]);
173
- messageIndex++;
174
- setTimeout(showNextMessage, 1500); // 1.5s between messages
175
- } else {
176
- // All messages shown, check final result
177
- if (captureResult?.success) {
178
- // Capture successful - end battle and add to roster
179
- setTimeout(() => {
180
- battleEnded = true;
181
- onBattleEnd(currentEnemyPiclet); // Pass captured Piclet to add to roster
182
- }, 1000);
183
- } else if (battleState.winner) {
184
- // Battle ended (player probably fainted from enemy attack)
185
- battleEnded = true;
186
- const defeatedPiclet = battleState.winner === 'player' ? currentEnemyPiclet : currentPlayerPiclet;
187
-
188
- // Show the faint message and trigger animation
189
- currentMessage = `${defeatedPiclet.nickname} fainted!`;
190
-
191
- // Trigger faint animation for the defeated Piclet
192
- if (battleState.winner === 'player') {
193
- enemyFaint = true;
194
- } else {
195
- playerFaint = true;
196
- }
197
-
198
- // Wait for faint message, then process battle results
199
- setTimeout(async () => {
200
- await handleBattleResults(battleState.winner === 'player');
201
- }, 2500); // Wait time for faint message and animation
202
- } else {
203
- // Capture failed - continue battle
204
- setTimeout(() => {
205
- processingTurn = false;
206
- currentMessage = `What will ${currentPlayerPiclet.nickname} do?`;
207
- }, 1000);
208
- }
209
- }
210
- };
211
-
212
- showNextMessage();
213
- } else {
214
- // No messages, fall back to basic handling
215
- currentMessage = 'The capture attempt failed!';
216
- setTimeout(() => {
217
- processingTurn = false;
218
- }, 2000);
219
- }
220
-
221
- } catch (error) {
222
- console.error('Capture error:', error);
223
- currentMessage = 'Something went wrong with the capture attempt!';
224
- processingTurn = false;
225
- }
226
- } else if (!isWildBattle) {
227
- currentMessage = "You can't capture a trainer's Piclet!";
228
- }
229
- break;
230
- case 'run':
231
- if (isWildBattle) {
232
- currentMessage = 'Got away safely!';
233
- battleEnded = true;
234
- setTimeout(() => onBattleEnd(false), 1500);
235
- } else {
236
- currentMessage = "You can't run from a trainer battle!";
237
- }
238
- break;
239
- }
240
- }
241
-
242
- function handleMoveSelect(move: BattleMove) {
243
- if (!battleEngine) return;
244
-
245
- battlePhase = 'main';
246
- processingTurn = true;
247
-
248
- // Find the corresponding move in the battle engine
249
- const battleMove = battleState.playerPiclet.moves.find(m => m.move.name === move.name);
250
- if (!battleMove) return;
251
-
252
- const moveAction: MoveAction = {
253
- type: 'move',
254
- piclet: 'player',
255
- moveIndex: battleState.playerPiclet.moves.indexOf(battleMove)
256
- };
257
-
258
- try {
259
- // Select enemy move (wild Piclets always random, trainers could use AI)
260
- const enemyAction = selectEnemyMove();
261
- if (!enemyAction) {
262
- processingTurn = false;
263
- return;
264
- }
265
-
266
- // Get log entries before action to track new messages
267
- const logBefore = battleEngine.getLog();
268
-
269
- // Execute the turn - battle engine handles priority automatically
270
- battleEngine.executeActions(moveAction, enemyAction);
271
- battleState = battleEngine.getState();
272
-
273
- // Get only the new log entries from this turn
274
- const logAfter = battleEngine.getLog();
275
- const newLogEntries = logAfter.slice(logBefore.length);
276
- // Filter out faint messages since we handle them manually for proper sequencing
277
- const filteredLogEntries = newLogEntries.filter(message => !message.includes('fainted'));
278
- const result = { log: filteredLogEntries };
279
-
280
- // Show battle messages with tap-to-continue system
281
- if (result.log && result.log.length > 0) {
282
- showMessageSequence(result.log, finalizeTurn);
283
- } else {
284
- finalizeTurn();
285
- }
286
-
287
- function finalizeTurn() {
288
- // Update UI state from battle engine
289
- updateUIFromBattleState();
290
-
291
- // Check for battle end - only show faint message
292
- if (battleState.winner) {
293
- battleEnded = true;
294
- const defeatedPiclet = battleState.winner === 'player' ? currentEnemyPiclet : currentPlayerPiclet;
295
-
296
- // Show the faint message and trigger animation
297
- currentMessage = `${defeatedPiclet.nickname} fainted!`;
298
-
299
- // Trigger faint animation for the defeated Piclet
300
- if (battleState.winner === 'player') {
301
- enemyFaint = true;
302
- } else {
303
- playerFaint = true;
304
- }
305
-
306
- // Wait for faint message, then process battle results
307
- setTimeout(async () => {
308
- await handleBattleResults(battleState.winner === 'player');
309
- }, 2500); // Wait time for faint message and animation
310
- } else {
311
- // Check if player Piclet switched due to fainting
312
- const newPlayerPiclet = battlePicletToInstance(battleState.playerPiclet, currentPlayerPiclet);
313
- const playerPicletChanged = currentPlayerPiclet.id !== newPlayerPiclet.id;
314
-
315
- if (playerPicletChanged) {
316
- // Player Piclet fainted and auto-switched - show faint message first
317
- const faintedPiclet = currentPlayerPiclet;
318
- currentMessage = `${faintedPiclet.nickname} fainted!`;
319
- playerFaint = true;
320
-
321
- // Wait for faint message, then show switch and continue
322
- setTimeout(() => {
323
- currentMessage = `Go, ${newPlayerPiclet.nickname}!`;
324
- // updateUIFromBattleState will handle the white flash transition
325
- setTimeout(() => {
326
- currentMessage = `What will ${currentPlayerPiclet.nickname} do?`;
327
- processingTurn = false;
328
- }, 1000);
329
- }, 2500);
330
- } else {
331
- // Normal turn end - no faint or switch
332
- setTimeout(() => {
333
- currentMessage = `What will ${currentPlayerPiclet.nickname} do?`;
334
- processingTurn = false;
335
- }, 1000);
336
- }
337
- }
338
- }
339
- } catch (error) {
340
- console.error('Battle engine error:', error);
341
- currentMessage = 'Something went wrong in battle!';
342
- processingTurn = false;
343
- }
344
- }
345
-
346
-
347
- function triggerVisualEffectsFromMessage(message: string) {
348
- // Use stripped names since battle messages no longer have prefixes
349
- const playerName = stripBattlePrefix(battleState?.playerPiclet?.definition?.name || '');
350
- const enemyName = stripBattlePrefix(battleState?.opponentPiclet?.definition?.name || '');
351
-
352
- // Track who is the current attacker for effects that should appear on the defender
353
- let currentAttacker: 'player' | 'enemy' | null = null;
354
-
355
- // Attack lunge effects - trigger immediately when a Piclet uses a move
356
- if (message.includes(' used ')) {
357
- if (message.includes(playerName)) {
358
- triggerLungeAnimation('player');
359
- currentAttacker = 'player';
360
- } else if (message.includes(enemyName)) {
361
- triggerLungeAnimation('enemy');
362
- currentAttacker = 'enemy';
363
- }
364
- }
365
-
366
- // Damage flash effects - trigger when damage is taken
367
- if (message.includes('took') && message.includes('damage')) {
368
- if (message.includes(playerName)) {
369
- triggerDamageFlash('player');
370
- updateUIFromBattleState();
371
- } else if (message.includes(enemyName)) {
372
- triggerDamageFlash('enemy');
373
- updateUIFromBattleState();
374
- }
375
- }
376
-
377
- // Critical hit effects - show on the target that was hit
378
- if (message.includes('critical hit')) {
379
- // Critical hits appear after damage, so check who took damage
380
- const target = message.includes(`${enemyName} took`) ? 'enemy' :
381
- message.includes(`${playerName} took`) ? 'player' :
382
- // Fallback: if player used move, enemy is target and vice versa
383
- message.includes(`${playerName} used`) ? 'enemy' : 'player';
384
- triggerEffect(target, 'critical', '💥', 1000);
385
- }
386
-
387
- // Effectiveness messages - show on the target that was hit
388
- if (message.includes("It's super effective")) {
389
- // Super effective appears after the move, determine target based on attacker
390
- const target = message.includes(`${playerName} used`) ? 'enemy' :
391
- message.includes(`${enemyName} used`) ? 'player' :
392
- // Fallback based on damage message
393
- message.includes(`${enemyName} took`) ? 'enemy' : 'player';
394
- triggerEffect(target, 'superEffective', '⚡', 800);
395
- } else if (message.includes("not very effective")) {
396
- // Not very effective appears after the move, determine target based on attacker
397
- const target = message.includes(`${playerName} used`) ? 'enemy' :
398
- message.includes(`${enemyName} used`) ? 'player' :
399
- // Fallback based on damage message
400
- message.includes(`${enemyName} took`) ? 'enemy' : 'player';
401
- triggerEffect(target, 'notVeryEffective', '💨', 800);
402
- }
403
-
404
- // Status effects
405
- if (message.includes('was burned')) {
406
- const target = message.includes(playerName) ? 'player' : 'enemy';
407
- triggerEffect(target, 'burn', '🔥', 1200);
408
- } else if (message.includes('was poisoned')) {
409
- const target = message.includes(playerName) ? 'player' : 'enemy';
410
- triggerEffect(target, 'poison', '☠️', 1200);
411
- } else if (message.includes('was paralyzed')) {
412
- const target = message.includes(playerName) ? 'player' : 'enemy';
413
- triggerEffect(target, 'paralyze', '⚡', 1200);
414
- } else if (message.includes('fell asleep')) {
415
- const target = message.includes(playerName) ? 'player' : 'enemy';
416
- triggerEffect(target, 'sleep', '😴', 1200);
417
- } else if (message.includes('was frozen')) {
418
- const target = message.includes(playerName) ? 'player' : 'enemy';
419
- triggerEffect(target, 'freeze', '❄️', 1200);
420
- }
421
-
422
- // Stat changes
423
- if (message.includes("'s") && (message.includes('rose') || message.includes('fell'))) {
424
- const target = message.includes(playerName) ? 'player' : 'enemy';
425
- const isIncrease = message.includes('rose');
426
-
427
- if (message.includes('attack')) {
428
- triggerEffect(target, isIncrease ? 'attackUp' : 'attackDown', isIncrease ? '⚔️' : '🔻', 1000);
429
- } else if (message.includes('defense')) {
430
- triggerEffect(target, isIncrease ? 'defenseUp' : 'defenseDown', isIncrease ? '🛡️' : '🔻', 1000);
431
- } else if (message.includes('speed')) {
432
- triggerEffect(target, isIncrease ? 'speedUp' : 'speedDown', isIncrease ? '💨' : '🐌', 1000);
433
- } else if (message.includes('accuracy')) {
434
- triggerEffect(target, isIncrease ? 'accuracyUp' : 'accuracyDown', isIncrease ? '🎯' : '👁️', 1000);
435
- }
436
- }
437
-
438
- // Healing effects
439
- if (message.includes('recovered') && message.includes('HP')) {
440
- const target = message.includes(playerName) ? 'player' : 'enemy';
441
- triggerEffect(target, 'heal', '💚', 1000);
442
- // Update HP bar immediately for healing animation sync
443
- updateUIFromBattleState();
444
- }
445
-
446
- // Miss effects - show on the attacker who missed
447
- if (message.includes('missed')) {
448
- const target = message.includes(`${playerName}'s attack`) || message.includes(`${playerName} used`) ? 'player' :
449
- message.includes(`${enemyName}'s attack`) || message.includes(`${enemyName} used`) ? 'enemy' :
450
- 'player'; // Fallback
451
- triggerEffect(target, 'miss', '💫', 800);
452
- }
453
-
454
- // Faint effects - only trigger when we see the faint message from battle log
455
- // Don't trigger here since we handle it in finalizeTurn for proper sequencing
456
- }
457
-
458
- function triggerDamageFlash(target: 'player' | 'enemy') {
459
- if (target === 'player') {
460
- playerFlash = true;
461
- setTimeout(() => playerFlash = false, 1000); // Match original Snaplings flicker duration
462
- } else {
463
- enemyFlash = true;
464
- setTimeout(() => enemyFlash = false, 1000); // Match original Snaplings flicker duration
465
- }
466
- }
467
-
468
- function triggerFaintAnimation(target: 'player' | 'enemy') {
469
- if (target === 'player') {
470
- playerFaint = true;
471
- // Don't reset - faint animation should persist until battle ends
472
- } else {
473
- enemyFaint = true;
474
- // Don't reset - faint animation should persist until battle ends
475
- }
476
- }
477
-
478
- function triggerLungeAnimation(target: 'player' | 'enemy') {
479
- if (target === 'player') {
480
- playerLunge = true;
481
- setTimeout(() => playerLunge = false, 600); // Reset after animation
482
- } else {
483
- enemyLunge = true;
484
- setTimeout(() => enemyLunge = false, 600); // Reset after animation
485
- }
486
- }
487
-
488
- function triggerEffect(target: 'player' | 'enemy' | 'both', type: string, emoji: string, duration: number) {
489
- const effect = { type, emoji, duration };
490
-
491
- if (target === 'player' || target === 'both') {
492
- playerEffects = [...playerEffects, effect];
493
- setTimeout(() => {
494
- playerEffects = playerEffects.filter(e => e !== effect);
495
- }, duration);
496
- }
497
-
498
- if (target === 'enemy' || target === 'both') {
499
- enemyEffects = [...enemyEffects, effect];
500
- setTimeout(() => {
501
- enemyEffects = enemyEffects.filter(e => e !== effect);
502
- }, duration);
503
- }
504
  }
505
 
506
- function showMessageSequence(messages: string[], callback: () => void) {
507
- if (!messages || messages.length === 0) {
508
- callback();
509
- return;
510
- }
511
-
512
- messageQueue = messages;
513
- currentMessageIndex = 0;
514
- continueCallback = callback;
515
-
516
- // Show first message and trigger its effects
517
- currentMessage = messageQueue[0];
518
- waitingForContinue = true;
519
-
520
- // Trigger visual effects automatically after text appears (with small delay for text animation)
521
- setTimeout(() => {
522
- triggerVisualEffectsFromMessage(currentMessage);
523
- }, 500); // Allow time for typewriter text to complete
524
- }
525
-
526
- function selectEnemyMove(): MoveAction | null {
527
- const availableEnemyMoves = battleState.opponentPiclet.moves.filter(m => m.currentPP > 0);
528
-
529
- if (availableEnemyMoves.length === 0) {
530
- currentMessage = `${currentEnemyPiclet.nickname} has no moves left!`;
531
- return null;
532
- }
533
-
534
- if (isWildBattle) {
535
- // Wild Piclets always use completely random moves (no trainer strategy)
536
- const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)];
537
- const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove);
538
-
539
- return {
540
- type: 'move',
541
- piclet: 'opponent',
542
- moveIndex: enemyMoveIndex
543
- };
544
- } else {
545
- // Trainer battles - currently also random, but could be enhanced with AI later
546
- const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)];
547
- const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove);
548
-
549
- return {
550
- type: 'move',
551
- piclet: 'opponent',
552
- moveIndex: enemyMoveIndex
553
- };
554
- }
555
- }
556
 
557
- function handleContinueTap() {
558
- if (!waitingForContinue || !messageQueue.length) return;
559
-
560
- currentMessageIndex++;
561
-
562
- if (currentMessageIndex >= messageQueue.length) {
563
- // Sequence finished
564
- waitingForContinue = false;
565
- messageQueue = [];
566
- currentMessageIndex = 0;
567
-
568
- if (continueCallback) {
569
- continueCallback();
570
- continueCallback = null;
571
- }
572
- } else {
573
- // Show next message
574
- currentMessage = messageQueue[currentMessageIndex];
575
-
576
- // Trigger visual effects automatically after text appears
577
- setTimeout(() => {
578
- triggerVisualEffectsFromMessage(currentMessage);
579
- }, 500); // Allow time for typewriter text to complete
580
- }
581
- }
582
 
583
- function updateUIFromBattleState() {
584
- if (!battleState) return;
585
-
586
- // Check if player Piclet has changed (indicating auto-switch due to fainting)
587
- const newPlayerPiclet = battlePicletToInstance(battleState.playerPiclet, currentPlayerPiclet);
588
- const playerPicletChanged = currentPlayerPiclet.id !== newPlayerPiclet.id;
589
-
590
- if (playerPicletChanged) {
591
- // Player Piclet auto-switched due to fainting - show transition
592
- showWhiteFlash = true;
593
- setTimeout(() => {
594
- currentPlayerPiclet = newPlayerPiclet;
595
- playerHpPercentage = battleState.playerPiclet.currentHp / battleState.playerPiclet.maxHp;
596
- showWhiteFlash = false;
597
- }, 300);
598
- } else {
599
- // Normal update without switch
600
- currentPlayerPiclet = newPlayerPiclet;
601
- playerHpPercentage = battleState.playerPiclet.currentHp / battleState.playerPiclet.maxHp;
602
- }
603
-
604
- // Update enemy piclet state
605
- currentEnemyPiclet = battlePicletToInstance(battleState.opponentPiclet, currentEnemyPiclet);
606
- enemyHpPercentage = battleState.opponentPiclet.currentHp / battleState.opponentPiclet.maxHp;
607
- }
608
-
609
- function handlePicletSelect(piclet: PicletInstance) {
610
- if (!battleEngine) return;
611
 
612
- battlePhase = 'main';
613
- processingTurn = true;
614
 
615
- // Find the index of the selected piclet in the roster
616
- const picletIndex = rosterPiclets.findIndex(p => p.id === piclet.id);
617
- if (picletIndex === -1) {
618
- console.error('Selected piclet not found in roster');
619
- processingTurn = false;
620
- return;
621
- }
622
-
623
- // Show the switch message and trigger white flash animation
624
- currentMessage = `Go, ${piclet.nickname}!`;
625
- showWhiteFlash = true;
626
-
627
- // After flash, update display
628
  setTimeout(() => {
629
- currentPlayerPiclet = piclet;
630
- playerHpPercentage = piclet.currentHp / piclet.maxHp;
631
- showWhiteFlash = false;
632
- }, 300);
633
-
634
- const switchAction: SwitchAction = {
635
- type: 'switch',
636
- piclet: 'player',
637
- newPicletIndex: picletIndex
638
- };
639
-
640
- try {
641
- // Select enemy move (wild Piclets always random, trainers could use AI)
642
- const enemyAction = selectEnemyMove();
643
- if (!enemyAction) {
644
- processingTurn = false;
645
- return;
646
- }
647
-
648
- // Allow time for the visual switch to be seen before processing the turn
649
- setTimeout(() => {
650
- // Get log entries before action to track new messages
651
- const logBefore = battleEngine.getLog();
652
-
653
- // Execute the turn - switching vs enemy move
654
- battleEngine.executeActions(switchAction, enemyAction);
655
- battleState = battleEngine.getState();
656
-
657
- processAfterSwitchTurn(logBefore);
658
- }, 1000); // 1 second delay to show the switch visually
659
- } catch (error) {
660
- console.error('Battle engine error:', error);
661
- currentMessage = 'Unable to switch Piclets!';
662
- processingTurn = false;
663
- }
664
- }
665
-
666
- function processAfterSwitchTurn(logBefore: string[]) {
667
- try {
668
-
669
- // Get only the new log entries from this turn
670
- const logAfter = battleEngine.getLog();
671
- const newLogEntries = logAfter.slice(logBefore.length);
672
- // Filter out faint messages since we handle them manually for proper sequencing
673
- const filteredLogEntries = newLogEntries.filter(message => !message.includes('fainted'));
674
- const result = { log: filteredLogEntries };
675
-
676
- // Show battle messages with tap-to-continue system
677
- if (result.log && result.log.length > 0) {
678
- showMessageSequence(result.log, finalizeSwitchTurn);
679
- } else {
680
- finalizeSwitchTurn();
681
- }
682
-
683
- function finalizeSwitchTurn() {
684
- // Update UI state from battle engine
685
- updateUIFromBattleState();
686
-
687
- // Check for battle end - only show faint message
688
- if (battleState.winner) {
689
- battleEnded = true;
690
- const defeatedPiclet = battleState.winner === 'player' ? currentEnemyPiclet : currentPlayerPiclet;
691
-
692
- // Show the faint message and trigger animation
693
- currentMessage = `${defeatedPiclet.nickname} fainted!`;
694
-
695
- // Trigger faint animation for the defeated Piclet
696
- if (battleState.winner === 'player') {
697
- enemyFaint = true;
698
- } else {
699
- playerFaint = true;
700
- }
701
-
702
- // Wait for faint message, then process battle results
703
- setTimeout(async () => {
704
- await handleBattleResults(battleState.winner === 'player');
705
- }, 2500); // Wait time for faint message and animation
706
- } else {
707
- setTimeout(() => {
708
- currentMessage = `What will ${currentPlayerPiclet.nickname} do?`;
709
- processingTurn = false;
710
- }, 1000);
711
- }
712
- }
713
- } catch (error) {
714
- console.error('Switch error:', error);
715
- currentMessage = 'Unable to switch Piclets!';
716
- processingTurn = false;
717
- }
718
- }
719
-
720
- function handleBack() {
721
- battlePhase = 'main';
722
- }
723
-
724
- async function handleBattleResults(playerWon: boolean) {
725
- console.log('🏆 handleBattleResults called:', { playerWon });
726
-
727
- if (playerWon) {
728
- // Calculate XP gained from defeating the enemy
729
- console.log('💰 Calculating XP for enemy:', {
730
- enemyName: currentEnemyPiclet.nickname,
731
- enemyLevel: currentEnemyPiclet.level
732
- });
733
- const xpGained = calculateBattleXp(currentEnemyPiclet, 1);
734
- console.log('�� XP calculation result:', xpGained);
735
-
736
- if (xpGained > 0) {
737
- console.log('✅ XP > 0, processing XP gain...');
738
- // Animate XP gain by updating UI first
739
- const updatedPlayerPiclet = {
740
- ...currentPlayerPiclet,
741
- xp: currentPlayerPiclet.xp + xpGained
742
- };
743
- currentPlayerPiclet = updatedPlayerPiclet;
744
-
745
- // Wait a moment for XP bar animation
746
- await new Promise(resolve => setTimeout(resolve, 1500));
747
-
748
- // Process any level ups
749
- const { newInstance, levelUpInfo } = processAllLevelUps(updatedPlayerPiclet);
750
-
751
- // Save updated Piclet to database
752
- if (newInstance.id) {
753
- console.log('💾 Saving XP to database:', {
754
- picletId: newInstance.id,
755
- oldXP: currentPlayerPiclet.xp,
756
- newXP: newInstance.xp,
757
- xpGained: xpGained
758
- });
759
- await db.picletInstances.update(newInstance.id, newInstance);
760
- console.log('✅ Database update completed');
761
- } else {
762
- console.log('❌ No piclet ID, cannot save to database');
763
- }
764
-
765
- // Update local state with final leveled instance
766
- currentPlayerPiclet = newInstance;
767
- console.log('🔄 Updated currentPlayerPiclet with new XP:', currentPlayerPiclet.xp);
768
-
769
- // Show level up results if any occurred
770
- if (levelUpInfo.length > 0) {
771
- battleResults = {
772
- victory: true,
773
- xpGained,
774
- levelUps: levelUpInfo,
775
- newLevel: newInstance.level
776
- };
777
-
778
- battleResultsVisible = true;
779
-
780
- // Auto-dismiss after showing level up
781
- setTimeout(() => {
782
- battleResultsVisible = false;
783
- onBattleEnd(true);
784
- }, 4000);
785
- } else {
786
- // No level up, just end battle
787
- console.log('📈 No level up, ending battle with XP gain');
788
- onBattleEnd(true);
789
- }
790
- } else {
791
- console.log('❌ XP is 0 or negative, ending battle without XP');
792
- onBattleEnd(true);
793
- }
794
- } else {
795
- // Player lost - no XP gained
796
- onBattleEnd(false);
797
- }
798
  }
799
  </script>
800
 
801
- <div class="battle-page" transition:fade={{ duration: 300 }}>
802
- <nav class="battle-nav">
803
- <button class="back-button" on:click={() => onBattleEnd('cancelled')} style="display: none;">
804
- Back
805
- </button>
806
- <h1>{isWildBattle ? 'Wild Battle' : 'Battle'}</h1>
807
- <div class="nav-spacer"></div>
808
- </nav>
809
-
810
- <div class="battle-content">
811
- <BattleField
812
- playerPiclet={currentPlayerPiclet}
813
- enemyPiclet={currentEnemyPiclet}
814
- {playerHpPercentage}
815
- {enemyHpPercentage}
816
- showIntro={battlePhase === 'intro'}
817
- {battleState}
818
- {playerEffects}
819
- {enemyEffects}
820
- {playerFlash}
821
- {enemyFlash}
822
- {playerFaint}
823
- {enemyFaint}
824
- {playerLunge}
825
- {enemyLunge}
826
- {isWildBattle}
827
- {showWhiteFlash}
828
- {playerTrainerVisible}
829
- {enemyTrainerVisible}
830
- {playerTrainerSlideOut}
831
- {enemyTrainerSlideOut}
832
- />
833
-
834
- <BattleControls
835
- {currentMessage}
836
- {battlePhase}
837
- {processingTurn}
838
- {battleEnded}
839
- {isWildBattle}
840
- playerPiclet={currentPlayerPiclet}
841
- enemyPiclet={currentEnemyPiclet}
842
- {rosterPiclets}
843
- {battleState}
844
- {capturePercentage}
845
- {waitingForContinue}
846
- onAction={handleAction}
847
- onMoveSelect={handleMoveSelect}
848
- onPicletSelect={handlePicletSelect}
849
- onBack={handleBack}
850
- onContinueTap={handleContinueTap}
851
- />
852
  </div>
853
-
854
- <!-- Battle Results Overlay -->
855
- {#if battleResultsVisible}
856
- <div class="battle-results-overlay" transition:fade={{ duration: 300 }}>
857
- <div class="battle-results-card">
858
- <h2>{battleResults.victory ? 'Victory!' : 'Defeat!'}</h2>
859
-
 
 
 
 
 
 
 
860
 
861
- {#if battleResults.levelUps.length > 0}
862
- {#each battleResults.levelUps as levelUp}
863
- <div class="level-up" transition:fade={{ duration: 500 }}>
864
- <h3>🎉 Level Up! 🎉</h3>
865
- <p><strong>{currentPlayerPiclet.nickname}</strong> grew to level <strong>{levelUp.newLevel}</strong>!</p>
866
-
867
- <div class="stat-changes">
868
- {#if levelUp.statChanges.hp > 0}
869
- <div class="stat-change">HP +{levelUp.statChanges.hp}</div>
870
- {/if}
871
- {#if levelUp.statChanges.attack > 0}
872
- <div class="stat-change">Attack +{levelUp.statChanges.attack}</div>
873
- {/if}
874
- {#if levelUp.statChanges.defense > 0}
875
- <div class="stat-change">Defense +{levelUp.statChanges.defense}</div>
876
- {/if}
877
- {#if levelUp.statChanges.speed > 0}
878
- <div class="stat-change">Speed +{levelUp.statChanges.speed}</div>
879
- {/if}
880
- </div>
881
- </div>
882
- {/each}
883
  {/if}
884
  </div>
885
- </div>
886
- {/if}
887
  </div>
888
 
889
  <style>
890
  .battle-page {
891
- position: fixed;
892
- inset: 0;
893
- z-index: 1000;
894
  height: 100vh;
895
  display: flex;
896
  flex-direction: column;
897
- background: #f8f9fa;
898
- overflow: hidden;
899
- padding-top: env(safe-area-inset-top);
900
- }
901
-
902
- @media (max-width: 768px) {
903
- .battle-page {
904
- background: white;
905
- }
906
-
907
- .battle-page::before {
908
- content: '';
909
- position: absolute;
910
- top: 0;
911
- left: 0;
912
- right: 0;
913
- height: env(safe-area-inset-top);
914
- background: white;
915
- z-index: 1;
916
- }
917
- }
918
-
919
- .battle-nav {
920
- display: none; /* Hide navigation in battle */
921
- }
922
-
923
- .back-button {
924
- background: none;
925
- border: none;
926
- color: #007bff;
927
- font-size: 1rem;
928
- cursor: pointer;
929
- padding: 0.5rem;
930
  }
931
 
932
- .battle-nav h1 {
933
- margin: 0;
934
- font-size: 1.25rem;
935
- font-weight: 600;
936
- color: #1a1a1a;
937
- position: absolute;
938
- left: 50%;
939
- transform: translateX(-50%);
940
- }
941
-
942
- .nav-spacer {
943
- width: 60px;
944
  }
945
 
946
- .battle-content {
947
- flex: 1;
948
  display: flex;
949
- flex-direction: column;
950
- overflow: hidden;
951
- position: relative;
952
- background: #f8f9fa;
953
  }
954
 
955
- /* Battle Results Overlay */
956
- .battle-results-overlay {
957
- position: fixed;
958
- inset: 0;
959
- background: rgba(0, 0, 0, 0.8);
960
  display: flex;
 
961
  align-items: center;
962
- justify-content: center;
963
- z-index: 2000;
964
  }
965
 
966
- .battle-results-card {
967
- background: white;
968
- border-radius: 16px;
969
- padding: 2rem;
970
- max-width: 400px;
971
- width: 90%;
972
- text-align: center;
973
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
974
  }
975
 
976
- .battle-results-card h2 {
977
- margin: 0 0 1rem 0;
978
- font-size: 1.8rem;
979
- font-weight: 700;
980
- color: #1a1a1a;
981
  }
982
 
 
 
 
983
 
984
- .level-up {
985
- background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 100%);
986
  border-radius: 12px;
987
- padding: 1.5rem;
988
- margin: 1rem 0;
989
- border: 3px solid #ff6f00;
990
- animation: levelUpPulse 0.6s ease-in-out;
991
  }
992
 
993
- .level-up h3 {
994
- margin: 0 0 0.5rem 0;
995
- font-size: 1.4rem;
996
- color: #e65100;
997
- text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
 
 
 
 
 
998
  }
999
 
1000
- .level-up p {
1001
- margin: 0 0 1rem 0;
1002
- font-size: 1.2rem;
1003
- color: #bf360c;
 
 
 
 
1004
  }
1005
 
1006
- .stat-changes {
 
1007
  display: flex;
1008
- flex-wrap: wrap;
1009
- gap: 0.5rem;
1010
  justify-content: center;
 
 
 
1011
  }
1012
 
1013
- .stat-change {
1014
- background: rgba(76, 175, 80, 0.2);
1015
- border: 1px solid #4caf50;
1016
- border-radius: 20px;
1017
- padding: 0.25rem 0.75rem;
1018
- font-size: 0.9rem;
1019
- font-weight: 600;
1020
- color: #2e7d32;
1021
  }
1022
 
1023
- @keyframes levelUpPulse {
1024
- 0% {
1025
- transform: scale(0.9);
1026
- opacity: 0;
1027
- }
1028
- 50% {
1029
- transform: scale(1.05);
1030
- }
1031
- 100% {
1032
- transform: scale(1);
1033
- opacity: 1;
1034
- }
1035
- }
1036
  </style>
 
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 CHANGED
@@ -5,8 +5,7 @@
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 { calculateStat, calculateHp } from '$lib/services/levelingService';
9
- import { db } from '$lib/db';
10
  import { uiStore } from '$lib/stores/ui';
11
  import Battle from './Battle.svelte';
12
  import PullToRefresh from '../UI/PullToRefresh.svelte';
@@ -110,36 +109,9 @@
110
  } else if (encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId) {
111
  // Regular wild encounter - start battle
112
  await startBattle(encounter);
113
- } else if (encounter.type === EncounterType.SHOP) {
114
- await handleShopEncounter();
115
- } else if (encounter.type === EncounterType.HEALTH_CENTER) {
116
- await handleHealthCenterEncounter();
117
- } else if (encounter.type === EncounterType.TRAINER_BATTLE) {
118
- alert('Trainer battles coming soon!');
119
  }
120
  }
121
 
122
- async function handleShopEncounter() {
123
- alert('Shop features coming soon!');
124
- await forceEncounterRefresh();
125
- }
126
-
127
- async function handleHealthCenterEncounter() {
128
- try {
129
- // Heal all piclets
130
- const piclets = await db.picletInstances.toArray();
131
- for (const piclet of piclets) {
132
- await db.picletInstances.update(piclet.id!, {
133
- currentHp: piclet.maxHp
134
- });
135
- }
136
-
137
- alert('All your piclets have been healed to full health!');
138
- await forceEncounterRefresh();
139
- } catch (error) {
140
- console.error('Error at health center:', error);
141
- }
142
- }
143
 
144
  async function handleFirstPicletEncounter(encounter: Encounter) {
145
  try {
@@ -195,12 +167,6 @@
195
 
196
  function getEncounterIcon(encounter: Encounter): string {
197
  switch (encounter.type) {
198
- case EncounterType.SHOP:
199
- return '🛍️';
200
- case EncounterType.HEALTH_CENTER:
201
- return '❤️';
202
- case EncounterType.TRAINER_BATTLE:
203
- return '🏆';
204
  case EncounterType.FIRST_PICLET:
205
  return '✨';
206
  case EncounterType.WILD_PICLET:
@@ -215,12 +181,6 @@
215
  return '#4caf50';
216
  case EncounterType.FIRST_PICLET:
217
  return '#ffd700';
218
- case EncounterType.TRAINER_BATTLE:
219
- return '#ff9800';
220
- case EncounterType.SHOP:
221
- return '#2196f3';
222
- case EncounterType.HEALTH_CENTER:
223
- return '#9c27b0';
224
  default:
225
  return '#607d8b';
226
  }
@@ -242,11 +202,8 @@
242
  // Sort by roster position
243
  rosterPiclets.sort((a, b) => (a.rosterPosition ?? 0) - (b.rosterPosition ?? 0));
244
 
245
- // Get healthy piclets
246
- const healthyPiclets = rosterPiclets.filter(p => p.currentHp > 0);
247
-
248
- if (healthyPiclets.length === 0) {
249
- alert('You need at least one healthy piclet in your roster to battle!');
250
  return;
251
  }
252
 
@@ -262,7 +219,7 @@
262
  if (!enemyPiclet) return;
263
 
264
  // Set up battle
265
- battlePlayerPiclet = healthyPiclets[0];
266
  battleEnemyPiclet = enemyPiclet;
267
  battleIsWild = true;
268
  battleRosterPiclets = rosterPiclets; // Pass all roster piclets
@@ -290,31 +247,11 @@
290
  // Calculate stats based on template's base stats and encounter level
291
  const level = encounter.enemyLevel;
292
 
293
- // Calculate current stats based on level (using template's base stats)
294
-
295
- const maxHp = calculateHp(templatePiclet.baseHp, level);
296
-
297
- // Create enemy piclet instance based on template
298
  const enemyPiclet: PicletInstance = {
299
  ...templatePiclet,
300
  id: -1, // Temporary ID for enemy
301
 
302
- level: level,
303
- xp: 0,
304
- currentHp: maxHp,
305
- maxHp: maxHp,
306
- attack: calculateStat(templatePiclet.baseAttack, level),
307
- defense: calculateStat(templatePiclet.baseDefense, level),
308
- fieldAttack: calculateStat(templatePiclet.baseFieldAttack, level),
309
- fieldDefense: calculateStat(templatePiclet.baseFieldDefense, level),
310
- speed: calculateStat(templatePiclet.baseSpeed, level),
311
-
312
- // Reset move PP to full
313
- moves: templatePiclet.moves.map(move => ({
314
- ...move,
315
- currentPp: move.pp
316
- })),
317
-
318
  isInRoster: false,
319
  caughtAt: new Date()
320
  };
@@ -361,9 +298,7 @@
361
  caught: true,
362
  caughtAt: new Date(),
363
  isInRoster: true,
364
- rosterPosition: nextPosition,
365
- // Ensure level is valid (battle enemies might have invalid levels)
366
- level: Math.max(1, Math.min(100, capturedPiclet.level || 5))
367
  };
368
 
369
  // Add the captured piclet to the database
@@ -382,13 +317,11 @@
382
  console.log(`Added captured Piclet ${createdPiclet.nickname} to roster position ${nextPosition}`);
383
  } else {
384
  console.error('Could not retrieve newly created piclet from database');
385
- // Fix the level issue and use fallback data
386
  const fallbackPiclet = {
387
  ...capturedPiclet,
388
- level: Math.max(1, Math.min(100, capturedPiclet.level || 5)),
389
  id: newPicletId
390
  };
391
- console.log('Using fallback with fixed level:', fallbackPiclet.level);
392
  newlyCaughtPiclet = fallbackPiclet;
393
  showNewlyCaught = true;
394
  }
 
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';
 
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 {
 
167
 
168
  function getEncounterIcon(encounter: Encounter): string {
169
  switch (encounter.type) {
 
 
 
 
 
 
170
  case EncounterType.FIRST_PICLET:
171
  return '✨';
172
  case EncounterType.WILD_PICLET:
 
181
  return '#4caf50';
182
  case EncounterType.FIRST_PICLET:
183
  return '#ffd700';
 
 
 
 
 
 
184
  default:
185
  return '#607d8b';
186
  }
 
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
 
 
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
 
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
  };
 
298
  caught: true,
299
  caughtAt: new Date(),
300
  isInRoster: true,
301
+ rosterPosition: nextPosition
 
 
302
  };
303
 
304
  // Add the captured piclet to the database
 
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
  }
src/lib/components/Pages/Pictuary.svelte CHANGED
@@ -140,7 +140,6 @@
140
  typeId: piclet.typeId,
141
  nickname: piclet.nickname,
142
  primaryType: piclet.primaryType,
143
- secondaryType: piclet.secondaryType,
144
 
145
  // Current Stats
146
  currentHp: piclet.currentHp,
 
140
  typeId: piclet.typeId,
141
  nickname: piclet.nickname,
142
  primaryType: piclet.primaryType,
 
143
 
144
  // Current Stats
145
  currentHp: piclet.currentHp,
src/lib/components/PicletGenerator/PicletGenerator.svelte CHANGED
@@ -1,6 +1,5 @@
1
  <script lang="ts">
2
  import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
3
- import { Nature } from '$lib/types';
4
  import type { PicletInstance } from '$lib/db/schema';
5
  import UploadStep from './UploadStep.svelte';
6
  import WorkflowProgress from './WorkflowProgress.svelte';
@@ -230,8 +229,8 @@ Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit
230
  await generateConcept();
231
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
232
 
233
- // Step 3: Generate structured monster stats based on both caption and concept
234
- await generateStats();
235
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
236
 
237
  // Step 4: Generate image prompt with qwen3
@@ -241,8 +240,8 @@ Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit
241
  // Step 5: Generate monster image
242
  await generateMonsterImage();
243
 
244
- // Step 6: Auto-save the piclet as uncaught
245
- await autoSavePiclet();
246
 
247
  workflowState.currentStep = 'complete';
248
 
@@ -348,8 +347,11 @@ Format your response exactly as follows:
348
  # Monster Name
349
  {Creative name that hints at the original object, 11 letters max}
350
 
 
 
 
351
  # Monster Description
352
- {Detailed physical description showing how the object becomes a creature. Ensure the creature uses all the unique attributes of the object. Include colors, shapes, materials, eyes, limbs, mouth, and distinctive features. This section will be used for stats generation and lore.}
353
 
354
  # Monster Image Prompt
355
  {Extensive visual description of the Pokémon style monster for image generation. Focus on key visual elements: body shape, colors, distinctive features, pose. Keep this optimized for AI image generation.}
@@ -501,408 +503,63 @@ Create a concise visual description (1-3 sentences, max 100 words). Focus only o
501
  }
502
  }
503
 
504
- async function generateStats() {
505
  workflowState.currentStep = 'statsGenerating';
506
 
507
- const activeClient = getActiveTextClient();
508
- if (!activeClient || !workflowState.picletConcept || !workflowState.imageCaption) {
509
- throw new Error(`${currentTextClient} service not available or no concept/caption available for stats generation`);
510
- }
511
-
512
- // Default tier (will be set from the generated stats)
513
- let tier: 'low' | 'medium' | 'high' | 'legendary' = 'medium';
514
-
515
- // Extract monster name and rarity from the structured concept
516
- const monsterNameMatch = workflowState.picletConcept.match(/# Monster Name\s*\n([\s\S]*?)(?=^#|$)/m);
517
- let monsterName = monsterNameMatch ? monsterNameMatch[1].trim() : 'Unknown Monster';
518
-
519
- // Truncate name at first comma if present
520
- if (monsterName.includes(',')) {
521
- monsterName = monsterName.split(',')[0].trim();
522
- }
523
-
524
- // Cap name length to 12 characters
525
- if (monsterName.length > 12) {
526
- monsterName = monsterName.substring(0, 12);
527
- }
528
-
529
- const rarityMatch = workflowState.picletConcept.match(/# Object Rarity\s*\n([\s\S]*?)(?=^#)/m);
530
- const objectRarity = rarityMatch ? rarityMatch[1].trim().toLowerCase() : 'common';
531
-
532
- // Create comprehensive battle-ready monster prompt
533
- const statsPrompt = `Based on this detailed object description and monster concept, create a complete battle-ready monster for the Pictuary Battle System:
534
-
535
- ORIGINAL OBJECT DESCRIPTION:
536
- "${workflowState.imageCaption}"
537
-
538
- MONSTER CONCEPT:
539
- "${workflowState.picletConcept}"
540
-
541
- The object rarity has been assessed as: ${objectRarity}
542
-
543
- ## BATTLE SYSTEM OVERVIEW
544
- This monster will be used in a turn-based battle system with composable effects. You must create:
545
- 1. **Base Stats**: Core combat statistics
546
- 2. **Special Ability**: Passive trait with triggers and effects
547
- 3. **Movepool**: 4 battle moves with complex effect combinations
548
-
549
- ## TYPE SYSTEM
550
- Choose the primary type (and optional secondary type) based on the object:
551
- • **beast**: Vertebrate wildlife — mammals, birds, reptiles. Raw physicality, instincts
552
- • **bug**: Arthropods — butterflies, beetles, mantises. Agile swarms, precision strikes
553
- • **aquatic**: Life that swims, dives, sloshes — fish, octopus, sentient puddles. Tides, pressure
554
- • **flora**: Plants and fungi — blooming or decaying. Growth, spores, vines, seasonal shifts
555
- • **mineral**: Stones, crystals, metals — earth's depths. Durability, reflective armor, seismic shocks
556
- • **space**: Stars, moon, cosmic objects — not of this world. Celestial energy, gravitational effects
557
- • **machina**: Engineered devices — gadgets to machinery. Gears, circuits, drones, power surges
558
- • **structure**: Buildings, bridges, monuments — architectural titans. Fortification, terrain shaping
559
- • **culture**: Art, fashion, toys, symbols — creative expressions. Buffs, debuffs, illusion, stories
560
- • **cuisine**: Dishes, drinks, culinary art — flavors and aromas. Temperature, restorative effects
561
-
562
- ## EFFECT SYSTEM
563
- All abilities and moves use these **atomic building blocks**:
564
-
565
- ### **Effect Types:**
566
- 1. **damage**: Deal damage (weak/normal/strong/extreme) with formulas (standard/recoil/drain/fixed/percentage)
567
- 2. **modifyStats**: Change stats (increase/decrease/greatly_increase/greatly_decrease) for hp/attack/defense/speed/accuracy
568
- 3. **applyStatus**: Apply status effects (burn/freeze/paralyze/poison/sleep/confuse) with chance percentage
569
- 4. **heal**: Restore HP (small/medium/large/full) or by percentage/fixed amounts
570
- 5. **manipulatePP**: Drain, restore, or disable PP from moves
571
- 6. **fieldEffect**: Persistent battlefield modifications (reflect/lightScreen for defense, spikes for entry damage, mist for stat protection, healingField for regeneration, etc.)
572
- 7. **counter**: Reflect damage
573
- 8. **priority**: Modify move priority (-5 to +5)
574
- 9. **removeStatus**: Cure specific status conditions
575
- 10. **mechanicOverride**: Modify core game mechanics (immunity, type changes, etc.)
576
-
577
- ### **Targets:**
578
- - **self**: The move user
579
- - **opponent**: The target opponent
580
- - **all**: All combatants
581
- - **allies**: Allied creatures (team battles)
582
- - **field**: Entire battlefield
583
-
584
- ### **Conditions:**
585
- - **always**: Effect always applies
586
- - **onHit**: Only if move hits successfully
587
- - **afterUse**: After move execution regardless of hit/miss
588
- - **onCritical**: Only on critical hits
589
- - **ifLowHp**: If user's HP < 25%
590
- - **ifHighHp**: If user's HP > 75%
591
-
592
- ### **Move Flags:**
593
- Moves can have flags affecting interactions:
594
- - **contact**: Makes physical contact (triggers contact abilities)
595
- - **explosive**: Explosive move (affected by explosion-related abilities)
596
- - **draining**: Drains HP from target
597
- - **priority**: Natural priority move (+1 to +5)
598
- - **sacrifice**: Involves self-sacrifice or major cost
599
- - **reckless**: High power with drawbacks
600
-
601
- ## SPECIAL ABILITY TRIGGERS
602
- Special abilities activate on specific events:
603
- - **onSwitchIn**: When entering battle
604
- - **onDamageTaken**: When this monster takes damage
605
- - **onContactDamage**: When hit by a contact move
606
- - **endOfTurn**: At the end of each turn
607
- - **onLowHP**: When HP drops below 25%
608
- - **onStatusInflicted**: When a status is applied
609
-
610
- ## BALANCING GUIDELINES
611
- **Stat Ranges by Rarity:**
612
- - **common**: 45-80 total stats, individual stats 10-25
613
- - **uncommon**: 80-120 total stats, individual stats 15-35
614
- - **rare**: 120-160 total stats, individual stats 25-45
615
- - **legendary**: 160-200+ total stats, individual stats 35-50
616
-
617
- **Design Philosophy:**
618
- - **Risk-Reward**: Powerful moves must have meaningful drawbacks
619
- - **Type Synergy**: Moves should match the monster's type and concept
620
- - **Strategic Depth**: Abilities should create interesting decision points
621
- - **No Strictly Better**: Every powerful effect has a cost or condition
622
-
623
- The output should be formatted as a JSON instance that conforms to the schema below.
624
-
625
- \`\`\`json
626
- {
627
- "type": "object",
628
- "properties": {
629
- "name": {"type": "string", "description": "Creative name for the monster that hints at the original object"},
630
- "description": {"type": "string", "description": "Flavor text describing the monster (2-3 sentences)"},
631
- "tier": {"type": "string", "enum": ["low", "medium", "high", "legendary"], "description": "Power tier based on rarity: common=low, uncommon=medium, rare=high, legendary=legendary"},
632
- "primaryType": {"type": "string", "enum": ["beast", "bug", "aquatic", "flora", "mineral", "space", "machina", "structure", "culture", "cuisine"], "description": "Primary type based on object characteristics"},
633
- "secondaryType": {"type": ["string", "null"], "enum": ["beast", "bug", "aquatic", "flora", "mineral", "space", "machina", "structure", "culture", "cuisine", null], "description": "Optional secondary type for dual-type monsters"},
634
- "baseStats": {
635
- "type": "object",
636
- "properties": {
637
- "hp": {"type": "integer", "minimum": 10, "maximum": 50, "description": "Hit points"},
638
- "attack": {"type": "integer", "minimum": 10, "maximum": 50, "description": "Attack power"},
639
- "defense": {"type": "integer", "minimum": 10, "maximum": 50, "description": "Defensive capability"},
640
- "speed": {"type": "integer", "minimum": 10, "maximum": 50, "description": "Speed and agility"}
641
- },
642
- "required": ["hp", "attack", "defense", "speed"],
643
- "additionalProperties": false
644
- },
645
- "nature": {
646
- "type": "string",
647
- "enum": ["hardy", "docile", "serious", "bashful", "quirky", "lonely", "brave", "adamant", "naughty", "bold", "relaxed", "impish", "lax", "timid", "hasty", "jolly", "naive", "modest", "mild", "quiet", "gentle", "sassy", "careful", "calm", "reckless"],
648
- "description": "Personality trait affecting behavior and battle style"
649
- },
650
- "specialAbility": {
651
- "type": "object",
652
- "properties": {
653
- "name": {"type": "string", "description": "Name of the special ability"},
654
- "triggers": {
655
- "type": "array",
656
- "items": {"$ref": "#/definitions/Trigger"},
657
- "minItems": 1,
658
- "maxItems": 1,
659
- "description": "Single trigger effect for the special ability"
660
- }
661
- },
662
- "required": ["name"],
663
- "additionalProperties": false
664
- },
665
- "movepool": {
666
- "type": "array",
667
- "items": {"$ref": "#/definitions/Move"},
668
- "minItems": 4,
669
- "maxItems": 4,
670
- "description": "Exactly 4 battle moves"
671
- }
672
- },
673
- "required": ["name", "description", "tier", "primaryType", "baseStats", "nature", "specialAbility", "movepool"],
674
- "additionalProperties": false,
675
- "definitions": {
676
- "Effect": {
677
- "type": "object",
678
- "properties": {
679
- "type": {"type": "string", "enum": ["damage", "modifyStats", "applyStatus", "heal", "manipulatePP", "fieldEffect", "counter", "priority", "removeStatus", "mechanicOverride"]},
680
- "target": {"type": "string", "enum": ["self", "opponent", "allies", "all", "attacker", "field", "playerSide", "opponentSide"]},
681
- "condition": {"type": "string", "enum": ["always", "onHit", "afterUse", "onCritical", "ifLowHp", "ifHighHp", "thisTurn", "nextTurn", "restOfBattle"]}
682
- },
683
- "required": ["type"],
684
- "allOf": [
685
- {"if": {"properties": {"type": {"const": "damage"}}}, "then": {"properties": {"amount": {"enum": ["weak", "normal", "strong", "extreme"]}, "formula": {"enum": ["standard", "recoil", "drain", "fixed", "percentage"]}, "value": {"type": "number"}}}},
686
- {"if": {"properties": {"type": {"const": "modifyStats"}}}, "then": {"properties": {"stats": {"type": "object", "properties": {"hp": {"enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]}, "attack": {"enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]}, "defense": {"enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]}, "speed": {"enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]}, "accuracy": {"enum": ["increase", "decrease", "greatly_increase", "greatly_decrease"]}}}}}},
687
- {"if": {"properties": {"type": {"const": "applyStatus"}}}, "then": {"properties": {"status": {"enum": ["burn", "freeze", "paralyze", "poison", "sleep", "confuse"]}, "chance": {"type": "number", "minimum": 1, "maximum": 100}}}},
688
- {"if": {"properties": {"type": {"const": "heal"}}}, "then": {"properties": {"amount": {"enum": ["small", "medium", "large", "full"]}, "formula": {"enum": ["percentage", "fixed"]}, "value": {"type": "number"}}}},
689
- {"if": {"properties": {"type": {"const": "fieldEffect"}}}, "then": {"properties": {"effect": {"enum": ["reflect", "lightScreen", "spikes", "healingMist", "toxicSpikes"]}, "stackable": {"type": "boolean"}}}},
690
- {"if": {"properties": {"type": {"const": "manipulatePP"}}}, "then": {"properties": {"action": {"enum": ["drain", "restore", "disable"]}, "amount": {"enum": ["small", "medium", "large"]}}}},
691
- {"if": {"properties": {"type": {"const": "counter"}}}, "then": {"properties": {"strength": {"enum": ["weak", "normal", "strong"]}}}},
692
- {"if": {"properties": {"type": {"const": "priority"}}}, "then": {"properties": {"value": {"type": "integer", "minimum": -5, "maximum": 5}}}},
693
- {"if": {"properties": {"type": {"const": "removeStatus"}}}, "then": {"properties": {"status": {"enum": ["burn", "freeze", "paralyze", "poison", "sleep", "confuse"]}}}},
694
- {"if": {"properties": {"type": {"const": "mechanicOverride"}}}, "then": {"properties": {"mechanic": {"enum": ["criticalHits", "statusImmunity", "damageReflection", "damageAbsorption", "damageCalculation", "damageMultiplier", "healingInversion", "healingBlocked", "priorityOverride", "accuracyBypass", "typeImmunity", "typeChange", "contactDamage", "drainInversion", "weatherImmunity", "flagImmunity", "flagWeakness", "flagResistance", "statModification", "targetRedirection", "extraTurn"]}, "value": {}}}}
695
- ]
696
- },
697
- "Trigger": {
698
- "type": "object",
699
- "properties": {
700
- "event": {"type": "string", "enum": ["onSwitchIn", "onDamageTaken", "onContactDamage", "endOfTurn", "onLowHP", "onStatusInflicted", "beforeMoveUse", "afterMoveUse"]},
701
- "condition": {"type": "string", "enum": ["always", "ifLowHp", "ifHighHp", "ifStatus:burn", "ifStatus:freeze", "ifStatus:paralyze", "ifStatus:poison", "ifStatus:sleep", "ifStatus:confuse"]},
702
- "effects": {"type": "array", "items": {"$ref": "#/definitions/Effect"}, "minItems": 1}
703
- },
704
- "required": ["event", "effects"]
705
- },
706
- "Move": {
707
- "type": "object",
708
- "properties": {
709
- "name": {"type": "string", "description": "Name of the move"},
710
- "description": {"type": "string", "description": "Description of what the move does"},
711
- "type": {"type": "string", "enum": ["beast", "bug", "aquatic", "flora", "mineral", "space", "machina", "structure", "culture", "cuisine", "normal"], "description": "Move type for STAB and effectiveness"},
712
- "power": {"type": "integer", "minimum": 0, "maximum": 250, "description": "Base power (0 for status moves)"},
713
- "accuracy": {"type": "integer", "minimum": 30, "maximum": 100, "description": "Hit chance percentage"},
714
- "pp": {"type": "integer", "minimum": 1, "maximum": 40, "description": "Power points (uses per battle)"},
715
- "priority": {"type": "integer", "minimum": -5, "maximum": 5, "description": "Priority bracket"},
716
- "flags": {"type": "array", "items": {"enum": ["contact", "explosive", "draining", "priority", "sacrifice", "reckless", "bite", "punch", "sound", "ground"]}, "description": "Move characteristics"},
717
- "effects": {"type": "array", "items": {"$ref": "#/definitions/Effect"}, "minItems": 1, "description": "What the move does"}
718
- },
719
- "required": ["name", "type", "power", "accuracy", "pp", "priority", "flags", "effects"],
720
- "additionalProperties": false
721
  }
722
- }
723
- }
724
- \`\`\`
725
-
726
- **STAT GUIDELINES:**
727
- Base the tier and stats on the object rarity:
728
- - **common → low tier**: hp/attack/defense/speed should be 10-25 (total ~40-80)
729
- - **uncommon → medium tier**: hp/attack/defense/speed should be 15-35 (total ~80-120)
730
- - **rare → high tier**: hp/attack/defense/speed should be 25-45 (total ~120-160)
731
- - **legendary → legendary tier**: hp/attack/defense/speed should be 35-50 (total ~160-200)
732
-
733
- **CREATIVITY & DIVERSITY REQUIREMENTS:**
734
- 🎲 **CRITICAL**: Make this monster unique! Avoid generic patterns and create something distinctive.
735
-
736
- **SPECIAL ABILITY CREATIVITY:**
737
- - Draw inspiration from the SPECIFIC object description and monster concept
738
- - Avoid overused triggers like "onSwitchIn with healingMist"
739
- - Use diverse triggers: onContactDamage, onLowHP, onStatusInflicted, onCriticalHit, etc.
740
- - Create unique effects that match the monster's nature and appearance
741
-
742
- **MOVEPOOL DIVERSITY REQUIREMENTS:**
743
- - Each of the 4 moves MUST use different primary effect types
744
- - Vary the mechanics: one utility, one status, one damage, one defensive/field effect
745
- - Use different status effects if applying status (avoid repeating burn/sleep/paralyze)
746
- - Vary power/accuracy/PP patterns - don't use template values
747
- - Reference specific visual elements from the monster description in move names
748
-
749
- **FORBIDDEN REPETITIVE PATTERNS:**
750
- ❌ Do NOT use these overused combinations:
751
- - "healingMist" field effect on switch-in (find alternatives!)
752
- - Defense boost + small percentage heal combo moves
753
- - High power + self-recoil damage as the only "ultimate" move pattern
754
- - All moves having same status effect (sleep/paralyze)
755
- - Generic move names like "Basic Attack" or "Status Move"
756
-
757
- **ORIGINALITY GUIDELINES:**
758
- - Connect abilities to the original object's unique properties
759
- - Use unconventional effect combinations and mechanics
760
- - Experiment with mechanicOverride, priority manipulation, counter effects
761
- - Create moves that tell a story about how this creature fights
762
-
763
- Write your response within \`\`\`json\`\`\``;
764
-
765
- console.log('Generating monster stats');
766
 
767
  try {
768
- const responseText = await generateText(statsPrompt);
 
 
769
 
770
- if (!responseText || responseText.trim() === '') {
771
- throw new Error('Failed to generate monster stats');
 
772
  }
773
-
774
- console.log('Stats output:', responseText);
775
- let jsonString = responseText;
776
-
777
- // Extract JSON from the response (remove markdown if present)
778
- let cleanJson = jsonString;
779
- if (jsonString.includes('```')) {
780
- const matches = jsonString.match(/```(?:json)?\s*([\s\S]*?)```/);
781
- if (matches) {
782
- cleanJson = matches[1];
783
- } else {
784
- // If no closing ```, just remove the opening ```json
785
- cleanJson = jsonString.replace(/^```(?:json)?\s*/, '').replace(/```\s*$/, '');
786
- }
787
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
 
789
- try {
790
- // Extract JSON by properly balancing braces instead of using regex
791
- const startIndex = cleanJson.indexOf('{');
792
- if (startIndex !== -1) {
793
- let braceCount = 0;
794
- let endIndex = -1;
795
-
796
- // Find the matching closing brace by counting
797
- for (let i = startIndex; i < cleanJson.length; i++) {
798
- if (cleanJson[i] === '{') braceCount++;
799
- if (cleanJson[i] === '}') {
800
- braceCount--;
801
- if (braceCount === 0) {
802
- endIndex = i;
803
- break;
804
- }
805
- }
806
- }
807
-
808
- if (endIndex !== -1) {
809
- cleanJson = cleanJson.substring(startIndex, endIndex + 1);
810
- console.log('Balanced JSON extracted, length:', cleanJson.length);
811
- } else {
812
- throw new Error('JSON appears to be truncated - unable to balance braces');
813
- }
814
- } else {
815
- throw new Error('No JSON object found in response');
816
- }
817
-
818
- console.log('Final JSON to parse (length: ' + cleanJson.length + '):', cleanJson.substring(0, 500) + '...');
819
-
820
- // Fix invalid priority values like "priority": +1 to "priority": 1
821
- cleanJson = cleanJson.replace(/"priority":\s*\+(\d+)/g, '"priority": $1');
822
-
823
- // Fix unescaped newlines in string values
824
- cleanJson = cleanJson.replace(/"([^"\\]*)(?:\\.|[^"\\])*"/g, (match) => {
825
- // Only process if this looks like a string value (not a property name)
826
- if (match.includes('\n')) {
827
- return match.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
828
- }
829
- return match;
830
- });
831
-
832
- const parsedStats = JSON.parse(cleanJson.trim());
833
-
834
- // Validate the battle-ready monster structure
835
- console.log('Parsed battle monster:', parsedStats);
836
-
837
- // Ensure required fields exist
838
- if (!parsedStats.name || !parsedStats.baseStats || !parsedStats.specialAbility || !parsedStats.movepool) {
839
- throw new Error('Generated monster is missing required battle system fields');
840
- }
841
-
842
- // Validate movepool has exactly 4 moves
843
- if (!Array.isArray(parsedStats.movepool) || parsedStats.movepool.length !== 4) {
844
- throw new Error('Monster movepool must contain exactly 4 moves');
845
- }
846
-
847
- // Ensure all moves have required effect arrays
848
- for (const move of parsedStats.movepool) {
849
- if (!move.effects || !Array.isArray(move.effects) || move.effects.length === 0) {
850
- throw new Error(`Move "${move.name}" is missing effects array`);
851
- }
852
- }
853
-
854
- // Use tier from the JSON response
855
- tier = parsedStats.tier || 'medium';
856
-
857
- // Clean asterisks from JSON-parsed name (qwen3 often adds them for markdown bold)
858
- if (parsedStats.name) {
859
- parsedStats.name = parsedStats.name.replace(/\*/g, '');
860
- }
861
-
862
- // Clean asterisks from special ability name
863
- if (parsedStats.specialAbility?.name) {
864
- parsedStats.specialAbility.name = parsedStats.specialAbility.name.replace(/\*/g, '');
865
- }
866
-
867
- // Clean asterisks from move names
868
- if (parsedStats.movepool) {
869
- for (const move of parsedStats.movepool) {
870
- if (move.name) {
871
- move.name = move.name.replace(/\*/g, '');
872
- }
873
- }
874
- }
875
-
876
- // Ensure the name from structured concept is used if available
877
- if (monsterName && monsterName !== 'Unknown Monster') {
878
- // Remove asterisk characters used for markdown bold formatting
879
- parsedStats.name = monsterName.replace(/\*/g, '');
880
- }
881
-
882
- // Ensure baseStats are numbers within reasonable ranges
883
- if (parsedStats.baseStats) {
884
- const statFields = ['hp', 'attack', 'defense', 'speed'];
885
- for (const field of statFields) {
886
- if (parsedStats.baseStats[field] !== undefined) {
887
- parsedStats.baseStats[field] = Math.max(10, Math.min(50, parseInt(parsedStats.baseStats[field])));
888
- }
889
- }
890
- }
891
-
892
- const stats: PicletStats = parsedStats;
893
- workflowState.picletStats = stats;
894
- console.log('Monster stats generated:', stats);
895
- console.log('Monster stats JSON:', JSON.stringify(stats, null, 2));
896
- } catch (parseError) {
897
- console.error('Failed to parse JSON:', parseError, 'Raw output:', cleanJson);
898
- throw new Error('Failed to parse monster stats JSON');
899
- }
900
  } catch (error) {
 
901
  handleAPIError(error);
902
  }
903
  }
904
 
905
- async function autoSavePiclet() {
906
  if (!workflowState.picletImage || !workflowState.imageCaption || !workflowState.picletConcept || !workflowState.imagePrompt || !workflowState.picletStats) {
907
  console.error('Cannot auto-save: missing required data');
908
  return;
@@ -933,10 +590,15 @@ Write your response within \`\`\`json\`\`\``;
933
  console.log('- imagePrompt type:', typeof picletData.imagePrompt);
934
  console.log('- stats:', cleanStats);
935
 
936
- // Convert to PicletInstance format and save
937
  const picletInstance = await generatedDataToPicletInstance(picletData);
 
 
 
 
 
938
  const picletId = await savePicletInstance(picletInstance);
939
- console.log('Piclet auto-saved as uncaught with ID:', picletId);
940
 
941
  // If in trainer mode, notify completion
942
  if (isTrainerMode && onTrainerImageCompleted && trainerImagePaths[currentImageIndex]) {
 
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';
 
229
  await generateConcept();
230
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
231
 
232
+ // Step 3: Extract simple stats from enhanced concept
233
+ await extractSimpleStats();
234
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
235
 
236
  // Step 4: Generate image prompt with qwen3
 
240
  // Step 5: Generate monster image
241
  await generateMonsterImage();
242
 
243
+ // Step 6: Auto-save the piclet as caught (since scanning now auto-captures)
244
+ await autoSavePicletAsCaught();
245
 
246
  workflowState.currentStep = 'complete';
247
 
 
347
  # Monster Name
348
  {Creative name that hints at the original object, 11 letters max}
349
 
350
+ # Primary Type
351
+ {Based on the object, choose the most fitting primary type: beast, bug, aquatic, flora, mineral, space, machina, structure, culture, or cuisine}
352
+
353
  # Monster Description
354
+ {Detailed physical description showing how the object becomes a creature. Ensure the creature uses all the unique attributes of the object. Include colors, shapes, materials, eyes, limbs, mouth, and distinctive features. This section will be used for battle narratives and lore.}
355
 
356
  # Monster Image Prompt
357
  {Extensive visual description of the Pokémon style monster for image generation. Focus on key visual elements: body shape, colors, distinctive features, pose. Keep this optimized for AI image generation.}
 
503
  }
504
  }
505
 
506
+ async function extractSimpleStats() {
507
  workflowState.currentStep = 'statsGenerating';
508
 
509
+ if (!workflowState.picletConcept) {
510
+ throw new Error('No concept available for stats extraction');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
  try {
514
+ // Extract monster name
515
+ const monsterNameMatch = workflowState.picletConcept.match(/# Monster Name\s*\n([\s\S]*?)(?=^#|$)/m);
516
+ let monsterName = monsterNameMatch ? monsterNameMatch[1].trim() : 'Unknown Monster';
517
 
518
+ // Clean and truncate name
519
+ if (monsterName.includes(',')) {
520
+ monsterName = monsterName.split(',')[0].trim();
521
  }
522
+ if (monsterName.length > 12) {
523
+ monsterName = monsterName.substring(0, 12);
 
 
 
 
 
 
 
 
 
 
 
 
524
  }
525
+ monsterName = monsterName.replace(/\*/g, ''); // Remove markdown asterisks
526
+
527
+ // Extract rarity and convert to tier
528
+ const rarityMatch = workflowState.picletConcept.match(/# Object Rarity\s*\n([\s\S]*?)(?=^#)/m);
529
+ const objectRarity = rarityMatch ? rarityMatch[1].trim().toLowerCase() : 'common';
530
+
531
+ let tier: 'low' | 'medium' | 'high' | 'legendary' = 'medium';
532
+ if (objectRarity.includes('common')) tier = 'low';
533
+ else if (objectRarity.includes('uncommon')) tier = 'medium';
534
+ else if (objectRarity.includes('rare')) tier = 'high';
535
+ else if (objectRarity.includes('legendary') || objectRarity.includes('mythical')) tier = 'legendary';
536
+
537
+ // Extract primary type
538
+ const primaryTypeMatch = workflowState.picletConcept.match(/# Primary Type\s*\n([\s\S]*?)(?=^#|$)/m);
539
+ let primaryType: any = primaryTypeMatch ? primaryTypeMatch[1].trim().toLowerCase() : 'beast';
540
+
541
+ // Extract description
542
+ const descriptionMatch = workflowState.picletConcept.match(/# Monster Description\s*\n([\s\S]*?)(?=^#|$)/m);
543
+ let description = descriptionMatch ? descriptionMatch[1].trim() : workflowState.imageCaption || 'A mysterious creature';
544
+
545
+ // Create simplified stats
546
+ const stats: PicletStats = {
547
+ name: monsterName,
548
+ description: description,
549
+ tier: tier,
550
+ primaryType: primaryType
551
+ };
552
+
553
+ workflowState.picletStats = stats;
554
+ console.log('Simple stats extracted:', stats);
555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  } catch (error) {
557
+ console.error('Failed to extract simple stats:', error);
558
  handleAPIError(error);
559
  }
560
  }
561
 
562
+ async function autoSavePicletAsCaught() {
563
  if (!workflowState.picletImage || !workflowState.imageCaption || !workflowState.picletConcept || !workflowState.imagePrompt || !workflowState.picletStats) {
564
  console.error('Cannot auto-save: missing required data');
565
  return;
 
590
  console.log('- imagePrompt type:', typeof picletData.imagePrompt);
591
  console.log('- stats:', cleanStats);
592
 
593
+ // Convert to PicletInstance format and save as caught
594
  const picletInstance = await generatedDataToPicletInstance(picletData);
595
+
596
+ // Override the caught status to auto-capture scanned Piclets
597
+ picletInstance.caught = true;
598
+ picletInstance.caughtAt = new Date();
599
+
600
  const picletId = await savePicletInstance(picletInstance);
601
+ console.log('Piclet auto-saved as caught with ID:', picletId);
602
 
603
  // If in trainer mode, notify completion
604
  if (isTrainerMode && onTrainerImageCompleted && trainerImagePaths[currentImageIndex]) {
src/lib/components/Piclets/NewlyCaughtPicletDetail.svelte CHANGED
@@ -2,11 +2,6 @@
2
  import { onMount } from 'svelte';
3
  import type { PicletInstance } from '$lib/db/schema';
4
  import { TYPE_DATA } from '$lib/types/picletTypes';
5
- import AbilityDisplay from './AbilityDisplay.svelte';
6
- import MoveDisplay from './MoveDisplay.svelte';
7
- import { picletInstanceToBattleDefinition } from '$lib/utils/battleConversion';
8
- import { recalculatePicletStats, getXpProgress, getXpTowardsNextLevel } from '$lib/services/levelingService';
9
- import { isSpecialAbilityUnlocked } from '$lib/services/unlockLevels';
10
 
11
  interface Props {
12
  instance: PicletInstance;
 
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;
src/lib/components/Piclets/PicletCard.svelte CHANGED
@@ -1,7 +1,6 @@
1
  <script lang="ts">
2
  import type { PicletInstance } from '$lib/db/schema';
3
  import { TYPE_DATA } from '$lib/types/picletTypes';
4
- import { picletInstanceToBattleDefinition } from '$lib/utils/battleConversion';
5
 
6
  interface Props {
7
  piclet: PicletInstance;
@@ -11,19 +10,8 @@
11
 
12
  let { piclet, size = 100, onClick }: Props = $props();
13
 
14
- const hpPercentage = $derived(piclet.currentHp / piclet.maxHp);
15
- const hpColor = $derived(
16
- hpPercentage > 0.5 ? '#34c759' :
17
- hpPercentage > 0.25 ? '#ffcc00' :
18
- '#ff3b30'
19
- );
20
-
21
- const typeColor = $derived(TYPE_DATA[piclet.primaryType].color);
22
  const softTypeColor = $derived(`${typeColor}20`); // Add 20% opacity for soft background
23
-
24
- // Get battle definition for enhanced ability info
25
- const battleDefinition = $derived(picletInstanceToBattleDefinition(piclet));
26
- const ability = $derived(battleDefinition.specialAbility);
27
  </script>
28
 
29
  <button
@@ -35,25 +23,19 @@
35
  <div class="image-container">
36
  <img
37
  src={piclet.imageData || piclet.imageUrl}
38
- alt={piclet.nickname || 'Piclet'}
39
  class="piclet-image"
40
  style="width: {size * 0.8}px; height: {size * 0.8}px;"
41
  />
42
- <div class="level-badge">
43
- Lv.{piclet.level}
44
  </div>
45
  </div>
46
 
47
  <div class="details-section">
48
- <p class="nickname">{piclet.nickname || 'Unknown'}</p>
49
- <div class="hp-section">
50
- <span class="hp-text">HP: {piclet.currentHp}/{piclet.maxHp}</span>
51
- <div class="hp-bar">
52
- <div
53
- class="hp-fill"
54
- style="width: {hpPercentage * 100}%; background-color: {hpColor};"
55
- ></div>
56
- </div>
57
  </div>
58
  </div>
59
  </button>
@@ -92,18 +74,22 @@
92
  object-fit: contain;
93
  }
94
 
95
- .level-badge {
96
  position: absolute;
97
  top: 4px;
98
  right: 4px;
99
- background: rgba(255, 255, 255, 0.9);
100
  padding: 2px 6px;
101
  border-radius: 8px;
102
- font-size: 10px;
103
  font-weight: bold;
104
- color: black;
105
  }
106
 
 
 
 
 
 
107
  .details-section {
108
  height: 50px;
109
  padding: 6px 8px;
@@ -116,8 +102,8 @@
116
  }
117
 
118
  .nickname {
119
- margin: 0 0 2px 0;
120
- font-size: 10px;
121
  font-weight: 600;
122
  text-align: center;
123
  overflow: hidden;
@@ -126,29 +112,27 @@
126
  color: #333;
127
  }
128
 
129
- .hp-section {
130
  display: flex;
131
- flex-direction: column;
132
- gap: 1px;
133
  }
134
 
135
- .hp-text {
136
  font-size: 8px;
 
 
137
  font-weight: 500;
138
- text-align: center;
139
- color: #666;
140
  }
141
 
142
- .hp-bar {
143
- height: 3px;
144
- background: #f0f0f0;
145
- border-radius: 1.5px;
146
- overflow: hidden;
147
  }
148
 
149
- .hp-fill {
150
- height: 100%;
151
- border-radius: 1.5px;
152
- transition: width 0.3s ease;
153
  }
154
  </style>
 
1
  <script lang="ts">
2
  import type { PicletInstance } from '$lib/db/schema';
3
  import { TYPE_DATA } from '$lib/types/picletTypes';
 
4
 
5
  interface Props {
6
  piclet: PicletInstance;
 
10
 
11
  let { piclet, size = 100, onClick }: Props = $props();
12
 
13
+ const typeColor = $derived(TYPE_DATA[piclet.primaryType]?.color || '#6c757d');
 
 
 
 
 
 
 
14
  const softTypeColor = $derived(`${typeColor}20`); // Add 20% opacity for soft background
 
 
 
 
15
  </script>
16
 
17
  <button
 
23
  <div class="image-container">
24
  <img
25
  src={piclet.imageData || piclet.imageUrl}
26
+ alt={piclet.typeId || 'Piclet'}
27
  class="piclet-image"
28
  style="width: {size * 0.8}px; height: {size * 0.8}px;"
29
  />
30
+ <div class="tier-badge tier-{piclet.tier}">
31
+ {piclet.tier}
32
  </div>
33
  </div>
34
 
35
  <div class="details-section">
36
+ <p class="nickname">{piclet.typeId}</p>
37
+ <div class="types-section">
38
+ <span class="type primary">{piclet.primaryType}</span>
 
 
 
 
 
 
39
  </div>
40
  </div>
41
  </button>
 
74
  object-fit: contain;
75
  }
76
 
77
+ .tier-badge {
78
  position: absolute;
79
  top: 4px;
80
  right: 4px;
 
81
  padding: 2px 6px;
82
  border-radius: 8px;
83
+ font-size: 9px;
84
  font-weight: bold;
85
+ text-transform: uppercase;
86
  }
87
 
88
+ .tier-low { background: #6c757d; color: white; }
89
+ .tier-medium { background: #28a745; color: white; }
90
+ .tier-high { background: #fd7e14; color: white; }
91
+ .tier-legendary { background: #dc3545; color: white; }
92
+
93
  .details-section {
94
  height: 50px;
95
  padding: 6px 8px;
 
102
  }
103
 
104
  .nickname {
105
+ margin: 0 0 4px 0;
106
+ font-size: 11px;
107
  font-weight: 600;
108
  text-align: center;
109
  overflow: hidden;
 
112
  color: #333;
113
  }
114
 
115
+ .types-section {
116
  display: flex;
117
+ gap: 4px;
118
+ justify-content: center;
119
  }
120
 
121
+ .type {
122
  font-size: 8px;
123
+ padding: 1px 4px;
124
+ border-radius: 4px;
125
  font-weight: 500;
126
+ text-transform: uppercase;
 
127
  }
128
 
129
+ .type.primary {
130
+ background: var(--type-color);
131
+ color: white;
 
 
132
  }
133
 
134
+ .type.secondary {
135
+ background: rgba(0, 0, 0, 0.1);
136
+ color: #666;
 
137
  }
138
  </style>
src/lib/components/Piclets/PicletDetail.svelte CHANGED
@@ -1,14 +1,8 @@
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
3
  import type { PicletInstance } from '$lib/db/schema';
4
  import { deletePicletInstance } from '$lib/db/piclets';
5
  import { uiStore } from '$lib/stores/ui';
6
  import { TYPE_DATA } from '$lib/types/picletTypes';
7
- import AbilityDisplay from './AbilityDisplay.svelte';
8
- import MoveDisplay from './MoveDisplay.svelte';
9
- import { picletInstanceToBattleDefinition } from '$lib/utils/battleConversion';
10
- import { recalculatePicletStats, getXpProgress, getXpTowardsNextLevel } from '$lib/services/levelingService';
11
- import { isSpecialAbilityUnlocked } from '$lib/services/unlockLevels';
12
 
13
  interface Props {
14
  instance: PicletInstance;
@@ -17,792 +11,261 @@
17
  }
18
 
19
  let { instance, onClose, onDeleted }: Props = $props();
20
- let selectedTab = $state<'about' | 'abilities'>('about');
21
 
22
- // Ensure stats are up-to-date with current level and nature
23
- const updatedInstance = $derived(recalculatePicletStats(instance));
24
-
25
- // Convert to battle definition to get enhanced ability data
26
- const battleDefinition = $derived(picletInstanceToBattleDefinition(updatedInstance));
27
-
28
- // XP and level calculations
29
- const xpProgress = $derived(getXpProgress(updatedInstance.xp, updatedInstance.level, updatedInstance.tier));
30
- const xpTowardsNext = $derived(getXpTowardsNextLevel(updatedInstance.xp, updatedInstance.level, updatedInstance.tier));
31
-
32
- // Type-based styling
33
- const typeData = $derived(TYPE_DATA[instance.primaryType]);
34
- const typeColor = $derived(typeData.color);
35
- const typeLogoPath = $derived(`/classes/${instance.primaryType}.png`);
36
-
37
- onMount(() => {
38
- uiStore.openDetailPage();
39
- return () => {
40
- uiStore.closeDetailPage();
41
- };
42
- });
43
 
44
  async function handleDelete() {
45
- if (!instance.id) return;
46
-
47
- const confirmed = confirm(`Are you sure you want to release ${instance.nickname || instance.typeId}? This action cannot be undone.`);
48
- if (!confirmed) return;
49
-
50
- try {
51
- await deletePicletInstance(instance.id);
52
- onDeleted?.();
53
- onClose();
54
- } catch (err) {
55
- console.error('Failed to delete piclet:', err);
56
- }
57
- }
58
-
59
- function getStatPercentage(value: number, max: number = 255): number {
60
- return Math.round((value / max) * 100);
61
- }
62
-
63
- function getHpColor(current: number, max: number): string {
64
- const ratio = current / max;
65
- if (ratio < 0.2) return '#ff3b30';
66
- if (ratio < 0.5) return '#ff9500';
67
- return '#34c759';
68
- }
69
-
70
-
71
- function handleDownloadJSON() {
72
- try {
73
- // Create comprehensive export data
74
- const exportData = {
75
- exportVersion: "1.0",
76
- exportedAt: new Date().toISOString(),
77
- piclet: {
78
- name: updatedInstance.nickname || updatedInstance.typeId,
79
- typeId: updatedInstance.typeId,
80
- imageData: updatedInstance.imageData,
81
- stats: {
82
- // Core identification
83
- id: updatedInstance.id,
84
- typeId: updatedInstance.typeId,
85
- nickname: updatedInstance.nickname,
86
-
87
- // Type information
88
- primaryType: updatedInstance.primaryType,
89
- secondaryType: updatedInstance.secondaryType,
90
-
91
- // Current stats
92
- currentHp: updatedInstance.currentHp,
93
- maxHp: updatedInstance.maxHp,
94
- level: updatedInstance.level,
95
- xp: updatedInstance.xp,
96
- attack: updatedInstance.attack,
97
- defense: updatedInstance.defense,
98
- fieldAttack: updatedInstance.fieldAttack,
99
- fieldDefense: updatedInstance.fieldDefense,
100
- speed: updatedInstance.speed,
101
-
102
- // Base stats
103
- baseHp: updatedInstance.baseHp,
104
- baseAttack: updatedInstance.baseAttack,
105
- baseDefense: updatedInstance.baseDefense,
106
- baseFieldAttack: updatedInstance.baseFieldAttack,
107
- baseFieldDefense: updatedInstance.baseFieldDefense,
108
- baseSpeed: updatedInstance.baseSpeed,
109
-
110
- // Additional properties
111
- nature: updatedInstance.nature,
112
- tier: updatedInstance.tier,
113
- bst: updatedInstance.bst,
114
- caught: updatedInstance.caught,
115
- caughtAt: updatedInstance.caughtAt,
116
- isInRoster: updatedInstance.isInRoster,
117
- rosterPosition: updatedInstance.rosterPosition
118
- },
119
- battleData: {
120
- moves: updatedInstance.moves,
121
- specialAbility: updatedInstance.specialAbility,
122
- specialAbilityUnlockLevel: updatedInstance.specialAbilityUnlockLevel,
123
- types: [updatedInstance.primaryType, updatedInstance.secondaryType].filter(Boolean)
124
- },
125
- generationData: {
126
- imageUrl: updatedInstance.imageUrl,
127
- imageCaption: updatedInstance.imageCaption || null,
128
- concept: updatedInstance.concept || null,
129
- imagePrompt: updatedInstance.imagePrompt || null
130
- },
131
- metadata: {
132
- level: updatedInstance.level,
133
- tier: updatedInstance.tier,
134
- createdAt: updatedInstance.caughtAt || new Date().toISOString(),
135
- exportSource: "Pictuary Game"
136
- }
137
- }
138
- };
139
-
140
- // Create and download JSON file
141
- const jsonString = JSON.stringify(exportData, null, 2);
142
- const blob = new Blob([jsonString], { type: 'application/json' });
143
- const url = URL.createObjectURL(blob);
144
-
145
- // Create temporary download link
146
- const link = document.createElement('a');
147
- link.href = url;
148
- link.download = `piclet-${(updatedInstance.nickname || updatedInstance.typeId).replace(/[^a-zA-Z0-9-_]/g, '_')}-${Date.now()}.json`;
149
-
150
- // Trigger download
151
- document.body.appendChild(link);
152
- link.click();
153
- document.body.removeChild(link);
154
-
155
- // Clean up the blob URL
156
- URL.revokeObjectURL(url);
157
-
158
- console.log('Piclet JSON exported successfully');
159
- } catch (error) {
160
- console.error('Failed to export Piclet JSON:', error);
161
- alert('Failed to export Piclet data. Please try again.');
162
  }
163
  }
164
  </script>
165
 
166
- <div class="detail-page">
167
- <div class="content-scroll">
168
- <!-- Header Card -->
169
- <div class="header-card">
170
- <div class="card-background" style="--type-color: {typeColor}; --type-logo: url('{typeLogoPath}')">
171
- <!-- Faded Logo Background -->
172
- <div class="logo-background"></div>
173
-
174
- <!-- Card Header -->
175
- <div class="card-header">
176
- <button
177
- class="back-btn-card"
178
- onclick={onClose}
179
- aria-label="Go back"
180
- >
181
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
182
- <path d="M19 12H5m0 0l7 7m-7-7l7-7"></path>
183
- </svg>
184
- </button>
185
- <h1 class="card-title">{updatedInstance.nickname || updatedInstance.typeId}</h1>
186
- <button
187
- class="download-button"
188
- onclick={handleDownloadJSON}
189
- aria-label="Download JSON"
190
- >
191
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
192
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
193
- <polyline points="7,10 12,15 17,10"></polyline>
194
- <line x1="12" y1="15" x2="12" y2="3"></line>
195
- </svg>
196
- </button>
197
- </div>
198
-
199
- <!-- Large Image Section -->
200
- <div class="large-image-section">
201
- <div class="large-image-container">
202
- <img
203
- src={updatedInstance.imageData || updatedInstance.imageUrl}
204
- alt={updatedInstance.nickname || updatedInstance.typeId}
205
- class="large-piclet-image"
206
- />
207
- </div>
208
- </div>
209
- </div>
210
  </div>
211
 
212
- <!-- Level and Status Progress -->
213
- <div class="level-xp-section">
214
- <div class="level-info">
215
- <span class="level-label">Level {updatedInstance.level}</span>
216
- {#if updatedInstance.level < 100}
217
- <span class="xp-label">{xpTowardsNext.current}/{xpTowardsNext.needed} to next level</span>
218
- {:else}
219
- <span class="xp-label">MAX LEVEL</span>
220
- {/if}
221
  </div>
222
 
223
- <!-- HP Section -->
224
- <div class="stat-row">
225
- <div class="stat-label">HP</div>
226
- <div class="hp-bar">
227
- <div
228
- class="hp-fill"
229
- style="width: {(updatedInstance.currentHp / updatedInstance.maxHp) * 100}%; background-color: {getHpColor(updatedInstance.currentHp, updatedInstance.maxHp)}"
230
- ></div>
231
- </div>
232
- <div class="stat-value">{updatedInstance.currentHp}/{updatedInstance.maxHp}</div>
233
  </div>
234
 
235
- <!-- XP Section -->
236
- {#if updatedInstance.level < 100}
237
- <div class="stat-row">
238
- <div class="stat-label">XP</div>
239
- <div class="xp-bar">
240
- <div
241
- class="xp-fill"
242
- style="width: {xpTowardsNext.percentage}%"
243
- ></div>
244
- </div>
245
- <div class="stat-value">{xpTowardsNext.current}/{xpTowardsNext.needed}</div>
246
  </div>
247
- {:else}
248
- <div class="stat-row">
249
- <div class="stat-label">XP</div>
250
- <div class="max-level-indicator">MAX LEVEL</div>
251
- </div>
252
- {/if}
253
- </div>
254
-
255
- <!-- Tab Bar -->
256
- <div class="tab-bar" style="--type-color: {typeColor}">
257
- <button
258
- class="tab-button"
259
- class:active={selectedTab === 'about'}
260
- onclick={() => selectedTab = 'about'}
261
- >
262
- About
263
- </button>
264
- <button
265
- class="tab-button"
266
- class:active={selectedTab === 'abilities'}
267
- onclick={() => selectedTab = 'abilities'}
268
- >
269
- Abilities
270
- </button>
271
- </div>
272
-
273
- <!-- Tab Content -->
274
- <div class="tab-content">
275
- {#if selectedTab === 'about'}
276
- <div class="content-card">
277
- <p class="description">{instance.description}</p>
278
-
279
- <div class="divider"></div>
280
-
281
- <h3 class="section-heading">Stats</h3>
282
- <div class="stats-list">
283
- <div class="stat-row">
284
- <span>Attack</span>
285
- <span class="stat-value">{updatedInstance.attack}</span>
286
- </div>
287
- <div class="stat-row">
288
- <span>Defense</span>
289
- <span class="stat-value">{updatedInstance.defense}</span>
290
- </div>
291
- <div class="stat-row">
292
- <span>Field Attack</span>
293
- <span class="stat-value">{updatedInstance.fieldAttack}</span>
294
- </div>
295
- <div class="stat-row">
296
- <span>Field Defense</span>
297
- <span class="stat-value">{updatedInstance.fieldDefense}</span>
298
- </div>
299
- <div class="stat-row">
300
- <span>Speed</span>
301
- <span class="stat-value">{updatedInstance.speed}</span>
302
- </div>
303
  </div>
304
-
305
- <div class="divider"></div>
306
-
307
- <div class="stat-summary">
308
- <div class="summary-item">
309
- <span class="summary-label">BST</span>
310
- <span class="summary-value">{updatedInstance.bst}</span>
311
- </div>
312
- <div class="summary-item">
313
- <span class="summary-label">Tier</span>
314
- <span class="summary-value">{updatedInstance.tier.toUpperCase()}</span>
315
- </div>
316
- </div>
317
- </div>
318
- {:else if selectedTab === 'abilities'}
319
- <div class="content-card">
320
- <h3 class="section-heading">Special Ability</h3>
321
- {#if isSpecialAbilityUnlocked(updatedInstance.specialAbilityUnlockLevel, updatedInstance.level)}
322
- <AbilityDisplay
323
- ability={updatedInstance.specialAbility}
324
- expanded={true}
325
- />
326
- {:else}
327
- <div class="locked-ability">
328
- <div class="lock-header">
329
- <span class="lock-icon">🔒</span>
330
- <span class="lock-text">Unlocks at Level {updatedInstance.specialAbilityUnlockLevel}</span>
331
- </div>
332
- <div class="locked-content">
333
- <h4>{updatedInstance.specialAbility.name}</h4>
334
- <p>This special ability will be unlocked when {updatedInstance.nickname} reaches level {updatedInstance.specialAbilityUnlockLevel}.</p>
335
- </div>
336
- </div>
337
- {/if}
338
-
339
- <div class="divider"></div>
340
-
341
- <h3 class="section-heading">Moves</h3>
342
- <div class="moves-list">
343
- {#each updatedInstance.moves as move, index}
344
- {#if move.unlockLevel <= updatedInstance.level}
345
- <MoveDisplay
346
- {move}
347
- expanded={true}
348
- />
349
- {:else}
350
- <div class="locked-move">
351
- <div class="lock-header">
352
- <span class="lock-icon">🔒</span>
353
- <span class="lock-text">Unlocks at Level {move.unlockLevel}</span>
354
- </div>
355
- <div class="locked-content">
356
- <h4>{move.name}</h4>
357
- <p>This move will be unlocked when {updatedInstance.nickname} reaches level {move.unlockLevel}.</p>
358
- </div>
359
- </div>
360
- {/if}
361
- {/each}
362
  </div>
363
- </div>
364
- {/if}
365
- </div>
366
-
367
- <!-- Actions -->
368
- <div class="bottom-actions">
369
- <button class="btn btn-danger" onclick={handleDelete}>Release Piclet</button>
370
  </div>
371
  </div>
 
 
 
 
 
 
 
 
372
  </div>
373
 
374
  <style>
375
- .detail-page {
376
  position: fixed;
377
  top: 0;
378
  left: 0;
379
  right: 0;
380
  bottom: 0;
381
- background: #f2f2f7;
382
- z-index: 1000;
383
  display: flex;
384
- flex-direction: column;
385
- }
386
-
387
- /* Content Scroll */
388
- .content-scroll {
389
- flex: 1;
390
- overflow-y: auto;
391
- -webkit-overflow-scrolling: touch;
392
  }
393
 
394
- /* Header Card */
395
- .header-card {
396
- margin-bottom: 16px;
 
 
 
397
  overflow: hidden;
398
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
399
- position: relative;
400
  }
401
 
402
- .card-background {
403
- background: linear-gradient(135deg, var(--type-color, #4CAF50) 0%, color-mix(in srgb, var(--type-color, #4CAF50) 80%, white) 100%);
404
- padding: 24px;
405
- padding-top: calc(24px + env(safe-area-inset-top, 0));
406
  position: relative;
407
- overflow: hidden;
408
- }
409
-
410
- .logo-background {
411
- position: absolute;
412
- bottom: 5px;
413
- right: 5px;
414
- width: 120px;
415
- height: 120px;
416
- background-image: var(--type-logo);
417
- background-size: contain;
418
- background-repeat: no-repeat;
419
- background-position: center;
420
- opacity: 0.15;
421
- pointer-events: none;
422
- z-index: 1;
423
- }
424
-
425
- .card-header {
426
  display: flex;
427
  justify-content: space-between;
428
  align-items: center;
429
- margin-bottom: 20px;
430
- position: relative;
431
- z-index: 2;
432
  }
433
 
434
- .card-title {
435
- margin: 0;
436
- font-size: 24px;
437
- font-weight: bold;
438
- color: white;
439
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
440
- flex: 1;
441
- text-align: center;
442
- }
443
-
444
- .back-btn-card {
445
- background: rgba(255, 255, 255, 0.2);
446
  border: none;
447
  color: white;
 
448
  cursor: pointer;
449
- padding: 8px;
450
- border-radius: 12px;
 
451
  display: flex;
452
  align-items: center;
453
  justify-content: center;
454
- transition: all 0.2s;
 
455
  }
456
 
457
- .back-btn-card:hover {
458
- background: rgba(255, 255, 255, 0.3);
459
- }
460
-
461
- .download-button {
462
  background: rgba(255, 255, 255, 0.2);
463
- border: none;
464
- color: white;
465
- cursor: pointer;
466
- padding: 8px;
467
- border-radius: 12px;
468
- display: flex;
469
- align-items: center;
470
- justify-content: center;
471
- transition: all 0.2s;
472
- }
473
-
474
- .download-button:hover {
475
- background: rgba(255, 255, 255, 0.3);
476
- transform: scale(1.05);
477
- }
478
-
479
- .download-button:active {
480
- transform: scale(0.95);
481
  }
482
 
483
- .download-button:disabled {
484
- opacity: 0.5;
485
- cursor: not-allowed;
486
- }
487
-
488
- .download-button svg {
489
- width: 20px;
490
- height: 20px;
491
- }
492
-
493
- .large-image-section {
494
- display: flex;
495
- flex-direction: column;
496
- align-items: center;
497
- position: relative;
498
- z-index: 2;
499
- }
500
-
501
- .large-image-container {
502
- display: flex;
503
- align-items: center;
504
- justify-content: center;
505
- width: 360px;
506
- height: 360px;
507
- }
508
-
509
- .large-piclet-image {
510
- width: 360px;
511
- height: 360px;
512
- object-fit: contain;
513
- filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
514
  }
515
 
 
 
 
 
516
 
517
- /* Tab Bar */
518
- .tab-bar {
519
- margin: 0 16px 16px;
520
- height: 36px;
521
- background: #e5e5ea;
522
- border-radius: 12px;
523
- display: flex;
524
- padding: 2px;
525
- }
526
-
527
- .tab-button {
528
  flex: 1;
529
- background: none;
530
- border: none;
531
- border-radius: 10px;
532
- font-size: 14px;
533
- font-weight: 500;
534
- color: #8e8e93;
535
- cursor: pointer;
536
- transition: all 0.2s;
537
- }
538
-
539
- .tab-button.active {
540
- background: var(--type-color, #4CAF50);
541
- color: white;
542
- box-shadow: 0 2px 4px color-mix(in srgb, var(--type-color, #4CAF50) 30%, transparent);
543
  }
544
 
545
- /* Tab Content */
546
- .tab-content {
547
- margin: 0 16px 16px;
548
  }
549
 
550
- .content-card {
551
- background: white;
 
 
552
  border-radius: 12px;
553
- padding: 16px;
554
- border: 0.5px solid #c6c6c8;
555
- }
556
-
557
- .description {
558
- margin: 0 0 16px;
559
- font-size: 16px;
560
- line-height: 1.4;
561
- color: #000;
562
  }
563
 
564
-
565
- /* Stats Tab */
566
-
567
- .stats-list {
568
  display: flex;
569
  flex-direction: column;
570
- gap: 12px;
571
- }
572
-
573
- .stat-row {
574
- display: flex;
575
- justify-content: space-between;
576
- font-size: 15px;
577
- }
578
-
579
- .stat-value {
580
- font-weight: 600;
581
- }
582
-
583
- .divider {
584
- height: 1px;
585
- background: #e5e5ea;
586
- margin: 16px 0;
587
- }
588
-
589
- .stat-summary {
590
- display: flex;
591
- justify-content: space-around;
592
  }
593
 
594
- .summary-item {
595
- text-align: center;
596
- }
597
-
598
- .summary-label {
599
- display: block;
600
- font-size: 14px;
601
- color: #8e8e93;
602
- margin-bottom: 4px;
603
- }
604
-
605
- .summary-value {
606
- font-size: 16px;
607
  font-weight: bold;
608
- color: #000;
609
- }
610
-
611
-
612
- /* Bottom Actions */
613
- .bottom-actions {
614
- padding: 16px;
615
- background: white;
616
- border-top: 0.5px solid #c6c6c8;
617
  text-align: center;
 
618
  }
619
 
620
- .section-heading {
621
- font-size: 18px;
622
- font-weight: 600;
623
- color: #495057;
624
- margin: 0 0 12px 0;
625
- }
626
-
627
- .btn {
628
- padding: 0.75rem 1.5rem;
629
- border: none;
630
- border-radius: 8px;
631
- font-size: 16px;
632
- font-weight: 600;
633
- cursor: pointer;
634
- transition: transform 0.2s;
635
- }
636
-
637
- .btn:active {
638
- transform: scale(0.95);
639
- }
640
-
641
- .btn-danger {
642
- background: #ff3b30;
643
- color: white;
644
- width: 100%;
645
- }
646
-
647
- /* Enhanced ability and move display styles */
648
- .moves-list {
649
  display: flex;
650
- flex-direction: column;
651
- gap: 4px;
652
- }
653
-
654
- /* Locked content styles */
655
- .locked-ability,
656
- .locked-move {
657
- background: #f8f9fa;
658
- border: 1px dashed #dee2e6;
659
- border-radius: 8px;
660
- padding: 12px;
661
- margin-bottom: 8px;
662
- opacity: 0.7;
663
- }
664
-
665
- .lock-header {
666
- display: flex;
667
- align-items: center;
668
- gap: 8px;
669
- margin-bottom: 8px;
670
- }
671
-
672
- .lock-icon {
673
- font-size: 16px;
674
  }
675
 
676
- .lock-text {
677
- font-size: 12px;
 
 
678
  font-weight: 600;
679
- color: #6c757d;
680
  text-transform: uppercase;
681
- letter-spacing: 0.5px;
682
  }
683
 
684
- .locked-content h4 {
685
- margin: 0 0 4px 0;
686
- font-size: 16px;
687
- font-weight: 600;
688
- color: #495057;
689
  }
690
 
691
- .locked-content p {
692
- margin: 0;
693
- font-size: 14px;
694
- color: #6c757d;
695
- font-style: italic;
696
  }
697
 
698
- /* Level and Status Section */
699
- .level-xp-section {
700
- background: white;
701
- margin: 0 16px 16px;
702
- border-radius: 12px;
703
- padding: 16px;
704
- border: 0.5px solid #c6c6c8;
705
  }
706
 
707
- .level-info {
708
  display: flex;
709
- justify-content: space-between;
710
- align-items: center;
711
- margin-bottom: 16px;
712
- }
713
-
714
- .level-label {
715
- font-size: 18px;
716
- font-weight: 700;
717
- color: #1a1a1a;
718
- }
719
-
720
- .xp-label {
721
- font-size: 14px;
722
- font-weight: 500;
723
- color: #8e8e93;
724
  }
725
 
726
- /* Stat Rows (HP and XP bars) */
727
- .stat-row {
728
  display: flex;
729
  align-items: center;
730
- gap: 8px;
731
- margin-bottom: 8px;
732
  }
733
 
734
- .stat-row:last-child {
735
- margin-bottom: 0;
736
- }
737
-
738
- .stat-label {
739
- font-size: 12px;
740
  font-weight: 600;
741
- color: #666;
742
- text-transform: uppercase;
743
- letter-spacing: 0.3px;
744
- width: 24px;
745
- flex-shrink: 0;
746
- }
747
-
748
- /* HP Bar */
749
- .hp-bar {
750
- height: 8px;
751
- background: #e0e0e0;
752
- border-radius: 4px;
753
- overflow: hidden;
754
- flex: 1;
755
- min-width: 80px;
756
  }
757
 
758
- .hp-fill {
759
- height: 100%;
760
- transition: width 0.5s ease, background-color 0.3s ease;
761
- }
762
-
763
- /* XP Bar */
764
- .xp-bar {
765
- height: 8px;
766
- background: #e0e0e0;
767
- border-radius: 4px;
768
- overflow: hidden;
769
- flex: 1;
770
- min-width: 80px;
771
- }
772
-
773
- .xp-fill {
774
- height: 100%;
775
- background: #2196f3;
776
- transition: width 1.2s ease-out;
777
  }
778
 
779
- .stat-value {
780
- font-size: 12px;
 
 
 
 
781
  font-weight: 600;
782
- color: #666;
783
- min-width: 60px;
784
- text-align: right;
785
- flex-shrink: 0;
786
  }
787
 
788
- .max-level-indicator {
789
- font-size: 12px;
790
- font-weight: 600;
791
- color: #ff6f00;
792
- background: rgba(255, 111, 0, 0.1);
793
- border: 1px solid #ff6f00;
794
- border-radius: 4px;
795
- padding: 2px 8px;
796
- flex: 1;
797
- text-align: center;
798
- }
799
-
800
- @media (min-width: 768px) {
801
- .detail-page {
802
- position: relative;
803
- max-width: 600px;
804
- margin: 0 auto;
805
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
806
- }
807
  }
808
  </style>
 
1
  <script lang="ts">
 
2
  import type { PicletInstance } from '$lib/db/schema';
3
  import { deletePicletInstance } from '$lib/db/piclets';
4
  import { uiStore } from '$lib/stores/ui';
5
  import { TYPE_DATA } from '$lib/types/picletTypes';
 
 
 
 
 
6
 
7
  interface Props {
8
  instance: PicletInstance;
 
11
  }
12
 
13
  let { instance, onClose, onDeleted }: Props = $props();
 
14
 
15
+ const typeColor = $derived(TYPE_DATA[instance.primaryType]?.color || '#007bff');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  async function handleDelete() {
18
+ if (confirm(`Are you sure you want to delete ${instance.typeId}?`)) {
19
+ try {
20
+ await deletePicletInstance(instance.id!);
21
+ onDeleted?.();
22
+ onClose();
23
+ } catch (error) {
24
+ console.error('Failed to delete piclet:', error);
25
+ alert('Failed to delete piclet');
26
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
  }
29
  </script>
30
 
31
+ <div class="piclet-detail-overlay" onclick={onClose}>
32
+ <div class="piclet-detail" onclick={(e) => e.stopPropagation()}>
33
+ <!-- Header -->
34
+ <div class="header" style="--type-color: {typeColor}">
35
+ <button class="close-button" onclick={onClose}>×</button>
36
+ <div class="tier-badge tier-{instance.tier}">{instance.tier}</div>
37
+ </div>
38
+
39
+ <!-- Main Content -->
40
+ <div class="content">
41
+ <!-- Image Section -->
42
+ <div class="image-section">
43
+ <img
44
+ src={instance.imageData || instance.imageUrl}
45
+ alt={instance.typeId}
46
+ class="piclet-image"
47
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  </div>
49
 
50
+ <!-- Info Section -->
51
+ <div class="info-section">
52
+ <h2 class="name">{instance.typeId}</h2>
53
+
54
+ <div class="types">
55
+ <span class="type primary" style="background-color: {typeColor}">
56
+ {instance.primaryType}
57
+ </span>
 
58
  </div>
59
 
60
+ <div class="description">
61
+ <h3>Description</h3>
62
+ <p>{instance.description}</p>
 
 
 
 
 
 
 
63
  </div>
64
 
65
+ <div class="metadata">
66
+ <div class="meta-item">
67
+ <strong>Caught:</strong>
68
+ {instance.caught ? 'Yes' : 'No'}
 
 
 
 
 
 
 
69
  </div>
70
+ {#if instance.caughtAt}
71
+ <div class="meta-item">
72
+ <strong>Caught on:</strong>
73
+ {new Date(instance.caughtAt).toLocaleDateString()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </div>
75
+ {/if}
76
+ {#if instance.isInRoster}
77
+ <div class="meta-item">
78
+ <strong>Status:</strong>
79
+ <span class="roster-badge">In Battle Roster</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
+ {/if}
82
+ </div>
 
 
 
 
 
83
  </div>
84
  </div>
85
+
86
+ <!-- Actions -->
87
+ <div class="actions">
88
+ <button class="delete-button" onclick={handleDelete}>
89
+ 🗑️ Delete
90
+ </button>
91
+ </div>
92
+ </div>
93
  </div>
94
 
95
  <style>
96
+ .piclet-detail-overlay {
97
  position: fixed;
98
  top: 0;
99
  left: 0;
100
  right: 0;
101
  bottom: 0;
102
+ background: rgba(0, 0, 0, 0.5);
 
103
  display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ z-index: 1000;
107
+ padding: 1rem;
 
 
 
 
108
  }
109
 
110
+ .piclet-detail {
111
+ background: white;
112
+ border-radius: 16px;
113
+ width: 100%;
114
+ max-width: 400px;
115
+ max-height: 80vh;
116
  overflow: hidden;
117
+ display: flex;
118
+ flex-direction: column;
119
  }
120
 
121
+ .header {
122
+ background: var(--type-color, #007bff);
123
+ color: white;
124
+ padding: 1rem;
125
  position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  display: flex;
127
  justify-content: space-between;
128
  align-items: center;
 
 
 
129
  }
130
 
131
+ .close-button {
132
+ background: none;
 
 
 
 
 
 
 
 
 
 
133
  border: none;
134
  color: white;
135
+ font-size: 1.5rem;
136
  cursor: pointer;
137
+ padding: 0;
138
+ width: 32px;
139
+ height: 32px;
140
  display: flex;
141
  align-items: center;
142
  justify-content: center;
143
+ border-radius: 50%;
144
+ transition: background 0.2s;
145
  }
146
 
147
+ .close-button:hover {
 
 
 
 
148
  background: rgba(255, 255, 255, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
 
151
+ .tier-badge {
152
+ padding: 0.5rem 1rem;
153
+ border-radius: 20px;
154
+ font-size: 0.8rem;
155
+ font-weight: bold;
156
+ text-transform: uppercase;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
 
159
+ .tier-low { background: #6c757d; }
160
+ .tier-medium { background: #28a745; }
161
+ .tier-high { background: #fd7e14; }
162
+ .tier-legendary { background: #dc3545; }
163
 
164
+ .content {
 
 
 
 
 
 
 
 
 
 
165
  flex: 1;
166
+ overflow-y: auto;
167
+ padding: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
 
170
+ .image-section {
171
+ text-align: center;
172
+ margin-bottom: 1.5rem;
173
  }
174
 
175
+ .piclet-image {
176
+ width: 150px;
177
+ height: 150px;
178
+ object-fit: contain;
179
  border-radius: 12px;
180
+ background: rgba(0, 0, 0, 0.05);
 
 
 
 
 
 
 
 
181
  }
182
 
183
+ .info-section {
 
 
 
184
  display: flex;
185
  flex-direction: column;
186
+ gap: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  }
188
 
189
+ .name {
190
+ margin: 0;
191
+ font-size: 1.5rem;
 
 
 
 
 
 
 
 
 
 
192
  font-weight: bold;
 
 
 
 
 
 
 
 
 
193
  text-align: center;
194
+ color: var(--type-color, #007bff);
195
  }
196
 
197
+ .types {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  display: flex;
199
+ gap: 0.5rem;
200
+ justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
202
 
203
+ .type {
204
+ padding: 0.5rem 1rem;
205
+ border-radius: 20px;
206
+ font-size: 0.9rem;
207
  font-weight: 600;
 
208
  text-transform: uppercase;
209
+ color: white;
210
  }
211
 
212
+ .type.secondary {
213
+ background: #6c757d;
 
 
 
214
  }
215
 
216
+ .description h3 {
217
+ margin: 0 0 0.5rem 0;
218
+ color: #333;
219
+ font-size: 1.1rem;
 
220
  }
221
 
222
+ .description p {
223
+ margin: 0;
224
+ line-height: 1.5;
225
+ color: #666;
 
 
 
226
  }
227
 
228
+ .metadata {
229
  display: flex;
230
+ flex-direction: column;
231
+ gap: 0.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  }
233
 
234
+ .meta-item {
 
235
  display: flex;
236
  align-items: center;
237
+ gap: 0.5rem;
238
+ font-size: 0.9rem;
239
  }
240
 
241
+ .roster-badge {
242
+ background: #28a745;
243
+ color: white;
244
+ padding: 0.25rem 0.5rem;
245
+ border-radius: 12px;
246
+ font-size: 0.8rem;
247
  font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  }
249
 
250
+ .actions {
251
+ padding: 1rem 1.5rem;
252
+ border-top: 1px solid #eee;
253
+ display: flex;
254
+ justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  }
256
 
257
+ .delete-button {
258
+ background: #dc3545;
259
+ color: white;
260
+ border: none;
261
+ padding: 0.75rem 1.5rem;
262
+ border-radius: 8px;
263
  font-weight: 600;
264
+ cursor: pointer;
265
+ transition: background 0.2s;
 
 
266
  }
267
 
268
+ .delete-button:hover {
269
+ background: #c82333;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
  </style>
src/lib/db/battleService.ts DELETED
@@ -1,170 +0,0 @@
1
- import type { PicletInstance, BattleState, BattlePhase, BattleMove } from './schema';
2
- import { db } from './index';
3
- import { getEffectivenessMultiplier, AttackType } from '../types/picletTypes';
4
-
5
- export class BattleService {
6
- // Initialize a new battle
7
- static createBattleState(
8
- playerPiclet: PicletInstance,
9
- enemyPiclet: PicletInstance,
10
- isWildBattle: boolean = true
11
- ): BattleState {
12
- return {
13
- phase: 'intro' as BattlePhase,
14
- currentTurn: 0,
15
- playerPiclet,
16
- enemyPiclet,
17
- isWildBattle,
18
- processingTurn: false,
19
- battleEnded: false
20
- };
21
- }
22
-
23
- // Calculate damage with type effectiveness
24
- static calculateDamage(
25
- attacker: PicletInstance,
26
- defender: PicletInstance,
27
- move: BattleMove
28
- ): { damage: number; effectiveness: number } {
29
- const baseDamage = move.power || 50;
30
- const attackStat = move.power > 0 ? attacker.attack : attacker.fieldAttack;
31
- const defenseStat = move.power > 0 ? defender.defense : defender.fieldDefense;
32
-
33
- // Type effectiveness
34
- const effectiveness = getEffectivenessMultiplier(
35
- move.type,
36
- defender.primaryType,
37
- defender.secondaryType
38
- );
39
-
40
- // STAB (Same Type Attack Bonus) - 1.5x if move type matches attacker's type
41
- const stab = (move.type === attacker.primaryType as unknown as AttackType || move.type === attacker.secondaryType as unknown as AttackType) ? 1.5 : 1;
42
-
43
- // Simple damage formula
44
- let damage = Math.floor((baseDamage * (attackStat / defenseStat) * 0.5) + 10);
45
-
46
- // Apply type effectiveness and STAB
47
- damage = Math.floor(damage * effectiveness * stab);
48
-
49
- // Add some randomness (85-100% of calculated damage)
50
- const randomFactor = 0.85 + Math.random() * 0.15;
51
- damage = Math.floor(damage * randomFactor);
52
-
53
- // Minimum 1 damage for non-immune moves
54
- if (effectiveness > 0 && damage < 1) {
55
- damage = 1;
56
- }
57
-
58
- return { damage, effectiveness };
59
- }
60
-
61
- // Check if move hits (based on accuracy)
62
- static doesMoveHit(accuracy: number): boolean {
63
- return Math.random() * 100 < accuracy;
64
- }
65
-
66
- // Calculate capture rate for wild piclets
67
- static calculateCaptureRate(
68
- targetPiclet: PicletInstance,
69
- targetMaxHp: number
70
- ): number {
71
- const hpFactor = (targetMaxHp - targetPiclet.currentHp) / targetMaxHp;
72
- const levelFactor = Math.max(0.5, 1 - (targetPiclet.level / 100));
73
-
74
- // Base capture rate increases with damage and lower level
75
- const baseRate = 0.3; // 30% base rate
76
- const captureRate = baseRate + (hpFactor * 0.4) + (levelFactor * 0.3);
77
-
78
- return Math.min(0.95, captureRate); // Cap at 95%
79
- }
80
-
81
- // Attempt to catch a wild piclet
82
- static attemptCapture(
83
- targetPiclet: PicletInstance
84
- ): { success: boolean; shakes: number } {
85
- const captureRate = this.calculateCaptureRate(targetPiclet, targetPiclet.maxHp);
86
- const roll = Math.random();
87
-
88
- // Calculate shakes (0-3)
89
- let shakes = 0;
90
- if (roll < captureRate * 0.9) shakes = 1;
91
- if (roll < captureRate * 0.7) shakes = 2;
92
- if (roll < captureRate * 0.5) shakes = 3;
93
-
94
- return {
95
- success: roll < captureRate,
96
- shakes
97
- };
98
- }
99
-
100
- // Create a caught piclet instance
101
- static async createCaughtPiclet(
102
- wildPiclet: PicletInstance
103
- ): Promise<PicletInstance> {
104
- const caughtPiclet: Omit<PicletInstance, 'id'> = {
105
- ...wildPiclet,
106
- isInRoster: false, // Goes to storage initially
107
- rosterPosition: undefined,
108
- caughtAt: new Date()
109
- };
110
-
111
- const id = await db.picletInstances.add(caughtPiclet);
112
- return { ...caughtPiclet, id };
113
- }
114
-
115
- // Calculate experience gain
116
- static calculateExpGain(
117
- defeatedPiclet: PicletInstance,
118
- isWild: boolean
119
- ): number {
120
- const baseExp = 50 + (defeatedPiclet.level * 10);
121
- const wildModifier = isWild ? 1 : 1.5; // Trainer piclets give more exp
122
-
123
- return Math.floor(baseExp * wildModifier);
124
- }
125
-
126
- // Check if piclet should level up
127
- static checkLevelUp(
128
- piclet: PicletInstance,
129
- expGained: number
130
- ): { leveledUp: boolean; newLevel: number } {
131
- const newExp = piclet.xp + expGained;
132
- const expForNextLevel = this.getExpForLevel(piclet.level + 1);
133
-
134
- if (newExp >= expForNextLevel) {
135
- return {
136
- leveledUp: true,
137
- newLevel: piclet.level + 1
138
- };
139
- }
140
-
141
- return {
142
- leveledUp: false,
143
- newLevel: piclet.level
144
- };
145
- }
146
-
147
- // Get experience required for a level
148
- static getExpForLevel(level: number): number {
149
- // Simple exponential growth formula
150
- return Math.floor(Math.pow(level, 2.5) * 10);
151
- }
152
-
153
- // Apply stat boosts for level up
154
- static applyLevelUpStats(piclet: PicletInstance): PicletInstance {
155
- // Simple stat growth (5-10% increase per level)
156
- const growthFactor = 1.07;
157
-
158
- return {
159
- ...piclet,
160
- level: piclet.level + 1,
161
- maxHp: Math.floor(piclet.maxHp * growthFactor),
162
- currentHp: Math.floor(piclet.currentHp * growthFactor),
163
- attack: Math.floor(piclet.attack * growthFactor),
164
- defense: Math.floor(piclet.defense * growthFactor),
165
- fieldAttack: Math.floor(piclet.fieldAttack * growthFactor),
166
- fieldDefense: Math.floor(piclet.fieldDefense * growthFactor),
167
- speed: Math.floor(piclet.speed * growthFactor)
168
- };
169
- }
170
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/db/encounterService.ts CHANGED
@@ -2,14 +2,12 @@ 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, getUncaughtPiclets } from './piclets';
6
- import { calculateStat, calculateHp } from '../services/levelingService';
7
 
8
  // Configuration
9
  const ENCOUNTER_REFRESH_HOURS = 2;
10
  const MIN_WILD_ENCOUNTERS = 2;
11
- const MAX_WILD_ENCOUNTERS = 3;
12
- const LEVEL_VARIANCE = 2;
13
 
14
  export class EncounterService {
15
  // Check if encounters should be refreshed
@@ -38,61 +36,25 @@ export class EncounterService {
38
  await db.encounters.clear();
39
  }
40
 
41
-
42
- // Generate new encounters
43
  static async generateEncounters(): Promise<Encounter[]> {
44
  const encounters: Omit<Encounter, 'id'>[] = [];
45
 
46
- // Check for "Your First Piclet" scenario first
47
  const caughtPiclets = await getCaughtPiclets();
48
- const uncaughtPiclets = await getUncaughtPiclets();
49
 
50
  if (caughtPiclets.length === 0) {
51
- // Player has no caught piclets
52
- if (uncaughtPiclets.length > 0) {
53
- // Player has scanned piclets but hasn't caught any - create first piclet encounter
54
- console.log('Player has scanned piclets but no caught ones - creating first piclet encounter');
55
- const firstPicletEncounter = await this.createFirstCatchEncounter(uncaughtPiclets);
56
- encounters.push(firstPicletEncounter);
57
- } else {
58
- // Player has no piclets at all - return empty encounters
59
- console.log('Player has no piclets at all - returning empty encounters');
60
- await db.encounters.clear();
61
- await markEncountersRefreshed();
62
- return [];
63
- }
64
-
65
- // Save the first piclet encounter and return
66
  await db.encounters.clear();
67
- for (const encounter of encounters) {
68
- await db.encounters.add(encounter);
69
- }
70
  await markEncountersRefreshed();
71
- return await this.getCurrentEncounters();
72
  }
73
 
74
- // Player has caught piclets - generate normal encounters
75
- console.log('Generating normal encounters for player with caught piclets');
76
-
77
- // Generate wild piclet encounters FIRST to ensure they're included
78
- const wildEncounters = await this.generateWildEncounters();
79
- console.log('Wild encounters generated:', wildEncounters.length);
80
  encounters.push(...wildEncounters);
81
-
82
- // Always add shop and health center
83
- encounters.push({
84
- type: EncounterType.SHOP,
85
- title: 'Piclet Shop',
86
- description: 'Buy items and supplies for your journey',
87
- createdAt: new Date()
88
- });
89
-
90
- encounters.push({
91
- type: EncounterType.HEALTH_CENTER,
92
- title: 'Health Center',
93
- description: 'Heal your piclets back to full health',
94
- createdAt: new Date()
95
- });
96
 
97
  // Clear existing encounters and add new ones
98
  await db.encounters.clear();
@@ -104,182 +66,50 @@ export class EncounterService {
104
  return await this.getCurrentEncounters();
105
  }
106
 
107
- // Create first catch encounter
108
- private static async createFirstCatchEncounter(uncaughtPiclets: PicletInstance[]): Promise<Omit<Encounter, 'id'>> {
109
- // Use the most recently scanned (last in array) uncaught piclet
110
- const latestPiclet = uncaughtPiclets[uncaughtPiclets.length - 1];
111
-
112
- // Use the piclet's nickname or typeId for display
113
- const displayName = latestPiclet.nickname || latestPiclet.typeId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
114
-
115
- return {
116
- type: EncounterType.FIRST_PICLET,
117
- title: 'Your First Piclet!',
118
- description: `A friendly ${displayName} appears! This one seems easy to catch.`,
119
- picletInstanceId: latestPiclet.id, // Reference to the specific piclet instance
120
- picletTypeId: latestPiclet.typeId,
121
- enemyLevel: 5, // Easy level for first encounter
122
- createdAt: new Date()
123
- };
124
- }
125
-
126
- // Generate wild piclet encounters
127
- private static async generateWildEncounters(): Promise<Omit<Encounter, 'id'>[]> {
128
  const encounters: Omit<Encounter, 'id'>[] = [];
129
 
130
- // Get player's average level
131
- const avgLevel = await this.getPlayerAverageLevel();
132
-
133
- // Get uncaught piclets (these can appear as wild encounters to be caught)
134
- const uncaughtPiclets = await getUncaughtPiclets();
135
- console.log('Uncaught piclets for wild encounters:', uncaughtPiclets.length);
136
 
137
- if (uncaughtPiclets.length === 0) {
138
- console.log('No uncaught piclets - returning empty wild encounters');
139
  return encounters;
140
  }
 
 
 
141
 
142
- // Use uncaught piclets as templates for wild encounters
143
- const availablePiclets = uncaughtPiclets;
144
- console.log('Available piclets for encounters:', availablePiclets.map(p => p.typeId));
145
-
146
- const encounterCount = MIN_WILD_ENCOUNTERS + Math.floor(Math.random() * (MAX_WILD_ENCOUNTERS - MIN_WILD_ENCOUNTERS + 1));
147
- console.log('Generating', encounterCount, 'wild encounters');
148
-
149
- for (let i = 0; i < encounterCount; i++) {
150
- // Pick a random piclet from available ones
151
- const piclet = availablePiclets[Math.floor(Math.random() * availablePiclets.length)];
152
-
153
- const levelVariance = Math.floor(Math.random() * (LEVEL_VARIANCE * 2 + 1)) - LEVEL_VARIANCE;
154
- const enemyLevel = Math.max(1, avgLevel + levelVariance);
155
 
156
- // Use the piclet's nickname or typeId for display
157
- const displayName = piclet.nickname || piclet.typeId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
158
 
159
- const wildEncounter = {
160
  type: EncounterType.WILD_PICLET,
161
- title: `Wild ${displayName} Appeared!`,
162
- description: `A level ${enemyLevel} ${displayName} blocks your path!`,
163
- picletTypeId: piclet.typeId,
164
- enemyLevel,
 
165
  createdAt: new Date()
166
  };
167
 
168
- console.log('Created wild encounter:', wildEncounter.title, 'with typeId:', wildEncounter.picletTypeId);
169
- encounters.push(wildEncounter);
170
  }
171
-
172
- console.log('Generated', encounters.length, 'wild encounters');
173
  return encounters;
174
  }
175
 
176
- // Get player's average piclet level
177
- private static async getPlayerAverageLevel(): Promise<number> {
178
- const rosterPiclets = await db.picletInstances
179
- .where('isInRoster')
180
- .equals(1) // Dexie uses 1 for true in indexed fields
181
- .toArray();
182
-
183
- if (rosterPiclets.length === 0) {
184
- const caughtPiclets = await getCaughtPiclets();
185
- if (caughtPiclets.length === 0) return 5; // Default starting level
186
-
187
- const totalLevel = caughtPiclets.reduce((sum, p) => sum + p.level, 0);
188
- return Math.round(totalLevel / caughtPiclets.length);
189
- }
190
-
191
- const totalLevel = rosterPiclets.reduce((sum, p) => sum + p.level, 0);
192
- return Math.round(totalLevel / rosterPiclets.length);
193
  }
194
 
195
-
196
- // Catch a wild piclet (marks uncaught piclet as caught, or creates new instance for recatches)
197
- static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
198
- if (!encounter.picletTypeId) throw new Error('No piclet type specified');
199
-
200
- // First check if this is an uncaught piclet that can be directly marked as caught
201
- const uncaughtPiclets = await getUncaughtPiclets();
202
- const uncaughtPiclet = uncaughtPiclets.find(p => p.typeId === encounter.picletTypeId);
203
-
204
- if (uncaughtPiclet) {
205
- // This is the first time catching this type - mark the existing uncaught piclet as caught
206
- const newLevel = encounter.enemyLevel || uncaughtPiclet.level;
207
-
208
- // Update the existing uncaught piclet
209
- const updates = {
210
- caught: true,
211
- caughtAt: new Date(),
212
- level: newLevel,
213
- xp: 0,
214
- currentHp: calculateHp(uncaughtPiclet.baseHp, newLevel),
215
- maxHp: calculateHp(uncaughtPiclet.baseHp, newLevel),
216
- attack: calculateStat(uncaughtPiclet.baseAttack, newLevel),
217
- defense: calculateStat(uncaughtPiclet.baseDefense, newLevel),
218
- fieldAttack: calculateStat(uncaughtPiclet.baseFieldAttack, newLevel),
219
- fieldDefense: calculateStat(uncaughtPiclet.baseFieldDefense, newLevel),
220
- speed: calculateStat(uncaughtPiclet.baseSpeed, newLevel),
221
-
222
- // Reset move PP to full
223
- moves: uncaughtPiclet.moves.map(move => ({
224
- ...move,
225
- currentPp: move.pp
226
- }))
227
- };
228
-
229
- // Set roster position 0 if this is the first caught piclet
230
- const existingCaughtPiclets = await getCaughtPiclets();
231
- if (existingCaughtPiclets.length === 0) {
232
- Object.assign(updates, {
233
- rosterPosition: 0,
234
- isInRoster: true
235
- });
236
- }
237
-
238
- // Update the existing piclet
239
- await db.picletInstances.update(uncaughtPiclet.id!, updates);
240
-
241
- // Return the updated piclet
242
- return { ...uncaughtPiclet, ...updates };
243
- }
244
-
245
- // If no uncaught piclet found, this is a recatch - create a new instance using caught piclet as template
246
- const caughtPiclets = await getCaughtPiclets();
247
- const templatePiclet = caughtPiclets.find(p => p.typeId === encounter.picletTypeId);
248
-
249
- if (!templatePiclet) {
250
- throw new Error(`Piclet type not found: ${encounter.picletTypeId}`);
251
- }
252
-
253
- // Create a new piclet instance for recatch
254
- const newLevel = encounter.enemyLevel || 5;
255
-
256
- const newPiclet: Omit<PicletInstance, 'id'> = {
257
- ...templatePiclet,
258
- level: newLevel,
259
- xp: 0,
260
- currentHp: calculateHp(templatePiclet.baseHp, newLevel),
261
- maxHp: calculateHp(templatePiclet.baseHp, newLevel),
262
- attack: calculateStat(templatePiclet.baseAttack, newLevel),
263
- defense: calculateStat(templatePiclet.baseDefense, newLevel),
264
- fieldAttack: calculateStat(templatePiclet.baseFieldAttack, newLevel),
265
- fieldDefense: calculateStat(templatePiclet.baseFieldDefense, newLevel),
266
- speed: calculateStat(templatePiclet.baseSpeed, newLevel),
267
-
268
- // Reset move PP to full
269
- moves: templatePiclet.moves.map(move => ({
270
- ...move,
271
- currentPp: move.pp
272
- })),
273
-
274
- // Clear roster info for new catch
275
- isInRoster: false,
276
- rosterPosition: undefined,
277
- caught: true,
278
- caughtAt: new Date()
279
- };
280
-
281
- // Save the new piclet
282
- const id = await db.picletInstances.add(newPiclet);
283
- return { ...newPiclet, id };
284
  }
285
  }
 
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
 
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();
 
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/piclets.ts CHANGED
@@ -1,9 +1,7 @@
1
  import { db } from './index';
2
- import type { PicletInstance, BattleMove } from './schema';
3
- import { PicletType, getTypeFromConcept } from '../types/picletTypes';
4
  import type { PicletStats } from '../types';
5
- import type { Move as BattleEngineMove } from '../battle-engine/types';
6
- import { generateUnlockLevels } from '../services/unlockLevels';
7
 
8
  // Interface for generated piclet data (from PicletResult component)
9
  interface GeneratedPicletData {
@@ -18,140 +16,34 @@ interface GeneratedPicletData {
18
  }
19
 
20
  // Convert generated piclet data to a PicletInstance
21
- export async function generatedDataToPicletInstance(data: GeneratedPicletData, level: number = 5): Promise<Omit<PicletInstance, 'id'>> {
22
  if (!data.stats) {
23
  throw new Error('Generated data must have stats to create PicletInstance');
24
  }
25
 
26
- // All generated data must now have the battle-ready format
27
  const stats = data.stats as PicletStats;
28
 
29
- if (!stats.baseStats || !stats.specialAbility || !stats.movepool) {
30
- throw new Error('Generated stats must be in battle-ready format with baseStats, specialAbility, and movepool');
31
- }
32
-
33
- // Calculate base stats from battle-ready format
34
- const baseHp = Math.floor(stats.baseStats.hp * 2 + 50);
35
- const baseAttack = Math.floor(stats.baseStats.attack * 1.5 + 30);
36
- const baseDefense = Math.floor(stats.baseStats.defense * 1.5 + 30);
37
- const baseSpeed = Math.floor(stats.baseStats.speed * 1.5 + 30);
38
-
39
- // Determine primary type from battle stats
40
- const normalizedPrimaryType = stats.primaryType.toLowerCase();
41
- const validPrimaryType = Object.values(PicletType).find(type => type === normalizedPrimaryType);
42
- const primaryType = validPrimaryType || getTypeFromConcept(data.concept, data.imageCaption);
43
-
44
- if (!validPrimaryType) {
45
- console.warn(`Invalid primaryType "${stats.primaryType}" from stats, falling back to concept detection`);
46
- }
47
-
48
- // Handle secondary type
49
- let secondaryType: PicletType | undefined = undefined;
50
- if (stats.secondaryType && stats.secondaryType !== null) {
51
- const normalizedSecondaryType = stats.secondaryType.toLowerCase();
52
- const validSecondaryType = Object.values(PicletType).find(type => type === normalizedSecondaryType);
53
- secondaryType = validSecondaryType;
54
- if (!validSecondaryType) {
55
- console.warn(`Invalid secondaryType "${stats.secondaryType}" from stats, ignoring`);
56
- }
57
- }
58
-
59
- // Create moves from battle-ready format preserving all data
60
- // Convert from PicletStats.BattleMove (loose types) to schema.BattleMove (strict types)
61
- const baseMoves: BattleMove[] = stats.movepool.map(move => ({
62
- name: move.name,
63
- type: move.type as any, // Type conversion from string union to AttackType enum
64
- power: move.power,
65
- accuracy: move.accuracy,
66
- pp: move.pp,
67
- priority: move.priority,
68
- flags: move.flags as any, // Type conversion from string[] to MoveFlag[]
69
- effects: move.effects as any, // Type conversion between BattleEffect types
70
- currentPp: move.pp,
71
- unlockLevel: 1 // Temporary, will be set below
72
- }));
73
-
74
- // Generate unlock levels for moves and special ability
75
- // Convert from PicletStats.SpecialAbility to battle-engine.SpecialAbility
76
- const convertedSpecialAbility = {
77
- name: stats.specialAbility.name,
78
- description: `Special ability of ${stats.name}`, // Generate a generic description since it's removed from stats
79
- effects: stats.specialAbility.effects as any,
80
- triggers: stats.specialAbility.triggers as any
81
- };
82
- const { movesWithUnlocks, abilityUnlockLevel } = generateUnlockLevels(baseMoves, convertedSpecialAbility);
83
- const moves = movesWithUnlocks;
84
-
85
- // Field stats are variations of regular stats
86
- const baseFieldAttack = Math.floor(baseAttack * 0.8);
87
- const baseFieldDefense = Math.floor(baseDefense * 0.8);
88
-
89
- // Use Pokemon-accurate stat calculations (matching levelingService)
90
- const calculateStat = (base: number, level: number) => {
91
- if (level === 1) {
92
- return Math.max(1, Math.floor(base / 10) + 5);
93
- }
94
- return Math.floor((2 * base * level) / 100) + 5;
95
- };
96
-
97
- const calculateHp = (base: number, level: number) => {
98
- if (level === 1) {
99
- return Math.max(1, Math.floor(base / 10) + 11);
100
- }
101
- return Math.floor((2 * base * level) / 100) + level + 10;
102
- };
103
-
104
- const maxHp = calculateHp(baseHp, level);
105
-
106
- const bst = baseHp + baseAttack + baseDefense + baseFieldAttack + baseFieldDefense + baseSpeed;
107
 
108
  // Check if this is the first piclet (no existing piclets in database)
109
  const existingPiclets = await db.picletInstances.count();
110
  const isFirstPiclet = existingPiclets === 0;
111
 
112
  return {
113
- // Type Info
114
- typeId: data.name.toLowerCase().replace(/\s+/g, '-'),
115
- nickname: stats.name || data.name, // Use stats.name if available, fallback to data.name
116
- primaryType: primaryType,
117
- secondaryType: secondaryType,
118
-
119
- // Current Stats
120
- currentHp: maxHp,
121
- maxHp,
122
- level,
123
- xp: 0,
124
- attack: calculateStat(baseAttack, level),
125
- defense: calculateStat(baseDefense, level),
126
- fieldAttack: calculateStat(baseFieldAttack, level),
127
- fieldDefense: calculateStat(baseFieldDefense, level),
128
- speed: calculateStat(baseSpeed, level),
129
-
130
- // Base Stats
131
- baseHp,
132
- baseAttack,
133
- baseDefense,
134
- baseFieldAttack,
135
- baseFieldDefense,
136
- baseSpeed,
137
 
138
- // Battle
139
- moves,
140
- nature: stats.nature,
141
- specialAbility: convertedSpecialAbility,
142
- specialAbilityUnlockLevel: abilityUnlockLevel,
143
-
144
- // Roster
145
  isInRoster: isFirstPiclet,
146
  rosterPosition: isFirstPiclet ? 0 : undefined,
147
 
148
  // Metadata
149
  caught: isFirstPiclet, // First piclet is automatically caught
150
  caughtAt: isFirstPiclet ? new Date() : undefined,
151
- bst,
152
- tier: stats.tier, // Use tier from stats
153
- role: 'balanced', // Could be enhanced based on stat distribution
154
- variance: 0,
155
 
156
  // Original generation data
157
  imageUrl: data.imageUrl,
@@ -163,112 +55,65 @@ export async function generatedDataToPicletInstance(data: GeneratedPicletData, l
163
  };
164
  }
165
 
166
-
167
- // Save a new PicletInstance
168
- export async function savePicletInstance(piclet: Omit<PicletInstance, 'id'>): Promise<number> {
169
- return await db.picletInstances.add(piclet);
170
  }
171
 
172
- // Mark a Piclet as caught
173
- export async function catchPiclet(picletId: number): Promise<void> {
174
- await db.picletInstances.update(picletId, {
175
- caught: true,
176
- caughtAt: new Date()
177
- });
178
  }
179
 
180
- // Get only caught Piclets (for Pictuary and battle roster)
181
- export async function getCaughtPiclets(): Promise<PicletInstance[]> {
182
- const allPiclets = await db.picletInstances.toArray();
183
- return allPiclets.filter(p => p.caught === true);
184
- }
185
-
186
- // Get uncaught Piclets (for encounters)
187
- export async function getUncaughtPiclets(): Promise<PicletInstance[]> {
188
- const allPiclets = await db.picletInstances.toArray();
189
- return allPiclets.filter(p => p.caught === false);
190
  }
191
 
192
- // Get all PicletInstances
193
  export async function getAllPicletInstances(): Promise<PicletInstance[]> {
194
  return await db.picletInstances.toArray();
195
  }
196
 
197
- // Get roster PicletInstances
 
 
 
 
 
198
  export async function getRosterPiclets(): Promise<PicletInstance[]> {
199
- const allPiclets = await db.picletInstances.toArray();
200
- return allPiclets
201
- .filter(p =>
202
- p.caught === true && // Only caught Piclets can be in roster
203
- p.rosterPosition !== undefined &&
204
- p.rosterPosition !== null &&
205
- p.rosterPosition >= 0 &&
206
- p.rosterPosition <= 5
207
- )
208
- .sort((a, b) => (a.rosterPosition ?? 0) - (b.rosterPosition ?? 0));
209
  }
210
 
211
- // Update roster position
212
- export async function updateRosterPosition(id: number, position: number | undefined): Promise<void> {
213
- await db.picletInstances.update(id, {
214
- isInRoster: position !== undefined,
215
- rosterPosition: position
216
- });
217
  }
218
 
219
- // Move piclet to roster
220
- export async function moveToRoster(id: number, position: number): Promise<void> {
221
- // Check if position is already occupied
222
- const existingPiclet = await db.picletInstances
223
- .where('rosterPosition')
224
- .equals(position)
225
- .and(item => item.isInRoster)
226
- .first();
227
-
228
- if (existingPiclet) {
229
- // Move existing piclet to storage
230
- await db.picletInstances.update(existingPiclet.id!, {
231
- isInRoster: false,
232
- rosterPosition: undefined
233
- });
234
- }
235
-
236
- // Move new piclet to roster
237
- await db.picletInstances.update(id, {
238
  isInRoster: true,
239
  rosterPosition: position
240
  });
241
  }
242
 
243
- // Swap roster positions
 
 
 
 
 
244
  export async function swapRosterPositions(id1: number, position1: number, id2: number, position2: number): Promise<void> {
245
- await db.transaction('rw', db.picletInstances, async () => {
246
- await db.picletInstances.update(id1, { rosterPosition: position2 });
247
- await db.picletInstances.update(id2, { rosterPosition: position1 });
248
- });
249
  }
250
 
251
- // Move piclet to storage
252
- export async function moveToStorage(id: number): Promise<void> {
253
- await db.picletInstances.update(id, {
254
  isInRoster: false,
255
  rosterPosition: undefined
256
  });
257
- }
258
-
259
- // Get storage piclets
260
- export async function getStoragePiclets(): Promise<PicletInstance[]> {
261
- const allPiclets = await db.picletInstances.toArray();
262
- return allPiclets.filter(p =>
263
- p.caught === true && // Only caught Piclets can be in storage
264
- (p.rosterPosition === undefined ||
265
- p.rosterPosition === null ||
266
- p.rosterPosition < 0 ||
267
- p.rosterPosition > 5)
268
- );
269
- }
270
-
271
- // Delete a PicletInstance
272
- export async function deletePicletInstance(id: number): Promise<void> {
273
- await db.picletInstances.delete(id);
274
  }
 
1
  import { db } from './index';
2
+ import type { PicletInstance } from './schema';
3
+ import { PicletType } from '../types/picletTypes';
4
  import type { PicletStats } from '../types';
 
 
5
 
6
  // Interface for generated piclet data (from PicletResult component)
7
  interface GeneratedPicletData {
 
16
  }
17
 
18
  // Convert generated piclet data to a PicletInstance
19
+ export async function generatedDataToPicletInstance(data: GeneratedPicletData): Promise<Omit<PicletInstance, 'id'>> {
20
  if (!data.stats) {
21
  throw new Error('Generated data must have stats to create PicletInstance');
22
  }
23
 
 
24
  const stats = data.stats as PicletStats;
25
 
26
+ // Map tier from stats
27
+ let tier: string = stats.tier || 'medium';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  // Check if this is the first piclet (no existing piclets in database)
30
  const existingPiclets = await db.picletInstances.count();
31
  const isFirstPiclet = existingPiclets === 0;
32
 
33
  return {
34
+ // Basic Info
35
+ typeId: stats.name || data.name,
36
+ nickname: stats.name || data.name,
37
+ primaryType: stats.primaryType as PicletType,
38
+ tier: tier,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ // Roster Management
 
 
 
 
 
 
41
  isInRoster: isFirstPiclet,
42
  rosterPosition: isFirstPiclet ? 0 : undefined,
43
 
44
  // Metadata
45
  caught: isFirstPiclet, // First piclet is automatically caught
46
  caughtAt: isFirstPiclet ? new Date() : undefined,
 
 
 
 
47
 
48
  // Original generation data
49
  imageUrl: data.imageUrl,
 
55
  };
56
  }
57
 
58
+ // Save a piclet instance to the database
59
+ export async function savePicletInstance(picletInstance: Omit<PicletInstance, 'id'>): Promise<number> {
60
+ const id = await db.picletInstances.add(picletInstance as any);
61
+ return id;
62
  }
63
 
64
+ // Update a piclet instance in the database
65
+ export async function updatePicletInstance(id: number, updates: Partial<PicletInstance>): Promise<void> {
66
+ await db.picletInstances.update(id, updates);
 
 
 
67
  }
68
 
69
+ // Delete a piclet instance from the database
70
+ export async function deletePicletInstance(id: number): Promise<void> {
71
+ await db.picletInstances.delete(id);
 
 
 
 
 
 
 
72
  }
73
 
74
+ // Get all piclet instances
75
  export async function getAllPicletInstances(): Promise<PicletInstance[]> {
76
  return await db.picletInstances.toArray();
77
  }
78
 
79
+ // Get a single piclet instance by ID
80
+ export async function getPicletInstance(id: number): Promise<PicletInstance | undefined> {
81
+ return await db.picletInstances.get(id);
82
+ }
83
+
84
+ // Get roster piclets (those currently in battle roster)
85
  export async function getRosterPiclets(): Promise<PicletInstance[]> {
86
+ return await db.picletInstances.where('isInRoster').equals(1).toArray();
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
+ // Get caught piclets (those that have been captured)
90
+ export async function getCaughtPiclets(): Promise<PicletInstance[]> {
91
+ return await db.picletInstances.where('caught').equals(1).toArray();
 
 
 
92
  }
93
 
94
+ // Move a piclet to roster (simplified version)
95
+ export async function moveToRoster(picletId: number, position: number): Promise<void> {
96
+ await db.picletInstances.update(picletId, {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  isInRoster: true,
98
  rosterPosition: position
99
  });
100
  }
101
 
102
+ // Get uncaught piclets
103
+ export async function getUncaughtPiclets(): Promise<PicletInstance[]> {
104
+ return await db.picletInstances.where('caught').equals(0).toArray();
105
+ }
106
+
107
+ // Swap roster positions (simplified version)
108
  export async function swapRosterPositions(id1: number, position1: number, id2: number, position2: number): Promise<void> {
109
+ await db.picletInstances.update(id1, { rosterPosition: position2 });
110
+ await db.picletInstances.update(id2, { rosterPosition: position1 });
 
 
111
  }
112
 
113
+ // Move to storage (simplified version)
114
+ export async function moveToStorage(picletId: number): Promise<void> {
115
+ await db.picletInstances.update(picletId, {
116
  isInRoster: false,
117
  rosterPosition: undefined
118
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  }
src/lib/db/schema.ts CHANGED
@@ -1,74 +1,36 @@
1
- import type { PicletType, AttackType } from '../types/picletTypes';
2
- import type { SpecialAbility, Move } from '../battle-engine/types';
3
 
4
  // Enums
5
  export enum EncounterType {
6
  WILD_PICLET = 'wildPiclet',
7
- TRAINER_BATTLE = 'trainerBattle',
8
- SHOP = 'shop',
9
- HEALTH_CENTER = 'healthCenter',
10
  FIRST_PICLET = 'firstPiclet'
11
  }
12
 
13
- // Battle Move with unlock level - extends the complete Move interface
14
- export interface BattleMove extends Move {
15
- currentPp: number;
16
- unlockLevel: number; // Level at which this move is unlocked
17
- }
18
 
19
  // PicletInstance - Individual monster instances owned by the player
20
  export interface PicletInstance {
21
  id?: number;
22
 
23
- // Type Info
24
  typeId: string;
25
  nickname?: string;
26
  primaryType: PicletType;
27
- secondaryType?: PicletType;
28
-
29
- // Current Stats
30
- currentHp: number;
31
- maxHp: number;
32
- level: number;
33
- xp: number;
34
- attack: number;
35
- defense: number;
36
- fieldAttack: number;
37
- fieldDefense: number;
38
- speed: number;
39
-
40
- // Base Stats (from generation)
41
- baseHp: number;
42
- baseAttack: number;
43
- baseDefense: number;
44
- baseFieldAttack: number;
45
- baseFieldDefense: number;
46
- baseSpeed: number;
47
 
48
- // Battle
49
- moves: BattleMove[];
50
- nature: string;
51
- specialAbility: SpecialAbility;
52
- specialAbilityUnlockLevel: number; // Level at which special ability is unlocked
53
-
54
- // Roster
55
  isInRoster: boolean;
56
  rosterPosition?: number; // 0-5 when in roster
57
 
58
  // Metadata
59
  caught: boolean; // Whether this Piclet has been caught by the player
60
  caughtAt?: Date; // When this Piclet was caught (undefined if not caught)
61
- bst: number; // Base Stat Total
62
- tier: string;
63
- role: string;
64
- variance: number;
65
 
66
  // Original generation data
67
  imageUrl: string;
68
  imageData?: string; // Base64 encoded image with transparency
69
  imageCaption: string;
70
  concept: string;
71
- description: string; // Generated monster description from stats
72
  imagePrompt: string;
73
  }
74
 
@@ -105,35 +67,6 @@ export interface GameState {
105
  battlesLost: number;
106
  }
107
 
108
- // Battle System Types
109
- export enum BattlePhase {
110
- INTRO = 'intro',
111
- MAIN = 'main',
112
- MOVE_SELECT = 'moveSelect',
113
- PICLET_SELECT = 'picletSelect',
114
- FORCED_SWAP = 'forcedSwap',
115
- BATTLE_END = 'battleEnd'
116
- }
117
-
118
- export enum ActionView {
119
- MAIN = 'main',
120
- MOVES = 'moves',
121
- PICLETS = 'piclets',
122
- ITEMS = 'items',
123
- FORCED_SWAP = 'forcedSwap'
124
- }
125
-
126
- export interface BattleState {
127
- phase: BattlePhase;
128
- currentTurn: number;
129
- playerPiclet: PicletInstance;
130
- enemyPiclet: PicletInstance;
131
- isWildBattle: boolean;
132
- processingTurn: boolean;
133
- battleEnded: boolean;
134
- winner?: 'player' | 'enemy';
135
- capturedPiclet?: PicletInstance;
136
- }
137
 
138
  // Trainer Scanning Progress - Track automated trainer piclet generation
139
  export interface TrainerScanProgress {
 
1
+ import type { PicletType } from '../types/picletTypes';
 
2
 
3
  // Enums
4
  export enum EncounterType {
5
  WILD_PICLET = 'wildPiclet',
 
 
 
6
  FIRST_PICLET = 'firstPiclet'
7
  }
8
 
 
 
 
 
 
9
 
10
  // PicletInstance - Individual monster instances owned by the player
11
  export interface PicletInstance {
12
  id?: number;
13
 
14
+ // Basic Info
15
  typeId: string;
16
  nickname?: string;
17
  primaryType: PicletType;
18
+ tier: string; // 'low' | 'medium' | 'high' | 'legendary'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ // Roster Management
 
 
 
 
 
 
21
  isInRoster: boolean;
22
  rosterPosition?: number; // 0-5 when in roster
23
 
24
  // Metadata
25
  caught: boolean; // Whether this Piclet has been caught by the player
26
  caughtAt?: Date; // When this Piclet was caught (undefined if not caught)
 
 
 
 
27
 
28
  // Original generation data
29
  imageUrl: string;
30
  imageData?: string; // Base64 encoded image with transparency
31
  imageCaption: string;
32
  concept: string;
33
+ description: string; // Generated monster description
34
  imagePrompt: string;
35
  }
36
 
 
67
  battlesLost: number;
68
  }
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  // Trainer Scanning Progress - Track automated trainer piclet generation
72
  export interface TrainerScanProgress {
src/lib/services/captureService.ts DELETED
@@ -1,212 +0,0 @@
1
- /**
2
- * Pokemon-style Capture Mechanics for Pictuary
3
- * Based on Pokemon Emerald's capture formula from POKEMON_CAPTURE_MECHANICS.md
4
- */
5
-
6
- export interface CaptureResult {
7
- success: boolean;
8
- shakes: number; // 0-3 shakes before success/failure
9
- odds: number; // Internal capture odds for debugging
10
- }
11
-
12
- export interface CaptureAttemptParams {
13
- // Target Piclet stats
14
- maxHp: number;
15
- currentHp: number;
16
- baseCatchRate: number; // Species-specific catch rate (3-255)
17
-
18
- // Status effects (optional)
19
- statusEffect?: 'sleep' | 'freeze' | 'poison' | 'burn' | 'paralysis' | 'toxic' | null;
20
-
21
- // Battle context (optional - for future specialty ball mechanics)
22
- battleTurn?: number;
23
- picletLevel?: number;
24
- }
25
-
26
- /**
27
- * Get the catch rate multiplier for a given tier
28
- * Maps Pictuary tiers to Pokemon-style catch rates
29
- */
30
- export function getCatchRateForTier(tier: string): number {
31
- switch (tier.toLowerCase()) {
32
- case 'legendary': return 3; // Hardest to catch (like legendary Pokemon)
33
- case 'high': return 25; // Hard to catch (like pseudolegendaries)
34
- case 'medium': return 75; // Standard catch rate
35
- case 'low': return 150; // Easy to catch (like common Pokemon)
36
- default: return 75; // Default to medium
37
- }
38
- }
39
-
40
- /**
41
- * Get status condition multiplier for capture rate
42
- */
43
- function getStatusMultiplier(status: string | null | undefined): number {
44
- switch (status) {
45
- case 'sleep':
46
- case 'freeze':
47
- return 2.0; // Best status conditions for catching
48
- case 'poison':
49
- case 'burn':
50
- case 'paralysis':
51
- case 'toxic':
52
- return 1.5; // Good status conditions
53
- default:
54
- return 1.0; // No status effect
55
- }
56
- }
57
-
58
- /**
59
- * Calculate initial capture odds using Pokemon formula
60
- * Formula: odds = (catchRate × ballMultiplier ÷ 10) × (maxHP × 3 - currentHP × 2) ÷ (maxHP × 3) × statusMultiplier
61
- */
62
- function calculateCaptureOdds(params: CaptureAttemptParams): number {
63
- const { maxHp, currentHp, baseCatchRate, statusEffect } = params;
64
-
65
- // Ball multiplier - since we don't have different camera types, use baseline 1.0x (10 in Pokemon terms)
66
- const ballMultiplier = 10;
67
-
68
- // HP factor: (maxHP × 3 - currentHP × 2) ÷ (maxHP × 3)
69
- // This creates the 3x capture boost when HP is at 1
70
- const hpFactor = (maxHp * 3 - currentHp * 2) / (maxHp * 3);
71
-
72
- // Status multiplier
73
- const statusMultiplier = getStatusMultiplier(statusEffect);
74
-
75
- // Core formula
76
- const odds = (baseCatchRate * ballMultiplier / 10) * hpFactor * statusMultiplier;
77
-
78
- return Math.max(0, Math.floor(odds));
79
- }
80
-
81
- /**
82
- * Calculate shake probability when capture odds <= 254
83
- * Formula: shakeOdds = 1048560 ÷ sqrt(sqrt(16711680 ÷ odds))
84
- */
85
- function calculateShakeOdds(captureOdds: number): number {
86
- if (captureOdds === 0) return 0;
87
-
88
- const shakeOdds = 1048560 / Math.sqrt(Math.sqrt(16711680 / captureOdds));
89
- return Math.floor(shakeOdds);
90
- }
91
-
92
- /**
93
- * Simulate individual shake success
94
- * Each shake has a (shakeOdds / 65536) chance of success
95
- */
96
- function simulateShake(shakeOdds: number): boolean {
97
- const randomValue = Math.floor(Math.random() * 65536);
98
- return randomValue < shakeOdds;
99
- }
100
-
101
- /**
102
- * Attempt to capture a Piclet using Pokemon mechanics
103
- * Returns detailed results including number of shakes
104
- */
105
- export function attemptCapture(params: CaptureAttemptParams): CaptureResult {
106
- const odds = calculateCaptureOdds(params);
107
-
108
- // Immediate capture if odds > 254
109
- if (odds > 254) {
110
- return {
111
- success: true,
112
- shakes: 3,
113
- odds
114
- };
115
- }
116
-
117
- // If odds are 0, capture fails immediately
118
- if (odds === 0) {
119
- return {
120
- success: false,
121
- shakes: 0,
122
- odds
123
- };
124
- }
125
-
126
- // Calculate shake probability
127
- const shakeOdds = calculateShakeOdds(odds);
128
-
129
- // Simulate up to 3 shakes
130
- let shakes = 0;
131
- for (let i = 0; i < 3; i++) {
132
- if (simulateShake(shakeOdds)) {
133
- shakes++;
134
- } else {
135
- // Shake failed, capture fails
136
- return {
137
- success: false,
138
- shakes,
139
- odds
140
- };
141
- }
142
- }
143
-
144
- // All 3 shakes succeeded - capture success!
145
- return {
146
- success: true,
147
- shakes: 3,
148
- odds
149
- };
150
- }
151
-
152
- /**
153
- * Calculate capture rate percentage for display purposes
154
- * This gives players an approximate idea of their chances
155
- */
156
- export function calculateCapturePercentage(params: CaptureAttemptParams): number {
157
- const odds = calculateCaptureOdds(params);
158
-
159
- // Immediate capture
160
- if (odds > 254) return 100;
161
-
162
- // No chance
163
- if (odds === 0) return 0;
164
-
165
- // For odds <= 254, we need to calculate the probability of getting 3 successful shakes
166
- const shakeOdds = calculateShakeOdds(odds);
167
- const shakeSuccessRate = shakeOdds / 65536;
168
-
169
- // Probability of 3 consecutive successful shakes
170
- const captureRate = Math.pow(shakeSuccessRate, 3) * 100;
171
-
172
- return Math.min(100, Math.max(0.1, captureRate)); // At least 0.1% to show something
173
- }
174
-
175
- /**
176
- * Get a user-friendly description of capture difficulty based on percentage
177
- */
178
- export function getCaptureDescription(percentage: number): string {
179
- if (percentage >= 95) return "Almost certain";
180
- if (percentage >= 75) return "Very likely";
181
- if (percentage >= 50) return "Good chance";
182
- if (percentage >= 25) return "Moderate chance";
183
- if (percentage >= 10) return "Low chance";
184
- if (percentage >= 5) return "Very low chance";
185
- return "Extremely difficult";
186
- }
187
-
188
- /**
189
- * Simulate multiple capture attempts to get average results (for testing/balancing)
190
- */
191
- export function simulateMultipleCaptures(params: CaptureAttemptParams, attempts: number = 1000): {
192
- successRate: number;
193
- averageShakes: number;
194
- distribution: { [key: number]: number };
195
- } {
196
- let successes = 0;
197
- let totalShakes = 0;
198
- const shakeDistribution: { [key: number]: number } = { 0: 0, 1: 0, 2: 0, 3: 0 };
199
-
200
- for (let i = 0; i < attempts; i++) {
201
- const result = attemptCapture(params);
202
- if (result.success) successes++;
203
- totalShakes += result.shakes;
204
- shakeDistribution[result.shakes]++;
205
- }
206
-
207
- return {
208
- successRate: (successes / attempts) * 100,
209
- averageShakes: totalShakes / attempts,
210
- distribution: shakeDistribution
211
- };
212
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/services/levelingService.ts DELETED
@@ -1,332 +0,0 @@
1
- /**
2
- * Pokemon-style Leveling and Stat Calculation Service
3
- * Implements accurate Pokemon stat formulas based on pokemon_stat_calculation.md
4
- */
5
-
6
- import type { PicletInstance } from '$lib/db/schema';
7
-
8
- // Pokemon nature effects: [boosted_stat, lowered_stat] or [null, null] for neutral
9
- export const NATURES = {
10
- 'Hardy': [null, null], // Neutral
11
- 'Lonely': ['attack', 'defense'],
12
- 'Brave': ['attack', 'speed'],
13
- 'Adamant': ['attack', 'sp_attack'],
14
- 'Naughty': ['attack', 'sp_defense'],
15
- 'Bold': ['defense', 'attack'],
16
- 'Docile': [null, null], // Neutral
17
- 'Relaxed': ['defense', 'speed'],
18
- 'Impish': ['defense', 'sp_attack'],
19
- 'Lax': ['defense', 'sp_defense'],
20
- 'Timid': ['speed', 'attack'],
21
- 'Hasty': ['speed', 'defense'],
22
- 'Serious': [null, null], // Neutral
23
- 'Jolly': ['speed', 'sp_attack'],
24
- 'Naive': ['speed', 'sp_defense'],
25
- 'Modest': ['sp_attack', 'attack'],
26
- 'Mild': ['sp_attack', 'defense'],
27
- 'Quiet': ['sp_attack', 'speed'],
28
- 'Bashful': [null, null], // Neutral
29
- 'Rash': ['sp_attack', 'sp_defense'],
30
- 'Calm': ['sp_defense', 'attack'],
31
- 'Gentle': ['sp_defense', 'defense'],
32
- 'Sassy': ['sp_defense', 'speed'],
33
- 'Careful': ['sp_defense', 'sp_attack'],
34
- 'Quirky': [null, null], // Neutral
35
- } as const;
36
-
37
- export type NatureName = keyof typeof NATURES;
38
-
39
- // Growth rate multipliers for different tiers
40
- const TIER_XP_MULTIPLIERS = {
41
- 'low': 0.8, // 20% less XP required (faster leveling)
42
- 'medium': 1.0, // Base XP requirements
43
- 'high': 1.4, // 40% more XP required (slower leveling)
44
- 'legendary': 1.8 // 80% more XP required (much slower leveling)
45
- } as const;
46
-
47
- type TierType = keyof typeof TIER_XP_MULTIPLIERS;
48
-
49
- /**
50
- * Convert string tier to TierType, defaulting to 'medium' for unknown values
51
- */
52
- function normalizeTier(tier: string): TierType {
53
- if (tier in TIER_XP_MULTIPLIERS) {
54
- return tier as TierType;
55
- }
56
- return 'medium'; // Default fallback
57
- }
58
-
59
- // Base experience requirements for Medium Fast growth rate (level³)
60
- // Other tiers will use multipliers of this base
61
- const BASE_XP_REQUIREMENTS: number[] = [];
62
- for (let level = 1; level <= 100; level++) {
63
- BASE_XP_REQUIREMENTS[level] = level * level * level;
64
- }
65
-
66
- export interface LevelUpInfo {
67
- oldLevel: number;
68
- newLevel: number;
69
- statChanges: {
70
- hp: number;
71
- attack: number;
72
- defense: number;
73
- speed: number;
74
- };
75
- }
76
-
77
- export interface NatureModifiers {
78
- attack: number;
79
- defense: number;
80
- speed: number;
81
- }
82
-
83
- /**
84
- * Calculate HP using Pokemon's HP formula
85
- * Formula: floor((2 * base_hp * level) / 100) + level + 10
86
- */
87
- export function calculateHp(baseHp: number, level: number): number {
88
- if (level === 1) {
89
- return Math.max(1, Math.floor(baseHp / 10) + 11); // Special case for level 1
90
- }
91
-
92
- return Math.floor((2 * baseHp * level) / 100) + level + 10;
93
- }
94
-
95
- /**
96
- * Calculate non-HP stat using Pokemon's standard formula
97
- * Formula: floor((floor((2 * base_stat * level) / 100) + 5) * nature_modifier)
98
- */
99
- export function calculateStat(baseStat: number, level: number, natureModifier: number = 1.0): number {
100
- if (level === 1) {
101
- return Math.max(1, Math.floor(baseStat / 10) + 5); // Special case for level 1
102
- }
103
-
104
- const baseValue = Math.floor((2 * baseStat * level) / 100) + 5;
105
- return Math.floor(baseValue * natureModifier);
106
- }
107
-
108
- /**
109
- * Get nature modifiers for all stats
110
- */
111
- export function getNatureModifiers(nature: string): NatureModifiers {
112
- const natureName = nature as NatureName;
113
- const [boosted, lowered] = NATURES[natureName] || NATURES['Hardy'];
114
-
115
- const modifiers: NatureModifiers = {
116
- attack: 1.0,
117
- defense: 1.0,
118
- speed: 1.0,
119
- };
120
-
121
- if (boosted) {
122
- (modifiers as any)[boosted] = 1.1; // +10%
123
- }
124
- if (lowered) {
125
- (modifiers as any)[lowered] = 0.9; // -10%
126
- }
127
-
128
- return modifiers;
129
- }
130
-
131
- /**
132
- * Get XP required to reach a specific level
133
- */
134
- /**
135
- * Get XP required for a specific level based on tier
136
- */
137
- export function getXpForLevel(level: number, tier: string = 'medium'): number {
138
- if (level < 1 || level > 100) {
139
- throw new Error('Level must be between 1 and 100');
140
- }
141
- const normalizedTier = normalizeTier(tier);
142
- const baseXp = BASE_XP_REQUIREMENTS[level];
143
- const multiplier = TIER_XP_MULTIPLIERS[normalizedTier];
144
- return Math.floor(baseXp * multiplier);
145
- }
146
-
147
- /**
148
- * Get XP required for next level
149
- */
150
- /**
151
- * Get XP required for next level based on tier
152
- */
153
- export function getXpForNextLevel(currentLevel: number, tier: string = 'medium'): number {
154
- if (currentLevel >= 100) return 0; // Max level
155
- return getXpForLevel(currentLevel + 1, tier);
156
- }
157
-
158
- /**
159
- * Calculate XP progress percentage for current level
160
- */
161
- /**
162
- * Get XP progress percentage towards next level based on tier
163
- */
164
- export function getXpProgress(currentXp: number, currentLevel: number, tier: string = 'medium'): number {
165
- if (currentLevel >= 100) return 100;
166
-
167
- const currentLevelXp = getXpForLevel(currentLevel, tier);
168
- const nextLevelXp = getXpForLevel(currentLevel + 1, tier);
169
- const xpIntoLevel = currentXp - currentLevelXp;
170
- const xpNeededForLevel = nextLevelXp - currentLevelXp;
171
-
172
- return Math.min(100, Math.max(0, (xpIntoLevel / xpNeededForLevel) * 100));
173
- }
174
-
175
- /**
176
- * Get current XP towards next level in X/Y format
177
- */
178
- export function getXpTowardsNextLevel(currentXp: number, currentLevel: number, tier: string = 'medium'): {
179
- current: number;
180
- needed: number;
181
- percentage: number;
182
- } {
183
- if (currentLevel >= 100) {
184
- return { current: 0, needed: 0, percentage: 100 };
185
- }
186
-
187
- const currentLevelXp = getXpForLevel(currentLevel, tier);
188
- const nextLevelXp = getXpForLevel(currentLevel + 1, tier);
189
- const xpIntoLevel = Math.max(0, currentXp - currentLevelXp);
190
- const xpNeededForLevel = nextLevelXp - currentLevelXp;
191
- const percentage = Math.min(100, Math.max(0, (xpIntoLevel / xpNeededForLevel) * 100));
192
-
193
- return {
194
- current: xpIntoLevel,
195
- needed: xpNeededForLevel,
196
- percentage
197
- };
198
- }
199
-
200
- /**
201
- * Recalculate all stats for a Piclet based on current level and nature
202
- */
203
- export function recalculatePicletStats(instance: PicletInstance): PicletInstance {
204
- const natureModifiers = getNatureModifiers(instance.nature);
205
-
206
- // Calculate new stats
207
- const newMaxHp = calculateHp(instance.baseHp, instance.level);
208
- const newAttack = calculateStat(instance.baseAttack, instance.level, natureModifiers.attack);
209
- const newDefense = calculateStat(instance.baseDefense, instance.level, natureModifiers.defense);
210
- const newSpeed = calculateStat(instance.baseSpeed, instance.level, natureModifiers.speed);
211
-
212
- // Field stats are 80% of main stats (existing logic)
213
- const newFieldAttack = Math.floor(newAttack * 0.8);
214
- const newFieldDefense = Math.floor(newDefense * 0.8);
215
-
216
- // Maintain current HP ratio when stats change
217
- const hpRatio = instance.maxHp > 0 ? instance.currentHp / instance.maxHp : 1;
218
- const newCurrentHp = Math.ceil(newMaxHp * hpRatio);
219
-
220
- return {
221
- ...instance,
222
- maxHp: newMaxHp,
223
- currentHp: newCurrentHp,
224
- attack: newAttack,
225
- defense: newDefense,
226
- speed: newSpeed,
227
- fieldAttack: newFieldAttack,
228
- fieldDefense: newFieldDefense
229
- };
230
- }
231
-
232
- /**
233
- * Process potential level up and return results
234
- */
235
- export function processLevelUp(instance: PicletInstance): {
236
- newInstance: PicletInstance;
237
- levelUpInfo: LevelUpInfo | null;
238
- } {
239
- const requiredXp = getXpForNextLevel(instance.level, instance.tier);
240
-
241
- // Check if level up is possible
242
- if (instance.level >= 100 || instance.xp < requiredXp) {
243
- return { newInstance: instance, levelUpInfo: null };
244
- }
245
-
246
- // Calculate old stats for comparison
247
- const oldStats = {
248
- hp: instance.maxHp,
249
- attack: instance.attack,
250
- defense: instance.defense,
251
- speed: instance.speed
252
- };
253
-
254
- // Level up the Piclet
255
- const leveledUpInstance = {
256
- ...instance,
257
- level: instance.level + 1
258
- };
259
-
260
- // Recalculate stats with new level
261
- const newInstance = recalculatePicletStats(leveledUpInstance);
262
-
263
- // Heal to full HP on level up (Pokemon tradition)
264
- const finalInstance = {
265
- ...newInstance,
266
- currentHp: newInstance.maxHp
267
- };
268
-
269
- // Calculate stat changes
270
- const statChanges = {
271
- hp: finalInstance.maxHp - oldStats.hp,
272
- attack: finalInstance.attack - oldStats.attack,
273
- defense: finalInstance.defense - oldStats.defense,
274
- speed: finalInstance.speed - oldStats.speed
275
- };
276
-
277
- const levelUpInfo: LevelUpInfo = {
278
- oldLevel: instance.level,
279
- newLevel: finalInstance.level,
280
- statChanges
281
- };
282
-
283
- return { newInstance: finalInstance, levelUpInfo };
284
- }
285
-
286
- /**
287
- * Calculate XP gained from defeating a Piclet in battle
288
- * Based on Pokemon formula: (baseExpYield * level) / 7
289
- */
290
- export function calculateBattleXp(defeatedPiclet: PicletInstance, participantCount: number = 1): number {
291
- // Use BST as basis for exp yield (common Pokemon approach)
292
- const bst = defeatedPiclet.baseHp + defeatedPiclet.baseAttack + defeatedPiclet.baseDefense +
293
- defeatedPiclet.baseSpeed + defeatedPiclet.baseFieldAttack + defeatedPiclet.baseFieldDefense;
294
-
295
- // Convert BST to exp yield (roughly BST/4, minimum 50)
296
- const baseExpYield = Math.max(50, Math.floor(bst / 4));
297
-
298
- // Pokemon formula
299
- const baseXp = Math.floor((baseExpYield * defeatedPiclet.level) / 7);
300
-
301
- // Divide among participants
302
- return Math.max(1, Math.floor(baseXp / participantCount));
303
- }
304
-
305
- /**
306
- * Check if a level up should occur and process it recursively
307
- * (Handles multiple level ups from large XP gains)
308
- */
309
- export function processAllLevelUps(instance: PicletInstance): {
310
- newInstance: PicletInstance;
311
- levelUpInfo: LevelUpInfo[];
312
- } {
313
- const levelUps: LevelUpInfo[] = [];
314
- let currentInstance = instance;
315
-
316
- // Process level ups until no more are possible
317
- while (currentInstance.level < 100) {
318
- const result = processLevelUp(currentInstance);
319
-
320
- if (result.levelUpInfo) {
321
- levelUps.push(result.levelUpInfo);
322
- currentInstance = result.newInstance;
323
- } else {
324
- break;
325
- }
326
- }
327
-
328
- return {
329
- newInstance: currentInstance,
330
- levelUpInfo: levelUps
331
- };
332
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/services/picletMetadata.ts CHANGED
@@ -70,7 +70,6 @@ export async function embedPicletMetadata(imageBlob: Blob, piclet: PicletInstanc
70
  typeId: piclet.typeId,
71
  nickname: piclet.nickname,
72
  primaryType: piclet.primaryType,
73
- secondaryType: piclet.secondaryType,
74
  currentHp: piclet.maxHp, // Reset to full HP for sharing
75
  maxHp: piclet.maxHp,
76
  level: piclet.level,
 
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,
src/lib/services/unlockLevels.ts DELETED
@@ -1,119 +0,0 @@
1
- /**
2
- * Utility functions for calculating unlock levels for moves and abilities
3
- */
4
-
5
- import type { BattleMove } from '$lib/db/schema';
6
- import type { SpecialAbility } from '$lib/battle-engine/types';
7
-
8
- /**
9
- * Calculate unlock level for a move based on its power and characteristics
10
- * First 2 moves are always unlocked, moves 3-4 unlock at levels 9-14
11
- */
12
- export function calculateMoveUnlockLevel(move: BattleMove, moveIndex: number): number {
13
- // First two moves are always available at level 1
14
- if (moveIndex === 0 || moveIndex === 1) {
15
- return 1;
16
- }
17
-
18
- // Third and fourth moves unlock at levels 9-14
19
- if (moveIndex === 2) {
20
- // Third move unlocks between level 9-11
21
- return Math.floor(Math.random() * 3) + 9; // Level 9, 10, or 11
22
- }
23
-
24
- if (moveIndex === 3) {
25
- // Fourth move unlocks between level 12-14
26
- return Math.floor(Math.random() * 3) + 12; // Level 12, 13, or 14
27
- }
28
-
29
- // For any additional moves beyond the 4th (shouldn't normally happen)
30
- // They unlock at higher levels
31
- return 15 + (moveIndex - 4) * 5;
32
- }
33
-
34
- /**
35
- * Calculate unlock level for special ability based on its power and effects
36
- */
37
- export function calculateSpecialAbilityUnlockLevel(ability: SpecialAbility): number {
38
- // Base level based on ability impact
39
- let baseLevel = 15; // Default mid-level unlock
40
-
41
- // Analyze ability effects to determine power level
42
- const effects = ability.effects || [];
43
- let powerScore = 0;
44
-
45
- for (const effect of effects) {
46
- switch (effect.type) {
47
- case 'damage':
48
- powerScore += effect.amount === 'strong' ? 3 : effect.amount === 'normal' ? 2 : 1;
49
- break;
50
- case 'heal':
51
- powerScore += effect.amount === 'large' ? 3 : effect.amount === 'medium' ? 2 : 1;
52
- break;
53
- case 'modifyStats':
54
- // Stat modifications are powerful
55
- powerScore += 2;
56
- break;
57
- case 'applyStatus':
58
- powerScore += 2;
59
- break;
60
- case 'removeStatus':
61
- powerScore += 1;
62
- break;
63
- case 'manipulatePP':
64
- powerScore += 1;
65
- break;
66
- default:
67
- powerScore += 1;
68
- }
69
- }
70
-
71
- // Convert power score to unlock level
72
- if (powerScore <= 2) {
73
- baseLevel = Math.floor(Math.random() * 20) + 10; // Level 10-30
74
- } else if (powerScore <= 4) {
75
- baseLevel = Math.floor(Math.random() * 25) + 20; // Level 20-45
76
- } else if (powerScore <= 6) {
77
- baseLevel = Math.floor(Math.random() * 25) + 30; // Level 30-55
78
- } else {
79
- baseLevel = Math.floor(Math.random() * 25) + 40; // Level 40-65
80
- }
81
-
82
- // Cap at level 80
83
- return Math.min(baseLevel, 80);
84
- }
85
-
86
- /**
87
- * Get all unlocked moves for a Piclet at a given level
88
- */
89
- export function getUnlockedMoves(moves: BattleMove[], currentLevel: number): BattleMove[] {
90
- return moves.filter(move => move.unlockLevel <= currentLevel);
91
- }
92
-
93
- /**
94
- * Check if special ability is unlocked at given level
95
- */
96
- export function isSpecialAbilityUnlocked(unlockLevel: number, currentLevel: number): boolean {
97
- return currentLevel >= unlockLevel;
98
- }
99
-
100
- /**
101
- * Generate unlock levels for a new Piclet's moves and ability
102
- * This should be called when a Piclet is first generated
103
- */
104
- export function generateUnlockLevels(moves: BattleMove[], specialAbility: SpecialAbility): {
105
- movesWithUnlocks: BattleMove[];
106
- abilityUnlockLevel: number;
107
- } {
108
- const movesWithUnlocks = moves.map((move, index) => ({
109
- ...move,
110
- unlockLevel: calculateMoveUnlockLevel(move, index)
111
- }));
112
-
113
- const abilityUnlockLevel = calculateSpecialAbilityUnlockLevel(specialAbility);
114
-
115
- return {
116
- movesWithUnlocks,
117
- abilityUnlockLevel
118
- };
119
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/types/index.ts CHANGED
@@ -152,51 +152,6 @@ export interface PicletStats {
152
  description: string;
153
  tier: 'low' | 'medium' | 'high' | 'legendary';
154
  primaryType: 'beast' | 'bug' | 'aquatic' | 'flora' | 'mineral' | 'space' | 'machina' | 'structure' | 'culture' | 'cuisine';
155
- secondaryType?: 'beast' | 'bug' | 'aquatic' | 'flora' | 'mineral' | 'space' | 'machina' | 'structure' | 'culture' | 'cuisine' | null;
156
- baseStats: {
157
- hp: number;
158
- attack: number;
159
- defense: number;
160
- speed: number;
161
- };
162
- nature: Nature;
163
- specialAbility: {
164
- name: string;
165
- effects?: BattleEffect[];
166
- triggers?: AbilityTrigger[];
167
- };
168
- movepool: BattleMove[];
169
  }
170
 
171
- // Battle system effect types for PicletStats
172
- export interface BattleEffect {
173
- type: 'damage' | 'modifyStats' | 'applyStatus' | 'heal' | 'manipulatePP' | 'fieldEffect' | 'counter' | 'priority' | 'removeStatus' | 'mechanicOverride';
174
- target: 'self' | 'opponent' | 'allies' | 'all' | 'attacker' | 'field' | 'playerSide' | 'opponentSide';
175
- condition?: string;
176
- // Additional properties based on effect type
177
- amount?: string;
178
- formula?: string;
179
- value?: number;
180
- stats?: { [key: string]: string };
181
- status?: string;
182
- chance?: number;
183
- [key: string]: any; // Allow additional properties for different effect types
184
- }
185
 
186
- export interface AbilityTrigger {
187
- event: string;
188
- condition?: string;
189
- effects: BattleEffect[];
190
- }
191
-
192
- export interface BattleMove {
193
- name: string;
194
- description: string;
195
- type: 'beast' | 'bug' | 'aquatic' | 'flora' | 'mineral' | 'space' | 'machina' | 'structure' | 'culture' | 'cuisine' | 'normal';
196
- power: number;
197
- accuracy: number;
198
- pp: number;
199
- priority: number;
200
- flags: string[];
201
- effects: BattleEffect[];
202
- }
 
152
  description: string;
153
  tier: 'low' | 'medium' | 'high' | 'legendary';
154
  primaryType: 'beast' | 'bug' | 'aquatic' | 'flora' | 'mineral' | 'space' | 'machina' | 'structure' | 'culture' | 'cuisine';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  }
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/types/picletTypes.ts CHANGED
@@ -253,23 +253,15 @@ export function getTypeEffectiveness(attackType: AttackType, defenseType: Piclet
253
  return TYPE_EFFECTIVENESS[attackType][defenseType];
254
  }
255
 
256
- export function getEffectivenessMultiplier(attackType: AttackType, defenseType: PicletType, secondaryType?: PicletType): number {
257
  console.log('🔍 Type effectiveness lookup:', {
258
  attackType,
259
- defenseType,
260
- secondaryType,
261
  attackTypeValid: Object.values(AttackType).includes(attackType),
262
- defenseTypeValid: Object.values(PicletType).includes(defenseType),
263
- secondaryTypeValid: secondaryType ? Object.values(PicletType).includes(secondaryType) : 'N/A'
264
  });
265
 
266
- let multiplier = getTypeEffectiveness(attackType, defenseType);
267
-
268
- if (secondaryType && secondaryType !== defenseType) {
269
- multiplier *= getTypeEffectiveness(attackType, secondaryType);
270
- }
271
-
272
- return multiplier;
273
  }
274
 
275
  export function getEffectivenessText(effectiveness: number): string {
 
253
  return TYPE_EFFECTIVENESS[attackType][defenseType];
254
  }
255
 
256
+ export function getEffectivenessMultiplier(attackType: AttackType, defenseType: PicletType): number {
257
  console.log('🔍 Type effectiveness lookup:', {
258
  attackType,
259
+ defenseType,
 
260
  attackTypeValid: Object.values(AttackType).includes(attackType),
261
+ defenseTypeValid: Object.values(PicletType).includes(defenseType)
 
262
  });
263
 
264
+ return getTypeEffectiveness(attackType, defenseType);
 
 
 
 
 
 
265
  }
266
 
267
  export function getEffectivenessText(effectiveness: number): string {
src/lib/utils/battleConversion.ts DELETED
@@ -1,168 +0,0 @@
1
- /**
2
- * Conversion utilities for transforming database types to battle engine types
3
- */
4
-
5
- import type { PicletInstance } from '$lib/db/schema';
6
- import type { PicletDefinition, Move, BaseStats, SpecialAbility } from '$lib/battle-engine/types';
7
- import type { PicletStats } from '$lib/types';
8
- import { PicletType } from '$lib/types/picletTypes';
9
- import { recalculatePicletStats } from '$lib/services/levelingService';
10
- import { getUnlockedMoves, isSpecialAbilityUnlocked } from '$lib/services/unlockLevels';
11
-
12
- /**
13
- * Convert PicletInstance to PicletDefinition for battle engine use
14
- * Uses levelingService to ensure stats are properly calculated for current level
15
- */
16
- export function picletInstanceToBattleDefinition(instance: PicletInstance): PicletDefinition {
17
- // First ensure stats are up-to-date for current level and nature
18
- const updatedInstance = recalculatePicletStats(instance);
19
-
20
- // Use the calculated stats directly (no need for complex reverse formulas)
21
- const baseStats: BaseStats = {
22
- hp: updatedInstance.maxHp, // Pokemon-calculated HP
23
- attack: updatedInstance.attack, // Includes level scaling and nature modifiers
24
- defense: updatedInstance.defense, // Includes level scaling and nature modifiers
25
- speed: updatedInstance.speed // Includes level scaling and nature modifiers
26
- };
27
-
28
- // Only include unlocked moves - BattleMove now contains complete Move data
29
- const unlockedMoves = getUnlockedMoves(instance.moves, updatedInstance.level);
30
- const movepool: Move[] = unlockedMoves.map(move => ({
31
- name: move.name,
32
- type: move.type,
33
- power: move.power,
34
- accuracy: move.accuracy,
35
- pp: move.pp,
36
- priority: move.priority,
37
- flags: move.flags,
38
- effects: move.effects
39
- }));
40
-
41
- // Ensure at least two moves are available (first two moves should always be unlocked at level 1)
42
- if (movepool.length === 0) {
43
- console.warn(`Piclet ${instance.nickname} has no unlocked moves at level ${updatedInstance.level}!`);
44
- // Emergency fallback - unlock first two moves
45
- const movesToUnlock = Math.min(2, instance.moves.length);
46
- for (let i = 0; i < movesToUnlock; i++) {
47
- const move = instance.moves[i];
48
- movepool.push({
49
- name: move.name,
50
- type: move.type,
51
- power: move.power,
52
- accuracy: move.accuracy,
53
- pp: move.pp,
54
- priority: move.priority,
55
- flags: move.flags,
56
- effects: move.effects
57
- });
58
- }
59
- } else if (movepool.length === 1 && instance.moves.length > 1) {
60
- // Ensure we have at least 2 moves if possible
61
- const secondMove = instance.moves[1];
62
- if (secondMove && secondMove.unlockLevel > updatedInstance.level) {
63
- movepool.push({
64
- name: secondMove.name,
65
- type: secondMove.type,
66
- power: secondMove.power,
67
- accuracy: secondMove.accuracy,
68
- pp: secondMove.pp,
69
- priority: secondMove.priority,
70
- flags: secondMove.flags,
71
- effects: secondMove.effects
72
- });
73
- }
74
- }
75
-
76
- // All Piclets must now have special abilities
77
- if (!instance.specialAbility) {
78
- throw new Error('Piclet must have a special ability.');
79
- }
80
-
81
- // Only include special ability if unlocked
82
- let specialAbility: SpecialAbility | undefined;
83
- if (isSpecialAbilityUnlocked(instance.specialAbilityUnlockLevel, updatedInstance.level)) {
84
- specialAbility = instance.specialAbility;
85
- } else {
86
- // Create a placeholder ability for locked special abilities
87
- specialAbility = {
88
- name: "Locked Ability",
89
- description: `Unlocks at level ${instance.specialAbilityUnlockLevel}`,
90
- effects: []
91
- };
92
- }
93
-
94
- // Determine tier based on BST (Base Stat Total)
95
- const bst = baseStats.hp + baseStats.attack + baseStats.defense + baseStats.speed;
96
- let tier: 'low' | 'medium' | 'high' | 'legendary';
97
- if (bst <= 300) tier = 'low';
98
- else if (bst <= 400) tier = 'medium';
99
- else if (bst <= 500) tier = 'high';
100
- else tier = 'legendary';
101
-
102
- return {
103
- name: instance.nickname || instance.typeId, // Keep original name - we'll add prefixes in battle engine
104
- description: instance.concept,
105
- tier,
106
- primaryType: instance.primaryType,
107
- secondaryType: instance.secondaryType,
108
- baseStats,
109
- nature: instance.nature,
110
- specialAbility: specialAbility!,
111
- movepool
112
- };
113
- }
114
-
115
-
116
-
117
- /**
118
- * Convert battle engine BattlePiclet back to PicletInstance for state updates
119
- */
120
- export function battlePicletToInstance(battlePiclet: any, originalInstance: PicletInstance): PicletInstance {
121
- return {
122
- ...originalInstance,
123
- currentHp: battlePiclet.currentHp,
124
- level: battlePiclet.level,
125
- attack: battlePiclet.attack,
126
- defense: battlePiclet.defense,
127
- speed: battlePiclet.speed,
128
- // Update moves with current PP
129
- moves: battlePiclet.moves.map((moveData: any, index: number) => ({
130
- ...originalInstance.moves[index],
131
- currentPp: moveData.currentPP
132
- }))
133
- };
134
- }
135
-
136
- /**
137
- * Convert PicletStats (from generation) to PicletDefinition for battle engine
138
- */
139
- export function picletStatsToBattleDefinition(stats: PicletStats, name: string, concept: string): PicletDefinition {
140
- return {
141
- name: stats.name || name,
142
- description: stats.description || concept,
143
- tier: stats.tier,
144
- primaryType: stats.primaryType as PicletType,
145
- secondaryType: stats.secondaryType as PicletType || undefined,
146
- baseStats: stats.baseStats,
147
- nature: stats.nature,
148
- specialAbility: {
149
- ...stats.specialAbility,
150
- description: `Special ability of ${stats.name || name}` // Add description since it's removed from generation
151
- } as any,
152
- movepool: stats.movepool as any
153
- };
154
- }
155
-
156
- /**
157
- * Strip internal battle prefixes from piclet names for display purposes
158
- * Removes "player-" and "enemy-" prefixes that are used internally for animation targeting
159
- */
160
- export function stripBattlePrefix(name: string): string {
161
- if (name.startsWith('player-')) {
162
- return name.substring('player-'.length);
163
- }
164
- if (name.startsWith('enemy-')) {
165
- return name.substring('enemy-'.length);
166
- }
167
- return name;
168
- }