@pkmn/engine 0.1.0-dev.02b7f810

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.
Files changed (52) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +332 -0
  3. package/build/pkg/addon.d.ts +7 -0
  4. package/build/pkg/addon.js +111 -0
  5. package/build/pkg/addon.js.map +1 -0
  6. package/build/pkg/data/ids.json +240 -0
  7. package/build/pkg/data/layout.json +86 -0
  8. package/build/pkg/data/protocol.json +173 -0
  9. package/build/pkg/data.d.ts +91 -0
  10. package/build/pkg/data.js +150 -0
  11. package/build/pkg/data.js.map +1 -0
  12. package/build/pkg/gen1.d.ts +109 -0
  13. package/build/pkg/gen1.js +668 -0
  14. package/build/pkg/gen1.js.map +1 -0
  15. package/build/pkg/index.d.ts +383 -0
  16. package/build/pkg/index.js +122 -0
  17. package/build/pkg/index.js.map +1 -0
  18. package/build/pkg/protocol.d.ts +71 -0
  19. package/build/pkg/protocol.js +452 -0
  20. package/build/pkg/protocol.js.map +1 -0
  21. package/build/test/display.js +382 -0
  22. package/build/test/display.js.map +1 -0
  23. package/build/tools/debug.js +133 -0
  24. package/build/tools/debug.js.map +1 -0
  25. package/build.zig +346 -0
  26. package/package.json +202 -0
  27. package/src/bin/install-pkmn-engine +391 -0
  28. package/src/bin/pkmn-debug +62 -0
  29. package/src/lib/binding/node.zig +229 -0
  30. package/src/lib/binding/wasm.zig +10 -0
  31. package/src/lib/common/data.zig +129 -0
  32. package/src/lib/common/debug.zig +64 -0
  33. package/src/lib/common/options.zig +40 -0
  34. package/src/lib/common/protocol.zig +1297 -0
  35. package/src/lib/common/rng.zig +234 -0
  36. package/src/lib/gen1/README.md +791 -0
  37. package/src/lib/gen1/data/moves.zig +1854 -0
  38. package/src/lib/gen1/data/species.zig +1105 -0
  39. package/src/lib/gen1/data/types.zig +131 -0
  40. package/src/lib/gen1/data.zig +620 -0
  41. package/src/lib/gen1/helpers.zig +234 -0
  42. package/src/lib/gen1/mechanics.zig +2819 -0
  43. package/src/lib/gen2/README.md +309 -0
  44. package/src/lib/gen2/data/items.zig +236 -0
  45. package/src/lib/gen2/data/moves.zig +2784 -0
  46. package/src/lib/gen2/data/species.zig +1798 -0
  47. package/src/lib/gen2/data/types.zig +123 -0
  48. package/src/lib/gen2/data.zig +425 -0
  49. package/src/lib/gen2/helpers.zig +225 -0
  50. package/src/lib/gen2/mechanics.zig +53 -0
  51. package/src/lib/pkmn.zig +54 -0
  52. package/src/test/display/pkmn.html.tmpl +368 -0
