create-bloop 0.0.19 → 0.0.21
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 +7 -5
- package/templates/mario/src/game.ts +0 -6
- package/templates/mario/src/{chromatic-aberration.ts → glitchEffect.ts} +63 -1
- package/templates/mario/src/main.ts +16 -22
- package/templates/mario/src/tape-load.ts +0 -167
- 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
|
|
@@ -31,7 +31,9 @@ export function draw(g: typeof game, toodle: Toodle) {
|
|
|
31
31
|
|
|
32
32
|
toodle.startFrame();
|
|
33
33
|
|
|
34
|
-
const root = toodle.Node(
|
|
34
|
+
const root = toodle.Node();
|
|
35
|
+
|
|
36
|
+
toodle.camera.zoom = 3;
|
|
35
37
|
|
|
36
38
|
if (bag.phase !== "playing") {
|
|
37
39
|
// Title screen
|
|
@@ -60,7 +62,7 @@ export function draw(g: typeof game, toodle: Toodle) {
|
|
|
60
62
|
? "Tap to find opponent"
|
|
61
63
|
: "[Enter/Click] Online [Space] Local";
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
titleScreen.add(
|
|
64
66
|
toodle.Text("Roboto", subtitleText, {
|
|
65
67
|
fontSize: 10,
|
|
66
68
|
color: { r: 1, g: 1, b: 1, a: 1 },
|
|
@@ -78,13 +80,13 @@ export function draw(g: typeof game, toodle: Toodle) {
|
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
// Ground
|
|
81
|
-
gameScreen.add(
|
|
83
|
+
const ground = gameScreen.add(
|
|
82
84
|
toodle.shapes.Rect({
|
|
83
|
-
size: { width: viewport.size!.width, height:
|
|
84
|
-
position: { x: 0, y: GROUND_Y - 20 },
|
|
85
|
+
size: { width: viewport.size!.width, height: 1000 },
|
|
85
86
|
color: GROUND_COLOR,
|
|
86
87
|
}),
|
|
87
88
|
);
|
|
89
|
+
ground.setBounds({ top: GROUND_Y });
|
|
88
90
|
|
|
89
91
|
// Block
|
|
90
92
|
gameScreen.add(
|
|
@@ -100,12 +100,6 @@ 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
|
-
|
|
109
103
|
game.system(
|
|
110
104
|
"title-screen",
|
|
111
105
|
PhaseSystem("title", {
|
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
import { Backends, Colors, type Toodle } from "@bloopjs/toodle";
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function setupGlitchEffect(toodle: Toodle): () => void {
|
|
4
|
+
let glitchEffect: Backends.PostProcess | null = null;
|
|
5
|
+
let glitchTimeout: number = -1;
|
|
6
|
+
let glitchQueued = false;
|
|
7
|
+
|
|
8
|
+
if (toodle.backend.type === "webgpu") {
|
|
9
|
+
glitchEffect = createChromaticAberrationEffect(toodle);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function doGlitchEffect() {
|
|
13
|
+
if (!glitchEffect) {
|
|
14
|
+
return console.warn(
|
|
15
|
+
`No glitch effect available - are we running on webgl2?`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log("doGlitchEffect", performance.now(), {
|
|
20
|
+
hasFocus: document.hasFocus(),
|
|
21
|
+
});
|
|
22
|
+
// Queue effect if page isn't visible or window doesn't have focus (IDE covering browser)
|
|
23
|
+
if (document.visibilityState === "hidden" || !document.hasFocus()) {
|
|
24
|
+
console.log("queueing glitch");
|
|
25
|
+
glitchQueued = true;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log("doing glitch immediately");
|
|
29
|
+
if (glitchTimeout > 0) {
|
|
30
|
+
clearTimeout(glitchTimeout);
|
|
31
|
+
}
|
|
32
|
+
toodle.postprocess = glitchEffect;
|
|
33
|
+
glitchTimeout = setTimeout(() => {
|
|
34
|
+
toodle.postprocess = null;
|
|
35
|
+
}, 1000);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
document.addEventListener("visibilitychange", () => {
|
|
39
|
+
console.log(
|
|
40
|
+
"visibilityStateChange",
|
|
41
|
+
document.visibilityState,
|
|
42
|
+
performance.now(),
|
|
43
|
+
);
|
|
44
|
+
if (document.visibilityState === "visible" && glitchQueued) {
|
|
45
|
+
glitchQueued = false;
|
|
46
|
+
doGlitchEffect();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Handle case where another app (like IDE) covers the browser window
|
|
51
|
+
// visibilitychange doesn't fire for this, but focus does when clicking back
|
|
52
|
+
window.addEventListener("focus", () => {
|
|
53
|
+
console.log("window focus", performance.now());
|
|
54
|
+
if (glitchQueued) {
|
|
55
|
+
glitchQueued = false;
|
|
56
|
+
doGlitchEffect();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return doGlitchEffect;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createChromaticAberrationEffect(
|
|
64
|
+
toodle: Toodle,
|
|
65
|
+
): Backends.PostProcess {
|
|
4
66
|
if (!(toodle.backend instanceof Backends.WebGPUBackend)) {
|
|
5
67
|
throw new Error("Post-processing requires WebGPU backend");
|
|
6
68
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import "./style.css";
|
|
2
2
|
import { Toodle } from "@bloopjs/toodle";
|
|
3
3
|
import { start } from "@bloopjs/web";
|
|
4
|
-
import {
|
|
5
|
-
import { draw } from "./draw";
|
|
4
|
+
import { draw as drawFn } from "./draw";
|
|
6
5
|
import { game } from "./game";
|
|
6
|
+
import { setupGlitchEffect } from "./glitchEffect";
|
|
7
|
+
|
|
8
|
+
let draw = drawFn;
|
|
7
9
|
|
|
8
10
|
// boot up the game
|
|
9
11
|
const app = await start({
|
|
@@ -14,17 +16,6 @@ const app = await start({
|
|
|
14
16
|
},
|
|
15
17
|
});
|
|
16
18
|
|
|
17
|
-
// HMR support
|
|
18
|
-
if (import.meta.hot) {
|
|
19
|
-
import.meta.hot.accept("./game", async (newModule) => {
|
|
20
|
-
await app.acceptHmr(newModule?.game);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// import.meta.hot.accept("./draw", async (newModule) => {
|
|
24
|
-
// draw = newModule?.draw;
|
|
25
|
-
// });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
19
|
const canvas = app.canvas;
|
|
29
20
|
if (!canvas) throw new Error("No canvas element found");
|
|
30
21
|
|
|
@@ -87,14 +78,17 @@ window.addEventListener("keydown", (e) => {
|
|
|
87
78
|
}
|
|
88
79
|
});
|
|
89
80
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
const doGlitchEffect = setupGlitchEffect(toodle);
|
|
82
|
+
|
|
83
|
+
// HMR support
|
|
84
|
+
if (import.meta.hot) {
|
|
85
|
+
import.meta.hot.accept("./game", async (newModule) => {
|
|
86
|
+
doGlitchEffect();
|
|
87
|
+
await app.acceptHmr(newModule?.game);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
import.meta.hot.accept("./draw", async (newModule) => {
|
|
91
|
+
doGlitchEffect();
|
|
92
|
+
draw = newModule?.draw;
|
|
99
93
|
});
|
|
100
94
|
}
|
|
@@ -1,167 +0,0 @@
|
|
|
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
|
-
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
|
-
requestAnimationFrame(function frame() {
|
|
90
|
-
draw(app.game, toodle);
|
|
91
|
-
requestAnimationFrame(frame);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
statusEl.textContent = `Loaded tape: ${fileName}. Press Escape to toggle debug UI.`;
|
|
95
|
-
|
|
96
|
-
// Hide the file input and show the game
|
|
97
|
-
inputEl.style.display = "none";
|
|
98
|
-
replayBtn.style.display = "none";
|
|
99
|
-
document.querySelector(".container")!.remove();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function saveTapeToStorage(
|
|
103
|
-
bytes: Uint8Array,
|
|
104
|
-
fileName: string,
|
|
105
|
-
): Promise<void> {
|
|
106
|
-
const db = await openDB();
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
109
|
-
tx.objectStore(STORE_NAME).put({ bytes, fileName }, TAPE_KEY);
|
|
110
|
-
tx.oncomplete = () => resolve();
|
|
111
|
-
tx.onerror = () => reject(tx.error);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function loadTapeFromStorage(): Promise<{
|
|
116
|
-
bytes: Uint8Array;
|
|
117
|
-
fileName: string;
|
|
118
|
-
} | null> {
|
|
119
|
-
try {
|
|
120
|
-
const db = await openDB();
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
const tx = db.transaction(STORE_NAME, "readonly");
|
|
123
|
-
const request = tx.objectStore(STORE_NAME).get(TAPE_KEY);
|
|
124
|
-
request.onsuccess = () => resolve(request.result ?? null);
|
|
125
|
-
request.onerror = () => reject(request.error);
|
|
126
|
-
});
|
|
127
|
-
} catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Check for saved tape on page load
|
|
133
|
-
loadTapeFromStorage().then((savedTape) => {
|
|
134
|
-
if (savedTape) {
|
|
135
|
-
replayBtn.style.display = "block";
|
|
136
|
-
replayBtn.textContent = `Replay last tape (${savedTape.fileName})`;
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
replayBtn.addEventListener("click", async () => {
|
|
141
|
-
const saved = await loadTapeFromStorage();
|
|
142
|
-
if (!saved) {
|
|
143
|
-
statusEl.textContent = "No saved tape found";
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
await loadTape(saved.bytes, saved.fileName);
|
|
149
|
-
} catch (err) {
|
|
150
|
-
statusEl.textContent = `Error loading tape: ${err}`;
|
|
151
|
-
console.error(err);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
inputEl.addEventListener("change", async () => {
|
|
156
|
-
const file = inputEl.files?.[0];
|
|
157
|
-
if (!file) return;
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
161
|
-
await saveTapeToStorage(bytes, file.name);
|
|
162
|
-
await loadTape(bytes, file.name);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
statusEl.textContent = `Error loading tape: ${err}`;
|
|
165
|
-
console.error(err);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
@@ -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>
|