@ludra/cli 0.4.0 → 0.7.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/blocks/chasing-enemy/assets/slime.svg +21 -0
- package/blocks/chasing-enemy/block.json +64 -0
- package/blocks/chasing-enemy/demo.json +32 -0
- package/blocks/chasing-enemy/scripts/chaser.ts +19 -0
- package/blocks/coin-row/assets/coin.svg +22 -0
- package/blocks/coin-row/block.json +157 -0
- package/blocks/coin-row/demo.json +15 -0
- package/blocks/falling-platform/assets/falling-platform.svg +13 -0
- package/blocks/falling-platform/block.json +55 -0
- package/blocks/falling-platform/demo.json +24 -0
- package/blocks/falling-platform/scripts/falling-platform.ts +56 -0
- package/blocks/jump-pad/assets/jump-pad.svg +6 -0
- package/blocks/jump-pad/block.json +56 -0
- package/blocks/jump-pad/demo.json +17 -0
- package/blocks/jump-pad/scripts/jump-pad.ts +10 -0
- package/blocks/key-and-door/assets/door.svg +12 -0
- package/blocks/key-and-door/assets/key.svg +9 -0
- package/blocks/key-and-door/block.json +90 -0
- package/blocks/key-and-door/demo.json +31 -0
- package/blocks/key-and-door/scripts/key.ts +12 -0
- package/blocks/key-and-door/scripts/locked-door.ts +15 -0
- package/blocks/moving-platform/assets/lift.svg +9 -0
- package/blocks/moving-platform/block.json +38 -0
- package/blocks/moving-platform/demo.json +27 -0
- package/blocks/patrol-enemy/assets/slime.svg +21 -0
- package/blocks/patrol-enemy/block.json +60 -0
- package/blocks/patrol-enemy/demo.json +22 -0
- package/blocks/spikes/assets/spikes.svg +8 -0
- package/blocks/spikes/block.json +49 -0
- package/blocks/spikes/demo.json +16 -0
- package/blocks/teleporter/assets/teleporter.svg +14 -0
- package/blocks/teleporter/block.json +89 -0
- package/blocks/teleporter/demo.json +17 -0
- package/blocks/teleporter/scripts/teleporter.ts +17 -0
- package/blocks/time-limit/block.json +36 -0
- package/blocks/time-limit/demo.json +25 -0
- package/blocks/time-limit/scripts/time-limit.ts +16 -0
- package/blocks/turret/assets/bullet.svg +5 -0
- package/blocks/turret/assets/turret.svg +10 -0
- package/blocks/turret/block.json +59 -0
- package/blocks/turret/demo.json +35 -0
- package/blocks/turret/scripts/bullet.ts +11 -0
- package/blocks/turret/scripts/turret.ts +36 -0
- package/dist/blocks.d.ts +65 -0
- package/dist/blocks.js +95 -0
- package/dist/commands/blocks.d.ts +12 -0
- package/dist/commands/blocks.js +49 -0
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +8 -3
- package/dist/commands/components.js +5 -1
- package/dist/commands/dev.js +3 -2
- package/dist/commands/new.js +1 -1
- package/dist/commands/play.js +25 -9
- package/dist/commands/test.d.ts +1 -1
- package/dist/commands/test.js +4 -4
- package/dist/headless.js +6 -1
- package/dist/index.js +23 -3
- package/dist/project.d.ts +17 -1
- package/dist/project.js +91 -8
- package/dist/server.d.ts +1 -0
- package/dist/server.js +27 -2
- package/package.json +4 -3
- package/templates/breakout/CLAUDE.md +42 -0
- package/templates/breakout/_gitignore +2 -0
- package/templates/breakout/ludra.json +5 -0
- package/templates/breakout/package.json +13 -0
- package/templates/breakout/scenes/main.scene.json +648 -0
- package/templates/breakout/scripts/backdrop.ts +101 -0
- package/templates/breakout/scripts/ball.ts +90 -0
- package/templates/breakout/scripts/brick.ts +51 -0
- package/templates/breakout/scripts/paddle.ts +53 -0
- package/templates/breakout/scripts/rules.ts +128 -0
- package/templates/breakout/scripts/wall.ts +21 -0
- package/templates/breakout/shaders/neon.frag +42 -0
- package/templates/breakout/tests/gameplay.test.json +31 -0
- package/templates/platformer/CLAUDE.md +4 -1
- package/templates/platformer/package.json +1 -1
- package/templates/topdown/CLAUDE.md +38 -0
- package/templates/topdown/_gitignore +2 -0
- package/templates/topdown/assets/gem.svg +16 -0
- package/templates/topdown/assets/grass.svg +8 -0
- package/templates/topdown/assets/hedge.svg +10 -0
- package/templates/topdown/assets/hero.svg +23 -0
- package/templates/topdown/assets/portal.svg +15 -0
- package/templates/topdown/assets/slime.svg +21 -0
- package/templates/topdown/assets/thorns.svg +7 -0
- package/templates/topdown/ludra.json +5 -0
- package/templates/topdown/package.json +13 -0
- package/templates/topdown/scenes/main.scene.json +499 -0
- package/templates/topdown/scripts/hud.ts +94 -0
- package/templates/topdown/scripts/night.ts +67 -0
- package/templates/topdown/scripts/portal.ts +34 -0
- package/templates/topdown/shaders/dream.frag +31 -0
- package/templates/topdown/tests/gameplay.test.json +30 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Script } from "@ludra/core";
|
|
2
|
+
|
|
3
|
+
/** A stable pseudo-random number per index, so fireflies keep their paths. */
|
|
4
|
+
function hash(n: number): number {
|
|
5
|
+
const x = Math.sin(n * 91.7 + 47.3) * 43758.5453;
|
|
6
|
+
return x - Math.floor(x);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Moonlit night over the grove: darkness everywhere except the hero's lantern,
|
|
11
|
+
* then colored light from glowing things (params.lights maps an id prefix to a
|
|
12
|
+
* color), and fireflies. Presentation only: the darkness never hides a hazard
|
|
13
|
+
* completely (params.darkness stays well below 1).
|
|
14
|
+
*/
|
|
15
|
+
export default {
|
|
16
|
+
drawUI({ g, world, view, time }) {
|
|
17
|
+
const toScreen = (x: number, y: number) => [(x - view.cameraX) * view.zoom, (y - view.cameraY) * view.zoom] as const;
|
|
18
|
+
const hero = world.player();
|
|
19
|
+
const darkness = 0.62;
|
|
20
|
+
|
|
21
|
+
// Darkness with a soft hole where the lantern shines.
|
|
22
|
+
const [hx, hy] = hero ? toScreen(hero.x, hero.y) : [view.width / 2, view.height / 2];
|
|
23
|
+
const flicker = 1 + Math.sin(time * 11) * 0.015 + Math.sin(time * 7.3) * 0.02;
|
|
24
|
+
const night = g.createRadialGradient(hx, hy, 30 * flicker, hx, hy, 210 * flicker);
|
|
25
|
+
night.addColorStop(0, "rgba(6, 10, 30, 0)");
|
|
26
|
+
night.addColorStop(1, `rgba(6, 10, 30, ${darkness})`);
|
|
27
|
+
g.fillStyle = night;
|
|
28
|
+
g.fillRect(0, 0, view.width, view.height);
|
|
29
|
+
|
|
30
|
+
// Light from glowing things, added on top of the darkness.
|
|
31
|
+
g.globalCompositeOperation = "lighter";
|
|
32
|
+
const lights: Record<string, string> = { "gem-": "#2fb8ff", portal: "#b25cff", "slime-": "#8a3cff", thorns: "#ff3355" };
|
|
33
|
+
for (const e of world.all()) {
|
|
34
|
+
const prefix = Object.keys(lights).find((p) => e.id.startsWith(p));
|
|
35
|
+
if (!prefix) continue;
|
|
36
|
+
const [x, y] = toScreen(e.x, e.y);
|
|
37
|
+
const radius = (prefix === "portal" ? 110 : 60) * (0.85 + 0.15 * Math.sin(time * 2.4 + e.x));
|
|
38
|
+
const light = g.createRadialGradient(x, y, 0, x, y, radius);
|
|
39
|
+
light.addColorStop(0, `${lights[prefix]}66`);
|
|
40
|
+
light.addColorStop(1, `${lights[prefix]}00`);
|
|
41
|
+
g.fillStyle = light;
|
|
42
|
+
g.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
43
|
+
}
|
|
44
|
+
if (hero) {
|
|
45
|
+
const lantern = g.createRadialGradient(hx, hy, 0, hx, hy, 120 * flicker);
|
|
46
|
+
lantern.addColorStop(0, "rgba(255, 190, 110, 0.32)");
|
|
47
|
+
lantern.addColorStop(1, "rgba(255, 190, 110, 0)");
|
|
48
|
+
g.fillStyle = lantern;
|
|
49
|
+
g.fillRect(hx - 130, hy - 130, 260, 260);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fireflies drifting on their own little loops.
|
|
53
|
+
for (let i = 0; i < 36; i++) {
|
|
54
|
+
const [x, y] = toScreen(
|
|
55
|
+
hash(i) * world.width + Math.sin(time * (0.3 + hash(i + 50) * 0.5) + i) * 40,
|
|
56
|
+
hash(i + 100) * world.height + Math.cos(time * (0.25 + hash(i + 80) * 0.4) + i * 2) * 30,
|
|
57
|
+
);
|
|
58
|
+
const blink = Math.max(0, Math.sin(time * (1.5 + hash(i + 7) * 2) + i * 3));
|
|
59
|
+
g.fillStyle = `rgba(220, 255, 140, ${0.75 * blink})`;
|
|
60
|
+
g.shadowColor = "#d4ff6b";
|
|
61
|
+
g.shadowBlur = 10;
|
|
62
|
+
g.beginPath();
|
|
63
|
+
g.arc(x, y, 1.6, 0, Math.PI * 2);
|
|
64
|
+
g.fill();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
} satisfies Script;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Script } from "@ludra/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A vortex of light over the portal sprite. It spins slowly while the portal
|
|
5
|
+
* is asleep and wakes up (faster, brighter) once params.need crystals are found.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
draw({ g, self, world, params, time }) {
|
|
9
|
+
const awake = world.score >= (params.need as number);
|
|
10
|
+
const spin = time * (awake ? 3.2 : 0.9);
|
|
11
|
+
g.globalCompositeOperation = "lighter";
|
|
12
|
+
g.lineCap = "round";
|
|
13
|
+
for (let i = 0; i < 5; i++) {
|
|
14
|
+
const radius = 8 + i * 4.5;
|
|
15
|
+
const start = spin * (i % 2 === 0 ? 1 : -1.3) + i;
|
|
16
|
+
g.strokeStyle = i % 2 === 0 ? "#d8a6ff" : "#6be4ff";
|
|
17
|
+
g.globalAlpha = awake ? 0.9 : 0.35;
|
|
18
|
+
g.lineWidth = awake ? 2.5 : 1.5;
|
|
19
|
+
g.shadowColor = g.strokeStyle;
|
|
20
|
+
g.shadowBlur = awake ? 14 : 6;
|
|
21
|
+
g.beginPath();
|
|
22
|
+
g.arc(self.x, self.y, radius, start, start + Math.PI * 1.1);
|
|
23
|
+
g.stroke();
|
|
24
|
+
}
|
|
25
|
+
if (awake) {
|
|
26
|
+
const core = g.createRadialGradient(self.x, self.y, 0, self.x, self.y, 26);
|
|
27
|
+
core.addColorStop(0, "rgba(255, 255, 255, 0.9)");
|
|
28
|
+
core.addColorStop(1, "rgba(178, 92, 255, 0)");
|
|
29
|
+
g.globalAlpha = 0.7 + 0.3 * Math.sin(time * 6);
|
|
30
|
+
g.fillStyle = core;
|
|
31
|
+
g.fillRect(self.x - 26, self.y - 26, 52, 52);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
} satisfies Script;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// A soft, dreamy finish: bloom that makes the lights bleed, a cool-shadow and
|
|
2
|
+
// warm-highlight grade, and a vignette.
|
|
3
|
+
// The engine declares u_image, u_resolution, u_time and v_uv, plus every
|
|
4
|
+
// uniform listed under "postfx.uniforms" in the scene.
|
|
5
|
+
|
|
6
|
+
void main() {
|
|
7
|
+
vec2 pixel = 1.0 / u_resolution;
|
|
8
|
+
vec3 color = texture2D(u_image, v_uv).rgb;
|
|
9
|
+
|
|
10
|
+
// Gaussian-weighted taps: far taps count less, so thin bright text doesn't leave a dotted grid.
|
|
11
|
+
vec3 glow = vec3(0.0);
|
|
12
|
+
float total = 0.0;
|
|
13
|
+
for (int i = -5; i <= 5; i++) {
|
|
14
|
+
for (int j = -5; j <= 5; j++) {
|
|
15
|
+
float weight = exp(-float(i * i + j * j) / 12.0);
|
|
16
|
+
vec2 offset = vec2(float(i), float(j)) * pixel * 2.0;
|
|
17
|
+
glow += max(texture2D(u_image, v_uv + offset).rgb - 0.35, 0.0) * weight;
|
|
18
|
+
total += weight;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
color += glow / total * bloom;
|
|
22
|
+
|
|
23
|
+
// Shadows lean blue, highlights lean gold.
|
|
24
|
+
float luma = dot(color, vec3(0.299, 0.587, 0.114));
|
|
25
|
+
color = mix(color * vec3(0.9, 0.97, 1.12), color * vec3(1.08, 1.0, 0.88), smoothstep(0.2, 0.8, luma) * warmth);
|
|
26
|
+
|
|
27
|
+
vec2 centered = v_uv - 0.5;
|
|
28
|
+
color *= 1.0 - vignette * dot(centered, centered) * 1.6;
|
|
29
|
+
|
|
30
|
+
gl_FragColor = vec4(color, 1.0);
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tests": [
|
|
3
|
+
{
|
|
4
|
+
"name": "the hero stays put without input (no gravity in a top-down game)",
|
|
5
|
+
"duration": "1s",
|
|
6
|
+
"assert": ["hero.x == 80", "hero.y == 270", "deaths == 0"]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "the hero moves diagonally",
|
|
10
|
+
"inputs": "right+down:0.5s",
|
|
11
|
+
"duration": "0.5s",
|
|
12
|
+
"assert": ["hero.x > 130", "hero.y > 320"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "hedges block the hero",
|
|
16
|
+
"inputs": "right:3s",
|
|
17
|
+
"assert": ["hero.x < 270", "deaths == 0"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "rushing through the middle corridor gets the hero caught by the slime",
|
|
21
|
+
"inputs": "up:1s, right:0.39s, down:2s, right:0.17s, right:1.61s, left:0.5s, wait:0.1s, up:1.95s",
|
|
22
|
+
"assert": ["deaths == 1", "events.die == 1"]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "the grove can be cleared without dying",
|
|
26
|
+
"inputs": "up:1s, right:0.39s, down:2s, right:0.17s, right:1.61s, left:0.5s, wait:1.5s, up:1.95s, right:0.5s, right:1.94s, right:0.44s, wait:1.2s, down:2s",
|
|
27
|
+
"assert": ["won == true", "deaths == 0", "score == 5"]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|