@pkmn/engine 0.1.0-dev.83de29ee → 0.1.0-dev.8b83af6b
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.
- package/README.md +3 -3
- package/build/pkg/gen1.js +11 -3
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +4 -2
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +0 -1
- package/build/test/display.js.map +1 -1
- package/build.zig +7 -8
- package/package.json +14 -14
- package/src/bin/install-pkmn-engine +1 -1
- package/src/lib/binding/node.zig +17 -20
- package/src/lib/binding/wasm.zig +2 -2
- package/src/lib/common/data.zig +11 -11
- package/src/lib/common/protocol.zig +156 -140
- package/src/lib/common/rng.zig +5 -5
- package/src/lib/gen1/README.md +20 -14
- package/src/lib/gen1/data/moves.zig +10 -10
- package/src/lib/gen1/data/species.zig +2 -2
- package/src/lib/gen1/data/types.zig +3 -3
- package/src/lib/gen1/data.zig +19 -16
- package/src/lib/gen1/helpers.zig +10 -19
- package/src/lib/gen1/mechanics.zig +119 -101
- package/src/lib/gen2/data/items.zig +3 -3
- package/src/lib/gen2/data/moves.zig +1 -1
- package/src/lib/gen2/data/species.zig +1 -1
- package/src/lib/gen2/data/types.zig +3 -3
- package/src/lib/gen2/data.zig +12 -11
- package/src/lib/gen2/helpers.zig +6 -6
|
@@ -126,7 +126,7 @@ fn selectMove(
|
|
|
126
126
|
const stored = side.stored();
|
|
127
127
|
|
|
128
128
|
assert(!isForced(side.active) or
|
|
129
|
-
(choice.type == .Move and choice.data == @
|
|
129
|
+
(choice.type == .Move and choice.data == @intFromBool(showdown)));
|
|
130
130
|
|
|
131
131
|
// pre-battle menu
|
|
132
132
|
if (volatiles.Recharging) {
|
|
@@ -183,7 +183,7 @@ fn selectMove(
|
|
|
183
183
|
battle.last_moves.p1_index
|
|
184
184
|
else
|
|
185
185
|
battle.last_moves.p2_index;
|
|
186
|
-
const last = side.active.move(
|
|
186
|
+
const last = side.active.move(slot);
|
|
187
187
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
188
188
|
if (last.id == .MirrorMove) return Result.Error;
|
|
189
189
|
}
|
|
@@ -237,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
237
237
|
|
|
238
238
|
side.last_selected_move = move.id;
|
|
239
239
|
if (player == .P1) {
|
|
240
|
-
battle.last_moves.p1_index = @intCast(
|
|
240
|
+
battle.last_moves.p1_index = @intCast(c.data);
|
|
241
241
|
} else {
|
|
242
|
-
battle.last_moves.p2_index = @intCast(
|
|
242
|
+
battle.last_moves.p2_index = @intCast(c.data);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
}
|
|
@@ -318,7 +318,9 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
318
318
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
319
319
|
if (spe1 == spe2) {
|
|
320
320
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
321
|
-
if (showdown and m1 == .Counter and m2 == .Counter)
|
|
321
|
+
if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
|
|
322
|
+
battle.rng.advance(1);
|
|
323
|
+
}
|
|
322
324
|
|
|
323
325
|
const p1 = Rolls.speedTie(battle);
|
|
324
326
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
@@ -418,7 +420,7 @@ fn executeMove(
|
|
|
418
420
|
var side = battle.side(player);
|
|
419
421
|
|
|
420
422
|
if (choice.type == .Switch) {
|
|
421
|
-
try switchIn(battle, player,
|
|
423
|
+
try switchIn(battle, player, choice.data, false, log);
|
|
422
424
|
return null;
|
|
423
425
|
}
|
|
424
426
|
|
|
@@ -434,7 +436,7 @@ fn executeMove(
|
|
|
434
436
|
}
|
|
435
437
|
|
|
436
438
|
assert(choice.type == .Move);
|
|
437
|
-
var mslot = @intCast(
|
|
439
|
+
var mslot: u4 = @intCast(choice.data);
|
|
438
440
|
// Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
|
|
439
441
|
// because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
|
|
440
442
|
var auto = showdown and side.last_selected_move != .None;
|
|
@@ -443,7 +445,7 @@ fn executeMove(
|
|
|
443
445
|
if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
|
|
444
446
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
445
447
|
assert(!showdown);
|
|
446
|
-
mslot = @intCast(
|
|
448
|
+
mslot = @intCast(if (player == .P1)
|
|
447
449
|
battle.last_moves.p1_index
|
|
448
450
|
else
|
|
449
451
|
battle.last_moves.p2_index);
|
|
@@ -467,7 +469,7 @@ fn executeMove(
|
|
|
467
469
|
} else if (showdown and side.active.volatiles.Charging) {
|
|
468
470
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
469
471
|
assert(mslot == 1);
|
|
470
|
-
mslot = @intCast(
|
|
472
|
+
mslot = @intCast(if (player == .P1)
|
|
471
473
|
battle.last_moves.p1_index
|
|
472
474
|
else
|
|
473
475
|
battle.last_moves.p2_index);
|
|
@@ -627,7 +629,14 @@ fn beforeMove(
|
|
|
627
629
|
if (volatiles.Bide) {
|
|
628
630
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
629
631
|
|
|
630
|
-
|
|
632
|
+
if (showdown) {
|
|
633
|
+
// Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
|
|
634
|
+
// OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
|
|
635
|
+
// addition because 65535 is sufficient to faint any Pokémon anyway
|
|
636
|
+
volatiles.state +|= battle.last_damage;
|
|
637
|
+
} else {
|
|
638
|
+
volatiles.state +%= battle.last_damage;
|
|
639
|
+
}
|
|
631
640
|
|
|
632
641
|
assert(volatiles.attacks > 0);
|
|
633
642
|
|
|
@@ -728,6 +737,7 @@ fn canMove(
|
|
|
728
737
|
side.active.volatiles.Invulnerable = false;
|
|
729
738
|
} else if (move.effect == .Charge) {
|
|
730
739
|
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
740
|
+
setCounterable(battle, player, side, move);
|
|
731
741
|
try Effects.charge(battle, player, log);
|
|
732
742
|
return false;
|
|
733
743
|
}
|
|
@@ -744,19 +754,7 @@ fn canMove(
|
|
|
744
754
|
// regrettable, but this information is more "nice to have" than required
|
|
745
755
|
const f = if (from != null and from.? == .None) null else from;
|
|
746
756
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
747
|
-
|
|
748
|
-
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
749
|
-
// in cases where an early return happens the data for a players last selected move does not get
|
|
750
|
-
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
751
|
-
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
752
|
-
// as the desync results from actually having two separate battle states that subtly disagree.
|
|
753
|
-
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
754
|
-
(move.type == .Normal or move.type == .Fighting);
|
|
755
|
-
if (player == .P1) {
|
|
756
|
-
battle.last_moves.p1_counterable = @boolToInt(counterable);
|
|
757
|
-
} else {
|
|
758
|
-
battle.last_moves.p2_counterable = @boolToInt(counterable);
|
|
759
|
-
}
|
|
757
|
+
setCounterable(battle, player, side, move);
|
|
760
758
|
|
|
761
759
|
if (move.effect.onBegin()) {
|
|
762
760
|
try onBegin(battle, player, move, mslot, residual, log);
|
|
@@ -773,6 +771,21 @@ fn canMove(
|
|
|
773
771
|
return true;
|
|
774
772
|
}
|
|
775
773
|
|
|
774
|
+
fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
|
|
775
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
776
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
777
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
778
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
779
|
+
// as the desync results from actually having two separate battle states that subtly disagree
|
|
780
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
781
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
782
|
+
if (player == .P1) {
|
|
783
|
+
battle.last_moves.p1_counterable = @intFromBool(counterable);
|
|
784
|
+
} else {
|
|
785
|
+
battle.last_moves.p2_counterable = @intFromBool(counterable);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
776
789
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
777
790
|
if (side.last_selected_move == .Struggle) return;
|
|
778
791
|
|
|
@@ -783,11 +796,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
783
796
|
if (volatiles.Bide) return;
|
|
784
797
|
|
|
785
798
|
assert(active.move(mslot).pp > 0 or auto);
|
|
786
|
-
active.move(mslot).pp = @
|
|
799
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
|
|
787
800
|
if (volatiles.Transform) return;
|
|
788
801
|
|
|
789
802
|
assert(side.stored().move(mslot).pp > 0 or auto);
|
|
790
|
-
side.stored().move(mslot).pp = @
|
|
803
|
+
side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
|
|
791
804
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
792
805
|
}
|
|
793
806
|
|
|
@@ -795,12 +808,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
795
808
|
var active = &side.active;
|
|
796
809
|
const volatiles = &active.volatiles;
|
|
797
810
|
|
|
798
|
-
active.move(mslot).pp = @
|
|
811
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
|
|
799
812
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
800
813
|
if (showdown and volatiles.Transform) return;
|
|
801
814
|
|
|
802
815
|
assert(mslot > 0 and mslot <= 4);
|
|
803
|
-
side.stored().moves[mslot - 1].pp = @
|
|
816
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
|
|
804
817
|
}
|
|
805
818
|
|
|
806
819
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -836,8 +849,8 @@ fn doMove(
|
|
|
836
849
|
// calling moveHit early covers most of that but we also need to check type immunity first
|
|
837
850
|
var miss = showdown and miss: {
|
|
838
851
|
immune = move.target != .Self and !status and !counter and
|
|
839
|
-
(@
|
|
840
|
-
@
|
|
852
|
+
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
853
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
841
854
|
if (immune and move.effect != .Binding) break :miss true;
|
|
842
855
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
843
856
|
battle.last_damage = 0;
|
|
@@ -928,7 +941,7 @@ fn doMove(
|
|
|
928
941
|
}
|
|
929
942
|
if (move.effect == .JumpKick) {
|
|
930
943
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
931
|
-
assert(battle.last_damage == 0);
|
|
944
|
+
assert(showdown or battle.last_damage == 0);
|
|
932
945
|
battle.last_damage = 1;
|
|
933
946
|
_ = try applyDamage(battle, player, player.foe(), .None, log);
|
|
934
947
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
@@ -1110,7 +1123,7 @@ fn calcDamage(
|
|
|
1110
1123
|
d = @min(997, d);
|
|
1111
1124
|
d += 2;
|
|
1112
1125
|
|
|
1113
|
-
battle.last_damage = @intCast(
|
|
1126
|
+
battle.last_damage = @intCast(d);
|
|
1114
1127
|
|
|
1115
1128
|
return true;
|
|
1116
1129
|
}
|
|
@@ -1124,9 +1137,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1124
1137
|
var d = battle.last_damage;
|
|
1125
1138
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1126
1139
|
|
|
1127
|
-
const neutral = @
|
|
1128
|
-
const eff1: u16 = @
|
|
1129
|
-
const eff2: u16 = @
|
|
1140
|
+
const neutral = @intFromEnum(Effectiveness.Neutral);
|
|
1141
|
+
const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
|
|
1142
|
+
const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
|
|
1130
1143
|
|
|
1131
1144
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1132
1145
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -1149,7 +1162,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1149
1162
|
fn randomizeDamage(battle: anytype) void {
|
|
1150
1163
|
if (battle.last_damage <= 1) return;
|
|
1151
1164
|
const random = Rolls.damage(battle);
|
|
1152
|
-
battle.last_damage = @intCast(
|
|
1165
|
+
battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
|
|
1153
1166
|
}
|
|
1154
1167
|
|
|
1155
1168
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
@@ -1165,7 +1178,7 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1165
1178
|
.DragonRage => 40,
|
|
1166
1179
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1167
1180
|
.Psywave => power: {
|
|
1168
|
-
const max = @intCast(
|
|
1181
|
+
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1169
1182
|
// GLITCH: Psywave infinite glitch loop
|
|
1170
1183
|
if (!showdown and max <= 1) return Result.Error;
|
|
1171
1184
|
break :power Rolls.psywave(battle, max);
|
|
@@ -1219,7 +1232,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1219
1232
|
return null;
|
|
1220
1233
|
}
|
|
1221
1234
|
|
|
1222
|
-
battle.last_damage
|
|
1235
|
+
battle.last_damage *|= 2;
|
|
1223
1236
|
|
|
1224
1237
|
// Pokémon Showdown calls checkHit before Counter
|
|
1225
1238
|
if (!showdown and !try checkHit(battle, player, move, log)) return null;
|
|
@@ -1254,7 +1267,7 @@ fn applyDamage(
|
|
|
1254
1267
|
return true;
|
|
1255
1268
|
} else {
|
|
1256
1269
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1257
|
-
subbed.active.volatiles.substitute -= @intCast(
|
|
1270
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1258
1271
|
try log.activate(battle.active(sub_player), .Substitute);
|
|
1259
1272
|
return false;
|
|
1260
1273
|
}
|
|
@@ -1366,9 +1379,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1366
1379
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1367
1380
|
|
|
1368
1381
|
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1369
|
-
var boost = BOOSTS[@intCast(
|
|
1382
|
+
var boost = BOOSTS[@intCast(@as(i8, side.active.boosts.accuracy) + 6)];
|
|
1370
1383
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1371
|
-
boost = BOOSTS[@intCast(
|
|
1384
|
+
boost = BOOSTS[@intCast(@as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1372
1385
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1373
1386
|
accuracy = @min(255, @max(1, accuracy));
|
|
1374
1387
|
|
|
@@ -1387,7 +1400,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1387
1400
|
}
|
|
1388
1401
|
|
|
1389
1402
|
if (!miss) return true;
|
|
1390
|
-
battle.last_damage = 0;
|
|
1403
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1391
1404
|
side.active.volatiles.Binding = false;
|
|
1392
1405
|
return false;
|
|
1393
1406
|
}
|
|
@@ -1527,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1527
1540
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1528
1541
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1529
1542
|
|
|
1543
|
+
battle.turn += 1;
|
|
1544
|
+
|
|
1530
1545
|
if (showdown and options.ebc and checkEBC(battle)) {
|
|
1531
1546
|
try log.tie();
|
|
1532
1547
|
return Result.Tie;
|
|
1533
1548
|
}
|
|
1534
1549
|
|
|
1535
|
-
battle.turn += 1;
|
|
1536
|
-
|
|
1537
1550
|
if (showdown and battle.turn >= 1000) {
|
|
1538
1551
|
try log.tie();
|
|
1539
1552
|
return Result.Tie;
|
|
@@ -1548,7 +1561,7 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1548
1561
|
|
|
1549
1562
|
fn checkEBC(battle: anytype) bool {
|
|
1550
1563
|
for (battle.sides, 0..) |side, i| {
|
|
1551
|
-
const foe = battle.sides[~@
|
|
1564
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1552
1565
|
|
|
1553
1566
|
var foe_all_ghosts = true;
|
|
1554
1567
|
var foe_all_transform = true;
|
|
@@ -1842,7 +1855,7 @@ pub const Effects = struct {
|
|
|
1842
1855
|
return Result.Error;
|
|
1843
1856
|
}
|
|
1844
1857
|
|
|
1845
|
-
volatiles.disabled_move = @intCast(
|
|
1858
|
+
volatiles.disabled_move = @intCast(Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1846
1859
|
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1847
1860
|
|
|
1848
1861
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
@@ -1941,7 +1954,7 @@ pub const Effects = struct {
|
|
|
1941
1954
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1942
1955
|
if (showdown) {
|
|
1943
1956
|
for (&battle.sides, 0..) |*s, i| {
|
|
1944
|
-
const p = @
|
|
1957
|
+
const p: Player = @enumFromInt(i);
|
|
1945
1958
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1946
1959
|
if (p != player and Status.any(s.stored().status)) {
|
|
1947
1960
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
@@ -1972,7 +1985,9 @@ pub const Effects = struct {
|
|
|
1972
1985
|
|
|
1973
1986
|
// GLITCH: HP recovery move failure glitches
|
|
1974
1987
|
const delta = stored.stats.hp - stored.hp;
|
|
1975
|
-
if (delta == 0 or delta & 255 == 255
|
|
1988
|
+
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
1989
|
+
return try log.fail(ident, .None);
|
|
1990
|
+
}
|
|
1976
1991
|
|
|
1977
1992
|
const rest = side.last_selected_move == .Rest;
|
|
1978
1993
|
if (rest) {
|
|
@@ -2040,7 +2055,7 @@ pub const Effects = struct {
|
|
|
2040
2055
|
const has_mimic = has_mimic: {
|
|
2041
2056
|
for (side.active.moves, 0..) |m, i| {
|
|
2042
2057
|
if (m.id == .Mimic) {
|
|
2043
|
-
oslot = @intCast(
|
|
2058
|
+
oslot = @intCast(i + 1);
|
|
2044
2059
|
break :has_mimic true;
|
|
2045
2060
|
}
|
|
2046
2061
|
}
|
|
@@ -2186,9 +2201,12 @@ pub const Effects = struct {
|
|
|
2186
2201
|
var side = battle.side(player);
|
|
2187
2202
|
var stored = side.stored();
|
|
2188
2203
|
|
|
2189
|
-
|
|
2204
|
+
assert(showdown or battle.last_damage > 0);
|
|
2205
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2206
|
+
|
|
2207
|
+
const damage: i16 = @intCast(@max(battle.last_damage /
|
|
2190
2208
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2191
|
-
stored.hp = @intCast(
|
|
2209
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2192
2210
|
|
|
2193
2211
|
try log.damageOf(battle.active(player), stored, .RecoilOf, battle.active(player.foe()));
|
|
2194
2212
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
@@ -2248,7 +2266,7 @@ pub const Effects = struct {
|
|
|
2248
2266
|
|
|
2249
2267
|
assert(side.stored().stats.hp <= 1023);
|
|
2250
2268
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2251
|
-
const hp = @intCast(
|
|
2269
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2252
2270
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2253
2271
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2254
2272
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
@@ -2343,7 +2361,7 @@ pub const Effects = struct {
|
|
|
2343
2361
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2344
2362
|
if (boosts.atk == 6) return try log.fail(ident, .None);
|
|
2345
2363
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2346
|
-
boosts.atk = @intCast(
|
|
2364
|
+
boosts.atk = @intCast(@min(6, @as(i8, boosts.atk) + n));
|
|
2347
2365
|
const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
|
|
2348
2366
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2349
2367
|
boosts.atk -= 1;
|
|
@@ -2354,7 +2372,7 @@ pub const Effects = struct {
|
|
|
2354
2372
|
}
|
|
2355
2373
|
return try log.fail(ident, .None);
|
|
2356
2374
|
}
|
|
2357
|
-
var mod = BOOSTS[@intCast(
|
|
2375
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.atk) + 6)];
|
|
2358
2376
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2359
2377
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2378
|
try log.boost(ident, reason, n);
|
|
@@ -2365,7 +2383,7 @@ pub const Effects = struct {
|
|
|
2365
2383
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2366
2384
|
if (boosts.def == 6) return try log.fail(ident, .None);
|
|
2367
2385
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2368
|
-
boosts.def = @intCast(
|
|
2386
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2369
2387
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2370
2388
|
boosts.def -= 1;
|
|
2371
2389
|
if (showdown) {
|
|
@@ -2374,7 +2392,7 @@ pub const Effects = struct {
|
|
|
2374
2392
|
}
|
|
2375
2393
|
return try log.fail(ident, .None);
|
|
2376
2394
|
}
|
|
2377
|
-
var mod = BOOSTS[@intCast(
|
|
2395
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.def) + 6)];
|
|
2378
2396
|
const stat = unmodifiedStats(battle, side).def;
|
|
2379
2397
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
2398
|
try log.boost(ident, .Defense, n);
|
|
@@ -2382,7 +2400,7 @@ pub const Effects = struct {
|
|
|
2382
2400
|
.SpeedUp2 => {
|
|
2383
2401
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2384
2402
|
if (boosts.spe == 6) return try log.fail(ident, .None);
|
|
2385
|
-
boosts.spe = @intCast(
|
|
2403
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2386
2404
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2387
2405
|
boosts.spe -= 1;
|
|
2388
2406
|
if (showdown) {
|
|
@@ -2391,7 +2409,7 @@ pub const Effects = struct {
|
|
|
2391
2409
|
}
|
|
2392
2410
|
return try log.fail(ident, .None);
|
|
2393
2411
|
}
|
|
2394
|
-
var mod = BOOSTS[@intCast(
|
|
2412
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spe) + 6)];
|
|
2395
2413
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2396
2414
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2397
2415
|
try log.boost(ident, .Speed, 2);
|
|
@@ -2400,7 +2418,7 @@ pub const Effects = struct {
|
|
|
2400
2418
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2401
2419
|
if (boosts.spc == 6) return try log.fail(ident, .None);
|
|
2402
2420
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2403
|
-
boosts.spc = @intCast(
|
|
2421
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2404
2422
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2405
2423
|
boosts.spc -= 1;
|
|
2406
2424
|
if (showdown) {
|
|
@@ -2411,7 +2429,7 @@ pub const Effects = struct {
|
|
|
2411
2429
|
}
|
|
2412
2430
|
return try log.fail(ident, .None);
|
|
2413
2431
|
}
|
|
2414
|
-
var mod = BOOSTS[@intCast(
|
|
2432
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spc) + 6)];
|
|
2415
2433
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2416
2434
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2417
2435
|
try log.boost(ident, .SpecialAttack, n);
|
|
@@ -2420,7 +2438,7 @@ pub const Effects = struct {
|
|
|
2420
2438
|
.EvasionUp1 => {
|
|
2421
2439
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2422
2440
|
if (boosts.evasion == 6) return try log.fail(ident, .None);
|
|
2423
|
-
boosts.evasion = @intCast(
|
|
2441
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2424
2442
|
try log.boost(ident, .Evasion, 1);
|
|
2425
2443
|
},
|
|
2426
2444
|
else => unreachable,
|
|
@@ -2433,12 +2451,11 @@ pub const Effects = struct {
|
|
|
2433
2451
|
fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2434
2452
|
var foe = battle.foe(player);
|
|
2435
2453
|
const foe_ident = battle.active(player.foe());
|
|
2454
|
+
const secondary = move.effect.isStatDownChance();
|
|
2436
2455
|
|
|
2437
|
-
if (foe.active.volatiles.Substitute)
|
|
2438
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2439
|
-
}
|
|
2456
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2440
2457
|
|
|
2441
|
-
if (
|
|
2458
|
+
if (secondary) {
|
|
2442
2459
|
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2443
2460
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2444
2461
|
return; // checkHit already checks for Invulnerable
|
|
@@ -2446,12 +2463,13 @@ pub const Effects = struct {
|
|
|
2446
2463
|
|
|
2447
2464
|
var stats = &foe.active.stats;
|
|
2448
2465
|
var boosts = &foe.active.boosts;
|
|
2466
|
+
const fail = showdown or !secondary;
|
|
2449
2467
|
|
|
2450
2468
|
switch (move.effect) {
|
|
2451
2469
|
.AttackDown1, .AttackDownChance => {
|
|
2452
2470
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2453
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2454
|
-
boosts.atk = @intCast(
|
|
2471
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2472
|
+
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2455
2473
|
if (stats.atk == 1) {
|
|
2456
2474
|
boosts.atk += 1;
|
|
2457
2475
|
if (showdown) {
|
|
@@ -2460,16 +2478,16 @@ pub const Effects = struct {
|
|
|
2460
2478
|
}
|
|
2461
2479
|
return try log.fail(foe_ident, .None);
|
|
2462
2480
|
}
|
|
2463
|
-
var mod = BOOSTS[@intCast(
|
|
2481
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.atk) + 6)];
|
|
2464
2482
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2465
2483
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2466
2484
|
try log.boost(foe_ident, .Attack, -1);
|
|
2467
2485
|
},
|
|
2468
2486
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2469
2487
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2470
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2488
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2471
2489
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2472
|
-
boosts.def = @intCast(
|
|
2490
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2473
2491
|
if (stats.def == 1) {
|
|
2474
2492
|
boosts.def += 1;
|
|
2475
2493
|
if (showdown) {
|
|
@@ -2478,15 +2496,15 @@ pub const Effects = struct {
|
|
|
2478
2496
|
}
|
|
2479
2497
|
return try log.fail(foe_ident, .None);
|
|
2480
2498
|
}
|
|
2481
|
-
var mod = BOOSTS[@intCast(
|
|
2499
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.def) + 6)];
|
|
2482
2500
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2483
2501
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2484
2502
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2485
2503
|
},
|
|
2486
2504
|
.SpeedDown1, .SpeedDownChance => {
|
|
2487
2505
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2488
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2489
|
-
boosts.spe = @intCast(
|
|
2506
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2507
|
+
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2490
2508
|
if (stats.spe == 1) {
|
|
2491
2509
|
boosts.spe += 1;
|
|
2492
2510
|
if (showdown) {
|
|
@@ -2495,7 +2513,7 @@ pub const Effects = struct {
|
|
|
2495
2513
|
}
|
|
2496
2514
|
return try log.fail(foe_ident, .None);
|
|
2497
2515
|
}
|
|
2498
|
-
var mod = BOOSTS[@intCast(
|
|
2516
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spe) + 6)];
|
|
2499
2517
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2500
2518
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2501
2519
|
try log.boost(foe_ident, .Speed, -1);
|
|
@@ -2503,8 +2521,8 @@ pub const Effects = struct {
|
|
|
2503
2521
|
},
|
|
2504
2522
|
.SpecialDownChance => {
|
|
2505
2523
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2506
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2507
|
-
boosts.spc = @intCast(
|
|
2524
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2525
|
+
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2508
2526
|
if (stats.spc == 1) {
|
|
2509
2527
|
boosts.spc += 1;
|
|
2510
2528
|
if (showdown) {
|
|
@@ -2515,7 +2533,7 @@ pub const Effects = struct {
|
|
|
2515
2533
|
}
|
|
2516
2534
|
return try log.fail(foe_ident, .None);
|
|
2517
2535
|
}
|
|
2518
|
-
var mod = BOOSTS[@intCast(
|
|
2536
|
+
var mod = BOOSTS[@intCast(@as(i8, boosts.spc) + 6)];
|
|
2519
2537
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2520
2538
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2521
2539
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
@@ -2523,8 +2541,8 @@ pub const Effects = struct {
|
|
|
2523
2541
|
},
|
|
2524
2542
|
.AccuracyDown1 => {
|
|
2525
2543
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2526
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2527
|
-
boosts.accuracy = @intCast(
|
|
2544
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2545
|
+
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2528
2546
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2529
2547
|
},
|
|
2530
2548
|
else => unreachable,
|
|
@@ -2608,7 +2626,7 @@ pub const Rolls = struct {
|
|
|
2608
2626
|
}
|
|
2609
2627
|
|
|
2610
2628
|
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2611
|
-
if (showdown) return battle.rng.chance(u8, @
|
|
2629
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, @intCast(chance)), 256);
|
|
2612
2630
|
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2613
2631
|
}
|
|
2614
2632
|
|
|
@@ -2621,13 +2639,13 @@ pub const Rolls = struct {
|
|
|
2621
2639
|
}
|
|
2622
2640
|
|
|
2623
2641
|
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2624
|
-
if (showdown) return battle.rng.chance(u8, @
|
|
2642
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, @intCast(accuracy)), 256);
|
|
2625
2643
|
return battle.rng.next() < accuracy;
|
|
2626
2644
|
}
|
|
2627
2645
|
|
|
2628
2646
|
inline fn confusionDuration(battle: anytype) u3 {
|
|
2629
|
-
if (showdown) return @intCast(
|
|
2630
|
-
return @intCast(
|
|
2647
|
+
if (showdown) return @intCast(battle.rng.range(u8, 2, 6));
|
|
2648
|
+
return @intCast((battle.rng.next() & 3) + 2);
|
|
2631
2649
|
}
|
|
2632
2650
|
|
|
2633
2651
|
inline fn confused(battle: anytype) bool {
|
|
@@ -2656,16 +2674,16 @@ pub const Rolls = struct {
|
|
|
2656
2674
|
}
|
|
2657
2675
|
|
|
2658
2676
|
inline fn sleepDuration(battle: anytype) u3 {
|
|
2659
|
-
if (showdown) return @intCast(
|
|
2677
|
+
if (showdown) return @intCast(battle.rng.range(u8, 1, 8));
|
|
2660
2678
|
while (true) {
|
|
2661
2679
|
const r = battle.rng.next() & 7;
|
|
2662
|
-
if (r != 0) return @intCast(
|
|
2680
|
+
if (r != 0) return @intCast(r);
|
|
2663
2681
|
}
|
|
2664
2682
|
}
|
|
2665
2683
|
|
|
2666
2684
|
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2667
|
-
if (showdown) return @intCast(
|
|
2668
|
-
return @intCast(
|
|
2685
|
+
if (showdown) return @intCast(battle.rng.range(u4, 2, 4));
|
|
2686
|
+
return @intCast((battle.rng.next() & 1) + 2);
|
|
2669
2687
|
}
|
|
2670
2688
|
|
|
2671
2689
|
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
@@ -2675,11 +2693,11 @@ pub const Rolls = struct {
|
|
|
2675
2693
|
while (i > 0) {
|
|
2676
2694
|
i -= 1;
|
|
2677
2695
|
if (moves[i].id != .None) {
|
|
2678
|
-
return battle.rng.range(u4, 0, @
|
|
2696
|
+
return battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
|
|
2679
2697
|
}
|
|
2680
2698
|
}
|
|
2681
2699
|
} else {
|
|
2682
|
-
var r = battle.rng.range(u4, 0,
|
|
2700
|
+
var r = battle.rng.range(u4, 0, check_pp) + 1;
|
|
2683
2701
|
var i: usize = 0;
|
|
2684
2702
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
2685
2703
|
if (moves[i].pp > 0) {
|
|
@@ -2687,32 +2705,32 @@ pub const Rolls = struct {
|
|
|
2687
2705
|
if (r == 0) break;
|
|
2688
2706
|
}
|
|
2689
2707
|
}
|
|
2690
|
-
return @intCast(
|
|
2708
|
+
return @intCast(i + 1);
|
|
2691
2709
|
}
|
|
2692
2710
|
}
|
|
2693
2711
|
|
|
2694
2712
|
while (true) {
|
|
2695
|
-
const r = @intCast(
|
|
2713
|
+
const r: u4 = @intCast(battle.rng.next() & 3);
|
|
2696
2714
|
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2697
2715
|
}
|
|
2698
2716
|
}
|
|
2699
2717
|
|
|
2700
2718
|
inline fn disableDuration(battle: anytype) u4 {
|
|
2701
|
-
if (showdown) return @intCast(
|
|
2702
|
-
return @intCast(
|
|
2719
|
+
if (showdown) return @intCast(battle.rng.range(u8, 1, 9));
|
|
2720
|
+
return @intCast((battle.rng.next() & 7) + 1);
|
|
2703
2721
|
}
|
|
2704
2722
|
|
|
2705
2723
|
inline fn metronome(battle: anytype) Move {
|
|
2706
2724
|
if (showdown) {
|
|
2707
|
-
const r = battle.rng.range(u8, 0, @
|
|
2708
|
-
const mod = @as(u2, (if (r < @
|
|
2709
|
-
return @
|
|
2725
|
+
const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
|
|
2726
|
+
const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
|
|
2727
|
+
return @enumFromInt(r + mod);
|
|
2710
2728
|
}
|
|
2711
2729
|
while (true) {
|
|
2712
2730
|
const r = battle.rng.next();
|
|
2713
|
-
if (r == 0 or r == @
|
|
2714
|
-
if (r >= @
|
|
2715
|
-
return @
|
|
2731
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2732
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2733
|
+
return @enumFromInt(r);
|
|
2716
2734
|
}
|
|
2717
2735
|
}
|
|
2718
2736
|
|
|
@@ -2727,7 +2745,7 @@ pub const Rolls = struct {
|
|
|
2727
2745
|
inline fn distribution(battle: anytype) u3 {
|
|
2728
2746
|
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2729
2747
|
const r = (battle.rng.next() & 3);
|
|
2730
|
-
return @intCast(
|
|
2748
|
+
return @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2731
2749
|
}
|
|
2732
2750
|
|
|
2733
2751
|
inline fn unboost(battle: anytype) bool {
|
|
@@ -2740,7 +2758,7 @@ test "RNG agreement" {
|
|
|
2740
2758
|
if (!showdown) return;
|
|
2741
2759
|
var expected: [256]u32 = undefined;
|
|
2742
2760
|
for (0..expected.len) |i| {
|
|
2743
|
-
expected[i] = @intCast(
|
|
2761
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2744
2762
|
}
|
|
2745
2763
|
|
|
2746
2764
|
var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
|
|
@@ -2797,7 +2815,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2797
2815
|
// strictly correct as the player would not have been presented the option to move or
|
|
2798
2816
|
// switch at all.
|
|
2799
2817
|
if (isForced(active)) {
|
|
2800
|
-
out[n] = .{ .type = .Move, .data = @
|
|
2818
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2801
2819
|
n += 1;
|
|
2802
2820
|
return n;
|
|
2803
2821
|
}
|