@pkmn/engine 0.1.0-dev.4005e495 → 0.1.0-dev.4eeb346e

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,13 @@ 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
516
+ if (showdown and volatiles.Flinch) {
517
+ volatiles.Flinch = false;
518
+ try log.cant(ident, .Flinch);
519
+ return .done;
520
+ }
521
+
561
522
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
562
523
  // before the side had a selected a move - we simply need to assume this leads to a desync
563
524
  if (side.last_selected_move == .None) {
@@ -629,14 +590,7 @@ fn beforeMove(
629
590
  if (volatiles.Bide) {
630
591
  assert(!volatiles.Thrashing and !volatiles.Rage);
631
592
 
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
- }
593
+ volatiles.state +%= battle.last_damage;
640
594
 
641
595
  assert(volatiles.attacks > 0);
642
596
 
@@ -680,13 +634,8 @@ fn beforeMove(
680
634
  try log.start(battle.active(player), .ConfusionSilent);
681
635
  }
682
636
  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
- }
637
+ if (showdown and handleThrashing(battle, active)) {
638
+ try log.start(battle.active(player), .ConfusionSilent);
690
639
  }
691
640
  return .skip_can;
692
641
  }
@@ -701,11 +650,7 @@ fn beforeMove(
701
650
  side.last_selected_move,
702
651
  );
703
652
  if (showdown or battle.last_damage != 0) {
704
- const sub = showdown and foe.active.volatiles.Substitute;
705
653
  _ = 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
654
  }
710
655
  return .done;
711
656
  }
@@ -737,7 +682,6 @@ fn canMove(
737
682
  side.active.volatiles.Invulnerable = false;
738
683
  } else if (move.effect == .Charge) {
739
684
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
- setCounterable(battle, player, side, move);
741
685
  try Effects.charge(battle, player, log);
742
686
  return false;
743
687
  }
