create-bloop 0.0.1

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.
Files changed (41) hide show
  1. package/dist/index.js +5029 -0
  2. package/package.json +23 -0
  3. package/templates/hello/index.html +16 -0
  4. package/templates/hello/package.json +20 -0
  5. package/templates/hello/src/config.ts +1 -0
  6. package/templates/hello/src/draw.ts +26 -0
  7. package/templates/hello/src/game.ts +28 -0
  8. package/templates/hello/src/main.ts +30 -0
  9. package/templates/hello/src/style.css +15 -0
  10. package/templates/hello/test/game.test.ts +19 -0
  11. package/templates/hello/tsconfig.json +26 -0
  12. package/templates/hello/vite.config.ts +5 -0
  13. package/templates/mario/.claude/prototype-next-steps.md +19 -0
  14. package/templates/mario/.claude/sidequests.md +10 -0
  15. package/templates/mario/index.html +17 -0
  16. package/templates/mario/package.json +22 -0
  17. package/templates/mario/public/sprites/MarioIdle.json +26 -0
  18. package/templates/mario/public/sprites/MarioIdle.png +0 -0
  19. package/templates/mario/public/sprites/MarioJump.json +26 -0
  20. package/templates/mario/public/sprites/MarioJump.png +0 -0
  21. package/templates/mario/public/sprites/MarioSkid.json +26 -0
  22. package/templates/mario/public/sprites/MarioSkid.png +0 -0
  23. package/templates/mario/public/sprites/MarioWalk.json +54 -0
  24. package/templates/mario/public/sprites/MarioWalk.png +0 -0
  25. package/templates/mario/src/chromatic-aberration.ts +107 -0
  26. package/templates/mario/src/config.ts +26 -0
  27. package/templates/mario/src/draw.ts +312 -0
  28. package/templates/mario/src/flipbook.ts +159 -0
  29. package/templates/mario/src/game.ts +171 -0
  30. package/templates/mario/src/main.ts +126 -0
  31. package/templates/mario/src/sprites.ts +14 -0
  32. package/templates/mario/src/style.css +7 -0
  33. package/templates/mario/src/systems/animation.ts +30 -0
  34. package/templates/mario/src/systems/collision.ts +41 -0
  35. package/templates/mario/src/systems/inputs.ts +66 -0
  36. package/templates/mario/src/systems/phase.ts +12 -0
  37. package/templates/mario/src/systems/physics.ts +22 -0
  38. package/templates/mario/src/tape-load.ts +165 -0
  39. package/templates/mario/tape-load.html +68 -0
  40. package/templates/mario/tsconfig.json +27 -0
  41. package/templates/mario/vite.config.ts +5 -0
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "create-bloop",
3
+ "version": "0.0.1",
4
+ "description": "Create a new Bloop game",
5
+ "type": "module",
6
+ "bin": "./dist/index.js",
7
+ "files": ["dist", "templates"],
8
+ "scripts": {
9
+ "build": "bun build index.ts --outdir=dist --target=node"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/bloopgames/bloop"
14
+ },
15
+ "keywords": ["bloop", "game", "scaffold", "create"],
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "prompts": "^2.4.2"
19
+ },
20
+ "devDependencies": {
21
+ "@types/prompts": "^2.4.9"
22
+ }
23
+ }
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>hello</title>
9
+ </head>
10
+
11
+ <body>
12
+ <canvas></canvas>
13
+ <script type="module" src="/src/main.ts"></script>
14
+ </body>
15
+
16
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "devDependencies": {
12
+ "typescript": "~5.9.3",
13
+ "vite": "^7.2.2"
14
+ },
15
+ "dependencies": {
16
+ "@bloopjs/bloop": "^0.0.72",
17
+ "@bloopjs/toodle": "^0.0.100",
18
+ "@bloopjs/web": "^0.0.72"
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export const moveSpeed = 5;
@@ -0,0 +1,26 @@
1
+ import { Colors, type Toodle } from "@bloopjs/toodle";
2
+ import type { game } from "../src/game";
3
+
4
+ export function draw(g: typeof game, toodle: Toodle) {
5
+ const { bag } = g.context;
6
+ toodle.startFrame();
7
+ toodle.draw(
8
+ toodle.shapes.Circle({
9
+ idealSize: { width: 100, height: 100 },
10
+ scale: bag.scale,
11
+ position: { x: bag.x, y: bag.y },
12
+ color: Colors.web.hotPink,
13
+ }),
14
+ );
15
+ toodle.draw(
16
+ toodle.shapes.Rect({
17
+ idealSize: { width: 10, height: 10 },
18
+ position: toodle.convertSpace(
19
+ { x: bag.mouse.x, y: bag.mouse.y },
20
+ { from: "screen", to: "world" },
21
+ ),
22
+ color: Colors.web.lightGreen,
23
+ }),
24
+ );
25
+ toodle.endFrame();
26
+ }
@@ -0,0 +1,28 @@
1
+ import { Bloop } from "@bloopjs/bloop";
2
+ import { moveSpeed } from "./config";
3
+
4
+ export const game = Bloop.create({
5
+ bag: {
6
+ x: 0,
7
+ y: 0,
8
+ scale: 1,
9
+ mouse: {
10
+ x: 0,
11
+ y: 0,
12
+ },
13
+ },
14
+ });
15
+
16
+ game.system("move", {
17
+ update({ bag, inputs }) {
18
+ if (inputs.keys.a.held) bag.x -= moveSpeed;
19
+ if (inputs.keys.d.held) bag.x += moveSpeed;
20
+ if (inputs.keys.w.held) bag.y += moveSpeed;
21
+ if (inputs.keys.s.held) bag.y -= moveSpeed;
22
+
23
+ bag.mouse.x = inputs.mouse.x;
24
+ bag.mouse.y = inputs.mouse.y;
25
+
26
+ bag.scale = 2;
27
+ },
28
+ });
@@ -0,0 +1,30 @@
1
+ import "./style.css";
2
+ import { Toodle } from "@bloopjs/toodle";
3
+ import { start } from "@bloopjs/web";
4
+ import { draw } from "./draw";
5
+ import { game } from "./game";
6
+
7
+ // temp - use a monorepo dev wasm url instead of cdn
8
+ const monorepoWasmUrl = new URL("/bloop-wasm/bloop.wasm", window.location.href);
9
+
10
+ // 1. Set up simulation
11
+ const app = await start({
12
+ game,
13
+ engineWasmUrl: monorepoWasmUrl,
14
+ });
15
+
16
+ // 2. Set up rendering
17
+ const canvas = document.querySelector("canvas");
18
+ if (!canvas) throw new Error("Canvas element not found");
19
+ const toodle = await Toodle.attach(canvas);
20
+ requestAnimationFrame(function frame() {
21
+ draw(app.game, toodle);
22
+ requestAnimationFrame(frame);
23
+ });
24
+
25
+ // 3. Set up Hot Module Replacement (HMR)
26
+ import.meta.hot?.accept("./game", async (newModule) => {
27
+ await app.acceptHmr(newModule?.game, {
28
+ wasmUrl: monorepoWasmUrl,
29
+ });
30
+ });
@@ -0,0 +1,15 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ }
8
+
9
+ canvas {
10
+ display: block;
11
+ width: 100vw;
12
+ height: 100vh;
13
+ }
14
+
15
+ a, button { cursor: pointer; }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { mount } from "@bloopjs/bloop";
3
+ import { moveSpeed } from "../src/config";
4
+ import { game } from "../src/game";
5
+
6
+ describe("game", () => {
7
+ it("should initialize bag correctly", () => {
8
+ expect(game.bag).toMatchObject({ x: 0, y: 0 });
9
+ });
10
+
11
+ it("should update bag correctly after system execution", async () => {
12
+ const { sim } = await mount(game);
13
+
14
+ sim.emit.keydown("KeyD");
15
+ sim.emit.keydown("KeyW");
16
+ sim.step();
17
+ expect(game.bag).toMatchObject({ x: moveSpeed, y: moveSpeed });
18
+ });
19
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": ["vite/client"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src"]
26
+ }
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({
4
+ // WASM loads from CDN automatically via @bloopjs/engine
5
+ });
@@ -0,0 +1,19 @@
1
+ # Mario Rollback Prototype - Next Steps
2
+
3
+ ## Rendering
4
+ - [ ] Load Mario/Luigi sprites from Aseprite
5
+ - [ ] Load block sprite
6
+ - [ ] Load coin sprite (animated?)
7
+ - [ ] Add coin collection animation/effect
8
+
9
+ ## Physics & Gamefeel
10
+ - [ ] Tune jump velocity and gravity for better arc
11
+ - [ ] Add acceleration/deceleration for horizontal movement
12
+ - [ ] Consider coyote time (jump buffer after leaving ground)
13
+ - [ ] Tune collision detection for better "bonk" feel
14
+
15
+ ## Polish
16
+ - [ ] Add jump sound effect
17
+ - [ ] Add coin collect sound effect
18
+ - [ ] Add block bump animation
19
+ - [ ] Score display positioning/styling
@@ -0,0 +1,10 @@
1
+ # Sidequests
2
+
3
+ Non-critical issues and improvements that come up during development.
4
+
5
+ ## Open
6
+
7
+ - [ ] Fix crash when tape runs out of space during recording
8
+
9
+ ## Completed
10
+
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="icon"
8
+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍄</text></svg>">
9
+
10
+ <title>Mario Rollback</title>
11
+ </head>
12
+
13
+ <body>
14
+ <script type="module" src="/src/main.ts"></script>
15
+ </body>
16
+
17
+ </html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview",
10
+ "ci:tsc": "tsc --noEmit"
11
+ },
12
+ "devDependencies": {
13
+ "@webgpu/types": "^0.1.67",
14
+ "typescript": "~5.9.3",
15
+ "vite": "^7.2.2"
16
+ },
17
+ "dependencies": {
18
+ "@bloopjs/bloop": "^0.0.72",
19
+ "@bloopjs/toodle": "^0.1.1",
20
+ "@bloopjs/web": "^0.0.72"
21
+ }
22
+ }
@@ -0,0 +1,26 @@
1
+ { "frames": {
2
+ "MarioIdle.aseprite": {
3
+ "frame": { "x": 0, "y": 0, "w": 16, "h": 16 },
4
+ "rotated": false,
5
+ "trimmed": false,
6
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
7
+ "sourceSize": { "w": 16, "h": 16 },
8
+ "duration": 100
9
+ }
10
+ },
11
+ "meta": {
12
+ "app": "https://www.aseprite.org/",
13
+ "version": "1.3.8.1-arm64",
14
+ "image": "MarioIdle.png",
15
+ "format": "I8",
16
+ "size": { "w": 16, "h": 16 },
17
+ "scale": "1",
18
+ "frameTags": [
19
+ ],
20
+ "layers": [
21
+ { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
22
+ ],
23
+ "slices": [
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,26 @@
1
+ { "frames": {
2
+ "MarioJump.aseprite": {
3
+ "frame": { "x": 0, "y": 0, "w": 16, "h": 16 },
4
+ "rotated": false,
5
+ "trimmed": false,
6
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
7
+ "sourceSize": { "w": 16, "h": 16 },
8
+ "duration": 100
9
+ }
10
+ },
11
+ "meta": {
12
+ "app": "https://www.aseprite.org/",
13
+ "version": "1.3.8.1-arm64",
14
+ "image": "MarioJump.png",
15
+ "format": "I8",
16
+ "size": { "w": 16, "h": 16 },
17
+ "scale": "1",
18
+ "frameTags": [
19
+ ],
20
+ "layers": [
21
+ { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
22
+ ],
23
+ "slices": [
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,26 @@
1
+ { "frames": {
2
+ "MarioSkid.aseprite": {
3
+ "frame": { "x": 0, "y": 0, "w": 16, "h": 16 },
4
+ "rotated": false,
5
+ "trimmed": false,
6
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
7
+ "sourceSize": { "w": 16, "h": 16 },
8
+ "duration": 100
9
+ }
10
+ },
11
+ "meta": {
12
+ "app": "https://www.aseprite.org/",
13
+ "version": "1.3.8.1-arm64",
14
+ "image": "MarioSkid.png",
15
+ "format": "I8",
16
+ "size": { "w": 16, "h": 16 },
17
+ "scale": "1",
18
+ "frameTags": [
19
+ ],
20
+ "layers": [
21
+ { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
22
+ ],
23
+ "slices": [
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,54 @@
1
+ { "frames": [
2
+ {
3
+ "filename": "MarioWalk 0.aseprite",
4
+ "frame": { "x": 0, "y": 0, "w": 16, "h": 16 },
5
+ "rotated": false,
6
+ "trimmed": false,
7
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
8
+ "sourceSize": { "w": 16, "h": 16 },
9
+ "duration": 100
10
+ },
11
+ {
12
+ "filename": "MarioWalk 1.aseprite",
13
+ "frame": { "x": 16, "y": 0, "w": 16, "h": 16 },
14
+ "rotated": false,
15
+ "trimmed": false,
16
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
17
+ "sourceSize": { "w": 16, "h": 16 },
18
+ "duration": 100
19
+ },
20
+ {
21
+ "filename": "MarioWalk 2.aseprite",
22
+ "frame": { "x": 32, "y": 0, "w": 16, "h": 16 },
23
+ "rotated": false,
24
+ "trimmed": false,
25
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
26
+ "sourceSize": { "w": 16, "h": 16 },
27
+ "duration": 100
28
+ },
29
+ {
30
+ "filename": "MarioWalk 3.aseprite",
31
+ "frame": { "x": 48, "y": 0, "w": 16, "h": 16 },
32
+ "rotated": false,
33
+ "trimmed": false,
34
+ "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 16 },
35
+ "sourceSize": { "w": 16, "h": 16 },
36
+ "duration": 100
37
+ }
38
+ ],
39
+ "meta": {
40
+ "app": "https://www.aseprite.org/",
41
+ "version": "1.3.8.1-arm64",
42
+ "image": "MarioWalk.png",
43
+ "format": "I8",
44
+ "size": { "w": 64, "h": 16 },
45
+ "scale": "1",
46
+ "frameTags": [
47
+ ],
48
+ "layers": [
49
+ { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
50
+ ],
51
+ "slices": [
52
+ ]
53
+ }
54
+ }
@@ -0,0 +1,107 @@
1
+ import { Backends, Colors, type Toodle } from "@bloopjs/toodle";
2
+
3
+ export function createChromaticAberrationEffect(toodle: Toodle): Backends.PostProcess {
4
+ if (!(toodle.backend instanceof Backends.WebGPUBackend)) {
5
+ throw new Error("Post-processing requires WebGPU backend");
6
+ }
7
+
8
+ const device = toodle.backend.device;
9
+ const presentationFormat = toodle.backend.presentationFormat;
10
+
11
+ const pipeline = device.createRenderPipeline({
12
+ label: "chromatic aberration pipeline",
13
+ layout: "auto",
14
+ primitive: { topology: "triangle-strip" },
15
+ vertex: {
16
+ module: Backends.PostProcessDefaults.vertexShader(device),
17
+ },
18
+ fragment: {
19
+ targets: [{ format: presentationFormat }],
20
+ module: device.createShaderModule({
21
+ label: "chromatic aberration fragment shader",
22
+ code: /*wgsl*/ `
23
+ @group(0) @binding(0) var inputTex: texture_2d<f32>;
24
+ @group(0) @binding(1) var inputSampler: sampler;
25
+ @group(0) @binding(2) var<uniform> time: f32;
26
+
27
+ @fragment
28
+ fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
29
+ // Time-based jitter for animation
30
+ let jitter = sin(time * 0.1) * 0.003;
31
+ let baseOffset = 0.008 + jitter;
32
+
33
+ // Sample RGB at different horizontal offsets
34
+ let r = textureSample(inputTex, inputSampler, uv + vec2f(baseOffset, 0.0)).r;
35
+ let g = textureSample(inputTex, inputSampler, uv).g;
36
+ let b = textureSample(inputTex, inputSampler, uv - vec2f(baseOffset, 0.0)).b;
37
+ let a = textureSample(inputTex, inputSampler, uv).a;
38
+
39
+ // Scanline effect
40
+ let scanline = sin(uv.y * 800.0) * 0.04 + 0.96;
41
+
42
+ // Horizontal glitch bands (occasional)
43
+ let glitchBand = step(0.98, fract(sin(floor(uv.y * 20.0 + time * 0.05)) * 43758.5453));
44
+ let glitchOffset = glitchBand * 0.02;
45
+
46
+ // Apply glitch offset to final sample
47
+ let glitchedUv = uv + vec2f(glitchOffset, 0.0);
48
+ let glitchedR = textureSample(inputTex, inputSampler, glitchedUv + vec2f(baseOffset, 0.0)).r;
49
+ let glitchedG = textureSample(inputTex, inputSampler, glitchedUv).g;
50
+ let glitchedB = textureSample(inputTex, inputSampler, glitchedUv - vec2f(baseOffset, 0.0)).b;
51
+
52
+ // Mix glitched and non-glitched based on band
53
+ let finalR = mix(r, glitchedR, glitchBand);
54
+ let finalG = mix(g, glitchedG, glitchBand);
55
+ let finalB = mix(b, glitchedB, glitchBand);
56
+
57
+ return vec4f(finalR, finalG, finalB, a) * scanline;
58
+ }
59
+ `,
60
+ }),
61
+ },
62
+ });
63
+
64
+ const sampler = Backends.PostProcessDefaults.sampler(device);
65
+
66
+ // Create a buffer for the time uniform
67
+ const timeBuffer = device.createBuffer({
68
+ label: "time uniform buffer",
69
+ size: 4,
70
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
71
+ });
72
+
73
+ return {
74
+ process(queue, encoder, pingpong, screen) {
75
+ // Update time uniform
76
+ const timeData = new Float32Array([toodle.diagnostics.frames]);
77
+ queue.writeBuffer(timeBuffer, 0, timeData);
78
+
79
+ const renderPass = encoder.beginRenderPass({
80
+ label: "chromatic aberration render pass",
81
+ colorAttachments: [
82
+ {
83
+ view: screen.createView(),
84
+ clearValue: Colors.web.black,
85
+ loadOp: "clear" as const,
86
+ storeOp: "store" as const,
87
+ },
88
+ ],
89
+ });
90
+
91
+ const bindGroup = device.createBindGroup({
92
+ label: "chromatic aberration bind group",
93
+ layout: pipeline.getBindGroupLayout(0),
94
+ entries: [
95
+ { binding: 0, resource: pingpong[0].createView() },
96
+ { binding: 1, resource: sampler },
97
+ { binding: 2, resource: { buffer: timeBuffer } },
98
+ ],
99
+ });
100
+
101
+ renderPass.setPipeline(pipeline);
102
+ renderPass.setBindGroup(0, bindGroup);
103
+ renderPass.draw(4);
104
+ renderPass.end();
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,26 @@
1
+ // Physics (Y+ is up in Toodle)
2
+ export const GRAVITY = 0.5;
3
+ export const JUMP_VELOCITY = 9; // positive = up
4
+ export const MOVE_SPEED = 3;
5
+ export const MAX_FALL_SPEED = 10;
6
+
7
+ // World (0,0 is center of screen, Y+ is up)
8
+ export const GROUND_Y = -100;
9
+ export const BLOCK_Y = -10;
10
+ export const BLOCK_SPEED = 1;
11
+ export const BLOCK_MIN_X = -100;
12
+ export const BLOCK_MAX_X = 100;
13
+
14
+ // Player sizes
15
+ export const PLAYER_WIDTH = 16;
16
+ export const PLAYER_HEIGHT = 16;
17
+ export const BLOCK_SIZE = 16;
18
+ export const COIN_SIZE = 12;
19
+
20
+ // Starting positions
21
+ export const P1_START_X = -60;
22
+ export const P2_START_X = 60;
23
+
24
+ // Animation timings
25
+ export const COIN_VISIBLE_DURATION = 0.8; // seconds
26
+ export const COIN_V_Y = 0.7;