create-bloop 0.0.18 → 0.0.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bloop",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Create a new Bloop game",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.js",
@@ -13,8 +13,8 @@
13
13
  "vite": "^7.2.2"
14
14
  },
15
15
  "dependencies": {
16
- "@bloopjs/bloop": "^0.0.90",
16
+ "@bloopjs/bloop": "^0.0.91",
17
17
  "@bloopjs/toodle": "^0.1.3",
18
- "@bloopjs/web": "^0.0.90"
18
+ "@bloopjs/web": "^0.0.91"
19
19
  }
20
20
  }
@@ -15,8 +15,8 @@
15
15
  "vite": "^7.2.2"
16
16
  },
17
17
  "dependencies": {
18
- "@bloopjs/bloop": "^0.0.90",
18
+ "@bloopjs/bloop": "^0.0.91",
19
19
  "@bloopjs/toodle": "^0.1.3",
20
- "@bloopjs/web": "^0.0.90"
20
+ "@bloopjs/web": "^0.0.91"
21
21
  }
22
22
  }
@@ -5,11 +5,11 @@ export const MOVE_SPEED = 3;
5
5
  export const MAX_FALL_SPEED = 10;
6
6
 
7
7
  // World (0,0 is center of screen, Y+ is up)
8
- export const GROUND_Y = -100;
8
+ export const GROUND_Y = -80;
9
9
  export const BLOCK_Y = -10;
10
10
  export const BLOCK_SPEED = 1;
11
- export const BLOCK_MIN_X = -100;
12
- export const BLOCK_MAX_X = 100;
11
+ export const BLOCK_MIN_X = -55;
12
+ export const BLOCK_MAX_X = 55;
13
13
 
14
14
  // Player sizes
15
15
  export const PLAYER_WIDTH = 16;
@@ -18,8 +18,8 @@ export const BLOCK_SIZE = 16;
18
18
  export const COIN_SIZE = 12;
19
19
 
20
20
  // Starting positions
21
- export const P1_START_X = -60;
22
- export const P2_START_X = 60;
21
+ export const P1_START_X = -55;
22
+ export const P2_START_X = 55;
23
23
 
24
24
  // Animation timings
25
25
  export const COIN_VISIBLE_DURATION = 0.8; // seconds
@@ -1,11 +1,5 @@
1
1
  import { unwrap } from "@bloopjs/bloop";
2
- import type {
3
- Color,
4
- QuadNode,
5
- SceneNode,
6
- TextNode,
7
- Toodle,
8
- } from "@bloopjs/toodle";
2
+ import type { Color, Toodle } from "@bloopjs/toodle";
9
3
  import { Colors } from "@bloopjs/toodle";
