frogoe 0.1.0
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/bin/frogoe.mjs +11 -0
- package/dist/cli.js +3124 -0
- package/dist/contract/contract.js +236 -0
- package/dist/registry/blocks/coin-counter/coin-counter.html +101 -0
- package/dist/registry/blocks/coin-counter/registry-item.json +27 -0
- package/dist/registry/blocks/combo-counter/combo-counter.html +106 -0
- package/dist/registry/blocks/combo-counter/registry-item.json +27 -0
- package/dist/registry/blocks/fuel-gauge/fuel-gauge.html +130 -0
- package/dist/registry/blocks/fuel-gauge/registry-item.json +29 -0
- package/dist/registry/blocks/game-over-card/game-over-card.html +185 -0
- package/dist/registry/blocks/game-over-card/registry-item.json +30 -0
- package/dist/registry/blocks/hearts-row/hearts-row.html +92 -0
- package/dist/registry/blocks/hearts-row/registry-item.json +27 -0
- package/dist/registry/blocks/mini-board/mini-board.html +125 -0
- package/dist/registry/blocks/mini-board/registry-item.json +28 -0
- package/dist/registry/blocks/objective-chip/objective-chip.html +101 -0
- package/dist/registry/blocks/objective-chip/registry-item.json +29 -0
- package/dist/registry/blocks/ready-gate/ready-gate.html +124 -0
- package/dist/registry/blocks/ready-gate/registry-item.json +28 -0
- package/dist/registry/blocks/ready-hint/ready-hint.html +104 -0
- package/dist/registry/blocks/ready-hint/registry-item.json +29 -0
- package/dist/registry/blocks/score-card/registry-item.json +27 -0
- package/dist/registry/blocks/score-card/score-card.html +92 -0
- package/dist/registry/blocks/timer-ring/registry-item.json +28 -0
- package/dist/registry/blocks/timer-ring/timer-ring.html +123 -0
- package/dist/registry/registry.json +51 -0
- package/dist/registry/schema/registry-item.schema.json +36 -0
- package/dist/registry/schema/registry.schema.json +22 -0
- package/dist/runtimeVersion.js +10 -0
- package/dist/templates/_shared/AGENTS.md +105 -0
- package/dist/templates/_shared/CLAUDE.md +105 -0
- package/package.json +42 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frogoe contract — the whole platform. ~180 lines. Zero taste.
|
|
3
|
+
*
|
|
4
|
+
* A game is one closure. The platform hands it four nouns:
|
|
5
|
+
* stage — where: size, safe-area insets, capped play column, ctx
|
|
6
|
+
* input — action: unified pointer (touch+mouse), cancel/blur safe
|
|
7
|
+
* loop — life: fill loop.update(dt) and loop.render(ctx)
|
|
8
|
+
* finish — end: report the run's score to the host
|
|
9
|
+
*
|
|
10
|
+
* Everything visible (HUD, menus, art) lives in the game + recipe blocks.
|
|
11
|
+
* The platform publishes window.__frogoe so a host (feed/shell) can
|
|
12
|
+
* pause/resume/mute the game without the game's cooperation.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const BOOT_ERRORS = {
|
|
16
|
+
canvas: 'frogoe: missing <canvas id="c"> — the contract boots on that exact element',
|
|
17
|
+
update:
|
|
18
|
+
"frogoe: game has no loop.update — fill it inside the closure you pass to defineGame: loop.update = (dt) => {...} (60 Hz steps, dt in seconds)",
|
|
19
|
+
render:
|
|
20
|
+
"frogoe: game has no loop.render — fill it inside the closure you pass to defineGame: loop.render = (ctx) => {...}",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const cssLength = (value) => {
|
|
24
|
+
const parsed = Number.parseFloat(value);
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const defineGame = (game) => {
|
|
29
|
+
const boot = () => {
|
|
30
|
+
const canvas = document.querySelector("#c");
|
|
31
|
+
if (!(canvas instanceof HTMLCanvasElement)) {
|
|
32
|
+
throw new TypeError(BOOT_ERRORS.canvas);
|
|
33
|
+
}
|
|
34
|
+
const runtime = createRuntime(game, canvas);
|
|
35
|
+
runtime.start();
|
|
36
|
+
};
|
|
37
|
+
if (document.readyState === "loading") {
|
|
38
|
+
document.addEventListener("DOMContentLoaded", boot);
|
|
39
|
+
} else {
|
|
40
|
+
boot();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const createRuntime = (game, canvas) => {
|
|
45
|
+
const ctx = canvas.getContext("2d");
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
throw new Error(BOOT_ERRORS.canvas);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// stage — measured truth: viewport, notch insets, capped play column
|
|
51
|
+
const probe = document.createElement("div");
|
|
52
|
+
probe.style.cssText =
|
|
53
|
+
"position:fixed;top:env(safe-area-inset-top);bottom:env(safe-area-inset-bottom);" +
|
|
54
|
+
"left:env(safe-area-inset-left);right:env(safe-area-inset-right);" +
|
|
55
|
+
"visibility:hidden;pointer-events:none;";
|
|
56
|
+
document.body.append(probe);
|
|
57
|
+
const safe = { bottom: 0, left: 0, right: 0, top: 0 };
|
|
58
|
+
const readInsets = () => {
|
|
59
|
+
const cs = getComputedStyle(probe);
|
|
60
|
+
safe.top = cssLength(cs.top);
|
|
61
|
+
safe.bottom = cssLength(cs.bottom);
|
|
62
|
+
safe.left = cssLength(cs.left);
|
|
63
|
+
safe.right = cssLength(cs.right);
|
|
64
|
+
};
|
|
65
|
+
const play = { center: 0, left: 0, right: 0, width: 0 };
|
|
66
|
+
const stage = {
|
|
67
|
+
ctx,
|
|
68
|
+
height: 0,
|
|
69
|
+
play,
|
|
70
|
+
refresh: () => {
|
|
71
|
+
readInsets();
|
|
72
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
73
|
+
stage.width = window.innerWidth;
|
|
74
|
+
stage.height = window.innerHeight;
|
|
75
|
+
canvas.width = Math.round(stage.width * dpr);
|
|
76
|
+
canvas.height = Math.round(stage.height * dpr);
|
|
77
|
+
canvas.style.width = `${stage.width}px`;
|
|
78
|
+
canvas.style.height = `${stage.height}px`;
|
|
79
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
80
|
+
const width = Math.min(stage.width, 460);
|
|
81
|
+
play.center = stage.width / 2;
|
|
82
|
+
play.left = (stage.width - width) / 2;
|
|
83
|
+
play.right = (stage.width + width) / 2;
|
|
84
|
+
play.width = width;
|
|
85
|
+
},
|
|
86
|
+
safe,
|
|
87
|
+
width: 0,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// input — unified pointer. dx/dy are anchor-relative (since touch-down):
|
|
91
|
+
// steer with `x = grabX + p.dx` or track your own lastX. Never `x += p.dx`.
|
|
92
|
+
const pointer = { down: false, dx: 0, dy: 0, x: 0, y: 0 };
|
|
93
|
+
const handlers = { down: [], drag: [], up: [] };
|
|
94
|
+
let anchorX = 0;
|
|
95
|
+
let anchorY = 0;
|
|
96
|
+
const input = {
|
|
97
|
+
on(event, handler) {
|
|
98
|
+
if (handlers[event]) {
|
|
99
|
+
handlers[event].push(handler);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
pointer,
|
|
103
|
+
};
|
|
104
|
+
const fire = (event) => {
|
|
105
|
+
for (const handler of handlers[event] ?? []) {
|
|
106
|
+
handler(pointer);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const setPointer = (event) => {
|
|
110
|
+
pointer.x = event.clientX;
|
|
111
|
+
pointer.y = event.clientY;
|
|
112
|
+
pointer.dx = pointer.x - anchorX;
|
|
113
|
+
pointer.dy = pointer.y - anchorY;
|
|
114
|
+
};
|
|
115
|
+
const onDown = (event) => {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
pointer.down = true;
|
|
118
|
+
anchorX = event.clientX;
|
|
119
|
+
anchorY = event.clientY;
|
|
120
|
+
setPointer(event);
|
|
121
|
+
fire("down");
|
|
122
|
+
};
|
|
123
|
+
const onMove = (event) => {
|
|
124
|
+
if (!pointer.down) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
setPointer(event);
|
|
129
|
+
fire("drag");
|
|
130
|
+
};
|
|
131
|
+
const onUp = () => {
|
|
132
|
+
pointer.down = false;
|
|
133
|
+
fire("up");
|
|
134
|
+
};
|
|
135
|
+
window.addEventListener("pointerdown", onDown, { passive: false });
|
|
136
|
+
window.addEventListener("pointermove", onMove, { passive: false });
|
|
137
|
+
window.addEventListener("pointerup", onUp, { passive: false });
|
|
138
|
+
window.addEventListener("pointercancel", onUp, { passive: false });
|
|
139
|
+
|
|
140
|
+
// loop — fixed 60 Hz steps, clamped; render every rAF; pauses on hide/blur
|
|
141
|
+
const loop = {};
|
|
142
|
+
let paused = false;
|
|
143
|
+
let accumulator = 0;
|
|
144
|
+
let last = performance.now();
|
|
145
|
+
const STEP = 1000 / 60;
|
|
146
|
+
const frame = (now) => {
|
|
147
|
+
if (!paused) {
|
|
148
|
+
accumulator += Math.min(now - last, STEP * 4);
|
|
149
|
+
last = now;
|
|
150
|
+
while (accumulator >= STEP) {
|
|
151
|
+
loop.update?.(STEP / 1000);
|
|
152
|
+
accumulator -= STEP;
|
|
153
|
+
}
|
|
154
|
+
loop.render?.(ctx);
|
|
155
|
+
} else {
|
|
156
|
+
last = now;
|
|
157
|
+
}
|
|
158
|
+
requestAnimationFrame(frame);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// finish — tell any host the run ended. Reporting only: the game-over
|
|
162
|
+
// SCREEN is a recipe block (hud-game-over-card), never drawn here. Hosts
|
|
163
|
+
// listen through the standard DOM event — each host brings its own adapter
|
|
164
|
+
// (RN WebView, iframe parent, feed shell); the contract stays host-blind.
|
|
165
|
+
let finished = false;
|
|
166
|
+
const finish = (score) => {
|
|
167
|
+
if (finished) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
finished = true;
|
|
171
|
+
api.state = "over";
|
|
172
|
+
document.dispatchEvent(
|
|
173
|
+
new CustomEvent("frogoe:finish", {
|
|
174
|
+
detail: { score: Number(score) || 0 },
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// __frogoe — the host's handle. Published by the platform, guaranteed
|
|
180
|
+
// by construction; game code never touches it. Mute is a broadcast event
|
|
181
|
+
// ("frogoe:mute") so the game's audio layer can follow without polling.
|
|
182
|
+
const api = {
|
|
183
|
+
mute(state) {
|
|
184
|
+
api.muted = state === true;
|
|
185
|
+
document.dispatchEvent(new CustomEvent("frogoe:mute", { detail: { muted: api.muted } }));
|
|
186
|
+
},
|
|
187
|
+
muted: false,
|
|
188
|
+
pause() {
|
|
189
|
+
paused = true;
|
|
190
|
+
api.state = "paused";
|
|
191
|
+
},
|
|
192
|
+
resume() {
|
|
193
|
+
paused = false;
|
|
194
|
+
last = performance.now();
|
|
195
|
+
api.state = "playing";
|
|
196
|
+
},
|
|
197
|
+
state: "loading",
|
|
198
|
+
version: "0.1.0",
|
|
199
|
+
};
|
|
200
|
+
window.__frogoe = api;
|
|
201
|
+
let autoPaused = false;
|
|
202
|
+
const autoPause = () => {
|
|
203
|
+
if (document.hidden && !paused) {
|
|
204
|
+
autoPaused = true;
|
|
205
|
+
api.pause();
|
|
206
|
+
} else if (!document.hidden && autoPaused) {
|
|
207
|
+
autoPaused = false;
|
|
208
|
+
api.resume();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
document.addEventListener("visibilitychange", autoPause);
|
|
212
|
+
window.addEventListener("blur", () => {
|
|
213
|
+
onUp();
|
|
214
|
+
autoPause();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
start: () => {
|
|
219
|
+
stage.refresh();
|
|
220
|
+
window.addEventListener("resize", stage.refresh);
|
|
221
|
+
game({ finish, input, loop, stage });
|
|
222
|
+
if (!loop.update) {
|
|
223
|
+
throw new TypeError(BOOT_ERRORS.update);
|
|
224
|
+
}
|
|
225
|
+
if (!loop.render) {
|
|
226
|
+
throw new TypeError(BOOT_ERRORS.render);
|
|
227
|
+
}
|
|
228
|
+
api.state = "playing";
|
|
229
|
+
paused = false;
|
|
230
|
+
last = performance.now();
|
|
231
|
+
requestAnimationFrame(frame);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export { defineGame };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- frogoe block source: coin-counter — this file IS viewable in a browser (default
|
|
3
|
+
state on a neutral backdrop). Copy ONLY between the markers into your
|
|
4
|
+
game's blocks/ layer; the interactive playground is demo.html. -->
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
9
|
+
<title>frogoe block — coin-counter</title>
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: grid;
|
|
15
|
+
place-items: center;
|
|
16
|
+
background: radial-gradient(120% 120% at 50% 0%, #182031 0%, #0d1220 60%, #080b13 100%);
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<!-- ===================== COPY FROM HERE ===================== -->
|
|
22
|
+
<!--
|
|
23
|
+
frogoe block: coin-counter
|
|
24
|
+
Icon + animated count-up number — the classic corner pickup counter.
|
|
25
|
+
|
|
26
|
+
Bind from game.js (vanilla, no helper):
|
|
27
|
+
const el = document.querySelector("[data-block-coins]");
|
|
28
|
+
el.textContent = String(coins); // count up in your loop or instantly
|
|
29
|
+
el.parentElement.setAttribute("data-gain", ""); // restart the gain pop
|
|
30
|
+
|
|
31
|
+
Theme: --block-accent, --block-fg, --block-outline, --block-font.
|
|
32
|
+
-->
|
|
33
|
+
<style>
|
|
34
|
+
.block-coin-counter {
|
|
35
|
+
--block-accent: #ffd166;
|
|
36
|
+
--block-fg: #fffdf7;
|
|
37
|
+
--block-font: "Fredoka", "Baloo 2", system-ui, sans-serif;
|
|
38
|
+
--block-outline: #26180a;
|
|
39
|
+
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.45em;
|
|
43
|
+
color: var(--block-fg);
|
|
44
|
+
font-family: var(--block-font);
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
-webkit-user-select: none;
|
|
48
|
+
user-select: none;
|
|
49
|
+
}
|
|
50
|
+
.block-coin-counter__coin {
|
|
51
|
+
width: 1.35em;
|
|
52
|
+
height: 1.35em;
|
|
53
|
+
flex: none;
|
|
54
|
+
border-radius: 50%;
|
|
55
|
+
background: var(--block-accent);
|
|
56
|
+
/* machined coin: inner ring + top-left light, no icon font */
|
|
57
|
+
box-shadow:
|
|
58
|
+
inset 0 0 0 0.14em color-mix(in srgb, var(--block-accent) 55%, #fff),
|
|
59
|
+
inset -0.08em -0.12em 0 0 color-mix(in srgb, var(--block-accent) 60%, #000),
|
|
60
|
+
0 2px 0 var(--block-outline);
|
|
61
|
+
}
|
|
62
|
+
.block-coin-counter__value {
|
|
63
|
+
font-size: clamp(1.1rem, 4.5vmin, 1.5rem);
|
|
64
|
+
font-variant-numeric: tabular-nums;
|
|
65
|
+
text-shadow: 0 2px 0 var(--block-outline);
|
|
66
|
+
min-width: 3ch;
|
|
67
|
+
}
|
|
68
|
+
.block-coin-counter[data-gain] .block-coin-counter__value {
|
|
69
|
+
animation: block-coin-gain 260ms cubic-bezier(0.34, 1.8, 0.5, 1);
|
|
70
|
+
}
|
|
71
|
+
.block-coin-counter[data-gain] .block-coin-counter__coin {
|
|
72
|
+
animation: block-coin-spin 420ms cubic-bezier(0.34, 1.56, 0.5, 1);
|
|
73
|
+
}
|
|
74
|
+
@keyframes block-coin-gain {
|
|
75
|
+
from {
|
|
76
|
+
transform: scale(1.3);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
@keyframes block-coin-spin {
|
|
80
|
+
40% {
|
|
81
|
+
transform: rotateY(180deg) scale(1.12);
|
|
82
|
+
}
|
|
83
|
+
100% {
|
|
84
|
+
transform: rotateY(360deg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
@media (prefers-reduced-motion: reduce) {
|
|
88
|
+
.block-coin-counter[data-gain] .block-coin-counter__value,
|
|
89
|
+
.block-coin-counter[data-gain] .block-coin-counter__coin {
|
|
90
|
+
animation: none;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
94
|
+
|
|
95
|
+
<div class="block-coin-counter">
|
|
96
|
+
<span class="block-coin-counter__coin"></span>
|
|
97
|
+
<span class="block-coin-counter__value" data-block-coins>0</span>
|
|
98
|
+
</div>
|
|
99
|
+
<!-- ===================== COPY TO HERE ======================= -->
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/registry-item.schema.json",
|
|
3
|
+
"name": "coin-counter",
|
|
4
|
+
"type": "frogoe:block",
|
|
5
|
+
"title": "Coin Counter",
|
|
6
|
+
"description": "Machined coin icon + tabular count with a gain pop and coin spin; themeable via --block-*",
|
|
7
|
+
"tags": [
|
|
8
|
+
"hud",
|
|
9
|
+
"coins",
|
|
10
|
+
"currency",
|
|
11
|
+
"pickup"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "coin-counter.html",
|
|
16
|
+
"target": "blocks/coin-counter.html",
|
|
17
|
+
"type": "frogoe:markup"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"bindings": [
|
|
21
|
+
"data-block-coins"
|
|
22
|
+
],
|
|
23
|
+
"placement": "overlay",
|
|
24
|
+
"preview": {
|
|
25
|
+
"demo": "demo.html"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- frogoe block source: combo-counter — this file IS viewable in a browser (default
|
|
3
|
+
state on a neutral backdrop). Copy ONLY between the markers into your
|
|
4
|
+
game's blocks/ layer; the interactive playground is demo.html. -->
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
9
|
+
<title>frogoe block — combo-counter</title>
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: grid;
|
|
15
|
+
place-items: center;
|
|
16
|
+
background: radial-gradient(120% 120% at 50% 0%, #182031 0%, #0d1220 60%, #080b13 100%);
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<!-- ===================== COPY FROM HERE ===================== -->
|
|
22
|
+
<!--
|
|
23
|
+
frogoe block: combo-counter
|
|
24
|
+
Streak/combo readout with escalation tiers — the number grows legs as the
|
|
25
|
+
streak climbs. Tiers are semantic (streak heat), licensed escalation.
|
|
26
|
+
|
|
27
|
+
Bind from game.js (vanilla, no helper):
|
|
28
|
+
const el = document.querySelector("[data-block-combo]");
|
|
29
|
+
el.textContent = String(streak);
|
|
30
|
+
el.parentElement.dataset.tier = streak >= 12 ? "3" : streak >= 6 ? "2" : streak >= 3 ? "1" : "0";
|
|
31
|
+
el.parentElement.toggleAttribute("data-hot", streak >= 3);
|
|
32
|
+
|
|
33
|
+
Theme: --block-accent (tier colors derive from it), --block-fg, --block-outline.
|
|
34
|
+
-->
|
|
35
|
+
<style>
|
|
36
|
+
.block-combo {
|
|
37
|
+
--block-accent: #ffd166;
|
|
38
|
+
--block-fg: #fffdf7;
|
|
39
|
+
--block-font: "Fredoka", "Baloo 2", system-ui, sans-serif;
|
|
40
|
+
--block-outline: #26180a;
|
|
41
|
+
|
|
42
|
+
display: grid;
|
|
43
|
+
justify-items: center;
|
|
44
|
+
line-height: 1;
|
|
45
|
+
color: var(--block-fg);
|
|
46
|
+
font-family: var(--block-font);
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
-webkit-user-select: none;
|
|
49
|
+
user-select: none;
|
|
50
|
+
}
|
|
51
|
+
.block-combo__label {
|
|
52
|
+
font-size: 0.72rem;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
letter-spacing: 0.4em;
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
text-indent: 0.4em; /* optical center against tracking */
|
|
57
|
+
opacity: 0.75;
|
|
58
|
+
}
|
|
59
|
+
.block-combo__value {
|
|
60
|
+
font-size: clamp(2rem, 9vmin, 3.2rem);
|
|
61
|
+
font-weight: 700;
|
|
62
|
+
font-variant-numeric: tabular-nums;
|
|
63
|
+
text-shadow: 0 3px 0 var(--block-outline);
|
|
64
|
+
}
|
|
65
|
+
.block-combo__value[data-pop] {
|
|
66
|
+
animation: block-combo-pop 240ms cubic-bezier(0.34, 1.8, 0.5, 1);
|
|
67
|
+
}
|
|
68
|
+
@keyframes block-combo-pop {
|
|
69
|
+
from {
|
|
70
|
+
transform: scale(1.32) rotate(-2deg);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/* escalation tiers — derived from the single accent, saturation climbs */
|
|
74
|
+
.block-combo[data-tier="1"] {
|
|
75
|
+
--block-fg: color-mix(in srgb, var(--block-accent) 45%, #fffdf7);
|
|
76
|
+
}
|
|
77
|
+
.block-combo[data-tier="2"] {
|
|
78
|
+
--block-fg: var(--block-accent);
|
|
79
|
+
}
|
|
80
|
+
.block-combo[data-tier="3"] {
|
|
81
|
+
--block-fg: color-mix(in srgb, var(--block-accent) 70%, #ff5d5d);
|
|
82
|
+
animation: block-combo-hot 600ms ease-in-out infinite alternate;
|
|
83
|
+
}
|
|
84
|
+
@keyframes block-combo-hot {
|
|
85
|
+
from {
|
|
86
|
+
scale: 1;
|
|
87
|
+
}
|
|
88
|
+
to {
|
|
89
|
+
scale: 1.06;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
@media (prefers-reduced-motion: reduce) {
|
|
93
|
+
.block-combo__value[data-pop],
|
|
94
|
+
.block-combo[data-tier="3"] {
|
|
95
|
+
animation: none;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
99
|
+
|
|
100
|
+
<div class="block-combo" data-tier="0">
|
|
101
|
+
<span class="block-combo__label">Combo</span>
|
|
102
|
+
<span class="block-combo__value" data-block-combo>0</span>
|
|
103
|
+
</div>
|
|
104
|
+
<!-- ===================== COPY TO HERE ======================= -->
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/registry-item.schema.json",
|
|
3
|
+
"name": "combo-counter",
|
|
4
|
+
"type": "frogoe:block",
|
|
5
|
+
"title": "Combo Counter",
|
|
6
|
+
"description": "Streak readout with three escalation tiers derived from one accent; pop on gain, hot pulse at max tier; themeable via --block-*",
|
|
7
|
+
"tags": [
|
|
8
|
+
"hud",
|
|
9
|
+
"combo",
|
|
10
|
+
"streak",
|
|
11
|
+
"score"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "combo-counter.html",
|
|
16
|
+
"target": "blocks/combo-counter.html",
|
|
17
|
+
"type": "frogoe:markup"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"bindings": [
|
|
21
|
+
"data-block-combo"
|
|
22
|
+
],
|
|
23
|
+
"placement": "overlay",
|
|
24
|
+
"preview": {
|
|
25
|
+
"demo": "demo.html"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- frogoe block source: fuel-gauge — this file IS viewable in a browser (default
|
|
3
|
+
state on a neutral backdrop). Copy ONLY between the markers into your
|
|
4
|
+
game's blocks/ layer; the interactive playground is demo.html. -->
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
9
|
+
<title>frogoe block — fuel-gauge</title>
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: grid;
|
|
15
|
+
place-items: center;
|
|
16
|
+
background: radial-gradient(120% 120% at 50% 0%, #182031 0%, #0d1220 60%, #080b13 100%);
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<!-- ===================== COPY FROM HERE ===================== -->
|
|
22
|
+
<!--
|
|
23
|
+
frogoe block: fuel-gauge
|
|
24
|
+
Horizontal resource bar (fuel / health / boost) with icon and label.
|
|
25
|
+
Fills drain smoothly; under 25% it pulses danger — that state is a
|
|
26
|
+
data attribute the game toggles, never guessed from color alone.
|
|
27
|
+
|
|
28
|
+
Bind from game.js (vanilla, no helper):
|
|
29
|
+
const bar = document.querySelector("[data-block-fuel]");
|
|
30
|
+
bar.style.setProperty("--block-fill", `${percent}%`); // 0–100
|
|
31
|
+
bar.toggleAttribute("data-low", percent < 25);
|
|
32
|
+
|
|
33
|
+
Theme: --block-fill-color, --block-track, --block-accent, --block-danger.
|
|
34
|
+
-->
|
|
35
|
+
<style>
|
|
36
|
+
.block-fuel-gauge {
|
|
37
|
+
--block-accent: #ffd166;
|
|
38
|
+
--block-danger: #ff5d5d;
|
|
39
|
+
--block-fill-color: var(--block-accent);
|
|
40
|
+
--block-font: "Fredoka", "Baloo 2", system-ui, sans-serif;
|
|
41
|
+
--block-outline: #26180a;
|
|
42
|
+
--block-track: rgba(10, 14, 26, 0.55);
|
|
43
|
+
--block-width: clamp(9rem, 34vmin, 13rem);
|
|
44
|
+
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 0.55em;
|
|
48
|
+
color: #fffdf7;
|
|
49
|
+
font-family: var(--block-font);
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
-webkit-user-select: none;
|
|
52
|
+
user-select: none;
|
|
53
|
+
}
|
|
54
|
+
.block-fuel-gauge__icon {
|
|
55
|
+
width: 1.5em;
|
|
56
|
+
height: 1.5em;
|
|
57
|
+
flex: none;
|
|
58
|
+
/* fuel-can silhouette, drawn as data — crisp at any size */
|
|
59
|
+
background: var(--block-accent);
|
|
60
|
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M19.77 7.23l-.01-.01-3.72-3.72L15 4.56l2.11 2.11c-.94.36-1.61 1.26-1.61 2.33 0 1.38 1.12 2.5 2.5 2.5.36 0 .69-.08 1-.21v7.21c0 .55-.45 1-1 1s-1-.45-1-1V14c0-1.1-.9-2-2-2h-1V5c0-1.1-.9-2-2-2H6c-1.1 0-2 .9-2 2v16h10v-7.5h1.5v2.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V9c0-.69-.28-1.32-.73-1.77zM12 10H6V5h6v5zm6 0c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z'/></svg>")
|
|
61
|
+
center / contain no-repeat;
|
|
62
|
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M19.77 7.23l-.01-.01-3.72-3.72L15 4.56l2.11 2.11c-.94.36-1.61 1.26-1.61 2.33 0 1.38 1.12 2.5 2.5 2.5.36 0 .69-.08 1-.21v7.21c0 .55-.45 1-1 1s-1-.45-1-1V14c0-1.1-.9-2-2-2h-1V5c0-1.1-.9-2-2-2H6c-1.1 0-2 .9-2 2v16h10v-7.5h1.5v2.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V9c0-.69-.28-1.32-.73-1.77zM12 10H6V5h6v5zm6 0c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z'/></svg>")
|
|
63
|
+
center / contain no-repeat;
|
|
64
|
+
filter: drop-shadow(0 2px 0 var(--block-outline));
|
|
65
|
+
}
|
|
66
|
+
.block-fuel-gauge__track {
|
|
67
|
+
position: relative;
|
|
68
|
+
width: var(--block-width);
|
|
69
|
+
height: 1.15em;
|
|
70
|
+
background: var(--block-track);
|
|
71
|
+
border: 2px solid rgba(255, 255, 255, 0.16);
|
|
72
|
+
border-radius: 999px;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.35);
|
|
75
|
+
}
|
|
76
|
+
.block-fuel-gauge__fill {
|
|
77
|
+
--block-fill: 100%;
|
|
78
|
+
position: absolute;
|
|
79
|
+
inset: 0 auto 0 0;
|
|
80
|
+
width: var(--block-fill);
|
|
81
|
+
background: linear-gradient(
|
|
82
|
+
180deg,
|
|
83
|
+
color-mix(in srgb, var(--block-fill-color) 78%, white) 0%,
|
|
84
|
+
var(--block-fill-color) 55%,
|
|
85
|
+
color-mix(in srgb, var(--block-fill-color) 72%, black) 100%
|
|
86
|
+
);
|
|
87
|
+
border-radius: inherit;
|
|
88
|
+
transition: width 240ms ease-out;
|
|
89
|
+
}
|
|
90
|
+
.block-fuel-gauge__label {
|
|
91
|
+
font-size: 0.95rem;
|
|
92
|
+
font-weight: 700;
|
|
93
|
+
font-variant-numeric: tabular-nums;
|
|
94
|
+
text-shadow: 0 2px 0 var(--block-outline);
|
|
95
|
+
min-width: 3ch;
|
|
96
|
+
}
|
|
97
|
+
.block-fuel-gauge[data-low] {
|
|
98
|
+
--block-fill-color: var(--block-danger);
|
|
99
|
+
}
|
|
100
|
+
.block-fuel-gauge[data-low] .block-fuel-gauge__fill {
|
|
101
|
+
animation: block-fuel-low 640ms ease-in-out infinite alternate;
|
|
102
|
+
}
|
|
103
|
+
@keyframes block-fuel-low {
|
|
104
|
+
from {
|
|
105
|
+
opacity: 1;
|
|
106
|
+
}
|
|
107
|
+
to {
|
|
108
|
+
opacity: 0.55;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
@media (prefers-reduced-motion: reduce) {
|
|
112
|
+
.block-fuel-gauge__fill {
|
|
113
|
+
transition: none;
|
|
114
|
+
}
|
|
115
|
+
.block-fuel-gauge[data-low] .block-fuel-gauge__fill {
|
|
116
|
+
animation: none;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
120
|
+
|
|
121
|
+
<div class="block-fuel-gauge" data-block-fuel>
|
|
122
|
+
<span class="block-fuel-gauge__icon"></span>
|
|
123
|
+
<span class="block-fuel-gauge__track">
|
|
124
|
+
<span class="block-fuel-gauge__fill"></span>
|
|
125
|
+
</span>
|
|
126
|
+
<span class="block-fuel-gauge__label" data-block-fuel-label>100</span>
|
|
127
|
+
</div>
|
|
128
|
+
<!-- ===================== COPY TO HERE ======================= -->
|
|
129
|
+
</body>
|
|
130
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/registry-item.schema.json",
|
|
3
|
+
"name": "fuel-gauge",
|
|
4
|
+
"type": "frogoe:block",
|
|
5
|
+
"title": "Fuel Gauge",
|
|
6
|
+
"description": "Resource bar with fuel-can icon and numeric label; drains smoothly, pulses danger under 25% via data-low; themeable via --block-*",
|
|
7
|
+
"tags": [
|
|
8
|
+
"hud",
|
|
9
|
+
"fuel",
|
|
10
|
+
"bar",
|
|
11
|
+
"resource",
|
|
12
|
+
"timer"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "fuel-gauge.html",
|
|
17
|
+
"target": "blocks/fuel-gauge.html",
|
|
18
|
+
"type": "frogoe:markup"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"bindings": [
|
|
22
|
+
"data-block-fuel",
|
|
23
|
+
"data-block-fuel-label"
|
|
24
|
+
],
|
|
25
|
+
"placement": "overlay",
|
|
26
|
+
"preview": {
|
|
27
|
+
"demo": "demo.html"
|
|
28
|
+
}
|
|
29
|
+
}
|