BIG CHANGE
Browse files- battle-system-todo.md +0 -121
- battle_system_design.md +0 -1868
- src/lib/battle-engine/BattleEngine.test.ts +0 -364
- src/lib/battle-engine/BattleEngine.ts +0 -1876
- src/lib/battle-engine/MultiBattleEngine.test.ts +0 -671
- src/lib/battle-engine/MultiBattleEngine.ts +0 -701
- src/lib/battle-engine/README.md +0 -232
- src/lib/battle-engine/ability-triggers.test.ts +0 -388
- src/lib/battle-engine/advanced-effects.test.ts +0 -613
- src/lib/battle-engine/advanced-mechanic-overrides.test.ts +0 -348
- src/lib/battle-engine/advanced-status-effects.test.ts +0 -369
- src/lib/battle-engine/debug-field-effects.test.ts +0 -101
- src/lib/battle-engine/extreme-moves.test.ts +0 -544
- src/lib/battle-engine/extreme-risk-reward.test.ts +0 -383
- src/lib/battle-engine/field-effects.test.ts +0 -493
- src/lib/battle-engine/integration.test.ts +0 -255
- src/lib/battle-engine/mechanic-overrides.test.ts +0 -544
- src/lib/battle-engine/missing-features.test.ts +0 -498
- src/lib/battle-engine/move-flags.test.ts +0 -692
- src/lib/battle-engine/multi-piclet-types.ts +0 -160
- src/lib/battle-engine/package.json +0 -30
- src/lib/battle-engine/remaining-triggers.test.ts +0 -363
- src/lib/battle-engine/switching-system.test.ts +0 -445
- src/lib/battle-engine/tempest-wraith.test.ts +0 -507
- src/lib/battle-engine/test-data.ts +0 -234
- src/lib/battle-engine/types.ts +0 -262
- src/lib/components/Battle/ActionViewSelector.svelte +1 -47
- src/lib/components/Battle/BattleControls.svelte +0 -136
- src/lib/components/Battle/BattleEffects.svelte +0 -522
- src/lib/components/Battle/BattleField.svelte +0 -536
- src/lib/components/Battle/LLMBattleEngine.svelte +455 -0
- src/lib/components/Pages/Battle.svelte +124 -983
- src/lib/components/Pages/Encounters.svelte +7 -74
- src/lib/components/Pages/Pictuary.svelte +0 -1
- src/lib/components/PicletGenerator/PicletGenerator.svelte +58 -396
- src/lib/components/Piclets/NewlyCaughtPicletDetail.svelte +0 -5
- src/lib/components/Piclets/PicletCard.svelte +30 -46
- src/lib/components/Piclets/PicletDetail.svelte +160 -697
- src/lib/db/battleService.ts +0 -170
- src/lib/db/encounterService.ts +37 -207
- src/lib/db/piclets.ts +46 -201
- src/lib/db/schema.ts +5 -72
- src/lib/services/captureService.ts +0 -212
- src/lib/services/levelingService.ts +0 -332
- src/lib/services/picletMetadata.ts +0 -1
- src/lib/services/unlockLevels.ts +0 -119
- src/lib/types/index.ts +0 -45
- src/lib/types/picletTypes.ts +4 -12
- 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' | '
|
| 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
|
| 5 |
-
import
|
| 6 |
-
import
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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 |
-
|
| 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 |
-
|
| 558 |
-
|
| 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 |
-
|
| 584 |
-
|
| 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 |
-
|
| 613 |
-
|
| 614 |
|
| 615 |
-
//
|
| 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 |
-
|
| 630 |
-
|
| 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
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 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
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
<
|
| 858 |
-
|
| 859 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 860 |
|
| 861 |
-
{#if
|
| 862 |
-
{
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 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 |
-
|
| 886 |
-
|
| 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: #
|
| 898 |
-
|
| 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-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
position: absolute;
|
| 938 |
-
left: 50%;
|
| 939 |
-
transform: translateX(-50%);
|
| 940 |
-
}
|
| 941 |
-
|
| 942 |
-
.nav-spacer {
|
| 943 |
-
width: 60px;
|
| 944 |
}
|
| 945 |
|
| 946 |
-
.battle-
|
| 947 |
-
flex: 1;
|
| 948 |
display: flex;
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
}
|
| 954 |
|
| 955 |
-
|
| 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 |
-
|
| 963 |
-
z-index: 2000;
|
| 964 |
}
|
| 965 |
|
| 966 |
-
.
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
text-align: center;
|
| 973 |
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 974 |
}
|
| 975 |
|
| 976 |
-
.
|
| 977 |
-
|
| 978 |
-
font-size: 1.8rem;
|
| 979 |
-
font-weight: 700;
|
| 980 |
-
color: #1a1a1a;
|
| 981 |
}
|
| 982 |
|
|
|
|
|
|
|
|
|
|
| 983 |
|
| 984 |
-
.
|
| 985 |
-
|
| 986 |
border-radius: 12px;
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
animation: levelUpPulse 0.6s ease-in-out;
|
| 991 |
}
|
| 992 |
|
| 993 |
-
.
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
}
|
| 999 |
|
| 1000 |
-
.
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
}
|
| 1005 |
|
| 1006 |
-
.
|
|
|
|
| 1007 |
display: flex;
|
| 1008 |
-
flex-
|
| 1009 |
-
|
| 1010 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
| 1011 |
}
|
| 1012 |
|
| 1013 |
-
.
|
| 1014 |
-
|
| 1015 |
-
|
| 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 |
-
|
| 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 |
-
|
| 246 |
-
|
| 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 =
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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:
|
| 234 |
-
await
|
| 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
|
| 245 |
-
await
|
| 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
|
| 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
|
| 505 |
workflowState.currentStep = 'statsGenerating';
|
| 506 |
|
| 507 |
-
|
| 508 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 769 |
|
| 770 |
-
|
| 771 |
-
|
|
|
|
| 772 |
}
|
| 773 |
-
|
| 774 |
-
|
| 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
|
| 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
|
| 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
|
| 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.
|
| 39 |
class="piclet-image"
|
| 40 |
style="width: {size * 0.8}px; height: {size * 0.8}px;"
|
| 41 |
/>
|
| 42 |
-
<div class="
|
| 43 |
-
|
| 44 |
</div>
|
| 45 |
</div>
|
| 46 |
|
| 47 |
<div class="details-section">
|
| 48 |
-
<p class="nickname">{piclet.
|
| 49 |
-
<div class="
|
| 50 |
-
<span class="
|
| 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 |
-
.
|
| 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:
|
| 103 |
font-weight: bold;
|
| 104 |
-
|
| 105 |
}
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
.details-section {
|
| 108 |
height: 50px;
|
| 109 |
padding: 6px 8px;
|
|
@@ -116,8 +102,8 @@
|
|
| 116 |
}
|
| 117 |
|
| 118 |
.nickname {
|
| 119 |
-
margin: 0 0
|
| 120 |
-
font-size:
|
| 121 |
font-weight: 600;
|
| 122 |
text-align: center;
|
| 123 |
overflow: hidden;
|
|
@@ -126,29 +112,27 @@
|
|
| 126 |
color: #333;
|
| 127 |
}
|
| 128 |
|
| 129 |
-
.
|
| 130 |
display: flex;
|
| 131 |
-
|
| 132 |
-
|
| 133 |
}
|
| 134 |
|
| 135 |
-
.
|
| 136 |
font-size: 8px;
|
|
|
|
|
|
|
| 137 |
font-weight: 500;
|
| 138 |
-
text-
|
| 139 |
-
color: #666;
|
| 140 |
}
|
| 141 |
|
| 142 |
-
.
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
border-radius: 1.5px;
|
| 146 |
-
overflow: hidden;
|
| 147 |
}
|
| 148 |
|
| 149 |
-
.
|
| 150 |
-
|
| 151 |
-
|
| 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 |
-
|
| 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 (
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 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-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 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 |
-
<!--
|
| 213 |
-
<div class="
|
| 214 |
-
<
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
{/if}
|
| 221 |
</div>
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
<
|
| 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 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 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 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 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 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
<
|
| 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 |
-
|
| 364 |
-
|
| 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-
|
| 376 |
position: fixed;
|
| 377 |
top: 0;
|
| 378 |
left: 0;
|
| 379 |
right: 0;
|
| 380 |
bottom: 0;
|
| 381 |
-
background:
|
| 382 |
-
z-index: 1000;
|
| 383 |
display: flex;
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
.content-scroll {
|
| 389 |
-
flex: 1;
|
| 390 |
-
overflow-y: auto;
|
| 391 |
-
-webkit-overflow-scrolling: touch;
|
| 392 |
}
|
| 393 |
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
| 397 |
overflow: hidden;
|
| 398 |
-
|
| 399 |
-
|
| 400 |
}
|
| 401 |
|
| 402 |
-
.
|
| 403 |
-
background:
|
| 404 |
-
|
| 405 |
-
padding
|
| 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 |
-
.
|
| 435 |
-
|
| 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:
|
| 450 |
-
|
|
|
|
| 451 |
display: flex;
|
| 452 |
align-items: center;
|
| 453 |
justify-content: center;
|
| 454 |
-
|
|
|
|
| 455 |
}
|
| 456 |
|
| 457 |
-
.
|
| 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 |
-
.
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 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 |
-
|
| 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 |
-
|
| 530 |
-
|
| 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 |
-
|
| 546 |
-
|
| 547 |
-
margin:
|
| 548 |
}
|
| 549 |
|
| 550 |
-
.
|
| 551 |
-
|
|
|
|
|
|
|
| 552 |
border-radius: 12px;
|
| 553 |
-
|
| 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:
|
| 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 |
-
.
|
| 595 |
-
|
| 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 |
-
.
|
| 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 |
-
|
| 651 |
-
|
| 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 |
-
.
|
| 677 |
-
|
|
|
|
|
|
|
| 678 |
font-weight: 600;
|
| 679 |
-
color: #6c757d;
|
| 680 |
text-transform: uppercase;
|
| 681 |
-
|
| 682 |
}
|
| 683 |
|
| 684 |
-
.
|
| 685 |
-
|
| 686 |
-
font-size: 16px;
|
| 687 |
-
font-weight: 600;
|
| 688 |
-
color: #495057;
|
| 689 |
}
|
| 690 |
|
| 691 |
-
.
|
| 692 |
-
margin: 0;
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
font-style: italic;
|
| 696 |
}
|
| 697 |
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
border-radius: 12px;
|
| 703 |
-
padding: 16px;
|
| 704 |
-
border: 0.5px solid #c6c6c8;
|
| 705 |
}
|
| 706 |
|
| 707 |
-
.
|
| 708 |
display: flex;
|
| 709 |
-
|
| 710 |
-
|
| 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 |
-
|
| 727 |
-
.stat-row {
|
| 728 |
display: flex;
|
| 729 |
align-items: center;
|
| 730 |
-
gap:
|
| 731 |
-
|
| 732 |
}
|
| 733 |
|
| 734 |
-
.
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
font-size:
|
| 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 |
-
.
|
| 759 |
-
|
| 760 |
-
|
| 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 |
-
.
|
| 780 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
font-weight: 600;
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
text-align: right;
|
| 785 |
-
flex-shrink: 0;
|
| 786 |
}
|
| 787 |
|
| 788 |
-
.
|
| 789 |
-
|
| 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
|
| 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 =
|
| 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
|
| 47 |
const caughtPiclets = await getCaughtPiclets();
|
| 48 |
-
const uncaughtPiclets = await getUncaughtPiclets();
|
| 49 |
|
| 50 |
if (caughtPiclets.length === 0) {
|
| 51 |
-
//
|
| 52 |
-
|
| 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
|
| 72 |
}
|
| 73 |
|
| 74 |
-
// Player has
|
| 75 |
-
console.log('Generating
|
| 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 |
-
//
|
| 108 |
-
private static async
|
| 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
|
| 131 |
-
const
|
| 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 (
|
| 138 |
-
console.log('No uncaught piclets - returning empty wild encounters');
|
| 139 |
return encounters;
|
| 140 |
}
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 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
|
| 157 |
-
const displayName =
|
| 158 |
|
| 159 |
-
const
|
| 160 |
type: EncounterType.WILD_PICLET,
|
| 161 |
-
title: `Wild ${displayName}
|
| 162 |
-
description: `A
|
| 163 |
-
|
| 164 |
-
|
|
|
|
| 165 |
createdAt: new Date()
|
| 166 |
};
|
| 167 |
|
| 168 |
-
|
| 169 |
-
encounters.push(wildEncounter);
|
| 170 |
}
|
| 171 |
-
|
| 172 |
-
console.log('Generated', encounters.length, 'wild encounters');
|
| 173 |
return encounters;
|
| 174 |
}
|
| 175 |
|
| 176 |
-
// Get
|
| 177 |
-
|
| 178 |
-
|
| 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 |
-
|
| 197 |
-
|
| 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
|
| 3 |
-
import { PicletType
|
| 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
|
| 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 |
-
|
| 30 |
-
|
| 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 |
-
//
|
| 114 |
-
typeId: data.name
|
| 115 |
-
nickname: stats.name || data.name,
|
| 116 |
-
primaryType: primaryType,
|
| 117 |
-
|
| 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 |
-
//
|
| 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 |
-
|
| 168 |
-
|
| 169 |
-
return
|
| 170 |
}
|
| 171 |
|
| 172 |
-
//
|
| 173 |
-
export async function
|
| 174 |
-
await db.picletInstances.update(
|
| 175 |
-
caught: true,
|
| 176 |
-
caughtAt: new Date()
|
| 177 |
-
});
|
| 178 |
}
|
| 179 |
|
| 180 |
-
//
|
| 181 |
-
export async function
|
| 182 |
-
|
| 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
|
| 193 |
export async function getAllPicletInstances(): Promise<PicletInstance[]> {
|
| 194 |
return await db.picletInstances.toArray();
|
| 195 |
}
|
| 196 |
|
| 197 |
-
// Get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
export async function getRosterPiclets(): Promise<PicletInstance[]> {
|
| 199 |
-
|
| 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 |
-
//
|
| 212 |
-
export async function
|
| 213 |
-
await db.picletInstances.
|
| 214 |
-
isInRoster: position !== undefined,
|
| 215 |
-
rosterPosition: position
|
| 216 |
-
});
|
| 217 |
}
|
| 218 |
|
| 219 |
-
// Move piclet to roster
|
| 220 |
-
export async function moveToRoster(
|
| 221 |
-
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
export async function swapRosterPositions(id1: number, position1: number, id2: number, position2: number): Promise<void> {
|
| 245 |
-
await db.
|
| 246 |
-
|
| 247 |
-
await db.picletInstances.update(id2, { rosterPosition: position1 });
|
| 248 |
-
});
|
| 249 |
}
|
| 250 |
|
| 251 |
-
// Move
|
| 252 |
-
export async function moveToStorage(
|
| 253 |
-
await db.picletInstances.update(
|
| 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
|
| 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 |
-
//
|
| 24 |
typeId: string;
|
| 25 |
nickname?: string;
|
| 26 |
primaryType: PicletType;
|
| 27 |
-
|
| 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 |
-
//
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|