@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.d97c6e03

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,28 +62,26 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
62
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
63
63
  if (battle.turn == 0) return start(battle, log);
64
64
 
65
- var s1 = false;
66
- var s2 = false;
67
65
  var f1: ?Move = null;
68
66
  var f2: ?Move = null;
69
67
 
70
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
71
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
-
73
- var p1 = battle.side(.P1);
74
- var p2 = battle.side(.P2);
75
-
76
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
68
+ if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
69
+ if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
78
70
 
79
71
  if (turnOrder(battle, c1, c2) == .P1) {
80
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
72
+ if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
81
73
  } else {
82
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
74
+ if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
83
75
  }
84
76
 
85
- if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
- if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
77
+ var p1 = battle.side(.P1);
78
+ if (p1.active.volatiles.attacks == 0) {
79
+ p1.active.volatiles.Binding = false;
80
+ }
81
+ var p2 = battle.side(.P2);
82
+ if (p2.active.volatiles.attacks == 0) {
83
+ p2.active.volatiles.Binding = false;
84
+ }
87
85
 
88
86
  return endTurn(battle, log);
89
87
  }
@@ -116,7 +114,6 @@ fn selectMove(
116
114
  player: Player,
117
115
  choice: Choice,
118
116
  foe_choice: Choice,
119
- skip_turn: *bool,
120
117
  from: *?Move,
121
118
  ) ?Result {
122
119
  if (choice.type == .Pass) return null;
@@ -129,26 +126,17 @@ fn selectMove(
129
126
  (choice.type == .Move and choice.data == @boolToInt(showdown)));
130
127
 
131
128
  // pre-battle menu
132
- if (volatiles.Recharging) {
133
- if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
134
- return null;
135
- }
129
+ if (volatiles.Recharging) return null;
136
130
  if (volatiles.Rage) {
137
131
  from.* = side.last_used_move;
138
- if (showdown) {
139
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
140
- saveMove(battle, player, null);
141
- }
132
+ if (showdown) saveMove(battle, player, null);
142
133
  return null;
143
134
  }
144
135
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
145
136
  if (!showdown) volatiles.Flinch = false;
146
137
  if (volatiles.Thrashing or volatiles.Charging) {
147
138
  from.* = side.last_selected_move;
148
- if (showdown) {
149
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
150
- saveMove(battle, player, null);
151
- }
139
+ if (showdown) saveMove(battle, player, null);
152
140
  return null;
153
141
  }
154
142
 
@@ -158,10 +146,7 @@ fn selectMove(
158
146
  // pre-move select
159
147
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
160
148
  assert(showdown or choice.data == 0);
161
- if (showdown) {
162
- if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
163
- saveMove(battle, player, choice);
164
- }
149
+ if (showdown) saveMove(battle, player, choice);
165
150
  return null;
166
151
  }
167
152
  if (volatiles.Binding) {
@@ -180,9 +165,9 @@ fn selectMove(
180
165
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
181
166
  if (foe_choice.type == .Switch) {
182
167
  const slot = if (player == .P1)
183
- battle.last_moves.p1_index
168
+ battle.last_selected_indexes.p1
184
169
  else
185
- battle.last_moves.p2_index;
170
+ battle.last_selected_indexes.p2;
186
171
  const last = side.active.move(@intCast(u4, slot));
187
172
  if (last.id == .Metronome) side.last_selected_move = last.id;
188
173
  if (last.id == .MirrorMove) return Result.Error;
@@ -192,7 +177,6 @@ fn selectMove(
192
177
  }
193
178
 
194
179
  if (battle.foe(player).active.volatiles.Binding) {
195
- skip_turn.* = true;
196
180
  if (showdown) {
197
181
  saveMove(battle, player, choice);
198
182
  } else {
@@ -237,9 +221,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
237
221
 
238
222
  side.last_selected_move = move.id;
239
223
  if (player == .P1) {
240
- battle.last_moves.p1_index = @intCast(u3, c.data);
224
+ battle.last_selected_indexes.p1 = @intCast(u4, c.data);
241
225
  } else {
242
- battle.last_moves.p2_index = @intCast(u3, c.data);
226
+ battle.last_selected_indexes.p2 = @intCast(u4, c.data);
243
227
  }
244
228
  }
245
229
  }
@@ -259,9 +243,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
259
243
  side.order[slot - 1] = out;
260
244
 
261
245
  if (player == .P1) {
262
- battle.last_moves.p1_index = 1;
246
+ battle.last_selected_indexes.p1 = 1;
263
247
  } else {
264
- battle.last_moves.p2_index = 1;
248
+ battle.last_selected_indexes.p2 = 1;
265
249
  }
266
250
 
267
251
  side.last_used_move = .None;
@@ -318,9 +302,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
318
302
  const spe2 = battle.side(.P2).active.stats.spe;
319
303
  if (spe1 == spe2) {
320
304
  // Pokémon Showdown's beforeTurnCallback shenanigans
321
- if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
322
- battle.rng.advance(1);
323
- }
305
+ if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
324
306
 
325
307
  const p1 = Rolls.speedTie(battle);
326
308
  if (!showdown) return if (p1) .P1 else .P2;
@@ -345,13 +327,9 @@ fn doTurn(
345
327
  battle: anytype,
346
328
  player: Player,
347
329
  player_choice: Choice,
348
- player_rewrap: bool,
349
- player_skip: bool,
350
330
  player_from: ?Move,
351
331
  foe_player: Player,
352
332
  foe_choice: Choice,
353
- foe_rewrap: bool,
354
- foe_skip: bool,
355
333
  foe_from: ?Move,
356
334
  log: anytype,
357
335
  ) !?Result {
@@ -359,16 +337,7 @@ fn doTurn(
359
337
 
360
338
  var residual = true;
361
339
  var replace = battle.side(player).stored().hp == 0;
362
- if (try executeMove(
363
- battle,
364
- player,
365
- player_choice,
366
- player_rewrap,
367
- player_skip,
368
- player_from,
369
- &residual,
370
- log,
371
- )) |r| return r;
340
+ if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
372
341
  if (!replace) {
373
342
  if (player_choice.type != .Switch) {
374
343
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -379,16 +348,7 @@ fn doTurn(
379
348
 
380
349
  residual = true;
381
350
  replace = battle.side(foe_player).stored().hp == 0;
382
- if (try executeMove(
383
- battle,
384
- foe_player,
385
- foe_choice,
386
- foe_rewrap,
387
- foe_skip,
388
- foe_from,
389
- &residual,
390
- log,
391
- )) |r| return r;
351
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
392
352
  if (!replace) {
393
353
  if (foe_choice.type != .Switch) {
394
354
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -411,8 +371,6 @@ fn executeMove(
411
371
  battle: anytype,
412
372
  player: Player,
413
373
  choice: Choice,
414
- rewrap: bool,
415
- skip: bool,
416
374
  from: ?Move,
417
375
  residual: *bool,
418
376
  log: anytype,
@@ -424,10 +382,9 @@ fn executeMove(
424
382
  return null;
425
383
  }
426
384
 
427
- // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
428
- // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
429
- // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
430
385
  if (side.last_selected_move == .SKIP_TURN) {
386
+ // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
387
+ // Binding instead gets handled in beforeMove after sleep and freeze
431
388
  assert(!showdown);
432
389
  if (battle.foe(player).active.volatiles.Binding) {
433
390
  try log.cant(battle.active(player), .Bound);
@@ -446,9 +403,9 @@ fn executeMove(
446
403
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
447
404
  assert(!showdown);
448
405
  mslot = @intCast(u4, if (player == .P1)
449
- battle.last_moves.p1_index
406
+ battle.last_selected_indexes.p1
450
407
  else
451
- battle.last_moves.p2_index);
408
+ battle.last_selected_indexes.p2);
452
409
  const stored = side.stored();
453
410
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
454
411
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -470,14 +427,14 @@ fn executeMove(
470
427
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
471
428
  assert(mslot == 1);
472
429
  mslot = @intCast(u4, if (player == .P1)
473
- battle.last_moves.p1_index
430
+ battle.last_selected_indexes.p1
474
431
  else
475
- battle.last_moves.p2_index);
432
+ battle.last_selected_indexes.p2);
476
433
  }
477
434
 
478
435
  var skip_can = false;
479
436
  var skip_pp = false;
480
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
437
+ switch (try beforeMove(battle, player, from, residual, log)) {
481
438
  .done => return null,
482
439
  .skip_can => skip_can = true,
483
440
  .skip_pp => skip_pp = true,
@@ -489,7 +446,7 @@ fn executeMove(
489
446
  return null;
490
447
  }
491
448
 
492
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
449
+ return doMove(battle, player, mslot, auto, residual, log);
493
450
  }
494
451
 
495
452
  const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
@@ -497,7 +454,6 @@ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
497
454
  fn beforeMove(
498
455
  battle: anytype,
499
456
  player: Player,
500
- skip: bool,
501
457
  from: ?Move,
502
458
  residual: *bool,
503
459
  log: anytype,
@@ -529,15 +485,13 @@ fn beforeMove(
529
485
  return .done;
530
486
  }
531
487
 
532
- if (skip or foe.active.volatiles.Binding) {
488
+ if (foe.active.volatiles.Binding) {
533
489
  try log.cant(ident, .Bound);
534
490
  return .done;
535
491
  }
536
492
 
537
- if (volatiles.Flinch) {
538
- // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
539
- // Pokémon can sometimes flinch multiple times from the same original hit
540
- if (!showdown) volatiles.Flinch = false;
493
+ if (!showdown and volatiles.Flinch) {
494
+ volatiles.Flinch = false;
541
495
  try log.cant(ident, .Flinch);
542
496
  return .done;
543
497
  }
@@ -558,6 +512,14 @@ fn beforeMove(
558
512
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
559
513
  if (showdown and try disabled(side, ident, log)) return .done;
560
514
 
515
+ // Pokémon Showdown checks for Flinch *after* instead of before... and doesn't clear Flinch
516
+ // until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
517
+ // from the same original hit
518
+ if (showdown and volatiles.Flinch) {
519
+ try log.cant(ident, .Flinch);
520
+ return .done;
521
+ }
522
+
561
523
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
562
524
  // before the side had a selected a move - we simply need to assume this leads to a desync
563
525
  if (side.last_selected_move == .None) {
@@ -629,14 +591,7 @@ fn beforeMove(
629
591
  if (volatiles.Bide) {
630
592
  assert(!volatiles.Thrashing and !volatiles.Rage);
631
593
 
632
- if (showdown) {
633
- // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
634
- // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
635
- // addition because 65535 is sufficient to faint any Pokémon anyway
636
- volatiles.state +|= battle.last_damage;
637
- } else {
638
- volatiles.state +%= battle.last_damage;
639
- }
594
+ volatiles.state +%= battle.last_damage;
640
595
 
641
596
  assert(volatiles.attacks > 0);
642
597
 
@@ -680,13 +635,8 @@ fn beforeMove(
680
635
  try log.start(battle.active(player), .ConfusionSilent);
681
636
  }
682
637
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
683
- if (showdown) {
684
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
685
- // used in useMove and doesn't have the notion of skipping canMove semantics
686
- side.last_used_move = side.last_selected_move;
687
- if (handleThrashing(battle, active)) {
688
- try log.start(battle.active(player), .ConfusionSilent);
689
- }
638
+ if (showdown and handleThrashing(battle, active)) {
639
+ try log.start(battle.active(player), .ConfusionSilent);
690
640
  }
691
641
  return .skip_can;
692
642
  }
@@ -701,11 +651,7 @@ fn beforeMove(
701
651
  side.last_selected_move,
702
652
  );
703
653
  if (showdown or battle.last_damage != 0) {
704
- const sub = showdown and foe.active.volatiles.Substitute;
705
654
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
706
- if (battle.foe(player).stored().hp > 0 and !sub) {
707
- try buildRage(battle, player.foe(), log);
708
- }
709
655
  }
710
656
  return .done;
711
657
  }
@@ -737,7 +683,6 @@ fn canMove(
737
683
  side.active.volatiles.Invulnerable = false;
738
684
  } else if (move.effect == .Charge) {
739
685
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
- setCounterable(battle, player, side, move);
741
686
  try Effects.charge(battle, player, log);
742
687
  return false;
743
688
  }
@@ -754,7 +699,6 @@ fn canMove(
754
699
  // regrettable, but this information is more "nice to have" than required
755
700
  const f = if (from != null and from.? == .None) null else from;
756
701
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
- setCounterable(battle, player, side, move);
758
702
 
759
703
  if (move.effect.onBegin()) {
760
704
  try onBegin(battle, player, move, mslot, residual, log);
@@ -765,27 +709,12 @@ fn canMove(
765
709
  Effects.thrashing(battle, player);
766
710
  } else if (!showdown and move.effect == .Binding) {
767
711
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
768
- Effects.binding(battle, player, false);
712
+ Effects.binding(battle, player);
769
713
  }
770
714
 
771
715
  return true;
772
716
  }
773
717
 
774
- fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
775
- // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
776
- // in cases where an early return happens the data for a players last selected move does not get
777
- // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
778
- // This boolean state we track here doesn't exist on the cartridge because it instead manifests
779
- // as the desync results from actually having two separate battle states that subtly disagree
780
- const counterable = side.last_selected_move != .Counter and move.bp > 0 and
781
- (move.type == .Normal or move.type == .Fighting);
782
- if (player == .P1) {
783
- battle.last_moves.p1_counterable = @boolToInt(counterable);
784
- } else {
785
- battle.last_moves.p2_counterable = @boolToInt(counterable);
786
- }
787
- }
788
-
789
718
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
790
719
  if (side.last_selected_move == .Struggle) return;
791
720
 
@@ -821,7 +750,6 @@ fn doMove(
821
750
  battle: anytype,
822
751
  player: Player,
823
752
  mslot: u4,
824
- rewrap: bool,
825
753
  auto: bool,
826
754
  residual: *bool,
827
755
  log: anytype,
@@ -913,9 +841,9 @@ fn doMove(
913
841
 
914
842
  if (!showdown or !miss) {
915
843
  if (move.effect == .MirrorMove) {
916
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
844
+ return mirrorMove(battle, player, mslot, auto, residual, log);
917
845
  } else if (move.effect == .Metronome) {
918
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
846
+ return metronome(battle, player, mslot, auto, residual, log);
919
847
  } else if (move.effect.onEnd()) {
920
848
  try onEnd(battle, player, move, log);
921
849
  return null;
@@ -930,7 +858,7 @@ fn doMove(
930
858
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
859
  if (ohko) {
932
860
  try log.immune(foe_ident, .OHKO);
933
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
861
+ } else if (immune and !invulnerable) {
934
862
  try log.immune(foe_ident, .None);
935
863
  } else if (mist) {
936
864
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -996,17 +924,11 @@ fn doMove(
996
924
  } else if (showdown) {
997
925
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
998
926
  if (move.effect == .Binding) {
999
- Effects.binding(battle, player, rewrap);
927
+ Effects.binding(battle, player);
1000
928
  if (immune) {
1001
929
  battle.last_damage = 0;
1002
- assert(foe.stored().hp > 0);
1003
930
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1004
- if (sub) {
1005
- try log.activate(battle.active(player.foe()), .Substitute);
1006
- } else {
1007
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
- try buildRage(battle, player.foe(), log);
1009
- }
931
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1010
932
  return null;
1011
933
  }
1012
934
  }
@@ -1203,17 +1125,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1203
1125
  return null;
1204
1126
  }
1205
1127
 
1206
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1128
+ // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1129
+ const foe_last_used_move =
1130
+ Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1207
1131
  const foe_last_selected_move =
1208
1132
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1209
- .Splash
1133
+ .Counter
1210
1134
  else
1211
1135
  foe.last_selected_move);
1212
1136
 
1213
- const used = (if (player.foe() == .P1)
1214
- battle.last_moves.p1_counterable
1215
- else
1216
- battle.last_moves.p2_counterable) != 0;
1137
+ const used = foe_last_used_move.bp > 0 and
1138
+ foe.last_used_move != .Counter and
1139
+ (foe_last_used_move.type == .Normal or
1140
+ foe_last_used_move.type == .Fighting);
1217
1141
 
1218
1142
  const selected = foe_last_selected_move.bp > 0 and
1219
1143
  foe.last_selected_move != .Counter and
@@ -1232,14 +1156,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1232
1156
  return null;
1233
1157
  }
1234
1158
 
1235
- battle.last_damage *|= 2;
1159
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1236
1160
 
1237
1161
  // Pokémon Showdown calls checkHit before Counter
1238
1162
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1239
1163
 
1240
- const sub = showdown and foe.active.volatiles.Substitute;
1241
1164
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1242
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1243
1165
  return null;
1244
1166
  }
1245
1167
 
@@ -1283,7 +1205,6 @@ fn mirrorMove(
1283
1205
  battle: anytype,
1284
1206
  player: Player,
1285
1207
  mslot: u4,
1286
- rewrap: bool,
1287
1208
  auto: bool,
1288
1209
  residual: *bool,
1289
1210
  log: anytype,
@@ -1296,19 +1217,18 @@ fn mirrorMove(
1296
1217
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1297
1218
  try log.fail(battle.active(player), .None);
1298
1219
  return null;
1299
- } else if (!showdown or foe.last_used_move != .Struggle) {
1300
- incrementPP(side, mslot);
1301
1220
  }
1302
1221
 
1222
+ incrementPP(side, mslot);
1223
+
1303
1224
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1225
+ return doMove(battle, player, mslot, auto, residual, log);
1305
1226
  }
1306
1227
 
1307
1228
  fn metronome(
1308
1229
  battle: anytype,
1309
1230
  player: Player,
1310
1231
  mslot: u4,
1311
- rewrap: bool,
1312
1232
  auto: bool,
1313
1233
  residual: *bool,
1314
1234
  log: anytype,
@@ -1319,7 +1239,7 @@ fn metronome(
1319
1239
  incrementPP(side, mslot);
1320
1240
 
1321
1241
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1242
+ return doMove(battle, player, mslot, auto, residual, log);
1323
1243
  }
1324
1244
 
1325
1245
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1526,7 +1446,11 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1526
1446
  stored.hp -= amount;
1527
1447
 
1528
1448
  // As above, Pokémon Showdown uses damageOf but its not relevant
1529
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1449
+ if (amount > 0) {
1450
+ try log.damage(ident, stored, .LeechSeed);
1451
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1452
+ if (showdown) battle.last_damage = damage;
1453
+ }
1530
1454
 
1531
1455
  const before = foe_stored.hp;
1532
1456
  // Uncapped damage is added back to the foe
@@ -1540,13 +1464,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1540
1464
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1541
1465
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1542
1466
 
1543
- battle.turn += 1;
1544
-
1545
1467
  if (showdown and options.ebc and checkEBC(battle)) {
1546
1468
  try log.tie();
1547
1469
  return Result.Tie;
1548
1470
  }
1549
1471
 
1472
+ battle.turn += 1;
1473
+
1550
1474
  if (showdown and battle.turn >= 1000) {
1551
1475
  try log.tie();
1552
1476
  return Result.Tie;
@@ -2059,9 +1983,9 @@ pub const Effects = struct {
2059
1983
  }
2060
1984
  break :has_mimic false;
2061
1985
  };
2062
- if (!has_mimic) {
2063
- // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2064
- if (!try checkHit(battle, player, move, log)) return;
1986
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1987
+ // trigger |-miss| instead of |-fail|
1988
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2065
1989
  return try log.fail(battle.active(player.foe()), .None);
2066
1990
  }
2067
1991
  }
@@ -2329,7 +2253,7 @@ pub const Effects = struct {
2329
2253
  try log.transform(battle.active(player), foe_ident);
2330
2254
  }
2331
2255
 
2332
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2256
+ fn binding(battle: anytype, player: Player) void {
2333
2257
  var side = battle.side(player);
2334
2258
  var foe = battle.foe(player);
2335
2259
 
@@ -2337,9 +2261,7 @@ pub const Effects = struct {
2337
2261
  side.active.volatiles.Binding = true;
2338
2262
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2339
2263
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2340
- if (!showdown) {
2341
- foe.active.volatiles.Recharging = false;
2342
- } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2264
+ if (!showdown) foe.active.volatiles.Recharging = false;
2343
2265
 
2344
2266
  side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2345
2267
  }
package/src/lib/pkmn.zig CHANGED
@@ -25,12 +25,12 @@ pub const Result = @import("common/data.zig").Result;
25
25
  /// Pokémon Showdown's RNG (backed by a Generation V & VI RNG).
26
26
  pub const PSRNG = @import("common/rng.zig").PSRNG;
27
27
 
28
- /// Namespace for helpers related to protocol message logging.
28
+ /// Namespace for helpers related to protocol trace logging.
29
29
  pub const protocol = if (options.internal) struct {
30
30
  pub usingnamespace @import("common/protocol.zig");
31
31
  } else struct {
32
32
  /// Logs protocol information to its `Writer` during a battle update when
33
- /// `options.log` is enabled.
33
+ /// `options.trace` is enabled.
34
34
  pub const Log = @import("common/protocol.zig").Log;
35
35
  /// Stripped down version of `std.io.FixedBufferStream` optimized for
36
36
  /// efficiently writing the individual protocol bytes. Note that the
@@ -41,8 +41,8 @@ pub const protocol = if (options.internal) struct {
41
41
  /// intialized with a `LOGS_SIZE`-sized buffer.
42
42
  pub const FixedLog = @import("common/protocol.zig").FixedLog;
43
43
  /// Null object pattern implementation of `Log` backed by a
44
- /// `std.io.null_writer`. Ignores anything sent to it, though protocol
45
- /// logging should additionally be turned off entirely with `options.log`.
44
+ /// `std.io.null_writer`. Ignores anything sent to it, though trace logging
45
+ /// should addtionally be turned off entirely by using `options.trace`.
46
46
  pub const NULL = @import("common/protocol.zig").NULL;
47
47
  };
48
48