File size: 38,207 Bytes
6c7a453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
from typing import Dict, List, Any, Optional, Tuple
import json
import os
import re

LOGIC_PATH = os.path.join(os.path.dirname(__file__), "../../data/datasets/abilities_logic.json")
METADATA_PATH = os.path.join(os.path.dirname(__file__), "../../data/datasets/abilities.json")

def load_abilities_config():
    config = {}
    
    if os.path.exists(LOGIC_PATH):
        with open(LOGIC_PATH, 'r') as f:
            config = json.load(f)
            
    if os.path.exists(METADATA_PATH):
        with open(METADATA_PATH, 'r') as f:
            metadata = json.load(f)
            for id, data in metadata.items():
                if id in config:
                    config[id]["desc"] = data.get("desc", config[id].get("desc", ""))
                    config[id]["shortDesc"] = data.get("shortDesc", config[id].get("shortDesc", ""))
                else:
                    config[id] = {
                        "id": id,
                        "name": data.get("name", id),
                        "desc": data.get("desc", ""),
                        "shortDesc": data.get("shortDesc", "")
                    }
    
    if "levitate" in config:
        config["levitate"]["immunities"] = {"types": ["ground"]}
        
    return config

ABILITIES_CONFIG = load_abilities_config()

class Ability:
    def __init__(self, name: str):
        self.id = name.lower().replace(" ", "").replace("-", "").replace("'", "").replace("(", "").replace(")", "")
        self.config = ABILITIES_CONFIG.get(self.id, {})
        self.name = self.config.get("name", name)
        self.description = self.config.get("desc", "No effect.")
        # State for specific abilities
        self.state = {}
        if self.id == 'slowstart':
            self.state['counter'] = 5
        elif self.id == 'truant':
            self.state['skip'] = False
        
    def _parse_chain_modify(self, logic_str: str) -> float:
        """Extract multiplier from chainModify([num1, num2]) or chainModify(float)."""
        if not logic_str or "chainModify" not in logic_str:
            return 1.0
            
        # Look for [num1, num2] pattern
        match = re.search(r'chainModify\(\[?([\d., ]+)\]?\)', logic_str)
        if match:
            parts = [p.strip() for p in match.group(1).split(',')]
            if len(parts) >= 2:
                try:
                    return float(parts[0]) / float(parts[1])
                except (ValueError, ZeroDivisionError):
                    return 1.0
            else:
                try:
                    return float(parts[0])
                except ValueError:
                    return 1.0
        
        # Look for modify(stat, mult) pattern
        match = re.search(r'this\.modify\([^,]+,\s*([\d.]+)\)', logic_str)
        if match:
            try:
                return float(match.group(1))
            except ValueError:
                return 1.0
                
        return 1.0

    def _check_condition(self, logic_str: str, pokemon, opponent, move=None) -> bool:
        """Check if conditions in logic_str are met."""
        if not logic_str:
            return True
            
        curr_hp = getattr(pokemon, 'current_hp', getattr(pokemon, 'max_hp', 100))
        max_hp = getattr(pokemon, 'max_hp', 100)
            
        # Pattern: pokemon.hp <= pokemon.maxhp / 3
        if "hp <= pokemon.maxhp / 3" in logic_str or "hp <= attacker.maxhp / 3" in logic_str:
            if curr_hp <= max_hp / 3:
                return True
            return False
            
        if "hp <= pokemon.maxhp / 2" in logic_str:
            if curr_hp <= max_hp / 2:
                return True
            return False

        # Pattern: pokemon.status
        if "pokemon.status" in logic_str or "attacker.status" in logic_str:
            if pokemon.major_status:
                return True
            return False
            
        # Pattern: move.type === 'Fire'
        match = re.search(r"move\.type === '([^']+)'", logic_str)
        if match:
            target_type = match.group(1).lower()
            if move and move.type.lower() == target_type:
                return True
            return False

        # Pattern: this.field.isTerrain('electricterrain')
        match = re.search(r"isTerrain\('([^']+)'\)", logic_str)
        if match:
            target_terrain = match.group(1).lower()
            # Simple check for now - would need terrain system in game.py
            return False # Fallback until terrain is implemented
            
        # Handle basePowerAfterMultiplier <= 60 or move.power <= 60
        if "power <= 60" in logic_str or "basePowerAfterMultiplier <= 60" in logic_str:
            if move and 0 < move.power <= 60:
                return True
            return False
        
        # Generic power check
        match = re.search(r"power <= (\d+)", logic_str)
        if match:
            threshold = int(match.group(1))
            if move and 0 < move.power <= threshold:
                return True
            return False

        # Pattern: this.effectState.counter
        if "this.effectState.counter" in logic_str:
            if self.state.get('counter', 0) > 0:
                return True
            return False

        # Pattern: move.flags['pulse']
        match = re.search(r"move\.flags\[['\"](\w+)['\"]\]", logic_str)
        if match:
            flag = match.group(1).lower()
            if move and hasattr(move, 'flags') and move.flags.get(flag):
                return True
            return False

        # If no obvious condition, assume it applies only if it's a simple logic string
        # If it contains complex JS patterns we don't recognize, it's safer to return False
        # than to always return True (which was causing over-damage)
        if "(" in logic_str or "{" in logic_str or "===" in logic_str:
            return False
            
        return True

    def _extract_popups(self, logic_str: str, pokemon) -> List[Dict[str, Any]]:
        """Detect and extract battle log/popup events from Showdown-style logic strings."""
        if not logic_str: return []
        results = []
        is_p = hasattr(pokemon, "is_player") and pokemon.is_player
        
        # Pattern mappings for common Showdown log calls
        patterns = [
            (r"this\.add\('-ability',\s*[^,]+,\s*'([^']+)'\)", "{user}'s {ability} activated!"),
            (r"this\.add\('cant',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user} is loafing around!"),
            (r"this\.add\('-activate',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user}'s {ability} activated!"),
            (r"this\.add\('-immune',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user} is immune thanks to its {ability}!"),
            (r"this\.add\('-fail',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user}'s {ability} failed!"),
            (r"this\.add\('-block',\s*[^,]+,\s*'ability:\s*([^']+)'\)", "{user}'s {ability} blocked the effect!"),
        ]
        
        for pattern, template in patterns:
            matches = re.findall(pattern, logic_str)
            for ability_name in matches:
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": template.format(user=pokemon.get_display_name(), ability=ability_name),
                    "is_player": is_p
                })
        
        return results

    def modify_stat(self, pokemon, stat_name: str, value: int) -> int:
        """Applies stat multipliers based on the ability."""
        # 1. Check direct config (legacy/simple)
        condition = self.config.get("condition")
        if condition == "has_status":
            if not pokemon.major_status:
                return value
        
        stat_modifiers = self.config.get("stat_modifiers", {})
        multiplier = stat_modifiers.get(stat_name, 1.0)
        
        if multiplier != 1.0:
            value = int(value * multiplier)
            
        # 2. Check dynamic logic from JSON
        stat_map = {
            'attack': 'onModifyAtk',
            'defense': 'onModifyDef',
            'special_attack': 'onModifySpA',
            'special_defense': 'onModifySpD',
            'speed': 'onModifySpe'
        }
        
        hook = stat_map.get(stat_name)
        if hook and hook in self.config:
            logic = self.config[hook]
            
            # Special case for Slow Start
            if self.id == 'slowstart' and self.state.get('counter', 0) > 0:
                multiplier = 0.5
                value = int(value * multiplier)
            elif self._check_condition(logic, pokemon, None):
                multiplier = self._parse_chain_modify(logic)
                value = int(value * multiplier)
        
        return value

    def _parse_boost_amounts(self, logic_str: str) -> Dict[str, int]:
        """Extract boost amounts from ability logic strings."""
        boosts = {}
        
        if not logic_str:
            return boosts
        
        # Pattern: this.boost({ atk: 2, spa: 1 }, target, ...)
        match = re.search(r"this\.boost\(\{\s*([^}]+)\s*\}", logic_str)
        if match:
            boost_str = match.group(1)
            # Extract individual stats: atk: 1, def: -1, etc.
            stat_pairs = re.findall(r"(\w+):\s*(-?\d+)", boost_str)
            for stat_abbr, value in stat_pairs:
                stat_name = self._abbr_to_stat_name(stat_abbr)
                if stat_name:
                    boosts[stat_name] = int(value)
        
        return boosts
    
    def _abbr_to_stat_name(self, abbr: str) -> Optional[str]:
        """Convert stat abbreviations to full names."""
        abbr_map = {
            'hp': 'hp',
            'atk': 'attack',
            'def': 'defense',
            'spa': 'special_attack',
            'spd': 'special_defense',
            'spe': 'speed'
        }
        return abbr_map.get(abbr.lower())

    def on_switch_in(self, pokemon, opponent) -> List[Dict[str, Any]]:
        results = []
        is_p = hasattr(pokemon, "is_player") and pokemon.is_player
        
        # Priority handlers for common switch-in mechanics that need complex logic
        MECHANICS = {
            "download": None,
        }
        
        if self.id in MECHANICS:
            if self.id == "download":
                if hasattr(opponent, "defense") and hasattr(opponent, "special_defense"):
                    stat = "attack" if opponent.defense < opponent.special_defense else "special_attack"
                    msg = pokemon.modify_stat_stage(stat, 1)
                    if msg: results.append({"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": f"{pokemon.get_display_name()}'s Download boosted its {stat.replace('_', ' ').title()}!", "is_player": is_p})
            else:
                # Fallback for any remaining mechanics
                pass
        
        # Special switch-in popups for bad abilities
        if self.id == 'slowstart' and self.state.get('counter', 0) > 0:
            results.append({
                "type": "ability",
                "ability_name": self.name,
                "pokemon_name": pokemon.get_display_name(),
                "message": f"{pokemon.get_display_name()} can't get it going!",
                "is_player": is_p
            })
        elif self.id == 'defeatist':
            is_active = pokemon.current_hp <= pokemon.max_hp / 2
            if is_active and not self.state.get('activated'):
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()}'s Defeatist activated! Its Attack and Sp. Atk were halved!",
                    "is_player": is_p
                })
                self.state['activated'] = True
            elif not is_active:
                self.state['activated'] = False

        # Process dynamic hooks (onStart, onSwitchIn)
        for hook in ["onStart", "onSwitchIn"]:
            logic = self.config.get(hook)
            if not logic: continue
            
            # Extract generic popups from JSON logic (e.g. Turboblaze)
            results.extend(self._extract_popups(logic, pokemon))
            
            # Weather/Terrain
            if "setWeather" in logic or "setTerrain" in logic:
                res = {"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": f"{pokemon.get_display_name()}'s {self.name} changed the field!", "is_player": is_p}
                
                # Extract the weather/terrain name if possible
                w_match = re.search(r"setWeather\('([^']+)'\)", logic)
                t_match = re.search(r"setTerrain\('([^']+)'\)", logic)
                if w_match: res["set_weather"] = w_match.group(1).lower()
                if t_match: res["set_terrain"] = t_match.group(1).lower()
                
                results.append(res)
            
            # Boosts
            boosts = self._parse_boost_amounts(logic)
            if boosts:
                target = opponent if "target" in logic else pokemon
                for s_name, stages in boosts.items():
                    if hasattr(target, "modify_stat_stage"):
                        msg = target.modify_stat_stage(s_name, stages)
                        if msg: results.append({"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": msg if isinstance(msg, str) else f"{pokemon.get_display_name()}'s {self.name} activated!", "is_player": is_p})

        # Process effect list
        for effect in self.config.get("on_switch_in", []):
            target = opponent if effect.get("target") == "opponent" else pokemon
            if effect.get("action") == "boost":
                for s_name, stages in effect.get("stats", {}).items():
                    if hasattr(target, "modify_stat_stage"):
                        msg = target.modify_stat_stage(s_name, stages)
                        if msg:
                            f_msg = effect.get("message", msg).format(user=pokemon.get_display_name(), target=target.get_display_name())
                            results.append({"type": "ability", "ability_name": self.name, "pokemon_name": pokemon.get_display_name(), "message": f_msg, "is_player": is_p})
        
        return results

    def on_turn_end(self, pokemon, opponent) -> List[Dict[str, Any]]:
        """Trigger end-of-turn effects (e.g. Speed Boost)."""
        results = []
        
        # Common turn-end abilities
        if self.id == "speedboost":
            msg = pokemon.modify_stat_stage("speed", 1)
            if msg:
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()}'s Speed Boost increased its Speed!",
                    "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                })
        elif self.id == "losteye":
            msg = pokemon.modify_stat_stage("accuracy", -1)
            if msg:
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()}'s Lost Eye lowered its Accuracy!",
                    "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                })
        elif self.id == "powerspotboost":
            msg = pokemon.modify_stat_stage("special_attack", 1)
            if msg:
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()}'s ability boosted its Special Attack!",
                    "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                })
        elif self.id == "contrariness":
            # Flips stat changes
            msg = pokemon.modify_stat_stage("attack", 1)  # Placeholder - actual logic would flip boosts
            if msg:
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()}'s Contrariness flipped the stat changes!",
                    "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                })
        
        # Parse onResidual for passive damage/healing
        residual_logic = self.config.get("onResidual")
        if residual_logic:
            if "damage" in residual_logic.lower():
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()}'s {self.name} activated!",
                    "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                })
        
        # Slow Start counter
        if self.id == 'slowstart' and self.state.get('counter', 0) > 0:
            self.state['counter'] -= 1
            if self.state['counter'] == 0:
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()} finally got its act together!",
                    "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                })
        
        return results

    def on_faint(self, pokemon, opponent) -> List[Dict[str, Any]]:
        """Trigger effects when the Pokemon faints (e.g. Aftermath)."""
        results = []
        if self.id == "aftermath":
            damage = opponent.max_hp // 4
            opponent.current_hp = max(0, opponent.current_hp - damage)
            results.append({
                "type": "ability",
                "ability_name": self.name,
                "pokemon_name": pokemon.get_display_name(),
                "message": f"{pokemon.get_display_name()}'s Aftermath hurt {opponent.get_display_name()}!",
                "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
            })
        return results

    def on_source_after_faint(self, pokemon, opponent) -> List[Dict[str, Any]]:
        """Trigger effects when this Pokemon faints an opponent (e.g. Moxie)."""
        results = []
        
        # Generic parsing for boost patterns in onSourceAfterFaint hook
        logic = self.config.get("onSourceAfterFaint")
        if logic:
            # Check for bestStat call (Beast Boost)
            if "getBestStat" in logic:
                if hasattr(pokemon, "get_best_stat"):
                    best_stat_abbr = pokemon.get_best_stat(True, True)
                    # Convert to full name for modify_stat_stage
                    stat_map = {'atk': 'attack', 'def': 'defense', 'spa': 'special_attack', 'spd': 'special_defense', 'spe': 'speed'}
                    stat_name = stat_map.get(best_stat_abbr, best_stat_abbr)
                    
                    if hasattr(pokemon, "modify_stat_stage"):
                        msg = pokemon.modify_stat_stage(stat_name, 1)
                        if msg:
                            results.append({
                                "type": "ability",
                                "ability_name": self.name,
                                "pokemon_name": pokemon.get_display_name(),
                                "message": f"{pokemon.get_display_name()}'s {self.name} boosted its {stat_name}!",
                                "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                            })
                return results

            boosts = self._parse_boost_amounts(logic)
            if boosts:
                for stat_name, stages in boosts.items():
                    # In onSourceAfterFaint, 'length' is usually used for boost amount (usually 1)
                    # We'll use the parsed value or default to 1
                    amount = stages if stages != 0 else 1
                    if hasattr(pokemon, "modify_stat_stage"):
                        msg = pokemon.modify_stat_stage(stat_name, amount)
                        if msg:
                            results.append({
                                "type": "ability",
                                "ability_name": self.name,
                                "pokemon_name": pokemon.get_display_name(),
                                "message": f"{pokemon.get_display_name()}'s {self.name} boosted its {stat_name}!",
                                "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                            })
                            
        return results

    def on_any_faint(self, pokemon) -> List[Dict[str, Any]]:
        """Trigger effects when any Pokemon faints (e.g. Soul-Heart)."""
        results = []
        
        logic = self.config.get("onAnyFaint")
        if logic:
            boosts = self._parse_boost_amounts(logic)
            if boosts:
                for stat_name, stages in boosts.items():
                    if hasattr(pokemon, "modify_stat_stage"):
                        msg = pokemon.modify_stat_stage(stat_name, stages)
                        if msg:
                            results.append({
                                "type": "ability",
                                "ability_name": self.name,
                                "pokemon_name": pokemon.get_display_name(),
                                "message": f"{pokemon.get_display_name()}'s {self.name} boosted its {stat_name}!",
                                "is_player": hasattr(pokemon, "is_player") and pokemon.is_player
                            })
                            
        return results

    def on_stat_drop(self, pokemon, stat_name: str) -> List[Dict[str, Any]]:
        """Trigger effects when a stat is lowered (e.g. Defiant)."""
        results = []
        
        if self.id == "defiant":
            # Defiant: +2 Atk when any stat is lowered
            if hasattr(pokemon, "modify_stat_stage"):
                pokemon.modify_stat_stage("attack", 2)
        elif self.id == "competitive":
            # Competitive: +2 SpA when any stat is lowered
            if hasattr(pokemon, "modify_stat_stage"):
                pokemon.modify_stat_stage("special_attack", 2)
                
        return results

    def modify_damage_taken(self, pokemon, opponent, move, damage: int) -> int:
        """Modifies damage taken by the Pokemon."""
        final_damage = damage
        
        # Abilities that reduce damage
        if self.id == "filter" or self.id == "solidrock":
            # Reduces super-effective damage to 1/4x (6/8 = 0.75)
            if hasattr(move, 'effectiveness'):
                if move.effectiveness > 1:
                    final_damage = int(final_damage * 0.75)
        elif self.id == "thickfat":
            # Reduces Fire and Ice type moves by 50%
            if hasattr(move, 'type') and move.type.lower() in ['fire', 'ice']:
                final_damage = int(final_damage * 0.5)
        elif self.id == "waterabsorb" or self.id == "dryskin":
            # Heals from Water type moves instead of taking damage
            if hasattr(move, 'type') and move.type.lower() == 'water':
                return 0
            if hasattr(move, 'type') and move.type.lower() == 'fire':
                final_damage = int(final_damage * 1.25)
        elif self.id == "heatproof":
            if hasattr(move, 'type') and move.type.lower() == 'fire':
                final_damage = int(final_damage * 0.5)
        elif self.id == "flashfire":
            # Absorbs Fire type moves
            if hasattr(move, 'type') and move.type.lower() == 'fire':
                return 0
        elif self.id == "voltabsorb":
            # Absorbs Electric type moves
            if hasattr(move, 'type') and move.type.lower() == 'electric':
                return 0
        elif self.id == "sapsipper":
            # Absorbs Grass type moves
            if hasattr(move, 'type') and move.type.lower() == 'grass':
                return 0
        elif self.id == "furcoat":
            # Halves physical damage
            if hasattr(move, 'category') and move.category == 'physical':
                final_damage = int(final_damage * 0.5)
        elif self.id == "marvelscale":
            # Reduces all damage to 50% when having status
            if pokemon.major_status:
                final_damage = int(final_damage * 0.5)
        elif self.id == "unaware":
            # Ignores opponent stat boosts (reduce damage)
            final_damage = int(final_damage * 0.8)  # Simplified
        elif self.id == "regenerator":
            # Heals 1/3 HP per turn (handled elsewhere)
            pass
        
        # Generic damage reduction from onDamage hooks
        on_damage = self.config.get("onDamage")
        if on_damage:
            if "damage * 0.5" in on_damage or "chainModify(0.5)" in on_damage:
                final_damage = int(final_damage * 0.5)
            elif "damage * 0.75" in on_damage or "chainModify(0.75)" in on_damage:
                final_damage = int(final_damage * 0.75)
        
        return final_damage

    def modify_damage_dealt(self, pokemon, opponent, move, damage: int) -> int:
        """Modifies damage dealt by the Pokemon."""
        final_damage = damage
        
        # Hardcoded abilities with damage modifiers
        if self.id == "technician":
            # 1.5x damage for moves with 60 or less base power
            if hasattr(move, 'power') and 0 < move.power <= 60:
                final_damage = int(final_damage * 1.5)
        elif self.id == "adaptability":
            # 2.25x STAB instead of 1.5x (handled in STAB calculation)
            pass
        elif self.id == "sheerforce":
            # 1.3125x (1.3x boost) for moves with secondary effects
            if hasattr(move, 'secondary') and move.secondary:
                final_damage = int(final_damage * 1.3125)
        elif self.id == "hugepower" or self.id == "purplepower":
            # Doubles attack (handled in modify_stat)
            pass
        elif self.id == "ironbarbs":
            # Reflects 1/8 damage back (handled separately)
            pass
        elif self.id == "roughskin":
            # Reflects 1/8 damage back (handled separately)
            pass
        elif self.id == "effectspore":
            # 30% chance to cause status on contact (handled separately)
            pass
        elif self.id == "sandstream":
            # Weakens water moves (handled with weather)
            pass
        elif self.id == "swordofruin":
            # Reduces opponent Special Defense (handled separately)
            final_damage = int(final_damage * 0.8)  # Simplified
        elif self.id == "beadsofruin":
            # Reduces opponent Special Defense (handled separately)
            final_damage = int(final_damage * 0.8)  # Simplified
        elif self.id == "tabletsofruin":
            # Reduces opponent Special Defense (handled separately)
            final_damage = int(final_damage * 0.8)  # Simplified
        elif self.id == "vesselofruin":
            # Reduces opponent Special Defense (handled separately)
            final_damage = int(final_damage * 0.8)  # Simplified
        
        # 1. Check direct modifiers list (legacy/simple)
        modifiers = self.config.get("damage_modifiers", [])
        for mod in modifiers:
            condition = mod.get("condition")
            multiplier = mod.get("multiplier", 1.0)
            
            if condition == "hp_threshold":
                threshold = mod.get("threshold", 0.33)
                required_type = mod.get("move_type")
                if pokemon.current_hp / pokemon.max_hp <= threshold:
                    if not required_type or move.type.lower() == required_type.lower():
                        final_damage = int(final_damage * multiplier)
            elif condition == "base_power_below":
                threshold = mod.get("threshold", 60)
                if move.power <= threshold and move.power > 0:
                    final_damage = int(final_damage * multiplier)
                    
        # 2. Check raw logic for multipliers (Technician, Aerilate, etc.)
        for hook in ["onBasePower", "onModifyAtk", "onModifySpA"]:
            # Only apply Atk/SpA hooks if they match the move category
            if hook == "onModifyAtk" and move.category != 'physical':
                continue
            if hook == "onModifySpA" and move.category != 'special':
                continue
                
            logic = self.config.get(hook)
            if not logic: continue
            
            if self._check_condition(logic, pokemon, opponent, move):
                multiplier = self._parse_chain_modify(logic)
                final_damage = int(final_damage * multiplier)
                            
        return final_damage

    def is_immune(self, move_type: str, move_category: str) -> bool:
        """Checks if the ability provides immunity to a certain move type/category."""
        immunities = self.config.get("immunities", {})
        
        # 1. Check direct immunities (e.g. Levitate)
        if move_type.lower() in [t.lower() for t in immunities.get("types", [])]:
            return True
        
        # 2. Hardcoded type immunities
        type_immunities = {
            'levitate': ['ground'],
            'waterabsorb': ['water'],
            'voltabsorb': ['electric'],
            'dryskin': ['water'],
            'flashfire': ['fire'],
            'sapsipper': ['grass'],
            'lightningrod': ['electric'],
            'motordrive': ['electric'],
            'immunity': ['poison'],
            'wonderguard': [],  # Only takes super-effective damage
            'goodasgold': ['item-based'],
        }
        
        if self.id in type_immunities:
            if move_type.lower() in type_immunities[self.id]:
                return True
            
        # 3. Check raw logic for immunity (onTryHit)
        on_try_hit = self.config.get("onTryHit")
        if on_try_hit:
            # Check if this move type is mentioned as being blocked
            if f"move.type === '{move_type.capitalize()}'" in on_try_hit or f"move.type === '{move_type.lower()}'" in on_try_hit:
                if "return null" in on_try_hit or "return false" in on_try_hit:
                    return True

        return False

    def get_type_change(self, move) -> Optional[str]:
        """Returns the type a move is changed to by this ability (e.g. Aerilate)."""
        type_changes = {
            'aerilate': 'flying',
            'pixilate': 'fairy',
            'refrigerate': 'ice',
            'iondeluge': 'electric',
            'normalize': 'normal',
        }
        
        if self.id in type_changes and hasattr(move, 'type') and move.type.lower() == 'normal':
            return type_changes[self.id]
        
        return None

    def get_weather_boost(self, move_type: str, weather: Optional[str]) -> float:
        """Returns damage multiplier based on weather and this ability."""
        if not weather:
            return 1.0
        
        weather_boosts = {
            'drizzle': {'water': 1.5, 'fire': 0.5},
            'drought': {'fire': 1.5, 'water': 0.5},
            'sandstream': {'rock': 1.5, 'steel': 1.5, 'ground': 1.5, 'fire': 0.5},
            'snowwarning': {'ice': 1.5, 'fire': 0.5},
            'hail': {'ice': 1.5},
        }
        
        if weather in weather_boosts:
            return weather_boosts[weather].get(move_type.lower(), 1.0)
        
        return 1.0

    def get_stab_multiplier(self) -> float:
        """Returns the STAB multiplier (usually 1.5, Adaptability makes it 2.0)."""
        # 1. Check direct config
        if "on_modify_stab" in self.config:
            return self.config["on_modify_stab"]
        
        # Hardcoded STAB multipliers
        stab_multipliers = {
            'adaptability': 2.0,  # Adaptability changes STAB from 1.5x to 2x.
        }
        
        if self.id in stab_multipliers:
            return stab_multipliers[self.id]
            
        # 2. Check raw logic for Adaptability pattern
        on_modify_stab = self.config.get("onModifySTAB")
        if on_modify_stab:
            if "return 2.25" in on_modify_stab: return 2.25
            if "return 2" in on_modify_stab: return 2.0
            
        return 1.5

    def get_secondary_multiplier(self) -> float:
        """Returns the multiplier for secondary effect chances."""
        # 1. Check direct config
        if "secondary_multiplier" in self.config:
            return self.config["secondary_multiplier"]
        
        # Hardcoded secondary multipliers
        secondary_multipliers = {
            'serenegrace': 2.0,  # 2x secondary chance
            'sheerforce': 1.3,  # 1.3x damage but removes secondary effects
        }
        
        if self.id in secondary_multipliers:
            return secondary_multipliers[self.id]
            
        # 2. Check raw logic for Serene Grace pattern
        on_modify_move = self.config.get("onModifyMove")
        if on_modify_move:
            if "* 2" in on_modify_move or "*= 2" in on_modify_move: return 2.0
            if "* 3" in on_modify_move or "*= 3" in on_modify_move: return 3.0
            
        return 1.0

    def get_accuracy_modifier(self) -> float:
        """Returns the accuracy multiplier for moves used by this ability."""
        accuracy_modifiers = {
            'compoundeyes': 1.3,  # 1.3x accuracy
            'victorystar': 1.1,  # 1.1x accuracy
            'keeneye': 1.0,  # Prevents accuracy lowering
        }
        
        if self.id in accuracy_modifiers:
            return accuracy_modifiers[self.id]
        
        return 1.0

    def get_ability_summary(self) -> Dict[str, Any]:
        """Returns a summary of what this ability does."""
        summary = {
            'id': self.id,
            'name': self.name,
            'description': self.description,
            'rating': self.config.get('rating', 0),
        }
        
        # Determine ability category
        hooks = list(self.config.keys())
        
        if any(h in hooks for h in ['onStart', 'onSwitchIn', 'drizzle', 'drought']):
            summary['type'] = 'Stat/Weather Setter'
        elif any(h in hooks for h in ['onBasePower', 'onModifyAtk', 'onModifySpA']):
            summary['type'] = 'Damage Modifier'
        elif any(h in hooks for h in ['onDamage', 'onTryHit']):
            summary['type'] = 'Defensive'
        elif any(h in hooks for h in ['onResidual']):
            summary['type'] = 'End-of-turn'
        elif any(h in hooks for h in ['onTryBoost']):
            summary['type'] = 'Stat Protection'
        else:
            summary['type'] = 'Special Effect'
        
        return summary

    def get_priority_modification(self) -> float:
        """Returns priority modification (e.g. Stall)."""
        return self.config.get('onFractionalPriority', 0.0)

    def can_use_move(self, pokemon) -> Tuple[bool, str]:
        """Checks if the ability allows the move (e.g. Truant)."""
        if self.id == 'truant':
            if self.state.get('skip'):
                self.state['skip'] = False
                return False, f"{pokemon.get_display_name()} is loafing around!"
            self.state['skip'] = True
        return True, ""

    def on_damage(self, pokemon, damage: int) -> List[Dict[str, Any]]:
        """Trigger effects when taking damage (e.g. Defeatist)."""
        results = []
        is_p = hasattr(pokemon, "is_player") and pokemon.is_player
        
        if self.id == 'defeatist':
            # Check if we just dropped below 50%
            old_hp = pokemon.current_hp + damage
            is_active = pokemon.current_hp <= pokemon.max_hp / 2
            
            if old_hp > pokemon.max_hp / 2 and is_active:
                if not self.state.get('activated'):
                    results.append({
                        "type": "ability",
                        "ability_name": self.name,
                        "pokemon_name": pokemon.get_display_name(),
                        "message": f"{pokemon.get_display_name()}'s {self.name} activated! Its Attack and Sp. Atk were halved!",
                        "is_player": is_p
                    })
                    self.state['activated'] = True
        elif self.id == 'sturdy':
            # If the pokemon survived with 1 HP and the damage was essentially its entire HP
            # We check if it's currently at 1 and the damage dealt was prev_hp - 1
            if pokemon.current_hp == 1 and damage >= 1:
                # This is a bit of a heuristic but works since Sturdy only triggers at max HP
                results.append({
                    "type": "ability",
                    "ability_name": self.name,
                    "pokemon_name": pokemon.get_display_name(),
                    "message": f"{pokemon.get_display_name()} endured the hit with Sturdy!",
                    "is_player": is_p
                })
        
        return results

    def can_use_item(self) -> bool:
        """Checks if the ability allows using held items (e.g. Klutz)."""
        if self.id == 'klutz':
            return False
        return True

def create_ability(name: str) -> Ability:
    """Helper to create an ability instance."""
    if not name:
        return Ability("noability")
    return Ability(name)