dotframe 0.1.6 → 0.1.7
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/CHANGELOG.md +14 -0
- package/cli/commands/docker.ts +179 -0
- package/cli/commands/doctor.ts +70 -7
- package/cli/commands/online.ts +93 -0
- package/cli/commands/play.ts +17 -3
- package/cli/commands/ship.ts +23 -9
- package/cli/ios.ts +271 -0
- package/cli/lib.ts +4 -2
- package/cli/main.ts +44 -11
- package/cli/native.ts +10 -5
- package/cli/relay.ts +65 -0
- package/native/df_native.c +3 -0
- package/package.json +1 -1
- package/scripts/vendor.sh +11 -5
- package/skill-data/core/SKILL.md +6 -0
- package/skill-data/export-web/SKILL.md +14 -0
- package/skill-data/ios/SKILL.md +45 -10
- package/skill-data/netplay/SKILL.md +15 -0
- package/src/checksum.ts +22 -0
- package/src/detmath.ts +217 -0
- package/src/netplay.ts +232 -0
- package/src/relay-client.ts +42 -0
- package/templates/_base/dotframe.json +10 -1
- package/templates/_base/main.ios.ts +19 -0
- package/templates/_base/main.native.ts +2 -1
- package/templates/_base/main.web.ts +9 -1
- package/templates/_base/sim.ts +2 -4
- package/templates/_base/src/setup.ts +87 -8
- package/templates/_base/tsconfig.json +1 -1
- package/templates/blank/game.ts +8 -1
- package/templates/fighter/game.ts +13 -1
- package/templates/platformer/game.ts +25 -1
package/templates/_base/sim.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The dotframe CLI contract: dotframe sim, snap, replay, and desync drive the game through this module.
|
|
2
2
|
import { defineSim, type SimPlatform, type SimRun } from "dotframe/src/sim";
|
|
3
|
-
import { checksum, createGame,
|
|
3
|
+
import { checksum, createGame, encode, type Game, PLAYERS, randomInput, render, restore, snapshot, state, step, WINDOW } from "./src/game";
|
|
4
4
|
|
|
5
5
|
export default defineSim({
|
|
6
6
|
players: PLAYERS,
|
|
@@ -23,9 +23,7 @@ export default defineSim({
|
|
|
23
23
|
state: (): unknown => state(game),
|
|
24
24
|
over: (): boolean => game.winner >= 0,
|
|
25
25
|
save: (): unknown => snapshot(game),
|
|
26
|
-
restore: (saved: unknown): void =>
|
|
27
|
-
Object.assign(game, snapshot(saved as Game));
|
|
28
|
-
},
|
|
26
|
+
restore: (saved: unknown): void => restore(game, saved as Game),
|
|
29
27
|
inspect: (): unknown => game,
|
|
30
28
|
render: (draw) => render(game, draw),
|
|
31
29
|
};
|
|
@@ -1,18 +1,61 @@
|
|
|
1
1
|
// Shared by main.web.ts and main.native.ts: keyboard and pointer to inputs, fixed 60 Hz steps, one render per frame.
|
|
2
|
+
// Online, the same steps go through dotframe's rollback netplay instead of straight to step().
|
|
2
3
|
import { createDraw2D } from "dotframe/src/draw2d";
|
|
3
|
-
import type {
|
|
4
|
+
import type { AudioPlayer } from "dotframe/src/audio";
|
|
5
|
+
import type { Frame, RenderGpu } from "dotframe/src/gpu";
|
|
6
|
+
import type { Input } from "dotframe/src/input";
|
|
4
7
|
import { Key, MouseButton } from "dotframe/src/input";
|
|
5
|
-
import type
|
|
6
|
-
import { createGame, keyInput, PLAYERS, POINTER_BIT, render, step, WINDOW } from "./game";
|
|
8
|
+
import { createRollback, type RelayLink, type Rollback } from "dotframe/src/netplay";
|
|
9
|
+
import { checksum, createGame, type Game, keyInput, PLAYERS, POINTER_BIT, randomInput, render, restore, snapshot, step, WINDOW } from "./game";
|
|
10
|
+
|
|
11
|
+
export interface SetupOptions {
|
|
12
|
+
// An open relay link plays online as the slot the relay assigns; null plays locally.
|
|
13
|
+
link: RelayLink | null;
|
|
14
|
+
// A seed makes player 1 a scripted masher (tests); null reads the keyboard and pointer.
|
|
15
|
+
mash: number | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// What `dotframe play --online` reads from each browser.
|
|
19
|
+
interface Probe {
|
|
20
|
+
frame: number;
|
|
21
|
+
confirmed: number;
|
|
22
|
+
status: string;
|
|
23
|
+
sums: Record<number, number>;
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
const STEP = 1 / 60;
|
|
9
27
|
// A full charge takes one second of holding.
|
|
10
28
|
const FULL_CHARGE = 60;
|
|
11
29
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
// What setup needs from a platform: the async web and native platforms and iOS's library platform all have it.
|
|
31
|
+
export interface SetupPlatform {
|
|
32
|
+
gpu: RenderGpu;
|
|
33
|
+
input: Input;
|
|
34
|
+
audio: AudioPlayer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createSetup(options: SetupOptions): (platform: SetupPlatform) => Frame {
|
|
38
|
+
return ({ gpu, input, audio }: SetupPlatform): Frame => {
|
|
39
|
+
// The logical canvas keeps the game's aspect and grows to fill the screen: on a 2.16 phone the game area is
|
|
40
|
+
// centered with extra width around it instead of being stretched.
|
|
41
|
+
const aspect = gpu.aspect();
|
|
42
|
+
const logicalWidth = Math.max(WINDOW.width, Math.round(WINDOW.height * aspect));
|
|
43
|
+
const logicalHeight = Math.max(WINDOW.height, Math.round(WINDOW.width / aspect));
|
|
44
|
+
const offsetX = (logicalWidth - WINDOW.width) / 2;
|
|
45
|
+
const offsetY = (logicalHeight - WINDOW.height) / 2;
|
|
46
|
+
const draw = createDraw2D(gpu, logicalWidth, logicalHeight);
|
|
47
|
+
// Online peers must start from the same state, so the seed is fixed there.
|
|
48
|
+
const game: Game = createGame(options.link ? 1 : Math.floor(Math.random() * 1e9));
|
|
49
|
+
let rollback: Rollback | null = null;
|
|
50
|
+
let mashState = options.mash ?? 0;
|
|
51
|
+
const nextRandom = (): number => {
|
|
52
|
+
mashState = (Math.imul(mashState, 1103515245) + 12345) >>> 0;
|
|
53
|
+
return mashState / 4294967296;
|
|
54
|
+
};
|
|
55
|
+
let mashed = 0;
|
|
56
|
+
let mashInput = 0;
|
|
57
|
+
const probe: Probe = { frame: 0, confirmed: 0, status: options.link ? "connecting" : "local", sums: {} };
|
|
58
|
+
(globalThis as { __dotframe?: Probe }).__dotframe = probe;
|
|
16
59
|
let simulated = -1;
|
|
17
60
|
// Hold to charge, release to act: the pattern most mouse and touch games need.
|
|
18
61
|
let charge = 0;
|
|
@@ -34,16 +77,52 @@ export function createSetup(): Setup {
|
|
|
34
77
|
wasDown = down;
|
|
35
78
|
const keys = [keyInput(input, [Key.A, Key.D, Key.W, Key.J]), keyInput(input, [Key.Left, Key.Right, Key.Up, Key.L])];
|
|
36
79
|
if (released) keys[0] = keys[0] | POINTER_BIT;
|
|
37
|
-
|
|
80
|
+
if (options.mash !== null) {
|
|
81
|
+
// A new random input every 6 frames, like a mashing player.
|
|
82
|
+
if (mashed % 6 === 0) mashInput = randomInput(nextRandom);
|
|
83
|
+
keys[0] = mashInput;
|
|
84
|
+
mashed += 1;
|
|
85
|
+
}
|
|
86
|
+
const link = options.link;
|
|
87
|
+
if (link) {
|
|
88
|
+
probe.status = link.status();
|
|
89
|
+
if (!rollback && link.status() === "paired") {
|
|
90
|
+
rollback = createRollback({
|
|
91
|
+
sim: {
|
|
92
|
+
step: (inputs: number[]): void => step(game, inputs),
|
|
93
|
+
save: (): Game => snapshot(game),
|
|
94
|
+
restore: (saved: Game): void => restore(game, saved),
|
|
95
|
+
checksum: (): number => checksum(game),
|
|
96
|
+
},
|
|
97
|
+
transport: link.transport,
|
|
98
|
+
localPort: link.slot(),
|
|
99
|
+
neutral: 0,
|
|
100
|
+
inputDelay: 2,
|
|
101
|
+
maxRollback: 20,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Online, each browser controls its own player with player 1's keys.
|
|
105
|
+
if (rollback) {
|
|
106
|
+
rollback.tick(keys[0]);
|
|
107
|
+
probe.frame = rollback.stats().frame;
|
|
108
|
+
probe.confirmed = rollback.confirmedFrame();
|
|
109
|
+
for (let f = 30; f < probe.confirmed; f += 30) if (probe.sums[f] === undefined) probe.sums[f] = rollback.sumAt(f) ?? 0;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
step(game, keys.slice(0, PLAYERS));
|
|
113
|
+
probe.frame += 1;
|
|
114
|
+
}
|
|
38
115
|
simulated += STEP;
|
|
39
116
|
}
|
|
40
117
|
draw.begin();
|
|
118
|
+
draw.translate(offsetX, offsetY);
|
|
41
119
|
render(game, draw);
|
|
42
120
|
if (charge > 0) {
|
|
43
121
|
// Charge meter under the play area.
|
|
44
122
|
draw.setFillStyle("#ffffff");
|
|
45
123
|
draw.fillRect(20, WINDOW.height - 16, (WINDOW.width - 40) * (charge / FULL_CHARGE), 6);
|
|
46
124
|
}
|
|
125
|
+
draw.resetTransform();
|
|
47
126
|
draw.end({ r: 0, g: 0, b: 0 });
|
|
48
127
|
return true;
|
|
49
128
|
};
|
package/templates/blank/game.ts
CHANGED
|
@@ -52,8 +52,15 @@ export function state(game: Game): unknown {
|
|
|
52
52
|
return game;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Field by field (scriptc compiles neither structuredClone nor Object.assign): netplay saves every frame.
|
|
55
56
|
export function snapshot(game: Game): Game {
|
|
56
|
-
return
|
|
57
|
+
return { frame: game.frame, players: game.players.map((p) => ({ x: p.x, y: p.y })), winner: game.winner };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function restore(game: Game, saved: Game): void {
|
|
61
|
+
game.frame = saved.frame;
|
|
62
|
+
game.players = saved.players.map((p) => ({ x: p.x, y: p.y }));
|
|
63
|
+
game.winner = saved.winner;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
export function render(game: Game, draw: Draw2D): void {
|
|
@@ -114,8 +114,20 @@ export function state(game: Game): unknown {
|
|
|
114
114
|
return { frame: game.frame, winner: game.winner, fighters: game.fighters.map(({ x, y, damage, stocks }) => ({ x: Math.round(x), y: Math.round(y), damage, stocks })) };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// Field by field (scriptc compiles neither structuredClone nor Object.assign): netplay saves every frame.
|
|
118
|
+
function copyFighter(f: Fighter): Fighter {
|
|
119
|
+
return { x: f.x, y: f.y, vx: f.vx, vy: f.vy, facing: f.facing, damage: f.damage, stocks: f.stocks, cooldown: f.cooldown, stun: f.stun, held: f.held };
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
export function snapshot(game: Game): Game {
|
|
118
|
-
return
|
|
123
|
+
return { frame: game.frame, seed: game.seed, fighters: game.fighters.map(copyFighter), winner: game.winner };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function restore(game: Game, saved: Game): void {
|
|
127
|
+
game.frame = saved.frame;
|
|
128
|
+
game.seed = saved.seed;
|
|
129
|
+
game.fighters = saved.fighters.map(copyFighter);
|
|
130
|
+
game.winner = saved.winner;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
const COLORS = ["#ff5a5f", "#3fa7ff"];
|
|
@@ -102,8 +102,32 @@ export function state(game: Game): unknown {
|
|
|
102
102
|
return { frame: game.frame, x: Math.round(game.x), y: Math.round(game.y), coins: game.coins.filter((c) => c.taken).length, of: game.coins.length, won: game.winner === 0 };
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Field by field (scriptc compiles neither structuredClone nor Object.assign): netplay saves every frame.
|
|
105
106
|
export function snapshot(game: Game): Game {
|
|
106
|
-
return
|
|
107
|
+
return {
|
|
108
|
+
frame: game.frame,
|
|
109
|
+
x: game.x,
|
|
110
|
+
y: game.y,
|
|
111
|
+
vx: game.vx,
|
|
112
|
+
vy: game.vy,
|
|
113
|
+
onGround: game.onGround,
|
|
114
|
+
held: game.held,
|
|
115
|
+
coins: game.coins.map((c) => ({ x: c.x, y: c.y, taken: c.taken })),
|
|
116
|
+
winner: game.winner,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function restore(game: Game, saved: Game): void {
|
|
121
|
+
const s = snapshot(saved);
|
|
122
|
+
game.frame = s.frame;
|
|
123
|
+
game.x = s.x;
|
|
124
|
+
game.y = s.y;
|
|
125
|
+
game.vx = s.vx;
|
|
126
|
+
game.vy = s.vy;
|
|
127
|
+
game.onGround = s.onGround;
|
|
128
|
+
game.held = s.held;
|
|
129
|
+
game.coins = s.coins;
|
|
130
|
+
game.winner = s.winner;
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
export function render(game: Game, draw: Draw2D): void {
|