@homie-rocks/studio 0.58.7 → 0.58.8
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/CHANGELOG.md +11 -0
- package/lib/template-history.json +42 -0
- package/package.json +1 -1
- package/starters/block-drop/README.md +2 -0
- package/starters/block-drop/game.json +1 -1
- package/starters/block-drop/index.html +1 -1
- package/starters/block-drop/src/view.ts +34 -8
- package/worker/build-id.mjs +1 -1
- package/worker/index.mjs +3 -1
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,17 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.58.8] - 2026-10-19
|
|
21
|
+
|
|
22
|
+
**Plugin 0.58.3**
|
|
23
|
+
|
|
24
|
+
Play falling-block games with visible standings, round winners and controls that follow a sliding thumb.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Block Drop shows a live leaderboard on phones and ranked round results, keeps its controls above the room chip, enlarges the landscape well, and follows a thumb sliding between directional buttons. Its synthesized audio is more audible, and watching a falling piece no longer pauses the round after 60 seconds without input.
|
|
29
|
+
- Concept previews label the outer page once, without injecting a second banner over embedded game and app controls. No-index headers remain on every response.
|
|
30
|
+
|
|
20
31
|
## [0.58.7] - 2026-10-19
|
|
21
32
|
|
|
22
33
|
**Plugin 0.58.3**
|
|
@@ -4470,6 +4470,48 @@
|
|
|
4470
4470
|
}
|
|
4471
4471
|
}
|
|
4472
4472
|
}
|
|
4473
|
+
},
|
|
4474
|
+
"0.58.8": {
|
|
4475
|
+
"files": {
|
|
4476
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4477
|
+
"README.md": "684b3cca8001673c",
|
|
4478
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4479
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4480
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4481
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4482
|
+
"posts/README.md": "37841aae476bd540",
|
|
4483
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4484
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4485
|
+
},
|
|
4486
|
+
"sections": {
|
|
4487
|
+
"AGENTS.md": {
|
|
4488
|
+
"(intro)": "af7f2f732505d379",
|
|
4489
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4490
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4491
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
4492
|
+
"## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
|
|
4493
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4494
|
+
"## Making games": "bb715272fd1a615b",
|
|
4495
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4496
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4497
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4498
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
4499
|
+
"## Selling things (the shop)": "0df5a5fe1833c624",
|
|
4500
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4501
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4502
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4503
|
+
"## Beta": "9d02638207a26e73"
|
|
4504
|
+
}
|
|
4505
|
+
},
|
|
4506
|
+
"named": {
|
|
4507
|
+
"Homie Arcade": {
|
|
4508
|
+
"sections": {
|
|
4509
|
+
"AGENTS.md": {
|
|
4510
|
+
"## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4473
4515
|
}
|
|
4474
4516
|
}
|
|
4475
4517
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.8",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -10,3 +10,5 @@ Start here for a falling-block puzzle. It already plays: preserve the working me
|
|
|
10
10
|
Build and repair the exact diagnostic before adding more. A file write replaces its entire contents; use targeted edits for small repairs. After an interrupted edit, read the file before continuing.
|
|
11
11
|
|
|
12
12
|
Exercise left/right, rotation, soft and hard drop on keyboard and with real touchscreen taps. Play two independent wells, clear a row, top out, finish/restart a round and join a bot's seat. Inspect computer, phone and sideways-phone frames. A green movement probe or compiler alone does not prove a board game plays.
|
|
13
|
+
|
|
14
|
+
The room uses `rest: "quiet"`: gravity and the round clock keep running while a connected person watches. Do not use the movement starters' away-from-keyboard `idle` pause for this continuous falling-block game. Empty rooms still stop normally.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id":"block-drop","name":"Block Drop","blurb":"Seven shapes. One well. Stack, rotate and clear lines before the clock runs out.",
|
|
3
|
-
"players":{"min":1,"max":4},"entry":"src/view.ts","room":{"host":"server","rest":"
|
|
3
|
+
"players":{"min":1,"max":4},"entry":"src/view.ts","room":{"host":"server","rest":"quiet"},
|
|
4
4
|
"playtest":{"primary":{"label":"hard drop","computer":{"key":"Space"},"phone":{"selector":"[data-action=drop]"}}},
|
|
5
5
|
"screen":{"share":{"desk":"top-right","phone":"top-right"},"chat":{"lines":"sheet-only"}},
|
|
6
6
|
"landing":{"pitch":"Find the gap. Make the line.","about":"A falling-block puzzle with a separate well for every player. Seven-piece bags, ghost pieces, wall kicks and line-clear scoring. Play with a friend or a bot in two-minute rounds.","howToPlay":["Move and rotate each falling shape into a gap.","Fill a horizontal row to clear it. Clear four at once for the biggest score.","Use the ghost outline to aim a hard drop. Keep your stack below the top."],"controls":{"phone":"Tap the arrows to move, ↻ to rotate and DROP to place the piece.","computer":"Left/right to move, up to rotate, down for soft drop and space for hard drop.","tv":"Watch every well; each player uses their own phone."},"credits":[{"role":"Starter","name":"Homie · Block Drop"}]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><title>Block Drop</title><link rel="icon" href="data:,"><style>
|
|
3
|
-
*{box-sizing:border-box}html,body{margin:0;height:100%;background:#060b16;overflow:hidden;overscroll-behavior:none;font-family:system-ui;color:#edf5ff}canvas{position:fixed;inset:0;width:100%;height:100%;touch-action:none}#controls{position:fixed;bottom:max(
|
|
3
|
+
*{box-sizing:border-box}html,body{margin:0;height:100%;background:#060b16;overflow:hidden;overscroll-behavior:none;font-family:system-ui;color:#edf5ff}canvas{position:fixed;inset:0;width:100%;height:100%;touch-action:none}#controls{position:fixed;bottom:max(44px,env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);display:flex;gap:7px}button{width:55px;height:49px;border:1px solid #92b6e34d;border-radius:12px;background:#172741;color:#eaf4ff;font:600 19px system-ui;touch-action:none;user-select:none;-webkit-user-select:none;box-shadow:0 4px 0 #030713}button[data-action=drop]{width:70px;font-size:12px;background:#185950;border-color:#68e9cc66}button.pressed,button:active{transform:translateY(3px);background:#398b89;box-shadow:none}button[data-action=audio]{width:38px;font-size:16px}button:focus-visible{outline:2px solid #fff;outline-offset:3px}#hint{position:fixed;bottom:102px;left:0;right:0;text-align:center;font:10px system-ui;color:#a3b5ce;pointer-events:none}@media(max-width:600px){#hint{display:none}}@media(max-height:460px) and (min-width:600px){button{width:48px;height:44px}button[data-action=drop]{width:58px}#controls{left:14px;top:100px;bottom:auto;transform:none;display:grid;grid-template-columns:58px 58px;gap:9px}#hint{display:none}}
|
|
4
4
|
</style></head><body><canvas></canvas><div id="hint">← → move · ↑ rotate · ↓ soft drop · SPACE hard drop</div><div id="controls" aria-label="Game controls"><button data-action="left" aria-label="Move left">←</button><button data-action="rotate" aria-label="Rotate">↻</button><button data-action="soft" aria-label="Soft drop">↓</button><button data-action="right" aria-label="Move right">→</button><button data-action="drop" aria-label="Hard drop">DROP</button><button data-action="audio" aria-label="Sound" aria-pressed="true">♪</button></div><script type="module" crossorigin src="./assets/main.js"></script></body></html>
|
|
@@ -25,7 +25,7 @@ function tone(hz: number, seconds = 0.055, volume = 0.055): void {
|
|
|
25
25
|
if (!audio || audio.state !== 'running' || muted) return;
|
|
26
26
|
const o = audio.createOscillator(), gain = audio.createGain();
|
|
27
27
|
o.type = 'triangle'; o.frequency.value = hz;
|
|
28
|
-
gain.gain.setValueAtTime(volume, audio.currentTime);
|
|
28
|
+
gain.gain.setValueAtTime(volume * 3, audio.currentTime);
|
|
29
29
|
gain.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + seconds);
|
|
30
30
|
o.connect(gain).connect(audio.destination); o.start(); o.stop(audio.currentTime + seconds);
|
|
31
31
|
}
|
|
@@ -52,7 +52,15 @@ for (const button of document.querySelectorAll<HTMLButtonElement>('[data-action]
|
|
|
52
52
|
if (action === 'drop') drop = true;
|
|
53
53
|
if (action === 'audio') { muted = !muted; button.textContent = muted ? '×♪' : '♪'; button.setAttribute('aria-pressed', String(!muted)); }
|
|
54
54
|
});
|
|
55
|
-
|
|
55
|
+
button.addEventListener('pointermove', e => {
|
|
56
|
+
if (!button.hasPointerCapture(e.pointerId) || !['left','right','soft'].includes(action ?? '')) return;
|
|
57
|
+
const target = document.elementFromPoint(e.clientX, e.clientY)?.closest<HTMLButtonElement>('[data-action]');
|
|
58
|
+
const next = target?.dataset.action;
|
|
59
|
+
touchDx = next === 'left' ? -1 : next === 'right' ? 1 : 0; soft = next === 'soft';
|
|
60
|
+
for (const control of document.querySelectorAll<HTMLButtonElement>('[data-action]'))
|
|
61
|
+
control.classList.toggle('pressed', control === target && ['left','right','soft'].includes(next ?? ''));
|
|
62
|
+
});
|
|
63
|
+
const release = (): void => { for (const control of document.querySelectorAll('[data-action]')) control.classList.remove('pressed'); if (['left','right','soft'].includes(action ?? '')) { touchDx = 0; soft = false; } };
|
|
56
64
|
button.addEventListener('pointerup', release); button.addEventListener('pointercancel', release);
|
|
57
65
|
// Keyboard activation for focused native buttons; pointer input is handled above.
|
|
58
66
|
button.addEventListener('click', e => { if (e.detail === 0) { unlock(); if (action === 'rotate') rotate = true; if (action === 'drop') drop = true; } });
|
|
@@ -91,15 +99,15 @@ function frame(): void {
|
|
|
91
99
|
if (audio?.state === 'running' && now >= nextNote) { const hz = melody[note++ % melody.length]; if (hz) tone(hz, 0.21, 0.075); nextNote = now + 250; }
|
|
92
100
|
room.input({ dx: touchDx || (keys.has('ArrowRight') || keys.has('KeyD') ? 1 : 0) - (keys.has('ArrowLeft') || keys.has('KeyA') ? 1 : 0), soft: soft || keys.has('ArrowDown') || keys.has('KeyS'), rotate, drop }); rotate = false; drop = false;
|
|
93
101
|
const bg = g.createRadialGradient(w * 0.5, h * 0.3, 10, w * 0.5, h * 0.5, Math.max(w, h)); bg.addColorStop(0, '#172747'); bg.addColorStop(1, '#060b16'); g.fillStyle = bg; g.fillRect(0, 0, w, h);
|
|
94
|
-
text(document.title.toUpperCase(), 20,
|
|
102
|
+
text(document.title.toUpperCase(), 20, 61, 18, '#eef6ff');
|
|
95
103
|
const round = room.round;
|
|
96
|
-
text(round?.phase === 'over' ? `Next round in ${Math.ceil(round.secondsLeft)}` : round ? `${Math.ceil(round.secondsLeft)}s` : 'Joining…', 20,
|
|
104
|
+
text(round?.phase === 'over' ? `Next round in ${Math.ceil(round.secondsLeft)}` : round ? `${Math.ceil(round.secondsLeft)}s` : 'Joining…', 20, 80, 12);
|
|
97
105
|
const boards: Board[] = []; room.each('board', b => boards.push(b));
|
|
98
106
|
const me = room.me; const focus = me ?? boards[0];
|
|
99
107
|
if (focus) {
|
|
100
|
-
const compact = w < 640, top =
|
|
108
|
+
const compact = w < 640, landscape = w >= 640 && h <= 460, top = landscape ? 58 : 108, bottom = compact ? 174 : landscape ? 18 : 110;
|
|
101
109
|
const s = Math.max(5, Math.min(32, (h - top - bottom) / 20, (w - (compact ? 116 : 330)) / 10));
|
|
102
|
-
const total = 10 * s + 98 + (!compact && boards.length > 1 ? 150 : 0);
|
|
110
|
+
const total = 10 * s + 98 + (!compact && !landscape && boards.length > 1 ? 150 : 0);
|
|
103
111
|
const x = Math.max(12, (w - total) / 2), y = top;
|
|
104
112
|
const roster = room.roster.find(r => r.seat === focus.seat);
|
|
105
113
|
text(me ? 'YOUR WELL' : (roster?.name ?? 'WATCHING'), x, y - 13, 11, '#7aeee1');
|
|
@@ -111,7 +119,7 @@ function frame(): void {
|
|
|
111
119
|
text('SCORE', side, y + 110, 10); text(String(focus.score), side, y + 136, 21, '#f5f8ff');
|
|
112
120
|
if (h >= 350) { text('LINES', side, y + 170, 10); text(String(focus.lines), side, y + 194, 19, '#f5f8ff'); }
|
|
113
121
|
if (20 * s > 260) { text('LEVEL', side, y + 230, 10); text(String(1 + Math.floor(focus.lines / 10)), side, y + 254, 19, '#f5f8ff'); }
|
|
114
|
-
if (!compact) {
|
|
122
|
+
if (!compact && !landscape) {
|
|
115
123
|
let ry = y;
|
|
116
124
|
for (const rival of boards.filter(b => b.id !== focus.id).slice(0, 3)) {
|
|
117
125
|
const mini = Math.min(6, s * 0.25), rx = side + 112;
|
|
@@ -120,7 +128,25 @@ function frame(): void {
|
|
|
120
128
|
well(rival, rx, ry, mini, false); text(String(rival.score), rx + 10 * mini + 9, ry + 14, 12, '#eef6ff'); ry += 20 * mini + 38;
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
|
-
|
|
131
|
+
const ranked = boards.slice().sort((a, b) => b.score - a.score || a.seat - b.seat);
|
|
132
|
+
const nameOf = (b: Board): string => { const r = room.roster.find(r => r.seat === b.seat); return b.id === me?.id ? 'YOU' : `${r?.driver === 'bot' ? 'BOT ' : ''}${r?.name ?? 'Player'}`; };
|
|
133
|
+
const rank = ranked.findIndex(b => b.id === focus.id) + 1;
|
|
134
|
+
if (compact) {
|
|
135
|
+
text(`RANK ${rank}/${ranked.length} · LEADER ${ranked[0]?.score ?? 0}`, 16, h - 155, 12, '#7aeee1');
|
|
136
|
+
ranked.slice(0, 4).forEach((b, i) => text(`${i + 1}. ${nameOf(b).slice(0, 12)} ${b.score}`, 16 + (i % 2) * (w / 2), h - 132 + Math.floor(i / 2) * 19, 11, b.id === me?.id ? '#f5f8ff' : '#a7b8d5'));
|
|
137
|
+
} else if (landscape) {
|
|
138
|
+
text(`RANK ${rank}/${ranked.length}`, side + 112, y + 142, 12, '#7aeee1');
|
|
139
|
+
ranked.slice(0, 4).forEach((b, i) => text(`${i + 1}. ${nameOf(b).slice(0, 10)} ${b.score}`, side + 112, y + 15 + i * 24, 12, b.id === me?.id ? '#f5f8ff' : '#a7b8d5'));
|
|
140
|
+
}
|
|
141
|
+
if (round?.phase === 'over') {
|
|
142
|
+
const results = round.results?.length ? round.results : ranked.map((b, i) => ({ name: nameOf(b), score: b.score, seat: b.seat, place: i + 1 }));
|
|
143
|
+
const pw = Math.min(340, w - 32), ph = 110 + results.length * 27, px = (w - pw) / 2, py = Math.max(42, (h - ph) / 2);
|
|
144
|
+
g.fillStyle = '#081329f5'; g.fillRect(px, py, pw, ph); g.strokeStyle = '#71e9e0'; g.strokeRect(px, py, pw, ph);
|
|
145
|
+
text(`${results[0]?.name ?? 'Player'} WINS`, px + 16, py + 30, 18, '#ffd75b');
|
|
146
|
+
text('FINAL SCORES', px + 16, py + 55, 11);
|
|
147
|
+
results.forEach((r, i) => text(`${r.place}. ${r.name.slice(0, 18)}${r.seat === me?.seat ? ' (YOU)' : ''} ${r.score}`, px + 16, py + 82 + i * 27, 13, '#f5f8ff'));
|
|
148
|
+
text(`Next round in ${Math.ceil(round.secondsLeft)}s`, px + 16, py + ph - 15, 11);
|
|
149
|
+
}
|
|
124
150
|
} else text('Finding your seat…', Math.max(20, w / 2 - 70), h / 2, 15);
|
|
125
151
|
requestAnimationFrame(frame);
|
|
126
152
|
}
|
package/worker/build-id.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
|
|
2
|
-
export const STUDIO_BUILD = Object.freeze({"version":"0.58.
|
|
2
|
+
export const STUDIO_BUILD = Object.freeze({"version":"0.58.8","sha256":"dd01f171a16a619c527dc71c2beaaf0c9a9b70c395cb28156214d3ac988f81c5"});
|
package/worker/index.mjs
CHANGED
|
@@ -99,7 +99,9 @@ export default {
|
|
|
99
99
|
if(env.HOMIE_CONCEPT==='1' && res.status!==101) {
|
|
100
100
|
const headers=new Headers(res.headers);headers.set('x-robots-tag','noindex, nofollow');
|
|
101
101
|
let body=res.body;
|
|
102
|
-
|
|
102
|
+
// The outer page labels the whole preview. Repeating that overlay in an
|
|
103
|
+
// embedded game/app obscures its HUD and counts as the game's own UI.
|
|
104
|
+
if(request.headers.get('sec-fetch-dest')!=='iframe' && /text\/html/i.test(headers.get('content-type')??'')) {
|
|
103
105
|
const html=await res.text(),banner='<aside role="status" style="position:fixed;z-index:2147483647;top:0;left:0;right:0;text-align:center;background:#ffda44;color:#171717;padding:6px;font:bold 14px system-ui;pointer-events:none">TEST / CONCEPT</aside>';
|
|
104
106
|
body=/<body[^>]*>/i.test(html)?html.replace(/<body[^>]*>/i,m=>m+banner):banner+html;
|
|
105
107
|
headers.delete('content-length');
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.58.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.58.8';
|