@pkmn/engine 0.1.0-dev.a5a9f4b2 → 0.1.0-dev.af1565df

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