@@ -0,0 +1,2819 @@
1
+ const std = @import("std");
2
+
3
+ const common = @import("../common/data.zig");
4
+ const DEBUG = @import("../common/debug.zig").print;
5
+ const options = @import("../common/options.zig");
6
+ const protocol = @import("../common/protocol.zig");
7
+ const rng = @import("../common/rng.zig");
8
+
9
+ const data = @import("data.zig");
10
+
11
+ const assert = std.debug.assert;
12
+
13
+ const expectEqual = std.testing.expectEqual;
14
+
15
+ const Choice = common.Choice;
16
+ const ID = common.ID;
17
+ const Player = common.Player;
18
+ const Result = common.Result;
19
+
20
+ const showdown = options.showdown;
21
+
22
+ const Boost = protocol.Boost;
23
+ const Damage = protocol.Damage;
24
+ const Heal = protocol.Heal;
25
+
26
+ const Gen12 = rng.Gen12;
27
+
28
+ const ActivePokemon = data.ActivePokemon;
29
+ const Effectiveness = data.Effectiveness;
30
+ const Move = data.Move;
31
+ const MoveSlot = data.MoveSlot;
32
+ const Pokemon = data.Pokemon;
33
+ const Side = data.Side;
34
+ const Species = data.Species;
35
+ const Stats = data.Stats;
36
+ const Status = data.Status;
37
+ const Type = data.Type;
38
+
39
+ // zig fmt: off
40
+ const BOOSTS = &[_][2]u8{
41
+ .{ 25, 100 }, // -6
42
+ .{ 28, 100 }, // -5
43
+ .{ 33, 100 }, // -4
44
+ .{ 40, 100 }, // -3
45
+ .{ 50, 100 }, // -2
46
+ .{ 66, 100 }, // -1
47
+ .{ 1, 1 }, // 0
48
+ .{ 15, 10 }, // +1
49
+ .{ 2, 1 }, // +2
50
+ .{ 25, 10 }, // +3
51
+ .{ 3, 1 }, // +4
52
+ .{ 35, 10 }, // +5
53
+ .{ 4, 1 }, // +6
54
+ };
55
+ // zig fmt: on
56
+
57
+ const MAX_STAT_VALUE = 999;
58
+
59
+ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
60
+ assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
61
+ if (battle.turn == 0) return start(battle, log);
62
+
63
+ var f1: ?Move = null;
64
+ var f2: ?Move = null;
65
+
66
+ if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
67
+ if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
68
+
69
+ if (turnOrder(battle, c1, c2) == .P1) {
70
+ if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
71
+ } else {
72
+ if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
73
+ }
74
+
75
+ var p1 = battle.side(.P1);
76
+ if (p1.active.volatiles.attacks == 0) {
77
+ p1.active.volatiles.Binding = false;
78
+ }
79
+ var p2 = battle.side(.P2);
80
+ if (p2.active.volatiles.attacks == 0) {
81
+ p2.active.volatiles.Binding = false;
82
+ }
83
+
84
+ return endTurn(battle, log);
85
+ }
86
+
87
+ fn start(battle: anytype, log: anytype) !Result {
88
+ const p1 = battle.side(.P1);
89
+ const p2 = battle.side(.P2);
90
+
91
+ var p1_slot = findFirstAlive(p1);
92
+ assert(!showdown or p1_slot == 1);
93
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
94
+
95
+ var p2_slot = findFirstAlive(p2);
96
+ assert(!showdown or p2_slot == 1);
97
+ if (p2_slot == 0) return Result.Win;
98
+
99
+ try switchIn(battle, .P1, p1_slot, true, log);
100
+ try switchIn(battle, .P2, p2_slot, true, log);
101
+
102
+ return endTurn(battle, log);
103
+ }
104
+
105
+ fn findFirstAlive(side: *const Side) u8 {
106
+ for (side.pokemon, 0..) |pokemon, i| if (pokemon.hp > 0) return side.order[i];
107
+ return 0;
108
+ }
109
+
110
+ fn selectMove(
111
+ battle: anytype,
112
+ player: Player,
113
+ choice: Choice,
114
+ foe_choice: Choice,
115
+ from: *?Move,
116
+ ) ?Result {
117
+ if (choice.type == .Pass) return null;
118
+
119
+ var side = battle.side(player);
120
+ var volatiles = &side.active.volatiles;
121
+ const stored = side.stored();
122
+
123
+ assert(!isForced(side.active) or
124
+ (choice.type == .Move and choice.data == @boolToInt(showdown)));
125
+
126
+ // pre-battle menu
127
+ if (volatiles.Recharging) return null;
128
+ if (volatiles.Rage) {
129
+ from.* = side.last_used_move;
130
+ if (showdown) saveMove(battle, player, null);
131
+ return null;
132
+ }
133
+ // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
134
+ if (!showdown) volatiles.Flinch = false;
135
+ if (volatiles.Thrashing or volatiles.Charging) {
136
+ from.* = side.last_selected_move;
137
+ if (showdown) saveMove(battle, player, null);
138
+ return null;
139
+ }
140
+
141
+ // battle menu
142
+ if (choice.type == .Switch) return null;
143
+
144
+ // pre-move select
145
+ if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
146
+ assert(showdown or choice.data == 0);
147
+ if (showdown) saveMove(battle, player, choice);
148
+ return null;
149
+ }
150
+ if (volatiles.Binding) {
151
+ from.* = side.last_used_move;
152
+ if (showdown) {
153
+ if (foe_choice.type == .Switch) from.* = null;
154
+ // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
155
+ // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
156
+ // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
157
+ // set the selected move will not actually be used, it will just be reported as having
158
+ // been used (this differs from how Pokémon Showdown works, but its impossible to
159
+ // replicate the incorrect behavior with the correct mechanisms).
160
+ saveMove(battle, player, choice);
161
+ } else {
162
+ assert(choice.data == 0);
163
+ // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
164
+ if (foe_choice.type == .Switch) {
165
+ const slot = if (player == .P1)
166
+ battle.last_selected_indexes.p1
167
+ else
168
+ battle.last_selected_indexes.p2;
169
+ const last = side.active.move(@intCast(u4, slot));
170
+ if (last.id == .Metronome) side.last_selected_move = last.id;
171
+ if (last.id == .MirrorMove) return Result.Error;
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+
177
+ if (battle.foe(player).active.volatiles.Binding) {
178
+ if (showdown) {
179
+ saveMove(battle, player, choice);
180
+ } else {
181
+ assert(choice.data == 0);
182
+ side.last_selected_move = .SKIP_TURN;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ // move select
188
+ volatiles.state = 0;
189
+ if (choice.data == 0) {
190
+ const struggle = ok: {
191
+ for (side.active.moves, 0..) |move, i| {
192
+ if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
193
+ }
194
+ break :ok true;
195
+ };
196
+
197
+ assert(struggle);
198
+ saveMove(battle, player, choice);
199
+ } else {
200
+ saveMove(battle, player, choice);
201
+ }
202
+
203
+ return null;
204
+ }
205
+
206
+ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
207
+ var side = battle.side(player);
208
+
209
+ if (choice) |c| {
210
+ assert(c.type == .Move);
211
+ if (c.data == 0) {
212
+ side.last_selected_move = .Struggle;
213
+ } else {
214
+ assert(showdown or side.active.volatiles.disabled_move != c.data);
215
+ const move = side.active.move(c.data);
216
+ // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
217
+ // sometimes required...), but a 0 PP move can be used automatically
218
+ assert(showdown or move.pp != 0);
219
+
220
+ side.last_selected_move = move.id;
221
+ if (player == .P1) {
222
+ battle.last_selected_indexes.p1 = @intCast(u4, c.data);
223
+ } else {
224
+ battle.last_selected_indexes.p2 = @intCast(u4, c.data);
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anytype) !void {
231
+ var side = battle.side(player);
232
+ var foe = battle.foe(player);
233
+ var active = &side.active;
234
+ const incoming = side.get(slot);
235
+
236
+ assert(incoming.hp != 0);
237
+ assert(slot != 1 or initial);
238
+
239
+ const out = side.order[0];
240
+ side.order[0] = side.order[slot - 1];
241
+ side.order[slot - 1] = out;
242
+
243
+ if (player == .P1) {
244
+ battle.last_selected_indexes.p1 = 1;
245
+ } else {
246
+ battle.last_selected_indexes.p2 = 1;
247
+ }
248
+
249
+ side.last_used_move = .None;
250
+ foe.last_used_move = .None;
251
+
252
+ active.stats = incoming.stats;
253
+ active.species = incoming.species;
254
+ active.types = incoming.types;
255
+ active.boosts = .{};
256
+ active.volatiles = .{};
257
+ active.moves = incoming.moves;
258
+
259
+ statusModify(incoming.status, &active.stats);
260
+
261
+ foe.active.volatiles.Binding = false;
262
+
263
+ try log.switched(battle.active(player), incoming);
264
+
265
+ if (showdown and incoming.status == Status.TOX) {
266
+ incoming.status = Status.init(.PSN);
267
+ // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
268
+ // rather not clutter up turnOrder just for this (incorrect) log message
269
+ try log.status(battle.active(player), incoming.status, .Silent);
270
+ }
271
+ }
272
+
273
+ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
274
+ assert(c1.type != .Pass or c2.type != .Pass);
275
+
276
+ if (c1.type == .Pass) return .P2;
277
+ if (c2.type == .Pass) return .P1;
278
+
279
+ if ((c1.type == .Switch) != (c2.type == .Switch)) return if (c1.type == .Switch) .P1 else .P2;
280
+
281
+ // https://www.smogon.com/forums/threads/adv-switch-priority.3622189/
282
+ // > In Gen 1 it's irrelevant [which player switches first] because switches happen instantly on
283
+ // > your own screen without waiting for the other player's choice (and their choice will appear
284
+ // > to happen first for them too, unless they attacked in which case your switch happens first)
285
+ // A cartridge-compatible implemention must not advance the RNG so we simply default to P1
286
+ const double_switch = c1.type == .Switch and c2.type == .Switch;
287
+ if (!showdown and double_switch) return .P1;
288
+
289
+ const m1 = battle.side(.P1).last_selected_move;
290
+ const m2 = battle.side(.P2).last_selected_move;
291
+ if (!showdown or !double_switch) {
292
+ if ((m1 == .QuickAttack) != (m2 == .QuickAttack)) {
293
+ return if (m1 == .QuickAttack) .P1 else .P2;
294
+ } else if ((m1 == .Counter) != (m2 == .Counter)) {
295
+ return if (m1 == .Counter) .P2 else .P1;
296
+ }
297
+ }
298
+
299
+ const spe1 = battle.side(.P1).active.stats.spe;
300
+ const spe2 = battle.side(.P2).active.stats.spe;
301
+ if (spe1 == spe2) {
302
+ // Pokémon Showdown's beforeTurnCallback shenanigans
303
+ if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
304
+
305
+ const p1 = if (showdown)
306
+ battle.rng.range(u8, 0, 2) == 0
307
+ else
308
+ battle.rng.next() < Gen12.percent(50) + 1;
309
+
310
+ if (!showdown) return if (p1) .P1 else .P2;
311
+
312
+ // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
313
+ // meaning that if a side is locked into a thrashing move and wins the speed tie, it
314
+ // actually uses its priority to simply insert its actual changed action into the queue,
315
+ // causing it to then execute *after* the side which should go second...
316
+ const t1 = battle.side(.P1).active.volatiles.Thrashing;
317
+ const t2 = battle.side(.P2).active.volatiles.Thrashing;
318
+ // If *both* sides are thrashing it really should be another speed tie, but we've patched
319
+ // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
320
+ // won the original coin flip
321
+ if (t1 and t2) return .P1;
322
+ return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
323
+ }
324
+
325
+ return if (spe1 > spe2) .P1 else .P2;
326
+ }
327
+
328
+ fn doTurn(
329
+ battle: anytype,
330
+ player: Player,
331
+ player_choice: Choice,
332
+ player_from: ?Move,
333
+ foe_player: Player,
334
+ foe_choice: Choice,
335
+ foe_from: ?Move,
336
+ log: anytype,
337
+ ) !?Result {
338
+ assert(player_choice.type != .Pass);
339
+
340
+ var residual = true;
341
+ var replace = battle.side(player).stored().hp == 0;
342
+ if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
343
+ if (!replace) {
344
+ if (player_choice.type != .Switch) {
345
+ if (try checkFaint(battle, foe_player, log)) |r| return r;
346
+ }
347
+ if (residual) try handleResidual(battle, player, log);
348
+ if (try checkFaint(battle, player, log)) |r| return r;
349
+ } else if (foe_choice.type == .Pass) return null;
350
+
351
+ residual = true;
352
+ replace = battle.side(foe_player).stored().hp == 0;
353
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
354
+ if (!replace) {
355
+ if (foe_choice.type != .Switch) {
356
+ if (try checkFaint(battle, player, log)) |r| return r;
357
+ }
358
+ if (residual) try handleResidual(battle, foe_player, log);
359
+ if (try checkFaint(battle, foe_player, log)) |r| return r;
360
+ }
361
+
362
+ // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
363
+ // it to get removed in the non-existent "residual" phase instead of during move selection
364
+ if (showdown) {
365
+ battle.side(.P1).active.volatiles.Flinch = false;
366
+ battle.side(.P2).active.volatiles.Flinch = false;
367
+ }
368
+
369
+ return null;
370
+ }
371
+
372
+ fn executeMove(
373
+ battle: anytype,
374
+ player: Player,
375
+ choice: Choice,
376
+ from: ?Move,
377
+ residual: *bool,
378
+ log: anytype,
379
+ ) !?Result {
380
+ var side = battle.side(player);
381
+
382
+ if (choice.type == .Switch) {
383
+ try switchIn(battle, player, @intCast(u4, choice.data), false, log);
384
+ return null;
385
+ }
386
+
387
+ if (side.last_selected_move == .SKIP_TURN) {
388
+ // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
389
+ // Binding instead gets handled in beforeMove after sleep and freeze
390
+ assert(!showdown);
391
+ if (battle.foe(player).active.volatiles.Binding) {
392
+ try log.cant(battle.active(player), .Bound);
393
+ }
394
+ return null;
395
+ }
396
+
397
+ assert(choice.type == .Move);
398
+ var mslot = @intCast(u4, choice.data);
399
+ // Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
400
+ // because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
401
+ var auto = showdown and side.last_selected_move != .None;
402
+
403
+ // GLITCH: Freeze top move selection desync & PP underflow shenanigans
404
+ if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
405
+ // choice.data == 0 only happens with Struggle on Pokémon Showdown
406
+ assert(!showdown);
407
+ mslot = @intCast(u4, if (player == .P1)
408
+ battle.last_selected_indexes.p1
409
+ else
410
+ battle.last_selected_indexes.p2);
411
+ const stored = side.stored();
412
+ // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
413
+ auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
414
+ from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
415
+ // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
416
+ // thawed, in which case we will desync unless the last_selected_move happened to be at
417
+ // index 1 and the current Pokémon has the same move in its first slot.
418
+ if (!auto) {
419
+ // side.active.moves(slot) is safe to check even though the slot in question might not
420
+ // technically be from this Pokémon because it must be exactly 1 to not desync and
421
+ // every Pokémon must have at least one move
422
+ if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
423
+ return Result.Error;
424
+ } else {
425
+ auto = true;
426
+ }
427
+ }
428
+ } else if (showdown and side.active.volatiles.Charging) {
429
+ // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
430
+ assert(mslot == 1);
431
+ mslot = @intCast(u4, if (player == .P1)
432
+ battle.last_selected_indexes.p1
433
+ else
434
+ battle.last_selected_indexes.p2);
435
+ }
436
+
437
+ var skip_can = false;
438
+ var skip_pp = false;
439
+ switch (try beforeMove(battle, player, from, residual, log)) {
440
+ .done => return null,
441
+ .skip_can => skip_can = true,
442
+ .skip_pp => skip_pp = true,
443
+ .ok => {},
444
+ .err => return @as(?Result, Result.Error),
445
+ }
446
+
447
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
448
+ return null;
449
+ }
450
+
451
+ return doMove(battle, player, mslot, auto, residual, log);
452
+ }
453
+
454
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
455
+
456
+ fn beforeMove(
457
+ battle: anytype,
458
+ player: Player,
459
+ from: ?Move,
460
+ residual: *bool,
461
+ log: anytype,
462
+ ) !BeforeMove {
463
+ var side = battle.side(player);
464
+ const foe = battle.foe(player);
465
+ var active = &side.active;
466
+ var stored = side.stored();
467
+ const ident = battle.active(player);
468
+ var volatiles = &active.volatiles;
469
+
470
+ if (Status.is(stored.status, .SLP)) {
471
+ const status = stored.status;
472
+ // Even if the EXT bit is set this will still correctly modify the sleep duration
473
+ stored.status -= 1;
474
+ if (Status.duration(stored.status) == 0) {
475
+ try log.curestatus(ident, status, .Message);
476
+ stored.status = 0; // clears EXT if present
477
+ } else {
478
+ try log.cant(ident, .Sleep);
479
+ }
480
+ side.last_used_move = .None;
481
+ return .done;
482
+ }
483
+
484
+ if (Status.is(stored.status, .FRZ)) {
485
+ try log.cant(ident, .Freeze);
486
+ side.last_used_move = .None;
487
+ return .done;
488
+ }
489
+
490
+ if (foe.active.volatiles.Binding) {
491
+ try log.cant(ident, .Bound);
492
+ return .done;
493
+ }
494
+
495
+ if (!showdown and volatiles.Flinch) {
496
+ volatiles.Flinch = false;
497
+ try log.cant(ident, .Flinch);
498
+ return .done;
499
+ }
500
+
501
+ if (volatiles.Recharging) {
502
+ volatiles.Recharging = false;
503
+ try log.cant(ident, .Recharge);
504
+ return .done;
505
+ }
506
+
507
+ if (volatiles.disabled_duration > 0) {
508
+ volatiles.disabled_duration -= 1;
509
+ if (volatiles.disabled_duration == 0) {
510
+ volatiles.disabled_move = 0;
511
+ try log.end(ident, .Disable);
512
+ }
513
+ }
514
+ // Pokémon Showdown's disable condition has a single onBeforeMove handler
515
+ if (showdown and try disabled(side, ident, log)) return .done;
516
+
517
+ // Pokémon Showdown checks for Flinch *after* instead of before
518
+ if (showdown and volatiles.Flinch) {
519
+ volatiles.Flinch = false;
520
+ try log.cant(ident, .Flinch);
521
+ return .done;
522
+ }
523
+
524
+ // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
525
+ // before the side had a selected a move - we simply need to assume this leads to a desync
526
+ if (side.last_selected_move == .None) {
527
+ assert(!showdown);
528
+ return .err;
529
+ }
530
+
531
+ if (volatiles.Confusion) {
532
+ assert(volatiles.confusion > 0);
533
+
534
+ volatiles.confusion -= 1;
535
+ if (volatiles.confusion == 0) {
536
+ volatiles.Confusion = false;
537
+ try log.end(ident, .Confusion);
538
+ } else {
539
+ try log.activate(ident, .Confusion);
540
+
541
+ const confused = if (showdown)
542
+ !battle.rng.chance(u8, 128, 256)
543
+ else
544
+ battle.rng.next() >= Gen12.percent(50) + 1;
545
+
546
+ if (confused) {
547
+ assert(!volatiles.MultiHit);
548
+ if (!volatiles.Rage) volatiles.state = 0;
549
+ volatiles.Bide = false;
550
+ volatiles.Thrashing = false;
551
+ volatiles.MultiHit = false;
552
+ volatiles.Flinch = false;
553
+ volatiles.Charging = false;
554
+ volatiles.Binding = false;
555
+ volatiles.Invulnerable = false;
556
+ {
557
+ // This feels (and is) disgusting but the cartridge literally just overwrites
558
+ // the opponent's defense with the user's defense and resets it after. As a
559
+ // result of this the *opponent's* Reflect impacts confusion self-hit damage
560
+ const def = foe.active.stats.def;
561
+ foe.active.stats.def = active.stats.def;
562
+ defer foe.active.stats.def = def;
563
+ if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
564
+ }
565
+ // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
566
+ // on the targeting behavior of the confused Pokémon's selected move which results
567
+ // in the wrong behavior with respect to the Substitute + Confusion glitch
568
+ const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
569
+ player
570
+ else
571
+ player.foe();
572
+
573
+ const uncapped = battle.last_damage;
574
+ // Skipping adjustDamage / randomizeDamage / checkHit
575
+ _ = try applyDamage(battle, player, target, .Confusion, log);
576
+ // Pokémon Showdown thinks that Confusion damage is uncapped ¯\_(ツ)_/¯
577
+ if (showdown) battle.last_damage = uncapped;
578
+
579
+ return .done;
580
+ }
581
+ }
582
+ }
583
+
584
+ if (!showdown and try disabled(side, ident, log)) return .done;
585
+
586
+ if (Status.is(stored.status, .PAR)) {
587
+ const paralyzed = if (showdown)
588
+ battle.rng.chance(u8, 63, 256)
589
+ else
590
+ battle.rng.next() < Gen12.percent(25);
591
+
592
+ if (paralyzed) {
593
+ if (!volatiles.Rage) volatiles.state = 0;
594
+ volatiles.Bide = false;
595
+ volatiles.Thrashing = false;
596
+ volatiles.Charging = false;
597
+ volatiles.Binding = false;
598
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
599
+ try log.cant(ident, .Paralysis);
600
+ return .done;
601
+ }
602
+ }
603
+
604
+ if (volatiles.Bide) {
605
+ assert(!volatiles.Thrashing and !volatiles.Rage);
606
+
607
+ volatiles.state +%= battle.last_damage;
608
+
609
+ assert(volatiles.attacks > 0);
610
+
611
+ volatiles.attacks -= 1;
612
+ if (volatiles.attacks != 0) {
613
+ try log.activate(ident, .Bide);
614
+ return .done;
615
+ }
616
+
617
+ volatiles.Bide = false;
618
+ try log.end(ident, .Bide);
619
+
620
+ battle.last_damage = volatiles.state *% 2;
621
+ volatiles.state = 0;
622
+
623
+ if (battle.last_damage == 0) {
624
+ try log.fail(ident, .None);
625
+ return .done;
626
+ }
627
+
628
+ const sub = showdown and foe.active.volatiles.Substitute;
629
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
630
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
631
+
632
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
633
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
634
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
635
+ if (showdown) {
636
+ residual.* = false;
637
+ try handleResidual(battle, player, log);
638
+ }
639
+
640
+ return .done;
641
+ }
642
+
643
+ if (volatiles.Thrashing) {
644
+ assert(volatiles.attacks > 0);
645
+ assert(from != null);
646
+ volatiles.attacks -= 1;
647
+ if (!showdown and handleThrashing(battle, active)) {
648
+ try log.start(battle.active(player), .ConfusionSilent);
649
+ }
650
+ try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
651
+ if (showdown and handleThrashing(battle, active)) {
652
+ try log.start(battle.active(player), .ConfusionSilent);
653
+ }
654
+ return .skip_can;
655
+ }
656
+
657
+ if (volatiles.Binding) {
658
+ assert(volatiles.attacks > 0);
659
+ volatiles.attacks -= 1;
660
+ try log.move(
661
+ battle.active(player),
662
+ side.last_selected_move,
663
+ battle.active(player.foe()),
664
+ side.last_selected_move,
665
+ );
666
+ if (showdown or battle.last_damage != 0) {
667
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
668
+ }
669
+ return .done;
670
+ }
671
+
672
+ return if (volatiles.Rage) .skip_pp else .ok;
673
+ }
674
+
675
+ fn canMove(
676
+ battle: anytype,
677
+ player: Player,
678
+ mslot: u4,
679
+ auto: bool,
680
+ skip_pp: bool,
681
+ from: ?Move,
682
+ residual: *bool,
683
+ log: anytype,
684
+ ) !bool {
685
+ var side = battle.side(player);
686
+ const player_ident = battle.active(player);
687
+ const move = Move.get(side.last_selected_move);
688
+
689
+ const foe_last = battle.foe(player).last_used_move;
690
+ const special = from != null and (from.? == .Metronome and
691
+ (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
692
+
693
+ var skip = skip_pp;
694
+ if (side.active.volatiles.Charging) {
695
+ side.active.volatiles.Charging = false;
696
+ side.active.volatiles.Invulnerable = false;
697
+ } else if (move.effect == .Charge) {
698
+ try log.move(player_ident, side.last_selected_move, .{}, from);
699
+ try Effects.charge(battle, player, log);
700
+ return false;
701
+ }
702
+
703
+ if (!showdown or !special) side.last_used_move = side.last_selected_move;
704
+ if (!skip) decrementPP(side, mslot, auto);
705
+
706
+ const target = if (move.target == .Self) player else player.foe();
707
+ // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
708
+ // don't have space to be able to track it in all circumstances and thus usually try to infer it
709
+ // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
710
+ // meaning sometimes we lose this information. from is still set because it not being null is
711
+ // relevant for control flow, but we don't have anything useful to put in the log. This is
712
+ // regrettable, but this information is more "nice to have" than required
713
+ const f = if (from != null and from.? == .None) null else from;
714
+ try log.move(player_ident, side.last_selected_move, battle.active(target), f);
715
+
716
+ if (move.effect.onBegin()) {
717
+ try onBegin(battle, player, move, mslot, residual, log);
718
+ return false;
719
+ }
720
+
721
+ if (move.effect == .Thrashing) {
722
+ Effects.thrashing(battle, player);
723
+ } else if (!showdown and move.effect == .Binding) {
724
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation
725
+ Effects.binding(battle, player);
726
+ }
727
+
728
+ return true;
729
+ }
730
+
731
+ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
732
+ if (side.last_selected_move == .Struggle) return;
733
+
734
+ var active = &side.active;
735
+ const volatiles = &active.volatiles;
736
+
737
+ assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
738
+ if (volatiles.Bide) return;
739
+
740
+ assert(active.move(mslot).pp > 0 or auto);
741
+ active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) -% 1;
742
+ if (volatiles.Transform) return;
743
+
744
+ assert(side.stored().move(mslot).pp > 0 or auto);
745
+ side.stored().move(mslot).pp = @intCast(u6, side.stored().move(mslot).pp) -% 1;
746
+ assert(active.move(mslot).pp == side.stored().move(mslot).pp);
747
+ }
748
+
749
+ fn incrementPP(side: *Side, mslot: u4) void {
750
+ var active = &side.active;
751
+ const volatiles = &active.volatiles;
752
+
753
+ active.move(mslot).pp = @intCast(u6, active.move(mslot).pp) +% 1;
754
+ // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
755
+ if (showdown and volatiles.Transform) return;
756
+
757
+ assert(mslot > 0 and mslot <= 4);
758
+ side.stored().moves[mslot - 1].pp = @intCast(u6, side.stored().moves[mslot - 1].pp) +% 1;
759
+ }
760
+
761
+ // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
762
+ fn doMove(
763
+ battle: anytype,
764
+ player: Player,
765
+ mslot: u4,
766
+ auto: bool,
767
+ residual: *bool,
768
+ log: anytype,
769
+ ) !?Result {
770
+ var side = battle.side(player);
771
+ const foe = battle.foe(player);
772
+
773
+ var move = Move.get(side.last_selected_move);
774
+ const counter = side.last_selected_move == .Counter;
775
+ const status = move.bp == 0 and move.effect != .OHKO;
776
+
777
+ var crit = false;
778
+ var ohko = false;
779
+ var immune = false;
780
+ var mist = false;
781
+ var hits: u4 = 1;
782
+ var effectiveness = Effectiveness.neutral;
783
+
784
+ // The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
785
+ if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
786
+ return specialDamage(battle, player, move, log);
787
+ }
788
+
789
+ // Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
790
+ // calling moveHit early covers most of that but we also need to check type immunity first
791
+ var miss = showdown and miss: {
792
+ immune = move.target != .Self and !status and !counter and
793
+ (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
794
+ @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
795
+ if (immune and move.effect != .Binding) break :miss true;
796
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
797
+ battle.last_damage = 0;
798
+ break :miss true;
799
+ }
800
+ break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
801
+ };
802
+ assert(!immune or miss or (showdown and move.effect == .Binding));
803
+
804
+ var late = showdown and move.effect != .Explode;
805
+ const skip = status or immune;
806
+ if ((!showdown or (!skip or counter)) and !miss) blk: {
807
+ if (showdown and move.effect.isMulti()) {
808
+ Effects.multiHit(battle, player, move);
809
+ hits = side.active.volatiles.attacks;
810
+ late = false;
811
+ }
812
+
813
+ // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
814
+ const check = !showdown or (!counter and move.effect != .OHKO);
815
+ if (check) crit = checkCriticalHit(battle, player, move);
816
+
817
+ if (counter) return counterDamage(battle, player, move, log);
818
+
819
+ battle.last_damage = 0;
820
+
821
+ // Disassembly does a check to allow 0 BP MultiHit moves but this isn't possible in practice
822
+ assert(move.effect != .MultiHit or move.bp > 0);
823
+ if (!skip) {
824
+ if (move.effect == .OHKO) {
825
+ ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
826
+ // This can overflow after adjustDamage, but will still be sufficient to OHKO
827
+ battle.last_damage = if (ohko) 65535 else 0;
828
+ if (showdown) break :blk; // skip adjustDamage / randomizeDamage
829
+ } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
830
+ return @as(?Result, Result.Error);
831
+ }
832
+ if (battle.last_damage == 0) {
833
+ immune = true;
834
+ effectiveness = 0;
835
+ } else {
836
+ effectiveness = adjustDamage(battle, player);
837
+ immune = effectiveness == 0;
838
+ }
839
+ randomizeDamage(battle);
840
+ }
841
+ }
842
+
843
+ // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
844
+ if (showdown and skip) battle.last_damage = 0;
845
+
846
+ miss = if (showdown or skip)
847
+ miss
848
+ else
849
+ (!moveHit(battle, player, move, &immune, &mist) or battle.last_damage == 0);
850
+
851
+ assert(showdown or miss or battle.last_damage > 0 or skip);
852
+ assert((!showdown and miss) or !(ohko and immune));
853
+ assert(!immune or miss or move.effect == .Binding);
854
+
855
+ if (!showdown or !miss) {
856
+ if (move.effect == .MirrorMove) {
857
+ return mirrorMove(battle, player, mslot, auto, residual, log);
858
+ } else if (move.effect == .Metronome) {
859
+ return metronome(battle, player, mslot, auto, residual, log);
860
+ } else if (move.effect.onEnd()) {
861
+ try onEnd(battle, player, move, log);
862
+ return null;
863
+ }
864
+ }
865
+
866
+ if (miss) {
867
+ const foe_ident = battle.active(player.foe());
868
+ const invulnerable =
869
+ showdown and foe.active.volatiles.Invulnerable and move.effect != .Swift;
870
+ ohko = (!showdown or (!immune and !invulnerable)) and
871
+ move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
872
+ if (ohko) {
873
+ try log.immune(foe_ident, .OHKO);
874
+ } else if (immune and !invulnerable) {
875
+ try log.immune(foe_ident, .None);
876
+ } else if (mist) {
877
+ if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
878
+ try log.fail(foe_ident, .None);
879
+ } else {
880
+ try log.lastmiss();
881
+ try log.miss(battle.active(player));
882
+ }
883
+ if (move.effect == .JumpKick) {
884
+ // Recoil is supposed to be damage/8 but damage will always be 0 here
885
+ assert(battle.last_damage == 0);
886
+ battle.last_damage = 1;
887
+ _ = try applyDamage(battle, player, player.foe(), .None, log);
888
+ if (showdown and side.stored().hp == 0) residual.* = false;
889
+ } else if (move.effect == .Explode) {
890
+ try Effects.explode(battle, player);
891
+ try buildRage(battle, player.foe(), log);
892
+ } else if (showdown and move.effect == .Disable) {
893
+ try buildRage(battle, player.foe(), log);
894
+ }
895
+ return null;
896
+ }
897
+
898
+ // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
899
+ // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
900
+ // it up above before damage calculation).
901
+ if (!showdown and move.effect.isMulti()) {
902
+ Effects.multiHit(battle, player, move);
903
+ hits = side.active.volatiles.attacks;
904
+ }
905
+
906
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
907
+ const sub = showdown and foe.active.volatiles.Substitute;
908
+
909
+ var nullified = false;
910
+ var hit: u4 = 0;
911
+ while (hit < hits) {
912
+ if (hit == 0) {
913
+ if (crit) try log.crit(battle.active(player.foe()));
914
+ if (effectiveness > Effectiveness.neutral) {
915
+ try log.supereffective(battle.active(player.foe()));
916
+ } else if (effectiveness < Effectiveness.neutral) {
917
+ try log.resisted(battle.active(player.foe()));
918
+ }
919
+ }
920
+ if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, log);
921
+ if (hit == 0 and ohko) try log.ohko();
922
+ hit += 1;
923
+ if (foe.stored().hp == 0) break;
924
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
925
+ // If the substitute breaks during a multi-hit attack, the attack ends
926
+ if (nullified) break;
927
+ }
928
+
929
+ if (side.active.volatiles.MultiHit) {
930
+ side.active.volatiles.MultiHit = false;
931
+ assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
932
+ side.active.volatiles.attacks = 0;
933
+ if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
934
+ try Effects.poison(battle, player, Move.get(.PoisonSting), log);
935
+ }
936
+ try log.hitcount(battle.active(player.foe()), hit);
937
+ } else if (showdown) {
938
+ // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
939
+ if (move.effect == .Binding) {
940
+ Effects.binding(battle, player);
941
+ if (immune) {
942
+ battle.last_damage = 0;
943
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
944
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
945
+ return null;
946
+ }
947
+ }
948
+ }
949
+
950
+ // Substitute being broken nullifies the move's effect completely so even
951
+ // if an effect was intended to "always happen" it will still get skipped.
952
+ if (nullified) return null;
953
+
954
+ // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
955
+ // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
956
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches.
957
+ if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, log);
958
+
959
+ if (foe.stored().hp == 0) return null;
960
+
961
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
962
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
963
+
964
+ if (!move.effect.isSpecial()) {
965
+ // On the cartridge Rage is not considered to be "special" and thus gets executed for a
966
+ // second time here (after being executed in the "always happens" block above) but that
967
+ // doesn't matter since its idempotent (on the cartridge, but not in the implementation
968
+ // below). For Twineedle we change the data to that of one with PoisonChance1 given its
969
+ // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
970
+ // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
971
+ // puts the |-status| message before |-hitcount| instead of after.
972
+ if (move.effect == .Twineedle) {
973
+ if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
974
+ } else if (move.effect == .Disable) {
975
+ const result = try Effects.disable(battle, player, move, log);
976
+ if (showdown) try buildRage(battle, player.foe(), log);
977
+ return result;
978
+ } else {
979
+ try moveEffect(battle, player, move, log);
980
+ }
981
+ }
982
+
983
+ return null;
984
+ }
985
+
986
+ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
987
+ const side = battle.side(player);
988
+
989
+ // Base speed is used for the critical hit calculation, even when Transform-ed
990
+ var chance = @as(u16, Species.chance(side.stored().species));
991
+ // GLITCH: Focus Energy reduces critical hit chance instead of increasing it
992
+ chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
993
+ chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
994
+
995
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
996
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
997
+ }
998
+
999
+ fn calcDamage(
1000
+ battle: anytype,
1001
+ player: Player,
1002
+ target_player: Player,
1003
+ m: ?Move.Data,
1004
+ crit: bool,
1005
+ ) bool {
1006
+ // Confusion (indicated when m == null) just needs a 40 BP physical move
1007
+ const cfz = m == null;
1008
+ const move = m orelse Move.get(.Pound);
1009
+ assert(move.bp != 0);
1010
+
1011
+ const side = battle.side(player);
1012
+ const target = battle.side(target_player);
1013
+
1014
+ const special = move.type.special();
1015
+
1016
+ // zig fmt: off
1017
+ var atk: u32 =
1018
+ if (crit)
1019
+ if (special) side.stored().stats.spc
1020
+ else side.stored().stats.atk
1021
+ else
1022
+ if (special) side.active.stats.spc
1023
+ else side.active.stats.atk;
1024
+ var def: u32 =
1025
+ if (crit)
1026
+ if (special) target.stored().stats.spc
1027
+ else target.stored().stats.def
1028
+ else
1029
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1030
+ if (special)
1031
+ target.active.stats.spc *
1032
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1033
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1034
+ else
1035
+ target.active.stats.def *
1036
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1037
+ // zig fmt: on
1038
+
1039
+ // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1040
+ // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
1041
+ // attack is 366 from a level 100 Dragonite which has a max of 614,880 mid-calculation)
1042
+ if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1043
+ atk = @max((atk / 4) & 255, 1);
1044
+ // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1045
+ def = @max((def / 4) & 255, if (showdown) 1 else 0);
1046
+ }
1047
+
1048
+ const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1049
+
1050
+ def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1051
+
1052
+ if (def == 0) return false;
1053
+
1054
+ var d: u32 = (lvl * 2 / 5) + 2;
1055
+ d *%= @as(u32, move.bp);
1056
+ d *%= atk;
1057
+ d /= def;
1058
+ d /= 50;
1059
+ d = @min(997, d);
1060
+ d += 2;
1061
+
1062
+ battle.last_damage = @intCast(u16, d);
1063
+
1064
+ return true;
1065
+ }
1066
+
1067
+ fn adjustDamage(battle: anytype, player: Player) u16 {
1068
+ const side = battle.side(player);
1069
+ const foe = battle.foe(player);
1070
+ const types = foe.active.types;
1071
+ const move = Move.get(side.last_selected_move);
1072
+
1073
+ var d = battle.last_damage;
1074
+ if (side.active.types.includes(move.type)) d +%= d / 2;
1075
+
1076
+ const neutral = @enumToInt(Effectiveness.Neutral);
1077
+ const eff1: u16 = @enumToInt(move.type.effectiveness(types.type1));
1078
+ const eff2: u16 = @enumToInt(move.type.effectiveness(types.type2));
1079
+
1080
+ // Type effectiveness matchup precedence only matters with (NVE, SE)
1081
+ if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
1082
+ Type.precedence(move.type, types.type1) > Type.precedence(move.type, types.type2))
1083
+ {
1084
+ assert(eff2 != neutral);
1085
+ d = d *% eff2 / 10;
1086
+ assert(types.type1 != types.type2);
1087
+ assert(eff1 != neutral);
1088
+ d = d *% eff1 / 10;
1089
+ } else {
1090
+ if (eff1 != neutral) d = d *% eff1 / 10;
1091
+ if (types.type1 != types.type2 and eff2 != neutral) d = d *% eff2 / 10;
1092
+ }
1093
+
1094
+ battle.last_damage = d;
1095
+ return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
1096
+ }
1097
+
1098
+ fn randomizeDamage(battle: anytype) void {
1099
+ if (battle.last_damage <= 1) return;
1100
+
1101
+ const random = if (showdown)
1102
+ battle.rng.range(u8, 217, 256)
1103
+ else loop: {
1104
+ while (true) {
1105
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
1106
+ if (r >= 217) break :loop r;
1107
+ }
1108
+ };
1109
+
1110
+ battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1111
+ }
1112
+
1113
+ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1114
+ const side = battle.side(player);
1115
+ const foe = battle.foe(player);
1116
+
1117
+ if (!try checkHit(battle, player, move, log)) return null;
1118
+
1119
+ battle.last_damage = switch (side.last_selected_move) {
1120
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1121
+ .SeismicToss, .NightShade => side.stored().level,
1122
+ .SonicBoom => 20,
1123
+ .DragonRage => 40,
1124
+ // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1125
+ .Psywave => power: {
1126
+ const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1127
+ if (showdown) {
1128
+ break :power battle.rng.range(u8, 0, max);
1129
+ } else {
1130
+ // GLITCH: Psywave infinite glitch loop
1131
+ if (max <= 1) return Result.Error;
1132
+ while (true) {
1133
+ const r = battle.rng.next();
1134
+ if (r < max) break :power r;
1135
+ }
1136
+ }
1137
+ },
1138
+ else => unreachable,
1139
+ };
1140
+
1141
+ if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1142
+
1143
+ const sub = showdown and foe.active.volatiles.Substitute;
1144
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1145
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1146
+
1147
+ return null;
1148
+ }
1149
+
1150
+ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1151
+ const foe = battle.foe(player);
1152
+
1153
+ if (battle.last_damage == 0) {
1154
+ try log.fail(battle.active(player), .None);
1155
+ return null;
1156
+ }
1157
+
1158
+ // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1159
+ const foe_last_used_move =
1160
+ Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1161
+ const foe_last_selected_move =
1162
+ Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1163
+ .Counter
1164
+ else
1165
+ foe.last_selected_move);
1166
+
1167
+ const used = foe_last_used_move.bp > 0 and
1168
+ foe.last_used_move != .Counter and
1169
+ (foe_last_used_move.type == .Normal or
1170
+ foe_last_used_move.type == .Fighting);
1171
+
1172
+ const selected = foe_last_selected_move.bp > 0 and
1173
+ foe.last_selected_move != .Counter and
1174
+ (foe_last_selected_move.type == .Normal or
1175
+ foe_last_selected_move.type == .Fighting);
1176
+
1177
+ if (!used and !selected) {
1178
+ try log.fail(battle.active(player), .None);
1179
+ return null;
1180
+ }
1181
+
1182
+ if (!used or !selected) {
1183
+ // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1184
+ if (!showdown) return Result.Error;
1185
+ try log.fail(battle.active(player), .None);
1186
+ return null;
1187
+ }
1188
+
1189
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1190
+
1191
+ // Pokémon Showdown calls checkHit before Counter
1192
+ if (!showdown and !try checkHit(battle, player, move, log)) return null;
1193
+
1194
+ _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1195
+ return null;
1196
+ }
1197
+
1198
+ fn applyDamage(
1199
+ battle: anytype,
1200
+ target_player: Player,
1201
+ sub_player: Player,
1202
+ reason: Damage,
1203
+ log: anytype,
1204
+ ) !bool {
1205
+ assert(showdown or battle.last_damage != 0);
1206
+
1207
+ var target = battle.side(target_player);
1208
+ // GLITCH: Substitute + Confusion glitch
1209
+ // We check if the target has a Substitute but then apply damage to the "sub player" which
1210
+ // isn't guaranteed to be the same (e.g. crash or confusion damage) or to even have a Substitute
1211
+ if (target.active.volatiles.Substitute) {
1212
+ var subbed = battle.side(sub_player);
1213
+ if (!subbed.active.volatiles.Substitute) return false;
1214
+ if (battle.last_damage >= subbed.active.volatiles.substitute) {
1215
+ subbed.active.volatiles.substitute = 0;
1216
+ subbed.active.volatiles.Substitute = false;
1217
+ // battle.last_damage is not updated with the amount of HP the Substitute had
1218
+ try log.end(battle.active(sub_player), .Substitute);
1219
+ return true;
1220
+ } else {
1221
+ // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1222
+ subbed.active.volatiles.substitute -= @intCast(u8, battle.last_damage);
1223
+ try log.activate(battle.active(sub_player), .Substitute);
1224
+ return false;
1225
+ }
1226
+ }
1227
+
1228
+ if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1229
+ target.stored().hp -= battle.last_damage;
1230
+ try log.damage(battle.active(target_player), target.stored(), reason);
1231
+ return false;
1232
+ }
1233
+
1234
+ fn mirrorMove(
1235
+ battle: anytype,
1236
+ player: Player,
1237
+ mslot: u4,
1238
+ auto: bool,
1239
+ residual: *bool,
1240
+ log: anytype,
1241
+ ) !?Result {
1242
+ var side = battle.side(player);
1243
+ const foe = battle.foe(player);
1244
+
1245
+ side.last_selected_move = foe.last_used_move;
1246
+
1247
+ if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1248
+ try log.fail(battle.active(player), .None);
1249
+ return null;
1250
+ }
1251
+
1252
+ incrementPP(side, mslot);
1253
+
1254
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1255
+ return doMove(battle, player, mslot, auto, residual, log);
1256
+ }
1257
+
1258
+ fn metronome(
1259
+ battle: anytype,
1260
+ player: Player,
1261
+ mslot: u4,
1262
+ auto: bool,
1263
+ residual: *bool,
1264
+ log: anytype,
1265
+ ) !?Result {
1266
+ var side = battle.side(player);
1267
+
1268
+ side.last_selected_move = if (showdown) blk: {
1269
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1270
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1271
+ break :blk @intToEnum(Move, r + mod);
1272
+ } else loop: {
1273
+ while (true) {
1274
+ const r = battle.rng.next();
1275
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1276
+ if (r >= @enumToInt(Move.Struggle)) continue;
1277
+ break :loop @intToEnum(Move, r);
1278
+ }
1279
+ };
1280
+
1281
+ incrementPP(side, mslot);
1282
+
1283
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1284
+ return doMove(battle, player, mslot, auto, residual, log);
1285
+ }
1286
+
1287
+ fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
1288
+ var immune = false;
1289
+ var mist = false;
1290
+ if (moveHit(battle, player, move, &immune, &mist)) return true;
1291
+ assert(!immune);
1292
+ if (mist) {
1293
+ assert(!showdown);
1294
+ const foe_ident = battle.active(player.foe());
1295
+ try log.activate(foe_ident, .Mist);
1296
+ try log.fail(foe_ident, .None);
1297
+ } else {
1298
+ try log.lastmiss();
1299
+ try log.miss(battle.active(player));
1300
+ }
1301
+ return false;
1302
+ }
1303
+
1304
+ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist: *bool) bool {
1305
+ var side = battle.side(player);
1306
+ const foe = battle.foe(player);
1307
+
1308
+ var miss = miss: {
1309
+ assert(!side.active.volatiles.Bide);
1310
+
1311
+ // Invulnerability trumps everything on Pokémon Showdown
1312
+ if (showdown) {
1313
+ if (move.effect == .Swift) return true;
1314
+ if (foe.active.volatiles.Invulnerable) break :miss true;
1315
+ }
1316
+ if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1317
+ (showdown and foe.active.volatiles.Substitute)))
1318
+ {
1319
+ immune.* = true;
1320
+ if (showdown) return false;
1321
+ break :miss true;
1322
+ }
1323
+ if (!showdown) {
1324
+ if (move.effect == .Swift) return true;
1325
+ if (foe.active.volatiles.Invulnerable) break :miss true;
1326
+ }
1327
+
1328
+ // Hyper Beam + Sleep glitch needs to be special cased here due to control flow differences
1329
+ if (showdown and move.effect == .Sleep and foe.active.volatiles.Recharging) return true;
1330
+
1331
+ // Conversion / Haze / Light Screen / Reflect qualify but do not call moveHit
1332
+ if (foe.active.volatiles.Mist and move.effect.isStatDown()) {
1333
+ mist.* = true;
1334
+ if (!showdown) break :miss true;
1335
+ }
1336
+
1337
+ // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1338
+ const state = side.active.volatiles.state;
1339
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1340
+ const overwritten = overwrite and state > 0;
1341
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1342
+
1343
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1344
+ var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1345
+ accuracy = accuracy * boost[0] / boost[1];
1346
+ boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1347
+ accuracy = accuracy * boost[0] / boost[1];
1348
+ accuracy = @min(255, @max(1, accuracy));
1349
+
1350
+ // Pokémon Showdown only overwrites if the volatile is present
1351
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1352
+ if (overwrite) side.active.volatiles.state = accuracy;
1353
+
1354
+ // GLITCH: max accuracy is 255 so 1/256 chance of miss
1355
+ break :miss if (showdown)
1356
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1357
+ else
1358
+ battle.rng.next() >= accuracy;
1359
+ };
1360
+
1361
+ // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
1362
+ if (showdown and mist.*) {
1363
+ mist.* = !miss;
1364
+ miss = true;
1365
+ }
1366
+
1367
+ if (!miss) return true;
1368
+ battle.last_damage = 0;
1369
+ side.active.volatiles.Binding = false;
1370
+ return false;
1371
+ }
1372
+
1373
+ fn checkFaint(
1374
+ battle: anytype,
1375
+ player: Player,
1376
+ log: anytype,
1377
+ ) @TypeOf(log).Error!?Result {
1378
+ const side = battle.side(player);
1379
+ if (side.stored().hp > 0) return null;
1380
+
1381
+ const foe = battle.foe(player);
1382
+ const foe_fainted = foe.stored().hp == 0;
1383
+
1384
+ const player_out = findFirstAlive(side) == 0;
1385
+ const foe_out = findFirstAlive(foe) == 0;
1386
+ const tie = player_out and foe_out;
1387
+ const more = tie or player_out or foe_out;
1388
+
1389
+ if (try faint(battle, player, log, !(more or foe_fainted))) |r| return r;
1390
+ if (foe_fainted) if (try faint(battle, player.foe(), log, !more)) |r| return r;
1391
+
1392
+ assert(!side.active.volatiles.MultiHit);
1393
+ assert(!foe.active.volatiles.MultiHit);
1394
+
1395
+ if (tie) {
1396
+ try log.tie();
1397
+ return Result.Tie;
1398
+ } else if (player_out) {
1399
+ try log.win(player.foe());
1400
+ return if (player == .P1) Result.Lose else Result.Win;
1401
+ } else if (foe_out) {
1402
+ try log.win(player);
1403
+ return if (player == .P1) Result.Win else Result.Lose;
1404
+ }
1405
+
1406
+ const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1407
+ if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1408
+ return Result{ .p1 = foe_choice, .p2 = .Switch };
1409
+ }
1410
+
1411
+ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
1412
+ var side = battle.side(player);
1413
+ var foe = battle.foe(player);
1414
+ assert(side.stored().hp == 0);
1415
+
1416
+ var foe_volatiles = &foe.active.volatiles;
1417
+ assert(!foe_volatiles.MultiHit);
1418
+ if (foe_volatiles.Bide) {
1419
+ assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1420
+ foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1421
+ if (foe_volatiles.state != 0) return Result.Error;
1422
+ }
1423
+
1424
+ // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
1425
+ // just fainted will need to switch in a replacement and all of this gets cleared in switchIn
1426
+ // anyway. However, we would like to ensure the battle state presented to the players at each
1427
+ // decision point is consistent so we avoid "optimizing" this out
1428
+ // (we do always need to clear status though for Pokémon Showdown's Sleep/Freeze Clause Mod)
1429
+ side.active.volatiles = .{};
1430
+ side.last_used_move = .None;
1431
+ foe.last_used_move = .None;
1432
+ const status = side.stored().status;
1433
+
1434
+ side.stored().status = 0;
1435
+ // This shouldn't matter, but Pokémon Showdown decides double switching priority based on speed,
1436
+ // and resets a Pokémon's stats when it faints... only it still factors in paralysis -_-
1437
+ if (showdown) {
1438
+ side.active.stats.spe = if (Status.is(status, .PAR))
1439
+ @max(side.stored().stats.spe / 4, 1)
1440
+ else
1441
+ side.stored().stats.spe;
1442
+ }
1443
+
1444
+ try log.faint(battle.active(player), done);
1445
+ return null;
1446
+ }
1447
+
1448
+ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1449
+ var side = battle.side(player);
1450
+ var stored = side.stored();
1451
+ const ident = battle.active(player);
1452
+ var volatiles = &side.active.volatiles;
1453
+
1454
+ const brn = Status.is(stored.status, .BRN);
1455
+ if (brn or Status.is(stored.status, .PSN)) blk: {
1456
+ var damage = @max(stored.stats.hp / 16, 1);
1457
+
1458
+ if (volatiles.Toxic) {
1459
+ volatiles.toxic += 1;
1460
+ damage *= volatiles.toxic;
1461
+ }
1462
+
1463
+ const amount = @min(damage, stored.hp);
1464
+ if (showdown and amount == 0) break :blk;
1465
+
1466
+ stored.hp -= amount;
1467
+
1468
+ // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1469
+ try log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1470
+ }
1471
+
1472
+ if (volatiles.LeechSeed) {
1473
+ var foe = battle.foe(player);
1474
+ var foe_stored = foe.stored();
1475
+ const foe_ident = battle.active(player.foe());
1476
+
1477
+ if (foe_stored.hp == 0) {
1478
+ assert(showdown);
1479
+ return;
1480
+ }
1481
+
1482
+ var damage = @max(stored.stats.hp / 16, 1);
1483
+
1484
+ // GLITCH: Leech Seed + Toxic glitch
1485
+ if (volatiles.Toxic) {
1486
+ volatiles.toxic += 1;
1487
+ damage *= volatiles.toxic;
1488
+ }
1489
+
1490
+ const amount = @min(damage, stored.hp);
1491
+ stored.hp -= amount;
1492
+
1493
+ // As above, Pokémon Showdown uses damageOf but its not relevant
1494
+ if (amount > 0) {
1495
+ try log.damage(ident, stored, .LeechSeed);
1496
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1497
+ if (showdown) battle.last_damage = damage;
1498
+ }
1499
+
1500
+ const before = foe_stored.hp;
1501
+ // Uncapped damage is added back to the foe
1502
+ foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1503
+ // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1504
+ if (foe_stored.hp > before) try log.heal(foe_ident, foe_stored, .Silent);
1505
+ }
1506
+ }
1507
+
1508
+ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1509
+ assert(!battle.side(.P1).active.volatiles.MultiHit);
1510
+ assert(!battle.side(.P2).active.volatiles.MultiHit);
1511
+
1512
+ if (showdown and options.ebc and checkEBC(battle)) {
1513
+ try log.tie();
1514
+ return Result.Tie;
1515
+ }
1516
+
1517
+ battle.turn += 1;
1518
+
1519
+ if (showdown and battle.turn >= 1000) {
1520
+ try log.tie();
1521
+ return Result.Tie;
1522
+ } else if (battle.turn >= 65535) {
1523
+ return Result.Error;
1524
+ }
1525
+
1526
+ try log.turn(battle.turn);
1527
+
1528
+ return Result.Default;
1529
+ }
1530
+
1531
+ fn checkEBC(battle: anytype) bool {
1532
+ for (battle.sides, 0..) |side, i| {
1533
+ const foe = battle.sides[~@intCast(u1, i)];
1534
+
1535
+ var foe_all_ghosts = true;
1536
+ var foe_all_transform = true;
1537
+ for (foe.order, 0..) |id, j| {
1538
+ if (id == 0) break;
1539
+ const active = j == 0;
1540
+ const pokemon = foe.pokemon[id - 1];
1541
+
1542
+ const ghost = pokemon.hp == 0 or
1543
+ (if (active) foe.active.types else pokemon.types).includes(.Ghost);
1544
+ foe_all_ghosts = foe_all_ghosts and ghost;
1545
+ foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1546
+ for (if (active) foe.active.moves else pokemon.moves) |m| {
1547
+ if (m.id == .None) break :transform true;
1548
+ if (m.id != .Transform) break :transform false;
1549
+ }
1550
+ break :transform true;
1551
+ };
1552
+ }
1553
+
1554
+ for (side.order, 0..) |id, j| {
1555
+ if (id == 0) break;
1556
+ const active = j == 0;
1557
+ const pokemon = side.pokemon[id - 1];
1558
+
1559
+ if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1560
+ const transform = foe_all_transform and transform: {
1561
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1562
+ if (m.id == .None) break :transform true;
1563
+ if (m.id != .Transform) break :transform false;
1564
+ }
1565
+ break :transform true;
1566
+ };
1567
+ if (transform) continue;
1568
+ const no_pp = foe_all_ghosts and no_pp: {
1569
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1570
+ if (m.pp != 0) break :no_pp false;
1571
+ }
1572
+ break :no_pp true;
1573
+ };
1574
+ if (no_pp) continue;
1575
+
1576
+ return false;
1577
+ }
1578
+
1579
+ if (i == 1) return true;
1580
+ }
1581
+
1582
+ return false;
1583
+ }
1584
+
1585
+ inline fn onBegin(
1586
+ battle: anytype,
1587
+ player: Player,
1588
+ move: Move.Data,
1589
+ mslot: u8,
1590
+ residual: *bool,
1591
+ log: anytype,
1592
+ ) !void {
1593
+ assert(move.effect.onBegin());
1594
+ return switch (move.effect) {
1595
+ .Confusion => Effects.confusion(battle, player, move, log),
1596
+ .Conversion => Effects.conversion(battle, player, log),
1597
+ .FocusEnergy => Effects.focusEnergy(battle, player, log),
1598
+ .Haze => Effects.haze(battle, player, log),
1599
+ .Heal => Effects.heal(battle, player, log),
1600
+ .LeechSeed => Effects.leechSeed(battle, player, move, log),
1601
+ .LightScreen => Effects.lightScreen(battle, player, log),
1602
+ .Mimic => Effects.mimic(battle, player, move, mslot, log),
1603
+ .Mist => Effects.mist(battle, player, log),
1604
+ .Paralyze => Effects.paralyze(battle, player, move, log),
1605
+ .Poison => Effects.poison(battle, player, move, log),
1606
+ .Reflect => Effects.reflect(battle, player, log),
1607
+ .Splash => Effects.splash(battle, player, log),
1608
+ .Substitute => Effects.substitute(battle, player, residual, log),
1609
+ .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1610
+ .Transform => Effects.transform(battle, player, log),
1611
+ else => unreachable,
1612
+ };
1613
+ }
1614
+
1615
+ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1616
+ assert(move.effect.onEnd());
1617
+ return switch (move.effect) {
1618
+ .Bide => Effects.bide(battle, player, log),
1619
+ // zig fmt: off
1620
+ .AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
1621
+ .EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
1622
+ Effects.boost(battle, player, move, log),
1623
+ .AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
1624
+ Effects.unboost(battle, player, move, log),
1625
+ // zig fmt: on
1626
+ .Sleep => Effects.sleep(battle, player, move, log),
1627
+ else => unreachable,
1628
+ };
1629
+ }
1630
+
1631
+ inline fn alwaysHappens(
1632
+ battle: anytype,
1633
+ player: Player,
1634
+ move: Move.Data,
1635
+ residual: *bool,
1636
+ log: anytype,
1637
+ ) !void {
1638
+ assert(move.effect.alwaysHappens());
1639
+ return switch (move.effect) {
1640
+ .DrainHP, .DreamEater => Effects.drainHP(battle, player, log),
1641
+ .Explode => Effects.explode(battle, player),
1642
+ .PayDay => Effects.payDay(battle, player, log),
1643
+ .Rage => Effects.rage(battle, player),
1644
+ .Recoil => Effects.recoil(battle, player, residual, log),
1645
+ .JumpKick, .Binding => {},
1646
+ else => unreachable,
1647
+ };
1648
+ }
1649
+
1650
+ inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1651
+ return switch (move.effect) {
1652
+ .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, log),
1653
+ .ConfusionChance => Effects.confusion(battle, player, move, log),
1654
+ .FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move),
1655
+ .FreezeChance => Effects.freezeChance(battle, player, move, log),
1656
+ .HyperBeam => Effects.hyperBeam(battle, player, log),
1657
+ .MultiHit, .DoubleHit, .Twineedle => unreachable,
1658
+ .ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, log),
1659
+ .PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, log),
1660
+ // zig fmt: off
1661
+ .AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
1662
+ Effects.unboost(battle, player, move, log),
1663
+ // zig fmt: on
1664
+ else => {},
1665
+ };
1666
+ }
1667
+
1668
+ pub const Effects = struct {
1669
+ fn bide(battle: anytype, player: Player, log: anytype) !void {
1670
+ var side = battle.side(player);
1671
+
1672
+ side.active.volatiles.Bide = true;
1673
+ assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1674
+ side.active.volatiles.state = 0;
1675
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1676
+ battle.rng.range(u4, 2, 4)
1677
+ else
1678
+ (battle.rng.next() & 1) + 2);
1679
+
1680
+ try log.start(battle.active(player), .Bide);
1681
+ }
1682
+
1683
+ fn burnChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1684
+ var foe = battle.foe(player);
1685
+ var foe_stored = foe.stored();
1686
+
1687
+ if (foe.active.volatiles.Substitute) return;
1688
+
1689
+ if (Status.any(foe_stored.status)) {
1690
+ if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1691
+ // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1692
+ if (Status.is(foe_stored.status, .FRZ)) {
1693
+ assert(move.type == .Fire);
1694
+ try log.curestatus(battle.active(player.foe()), foe_stored.status, .Message);
1695
+ foe_stored.status = 0;
1696
+ }
1697
+ return;
1698
+ }
1699
+
1700
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1701
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1702
+ else
1703
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1704
+ Gen12.percent(10)
1705
+ else
1706
+ Gen12.percent(30));
1707
+ if (!chance) return;
1708
+
1709
+ foe_stored.status = Status.init(.BRN);
1710
+ foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1711
+
1712
+ try log.status(battle.active(player.foe()), foe_stored.status, .None);
1713
+ }
1714
+
1715
+ fn charge(battle: anytype, player: Player, log: anytype) !void {
1716
+ var side = battle.side(player);
1717
+ var volatiles = &side.active.volatiles;
1718
+
1719
+ volatiles.Charging = true;
1720
+ const move = side.last_selected_move;
1721
+ if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1722
+ try log.laststill();
1723
+ try log.prepare(battle.active(player), move);
1724
+ }
1725
+
1726
+ fn confusion(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1727
+ var foe = battle.foe(player);
1728
+ const sub = foe.active.volatiles.Substitute;
1729
+
1730
+ if (move.effect == .ConfusionChance) {
1731
+ const chance = if (showdown)
1732
+ battle.rng.chance(u8, 25, 256)
1733
+ else
1734
+ battle.rng.next() < Gen12.percent(10);
1735
+ if (!chance) return;
1736
+ } else {
1737
+ if (showdown) {
1738
+ if (!try checkHit(battle, player, move, log)) {
1739
+ return;
1740
+ } else if (sub) {
1741
+ return log.fail(battle.active(player.foe()), .None);
1742
+ }
1743
+ } else {
1744
+ if (sub) {
1745
+ return log.fail(battle.active(player.foe()), .None);
1746
+ } else if (!try checkHit(battle, player, move, log)) {
1747
+ return;
1748
+ }
1749
+ }
1750
+ }
1751
+
1752
+ if (foe.active.volatiles.Confusion) return;
1753
+ foe.active.volatiles.Confusion = true;
1754
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1755
+ battle.rng.range(u8, 2, 6)
1756
+ else
1757
+ (battle.rng.next() & 3) + 2);
1758
+
1759
+ try log.start(battle.active(player.foe()), .Confusion);
1760
+ }
1761
+
1762
+ fn conversion(battle: anytype, player: Player, log: anytype) !void {
1763
+ const foe = battle.foe(player);
1764
+
1765
+ if (foe.active.volatiles.Invulnerable) {
1766
+ try log.lastmiss();
1767
+ return log.miss(battle.active(player));
1768
+ }
1769
+
1770
+ battle.side(player).active.types = foe.active.types;
1771
+ return log.typechange(battle.active(player), foe.active.types, battle.active(player.foe()));
1772
+ }
1773
+
1774
+ fn disable(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1775
+ var foe = battle.foe(player);
1776
+ var volatiles = &foe.active.volatiles;
1777
+ const foe_ident = battle.active(player.foe());
1778
+
1779
+ // Pokémon Showdown handles hit/miss earlier in doMove
1780
+ if (!showdown and !try checkHit(battle, player, move, log)) return null;
1781
+
1782
+ if (volatiles.disabled_move != 0) {
1783
+ try log.fail(foe_ident, .None);
1784
+ return null;
1785
+ }
1786
+
1787
+ var n: u4 = 0;
1788
+ var err = true;
1789
+ for (foe.active.moves) |m| {
1790
+ if (m.pp > 0) {
1791
+ n += 1;
1792
+ if (m.id != .None) err = false;
1793
+ }
1794
+ }
1795
+
1796
+ // Technically this is still considered simply a "miss" on the cartridge,
1797
+ // but diverging from Pokémon Showdown here would mostly just be pedantic
1798
+ if (n == 0) {
1799
+ try log.fail(foe_ident, .None);
1800
+ return null;
1801
+ } else if (err) {
1802
+ // GLITCH: Transform + Mirror Move / Metronome PP softlock
1803
+ assert(!showdown);
1804
+ return Result.Error;
1805
+ }
1806
+
1807
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1808
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1809
+ battle.rng.range(u8, 1, 9)
1810
+ else
1811
+ (battle.rng.next() & 7) + 1);
1812
+
1813
+ try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1814
+ return null;
1815
+ }
1816
+
1817
+ fn drainHP(battle: anytype, player: Player, log: anytype) !void {
1818
+ var side = battle.side(player);
1819
+ var stored = side.stored();
1820
+
1821
+ if (battle.last_damage == 0) {
1822
+ assert(showdown);
1823
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1824
+ }
1825
+
1826
+ const drain = @max(battle.last_damage / 2, 1);
1827
+ battle.last_damage = drain;
1828
+
1829
+ if (stored.hp == stored.stats.hp) return;
1830
+ stored.hp = @min(stored.stats.hp, stored.hp + drain);
1831
+
1832
+ try log.drain(battle.active(player), stored, battle.active(player.foe()));
1833
+ }
1834
+
1835
+ fn explode(battle: anytype, player: Player) !void {
1836
+ var side = battle.side(player);
1837
+ var stored = side.stored();
1838
+
1839
+ stored.hp = 0;
1840
+ // Pokémon Showdown sets the status to 0 on faint(), and we need to do the same to be able
1841
+ // to correctly implement Pokémon Showdown's dumb speed-based switch mechanics
1842
+ if (!showdown) stored.status = 0;
1843
+ side.active.volatiles.LeechSeed = false;
1844
+ }
1845
+
1846
+ fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
1847
+ var volatiles = &battle.foe(player).active.volatiles;
1848
+
1849
+ if (volatiles.Substitute) return;
1850
+
1851
+ const chance = if (showdown)
1852
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1853
+ else
1854
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1855
+ Gen12.percent(10)
1856
+ else
1857
+ Gen12.percent(30));
1858
+ if (!chance) return;
1859
+
1860
+ volatiles.Flinch = true;
1861
+ volatiles.Recharging = false;
1862
+ }
1863
+
1864
+ fn focusEnergy(battle: anytype, player: Player, log: anytype) !void {
1865
+ var side = battle.side(player);
1866
+
1867
+ if (side.active.volatiles.FocusEnergy) return;
1868
+ side.active.volatiles.FocusEnergy = true;
1869
+
1870
+ try log.start(battle.active(player), .FocusEnergy);
1871
+ }
1872
+
1873
+ fn freezeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1874
+ var foe = battle.foe(player);
1875
+ var foe_stored = foe.stored();
1876
+ const foe_ident = battle.active(player.foe());
1877
+
1878
+ if (foe.active.volatiles.Substitute) return;
1879
+
1880
+ if (Status.any(foe_stored.status)) {
1881
+ return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1882
+ }
1883
+
1884
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1885
+ battle.rng.chance(u8, 26, 256)
1886
+ else
1887
+ battle.rng.next() < 1 + Gen12.percent(10);
1888
+ if (!chance) return;
1889
+
1890
+ // Freeze Clause Mod
1891
+ if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1892
+
1893
+ foe_stored.status = Status.init(.FRZ);
1894
+ // GLITCH: Hyper Beam recharging status is not cleared
1895
+
1896
+ try log.status(foe_ident, foe_stored.status, .None);
1897
+ }
1898
+
1899
+ fn haze(battle: anytype, player: Player, log: anytype) !void {
1900
+ var side = battle.side(player);
1901
+ var foe = battle.foe(player);
1902
+
1903
+ var side_stored = side.stored();
1904
+ var foe_stored = foe.stored();
1905
+
1906
+ const player_ident = battle.active(player);
1907
+ const foe_ident = battle.active(player.foe());
1908
+
1909
+ side.active.boosts = .{};
1910
+ foe.active.boosts = .{};
1911
+
1912
+ side.active.stats = side_stored.stats;
1913
+ foe.active.stats = foe_stored.stats;
1914
+
1915
+ try log.activate(player_ident, .Haze);
1916
+ try log.clearallboost();
1917
+
1918
+ // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1919
+ if (showdown) {
1920
+ for (&battle.sides, 0..) |*s, i| {
1921
+ const p = @intToEnum(Player, i);
1922
+ // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1923
+ if (p != player and Status.any(s.stored().status)) {
1924
+ try log.curestatus(foe_ident, foe_stored.status, .Silent);
1925
+ s.stored().status = 0;
1926
+ } else if (showdown and s.stored().status == Status.TOX) {
1927
+ s.stored().status = Status.init(.PSN);
1928
+ try log.status(battle.active(p), s.stored().status, .None);
1929
+ }
1930
+ try clearVolatiles(battle, p, log);
1931
+ }
1932
+ } else {
1933
+ if (Status.any(foe_stored.status)) {
1934
+ if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1935
+ foe.last_selected_move = .SKIP_TURN;
1936
+ }
1937
+ try log.curestatus(foe_ident, foe_stored.status, .Silent);
1938
+ foe_stored.status = 0;
1939
+ }
1940
+ try clearVolatiles(battle, player, log);
1941
+ try clearVolatiles(battle, player.foe(), log);
1942
+ }
1943
+ }
1944
+
1945
+ fn heal(battle: anytype, player: Player, log: anytype) !void {
1946
+ var side = battle.side(player);
1947
+ var stored = side.stored();
1948
+ const ident = battle.active(player);
1949
+
1950
+ // GLITCH: HP recovery move failure glitches
1951
+ const delta = stored.stats.hp - stored.hp;
1952
+ if (delta == 0 or delta & 255 == 255) return try log.fail(ident, .None);
1953
+
1954
+ const rest = side.last_selected_move == .Rest;
1955
+ if (rest) {
1956
+ // Adding the sleep status runs the sleep condition handler to roll duration
1957
+ if (showdown) battle.rng.advance(1);
1958
+ stored.status = Status.slf(2);
1959
+ try log.statusFrom(ident, stored.status, Move.Rest);
1960
+ stored.hp = stored.stats.hp;
1961
+ } else {
1962
+ stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
1963
+ }
1964
+ try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
1965
+ }
1966
+
1967
+ fn hyperBeam(battle: anytype, player: Player, log: anytype) !void {
1968
+ battle.side(player).active.volatiles.Recharging = true;
1969
+ try log.mustrecharge(battle.active(player));
1970
+ }
1971
+
1972
+ fn leechSeed(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
1973
+ var foe = battle.foe(player);
1974
+
1975
+ if (showdown) {
1976
+ // Invulnerability trumps type immunity on Pokémon Showdown
1977
+ if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
1978
+ return log.immune(battle.active(player.foe()), .None);
1979
+ }
1980
+ if (!try checkHit(battle, player, move, log)) return;
1981
+ if (foe.active.volatiles.LeechSeed) return;
1982
+ } else {
1983
+ if (!try checkHit(battle, player, move, log)) return;
1984
+ if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
1985
+ try log.lastmiss();
1986
+ return log.miss(battle.active(player));
1987
+ }
1988
+ }
1989
+
1990
+ foe.active.volatiles.LeechSeed = true;
1991
+
1992
+ try log.start(battle.active(player.foe()), .LeechSeed);
1993
+ }
1994
+
1995
+ fn lightScreen(battle: anytype, player: Player, log: anytype) !void {
1996
+ var side = battle.side(player);
1997
+
1998
+ if (side.active.volatiles.LightScreen) return log.fail(battle.active(player), .None);
1999
+ side.active.volatiles.LightScreen = true;
2000
+
2001
+ try log.start(battle.active(player), .LightScreen);
2002
+ }
2003
+
2004
+ fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
2005
+ var side = battle.side(player);
2006
+ var foe = battle.foe(player);
2007
+
2008
+ // Pokémon Showdown incorrectly requires the user to have Mimic (but not necessarily at
2009
+ // mslot). In reality, Mimic can also be called via Metronome or Mirror Move
2010
+ assert(showdown or side.active.move(mslot).id == .Mimic or
2011
+ side.active.move(mslot).id == .Metronome or
2012
+ side.active.move(mslot).id == .MirrorMove);
2013
+
2014
+ // Pokémon Showdown incorrectly replaces the existing Mimic's slot instead of mslot
2015
+ var oslot = mslot;
2016
+ if (showdown) {
2017
+ const has_mimic = has_mimic: {
2018
+ for (side.active.moves, 0..) |m, i| {
2019
+ if (m.id == .Mimic) {
2020
+ oslot = @intCast(u8, i + 1);
2021
+ break :has_mimic true;
2022
+ }
2023
+ }
2024
+ break :has_mimic false;
2025
+ };
2026
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
2027
+ // trigger |-miss| instead of |-fail|
2028
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2029
+ return try log.fail(battle.active(player.foe()), .None);
2030
+ }
2031
+ }
2032
+ if (!try checkHit(battle, player, move, log)) return;
2033
+
2034
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2035
+ side.active.move(oslot).id = foe.active.move(rslot).id;
2036
+
2037
+ try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2038
+ }
2039
+
2040
+ fn mist(battle: anytype, player: Player, log: anytype) !void {
2041
+ var side = battle.side(player);
2042
+
2043
+ if (side.active.volatiles.Mist) return;
2044
+ side.active.volatiles.Mist = true;
2045
+
2046
+ try log.start(battle.active(player), .Mist);
2047
+ }
2048
+
2049
+ fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
2050
+ var side = battle.side(player);
2051
+
2052
+ assert(!side.active.volatiles.MultiHit);
2053
+ side.active.volatiles.MultiHit = true;
2054
+
2055
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2056
+ }
2057
+
2058
+ fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2059
+ var foe = battle.foe(player);
2060
+ var foe_stored = foe.stored();
2061
+ const foe_ident = battle.active(player.foe());
2062
+
2063
+ // Only Thunder Wave checks for type-immunity, not Glare
2064
+ const immune = move.type == .Electric and foe.active.types.immune(move.type);
2065
+
2066
+ if (showdown) {
2067
+ // Invulnerability trumps type immunity on Pokémon Showdown
2068
+ if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2069
+ if (!try checkHit(battle, player, move, log)) return;
2070
+ }
2071
+ if (Status.any(foe_stored.status)) {
2072
+ return log.fail(
2073
+ foe_ident,
2074
+ if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2075
+ );
2076
+ }
2077
+ if (!showdown) {
2078
+ if (immune) return log.immune(foe_ident, .None);
2079
+ if (!try checkHit(battle, player, move, log)) return;
2080
+ }
2081
+
2082
+ foe_stored.status = Status.init(.PAR);
2083
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2084
+
2085
+ try log.status(foe_ident, foe_stored.status, .None);
2086
+ }
2087
+
2088
+ fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2089
+ var foe = battle.foe(player);
2090
+ var foe_stored = foe.stored();
2091
+
2092
+ if (foe.active.volatiles.Substitute) return;
2093
+
2094
+ if (Status.any(foe_stored.status)) {
2095
+ return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
2096
+ }
2097
+
2098
+ // Body Slam can't paralyze a Normal type Pokémon
2099
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2100
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2101
+ else
2102
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2103
+ Gen12.percent(10)
2104
+ else
2105
+ Gen12.percent(30));
2106
+ if (!chance) return;
2107
+
2108
+ foe_stored.status = Status.init(.PAR);
2109
+ foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2110
+
2111
+ try log.status(battle.active(player.foe()), foe_stored.status, .None);
2112
+ }
2113
+
2114
+ fn payDay(battle: anytype, player: Player, log: anytype) !void {
2115
+ if (!showdown or !battle.foe(player).active.volatiles.Substitute) try log.fieldactivate();
2116
+ }
2117
+
2118
+ fn poison(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2119
+ var foe = battle.foe(player);
2120
+ var foe_stored = foe.stored();
2121
+ const foe_ident = battle.active(player.foe());
2122
+ const toxic = battle.side(player).last_selected_move == .Toxic;
2123
+
2124
+ if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, log)) {
2125
+ return;
2126
+ } else if (foe.active.volatiles.Substitute) {
2127
+ if (move.effect != .Poison) return;
2128
+ return log.fail(foe_ident, .None);
2129
+ } else if (Status.any(foe_stored.status)) {
2130
+ if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2131
+ // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2132
+ // will not include a fail reason for Toxic vs. Poison or vice-versa...
2133
+ return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2134
+ if (!showdown)
2135
+ .Poison
2136
+ else if (toxic == (foe_stored.status == Status.TOX))
2137
+ if (toxic) .Toxic else .Poison
2138
+ else
2139
+ .None
2140
+ else
2141
+ .None);
2142
+ } else if (foe.active.types.includes(.Poison)) {
2143
+ if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2144
+ return log.immune(foe_ident, .None);
2145
+ }
2146
+
2147
+ if (move.effect == .Poison) {
2148
+ if (!showdown and !try checkHit(battle, player, move, log)) return;
2149
+ } else {
2150
+ const chance = if (showdown)
2151
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2152
+ else
2153
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2154
+ Gen12.percent(20)
2155
+ else
2156
+ Gen12.percent(40));
2157
+ if (!chance) return;
2158
+ }
2159
+
2160
+ foe_stored.status = Status.init(.PSN);
2161
+ if (toxic) {
2162
+ if (showdown) foe_stored.status = Status.TOX;
2163
+ foe.active.volatiles.Toxic = true;
2164
+ foe.active.volatiles.toxic = 0;
2165
+ }
2166
+
2167
+ try log.status(foe_ident, foe_stored.status, .None);
2168
+ }
2169
+
2170
+ fn rage(battle: anytype, player: Player) !void {
2171
+ var volatiles = &battle.side(player).active.volatiles;
2172
+ assert(!volatiles.Bide);
2173
+ volatiles.Rage = true;
2174
+ }
2175
+
2176
+ fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2177
+ var side = battle.side(player);
2178
+ var stored = side.stored();
2179
+
2180
+ const damage = @intCast(i16, @max(battle.last_damage /
2181
+ @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2182
+ stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
2183
+
2184
+ try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
2185
+ if (showdown and stored.hp == 0) residual.* = false;
2186
+ }
2187
+
2188
+ fn reflect(battle: anytype, player: Player, log: anytype) !void {
2189
+ var side = battle.side(player);
2190
+
2191
+ if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
2192
+ side.active.volatiles.Reflect = true;
2193
+
2194
+ try log.start(battle.active(player), .Reflect);
2195
+ }
2196
+
2197
+ fn sleep(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2198
+ var foe = battle.foe(player);
2199
+ var foe_stored = foe.stored();
2200
+ const foe_ident = battle.active(player.foe());
2201
+
2202
+ if (foe.active.volatiles.Recharging) {
2203
+ // Hit test not applied if the target is recharging (bypass)
2204
+ // The volatile itself actually gets cleared below since on Pokémon Showdown
2205
+ // the Sleep Clause Mod might activate, causing us to not actually bypass
2206
+ } else {
2207
+ if (Status.any(foe_stored.status)) {
2208
+ return log.fail(
2209
+ foe_ident,
2210
+ if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2211
+ );
2212
+ }
2213
+ // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2214
+ if (!showdown and !try checkHit(battle, player, move, log)) return;
2215
+ }
2216
+
2217
+ // Sleep Clause Mod
2218
+ if (showdown) {
2219
+ for (foe.pokemon) |p| {
2220
+ if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2221
+ }
2222
+ }
2223
+ foe.active.volatiles.Recharging = false;
2224
+
2225
+ const duration = @intCast(u3, if (showdown)
2226
+ battle.rng.range(u8, 1, 8)
2227
+ else loop: {
2228
+ while (true) {
2229
+ const r = battle.rng.next() & 7;
2230
+ if (r != 0) break :loop r;
2231
+ }
2232
+ });
2233
+
2234
+ foe_stored.status = Status.slp(duration);
2235
+ try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2236
+ }
2237
+
2238
+ fn splash(battle: anytype, player: Player, log: anytype) !void {
2239
+ try log.activate(battle.active(player), .Splash);
2240
+ }
2241
+
2242
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2243
+ var side = battle.side(player);
2244
+ if (side.active.volatiles.Substitute) {
2245
+ try log.fail(battle.active(player), .Substitute);
2246
+ return;
2247
+ }
2248
+
2249
+ assert(side.stored().stats.hp <= 1023);
2250
+ // Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
2251
+ const hp = @intCast(u8, side.stored().stats.hp / 4);
2252
+ // Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
2253
+ // a floating point value and thus only correctly implements the Substitute 1/4 glitch when
2254
+ // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2255
+ // avoid having to convert to floating point)
2256
+ const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2257
+ if (side.stored().hp < required_hp) {
2258
+ try log.fail(battle.active(player), .Weak);
2259
+ return;
2260
+ }
2261
+
2262
+ // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2263
+ side.stored().hp -= hp;
2264
+ side.active.volatiles.substitute = hp + 1;
2265
+ side.active.volatiles.Substitute = true;
2266
+ try log.start(battle.active(player), .Substitute);
2267
+ if (hp > 0) {
2268
+ try log.damage(battle.active(player), side.stored(), .None);
2269
+ if (showdown and side.stored().hp == 0) residual.* = false;
2270
+ }
2271
+ }
2272
+
2273
+ fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2274
+ if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2275
+
2276
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2277
+ _ = try checkHit(battle, player, move, log);
2278
+ battle.last_damage = 0;
2279
+ }
2280
+
2281
+ fn thrashing(battle: anytype, player: Player) void {
2282
+ var volatiles = &battle.side(player).active.volatiles;
2283
+ assert(!volatiles.Thrashing);
2284
+ assert(!volatiles.Bide);
2285
+
2286
+ volatiles.Thrashing = true;
2287
+ volatiles.attacks = @intCast(u3, if (showdown)
2288
+ battle.rng.range(u8, 2, 4)
2289
+ else
2290
+ (battle.rng.next() & 1) + 2);
2291
+ }
2292
+
2293
+ fn transform(battle: anytype, player: Player, log: anytype) !void {
2294
+ var side = battle.side(player);
2295
+ const foe = battle.foe(player);
2296
+ const foe_ident = battle.active(player.foe());
2297
+
2298
+ side.active.volatiles.Transform = true;
2299
+ // foe could themselves be transformed
2300
+ side.active.volatiles.transform = if (foe.active.volatiles.transform != 0)
2301
+ foe.active.volatiles.transform
2302
+ else
2303
+ foe_ident.int();
2304
+
2305
+ // HP is not copied by Transform
2306
+ side.active.stats.atk = foe.active.stats.atk;
2307
+ side.active.stats.def = foe.active.stats.def;
2308
+ side.active.stats.spe = foe.active.stats.spe;
2309
+ side.active.stats.spc = foe.active.stats.spc;
2310
+
2311
+ side.active.species = foe.active.species;
2312
+ side.active.types = foe.active.types;
2313
+ side.active.boosts = foe.active.boosts;
2314
+ for (foe.active.moves, 0..) |m, i| {
2315
+ side.active.moves[i].id = m.id;
2316
+ side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2317
+ }
2318
+
2319
+ try log.transform(battle.active(player), foe_ident);
2320
+ }
2321
+
2322
+ fn binding(battle: anytype, player: Player) void {
2323
+ var side = battle.side(player);
2324
+ var foe = battle.foe(player);
2325
+
2326
+ if (side.active.volatiles.Binding) return;
2327
+ side.active.volatiles.Binding = true;
2328
+ // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2329
+ // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2330
+ if (!showdown) foe.active.volatiles.Recharging = false;
2331
+
2332
+ side.active.volatiles.attacks = distribution(battle) - 1;
2333
+ }
2334
+
2335
+ fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2336
+ var side = battle.side(player);
2337
+ const ident = battle.active(player);
2338
+
2339
+ var stats = &side.active.stats;
2340
+ var boosts = &side.active.boosts;
2341
+
2342
+ switch (move.effect) {
2343
+ .AttackUp1, .AttackUp2, .Rage => {
2344
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
2345
+ if (boosts.atk == 6) return try log.fail(ident, .None);
2346
+ const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2347
+ boosts.atk = @intCast(i4, @min(6, @as(i8, boosts.atk) + n));
2348
+ const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2349
+ if (stats.atk == MAX_STAT_VALUE) {
2350
+ boosts.atk -= 1;
2351
+ if (showdown) {
2352
+ try log.boost(ident, reason, n);
2353
+ try log.boost(ident, Boost.Attack, -1);
2354
+ if (move.effect == .Rage) return;
2355
+ }
2356
+ return try log.fail(ident, .None);
2357
+ }
2358
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2359
+ const stat = unmodifiedStats(battle, player).atk;
2360
+ stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2361
+ try log.boost(ident, reason, n);
2362
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2363
+ if (showdown and move.effect == .Rage) return;
2364
+ },
2365
+ .DefenseUp1, .DefenseUp2 => {
2366
+ assert(boosts.def >= -6 and boosts.def <= 6);
2367
+ if (boosts.def == 6) return try log.fail(ident, .None);
2368
+ const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2369
+ boosts.def = @intCast(i4, @min(6, @as(i8, boosts.def) + n));
2370
+ if (stats.def == MAX_STAT_VALUE) {
2371
+ boosts.def -= 1;
2372
+ if (showdown) {
2373
+ try log.boost(ident, .Defense, n);
2374
+ try log.boost(ident, .Defense, -1);
2375
+ }
2376
+ return try log.fail(ident, .None);
2377
+ }
2378
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2379
+ const stat = unmodifiedStats(battle, player).def;
2380
+ stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2381
+ try log.boost(ident, .Defense, n);
2382
+ },
2383
+ .SpeedUp2 => {
2384
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
2385
+ if (boosts.spe == 6) return try log.fail(ident, .None);
2386
+ boosts.spe = @intCast(i4, @min(6, @as(i8, boosts.spe) + 2));
2387
+ if (stats.spe == MAX_STAT_VALUE) {
2388
+ boosts.spe -= 1;
2389
+ if (showdown) {
2390
+ try log.boost(ident, .Speed, 2);
2391
+ try log.boost(ident, .Speed, -1);
2392
+ }
2393
+ return try log.fail(ident, .None);
2394
+ }
2395
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2396
+ const stat = unmodifiedStats(battle, player).spe;
2397
+ stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2398
+ try log.boost(ident, .Speed, 2);
2399
+ },
2400
+ .SpecialUp1, .SpecialUp2 => {
2401
+ assert(boosts.spc >= -6 and boosts.spc <= 6);
2402
+ if (boosts.spc == 6) return try log.fail(ident, .None);
2403
+ const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2404
+ boosts.spc = @intCast(i4, @min(6, @as(i8, boosts.spc) + n));
2405
+ if (stats.spc == MAX_STAT_VALUE) {
2406
+ boosts.spc -= 1;
2407
+ if (showdown) {
2408
+ try log.boost(ident, .SpecialAttack, n);
2409
+ try log.boost(ident, .SpecialAttack, -1);
2410
+ try log.boost(ident, .SpecialDefense, n);
2411
+ try log.boost(ident, .SpecialDefense, -1);
2412
+ }
2413
+ return try log.fail(ident, .None);
2414
+ }
2415
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2416
+ const stat = unmodifiedStats(battle, player).spc;
2417
+ stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2418
+ try log.boost(ident, .SpecialAttack, n);
2419
+ try log.boost(ident, .SpecialDefense, n);
2420
+ },
2421
+ .EvasionUp1 => {
2422
+ assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2423
+ if (boosts.evasion == 6) return try log.fail(ident, .None);
2424
+ boosts.evasion = @intCast(i4, @min(6, @as(i8, boosts.evasion) + 1));
2425
+ try log.boost(ident, .Evasion, 1);
2426
+ },
2427
+ else => unreachable,
2428
+ }
2429
+
2430
+ // GLITCH: Stat modification errors glitch
2431
+ statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
2432
+ }
2433
+
2434
+ fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2435
+ var foe = battle.foe(player);
2436
+ const foe_ident = battle.active(player.foe());
2437
+
2438
+ if (foe.active.volatiles.Substitute) {
2439
+ return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2440
+ }
2441
+
2442
+ if (move.effect.isStatDownChance()) {
2443
+ const chance = if (showdown)
2444
+ battle.rng.chance(u8, 85, 256)
2445
+ else
2446
+ battle.rng.next() < Gen12.percent(33) + 1;
2447
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2448
+ } else if (!showdown and !try checkHit(battle, player, move, log)) {
2449
+ return; // checkHit already checks for Invulnerable
2450
+ }
2451
+
2452
+ var stats = &foe.active.stats;
2453
+ var boosts = &foe.active.boosts;
2454
+
2455
+ switch (move.effect) {
2456
+ .AttackDown1, .AttackDownChance => {
2457
+ assert(boosts.atk >= -6 and boosts.atk <= 6);
2458
+ if (boosts.atk == -6) return try log.fail(foe_ident, .None);
2459
+ boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
2460
+ if (stats.atk == 1) {
2461
+ boosts.atk += 1;
2462
+ if (showdown) {
2463
+ try log.boost(foe_ident, .Attack, -1);
2464
+ try log.boost(foe_ident, .Attack, 1);
2465
+ }
2466
+ return try log.fail(foe_ident, .None);
2467
+ }
2468
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2469
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2470
+ stats.atk = @max(1, stat * mod[0] / mod[1]);
2471
+ try log.boost(foe_ident, .Attack, -1);
2472
+ },
2473
+ .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2474
+ assert(boosts.def >= -6 and boosts.def <= 6);
2475
+ if (boosts.def == -6) return try log.fail(foe_ident, .None);
2476
+ const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2477
+ boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2478
+ if (stats.def == 1) {
2479
+ boosts.def += 1;
2480
+ if (showdown) {
2481
+ try log.boost(foe_ident, .Defense, -@as(i8, n));
2482
+ try log.boost(foe_ident, .Defense, 1);
2483
+ }
2484
+ return try log.fail(foe_ident, .None);
2485
+ }
2486
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2487
+ const stat = unmodifiedStats(battle, player.foe()).def;
2488
+ stats.def = @max(1, stat * mod[0] / mod[1]);
2489
+ try log.boost(foe_ident, .Defense, -@as(i8, n));
2490
+ },
2491
+ .SpeedDown1, .SpeedDownChance => {
2492
+ assert(boosts.spe >= -6 and boosts.spe <= 6);
2493
+ if (boosts.spe == -6) return try log.fail(foe_ident, .None);
2494
+ boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
2495
+ if (stats.spe == 1) {
2496
+ boosts.spe += 1;
2497
+ if (showdown) {
2498
+ try log.boost(foe_ident, .Speed, -1);
2499
+ try log.boost(foe_ident, .Speed, 1);
2500
+ }
2501
+ return try log.fail(foe_ident, .None);
2502
+ }
2503
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2504
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2505
+ stats.spe = @max(1, stat * mod[0] / mod[1]);
2506
+ try log.boost(foe_ident, .Speed, -1);
2507
+ assert(boosts.spe >= -6);
2508
+ },
2509
+ .SpecialDownChance => {
2510
+ assert(boosts.spc >= -6 and boosts.spc <= 6);
2511
+ if (boosts.spc == -6) return try log.fail(foe_ident, .None);
2512
+ boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
2513
+ if (stats.spc == 1) {
2514
+ boosts.spc += 1;
2515
+ if (showdown) {
2516
+ try log.boost(foe_ident, .SpecialAttack, -1);
2517
+ try log.boost(foe_ident, .SpecialAttack, 1);
2518
+ try log.boost(foe_ident, .SpecialDefense, -1);
2519
+ try log.boost(foe_ident, .SpecialDefense, 1);
2520
+ }
2521
+ return try log.fail(foe_ident, .None);
2522
+ }
2523
+ var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2524
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2525
+ stats.spc = @max(1, stat * mod[0] / mod[1]);
2526
+ try log.boost(foe_ident, .SpecialAttack, -1);
2527
+ try log.boost(foe_ident, .SpecialDefense, -1);
2528
+ },
2529
+ .AccuracyDown1 => {
2530
+ assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2531
+ if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
2532
+ boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
2533
+ try log.boost(foe_ident, .Accuracy, -1);
2534
+ },
2535
+ else => unreachable,
2536
+ }
2537
+
2538
+ // GLITCH: Stat modification errors glitch
2539
+ statusModify(foe.stored().status, stats);
2540
+ }
2541
+ };
2542
+
2543
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2544
+ const side = battle.side(who);
2545
+ if (!side.active.volatiles.Transform) return &side.stored().stats;
2546
+ const id = ID.from(side.active.volatiles.transform);
2547
+ return &battle.side(id.player).pokemon[id.id - 1].stats;
2548
+ }
2549
+
2550
+ fn statusModify(status: u8, stats: *Stats(u16)) void {
2551
+ if (Status.is(status, .PAR)) {
2552
+ stats.spe = @max(stats.spe / 4, 1);
2553
+ } else if (Status.is(status, .BRN)) {
2554
+ stats.atk = @max(stats.atk / 2, 1);
2555
+ }
2556
+ }
2557
+
2558
+ inline fn isForced(active: ActivePokemon) bool {
2559
+ return active.volatiles.Recharging or active.volatiles.Rage or
2560
+ active.volatiles.Thrashing or active.volatiles.Charging;
2561
+ }
2562
+
2563
+ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2564
+ var side = battle.side(who);
2565
+ var volatiles = &side.active.volatiles;
2566
+ const ident = battle.active(who);
2567
+
2568
+ if (volatiles.disabled_move != 0) {
2569
+ volatiles.disabled_move = 0;
2570
+ volatiles.disabled_duration = 0;
2571
+ try log.end(ident, .DisableSilent);
2572
+ }
2573
+ if (volatiles.Confusion) {
2574
+ // volatiles.confusion is left unchanged
2575
+ volatiles.Confusion = false;
2576
+ try log.end(ident, .ConfusionSilent);
2577
+ }
2578
+ if (volatiles.Mist) {
2579
+ volatiles.Mist = false;
2580
+ try log.end(ident, .Mist);
2581
+ }
2582
+ if (volatiles.FocusEnergy) {
2583
+ volatiles.FocusEnergy = false;
2584
+ try log.end(ident, .FocusEnergy);
2585
+ }
2586
+ if (volatiles.LeechSeed) {
2587
+ volatiles.LeechSeed = false;
2588
+ try log.end(ident, .LeechSeed);
2589
+ }
2590
+ if (!showdown and volatiles.Toxic) {
2591
+ volatiles.Toxic = false;
2592
+ // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
2593
+ }
2594
+ if (volatiles.LightScreen) {
2595
+ volatiles.LightScreen = false;
2596
+ try log.end(ident, .LightScreen);
2597
+ }
2598
+ if (volatiles.Reflect) {
2599
+ volatiles.Reflect = false;
2600
+ try log.end(ident, .Reflect);
2601
+ }
2602
+ if (showdown and volatiles.Toxic) {
2603
+ volatiles.Toxic = false;
2604
+ // Pokémon Showdown erroneously clears the toxic counter
2605
+ volatiles.toxic = 0;
2606
+ try log.end(ident, .Toxic);
2607
+ }
2608
+ }
2609
+
2610
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2611
+ if (side.active.volatiles.disabled_move != 0) {
2612
+ // A Pokémon that transforms after being disabled may end up with less move slots
2613
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2614
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2615
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2616
+ m
2617
+ else
2618
+ side.last_selected_move;
2619
+ if (m != .None and m == last) {
2620
+ side.active.volatiles.Charging = false;
2621
+ try log.disabled(ident, last);
2622
+ return true;
2623
+ }
2624
+ }
2625
+ return false;
2626
+ }
2627
+
2628
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2629
+ const side = battle.side(who);
2630
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2631
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2632
+ }
2633
+ }
2634
+
2635
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2636
+ var volatiles = &active.volatiles;
2637
+ assert(volatiles.Thrashing);
2638
+ if (volatiles.attacks > 0) return false;
2639
+
2640
+ volatiles.Thrashing = false;
2641
+ volatiles.Confusion = true;
2642
+ volatiles.confusion = @intCast(u3, if (showdown)
2643
+ battle.rng.range(u8, 2, 6)
2644
+ else
2645
+ (battle.rng.next() & 3) + 2);
2646
+
2647
+ return true;
2648
+ }
2649
+
2650
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2651
+
2652
+ fn distribution(battle: anytype) u3 {
2653
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2654
+ const r = (battle.rng.next() & 3);
2655
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2656
+ }
2657
+
2658
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2659
+ if (showdown) {
2660
+ if (check_pp == 0) {
2661
+ var i: usize = moves.len;
2662
+ while (i > 0) {
2663
+ i -= 1;
2664
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2665
+ }
2666
+ } else {
2667
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2668
+ var i: usize = 0;
2669
+ while (i < moves.len and r > 0) : (i += 1) {
2670
+ if (moves[i].pp > 0) {
2671
+ r -= 1;
2672
+ if (r == 0) break;
2673
+ }
2674
+ }
2675
+ return @intCast(u4, i + 1);
2676
+ }
2677
+ }
2678
+
2679
+ while (true) {
2680
+ const r = @intCast(u4, rand.next() & 3);
2681
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2682
+ }
2683
+ }
2684
+
2685
+ test "RNG agreement" {
2686
+ if (!showdown) return;
2687
+ var expected: [256]u32 = undefined;
2688
+ for (0..expected.len) |i| {
2689
+ expected[i] = @intCast(u32, i * 0x1000000);
2690
+ }
2691
+
2692
+ var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
2693
+ var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
2694
+ var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
2695
+ var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
2696
+ var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
2697
+
2698
+ for (0..expected.len) |i| {
2699
+ try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
2700
+ try expectEqual(!cfz.chance(u8, 128, 256), i >= Gen12.percent(50) + 1);
2701
+ try expectEqual(par.chance(u8, 63, 256), i < Gen12.percent(25));
2702
+ try expectEqual(brn.chance(u8, 26, 256), i < Gen12.percent(10) + 1);
2703
+ try expectEqual(eff.chance(u8, 85, 256), i < Gen12.percent(33) + 1);
2704
+ }
2705
+ }
2706
+
2707
+ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Choice) u8 {
2708
+ var n: u8 = 0;
2709
+ switch (request) {
2710
+ .Pass => {
2711
+ out[n] = .{};
2712
+ n += 1;
2713
+ },
2714
+ .Switch => {
2715
+ const side = battle.side(player);
2716
+ var slot: u4 = 2;
2717
+ while (slot <= 6) : (slot += 1) {
2718
+ const id = side.order[slot - 1];
2719
+ if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
2720
+ out[n] = .{ .type = .Switch, .data = slot };
2721
+ n += 1;
2722
+ }
2723
+ if (n == 0) {
2724
+ out[n] = .{};
2725
+ n += 1;
2726
+ }
2727
+ },
2728
+ .Move => {
2729
+ const side = battle.side(player);
2730
+ const foe = battle.foe(player);
2731
+
2732
+ var active = side.active;
2733
+ const stored = side.stored();
2734
+
2735
+ // While players are not given any input options on the cartridge in these cases,
2736
+ // Pokémon Showdown instead produces a list with a single move that must be chosen.
2737
+ //
2738
+ // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
2739
+ // here when not in compatibility mode, but the engine needs to be able to differentiate
2740
+ // between passing while waiting for an opponent's forced switch after fainting and
2741
+ // passing due to being forced into using a move. Instead of introducing another option
2742
+ // we simply repurpose Move with no move slot, even though pedantically this is not
2743
+ // strictly correct as the player would not have been presented the option to move or
2744
+ // switch at all.
2745
+ if (isForced(active)) {
2746
+ out[n] = .{ .type = .Move, .data = @boolToInt(showdown) };
2747
+ n += 1;
2748
+ return n;
2749
+ }
2750
+
2751
+ var slot: u4 = 2;
2752
+ while (slot <= 6) : (slot += 1) {
2753
+ const id = side.order[slot - 1];
2754
+ if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
2755
+ out[n] = .{ .type = .Switch, .data = slot };
2756
+ n += 1;
2757
+ }
2758
+
2759
+ const limited = active.volatiles.Bide or active.volatiles.Binding;
2760
+ // On the cartridge, all of these happen after "FIGHT" (indicating you are not
2761
+ // switching) but before you are allowed to select a move. Pokémon Showdown instead
2762
+ // either disables all other moves in the case of limited or requires you to select a
2763
+ // move normally if sleeping/frozen/bound.
2764
+ if (!showdown and (limited or foe.active.volatiles.Binding or
2765
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
2766
+ {
2767
+ out[n] = .{ .type = .Move, .data = 0 };
2768
+ n += 1;
2769
+ return n;
2770
+ }
2771
+
2772
+ slot = 1;
2773
+ // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
2774
+ // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
2775
+ // Move will not result in forcing the subsequent use unless the user also had the
2776
+ // proc-ed move in their moveset) and disabling all other moves.
2777
+ if (limited) {
2778
+ assert(showdown);
2779
+ assert(side.last_selected_move != .None);
2780
+ while (slot <= 4) : (slot += 1) {
2781
+ const m = active.moves[slot - 1];
2782
+ if (m.id == .None) break;
2783
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2784
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2785
+ const struggle =
2786
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2787
+ const s = if (struggle) 0 else slot;
2788
+ out[n] = .{ .type = .Move, .data = s };
2789
+ n += 1;
2790
+ return n;
2791
+ }
2792
+ }
2793
+ }
2794
+
2795
+ const before = n;
2796
+ slot = 1;
2797
+ while (slot <= 4) : (slot += 1) {
2798
+ const m = active.moves[slot - 1];
2799
+ if (m.id == .None) break;
2800
+ if (m.pp == 0) continue;
2801
+ if (active.volatiles.disabled_move == slot) continue;
2802
+ out[n] = .{ .type = .Move, .data = slot };
2803
+ n += 1;
2804
+ }
2805
+ // Struggle (Pokémon Showdown would use 'move 1' here)
2806
+ if (n == before) {
2807
+ // GLITCH: Transform + Mirror Move / Metronome PP softlock
2808
+ if (!showdown) {
2809
+ while (slot <= 4) : (slot += 1) {
2810
+ if (active.moves[slot - 1].pp != 0) return n;
2811
+ }
2812
+ }
2813
+ out[n] = .{ .type = .Move, .data = 0 };
2814
+ n += 1;
2815
+ }
2816
+ },
2817
+ }
2818
+ return n;
2819
+ }