create-bloop 0.0.18 → 0.0.19

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.19",
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
  }
@@ -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,200 @@ 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
+ };
27
+
28
+ export function draw(g: typeof game, toodle: Toodle) {
29
+ const { bag } = g.context;
30
+ const isMobile = toodle.resolution.width < toodle.resolution.height;
31
+
32
+ toodle.startFrame();
48
33
 
49
- export function createDrawState(toodle: Toodle): DrawState {
50
34
  const root = toodle.Node({ scale: 3 });
51
35
 
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
- );
36
+ if (bag.phase !== "playing") {
37
+ // Title screen
38
+ const titleScreen = root.add(toodle.Node({}));
39
+
40
+ titleScreen.add(
41
+ toodle.Text("Roboto", "Mario Rollback", {
42
+ fontSize: 20,
43
+ align: "center",
44
+ color: { r: 1, g: 1, b: 1, a: 1 },
45
+ position: { x: 0, y: 20 },
46
+ size: {
47
+ width: toodle.resolution.width - 100,
48
+ height: toodle.resolution.height,
49
+ },
50
+ shrinkToFit: {
51
+ minFontSize: 4,
52
+ },
53
+ }),
54
+ );
68
55
 
69
- // Game screen container
70
- const gameScreen = root.add(toodle.Node({}));
71
- gameScreen.isActive = false;
56
+ const subtitleText =
57
+ bag.phase === "waiting"
58
+ ? "Waiting for opponent..."
59
+ : isMobile
60
+ ? "Tap to find opponent"
61
+ : "[Enter/Click] Online [Space] Local";
62
+
63
+ const text = titleScreen.add(
64
+ toodle.Text("Roboto", subtitleText, {
65
+ fontSize: 10,
66
+ color: { r: 1, g: 1, b: 1, a: 1 },
67
+ }),
68
+ );
69
+ } else {
70
+ // Game screen
71
+ const gameScreen = root.add(toodle.Node({}));
72
72
 
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
- );
73
+ const viewport = toodle.Node({
74
+ size: {
75
+ width: toodle.resolution.width,
76
+ height: toodle.resolution.height,
77
+ },
78
+ });
80
79
 
81
- const block = gameScreen.add(
82
- toodle.Quad("brick", {
83
- size: { width: BLOCK_SIZE, height: BLOCK_SIZE },
84
- }),
85
- );
80
+ // Ground
81
+ gameScreen.add(
82
+ toodle.shapes.Rect({
83
+ size: { width: viewport.size!.width, height: 40 },
84
+ position: { x: 0, y: GROUND_Y - 20 },
85
+ color: GROUND_COLOR,
86
+ }),
87
+ );
86
88
 
87
- const coin = gameScreen.add(
88
- toodle.shapes.Circle({
89
- radius: COIN_SIZE / 2,
90
- color: COIN_COLOR,
91
- }),
92
- );
89
+ // Block
90
+ gameScreen.add(
91
+ toodle.Quad("brick", {
92
+ size: { width: BLOCK_SIZE, height: BLOCK_SIZE },
93
+ position: { x: bag.block.x, y: BLOCK_Y + BLOCK_SIZE / 2 },
94
+ }),
95
+ );
93
96
 
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,
97
+ // Coin
98
+ if (bag.coin.visible) {
99
+ const coinColor =
100
+ bag.coin.winner === 1
101
+ ? MARIO_COLOR
102
+ : bag.coin.winner === 2
103
+ ? LUIGI_COLOR
104
+ : COIN_COLOR;
105
+
106
+ gameScreen.add(
107
+ toodle.shapes.Circle({
108
+ radius: COIN_SIZE / 2,
109
+ color: coinColor,
110
+ position: { x: bag.coin.x, y: bag.coin.y },
111
111
  }),
112
112
  );
113
- quad.isActive = false; // Start inactive, draw will activate the right one
114
- poses[pose] = quad;
115
113
  }
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
-
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
114
 
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);
115
+ // Players
116
+ const p1Quad = drawPlayer(toodle, gameScreen, bag.p1);
117
+ const p2Quad = drawPlayer(toodle, gameScreen, bag.p2, LUIGI_COLOR);
192
118
 
119
+ // Scores
193
120
  const padding = 20;
194
121
 
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,
122
+ const p1Score = gameScreen.add(
123
+ toodle.Text("Roboto", `P1: ${bag.p1.score}`, {
124
+ fontSize: 16,
125
+ color: MARIO_COLOR,
126
+ }),
127
+ );
128
+ p1Score.setBounds({
129
+ left: viewport.bounds.left + padding,
130
+ top: viewport.bounds.top - padding,
208
131
  });
209
132
 
