@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,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "key-and-door",
|
|
3
|
+
"description": "A key and the locked door it opens. The door is solid until the player touches it after taking the key.",
|
|
4
|
+
"tags": [
|
|
5
|
+
"platformer",
|
|
6
|
+
"puzzle",
|
|
7
|
+
"script"
|
|
8
|
+
],
|
|
9
|
+
"usage": "--at x,y floats the key 40px above (x, y) and stands the door 200px to the right on the same surface. Move either freely; they stay linked through the key's params.door.",
|
|
10
|
+
"entities": [
|
|
11
|
+
{
|
|
12
|
+
"id": "{id}-key",
|
|
13
|
+
"components": {
|
|
14
|
+
"transform": {
|
|
15
|
+
"dx": 0,
|
|
16
|
+
"dy": -40
|
|
17
|
+
},
|
|
18
|
+
"body": {
|
|
19
|
+
"type": "static",
|
|
20
|
+
"width": 24,
|
|
21
|
+
"height": 24,
|
|
22
|
+
"sensor": true
|
|
23
|
+
},
|
|
24
|
+
"sprite": {
|
|
25
|
+
"color": "#ffd166",
|
|
26
|
+
"image": "assets/key.svg",
|
|
27
|
+
"layer": 1
|
|
28
|
+
},
|
|
29
|
+
"script": {
|
|
30
|
+
"src": "scripts/key.ts",
|
|
31
|
+
"params": {
|
|
32
|
+
"door": "{id}-door"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "{id}-door",
|
|
39
|
+
"components": {
|
|
40
|
+
"transform": {
|
|
41
|
+
"dx": 200,
|
|
42
|
+
"dy": -60
|
|
43
|
+
},
|
|
44
|
+
"body": {
|
|
45
|
+
"type": "static",
|
|
46
|
+
"width": 28,
|
|
47
|
+
"height": 120
|
|
48
|
+
},
|
|
49
|
+
"sprite": {
|
|
50
|
+
"color": "#7a4e2d",
|
|
51
|
+
"image": "assets/door.svg",
|
|
52
|
+
"layer": 1
|
|
53
|
+
},
|
|
54
|
+
"script": {
|
|
55
|
+
"src": "scripts/locked-door.ts"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"files": [
|
|
61
|
+
"scripts/key.ts",
|
|
62
|
+
"scripts/locked-door.ts",
|
|
63
|
+
"assets/key.svg",
|
|
64
|
+
"assets/door.svg"
|
|
65
|
+
],
|
|
66
|
+
"effects": {
|
|
67
|
+
"key": {
|
|
68
|
+
"sound": "powerup",
|
|
69
|
+
"particles": {
|
|
70
|
+
"count": 18,
|
|
71
|
+
"color": "#ffd166",
|
|
72
|
+
"speed": 150,
|
|
73
|
+
"life": 0.6,
|
|
74
|
+
"size": 3,
|
|
75
|
+
"gravity": 150
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"door-open": {
|
|
79
|
+
"sound": "explosion",
|
|
80
|
+
"shake": 6,
|
|
81
|
+
"particles": {
|
|
82
|
+
"count": 26,
|
|
83
|
+
"color": "#a0714b",
|
|
84
|
+
"speed": 200,
|
|
85
|
+
"life": 0.6,
|
|
86
|
+
"size": 4
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"at": [
|
|
3
|
+
300,
|
|
4
|
+
500
|
|
5
|
+
],
|
|
6
|
+
"tests": [
|
|
7
|
+
{
|
|
8
|
+
"name": "the door blocks a player without the key",
|
|
9
|
+
"playerAt": [
|
|
10
|
+
360,
|
|
11
|
+
482
|
|
12
|
+
],
|
|
13
|
+
"inputs": "right:2s",
|
|
14
|
+
"assert": [
|
|
15
|
+
"key-and-door-door.exists == true",
|
|
16
|
+
"player.x < 490",
|
|
17
|
+
"events.key == 0"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "taking the key unlocks the door, which opens on contact",
|
|
22
|
+
"inputs": "right:2.5s",
|
|
23
|
+
"assert": [
|
|
24
|
+
"events.key == 1",
|
|
25
|
+
"events.door-open == 1",
|
|
26
|
+
"key-and-door-door.exists == false",
|
|
27
|
+
"player.x > 520"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Script } from "@ludra/core";
|
|
2
|
+
|
|
3
|
+
/** Picked up by a player; unlocks the door named by params.door. */
|
|
4
|
+
export default {
|
|
5
|
+
touch({ self, world, params }, other) {
|
|
6
|
+
if (!other.has("platformer") && !other.has("topdown")) return;
|
|
7
|
+
const door = world.get(params.door as string);
|
|
8
|
+
if (door) door.state.unlocked = true;
|
|
9
|
+
world.emit("key");
|
|
10
|
+
world.destroy(self);
|
|
11
|
+
},
|
|
12
|
+
} satisfies Script;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Script } from "@ludra/core";
|
|
2
|
+
|
|
3
|
+
/** A solid door. Once a key has unlocked it, it opens (disappears) when a player walks into it. */
|
|
4
|
+
export default {
|
|
5
|
+
start({ self }) {
|
|
6
|
+
self.state.unlocked ??= false;
|
|
7
|
+
},
|
|
8
|
+
update({ self, world }) {
|
|
9
|
+
const player = world.player();
|
|
10
|
+
if (self.state.unlocked && player && self.touches(player)) {
|
|
11
|
+
world.emit("door-open");
|
|
12
|
+
world.destroy(self);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
} satisfies Script;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
|
2
|
+
<!-- Tile: riveted metal with hazard stripes for moving platforms. -->
|
|
3
|
+
<rect width="20" height="20" fill="#8fa3d9"/>
|
|
4
|
+
<rect width="20" height="3" fill="#c3d0f2"/>
|
|
5
|
+
<rect y="14" width="20" height="6" fill="#f2c14e"/>
|
|
6
|
+
<path d="M0 20 L 6 14 L 10 14 L 4 20 Z M10 20 L 16 14 L 20 14 L 14 20 Z" fill="#2b3150"/>
|
|
7
|
+
<circle cx="5" cy="8" r="1.4" fill="#5d6f9e"/>
|
|
8
|
+
<circle cx="15" cy="8" r="1.4" fill="#5d6f9e"/>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "moving-platform",
|
|
3
|
+
"description": "A platform that moves back and forth and carries whoever stands on it.",
|
|
4
|
+
"tags": [
|
|
5
|
+
"platformer",
|
|
6
|
+
"movement"
|
|
7
|
+
],
|
|
8
|
+
"usage": "--at x,y is the platform's center where it starts. It moves patrol.distance px right and back; set patrol.axis to \"y\" for an elevator (it moves down first).",
|
|
9
|
+
"entities": [
|
|
10
|
+
{
|
|
11
|
+
"id": "{id}",
|
|
12
|
+
"components": {
|
|
13
|
+
"transform": {
|
|
14
|
+
"dx": 0,
|
|
15
|
+
"dy": 0
|
|
16
|
+
},
|
|
17
|
+
"body": {
|
|
18
|
+
"type": "kinematic",
|
|
19
|
+
"width": 120,
|
|
20
|
+
"height": 20
|
|
21
|
+
},
|
|
22
|
+
"sprite": {
|
|
23
|
+
"color": "#8fa3d9",
|
|
24
|
+
"image": "assets/lift.svg",
|
|
25
|
+
"tile": true
|
|
26
|
+
},
|
|
27
|
+
"patrol": {
|
|
28
|
+
"axis": "x",
|
|
29
|
+
"distance": 160,
|
|
30
|
+
"speed": 80
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"files": [
|
|
36
|
+
"assets/lift.svg"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"at": [
|
|
3
|
+
400,
|
|
4
|
+
400
|
|
5
|
+
],
|
|
6
|
+
"tests": [
|
|
7
|
+
{
|
|
8
|
+
"name": "moves along its patrol",
|
|
9
|
+
"duration": "1s",
|
|
10
|
+
"assert": [
|
|
11
|
+
"moving-platform.x > 470"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "carries a player standing on it",
|
|
16
|
+
"playerAt": [
|
|
17
|
+
400,
|
|
18
|
+
372
|
|
19
|
+
],
|
|
20
|
+
"duration": "1s",
|
|
21
|
+
"assert": [
|
|
22
|
+
"player.grounded == true",
|
|
23
|
+
"player.x > 470"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="26" viewBox="0 0 64 26">
|
|
2
|
+
<!-- Spritesheet: 2 frames of 32x26, a bouncing slime facing right. -->
|
|
3
|
+
<g>
|
|
4
|
+
<path d="M2 25 C 2 10, 8 3, 16 3 C 24 3, 30 10, 30 25 Z" fill="#c77dff"/>
|
|
5
|
+
<path d="M5 25 C 5 18, 9 14, 13 14 C 9 17, 8 21, 8 25 Z" fill="#e0b0ff" opacity="0.7"/>
|
|
6
|
+
<path d="M14 11 l 6 2" stroke="#2a1440" stroke-width="2" stroke-linecap="round"/>
|
|
7
|
+
<path d="M27 11 l -4 2" stroke="#2a1440" stroke-width="2" stroke-linecap="round"/>
|
|
8
|
+
<circle cx="18" cy="15" r="2" fill="#2a1440"/>
|
|
9
|
+
<circle cx="24" cy="15" r="2" fill="#2a1440"/>
|
|
10
|
+
<path d="M17 20 q 4 2 8 0" stroke="#2a1440" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
|
11
|
+
</g>
|
|
12
|
+
<g transform="translate(32 0)">
|
|
13
|
+
<path d="M0 25 C 0 14, 7 8, 16 8 C 25 8, 32 14, 32 25 Z" fill="#c77dff"/>
|
|
14
|
+
<path d="M3 25 C 3 20, 7 17, 11 17 C 7 19, 6 22, 6 25 Z" fill="#e0b0ff" opacity="0.7"/>
|
|
15
|
+
<path d="M14 15 l 6 2" stroke="#2a1440" stroke-width="2" stroke-linecap="round"/>
|
|
16
|
+
<path d="M28 15 l -4 2" stroke="#2a1440" stroke-width="2" stroke-linecap="round"/>
|
|
17
|
+
<circle cx="18" cy="18.5" r="2" fill="#2a1440"/>
|
|
18
|
+
<circle cx="24.5" cy="18.5" r="2" fill="#2a1440"/>
|
|
19
|
+
<path d="M17 22.5 q 4 1.5 8 0" stroke="#2a1440" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "patrol-enemy",
|
|
3
|
+
"description": "An enemy that walks back and forth. Touching it kills; landing on it stomps it.",
|
|
4
|
+
"tags": [
|
|
5
|
+
"platformer",
|
|
6
|
+
"enemy"
|
|
7
|
+
],
|
|
8
|
+
"usage": "--at x,y starts it on the surface whose top is y; it walks patrol.distance px to the right and back.",
|
|
9
|
+
"entities": [
|
|
10
|
+
{
|
|
11
|
+
"id": "{id}",
|
|
12
|
+
"components": {
|
|
13
|
+
"transform": {
|
|
14
|
+
"dx": 0,
|
|
15
|
+
"dy": -13
|
|
16
|
+
},
|
|
17
|
+
"body": {
|
|
18
|
+
"type": "kinematic",
|
|
19
|
+
"width": 32,
|
|
20
|
+
"height": 26,
|
|
21
|
+
"sensor": true
|
|
22
|
+
},
|
|
23
|
+
"sprite": {
|
|
24
|
+
"color": "#c77dff",
|
|
25
|
+
"image": "assets/slime.svg",
|
|
26
|
+
"frameWidth": 32,
|
|
27
|
+
"idle": "0",
|
|
28
|
+
"run": "0-1",
|
|
29
|
+
"fps": 4,
|
|
30
|
+
"layer": 2
|
|
31
|
+
},
|
|
32
|
+
"patrol": {
|
|
33
|
+
"axis": "x",
|
|
34
|
+
"distance": 160,
|
|
35
|
+
"speed": 60
|
|
36
|
+
},
|
|
37
|
+
"hazard": {
|
|
38
|
+
"stompable": true,
|
|
39
|
+
"score": 1
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"files": [
|
|
45
|
+
"assets/slime.svg"
|
|
46
|
+
],
|
|
47
|
+
"effects": {
|
|
48
|
+
"stomp": {
|
|
49
|
+
"sound": "hit",
|
|
50
|
+
"shake": 5,
|
|
51
|
+
"particles": {
|
|
52
|
+
"count": 18,
|
|
53
|
+
"color": "#c77dff",
|
|
54
|
+
"speed": 180,
|
|
55
|
+
"life": 0.5,
|
|
56
|
+
"size": 4
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"at": [
|
|
3
|
+
500,
|
|
4
|
+
500
|
|
5
|
+
],
|
|
6
|
+
"tests": [
|
|
7
|
+
{
|
|
8
|
+
"name": "walks along its patrol",
|
|
9
|
+
"duration": "2s",
|
|
10
|
+
"assert": [
|
|
11
|
+
"patrol-enemy.x > 600"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "kills the player on contact",
|
|
16
|
+
"inputs": "right:3s",
|
|
17
|
+
"assert": [
|
|
18
|
+
"deaths >= 1"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 20 16">
|
|
2
|
+
<!-- Tile: repeats horizontally across the hazard. -->
|
|
3
|
+
<path d="M0 16 L 5 1 L 10 16 Z" fill="#e4e8f2"/>
|
|
4
|
+
<path d="M5 1 L 10 16 L 6.5 16 Z" fill="#a9b1c7"/>
|
|
5
|
+
<path d="M10 16 L 15 1 L 20 16 Z" fill="#e4e8f2"/>
|
|
6
|
+
<path d="M15 1 L 20 16 L 16.5 16 Z" fill="#a9b1c7"/>
|
|
7
|
+
<rect x="0" y="13" width="20" height="3" fill="#ff5d73"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spikes",
|
|
3
|
+
"description": "A row of spikes. Touching them kills the player.",
|
|
4
|
+
"tags": [
|
|
5
|
+
"platformer",
|
|
6
|
+
"hazard"
|
|
7
|
+
],
|
|
8
|
+
"usage": "--at x,y sets them on the surface whose top is y. Widen body.width in multiples of 20 so the art tiles cleanly.",
|
|
9
|
+
"entities": [
|
|
10
|
+
{
|
|
11
|
+
"id": "{id}",
|
|
12
|
+
"components": {
|
|
13
|
+
"transform": {
|
|
14
|
+
"dx": 0,
|
|
15
|
+
"dy": -8
|
|
16
|
+
},
|
|
17
|
+
"body": {
|
|
18
|
+
"type": "static",
|
|
19
|
+
"width": 80,
|
|
20
|
+
"height": 16,
|
|
21
|
+
"sensor": true
|
|
22
|
+
},
|
|
23
|
+
"sprite": {
|
|
24
|
+
"color": "#ff5d73",
|
|
25
|
+
"image": "assets/spikes.svg",
|
|
26
|
+
"tile": true,
|
|
27
|
+
"layer": 1
|
|
28
|
+
},
|
|
29
|
+
"hazard": {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"files": [
|
|
34
|
+
"assets/spikes.svg"
|
|
35
|
+
],
|
|
36
|
+
"effects": {
|
|
37
|
+
"die": {
|
|
38
|
+
"sound": "explosion",
|
|
39
|
+
"shake": 10,
|
|
40
|
+
"particles": {
|
|
41
|
+
"count": 30,
|
|
42
|
+
"color": "#ff5d73",
|
|
43
|
+
"speed": 220,
|
|
44
|
+
"life": 0.7,
|
|
45
|
+
"size": 4
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="12" viewBox="0 0 80 12">
|
|
2
|
+
<!-- Spritesheet: 2 frames of 40x12, a pulsing teleporter pad. -->
|
|
3
|
+
<g>
|
|
4
|
+
<rect x="0" y="6" width="40" height="6" rx="2" fill="#2b3150"/>
|
|
5
|
+
<ellipse cx="20" cy="6" rx="18" ry="4" fill="#4cc9f0"/>
|
|
6
|
+
<ellipse cx="20" cy="6" rx="11" ry="2.4" fill="#c9f4ff"/>
|
|
7
|
+
</g>
|
|
8
|
+
<g transform="translate(40 0)">
|
|
9
|
+
<rect x="0" y="6" width="40" height="6" rx="2" fill="#2b3150"/>
|
|
10
|
+
<ellipse cx="20" cy="6" rx="18" ry="4" fill="#3aa6c8"/>
|
|
11
|
+
<ellipse cx="20" cy="6" rx="14" ry="3" fill="#7fe0ff"/>
|
|
12
|
+
<ellipse cx="20" cy="6" rx="6" ry="1.4" fill="#ffffff"/>
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teleporter",
|
|
3
|
+
"description": "Two linked pads. Stepping on one moves the player to the other.",
|
|
4
|
+
"tags": [
|
|
5
|
+
"platformer",
|
|
6
|
+
"movement",
|
|
7
|
+
"script"
|
|
8
|
+
],
|
|
9
|
+
"usage": "--at x,y puts pad A on the surface whose top is y and pad B 300px to the right. Move B anywhere (even onto a high ledge); each pad's params.target names the other.",
|
|
10
|
+
"entities": [
|
|
11
|
+
{
|
|
12
|
+
"id": "{id}-a",
|
|
13
|
+
"components": {
|
|
14
|
+
"transform": {
|
|
15
|
+
"dx": 0,
|
|
16
|
+
"dy": -6
|
|
17
|
+
},
|
|
18
|
+
"body": {
|
|
19
|
+
"type": "static",
|
|
20
|
+
"width": 40,
|
|
21
|
+
"height": 12,
|
|
22
|
+
"sensor": true
|
|
23
|
+
},
|
|
24
|
+
"sprite": {
|
|
25
|
+
"color": "#4cc9f0",
|
|
26
|
+
"image": "assets/teleporter.svg",
|
|
27
|
+
"frameWidth": 40,
|
|
28
|
+
"idle": "0-1",
|
|
29
|
+
"fps": 4,
|
|
30
|
+
"layer": 1
|
|
31
|
+
},
|
|
32
|
+
"script": {
|
|
33
|
+
"src": "scripts/teleporter.ts",
|
|
34
|
+
"params": {
|
|
35
|
+
"target": "{id}-b"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "{id}-b",
|
|
42
|
+
"components": {
|
|
43
|
+
"transform": {
|
|
44
|
+
"dx": 300,
|
|
45
|
+
"dy": -6
|
|
46
|
+
},
|
|
47
|
+
"body": {
|
|
48
|
+
"type": "static",
|
|
49
|
+
"width": 40,
|
|
50
|
+
"height": 12,
|
|
51
|
+
"sensor": true
|
|
52
|
+
},
|
|
53
|
+
"sprite": {
|
|
54
|
+
"color": "#4cc9f0",
|
|
55
|
+
"image": "assets/teleporter.svg",
|
|
56
|
+
"frameWidth": 40,
|
|
57
|
+
"idle": "0-1",
|
|
58
|
+
"fps": 4,
|
|
59
|
+
"layer": 1
|
|
60
|
+
},
|
|
61
|
+
"script": {
|
|
62
|
+
"src": "scripts/teleporter.ts",
|
|
63
|
+
"params": {
|
|
64
|
+
"target": "{id}-a"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"files": [
|
|
71
|
+
"scripts/teleporter.ts",
|
|
72
|
+
"assets/teleporter.svg"
|
|
73
|
+
],
|
|
74
|
+
"effects": {
|
|
75
|
+
"teleport": {
|
|
76
|
+
"sound": "powerup",
|
|
77
|
+
"particles": {
|
|
78
|
+
"count": 20,
|
|
79
|
+
"color": "#4cc9f0",
|
|
80
|
+
"speed": 120,
|
|
81
|
+
"direction": -90,
|
|
82
|
+
"spread": 90,
|
|
83
|
+
"life": 0.5,
|
|
84
|
+
"size": 3,
|
|
85
|
+
"gravity": -60
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Script } from "@ludra/core";
|
|
2
|
+
|
|
3
|
+
/** Moves a player who steps on this pad onto the pad named by params.target. */
|
|
4
|
+
export default {
|
|
5
|
+
touch({ self, world, params }, other) {
|
|
6
|
+
if (!other.has("platformer") && !other.has("topdown")) return;
|
|
7
|
+
// Arriving on the other pad touches it too; a short cooldown stops an instant trip back.
|
|
8
|
+
if (world.time < ((self.state.cooldownUntil as number) ?? 0)) return;
|
|
9
|
+
const target = world.get(params.target as string);
|
|
10
|
+
if (!target) return;
|
|
11
|
+
other.x = target.x;
|
|
12
|
+
other.y = target.y - target.height / 2 - other.height / 2 - 2;
|
|
13
|
+
other.vy = 0;
|
|
14
|
+
target.state.cooldownUntil = world.time + 0.5;
|
|
15
|
+
world.emit("teleport");
|
|
16
|
+
},
|
|
17
|
+
} satisfies Script;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "time-limit",
|
|
3
|
+
"description": "A countdown. When it reaches zero the player dies and the clock restarts.",
|
|
4
|
+
"tags": [
|
|
5
|
+
"rules",
|
|
6
|
+
"script"
|
|
7
|
+
],
|
|
8
|
+
"usage": "No --at needed. Set params.seconds. Its state.left is the time remaining, so tests can check it.",
|
|
9
|
+
"positioned": false,
|
|
10
|
+
"entities": [
|
|
11
|
+
{
|
|
12
|
+
"id": "{id}",
|
|
13
|
+
"components": {
|
|
14
|
+
"transform": {
|
|
15
|
+
"dx": 0,
|
|
16
|
+
"dy": 0
|
|
17
|
+
},
|
|
18
|
+
"script": {
|
|
19
|
+
"src": "scripts/time-limit.ts",
|
|
20
|
+
"params": {
|
|
21
|
+
"seconds": 30
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"scripts/time-limit.ts"
|
|
29
|
+
],
|
|
30
|
+
"effects": {
|
|
31
|
+
"timeout": {
|
|
32
|
+
"sound": "explosion",
|
|
33
|
+
"shake": 8
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"at": [
|
|
3
|
+
0,
|
|
4
|
+
0
|
|
5
|
+
],
|
|
6
|
+
"tests": [
|
|
7
|
+
{
|
|
8
|
+
"name": "counts down",
|
|
9
|
+
"duration": "1s",
|
|
10
|
+
"assert": [
|
|
11
|
+
"time-limit.state.left < 29.1",
|
|
12
|
+
"time-limit.state.left > 28.9"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "kills the player when time runs out, then restarts",
|
|
17
|
+
"duration": "31s",
|
|
18
|
+
"assert": [
|
|
19
|
+
"deaths == 1",
|
|
20
|
+
"events.timeout == 1",
|
|
21
|
+
"time-limit.state.left > 28"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Script } from "@ludra/core";
|
|
2
|
+
|
|
3
|
+
/** Counts down params.seconds. When time runs out the player dies and the clock restarts. */
|
|
4
|
+
export default {
|
|
5
|
+
start({ self, params }) {
|
|
6
|
+
self.state.left = params.seconds as number;
|
|
7
|
+
},
|
|
8
|
+
update({ self, world, params, dt }) {
|
|
9
|
+
self.state.left = Math.max(0, (self.state.left as number) - dt);
|
|
10
|
+
if (self.state.left === 0) {
|
|
11
|
+
world.emit("timeout");
|
|
12
|
+
world.kill(world.player()!);
|
|
13
|
+
self.state.left = params.seconds as number;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
} satisfies Script;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="24" viewBox="0 0 32 24">
|
|
2
|
+
<!-- A squat cannon facing left. Mirror it (or set sprite.flip) for one that fires right. -->
|
|
3
|
+
<rect x="4" y="14" width="26" height="10" rx="3" fill="#4b5263"/>
|
|
4
|
+
<rect x="4" y="14" width="26" height="3" rx="1.5" fill="#6b7489"/>
|
|
5
|
+
<circle cx="18" cy="12" r="8" fill="#8fa3d9"/>
|
|
6
|
+
<circle cx="18" cy="12" r="5" fill="#c3d0f2"/>
|
|
7
|
+
<rect x="0" y="8" width="14" height="7" rx="2" fill="#5d6f9e"/>
|
|
8
|
+
<rect x="0" y="9" width="3" height="5" fill="#2b3150"/>
|
|
9
|
+
<circle cx="20" cy="11" r="2" fill="#ff5d73"/>
|
|
10
|
+
</svg>
|