@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.976cf3c3
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/build.zig +6 -9
- package/package.json +3 -3
- package/src/bin/install-pkmn-engine +44 -25
- package/src/lib/gen1/README.md +4 -0
- package/src/lib/gen1/mechanics.zig +22 -9
package/build.zig
CHANGED
|
@@ -28,7 +28,7 @@ pub fn build(b: *std.Build) !void {
|
|
|
28
28
|
const strip = b.option(bool, "strip", "Strip debugging symbols from binary") orelse false;
|
|
29
29
|
const pic = b.option(bool, "pic", "Force position independent code") orelse false;
|
|
30
30
|
|
|
31
|
-
const cmd = b.findProgram(
|
|
31
|
+
const cmd = b.findProgram(&.{"strip"}, &.{}) catch null;
|
|
32
32
|
|
|
33
33
|
var parser = std.json.Parser.init(b.allocator, false);
|
|
34
34
|
defer parser.deinit();
|
|
@@ -72,7 +72,7 @@ pub fn build(b: *std.Build) !void {
|
|
|
72
72
|
maybeStrip(b, lib, b.getInstallStep(), strip, cmd);
|
|
73
73
|
if (pic) lib.force_pic = pic;
|
|
74
74
|
// Always emit to build/lib because this is where the driver code expects to find it
|
|
75
|
-
// TODO(ziglang/zig#2231): using the following used to work (though
|
|
75
|
+
// TODO(ziglang/zig#2231): using the following used to work (though was hacky):
|
|
76
76
|
//
|
|
77
77
|
// lib.emit_bin = .{ .emit_to = b.fmt("build/lib/{s}", .{addon}) };
|
|
78
78
|
// b.getInstallStep().dependOn(&lib.step);
|
|
@@ -96,13 +96,10 @@ pub fn build(b: *std.Build) !void {
|
|
|
96
96
|
lib.rdynamic = true;
|
|
97
97
|
lib.strip = strip;
|
|
98
98
|
if (pic) lib.force_pic = pic;
|
|
99
|
-
const opt = b.findProgram(
|
|
100
|
-
&[_][]const u8{"wasm-opt"},
|
|
101
|
-
&[_][]const u8{"./node_modules/.bin"},
|
|
102
|
-
) catch null;
|
|
99
|
+
const opt = b.findProgram(&.{"wasm-opt"}, &.{"./node_modules/.bin"}) catch null;
|
|
103
100
|
if (optimize != .Debug and opt != null) {
|
|
104
101
|
const out = b.fmt("build/lib/{s}.wasm", .{name});
|
|
105
|
-
const sh = b.addSystemCommand(
|
|
102
|
+
const sh = b.addSystemCommand(&.{ opt.?, "-O4" });
|
|
106
103
|
sh.addArtifactArg(lib);
|
|
107
104
|
sh.addArg("-o");
|
|
108
105
|
sh.addFileSourceArg(.{ .path = out });
|
|
@@ -233,7 +230,7 @@ fn maybeStrip(
|
|
|
233
230
|
if (mac and artifact.isDynamicLibrary()) return;
|
|
234
231
|
// Assuming GNU strip, which complains "illegal pathname found in archive member"...
|
|
235
232
|
if (!mac and artifact.isStaticLibrary()) return;
|
|
236
|
-
const sh = b.addSystemCommand(
|
|
233
|
+
const sh = b.addSystemCommand(&.{ cmd.?, if (mac) "-x" else "-s" });
|
|
237
234
|
sh.addArtifactArg(artifact);
|
|
238
235
|
step.dependOn(&sh.step);
|
|
239
236
|
}
|
|
@@ -246,7 +243,7 @@ const Config = struct {
|
|
|
246
243
|
cmd: ?[]const u8,
|
|
247
244
|
};
|
|
248
245
|
|
|
249
|
-
const TESTS =
|
|
246
|
+
const TESTS = .{
|
|
250
247
|
"src/lib/common/test.zig",
|
|
251
248
|
"src/lib/gen1/test.zig",
|
|
252
249
|
"src/lib/gen2/test.zig",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pkmn/engine",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.976cf3c3",
|
|
4
4
|
"description": "A minimal, complete, Pokémon battle simulation engine optimized for performance",
|
|
5
5
|
"repository": "github:pkmn/engine",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@pkmn/data": "^0.7.
|
|
60
|
+
"@pkmn/data": "^0.7.36",
|
|
61
61
|
"@pkmn/protocol": "^0.6.11"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@pkmn/eslint-config": "^3.0.0",
|
|
65
65
|
"@pkmn/img": "^0.2.8",
|
|
66
66
|
"@pkmn/sets": "^5.0.4",
|
|
67
|
-
"@pkmn/sim": "0.7.
|
|
67
|
+
"@pkmn/sim": "0.7.36",
|
|
68
68
|
"@types/html-minifier": "^4.0.2",
|
|
69
69
|
"@types/jest": "^29.5.0",
|
|
70
70
|
"@types/minimist": "^1.2.2",
|
|
@@ -15,8 +15,10 @@ const DEV = ZIG_VERSION.dev ? `-dev.${ZIG_VERSION.dev}` : '';
|
|
|
15
15
|
const VERSION = `${ZIG_VERSION.major}.${ZIG_VERSION.minor}.${ZIG_VERSION.patch}${DEV}`;
|
|
16
16
|
const INDEX = 'https://ziglang.org/download/index.json';
|
|
17
17
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
18
|
+
let SILENT = false;
|
|
18
19
|
|
|
19
20
|
const sh = (cmd, args) => execFileSync(cmd, args, {encoding: 'utf8', cwd: ROOT});
|
|
21
|
+
const log = (...args) => { if (!SILENT) console.log(...args); };
|
|
20
22
|
|
|
21
23
|
// This is hardly the browser fetch API, but it works for our primitive needs
|
|
22
24
|
const fetch = url => new Promise((resolve, reject) => {
|
|
@@ -61,7 +63,7 @@ const unpack7 = (input, output) => new Promise((resolve, reject) => {
|
|
|
61
63
|
require.resolve('7zip-min');
|
|
62
64
|
} catch (err) {
|
|
63
65
|
if (err.code !== 'MODULE_NOT_FOUND') throw err;
|
|
64
|
-
|
|
66
|
+
log('Installing 7zip-min package locally to unpack the archive...');
|
|
65
67
|
sh('npm', ['install', '7zip-min', '--no-audit', '--no-save']);
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -83,7 +85,7 @@ const unpack = async (input, output) => {
|
|
|
83
85
|
sh('tar', ['xf', input, '-C', output]);
|
|
84
86
|
return;
|
|
85
87
|
} catch {
|
|
86
|
-
|
|
88
|
+
log('tar command not found, falling back to 7z');
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
// Try to use a 7z binary if we can, and only install 7zip-min as a last resort
|
|
@@ -120,10 +122,10 @@ const find = zig => {
|
|
|
120
122
|
(!ZIG_VERSION.dev || (dev && dev >= ZIG_VERSION.dev)));
|
|
121
123
|
|
|
122
124
|
if (compatible) {
|
|
123
|
-
|
|
125
|
+
log('Found existing compatible Zig executable:', zig_exe);
|
|
124
126
|
return zig_exe;
|
|
125
127
|
} else {
|
|
126
|
-
|
|
128
|
+
log(`Existing Zig executable is not compatible (${version} < ${VERSION}):`, zig_exe);
|
|
127
129
|
}
|
|
128
130
|
} catch (err) {
|
|
129
131
|
if (err.code !== 'ENOENT') throw err;
|
|
@@ -168,11 +170,11 @@ const install = async () => {
|
|
|
168
170
|
let base = path.basename(url);
|
|
169
171
|
const archive = path.join(dir, path.basename(url));
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
log(`Downloading Zig tarball from ${url} to ${archive}`);
|
|
172
174
|
try {
|
|
173
175
|
await download(url, archive, release.shasum);
|
|
174
176
|
|
|
175
|
-
|
|
177
|
+
log(`Extracting tarball and installing to ${dir}`);
|
|
176
178
|
await unpack(archive, dir);
|
|
177
179
|
if (archive.endsWith('.zip')) {
|
|
178
180
|
base = base.slice(0, -4);
|
|
@@ -187,17 +189,17 @@ const install = async () => {
|
|
|
187
189
|
} catch (err) {
|
|
188
190
|
if (err.code !== 'ENOENT') throw err;
|
|
189
191
|
}
|
|
190
|
-
|
|
192
|
+
log(`Zig${version === 'master' ? '' : ` v${version}`} executable:`, zig,
|
|
191
193
|
'(this version of Zig will be retained to speed up future updates)');
|
|
192
194
|
const [pre, post] = tty.isatty(1) ? ['\x1b[1m\x1b[35m', '\x1b[0m'] : ['', ''];
|
|
193
|
-
|
|
195
|
+
log(
|
|
194
196
|
`${pre}Consider moving the ${path.dirname(zig)} directory and adding it to your PATH`,
|
|
195
197
|
`to use Zig on other projects, or delete this if you wish to save space.${post}`
|
|
196
198
|
);
|
|
197
199
|
|
|
198
200
|
return zig;
|
|
199
201
|
} finally {
|
|
200
|
-
|
|
202
|
+
log(`Cleaning up ${archive}`);
|
|
201
203
|
try { fs.rmSync(archive); } catch {}
|
|
202
204
|
}
|
|
203
205
|
};
|
|
@@ -206,17 +208,17 @@ const install = async () => {
|
|
|
206
208
|
const downloadHeaders = async () => {
|
|
207
209
|
const include = path.resolve(process.execPath, '..', '..', 'include', 'node');
|
|
208
210
|
if (fs.existsSync(path.join(include, 'node.h'))) {
|
|
209
|
-
|
|
211
|
+
log('Node headers:', include);
|
|
210
212
|
return include;
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
const dir = path.join(ROOT, 'build', 'include');
|
|
214
216
|
const dest = path.join(dir, 'node');
|
|
215
217
|
if (fs.existsSync(path.join(dest, 'node.h'))) {
|
|
216
|
-
|
|
218
|
+
log('Node headers:', dest);
|
|
217
219
|
return dest;
|
|
218
220
|
} else {
|
|
219
|
-
|
|
221
|
+
log(`Could not find Node headers at ${include}, downloading...`);
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
try {
|
|
@@ -229,14 +231,14 @@ const downloadHeaders = async () => {
|
|
|
229
231
|
const targz = path.join(dir, path.basename(url));
|
|
230
232
|
const unpacked = path.join(dir, `node-${process.version}`, 'include');
|
|
231
233
|
|
|
232
|
-
|
|
234
|
+
log(`Downloading Node headers from ${url} to ${targz}`);
|
|
233
235
|
|
|
234
236
|
try {
|
|
235
237
|
await download(url, targz);
|
|
236
238
|
await unpack(targz, dir);
|
|
237
239
|
fs.renameSync(path.join(unpacked, 'node'), dest);
|
|
238
240
|
|
|
239
|
-
|
|
241
|
+
log('Node headers:', dest);
|
|
240
242
|
|
|
241
243
|
return dest;
|
|
242
244
|
} finally {
|
|
@@ -248,7 +250,7 @@ const downloadHeaders = async () => {
|
|
|
248
250
|
const downloadImportLib = async () => {
|
|
249
251
|
const library = path.resolve(process.execPath, '..', 'node.lib');
|
|
250
252
|
if (fs.existsSync(path.join(library, 'node.h'))) {
|
|
251
|
-
|
|
253
|
+
log('Node import library:', library);
|
|
252
254
|
return library;
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -256,10 +258,10 @@ const downloadImportLib = async () => {
|
|
|
256
258
|
const url = new URL('./win-x64/node.lib', process.release.headersUrl).href;
|
|
257
259
|
const dest = path.join(dir, path.basename(url));
|
|
258
260
|
if (fs.existsSync(path.join(dest))) {
|
|
259
|
-
|
|
261
|
+
log('Node import library:', dest);
|
|
260
262
|
return dest;
|
|
261
263
|
} else {
|
|
262
|
-
|
|
264
|
+
log(`Could not find Node import library at ${library}, downloading...`);
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
try {
|
|
@@ -268,24 +270,41 @@ const downloadImportLib = async () => {
|
|
|
268
270
|
if (err.code !== 'EEXIST') throw err;
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
|
|
273
|
+
log(`Downloading Node import library from ${url} to ${dest}`);
|
|
272
274
|
await download(url, dest);
|
|
273
|
-
|
|
275
|
+
log('Node import library:', dest);
|
|
274
276
|
|
|
275
277
|
return dest;
|
|
276
278
|
};
|
|
277
279
|
|
|
278
280
|
(async () => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
let only = false;
|
|
282
|
+
let options = undefined;
|
|
283
|
+
let error = false;
|
|
284
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
285
|
+
switch (process.argv[i]) {
|
|
286
|
+
case "--silent": {
|
|
287
|
+
SILENT = true;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "--zig": {
|
|
291
|
+
if (options || only) error = true;
|
|
292
|
+
only = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
default: {
|
|
296
|
+
if (options || only || !process.argv[i].startsWith('--options=')) error = true;
|
|
297
|
+
options = process.argv[i].slice(10).split(' ');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (error) {
|
|
283
302
|
console.error('Invalid arguments');
|
|
284
303
|
process.exit(1);
|
|
285
304
|
}
|
|
286
305
|
|
|
287
306
|
const zig = await install();
|
|
288
|
-
if (
|
|
307
|
+
if (only) process.exit(0);
|
|
289
308
|
|
|
290
309
|
let tmp;
|
|
291
310
|
try {
|
|
@@ -355,7 +374,7 @@ const downloadImportLib = async () => {
|
|
|
355
374
|
opts.includes('-Dshowdown') || opts.includes('-Dshowdown=true') ? '-showdown' : '';
|
|
356
375
|
const flags = opts.length ? ` (${opts.join(' ')})` : '';
|
|
357
376
|
const artifact = path.join('build', 'lib', `pkmn${suffix}.${config.type}`);
|
|
358
|
-
|
|
377
|
+
log(`Building ${artifact}${flags}`);
|
|
359
378
|
sh(zig, args);
|
|
360
379
|
// TODO: ziglang/zig#14647 broke emit_to so we get to rename the file ourselves...
|
|
361
380
|
if (config.type === 'node') {
|
package/src/lib/gen1/README.md
CHANGED
|
@@ -493,6 +493,10 @@ the pkmn engine, but the following moves have their broken behavior preserved in
|
|
|
493
493
|
instead cleared in Pokémon Showdown's "residual" phase, meaning the flinch status gets erroneously
|
|
494
494
|
preserved across fainting (as fainting triggers Pokémon Showdown's "instaswitch" behavior which
|
|
495
495
|
skips end-of-turn residuals).
|
|
496
|
+
- **Rage**: Rage boosts should still result in burn/paralysis [stat modification
|
|
497
|
+
errors](https://pkmn.cc/bulba-glitch-1#Stat_modification_errors) but don't on Pokémon Showdown.
|
|
498
|
+
Furthermore, Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking
|
|
499
|
+
into a Substitute instead of all moves.
|
|
496
500
|
- **Thrash** / **Petal Dance**: once a Pokémon is locked into a thrashing move they will lose all
|
|
497
501
|
speed ties on Pokémon Showdown (due to the fact that if they win the speed tie their action will
|
|
498
502
|
then get "changed" and inserted back into the queue after their opponent's action).
|
|
@@ -620,8 +620,9 @@ fn beforeMove(
|
|
|
620
620
|
return .done;
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
623
624
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
624
|
-
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
625
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
625
626
|
|
|
626
627
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
627
628
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -894,6 +895,9 @@ fn doMove(
|
|
|
894
895
|
hits = side.active.volatiles.attacks;
|
|
895
896
|
}
|
|
896
897
|
|
|
898
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
899
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
900
|
+
|
|
897
901
|
var nullified = false;
|
|
898
902
|
var hit: u4 = 0;
|
|
899
903
|
while (hit < hits) {
|
|
@@ -909,7 +913,7 @@ fn doMove(
|
|
|
909
913
|
if (hit == 0 and ohko) try log.ohko();
|
|
910
914
|
hit += 1;
|
|
911
915
|
if (foe.stored().hp == 0) break;
|
|
912
|
-
if (!late) try buildRage(battle, player.foe(), log);
|
|
916
|
+
if (!late and !sub) try buildRage(battle, player.foe(), log);
|
|
913
917
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
914
918
|
if (nullified) break;
|
|
915
919
|
}
|
|
@@ -946,8 +950,8 @@ fn doMove(
|
|
|
946
950
|
|
|
947
951
|
if (foe.stored().hp == 0) return null;
|
|
948
952
|
|
|
949
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
950
|
-
if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
953
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
954
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
951
955
|
|
|
952
956
|
if (!move.effect.isSpecial()) {
|
|
953
957
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -1100,11 +1104,12 @@ fn randomizeDamage(battle: anytype) void {
|
|
|
1100
1104
|
|
|
1101
1105
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1102
1106
|
const side = battle.side(player);
|
|
1107
|
+
const foe = battle.foe(player);
|
|
1103
1108
|
|
|
1104
1109
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1105
1110
|
|
|
1106
1111
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1107
|
-
.SuperFang => @max(
|
|
1112
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1108
1113
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1109
1114
|
.SonicBoom => 20,
|
|
1110
1115
|
.DragonRage => 40,
|
|
@@ -1127,8 +1132,9 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1127
1132
|
|
|
1128
1133
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1129
1134
|
|
|
1135
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1130
1136
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1131
|
-
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1137
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1132
1138
|
|
|
1133
1139
|
return null;
|
|
1134
1140
|
}
|
|
@@ -2343,6 +2349,8 @@ pub const Effects = struct {
|
|
|
2343
2349
|
const stat = unmodifiedStats(battle, player).atk;
|
|
2344
2350
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2351
|
try log.boost(ident, reason, n);
|
|
2352
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2353
|
+
if (showdown and move.effect == .Rage) return;
|
|
2346
2354
|
},
|
|
2347
2355
|
.DefenseUp1, .DefenseUp2 => {
|
|
2348
2356
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2593,9 +2601,14 @@ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
|
2593
2601
|
if (side.active.volatiles.disabled_move != 0) {
|
|
2594
2602
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2595
2603
|
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2596
|
-
|
|
2604
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
2605
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
2606
|
+
m
|
|
2607
|
+
else
|
|
2608
|
+
side.last_selected_move;
|
|
2609
|
+
if (m != .None and m == last) {
|
|
2597
2610
|
side.active.volatiles.Charging = false;
|
|
2598
|
-
try log.disabled(ident,
|
|
2611
|
+
try log.disabled(ident, last);
|
|
2599
2612
|
return true;
|
|
2600
2613
|
}
|
|
2601
2614
|
}
|
|
@@ -2757,7 +2770,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2757
2770
|
while (slot <= 4) : (slot += 1) {
|
|
2758
2771
|
const m = active.moves[slot - 1];
|
|
2759
2772
|
if (m.id == .None) break;
|
|
2760
|
-
if (m.id == side.last_selected_move) {
|
|
2773
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2761
2774
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2762
2775
|
const struggle =
|
|
2763
2776
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|