@@ -754,7 +698,6 @@ fn canMove(
754
698
  // regrettable, but this information is more "nice to have" than required
755
699
  const f = if (from != null and from.? == .None) null else from;
756
700
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
- setCounterable(battle, player, side, move);
758
701
 
759
702
  if (move.effect.onBegin()) {
760
703
  try onBegin(battle, player, move, mslot, residual, log);
@@ -765,27 +708,12 @@ fn canMove(
765
708
  Effects.thrashing(battle, player);
766
709
  } else if (!showdown and move.effect == .Binding) {
767
710
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
768
- Effects.binding(battle, player, false);
711
+ Effects.binding(battle, player);
769
712
  }
770
713
 
771
714
  return true;
772
715
  }
773
716
 
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
717
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
790
718
  if (side.last_selected_move == .Struggle) return;
791
719
 
@@ -821,7 +749,6 @@ fn doMove(
821
749
  battle: anytype,
822
750
  player: Player,
823
751
  mslot: u4,
824
- rewrap: bool,
825
752
  auto: bool,
826
753
  residual: *bool,
827
754
  log: anytype,
@@ -913,9 +840,9 @@ fn doMove(
913
840
 
914
841
  if (!showdown or !miss) {
915
842
  if (move.effect == .MirrorMove) {
916
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
843
+ return mirrorMove(battle, player, mslot, auto, residual, log);
917
844
  } else if (move.effect == .Metronome) {
918
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
845
+ return metronome(battle, player, mslot, auto, residual, log);
919
846
  } else if (move.effect.onEnd()) {
920
847
  try onEnd(battle, player, move, log);
921
848
  return null;
@@ -930,7 +857,7 @@ fn doMove(
930
857
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
858
  if (ohko) {
932
859
  try log.immune(foe_ident, .OHKO);
933
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
860
+ } else if (immune and !invulnerable) {
934
861
  try log.immune(foe_ident, .None);
935
862
  } else if (mist) {
936
863
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -996,17 +923,11 @@ fn doMove(
996
923
  } else if (showdown) {
997
924
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
998
925
  if (move.effect == .Binding) {
999
- Effects.binding(battle, player, rewrap);
926
+ Effects.binding(battle, player);
1000
927
  if (immune) {
1001
928
  battle.last_damage = 0;
1002
- assert(foe.stored().hp > 0);
1003
929
  // 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
- }
930
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1010
931
  return null;
1011
932
  }
1012
933
  }
@@ -1203,17 +1124,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1203
1124
  return null;
1204
1125
  }
1205
1126
 
1206
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1127
+ // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1128
+ const foe_last_used_move =
1129
+ Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1207
1130
  const foe_last_selected_move =
1208
1131
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1209
- .Splash
1132
+ .Counter
1210
1133
  else
1211
1134
  foe.last_selected_move);
1212
1135
 
1213
- const used = (if (player.foe() == .P1)
1214
- battle.last_moves.p1_counterable
1215
- else
1216
- battle.last_moves.p2_counterable) != 0;
1136
+ const used = foe_last_used_move.bp > 0 and
1137
+ foe.last_used_move != .Counter and
1138
+ (foe_last_used_move.type == .Normal or
1139
+ foe_last_used_move.type == .Fighting);
1217
1140
 
1218
1141
  const selected = foe_last_selected_move.bp > 0 and
1219
1142
  foe.last_selected_move != .Counter and
@@ -1232,14 +1155,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1232
1155
  return null;
1233
1156
  }
1234
1157
 
1235
- battle.last_damage *|= 2;
1158
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1236
1159
 
1237
1160
  // Pokémon Showdown calls checkHit before Counter
1238
1161
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1239
1162
 
1240
- const sub = showdown and foe.active.volatiles.Substitute;
1241
1163
  _ = 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
1164
  return null;
1244
1165
  }
1245
1166
 
@@ -1283,7 +1204,6 @@ fn mirrorMove(
1283
1204
  battle: anytype,
1284
1205
  player: Player,
1285
1206
  mslot: u4,
1286
- rewrap: bool,
1287
1207
  auto: bool,
1288
1208
  residual: *bool,
1289
1209
  log: anytype,
@@ -1296,19 +1216,18 @@ fn mirrorMove(
1296
1216
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1297
1217
  try log.fail(battle.active(player), .None);
1298
1218
  return null;
1299
- } else if (!showdown or foe.last_used_move != .Struggle) {
1300
- incrementPP(side, mslot);
1301
1219
  }
1302
1220
 
1221
+ incrementPP(side, mslot);
1222
+
1303
1223
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1224
+ return doMove(battle, player, mslot, auto, residual, log);
1305
1225
  }
1306
1226
 
1307
1227
  fn metronome(
1308
1228
  battle: anytype,
1309
1229
  player: Player,
1310
1230
  mslot: u4,
1311
- rewrap: bool,
1312
1231
  auto: bool,
1313
1232
  residual: *bool,
1314
1233
  log: anytype,
@@ -1319,7 +1238,7 @@ fn metronome(
1319
1238
  incrementPP(side, mslot);
1320
1239
 
1321
1240
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1241
+ return doMove(battle, player, mslot, auto, residual, log);
1323
1242
  }
1324
1243
 
1325
1244
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1526,7 +1445,11 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1526
1445
  stored.hp -= amount;
1527
1446
 
1528
1447
  // As above, Pokémon Showdown uses damageOf but its not relevant
1529
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1448
+ if (amount > 0) {
1449
+ try log.damage(ident, stored, .LeechSeed);
1450
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1451
+ if (showdown) battle.last_damage = damage;
1452
+ }
1530
1453
 
1531
1454
  const before = foe_stored.hp;
1532
1455
  // Uncapped damage is added back to the foe
@@ -1540,13 +1463,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1540
1463
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1541
1464
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1542
1465
 
1543
- battle.turn += 1;
1544
-
1545
1466
  if (showdown and options.ebc and checkEBC(battle)) {
1546
1467
  try log.tie();
1547
1468
  return Result.Tie;
1548
1469
  }
1549
1470
 
1471
+ battle.turn += 1;
1472
+
1550
1473
  if (showdown and battle.turn >= 1000) {
1551
1474
  try log.tie();
1552
1475
  return Result.Tie;
@@ -2059,9 +1982,9 @@ pub const Effects = struct {
2059
1982
  }
2060
1983
  break :has_mimic false;
2061
1984
  };
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;
1985
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1986
+ // trigger |-miss| instead of |-fail|
1987
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2065
1988
  return try log.fail(battle.active(player.foe()), .None);
2066
1989
  }
2067
1990
  }
@@ -2329,7 +2252,7 @@ pub const Effects = struct {
2329
2252
  try log.transform(battle.active(player), foe_ident);
2330
2253
  }
2331
2254
 
2332
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2255
+ fn binding(battle: anytype, player: Player) void {
2333
2256
  var side = battle.side(player);
2334
2257
  var foe = battle.foe(player);
2335
2258
 
@@ -2337,9 +2260,7 @@ pub const Effects = struct {
2337
2260
  side.active.volatiles.Binding = true;
2338
2261
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2339
2262
  // (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);
2263
+ if (!showdown) foe.active.volatiles.Recharging = false;
2343
2264
 
2344
2265
  side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2345
2266
  }
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