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 +1 -1
- package/templates/hello/package.json +2 -2
- package/templates/mario/package.json +2 -2
- package/templates/mario/src/config.ts +5 -5
- package/templates/mario/src/draw.ts +182 -237
- package/templates/mario/src/game.ts +1 -1
- package/templates/mario/src/main.ts +5 -9
- package/templates/mario/src/tape-load.ts +0 -169
- package/templates/mario/tape-load.html +0 -68
package/package.json
CHANGED
|
@@ -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 = -
|
|
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 = -
|
|
12
|
-
export const BLOCK_MAX_X =
|
|
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 = -
|
|
22
|
-
export const P2_START_X =
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
50
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
const viewport = toodle.Node({
|
|
76
|
+
size: {
|
|
77
|
+
width: toodle.resolution.width,
|
|
78
|
+
height: toodle.resolution.height,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
/**
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
toodle.draw(makeHitbox(toodle, state.block.bounds));
|
|
197
|
+
// Update flip based on facing direction
|
|
198
|
+
quad.flipX = player.facingDir === -1;
|
|
266
199
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
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
|
-
|
|
311
|
-
.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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>
|