10
4
  import {
11
5
  BLOCK_SIZE,
@@ -20,253 +14,202 @@ import type { game, Player, Pose } from "./game";
20
14
  // Placeholder colors until sprites are added
21
15
  const MARIO_COLOR = Colors.web.red;
22
16
  const LUIGI_COLOR = Colors.web.green;
23
- const BLOCK_COLOR = Colors.web.sienna;
24
17
  const COIN_COLOR = Colors.web.gold;
25
18
  const GROUND_COLOR = Colors.web.saddleBrown;
26
19
 
27
- /** Quads for each pose animation */
28
- type PoseQuads = Record<Pose, QuadNode>;
29
-
30
- export interface DrawState {
31
- root: SceneNode;
32
- // Screen containers
33
- titleScreen: SceneNode;
34
- gameScreen: SceneNode;
35
- // Game elements (under gameScreen)
36
- ground: QuadNode;
37
- block: SceneNode;
38
- coin: QuadNode;
39
- p1: PoseQuads;
40
- p2: PoseQuads;
41
- viewport: SceneNode;
42
- p1Score: TextNode;
43
- p2Score: TextNode;
44
- // Title elements (under titleScreen)
45
- titleText: TextNode;
46
- subtitleText: TextNode;
47
- }
20
+ // Map pose to texture name
21
+ const POSE_TEXTURES: Record<Pose, string> = {
22
+ idle: "marioIdle",
23
+ run: "marioWalk",
24
+ jump: "marioJump",
25
+ skid: "marioSkid",
26
+ };
48
27
 
49
- export function createDrawState(toodle: Toodle): DrawState {
50
- const root = toodle.Node({ scale: 3 });
28
+ export function draw(g: typeof game, toodle: Toodle) {
29
+ const { bag } = g.context;
30
+ const isMobile = toodle.resolution.width < toodle.resolution.height;
51
31
 
52
- // Title screen container
53
- const titleScreen = root.add(toodle.Node({}));
54
- const titleText = titleScreen.add(
55
- toodle.Text("Roboto", "MARIO ROLLBACK", {
56
- fontSize: 24,
57
- color: { r: 1, g: 1, b: 1, a: 1 },
58
- position: { x: 0, y: 20 },
59
- }),
60
- );
61
- const subtitleText = titleScreen.add(
62
- toodle.Text("Roboto", "[Space] Local [Enter] Online", {
63
- fontSize: 10,
64
- color: { r: 1, g: 1, b: 1, a: 1 },
65
- position: { x: 0, y: 0 },
66
- }),
67
- );
32
+ toodle.startFrame();
68
33
 
69
- // Game screen container
70
- const gameScreen = root.add(toodle.Node({}));
71
- gameScreen.isActive = false;
34
+ const root = toodle.Node();
35
+
36
+ toodle.camera.zoom = 3;
37
+
38
+ if (bag.phase !== "playing") {
39
+ // Title screen
40
+ const titleScreen = root.add(toodle.Node({}));
41
+
42
+ titleScreen.add(
43
+ toodle.Text("Roboto", "Mario Rollback", {
44
+ fontSize: 20,
45
+ align: "center",
46
+ color: { r: 1, g: 1, b: 1, a: 1 },
47
+ position: { x: 0, y: 20 },
48
+ size: {
49
+ width: toodle.resolution.width - 100,
50
+ height: toodle.resolution.height,
51
+ },
52
+ shrinkToFit: {
53
+ minFontSize: 4,
54
+ },
55
+ }),
56
+ );
72
57
 
73
- const ground = gameScreen.add(
74
- toodle.shapes.Rect({
75
- size: { width: 400, height: 40 },
76
- position: { x: 0, y: GROUND_Y - 20 },
77
- color: GROUND_COLOR,
78
- }),
79
- );
58
+ const subtitleText =
59
+ bag.phase === "waiting"
60
+ ? "Waiting for opponent..."
61
+ : isMobile
62
+ ? "Tap to find opponent"
63
+ : "[Enter/Click] Online [Space] Local";
64
+
65
+ titleScreen.add(
66
+ toodle.Text("Roboto", subtitleText, {
67
+ fontSize: 10,
68
+ color: { r: 1, g: 1, b: 1, a: 1 },
69
+ }),
70
+ );
71
+ } else {
72
+ // Game screen
73
+ const gameScreen = root.add(toodle.Node({}));
80
74
 
81
- const block = gameScreen.add(
82
- toodle.Quad("brick", {
83
- size: { width: BLOCK_SIZE, height: BLOCK_SIZE },
84
- }),
85
- );
75
+ const viewport = toodle.Node({
76
+ size: {
77
+ width: toodle.resolution.width,
78
+ height: toodle.resolution.height,
79
+ },
80
+ });
86
81
 
87
- const coin = gameScreen.add(
88
- toodle.shapes.Circle({
89
- radius: COIN_SIZE / 2,
90
- color: COIN_COLOR,
91
- }),
92
- );
82
+ // Ground
83
+ const ground = gameScreen.add(
84
+ toodle.shapes.Rect({
85
+ size: { width: viewport.size!.width, height: 1000 },
86
+ color: GROUND_COLOR,
87
+ }),
88
+ );
89
+ ground.setBounds({ top: GROUND_Y });
90
+
91
+ // Block
92
+ gameScreen.add(
93
+ toodle.Quad("brick", {
94
+ size: { width: BLOCK_SIZE, height: BLOCK_SIZE },
95
+ position: { x: bag.block.x, y: BLOCK_Y + BLOCK_SIZE / 2 },
96
+ }),
97
+ );
93
98
 
94
- // Map pose to texture name
95
- const poseTextures: Record<Pose, string> = {
96
- idle: "marioIdle",
97
- run: "marioWalk",
98
- jump: "marioJump",
99
- skid: "marioSkid",
100
- };
101
-
102
- // Create pose quads for a player
103
- const createPoseQuads = (color?: typeof LUIGI_COLOR): PoseQuads => {
104
- const poses = {} as PoseQuads;
105
- for (const pose of ["idle", "run", "jump", "skid"] satisfies Pose[]) {
106
- const quad = gameScreen.add(
107
- toodle.Quad(poseTextures[pose], {
108
- size: { width: PLAYER_WIDTH, height: PLAYER_HEIGHT },
109
- region: { x: 0, y: 0, width: PLAYER_WIDTH, height: PLAYER_HEIGHT },
110
- color,
99
+ // Coin
100
+ if (bag.coin.visible) {
101
+ const coinColor =
102
+ bag.coin.winner === 1
103
+ ? MARIO_COLOR
104
+ : bag.coin.winner === 2
105
+ ? LUIGI_COLOR
106
+ : COIN_COLOR;
107
+
108
+ gameScreen.add(
109
+ toodle.shapes.Circle({
110
+ radius: COIN_SIZE / 2,
111
+ color: coinColor,
112
+ position: { x: bag.coin.x, y: bag.coin.y },
111
113
  }),
112
114
  );
113
- quad.isActive = false; // Start inactive, draw will activate the right one
114
- poses[pose] = quad;
115
115
  }
116
- return poses;
117
- };
118
-
119
- const p1 = createPoseQuads();
120
- const p2 = createPoseQuads(LUIGI_COLOR);
121
-
122
- const p1Score = gameScreen.add(
123
- toodle.Text("Roboto", "P9: 0", {
124
- fontSize: 16,
125
- color: MARIO_COLOR,
126
- }),
127
- );
128
-
129
- const p2Score = gameScreen.add(
130
- toodle.Text("Roboto", "P2: 0", {
131
- fontSize: 16,
132
- color: LUIGI_COLOR,
133
- }),
134
- );
135
-
136
- const viewport = toodle.Node({
137
- size: { width: toodle.resolution.width, height: toodle.resolution.height },
138
- });
139
-
140
- return {
141
- root,
142
- titleScreen,
143
- gameScreen,
144
- ground,
145
- block,
146
- coin,
147
- p1,
148
- p2,
149
- p1Score,
150
- p2Score,
151
- titleText,
152
- subtitleText,
153
- viewport,
154
- };
155
- }
156
-
157
- export function draw(g: typeof game, toodle: Toodle, state: DrawState) {
158
- const { bag } = g.context;
159
116
 
160
- // Toggle screens
161
- state.titleScreen.isActive = bag.phase !== "playing";
162
- state.gameScreen.isActive = bag.phase === "playing";
163
-
164
- // Update title text based on phase
165
- if (bag.phase === "title") {
166
- const isMobile = toodle.resolution.width < toodle.resolution.height;
167
- state.subtitleText.text = isMobile
168
- ? "Tap to find opponent"
169
- : "[Enter/Click] Online [Space] Local";
170
- } else if (bag.phase === "waiting") {
171
- state.subtitleText.text = "Waiting for opponent...";
172
- }
173
-
174
- // Update game positions only when playing
175
- if (bag.phase === "playing") {
176
- state.block.position = { x: bag.block.x, y: BLOCK_Y + BLOCK_SIZE / 2 };
177
-
178
- state.coin.position = {
179
- x: bag.coin.x,
180
- y: bag.coin.y,
181
- };
182
- state.coin.isActive = bag.coin.visible;
183
- state.coin.color =
184
- bag.coin.winner === 1
185
- ? MARIO_COLOR
186
- : bag.coin.winner === 2
187
- ? LUIGI_COLOR
188
- : COIN_COLOR;
189
- // Update player sprites
190
- updatePlayerQuads(state.p1, bag.p1);
191
- updatePlayerQuads(state.p2, bag.p2);
117
+ // Players
118
+ const p1Quad = drawPlayer(toodle, gameScreen, bag.p1);
119
+ const p2Quad = drawPlayer(toodle, gameScreen, bag.p2, LUIGI_COLOR);
192
120
 
121
+ // Scores
193
122
  const padding = 20;
194
123
 
195
- state.p1Score.text = `P1: ${bag.p1.score}`;
196
- state.p2Score.text = `P2: ${bag.p2.score}`;
197
-
198
- state.viewport.size = {
199
- width: toodle.resolution.width,
200
- height: toodle.resolution.height,
201
- };
202
-
203
- state.ground.size.width = state.viewport.size.width;
204
-
205
- state.p1Score.setBounds({
206
- left: state.viewport.bounds.left + padding,
207
- top: state.viewport.bounds.top - padding,
124
+ const p1Score = gameScreen.add(
125
+ toodle.Text("Roboto", `P1: ${bag.p1.score}`, {
126
+ fontSize: 16,
127
+ color: MARIO_COLOR,
128
+ }),
129
+ );
130
+ p1Score.setBounds({
131
+ left: viewport.bounds.left + padding,
132
+ top: viewport.bounds.top - padding,
208
133
  });
209
134
 
210
- state.p2Score.setBounds({
211
- right: state.viewport.bounds.right - padding,
212
- top: state.viewport.bounds.top - padding,
135
+ const p2Score = gameScreen.add(
136
+ toodle.Text("Roboto", `P2: ${bag.p2.score}`, {
137
+ fontSize: 16,
138
+ color: LUIGI_COLOR,
139
+ }),
140
+ );
141
+ p2Score.setBounds({
142
+ right: viewport.bounds.right - padding,
143
+ top: viewport.bounds.top - padding,
213
144
  });
214
- }
215
145
 
216
- toodle.startFrame();
217
- toodle.draw(state.root);
218
- if (bag.debugHitboxes) {
219
- drawHitboxes(toodle, state, bag);
146
+ // Debug hitboxes
147
+ if (bag.debugHitboxes) {
148
+ toodle.draw(root);
149
+ drawHitbox(toodle, p1Quad.bounds);
150
+ drawHitbox(toodle, p2Quad.bounds);
151
+ // Need to get block bounds - draw a hitbox at block position
152
+ drawHitbox(toodle, {
153
+ left: bag.block.x - BLOCK_SIZE / 2,
154
+ right: bag.block.x + BLOCK_SIZE / 2,
155
+ bottom: BLOCK_Y,
156
+ top: BLOCK_Y + BLOCK_SIZE,
157
+ });
158
+ if (bag.coin.visible) {
159
+ drawHitbox(toodle, {
160
+ left: bag.coin.x - COIN_SIZE / 2,
161
+ right: bag.coin.x + COIN_SIZE / 2,
162
+ bottom: bag.coin.y - COIN_SIZE / 2,
163
+ top: bag.coin.y + COIN_SIZE / 2,
164
+ });
165
+ }
166
+ toodle.endFrame();
167
+ return;
168
+ }
220
169
  }
170
+
171
+ toodle.draw(root);
221
172
  toodle.endFrame();
222
173
  }
223
174
 
224
- /** Updates a player's pose quads based on their current state */
225
- function updatePlayerQuads(quads: PoseQuads, player: Player) {
226
- const poses: Pose[] = ["idle", "run", "jump", "skid"];
227
-
228
- for (const pose of poses) {
229
- const quad = quads[pose];
230
- const isActive = pose === player.pose;
231
- quad.isActive = isActive;
232
- if (!isActive) {
233
- continue;
234
- }
235
-
236
- // Update position
237
- quad.position = {
238
- x: player.x,
239
- y: player.y + PLAYER_HEIGHT / 2,
240
- };
241
-
242
- // Update flip based on facing direction
243
- quad.flipX = player.facingDir === -1;
244
-
245
- // Update region from flipbook frame using static flipbooks + bag AnimState
246
- const flipbook = unwrap(
247
- player.anims[pose],
248
- `No runtime flipbook for pose ${pose}`,
249
- );
250
- const frameIndex = flipbook.frameIndex % flipbook.frames.length;
251
- const frame = flipbook.frames[frameIndex];
252
- quad.region.x = frame.pos.x;
253
- quad.region.y = frame.pos.y;
254
- quad.region.width = frame.width;
255
- quad.region.height = frame.height;
256
- }
257
- }
175
+ /** Draws a player sprite and returns the quad for hitbox drawing */
176
+ function drawPlayer(
177
+ toodle: Toodle,
178
+ parent: ReturnType<Toodle["Node"]>,
179
+ player: Player,
180
+ color?: Color,
181
+ ) {
182
+ const pose = player.pose;
183
+ const textureName = POSE_TEXTURES[pose];
258
184
 
259
- function drawHitboxes(toodle: Toodle, state: DrawState, bag: typeof game.bag) {
260
- const p1Quad = state.p1[bag.p1.pose];
261
- const p2Quad = state.p2[bag.p2.pose];
185
+ const quad = parent.add(
186
+ toodle.Quad(textureName, {
187
+ size: { width: PLAYER_WIDTH, height: PLAYER_HEIGHT },
188
+ region: { x: 0, y: 0, width: PLAYER_WIDTH, height: PLAYER_HEIGHT },
189
+ color,
190
+ position: {
191
+ x: player.x,
192
+ y: player.y + PLAYER_HEIGHT / 2,
193
+ },
194
+ }),
195
+ );
262
196
 
263
- toodle.draw(makeHitbox(toodle, p1Quad.bounds));
264
- toodle.draw(makeHitbox(toodle, p2Quad.bounds));
265
- toodle.draw(makeHitbox(toodle, state.block.bounds));
197
+ // Update flip based on facing direction
198
+ quad.flipX = player.facingDir === -1;
266
199
 
267
- if (bag.coin.visible) {
268
- toodle.draw(makeHitbox(toodle, state.coin.bounds));
269
- }
200
+ // Update region from flipbook frame
201
+ const flipbook = unwrap(
202
+ player.anims[pose],
203
+ `No runtime flipbook for pose ${pose}`,
204
+ );
205
+ const frameIndex = flipbook.frameIndex % flipbook.frames.length;
206
+ const frame = flipbook.frames[frameIndex];
207
+ quad.region.x = frame.pos.x;
208
+ quad.region.y = frame.pos.y;
209
+ quad.region.width = frame.width;
210
+ quad.region.height = frame.height;
211
+
212
+ return quad;
270
213
  }
271
214
 
272
215
  const hitboxDefaultColor = { r: 1, g: 0, b: 1, a: 0.4 };
@@ -302,19 +245,21 @@ fn frag(vertex: VertexOutput) -> @location(0) vec4f {
302
245
  return hitboxShader;
303
246
  }
304
247
 
305
- function makeHitbox(
248
+ function drawHitbox(
306
249
  toodle: Toodle,
307
250
  bounds: { left: number; right: number; top: number; bottom: number },
308
251
  color: Color = hitboxDefaultColor,
309
252
  ) {
310
- return toodle.shapes
311
- .Rect({
312
- color,
313
- size: {
314
- width: bounds.right - bounds.left,
315
- height: bounds.top - bounds.bottom,
316
- },
317
- shader: getHitboxShader(toodle),
318
- })
319
- .setBounds(bounds);
253
+ toodle.draw(
254
+ toodle.shapes
255
+ .Rect({
256
+ color,
257
+ size: {
258
+ width: bounds.right - bounds.left,
259
+ height: bounds.top - bounds.bottom,
260
+ },
261
+ shader: getHitboxShader(toodle),
262
+ })
263
+ .setBounds(bounds),
264
+ );
320
265
  }
@@ -1,4 +1,4 @@
1
- import { Bloop } from "@bloopjs/bloop";
1
+ import { Bloop, Util } from "@bloopjs/bloop";
2
2
  import * as cfg from "./config";
3
3
  import type { Flipbook } from "./flipbook";
4
4
  import { FLIPBOOKS } from "./sprites";
@@ -2,11 +2,9 @@ import "./style.css";
2
2
  import { Toodle } from "@bloopjs/toodle";
3
3
  import { start } from "@bloopjs/web";
4
4
  import { createChromaticAberrationEffect } from "./chromatic-aberration";
5
- import { createDrawState, draw as drawFn } from "./draw";
5
+ import { draw } from "./draw";
6
6
  import { game } from "./game";
7
7
 
8
- let draw = drawFn;
9
-
10
8
  // boot up the game
11
9
  const app = await start({
12
10
  game,
@@ -22,9 +20,9 @@ if (import.meta.hot) {
22
20
  await app.acceptHmr(newModule?.game);
23
21
  });
24
22
 
25
- import.meta.hot.accept("./draw", async (newModule) => {
26
- draw = newModule?.draw;
27
- });
23
+ // import.meta.hot.accept("./draw", async (newModule) => {
24
+ // draw = newModule?.draw;
25
+ // });
28
26
  }
29
27
 
30
28
  const canvas = app.canvas;
@@ -61,10 +59,8 @@ await toodle.assets.loadFont(
61
59
  new URL("https://toodle.gg/fonts/Roboto-Regular-msdf.json"),
62
60
  );
63
61
 
64
- const drawState = createDrawState(toodle);
65
-
66
62
  requestAnimationFrame(function frame() {
67
- draw(app.game, toodle, drawState);
63
+ draw(app.game, toodle);
68
64
  requestAnimationFrame(frame);
69
65
  });
70
66
 
@@ -1,169 +0,0 @@
1
- import "./style.css";
2
- import { Toodle } from "@bloopjs/toodle";
3
- import { start } from "@bloopjs/web";
4
- import { createDrawState, draw } from "./draw";
5
- import { game } from "./game";
6
-
7
- const wasmUrl = import.meta.env.DEV
8
- ? new URL("/bloop-wasm/bloop.wasm", window.location.href)
9
- : new URL("./bloop.wasm", import.meta.url);
10
-
11
- const DB_NAME = "mario-tapes";
12
- const STORE_NAME = "tapes";
13
- const TAPE_KEY = "last";
14
-
15
- const statusEl = document.getElementById("status")!;
16
- const inputEl = document.getElementById("tape-input") as HTMLInputElement;
17
- const replayBtn = document.getElementById("replay-last") as HTMLButtonElement;
18
-
19
- function openDB(): Promise<IDBDatabase> {
20
- return new Promise((resolve, reject) => {
21
- const request = indexedDB.open(DB_NAME, 1);
22
- request.onerror = () => reject(request.error);
23
- request.onsuccess = () => resolve(request.result);
24
- request.onupgradeneeded = () => {
25
- request.result.createObjectStore(STORE_NAME);
26
- };
27
- });
28
- }
29
-
30
- async function loadTape(bytes: Uint8Array, fileName: string) {
31
- statusEl.textContent = "Loading tape...";
32
-
33
- // Start the app with recording disabled (we're loading a tape)
34
- const app = await start({
35
- game,
36
- wasmUrl: wasmUrl,
37
- debugUi: true,
38
- startRecording: false,
39
- });
40
-
41
- import.meta.hot?.accept("./game", async (newModule) => {
42
- await app.acceptHmr(newModule?.game, {
43
- wasmUrl,
44
- files: ["./game"],
45
- });
46
- });
47
-
48
- // Load the tape and pause playback
49
- app.loadTape(bytes, {
50
- checkpointInterval: 30,
51
- });
52
- app.sim.pause();
53
-
54
- const canvas = app.canvas;
55
- if (!canvas) throw new Error("No canvas element found");
56
-
57
- const toodle = await Toodle.attach(canvas, {
58
- filter: "nearest",
59
- backend: "webgpu",
60
- limits: { textureArrayLayers: 5 },
61
- });
62
-
63
- toodle.clearColor = { r: 0.36, g: 0.58, b: 0.99, a: 1 };
64
-
65
- // Load sprites
66
- const spriteUrl = (name: string) =>
67
- new URL(
68
- `${import.meta.env.BASE_URL}sprites/${name}.png`,
69
- window.location.href,
70
- );
71
-
72
- await toodle.assets.registerBundle("main", {
73
- textures: {
74
- marioIdle: spriteUrl("MarioIdle"),
75
- marioWalk: spriteUrl("MarioWalk"),
76
- marioJump: spriteUrl("MarioJump"),
77
- marioSkid: spriteUrl("MarioSkid"),
78
- brick: spriteUrl("Brick"),
79
- ground: spriteUrl("Ground"),
80
- },
81
- autoLoad: true,
82
- });
83
-
84
- await toodle.assets.loadFont(
85
- "Roboto",
86
- new URL("https://toodle.gg/fonts/Roboto-Regular-msdf.json"),
87
- );
88
-
89
- const drawState = createDrawState(toodle);
90
-
91
- requestAnimationFrame(function frame() {
92
- draw(app.game, toodle, drawState);
93
- requestAnimationFrame(frame);
94
- });
95
-
96
- statusEl.textContent = `Loaded tape: ${fileName}. Press Escape to toggle debug UI.`;
97
-
98
- // Hide the file input and show the game
99
- inputEl.style.display = "none";
100
- replayBtn.style.display = "none";
101
- document.querySelector(".container")!.remove();
102
- }
103
-
104
- async function saveTapeToStorage(
105
- bytes: Uint8Array,
106
- fileName: string,
107
- ): Promise<void> {
108
- const db = await openDB();
109
- return new Promise((resolve, reject) => {
110
- const tx = db.transaction(STORE_NAME, "readwrite");
111
- tx.objectStore(STORE_NAME).put({ bytes, fileName }, TAPE_KEY);
112
- tx.oncomplete = () => resolve();
113
- tx.onerror = () => reject(tx.error);
114
- });
115
- }
116
-
117
- async function loadTapeFromStorage(): Promise<{
118
- bytes: Uint8Array;
119
- fileName: string;
120
- } | null> {
121
- try {
122
- const db = await openDB();
123
- return new Promise((resolve, reject) => {
124
- const tx = db.transaction(STORE_NAME, "readonly");
125
- const request = tx.objectStore(STORE_NAME).get(TAPE_KEY);
126
- request.onsuccess = () => resolve(request.result ?? null);
127
- request.onerror = () => reject(request.error);
128
- });
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- // Check for saved tape on page load
135
- loadTapeFromStorage().then((savedTape) => {
136
- if (savedTape) {
137
- replayBtn.style.display = "block";
138
- replayBtn.textContent = `Replay last tape (${savedTape.fileName})`;
139
- }
140
- });
141
-
142
- replayBtn.addEventListener("click", async () => {
143
- const saved = await loadTapeFromStorage();
144
- if (!saved) {
145
- statusEl.textContent = "No saved tape found";
146
- return;
147
- }
148
-
149
- try {
150
- await loadTape(saved.bytes, saved.fileName);
151
- } catch (err) {
152
- statusEl.textContent = `Error loading tape: ${err}`;
153
- console.error(err);
154
- }
155
- });
156
-
157
- inputEl.addEventListener("change", async () => {
158
- const file = inputEl.files?.[0];
159
- if (!file) return;
160
-
161
- try {
162
- const bytes = new Uint8Array(await file.arrayBuffer());
163
- await saveTapeToStorage(bytes, file.name);
164
- await loadTape(bytes, file.name);
165
- } catch (err) {
166
- statusEl.textContent = `Error loading tape: ${err}`;
167
- console.error(err);
168
- }
169
- });
@@ -1,68 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Load Tape - Mario</title>
7
- <style>
8
- body {
9
- margin: 0;
10
- padding: 20px;
11
- font-family: system-ui, sans-serif;
12
- background: #1a1a2e;
13
- color: #eee;
14
- }
15
- .container {
16
- max-width: 600px;
17
- margin: 0 auto;
18
- }
19
- h1 {
20
- margin-bottom: 20px;
21
- }
22
- .file-input-wrapper {
23
- margin-bottom: 20px;
24
- }
25
- input[type="file"] {
26
- padding: 10px;
27
- background: #16213e;
28
- border: 2px dashed #0f3460;
29
- border-radius: 8px;
30
- color: #eee;
31
- cursor: pointer;
32
- width: 100%;
33
- box-sizing: border-box;
34
- }
35
- input[type="file"]:hover {
36
- border-color: #e94560;
37
- }
38
- .status {
39
- padding: 10px;
40
- background: #16213e;
41
- border-radius: 8px;
42
- margin-bottom: 20px;
43
- }
44
- .instructions {
45
- font-size: 14px;
46
- color: #888;
47
- line-height: 1.6;
48
- }
49
- </style>
50
- </head>
51
- <body>
52
- <div class="container">
53
- <h1>Load Tape</h1>
54
- <div class="file-input-wrapper">
55
- <input type="file" id="tape-input" accept=".bloop" />
56
- </div>
57
- <button id="replay-last" style="display: none; margin-bottom: 20px; padding: 10px 20px; background: #e94560; border: none; border-radius: 8px; color: #eee; cursor: pointer; font-size: 16px;">
58
- Replay last tape
59
- </button>
60
- <div class="status" id="status">Select a .bloop tape file to load</div>
61
- <div class="instructions">
62
- <p>To save a tape: Run the mario game and press Ctrl+S (or Cmd+S on Mac)</p>
63
- <p>Playback controls: Use the bottom bar buttons or hotkeys (5=back, 6=pause, 7=forward)</p>
64
- </div>
65
- </div>
66
- <script type="module" src="/src/tape-load.ts"></script>
67
- </body>
68
- </html>