210
- state.p2Score.setBounds({
211
- right: state.viewport.bounds.right - padding,
212
- top: state.viewport.bounds.top - padding,
133
+ const p2Score = gameScreen.add(
134
+ toodle.Text("Roboto", `P2: ${bag.p2.score}`, {
135
+ fontSize: 16,
136
+ color: LUIGI_COLOR,
137
+ }),
138
+ );
139
+ p2Score.setBounds({
140
+ right: viewport.bounds.right - padding,
141
+ top: viewport.bounds.top - padding,
213
142
  });
214
- }
215
143
 
216
- toodle.startFrame();
217
- toodle.draw(state.root);
218
- if (bag.debugHitboxes) {
219
- drawHitboxes(toodle, state, bag);
144
+ // Debug hitboxes
145
+ if (bag.debugHitboxes) {
146
+ toodle.draw(root);
147
+ drawHitbox(toodle, p1Quad.bounds);
148
+ drawHitbox(toodle, p2Quad.bounds);
149
+ // Need to get block bounds - draw a hitbox at block position
150
+ drawHitbox(toodle, {
151
+ left: bag.block.x - BLOCK_SIZE / 2,
152
+ right: bag.block.x + BLOCK_SIZE / 2,
153
+ bottom: BLOCK_Y,
154
+ top: BLOCK_Y + BLOCK_SIZE,
155
+ });
156
+ if (bag.coin.visible) {
157
+ drawHitbox(toodle, {
158
+ left: bag.coin.x - COIN_SIZE / 2,
159
+ right: bag.coin.x + COIN_SIZE / 2,
160
+ bottom: bag.coin.y - COIN_SIZE / 2,
161
+ top: bag.coin.y + COIN_SIZE / 2,
162
+ });
163
+ }
164
+ toodle.endFrame();
165
+ return;
166
+ }
220
167
  }
168
+
169
+ toodle.draw(root);
221
170
  toodle.endFrame();
222
171
  }
223
172
 
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
- }
173
+ /** Draws a player sprite and returns the quad for hitbox drawing */
174
+ function drawPlayer(
175
+ toodle: Toodle,
176
+ parent: ReturnType<Toodle["Node"]>,
177
+ player: Player,
178
+ color?: Color,
179
+ ) {
180
+ const pose = player.pose;
181
+ const textureName = POSE_TEXTURES[pose];
258
182
 
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];
183
+ const quad = parent.add(
184
+ toodle.Quad(textureName, {
185
+ size: { width: PLAYER_WIDTH, height: PLAYER_HEIGHT },
186
+ region: { x: 0, y: 0, width: PLAYER_WIDTH, height: PLAYER_HEIGHT },
187
+ color,
188
+ position: {
189
+ x: player.x,
190
+ y: player.y + PLAYER_HEIGHT / 2,
191
+ },
192
+ }),
193
+ );
262
194
 
263
- toodle.draw(makeHitbox(toodle, p1Quad.bounds));
264
- toodle.draw(makeHitbox(toodle, p2Quad.bounds));
265
- toodle.draw(makeHitbox(toodle, state.block.bounds));
195
+ // Update flip based on facing direction
196
+ quad.flipX = player.facingDir === -1;
266
197
 
267
- if (bag.coin.visible) {
268
- toodle.draw(makeHitbox(toodle, state.coin.bounds));
269
- }
198
+ // Update region from flipbook frame
199
+ const flipbook = unwrap(
200
+ player.anims[pose],
201
+ `No runtime flipbook for pose ${pose}`,
202
+ );
203
+ const frameIndex = flipbook.frameIndex % flipbook.frames.length;
204
+ const frame = flipbook.frames[frameIndex];
205
+ quad.region.x = frame.pos.x;
206
+ quad.region.y = frame.pos.y;
207
+ quad.region.width = frame.width;
208
+ quad.region.height = frame.height;
209
+
210
+ return quad;
270
211
  }
271
212
 
272
213
  const hitboxDefaultColor = { r: 1, g: 0, b: 1, a: 0.4 };
@@ -302,19 +243,21 @@ fn frag(vertex: VertexOutput) -> @location(0) vec4f {
302
243
  return hitboxShader;
303
244
  }
304
245
 
305
- function makeHitbox(
246
+ function drawHitbox(
306
247
  toodle: Toodle,
307
248
  bounds: { left: number; right: number; top: number; bottom: number },
308
249
  color: Color = hitboxDefaultColor,
309
250
  ) {
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);
251
+ toodle.draw(
252
+ toodle.shapes
253
+ .Rect({
254
+ color,
255
+ size: {
256
+ width: bounds.right - bounds.left,
257
+ height: bounds.top - bounds.bottom,
258
+ },
259
+ shader: getHitboxShader(toodle),
260
+ })
261
+ .setBounds(bounds),
262
+ );
320
263
  }
@@ -100,6 +100,12 @@ game.system("session-watcher", {
100
100
  },
101
101
  });
102
102
 
103
+ game.system("screen", {
104
+ resize({ screen }) {
105
+ console.log("resize", { width: screen.width, height: screen.height });
106
+ },
107
+ });
108
+
103
109
  game.system(
104
110
  "title-screen",
105
111
  PhaseSystem("title", {
@@ -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,7 +1,7 @@
1
1
  import "./style.css";
2
2
  import { Toodle } from "@bloopjs/toodle";
3
3
  import { start } from "@bloopjs/web";
4
- import { createDrawState, draw } from "./draw";
4
+ import { draw } from "./draw";
5
5
  import { game } from "./game";
6
6
 
7
7
  const wasmUrl = import.meta.env.DEV
@@ -86,10 +86,8 @@ async function loadTape(bytes: Uint8Array, fileName: string) {
86
86
  new URL("https://toodle.gg/fonts/Roboto-Regular-msdf.json"),
87
87
  );
88
88
 
89
- const drawState = createDrawState(toodle);
90
-
91
89
  requestAnimationFrame(function frame() {
92
- draw(app.game, toodle, drawState);
90
+ draw(app.game, toodle);
93
91
  requestAnimationFrame(frame);
94
92
  });
95
93