@pkmn/engine 0.1.0-dev.abd68573 → 0.1.0-dev.ac9225d5
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 +8 -6
- package/build.zig +13 -29
- package/package.json +9 -9
- package/src/lib/common/debug.zig +2 -2
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/rational.zig +69 -12
- package/src/lib/gen1/calc.zig +210 -40
- package/src/lib/gen1/chance.zig +61 -63
- package/src/lib/gen1/data.zig +3 -3
- package/src/lib/gen1/helpers.zig +90 -29
- package/src/lib/gen1/mechanics.zig +55 -54
- package/src/lib/gen2/helpers.zig +37 -7
- package/src/test/display/pkmn.html.tmpl +5 -5
|
@@ -166,7 +166,7 @@ fn selectMove(
|
|
|
166
166
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
167
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
168
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
169
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
170
|
saveMove(battle, player, choice);
|
|
171
171
|
} else {
|
|
172
172
|
assert(choice.data == 0);
|
|
@@ -347,10 +347,10 @@ fn doTurn(
|
|
|
347
347
|
)) |r| return r;
|
|
348
348
|
if (!replace) {
|
|
349
349
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
350
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
351
|
}
|
|
352
352
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
353
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
354
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
355
|
|
|
356
356
|
options.chance.clearPending();
|
|
@@ -369,10 +369,10 @@ fn doTurn(
|
|
|
369
369
|
)) |r| return r;
|
|
370
370
|
if (!replace) {
|
|
371
371
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
372
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
373
|
}
|
|
374
374
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
375
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -431,7 +431,7 @@ fn executeMove(
|
|
|
431
431
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
432
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
433
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
434
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
434
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
435
|
if (!auto) {
|
|
436
436
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
437
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -852,7 +852,7 @@ fn doMove(
|
|
|
852
852
|
break :miss true;
|
|
853
853
|
}
|
|
854
854
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
855
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
856
|
};
|
|
857
857
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
858
858
|
|
|
@@ -899,7 +899,7 @@ fn doMove(
|
|
|
899
899
|
miss = if (showdown or skip)
|
|
900
900
|
miss
|
|
901
901
|
else
|
|
902
|
-
(!moveHit(battle, player, move, &immune, &mist, options) or zero);
|
|
902
|
+
(!try moveHit(battle, player, move, &immune, &mist, options) or zero);
|
|
903
903
|
|
|
904
904
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
905
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -930,7 +930,7 @@ fn doMove(
|
|
|
930
930
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
931
|
try log.fail(.{ foe_ident, .None });
|
|
932
932
|
} else {
|
|
933
|
-
if (showdown
|
|
933
|
+
if (!showdown and !zero) try options.chance.commit(player, .miss);
|
|
934
934
|
try log.lastmiss(.{});
|
|
935
935
|
try log.miss(.{battle.active(player)});
|
|
936
936
|
}
|
|
@@ -949,13 +949,13 @@ fn doMove(
|
|
|
949
949
|
try options.chance.commit(player, .binding);
|
|
950
950
|
}
|
|
951
951
|
return null;
|
|
952
|
-
} else {
|
|
952
|
+
} else if (!showdown) {
|
|
953
953
|
try options.chance.commit(player, .hit);
|
|
954
954
|
}
|
|
955
955
|
|
|
956
956
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
957
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
958
|
+
// it up above before damage calculation)
|
|
959
959
|
if (!showdown and move.effect.isMulti()) {
|
|
960
960
|
try Effects.multiHit(battle, player, move, options);
|
|
961
961
|
hits = side.active.volatiles.attacks;
|
|
@@ -1014,12 +1014,12 @@ fn doMove(
|
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
1016
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1017
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1017
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1018
|
if (nullified) return null;
|
|
1019
1019
|
|
|
1020
1020
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1021
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1022
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1022
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1023
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1024
|
|
|
1025
1025
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1034,7 @@ fn doMove(
|
|
|
1034
1034
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1035
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1036
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1037
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1038
|
if (move.effect == .Twineedle) {
|
|
1039
1039
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1040
|
} else if (move.effect == .Disable) {
|
|
@@ -1058,7 +1058,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1058
1058
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1059
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1060
|
|
|
1061
|
-
|
|
1061
|
+
const spurious =
|
|
1062
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1063
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1064
|
}
|
|
1063
1065
|
|
|
1064
1066
|
fn calcDamage(
|
|
@@ -1173,7 +1175,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1175
|
const foe = battle.foe(player);
|
|
1174
1176
|
|
|
1175
1177
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1178
|
|
|
1178
1179
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1180
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1234,9 +1235,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1235
|
|
|
1235
1236
|
battle.last_damage *|= 2;
|
|
1236
1237
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1238
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1239
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1240
|
|
|
1241
1241
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1242
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1265,7 +1265,7 @@ fn applyDamage(
|
|
|
1265
1265
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1266
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1267
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1268
|
+
options.calc.capped(sub_player);
|
|
1269
1269
|
return true;
|
|
1270
1270
|
} else {
|
|
1271
1271
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1332,7 +1332,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1332
|
var immune = false;
|
|
1333
1333
|
var mist = false;
|
|
1334
1334
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1335
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1336
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1336
1339
|
|
|
1337
1340
|
assert(!immune);
|
|
1338
1341
|
if (mist) {
|
|
@@ -1341,11 +1344,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1344
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1345
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1346
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1347
|
try options.log.lastmiss(.{});
|
|
1346
1348
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1349
|
}
|
|
1348
1350
|
|
|
1351
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1352
|
return false;
|
|
1350
1353
|
}
|
|
1351
1354
|
|
|
@@ -1356,7 +1359,7 @@ fn moveHit(
|
|
|
1356
1359
|
immune: *bool,
|
|
1357
1360
|
mist: *bool,
|
|
1358
1361
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1362
|
+
) !bool {
|
|
1360
1363
|
var side = battle.side(player);
|
|
1361
1364
|
const foe = battle.foe(player);
|
|
1362
1365
|
|
|
@@ -1407,7 +1410,7 @@ fn moveHit(
|
|
|
1407
1410
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1411
|
|
|
1409
1412
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1413
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1414
|
};
|
|
1412
1415
|
|
|
1413
1416
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,6 +1428,7 @@ fn moveHit(
|
|
|
1425
1428
|
fn checkFaint(
|
|
1426
1429
|
battle: anytype,
|
|
1427
1430
|
player: Player,
|
|
1431
|
+
cap: bool,
|
|
1428
1432
|
options: anytype,
|
|
1429
1433
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1434
|
const side = battle.side(player);
|
|
@@ -1437,8 +1441,8 @@ fn checkFaint(
|
|
|
1437
1441
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1442
|
const more = player_out or foe_out;
|
|
1439
1443
|
|
|
1440
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1441
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1444
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1445
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1446
|
|
|
1443
1447
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1448
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1463,7 @@ fn checkFaint(
|
|
|
1459
1463
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1464
|
}
|
|
1461
1465
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1466
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1467
|
var side = battle.side(player);
|
|
1464
1468
|
var foe = battle.foe(player);
|
|
1465
1469
|
assert(side.stored().hp == 0);
|
|
@@ -1493,7 +1497,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1497
|
}
|
|
1494
1498
|
|
|
1495
1499
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1500
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1501
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1502
|
+
if (cap) options.calc.capped(player);
|
|
1497
1503
|
return null;
|
|
1498
1504
|
}
|
|
1499
1505
|
|
|
@@ -1813,7 +1819,6 @@ pub const Effects = struct {
|
|
|
1813
1819
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1820
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1821
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1822
|
}
|
|
1818
1823
|
|
|
1819
1824
|
if (foe.active.volatiles.Confusion) return;
|
|
@@ -1846,10 +1851,7 @@ pub const Effects = struct {
|
|
|
1846
1851
|
const foe_ident = battle.active(player.foe());
|
|
1847
1852
|
|
|
1848
1853
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1849
|
-
if (!showdown)
|
|
1850
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1851
|
-
try options.chance.commit(player, .hit);
|
|
1852
|
-
}
|
|
1854
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1855
|
|
|
1854
1856
|
if (volatiles.disable_move != 0) {
|
|
1855
1857
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -2051,7 +2053,6 @@ pub const Effects = struct {
|
|
|
2051
2053
|
}
|
|
2052
2054
|
}
|
|
2053
2055
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2056
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2057
|
|
|
2057
2058
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2093,10 @@ pub const Effects = struct {
|
|
|
2092
2093
|
if (!has_mimic) {
|
|
2093
2094
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2095
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2096
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2097
|
}
|
|
2098
2098
|
}
|
|
2099
2099
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2100
|
|
|
2102
2101
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2102
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2150,6 @@ pub const Effects = struct {
|
|
|
2151
2150
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2151
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2152
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2153
|
|
|
2156
2154
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2155
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2217,6 @@ pub const Effects = struct {
|
|
|
2219
2217
|
|
|
2220
2218
|
if (move.effect == .Poison) {
|
|
2221
2219
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2220
|
} else {
|
|
2224
2221
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2222
|
return;
|
|
@@ -2288,9 +2285,8 @@ pub const Effects = struct {
|
|
|
2288
2285
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2286
|
});
|
|
2290
2287
|
}
|
|
2291
|
-
// If
|
|
2288
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2289
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2290
|
}
|
|
2295
2291
|
|
|
2296
2292
|
// Sleep Clause Mod
|
|
@@ -2343,9 +2339,7 @@ pub const Effects = struct {
|
|
|
2343
2339
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2340
|
if (showdown) {
|
|
2345
2341
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2342
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2343
|
battle.last_damage = 0;
|
|
2350
2344
|
} else {
|
|
2351
2345
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2521,7 +2515,6 @@ pub const Effects = struct {
|
|
|
2521
2515
|
} else {
|
|
2522
2516
|
// checkHit already checks for Invulnerable
|
|
2523
2517
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2518
|
}
|
|
2526
2519
|
|
|
2527
2520
|
var stats = &foe.active.stats;
|
|
@@ -2700,7 +2693,13 @@ pub const Rolls = struct {
|
|
|
2700
2693
|
return p1;
|
|
2701
2694
|
}
|
|
2702
2695
|
|
|
2703
|
-
fn criticalHit(
|
|
2696
|
+
fn criticalHit(
|
|
2697
|
+
battle: anytype,
|
|
2698
|
+
player: Player,
|
|
2699
|
+
rate: u8,
|
|
2700
|
+
spurious: bool,
|
|
2701
|
+
options: anytype,
|
|
2702
|
+
) !bool {
|
|
2704
2703
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2705
2704
|
val == .true
|
|
2706
2705
|
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
@@ -2710,7 +2709,7 @@ pub const Rolls = struct {
|
|
|
2710
2709
|
else
|
|
2711
2710
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2712
2711
|
|
|
2713
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2712
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2714
2713
|
return crit;
|
|
2715
2714
|
}
|
|
2716
2715
|
|
|
@@ -2732,17 +2731,19 @@ pub const Rolls = struct {
|
|
|
2732
2731
|
return roll;
|
|
2733
2732
|
}
|
|
2734
2733
|
|
|
2735
|
-
|
|
2734
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2735
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2736
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2736
2737
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2737
2738
|
val == .true
|
|
2738
2739
|
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2739
|
-
battle.rng.hit(player,
|
|
2740
|
+
battle.rng.hit(player, acc)
|
|
2740
2741
|
else if (showdown)
|
|
2741
|
-
battle.rng.chance(
|
|
2742
|
+
battle.rng.chance(uN, acc, 256)
|
|
2742
2743
|
else
|
|
2743
|
-
battle.rng.next() <
|
|
2744
|
+
battle.rng.next() < acc;
|
|
2744
2745
|
|
|
2745
|
-
options.chance.hit(ok,
|
|
2746
|
+
try options.chance.hit(player, ok, acc);
|
|
2746
2747
|
return ok;
|
|
2747
2748
|
}
|
|
2748
2749
|
|
|
@@ -3068,7 +3069,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3068
3069
|
const stored = side.stored();
|
|
3069
3070
|
|
|
3070
3071
|
// While players are not given any input options on the cartridge in these cases,
|
|
3071
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3072
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3072
3073
|
//
|
|
3073
3074
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3074
3075
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3076,7 +3077,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3076
3077
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3077
3078
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3078
3079
|
// strictly correct as the player would not have been presented the option to move or
|
|
3079
|
-
// switch at all
|
|
3080
|
+
// switch at all
|
|
3080
3081
|
if (isForced(active)) {
|
|
3081
3082
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3082
3083
|
n += 1;
|
|
@@ -3095,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3095
3096
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3096
3097
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3097
3098
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3098
|
-
// move normally if sleeping/frozen/bound
|
|
3099
|
+
// move normally if sleeping/frozen/bound
|
|
3099
3100
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3100
3101
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3101
3102
|
{
|
|
@@ -3108,7 +3109,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3109
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3109
3110
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3110
3111
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3111
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3112
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3112
3113
|
if (limited) {
|
|
3113
3114
|
assert(showdown);
|
|
3114
3115
|
assert(side.last_selected_move != .None);
|
package/src/lib/gen2/helpers.zig
CHANGED
|
@@ -65,17 +65,47 @@ pub const Battle = struct {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
pub fn prng(rand: *PSRNG) data.PRNG {
|
|
68
|
-
// FIXME: what is max range in gen2?
|
|
69
68
|
return .{
|
|
70
69
|
.src = .{
|
|
71
70
|
.seed = if (showdown)
|
|
72
71
|
rand.newSeed()
|
|
73
|
-
else
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
else seed: {
|
|
73
|
+
// GLITCH: The initial RNG seed in Gen II link battles is heavily constrained
|
|
74
|
+
// https://www.smogon.com/forums/threads/rng-in-rby-link-battles.3746779
|
|
75
|
+
var seed = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
|
76
|
+
|
|
77
|
+
// Bytes in seed can only range from 0-252, not 0-255
|
|
78
|
+
const max: u8 = 253;
|
|
79
|
+
// Instructions take multiples of 4 cycles to execute
|
|
80
|
+
var timer: u16 = 4 * rand.range(u16, 0, 16384);
|
|
81
|
+
var add: u8 = rand.range(u8, 0, 256);
|
|
82
|
+
var sub: u8 = rand.range(u8, 0, 256);
|
|
83
|
+
var carry: u8 = 0;
|
|
84
|
+
var div: u8 = 0;
|
|
85
|
+
|
|
86
|
+
var i: u8 = 0;
|
|
87
|
+
while (i < seed.len) {
|
|
88
|
+
div = @intCast(timer >> 8);
|
|
89
|
+
// This is effectively @addWithOverflow but with three operands
|
|
90
|
+
const result = @as(u16, add) + @as(u16, div) + @as(u16, carry);
|
|
91
|
+
add = @intCast(result & 255);
|
|
92
|
+
carry = @intFromBool(result > 255);
|
|
93
|
+
timer +%= 44;
|
|
94
|
+
div = @intCast(timer >> 8);
|
|
95
|
+
sub -%= div +% carry;
|
|
96
|
+
|
|
97
|
+
if (sub < max) {
|
|
98
|
+
seed[i] = sub;
|
|
99
|
+
i += 1;
|
|
100
|
+
timer +%= 152;
|
|
101
|
+
carry = 1;
|
|
102
|
+
} else {
|
|
103
|
+
timer +%= 132;
|
|
104
|
+
carry = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
break :seed seed;
|
|
108
|
+
},
|
|
79
109
|
},
|
|
80
110
|
};
|
|
81
111
|
}
|
|
@@ -218,8 +218,9 @@
|
|
|
218
218
|
border-radius: 0 4px 4px 0;
|
|
219
219
|
}
|
|
220
220
|
.moves ul {
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
display: grid;
|
|
222
|
+
grid-template-columns: repeat(2, auto);
|
|
223
|
+
gap: 0 2em;
|
|
223
224
|
}
|
|
224
225
|
.left {
|
|
225
226
|
padding-right: 32px;
|
|
@@ -268,7 +269,7 @@
|
|
|
268
269
|
}
|
|
269
270
|
@media(max-width: 1200px) {
|
|
270
271
|
.moves ul {
|
|
271
|
-
|
|
272
|
+
grid-template-columns: repeat(1, auto);
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
@media(max-width: 1100px) {
|
|
@@ -283,7 +284,7 @@
|
|
|
283
284
|
margin-left: 5px;
|
|
284
285
|
}
|
|
285
286
|
.moves ul {
|
|
286
|
-
|
|
287
|
+
grid-template-columns: repeat(2, auto);
|
|
287
288
|
}
|
|
288
289
|
.left {
|
|
289
290
|
padding-right: 0;
|
|
@@ -309,7 +310,6 @@
|
|
|
309
310
|
justify-content: center;
|
|
310
311
|
}
|
|
311
312
|
.moves ul {
|
|
312
|
-
column-count: 1;
|
|
313
313
|
display: inline-block;
|
|
314
314
|
padding: 0;
|
|
315
315
|
}
|