@pkmn/engine 0.1.0-dev.3a273268 → 0.1.0-dev.4005e495

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