Fraser commited on
Commit
7b95878
·
1 Parent(s): 1ecc382
src/lib/battle-engine/BattleEngine.ts CHANGED
@@ -310,6 +310,19 @@ export class BattleEngine {
310
  private processDamageEffect(effect: { amount?: DamageAmount; formula?: string; value?: number; multiplier?: number }, attacker: BattlePiclet, target: BattlePiclet, move: Move): void {
311
  let damage = 0;
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  // Handle different damage formulas
314
  if (effect.formula) {
315
  damage = this.calculateDamageByFormula(effect, attacker, target, move);
@@ -317,6 +330,26 @@ export class BattleEngine {
317
  damage = this.calculateStandardDamage(effect.amount, attacker, target, move);
318
  }
319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  // Apply damage
321
  if (damage > 0) {
322
  target.currentHp = Math.max(0, target.currentHp - damage);
@@ -420,6 +453,12 @@ export class BattleEngine {
420
  }
421
  }
422
 
 
 
 
 
 
 
423
  if (!target.statusEffects.includes(effect.status)) {
424
  target.statusEffects.push(effect.status);
425
  this.log(`${target.definition.name} was ${effect.status}ed!`);
@@ -590,8 +629,86 @@ export class BattleEngine {
590
  }
591
 
592
  private processMechanicOverrideEffect(effect: { mechanic: string; value: any; condition?: string }, target: BattlePiclet): void {
593
- // Store mechanic override for processing
594
- // This is a placeholder - full implementation would be complex
 
 
 
 
 
 
 
 
 
 
595
  this.log(`Mechanic override '${effect.mechanic}' applied to ${target.definition.name}!`);
596
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  }
 
310
  private processDamageEffect(effect: { amount?: DamageAmount; formula?: string; value?: number; multiplier?: number }, attacker: BattlePiclet, target: BattlePiclet, move: Move): void {
311
  let damage = 0;
312
 
313
+ // Check type immunity first
314
+ if (this.checkTypeImmunity(target, move.type)) {
315
+ this.log(`${target.definition.name} is immune to ${move.type} type moves!`);
316
+ return;
317
+ }
318
+
319
+ // Check flag interactions
320
+ const flagInteraction = this.checkFlagInteraction(target, move.flags);
321
+ if (flagInteraction === 'immune') {
322
+ this.log(`${target.definition.name} is immune to this move!`);
323
+ return;
324
+ }
325
+
326
  // Handle different damage formulas
327
  if (effect.formula) {
328
  damage = this.calculateDamageByFormula(effect, attacker, target, move);
 
330
  damage = this.calculateStandardDamage(effect.amount, attacker, target, move);
331
  }
332
 
333
+ // Apply flag interaction modifiers
334
+ if (flagInteraction === 'weak') {
335
+ damage = Math.floor(damage * 1.5);
336
+ this.log("It's super effective!");
337
+ } else if (flagInteraction === 'resist') {
338
+ damage = Math.floor(damage * 0.5);
339
+ this.log("It's not very effective...");
340
+ }
341
+
342
+ // Apply damage multiplier from abilities
343
+ const damageMultiplier = this.getDamageMultiplier(attacker);
344
+ damage = Math.floor(damage * damageMultiplier);
345
+
346
+ // Check for critical hits
347
+ const critMod = this.checkCriticalHitModification(attacker, target);
348
+ if (critMod === 'always' || (critMod === 'normal' && Math.random() < 0.0625)) { // 1/16 base crit rate
349
+ damage = Math.floor(damage * 1.5);
350
+ this.log("A critical hit!");
351
+ }
352
+
353
  // Apply damage
354
  if (damage > 0) {
355
  target.currentHp = Math.max(0, target.currentHp - damage);
 
453
  }
454
  }
455
 
456
+ // Check for status immunity
457
+ if (this.checkStatusImmunity(target, effect.status)) {
458
+ this.log(`${target.definition.name} is immune to ${effect.status}!`);
459
+ return;
460
+ }
461
+
462
  if (!target.statusEffects.includes(effect.status)) {
463
  target.statusEffects.push(effect.status);
464
  this.log(`${target.definition.name} was ${effect.status}ed!`);
 
629
  }
630
 
631
  private processMechanicOverrideEffect(effect: { mechanic: string; value: any; condition?: string }, target: BattlePiclet): void {
632
+ // Store mechanic override as temporary effect for processing during relevant calculations
633
+ target.temporaryEffects.push({
634
+ effect: {
635
+ type: 'mechanicOverride',
636
+ mechanic: effect.mechanic,
637
+ value: effect.value,
638
+ condition: effect.condition,
639
+ target: 'self'
640
+ } as any,
641
+ duration: effect.condition === 'restOfBattle' ? 999 : 1
642
+ });
643
+
644
  this.log(`Mechanic override '${effect.mechanic}' applied to ${target.definition.name}!`);
645
  }
646
+
647
+ // Helper methods for checking mechanic overrides
648
+ private hasMechanicOverride(piclet: BattlePiclet, mechanic: string): any {
649
+ const override = piclet.temporaryEffects.find(
650
+ effect => effect.effect.type === 'mechanicOverride' &&
651
+ (effect.effect as any).mechanic === mechanic
652
+ );
653
+ return override ? (override.effect as any).value : null;
654
+ }
655
+
656
+ private checkCriticalHitModification(attacker: BattlePiclet, target: BattlePiclet): 'always' | 'never' | 'normal' {
657
+ // Check attacker's critical hit modifiers
658
+ const attackerOverride = this.hasMechanicOverride(attacker, 'criticalHits');
659
+ if (attackerOverride === true) return 'always';
660
+
661
+ // Check target's critical hit immunity
662
+ const targetOverride = this.hasMechanicOverride(target, 'criticalHits');
663
+ if (targetOverride === false) return 'never';
664
+
665
+ return 'normal';
666
+ }
667
+
668
+ private checkStatusImmunity(target: BattlePiclet, status: string): boolean {
669
+ const immunity = this.hasMechanicOverride(target, 'statusImmunity');
670
+ if (Array.isArray(immunity)) {
671
+ return immunity.includes(status);
672
+ }
673
+ return false;
674
+ }
675
+
676
+ private checkTypeImmunity(target: BattlePiclet, attackType: string): boolean {
677
+ const immunity = this.hasMechanicOverride(target, 'typeImmunity');
678
+ if (Array.isArray(immunity)) {
679
+ return immunity.includes(attackType);
680
+ }
681
+ return false;
682
+ }
683
+
684
+ private checkFlagInteraction(target: BattlePiclet, flags: string[]): 'immune' | 'weak' | 'resist' | 'normal' {
685
+ // Check immunities first
686
+ const immunity = this.hasMechanicOverride(target, 'flagImmunity');
687
+ if (Array.isArray(immunity) && flags.some(flag => immunity.includes(flag))) {
688
+ return 'immune';
689
+ }
690
+
691
+ // Check weaknesses
692
+ const weakness = this.hasMechanicOverride(target, 'flagWeakness');
693
+ if (Array.isArray(weakness) && flags.some(flag => weakness.includes(flag))) {
694
+ return 'weak';
695
+ }
696
+
697
+ // Check resistances
698
+ const resistance = this.hasMechanicOverride(target, 'flagResistance');
699
+ if (Array.isArray(resistance) && flags.some(flag => resistance.includes(flag))) {
700
+ return 'resist';
701
+ }
702
+
703
+ return 'normal';
704
+ }
705
+
706
+ private getDamageMultiplier(piclet: BattlePiclet): number {
707
+ const multiplier = this.hasMechanicOverride(piclet, 'damageMultiplier');
708
+ return typeof multiplier === 'number' ? multiplier : 1.0;
709
+ }
710
+
711
+ private shouldInvertHealing(target: BattlePiclet): boolean {
712
+ return !!this.hasMechanicOverride(target, 'healingInversion');
713
+ }
714
  }
src/lib/battle-engine/advanced-mechanic-overrides.test.ts ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/extreme-risk-reward.test.ts ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(80); // Should be very high damage
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: "Tackle",
142
+ type: AttackType.NORMAL,
143
+ power: 40,
144
+ accuracy: 100,
145
+ pp: 10,
146
+ priority: 0,
147
+ flags: ['contact'],
148
+ effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
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
+
170
+ if (postGambitHp === 0) {
171
+ faintedCount++;
172
+ } else if (postGambitHp > preGambitHp) {
173
+ healedCount++;
174
+ }
175
+ }
176
+
177
+ // Should have some of each outcome (allowing for randomness)
178
+ expect(healedCount + faintedCount).toBeGreaterThan(0);
179
+ expect(healedCount).toBeGreaterThan(0);
180
+ expect(faintedCount).toBeGreaterThan(0);
181
+ });
182
+ });
183
+
184
+ describe('Sacrifice Moves', () => {
185
+ it('should provide powerful effects at great personal cost', () => {
186
+ const sacrificer: PicletDefinition = {
187
+ name: "Blood Warrior",
188
+ description: "Sacrifices HP for power",
189
+ tier: 'medium',
190
+ primaryType: PicletType.BEAST,
191
+ baseStats: { hp: 100, attack: 70, defense: 60, speed: 60 },
192
+ nature: "Brave",
193
+ specialAbility: { name: "No Ability", description: "" },
194
+ movepool: [
195
+ {
196
+ name: "Blood Pact",
197
+ type: AttackType.BEAST,
198
+ power: 0,
199
+ accuracy: 100,
200
+ pp: 3,
201
+ priority: 0,
202
+ flags: ['sacrifice'],
203
+ effects: [
204
+ {
205
+ type: 'damage',
206
+ target: 'self',
207
+ formula: 'percentage',
208
+ value: 50
209
+ },
210
+ {
211
+ type: 'mechanicOverride',
212
+ target: 'self',
213
+ mechanic: 'damageMultiplier',
214
+ value: 2.0,
215
+ condition: 'restOfBattle'
216
+ }
217
+ ]
218
+ },
219
+ {
220
+ name: "Strike",
221
+ type: AttackType.BEAST,
222
+ power: 60,
223
+ accuracy: 100,
224
+ pp: 10,
225
+ priority: 0,
226
+ flags: ['contact'],
227
+ effects: [
228
+ {
229
+ type: 'damage',
230
+ target: 'opponent',
231
+ amount: 'normal'
232
+ }
233
+ ]
234
+ }
235
+ ]
236
+ };
237
+
238
+ const opponent: PicletDefinition = {
239
+ name: "Opponent",
240
+ description: "Standard opponent",
241
+ tier: 'medium',
242
+ primaryType: PicletType.BEAST,
243
+ baseStats: { hp: 80, attack: 60, defense: 60, speed: 70 },
244
+ nature: "Hardy",
245
+ specialAbility: { name: "No Ability", description: "" },
246
+ movepool: [
247
+ {
248
+ name: "Tackle",
249
+ type: AttackType.NORMAL,
250
+ power: 40,
251
+ accuracy: 100,
252
+ pp: 10,
253
+ priority: 0,
254
+ flags: ['contact'],
255
+ effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
256
+ }
257
+ ]
258
+ };
259
+
260
+ const engine = new BattleEngine(sacrificer, opponent);
261
+ const initialHp = engine.getState().playerPiclet.currentHp;
262
+
263
+ // Use Blood Pact
264
+ engine.executeActions(
265
+ { type: 'move', piclet: 'player', moveIndex: 0 }, // Blood Pact
266
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
267
+ );
268
+
269
+ const hpAfterSacrifice = engine.getState().playerPiclet.currentHp;
270
+ expect(hpAfterSacrifice).toBeLessThan(initialHp);
271
+
272
+ // Now attack should do double damage
273
+ const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
274
+ engine.executeActions(
275
+ { type: 'move', piclet: 'player', moveIndex: 1 }, // Strike (should be doubled)
276
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
277
+ );
278
+
279
+ const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
280
+ const damage = initialOpponentHp - finalOpponentHp;
281
+
282
+ // Should do significantly more damage than normal (doubled)
283
+ expect(damage).toBeGreaterThan(60); // Normal would be ~30-40
284
+
285
+ const log = engine.getLog();
286
+ expect(log.some(msg => msg.includes('Blood Pact') || msg.includes('sacrifice'))).toBe(true);
287
+ });
288
+ });
289
+
290
+ describe('Conditional Power Scaling', () => {
291
+ it('should scale damage based on conditions', () => {
292
+ const revengeUser: PicletDefinition = {
293
+ name: "Revenge Fighter",
294
+ description: "Gets stronger when damaged",
295
+ tier: 'medium',
296
+ primaryType: PicletType.BEAST,
297
+ baseStats: { hp: 90, attack: 70, defense: 80, speed: 50 },
298
+ nature: "Brave",
299
+ specialAbility: { name: "No Ability", description: "" },
300
+ movepool: [
301
+ {
302
+ name: "Revenge Strike",
303
+ type: AttackType.BEAST,
304
+ power: 60,
305
+ accuracy: 100,
306
+ pp: 10,
307
+ priority: 0,
308
+ flags: ['contact'],
309
+ effects: [
310
+ {
311
+ type: 'damage',
312
+ target: 'opponent',
313
+ amount: 'normal'
314
+ },
315
+ {
316
+ type: 'damage',
317
+ target: 'opponent',
318
+ amount: 'strong',
319
+ condition: 'ifDamagedThisTurn'
320
+ }
321
+ ]
322
+ }
323
+ ]
324
+ };
325
+
326
+ const attacker: PicletDefinition = {
327
+ name: "Fast Attacker",
328
+ description: "Quick attacker",
329
+ tier: 'medium',
330
+ primaryType: PicletType.BEAST,
331
+ baseStats: { hp: 70, attack: 80, defense: 60, speed: 100 },
332
+ nature: "Hasty",
333
+ specialAbility: { name: "No Ability", description: "" },
334
+ movepool: [
335
+ {
336
+ name: "Quick Strike",
337
+ type: AttackType.BEAST,
338
+ power: 50,
339
+ accuracy: 100,
340
+ pp: 10,
341
+ priority: 1,
342
+ flags: ['contact', 'priority'],
343
+ effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
344
+ }
345
+ ]
346
+ };
347
+
348
+ const engine = new BattleEngine(revengeUser, attacker);
349
+
350
+ // Test revenge without being damaged first
351
+ const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
352
+ engine.executeActions(
353
+ { type: 'move', piclet: 'player', moveIndex: 0 },
354
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
355
+ );
356
+
357
+ const hpAfterNormalRevenge = engine.getState().opponentPiclet.currentHp;
358
+ const normalRevengeDamage = initialOpponentHp - hpAfterNormalRevenge;
359
+
360
+ // Test revenge when damaged this turn (opponent should go first due to priority)
361
+ const preRevengeHp = engine.getState().opponentPiclet.currentHp;
362
+ engine.executeActions(
363
+ { type: 'move', piclet: 'player', moveIndex: 0 },
364
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
365
+ );
366
+
367
+ const hpAfterPoweredRevenge = engine.getState().opponentPiclet.currentHp;
368
+ const poweredRevengeDamage = preRevengeHp - hpAfterPoweredRevenge;
369
+
370
+ // Powered revenge should do more damage
371
+ expect(poweredRevengeDamage).toBeGreaterThan(normalRevengeDamage);
372
+ expect(engine.getLog().some(msg => msg.includes('revenge') || msg.includes('retaliate'))).toBe(true);
373
+ });
374
+ });
375
+ });
src/lib/battle-engine/missing-features.test.ts ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: -5,
303
+ flags: ['lowPriority'],
304
+ effects: [
305
+ {
306
+ type: 'counter',
307
+ counterType: 'physical',
308
+ strength: 'strong'
309
+ }
310
+ ]
311
+ }
312
+ ]
313
+ };
314
+
315
+ const opponent: PicletDefinition = {
316
+ name: "Physical Attacker",
317
+ description: "Uses physical moves",
318
+ tier: 'medium',
319
+ primaryType: PicletType.BEAST,
320
+ baseStats: { hp: 80, attack: 80, defense: 60, speed: 70 },
321
+ nature: "Adamant",
322
+ specialAbility: { name: "No Ability", description: "" },
323
+ movepool: [
324
+ {
325
+ name: "Physical Strike",
326
+ type: AttackType.BEAST,
327
+ power: 80,
328
+ accuracy: 100,
329
+ pp: 10,
330
+ priority: 0,
331
+ flags: ['contact'],
332
+ effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
333
+ }
334
+ ]
335
+ };
336
+
337
+ const engine = new BattleEngine(counterUser, opponent);
338
+ const initialOpponentHp = engine.getState().opponentPiclet.currentHp;
339
+
340
+ engine.executeActions(
341
+ { type: 'move', piclet: 'player', moveIndex: 0 },
342
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
343
+ );
344
+
345
+ const finalOpponentHp = engine.getState().opponentPiclet.currentHp;
346
+ expect(finalOpponentHp).toBeLessThan(initialOpponentHp);
347
+ expect(engine.getLog().some(msg => msg.includes('countered') || msg.includes('Counter'))).toBe(true);
348
+ });
349
+ });
350
+
351
+ describe('priority Effects', () => {
352
+ it('should modify move priority conditionally', () => {
353
+ const priorityUser: PicletDefinition = {
354
+ name: "Priority User",
355
+ description: "Uses priority moves based on conditions",
356
+ tier: 'medium',
357
+ primaryType: PicletType.SPACE,
358
+ baseStats: { hp: 60, attack: 70, defense: 50, speed: 40 },
359
+ nature: "Quiet",
360
+ specialAbility: { name: "No Ability", description: "" },
361
+ movepool: [
362
+ {
363
+ name: "Desperation Strike",
364
+ type: AttackType.SPACE,
365
+ power: 60,
366
+ accuracy: 100,
367
+ pp: 10,
368
+ priority: 0,
369
+ flags: [],
370
+ effects: [
371
+ {
372
+ type: 'damage',
373
+ target: 'opponent',
374
+ amount: 'normal'
375
+ },
376
+ {
377
+ type: 'priority',
378
+ target: 'self',
379
+ value: 1,
380
+ condition: 'ifLowHp'
381
+ }
382
+ ]
383
+ }
384
+ ]
385
+ };
386
+
387
+ const fastOpponent: PicletDefinition = {
388
+ name: "Fast Opponent",
389
+ description: "Very fast opponent",
390
+ tier: 'medium',
391
+ primaryType: PicletType.BEAST,
392
+ baseStats: { hp: 80, attack: 60, defense: 60, speed: 100 },
393
+ nature: "Timid",
394
+ specialAbility: { name: "No Ability", description: "" },
395
+ movepool: [
396
+ {
397
+ name: "Quick Attack",
398
+ type: AttackType.NORMAL,
399
+ power: 40,
400
+ accuracy: 100,
401
+ pp: 10,
402
+ priority: 0,
403
+ flags: [],
404
+ effects: [{ type: 'damage', target: 'opponent', amount: 'normal' }]
405
+ }
406
+ ]
407
+ };
408
+
409
+ const engine = new BattleEngine(priorityUser, fastOpponent);
410
+
411
+ // Damage the priority user to trigger low HP condition
412
+ engine['state'].playerPiclet.currentHp = Math.floor(engine['state'].playerPiclet.maxHp * 0.2);
413
+
414
+ engine.executeActions(
415
+ { type: 'move', piclet: 'player', moveIndex: 0 },
416
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
417
+ );
418
+
419
+ const log = engine.getLog();
420
+ const playerMoveIndex = log.findIndex(msg => msg.includes('Priority User used Desperation Strike'));
421
+ const opponentMoveIndex = log.findIndex(msg => msg.includes('Fast Opponent used Quick Attack'));
422
+
423
+ // When at low HP, priority user should go first despite lower speed
424
+ expect(playerMoveIndex).toBeLessThan(opponentMoveIndex);
425
+ });
426
+ });
427
+
428
+ describe('removeStatus Effects', () => {
429
+ it('should remove status effects from target', () => {
430
+ const statusClearer: PicletDefinition = {
431
+ name: "Status Clearer",
432
+ description: "Removes status effects",
433
+ tier: 'medium',
434
+ primaryType: PicletType.FLORA,
435
+ baseStats: { hp: 90, attack: 50, defense: 70, speed: 60 },
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 opponent: PicletDefinition = {
459
+ name: "Poisoner",
460
+ description: "Inflicts poison",
461
+ tier: 'medium',
462
+ primaryType: PicletType.BUG,
463
+ baseStats: { hp: 70, attack: 60, defense: 60, speed: 70 },
464
+ nature: "Modest",
465
+ specialAbility: { name: "No Ability", description: "" },
466
+ movepool: [
467
+ {
468
+ name: "Poison Sting",
469
+ type: AttackType.BUG,
470
+ power: 30,
471
+ accuracy: 100,
472
+ pp: 10,
473
+ priority: 0,
474
+ flags: [],
475
+ effects: [
476
+ {
477
+ type: 'damage',
478
+ target: 'opponent',
479
+ amount: 'weak'
480
+ },
481
+ {
482
+ type: 'applyStatus',
483
+ target: 'opponent',
484
+ status: 'poison'
485
+ }
486
+ ]
487
+ }
488
+ ]
489
+ };
490
+
491
+ const engine = new BattleEngine(statusClearer, opponent);
492
+
493
+ // First, get poisoned
494
+ engine.executeActions(
495
+ { type: 'move', piclet: 'player', moveIndex: 0 },
496
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
497
+ );
498
+
499
+ // Check if poisoned
500
+ expect(engine.getLog().some(msg => msg.includes('poisoned') || msg.includes('poison'))).toBe(true);
501
+
502
+ // Then cleanse the poison
503
+ engine.executeActions(
504
+ { type: 'move', piclet: 'player', moveIndex: 0 },
505
+ { type: 'move', piclet: 'opponent', moveIndex: 0 }
506
+ );
507
+
508
+ expect(engine.getLog().some(msg => msg.includes('cured') || msg.includes('cleansed'))).toBe(true);
509
+ });
510
+ });
511
+ });
src/lib/battle-engine/move-flags.test.ts ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 70, 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)
302
+ engine.executeActions(
303
+ { type: 'move', piclet: 'player', moveIndex: 0 },
304
+ { type: 'move', piclet: 'opponent', moveIndex: 1 } // Punch move
305
+ );
306
+
307
+ const hpAfterPunch = engine.getState().playerPiclet.currentHp;
308
+ expect(hpAfterPunch).toBeLessThan(hpAfterExplosive);
309
+ // Should take more damage than normal due to weakness
310
+ });
311
+ });
312
+
313
+ describe('Flag-Based Weaknesses', () => {
314
+ it('should take extra damage from specific flagged moves', () => {
315
+ const fragileShell: SpecialAbility = {
316
+ name: "Fragile Shell",
317
+ description: "Hard shell provides defense but shatters from explosions",
318
+ effects: [
319
+ {
320
+ type: 'modifyStats',
321
+ target: 'self',
322
+ stats: { defense: 'increase' }
323
+ },
324
+ {
325
+ type: 'mechanicOverride',
326
+ mechanic: 'flagWeakness',
327
+ value: ['explosive']
328
+ }
329
+ ]
330
+ };
331
+
332
+ const shelledCreature: PicletDefinition = {
333
+ name: "Shell Fighter",
334
+ description: "Protected by fragile shell",
335
+ tier: 'medium',
336
+ primaryType: PicletType.MINERAL,
337
+ baseStats: { hp: 80, attack: 60, defense: 90, speed: 50 },
338
+ nature: "Impish",
339
+ specialAbility: fragileShell,
340
+ movepool: [
341
+ {
342
+ name: "Rock Throw",
343
+ type: AttackType.MINERAL,
344
+ power: 50,
345
+ accuracy: 90,
346
+ pp: 10,
347
+ priority: 0,
348
+ flags: [],
349
+ effects: [
350
+ {
351
+ type: 'damage',
352
+ target: 'opponent',
353
+ amount: 'normal'
354
+ }
355
+ ]
356
+ }
357
+ ]
358
+ };
359
+
360
+ const explosiveUser: PicletDefinition = {
361
+ name: "Bomber",
362
+ description: "Uses explosive attacks",
363
+ tier: 'medium',
364
+ primaryType: PicletType.MACHINA,
365
+ baseStats: { hp: 70, attack: 80, defense: 60, speed: 70 },
366
+ nature: "Modest",
367
+ specialAbility: { name: "No Ability", description: "" },
368
+ movepool: [
369
+ {
370
+ name: "Normal Attack",
371
+ type: AttackType.NORMAL,
372
+ power: 60,
373
+ accuracy: 100,
374
+ pp: 10,
375
+ priority: 0,
376
+ flags: [],
377
+ effects: [
378
+ {
379
+ type: 'damage',
380
+ target: 'opponent',
381
+ amount: 'normal'
382
+ }
383
+ ]
384
+ },
385
+ {
386
+ name: "Bomb Blast",
387
+ type: AttackType.MACHINA,
388
+ power: 60,
389
+ accuracy: 100,
390
+ pp: 10,
391
+ priority: 0,
392
+ flags: ['explosive'],
393
+ effects: [
394
+ {
395
+ type: 'damage',
396
+ target: 'opponent',
397
+ amount: 'normal'
398
+ }
399
+ ]
400
+ }
401
+ ]
402
+ };
403
+
404
+ const engine = new BattleEngine(shelledCreature, explosiveUser);
405
+
406
+ // Test normal damage
407
+ const initialHp = engine.getState().playerPiclet.currentHp;
408
+ engine.executeActions(
409
+ { type: 'move', piclet: 'player', moveIndex: 0 },
410
+ { type: 'move', piclet: 'opponent', moveIndex: 0 } // Normal attack
411
+ );
412
+
413
+ const hpAfterNormal = engine.getState().playerPiclet.currentHp;
414
+ const normalDamage = initialHp - hpAfterNormal;
415
+
416
+ // Test explosive weakness (should do more damage)
417
+ const preExplosiveHp = engine.getState().playerPiclet.currentHp;
418
+ engine.executeActions(
419
+ { type: 'move', piclet: 'player', moveIndex: 0 },
420
+ { type: 'move', piclet: 'opponent', moveIndex: 1 } // Explosive attack
421
+ );
422
+
423
+ const hpAfterExplosive = engine.getState().playerPiclet.currentHp;
424
+ const explosiveDamage = preExplosiveHp - hpAfterExplosive;
425
+
426
+ // Explosive should do more damage due to weakness
427
+ expect(explosiveDamage).toBeGreaterThan(normalDamage);
428
+ expect(engine.getLog().some(msg => msg.includes('It\'s super effective') || msg.includes('weakness'))).toBe(true);
429
+ });
430
+ });
431
+
432
+ describe('Flag-Based Resistances', () => {
433
+ it('should take reduced damage from specific flagged moves', () => {
434
+ const thickHide: SpecialAbility = {
435
+ name: "Thick Hide",
436
+ description: "Tough skin reduces impact from physical contact",
437
+ effects: [
438
+ {
439
+ type: 'mechanicOverride',
440
+ mechanic: 'flagResistance',
441
+ value: ['contact']
442
+ }
443
+ ]
444
+ };
445
+
446
+ const toughCreature: PicletDefinition = {
447
+ name: "Tough Fighter",
448
+ description: "Has thick, resistant hide",
449
+ tier: 'medium',
450
+ primaryType: PicletType.BEAST,
451
+ baseStats: { hp: 90, attack: 70, defense: 90, speed: 40 },
452
+ nature: "Impish",
453
+ specialAbility: thickHide,
454
+ movepool: [
455
+ {
456
+ name: "Bite",
457
+ type: AttackType.BEAST,
458
+ power: 60,
459
+ accuracy: 100,
460
+ pp: 10,
461
+ priority: 0,
462
+ flags: ['contact', 'bite'],
463
+ effects: [
464
+ {
465
+ type: 'damage',
466
+ target: 'opponent',
467
+ amount: 'normal'
468
+ }
469
+ ]
470
+ }
471
+ ]
472
+ };
473
+
474
+ const attacker: PicletDefinition = {
475
+ name: "Mixed Attacker",
476
+ description: "Uses various attack types",
477
+ tier: 'medium',
478
+ primaryType: PicletType.BEAST,
479
+ baseStats: { hp: 75, attack: 85, defense: 60, speed: 70 },
480
+ nature: "Adamant",
481
+ specialAbility: { name: "No Ability", description: "" },
482
+ movepool: [
483
+ {
484
+ name: "Scratch",
485
+ type: AttackType.BEAST,
486
+ power: 60,
487
+ accuracy: 100,
488
+ pp: 10,
489
+ priority: 0,
490
+ flags: ['contact'],
491
+ effects: [
492
+ {
493
+ type: 'damage',
494
+ target: 'opponent',
495
+ amount: 'normal'
496
+ }
497
+ ]
498
+ },
499
+ {
500
+ name: "Energy Beam",
501
+ type: AttackType.SPACE,
502
+ power: 60,
503
+ accuracy: 100,
504
+ pp: 10,
505
+ priority: 0,
506
+ flags: [], // No contact
507
+ effects: [
508
+ {
509
+ type: 'damage',
510
+ target: 'opponent',
511
+ amount: 'normal'
512
+ }
513
+ ]
514
+ }
515
+ ]
516
+ };
517
+
518
+ const engine = new BattleEngine(toughCreature, attacker);
519
+
520
+ // Test contact move (should be resisted)
521
+ const initialHp = engine.getState().playerPiclet.currentHp;
522
+ engine.executeActions(
523
+ { type: 'move', piclet: 'player', moveIndex: 0 },
524
+ { type: 'move', piclet: 'opponent', moveIndex: 0 } // Contact move
525
+ );
526
+
527
+ const hpAfterContact = engine.getState().playerPiclet.currentHp;
528
+ const contactDamage = initialHp - hpAfterContact;
529
+
530
+ // Test non-contact move (normal damage)
531
+ const preBeamHp = engine.getState().playerPiclet.currentHp;
532
+ engine.executeActions(
533
+ { type: 'move', piclet: 'player', moveIndex: 0 },
534
+ { type: 'move', piclet: 'opponent', moveIndex: 1 } // Non-contact move
535
+ );
536
+
537
+ const hpAfterBeam = engine.getState().playerPiclet.currentHp;
538
+ const beamDamage = preBeamHp - hpAfterBeam;
539
+
540
+ // Contact damage should be less due to resistance
541
+ expect(contactDamage).toBeLessThan(beamDamage);
542
+ expect(engine.getLog().some(msg => msg.includes('not very effective') || msg.includes('resisted'))).toBe(true);
543
+ });
544
+ });
545
+
546
+ describe('Multi-Flag Interactions', () => {
547
+ it('should handle creatures with multiple flag interactions', () => {
548
+ const liquidBody: SpecialAbility = {
549
+ name: "Liquid Body",
550
+ description: "Fluid form flows around physical attacks but resonates with sound",
551
+ effects: [
552
+ {
553
+ type: 'mechanicOverride',
554
+ mechanic: 'flagImmunity',
555
+ value: ['punch', 'bite']
556
+ },
557
+ {
558
+ type: 'mechanicOverride',
559
+ mechanic: 'flagWeakness',
560
+ value: ['sound']
561
+ }
562
+ ]
563
+ };
564
+
565
+ const liquidCreature: PicletDefinition = {
566
+ name: "Liquid Fighter",
567
+ description: "Made of flowing liquid",
568
+ tier: 'medium',
569
+ primaryType: PicletType.AQUATIC,
570
+ baseStats: { hp: 85, attack: 70, defense: 60, speed: 75 },
571
+ nature: "Calm",
572
+ specialAbility: liquidBody,
573
+ movepool: [
574
+ {
575
+ name: "Water Pulse",
576
+ type: AttackType.AQUATIC,
577
+ power: 60,
578
+ accuracy: 100,
579
+ pp: 10,
580
+ priority: 0,
581
+ flags: [],
582
+ effects: [
583
+ {
584
+ type: 'damage',
585
+ target: 'opponent',
586
+ amount: 'normal'
587
+ }
588
+ ]
589
+ }
590
+ ]
591
+ };
592
+
593
+ const multiAttacker: PicletDefinition = {
594
+ name: "Multi Attacker",
595
+ description: "Uses different types of attacks",
596
+ tier: 'medium',
597
+ primaryType: PicletType.BEAST,
598
+ baseStats: { hp: 75, attack: 80, defense: 65, speed: 70 },
599
+ nature: "Hardy",
600
+ specialAbility: { name: "No Ability", description: "" },
601
+ movepool: [
602
+ {
603
+ name: "Punch",
604
+ type: AttackType.BEAST,
605
+ power: 70,
606
+ accuracy: 100,
607
+ pp: 10,
608
+ priority: 0,
609
+ flags: ['contact', 'punch'],
610
+ effects: [
611
+ {
612
+ type: 'damage',
613
+ target: 'opponent',
614
+ amount: 'normal'
615
+ }
616
+ ]
617
+ },
618
+ {
619
+ name: "Bite",
620
+ type: AttackType.BEAST,
621
+ power: 70,
622
+ accuracy: 100,
623
+ pp: 10,
624
+ priority: 0,
625
+ flags: ['contact', 'bite'],
626
+ effects: [
627
+ {
628
+ type: 'damage',
629
+ target: 'opponent',
630
+ amount: 'normal'
631
+ }
632
+ ]
633
+ },
634
+ {
635
+ name: "Sonic Roar",
636
+ type: AttackType.CULTURE,
637
+ power: 70,
638
+ accuracy: 100,
639
+ pp: 10,
640
+ priority: 0,
641
+ flags: ['sound'],
642
+ effects: [
643
+ {
644
+ type: 'damage',
645
+ target: 'opponent',
646
+ amount: 'normal'
647
+ }
648
+ ]
649
+ }
650
+ ]
651
+ };
652
+
653
+ const engine = new BattleEngine(liquidCreature, multiAttacker);
654
+ const initialHp = engine.getState().playerPiclet.currentHp;
655
+
656
+ // Test punch immunity
657
+ engine.executeActions(
658
+ { type: 'move', piclet: 'player', moveIndex: 0 },
659
+ { type: 'move', piclet: 'opponent', moveIndex: 0 } // Punch (should be immune)
660
+ );
661
+
662
+ const hpAfterPunch = engine.getState().playerPiclet.currentHp;
663
+ expect(hpAfterPunch).toBe(initialHp);
664
+
665
+ // Test bite immunity
666
+ engine.executeActions(
667
+ { type: 'move', piclet: 'player', moveIndex: 0 },
668
+ { type: 'move', piclet: 'opponent', moveIndex: 1 } // Bite (should be immune)
669
+ );
670
+
671
+ const hpAfterBite = engine.getState().playerPiclet.currentHp;
672
+ expect(hpAfterBite).toBe(hpAfterPunch);
673
+
674
+ // Test sound weakness
675
+ engine.executeActions(
676
+ { type: 'move', piclet: 'player', moveIndex: 0 },
677
+ { type: 'move', piclet: 'opponent', moveIndex: 2 } // Sound (should be weak)
678
+ );
679
+
680
+ const hpAfterSound = engine.getState().playerPiclet.currentHp;
681
+ expect(hpAfterSound).toBeLessThan(hpAfterBite);
682
+
683
+ const log = engine.getLog();
684
+ expect(log.some(msg => msg.includes('had no effect'))).toBe(true);
685
+ expect(log.some(msg => msg.includes('super effective') || msg.includes('weakness'))).toBe(true);
686
+ });
687
+ });
688
+ });