@homie-rocks/studio 0.38.0 → 0.40.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/CHANGELOG.md +57 -1
- package/bin/homie-studio.mjs +13 -1
- package/lib/build.mjs +4 -0
- package/lib/cloudflare.mjs +2 -2
- package/lib/dev.mjs +12 -1
- package/lib/mcp-tools.mjs +10 -0
- package/lib/port-check.mjs +33 -7
- package/lib/rules-build.mjs +8 -1
- package/lib/rules-check-worker.mjs +3 -3
- package/lib/rules-check.mjs +3 -1
- package/lib/scaffold.mjs +19 -6
- package/lib/studio.mjs +1 -1
- package/lib/template-history.json +82 -0
- package/lib/tools-build.mjs +55 -0
- package/lib/tools-cli.mjs +55 -0
- package/lib/worker-build.mjs +35 -0
- package/netplay/NETPLAY.md +74 -2
- package/package.json +17 -6
- package/rules/collision.ts +115 -0
- package/rules/core.ts +65 -25
- package/rules/host.ts +5 -1
- package/rules/math.ts +15 -5
- package/rules/rules.ts +33 -10
- package/rules/types.ts +2 -2
- package/rules/view.ts +126 -32
- package/starters/ember-vale/game.json +16 -4
- package/starters/ember-vale/map/main.json +66 -0
- package/starters/ember-vale/src/move.ts +31 -0
- package/starters/ember-vale/src/rules.ts +244 -0
- package/starters/ember-vale/src/{main.ts → view.ts} +89 -574
- package/starters/ember-vale/tunables.json +100 -11
- package/starters/gem-rush/src/move.ts +9 -1
- package/starters/gem-rush/src/view.ts +2 -1
- package/starters/gem-rush-3d/game.json +23 -5
- package/starters/gem-rush-3d/map/main.json +153 -0
- package/starters/gem-rush-3d/src/move.ts +37 -0
- package/starters/gem-rush-3d/src/rules.ts +133 -0
- package/starters/gem-rush-3d/src/{main.ts → view.ts} +63 -671
- package/starters/gem-rush-3d/tunables.json +98 -11
- package/starters/hero-rush-3d/game.json +23 -5
- package/starters/hero-rush-3d/map/main.json +153 -0
- package/starters/hero-rush-3d/src/move.ts +49 -0
- package/starters/hero-rush-3d/src/rules.ts +200 -0
- package/starters/hero-rush-3d/src/{main.ts → view.ts} +83 -976
- package/starters/hero-rush-3d/tunables.json +260 -30
- package/tools/TOOLS.md +113 -0
- package/tools/tools.ts +28 -0
- package/worker/app-records.mjs +8 -0
- package/worker/index.mjs +26 -0
- package/worker/mcp-room.mjs +61 -0
- package/worker/mcp-store.mjs +45 -0
- package/worker/mcp-tools.mjs +105 -0
- package/worker/mcp.mjs +159 -0
- package/worker/office-page.mjs +1 -1
- package/worker/office.mjs +2 -1
- package/worker/purchase-mcp.mjs +3 -3
- package/worker/tool-events.mjs +14 -0
- package/worker/tool-identity.mjs +4 -0
- package/worker/tool-webhook.mjs +28 -0
- package/worker/version.mjs +1 -1
|
@@ -1,13 +1,102 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"public": {
|
|
3
|
+
"strikeRange": {
|
|
4
|
+
"value": 1.92,
|
|
5
|
+
"min": 1.0,
|
|
6
|
+
"max": 3.6,
|
|
7
|
+
"step": 0.02,
|
|
8
|
+
"unit": "m",
|
|
9
|
+
"group": "Strike",
|
|
10
|
+
"note": "How far a strike reaches (a slime's size adds to it)"
|
|
11
|
+
},
|
|
12
|
+
"strikeMs": {
|
|
13
|
+
"value": 380,
|
|
14
|
+
"min": 150,
|
|
15
|
+
"max": 900,
|
|
16
|
+
"step": 10,
|
|
17
|
+
"unit": "ms",
|
|
18
|
+
"group": "Strike",
|
|
19
|
+
"note": "The shortest time between two strikes"
|
|
20
|
+
},
|
|
21
|
+
"flashMs": {
|
|
22
|
+
"value": 200,
|
|
23
|
+
"min": 60,
|
|
24
|
+
"max": 500,
|
|
25
|
+
"step": 10,
|
|
26
|
+
"unit": "ms",
|
|
27
|
+
"group": "Strike",
|
|
28
|
+
"note": "How long a strike shows on every screen"
|
|
29
|
+
},
|
|
30
|
+
"swingMs": {
|
|
31
|
+
"value": 150,
|
|
32
|
+
"min": 40,
|
|
33
|
+
"max": 400,
|
|
34
|
+
"step": 5,
|
|
35
|
+
"unit": "ms",
|
|
36
|
+
"group": "Strike",
|
|
37
|
+
"note": "The swipe around the hero, and its smear as it carries through"
|
|
38
|
+
},
|
|
39
|
+
"hitStopMs": {
|
|
40
|
+
"value": 60,
|
|
41
|
+
"min": 0,
|
|
42
|
+
"max": 200,
|
|
43
|
+
"step": 5,
|
|
44
|
+
"unit": "ms",
|
|
45
|
+
"group": "Hit",
|
|
46
|
+
"note": "The hit lands: the slime freezes, white, before it is pushed"
|
|
47
|
+
},
|
|
48
|
+
"slimePush": {
|
|
49
|
+
"value": 0.28,
|
|
50
|
+
"min": 0.0,
|
|
51
|
+
"max": 1.6,
|
|
52
|
+
"step": 0.02,
|
|
53
|
+
"unit": "m",
|
|
54
|
+
"group": "Hit",
|
|
55
|
+
"note": "How far a strike pushes a slime back"
|
|
56
|
+
},
|
|
57
|
+
"lunge": {
|
|
58
|
+
"value": 0.28,
|
|
59
|
+
"min": 0.0,
|
|
60
|
+
"max": 0.8,
|
|
61
|
+
"step": 0.02,
|
|
62
|
+
"unit": "m",
|
|
63
|
+
"group": "Strike",
|
|
64
|
+
"note": "The hero steps into each strike (as far as the push, so a combo stays in reach)"
|
|
65
|
+
},
|
|
66
|
+
"pushMs": {
|
|
67
|
+
"value": 220,
|
|
68
|
+
"min": 60,
|
|
69
|
+
"max": 600,
|
|
70
|
+
"step": 10,
|
|
71
|
+
"unit": "ms",
|
|
72
|
+
"group": "Hit",
|
|
73
|
+
"note": "How long the push takes (it eases out)"
|
|
74
|
+
},
|
|
75
|
+
"wobble": {
|
|
76
|
+
"value": 0.3,
|
|
77
|
+
"min": 0,
|
|
78
|
+
"max": 0.7,
|
|
79
|
+
"step": 0.01,
|
|
80
|
+
"group": "Hit",
|
|
81
|
+
"note": "How much a struck slime squashes and wobbles"
|
|
82
|
+
},
|
|
83
|
+
"wobbleMs": {
|
|
84
|
+
"value": 420,
|
|
85
|
+
"min": 100,
|
|
86
|
+
"max": 1000,
|
|
87
|
+
"step": 10,
|
|
88
|
+
"unit": "ms",
|
|
89
|
+
"group": "Hit",
|
|
90
|
+
"note": "How long the wobble takes to die away (overlap)"
|
|
91
|
+
},
|
|
92
|
+
"shake": {
|
|
93
|
+
"value": 4,
|
|
94
|
+
"min": 0,
|
|
95
|
+
"max": 16,
|
|
96
|
+
"step": 0.5,
|
|
97
|
+
"unit": "px",
|
|
98
|
+
"group": "Hit",
|
|
99
|
+
"note": "The camera's kick when your strike lands"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
13
102
|
}
|
|
@@ -18,6 +18,14 @@ export const move = defineMove({
|
|
|
18
18
|
const keep = M.pow(1 - m.accel / 60, ctx.dt * 60);
|
|
19
19
|
body.vel = M.add(want, M.scale(M.sub(body.vel, want), keep));
|
|
20
20
|
if (M.len(body.vel) > 0.6) body.heading = M.norm(body.vel);
|
|
21
|
-
|
|
21
|
+
// Keep the tangential travel when a thumb points slightly into an arena wall.
|
|
22
|
+
// The original canvas starter clamped each axis independently.
|
|
23
|
+
const delta = M.scale(body.vel, ctx.dt), before = body.pos;
|
|
24
|
+
const hit = ctx.map.sweep(body, delta);
|
|
25
|
+
if (hit) {
|
|
26
|
+
const remaining = M.sub(delta, M.sub(body.pos, before));
|
|
27
|
+
const into = M.dot(remaining, hit.normal);
|
|
28
|
+
if (into < 0) ctx.map.sweep(body, M.sub(remaining, M.scale(hit.normal, into)));
|
|
29
|
+
}
|
|
22
30
|
},
|
|
23
31
|
});
|
|
@@ -420,7 +420,8 @@ function frame(t: number): void {
|
|
|
420
420
|
const dt = lab.time.dt(t, 0.05);
|
|
421
421
|
updateView();
|
|
422
422
|
if (lab.on) labReport(dt);
|
|
423
|
-
|
|
423
|
+
// updateView can deliver an effect newer than a delayed rAF timestamp.
|
|
424
|
+
draw(performance.now()); frames++; requestAnimationFrame(frame);
|
|
424
425
|
}
|
|
425
426
|
room.probe({ frames: () => frames, waves: () => wavesSeen, knocks: () => knocksSeen, zone: () => zone,
|
|
426
427
|
camera: () => ({ ...cam, view: viewSeat() }),
|
|
@@ -2,11 +2,29 @@
|
|
|
2
2
|
"id": "gem-rush-3d",
|
|
3
3
|
"name": "Gem Rush 3D",
|
|
4
4
|
"blurb": "Gem Rush in a meadow: animals grab gems and bump each other away. Sixty-second rounds that never stop, drawn in three.js with free CC0 models.",
|
|
5
|
-
"players": {
|
|
5
|
+
"players": {
|
|
6
|
+
"min": 1,
|
|
7
|
+
"max": 8
|
|
8
|
+
},
|
|
6
9
|
"roundSeconds": 60,
|
|
7
|
-
"entry": "src/
|
|
8
|
-
"netplay": {
|
|
9
|
-
|
|
10
|
+
"entry": "src/view.ts",
|
|
11
|
+
"netplay": {
|
|
12
|
+
"v": 1,
|
|
13
|
+
"public": true
|
|
14
|
+
},
|
|
15
|
+
"needs": {
|
|
16
|
+
"three": "0.185.1",
|
|
17
|
+
"@types/three": "0.185.1"
|
|
18
|
+
},
|
|
10
19
|
"assets": "library",
|
|
11
|
-
"landing": {
|
|
20
|
+
"landing": {
|
|
21
|
+
"pitch": "Grab the gems. Bump your rivals away.",
|
|
22
|
+
"controls": {
|
|
23
|
+
"phone": "Drag to move; a second finger bumps.",
|
|
24
|
+
"computer": "WASD or arrows to move, Space to bump."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"room": {
|
|
28
|
+
"host": "server"
|
|
29
|
+
}
|
|
12
30
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bounds": {
|
|
3
|
+
"min": [
|
|
4
|
+
0,
|
|
5
|
+
0,
|
|
6
|
+
0
|
|
7
|
+
],
|
|
8
|
+
"max": [
|
|
9
|
+
26,
|
|
10
|
+
16,
|
|
11
|
+
8
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"circles": [
|
|
15
|
+
{
|
|
16
|
+
"at": [
|
|
17
|
+
13,
|
|
18
|
+
4,
|
|
19
|
+
0
|
|
20
|
+
],
|
|
21
|
+
"r": 1.05
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"at": [
|
|
25
|
+
4.6,
|
|
26
|
+
3.8,
|
|
27
|
+
0
|
|
28
|
+
],
|
|
29
|
+
"r": 0.75
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"at": [
|
|
33
|
+
21.4,
|
|
34
|
+
4.6,
|
|
35
|
+
0
|
|
36
|
+
],
|
|
37
|
+
"r": 0.55
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"at": [
|
|
41
|
+
6,
|
|
42
|
+
12.6,
|
|
43
|
+
0
|
|
44
|
+
],
|
|
45
|
+
"r": 0.5
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"at": [
|
|
49
|
+
20.2,
|
|
50
|
+
12.2,
|
|
51
|
+
0
|
|
52
|
+
],
|
|
53
|
+
"r": 0.75
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"at": [
|
|
57
|
+
9.6,
|
|
58
|
+
13.4,
|
|
59
|
+
0
|
|
60
|
+
],
|
|
61
|
+
"r": 0.45
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"at": [
|
|
65
|
+
23.6,
|
|
66
|
+
2,
|
|
67
|
+
0
|
|
68
|
+
],
|
|
69
|
+
"r": 0.5
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"spots": {
|
|
73
|
+
"start": [
|
|
74
|
+
[
|
|
75
|
+
19.6,
|
|
76
|
+
8.0,
|
|
77
|
+
0
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
17.66690475583121,
|
|
81
|
+
11.535533905932738,
|
|
82
|
+
0
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
13.0,
|
|
86
|
+
13.0,
|
|
87
|
+
0
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
8.333095244168787,
|
|
91
|
+
11.535533905932738,
|
|
92
|
+
0
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
6.4,
|
|
96
|
+
8.0,
|
|
97
|
+
0
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
8.333095244168785,
|
|
101
|
+
4.464466094067262,
|
|
102
|
+
0
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
13,
|
|
106
|
+
2.509,
|
|
107
|
+
0
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
17.66690475583121,
|
|
111
|
+
4.464466094067261,
|
|
112
|
+
0
|
|
113
|
+
]
|
|
114
|
+
],
|
|
115
|
+
"obstacles": [
|
|
116
|
+
[
|
|
117
|
+
13,
|
|
118
|
+
4,
|
|
119
|
+
1.05
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
4.6,
|
|
123
|
+
3.8,
|
|
124
|
+
0.75
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
21.4,
|
|
128
|
+
4.6,
|
|
129
|
+
0.55
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
6,
|
|
133
|
+
12.6,
|
|
134
|
+
0.5
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
20.2,
|
|
138
|
+
12.2,
|
|
139
|
+
0.75
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
9.6,
|
|
143
|
+
13.4,
|
|
144
|
+
0.45
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
23.6,
|
|
148
|
+
2,
|
|
149
|
+
0.5
|
|
150
|
+
]
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineMove, type Vec3 } from '@homie-rocks/studio/rules';
|
|
2
|
+
|
|
3
|
+
import type { MoveBody, MoveContext } from '@homie-rocks/studio/rules/types';
|
|
4
|
+
|
|
5
|
+
function slide<M>(body: MoveBody<M>, delta: Vec3, ctx: MoveContext<unknown, M>) {
|
|
6
|
+
const before = body.pos, hit = ctx.map.sweep(body, delta);
|
|
7
|
+
if (!hit) return;
|
|
8
|
+
const M = ctx.math, remaining = M.sub(delta, M.sub(body.pos, before));
|
|
9
|
+
const into = M.dot(remaining, hit.normal);
|
|
10
|
+
if (into < 0) ctx.map.sweep(body, M.sub(remaining, M.scale(hit.normal, into)));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const move = defineMove({
|
|
14
|
+
runner(body, input, ctx) {
|
|
15
|
+
const M = ctx.math, m = body.motion, T = ctx.tune;
|
|
16
|
+
if (m.knockAt > 0) {
|
|
17
|
+
const last = ctx.tick + 1 >= m.knockUntil;
|
|
18
|
+
const u = M.clamp((ctx.tick - m.knockAt) / (m.knockUntil - m.knockAt), 0, 1);
|
|
19
|
+
const ease = last ? 1 : 1 - M.pow(1 - u, T.knockEase);
|
|
20
|
+
// A wall consumes blocked travel. Chasing the absolute endpoint stores up
|
|
21
|
+
// that travel and releases it as a burst when the body slides clear.
|
|
22
|
+
const previous = M.clamp((ctx.tick - 1 - m.knockAt) / (m.knockUntil - m.knockAt), 0, 1);
|
|
23
|
+
const before = 1 - M.pow(1 - previous, T.knockEase);
|
|
24
|
+
const travel = M.scale(m.knockDir, T.knockDistance * (ease - before));
|
|
25
|
+
const delta = travel;
|
|
26
|
+
slide(body, delta, ctx);
|
|
27
|
+
body.vel = last ? { x: 0, y: 0, z: 0 } : M.scale(delta, 1 / ctx.dt);
|
|
28
|
+
if (last) m.knockAt = 0;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const want = M.scale(M.clampLen({ x: input.ax / 127, y: input.ay / 127, z: 0 }, 1), m.speed);
|
|
32
|
+
const keep = M.pow(1 - m.accel / 60, ctx.dt * 60);
|
|
33
|
+
body.vel = M.add(want, M.scale(M.sub(body.vel, want), keep));
|
|
34
|
+
if (M.len(body.vel) > 0.6) body.heading = M.norm(body.vel);
|
|
35
|
+
slide(body, M.scale(body.vel, ctx.dt), ctx);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { defineRules, f, type GameWorld } from '@homie-rocks/studio/rules';
|
|
2
|
+
import { move } from './move';
|
|
3
|
+
|
|
4
|
+
function gemPoint(world: Pick<GameWorld, 'random' | 'map' | 'math'>) {
|
|
5
|
+
let p = { x: 1.2 + world.random() * 23.6, y: 1.2 + world.random() * 13.6, z: 0 };
|
|
6
|
+
for (let i = 0; i < 12; i++) {
|
|
7
|
+
let blocked = false;
|
|
8
|
+
for (const o of world.map.spots('obstacles')) if (world.math.dist(p, { x: o.x, y: o.y, z: 0 }) < o.z + 0.7) blocked = true;
|
|
9
|
+
if (!blocked) break;
|
|
10
|
+
p = { x: 1.2 + world.random() * 23.6, y: 1.2 + world.random() * 13.6, z: 0 };
|
|
11
|
+
}
|
|
12
|
+
return p;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default defineRules({
|
|
16
|
+
contract: 2, space: { dims: 3 }, move,
|
|
17
|
+
shapes: {
|
|
18
|
+
events: { hit: { from: f.vec3(), by: f.ref() }, take: { by: f.ref(), at: f.vec3() }, score: { value: f.u8() }, zone: {} },
|
|
19
|
+
effects: { wave: {}, knock: { dir: f.dir(), by: f.ref() } },
|
|
20
|
+
},
|
|
21
|
+
shared: { gemSeq: f.u32(), zone: f.vec3(), zoneN: f.u32(), zoneUntil: f.tick() },
|
|
22
|
+
entities: {
|
|
23
|
+
runner: {
|
|
24
|
+
player: { away: 'think', leave: 'bot' },
|
|
25
|
+
fields: { score: f.u16({ score: true }), pickups: f.u16(), target: f.vec3(), noticed: f.fix(), arrived: f.bit(), targetGem: f.ref() },
|
|
26
|
+
motion: { speed: f.fix({ init: 6.8 }), accel: f.fix({ init: 14 }), knockDir: f.dir(), knockFrom: f.vec3(), knockAt: f.fix(), knockUntil: f.fix() },
|
|
27
|
+
input: { ax: f.i8(), ay: f.i8(), wave: f.press() },
|
|
28
|
+
body: { shape: 'capsule', radius: 0.44, height: 1.2, maxSpeed: 6.8 },
|
|
29
|
+
tick(world, self) {
|
|
30
|
+
if (self.driver === 'person' && !self.away) { self.motion.speed = 6.8; self.motion.accel = 14; }
|
|
31
|
+
if (self.input.wave) {
|
|
32
|
+
world.emit('wave', self.id, {});
|
|
33
|
+
world.sendArea({ sphere: { at: self.pos, r: world.tune.knockRange } }, 'hit', { from: self.pos, by: self.id });
|
|
34
|
+
}
|
|
35
|
+
if (world.round.phase === 'live') for (const gem of world.near(self.pos, 0.7, 'gem')) world.send(gem.id, 'take', { by: self.id, at: self.pos });
|
|
36
|
+
},
|
|
37
|
+
think(world, self) {
|
|
38
|
+
const M = world.math;
|
|
39
|
+
self.motion.speed = 5; self.motion.accel = 5;
|
|
40
|
+
if (world.stage === 'dummy' || self.motion.knockAt > 0) return { ax: 0, ay: 0 };
|
|
41
|
+
const level = (world.kids ? Math.min(3, world.level) : world.level) - 1;
|
|
42
|
+
const reactions = [650, 420, 250, 170, 110];
|
|
43
|
+
const noise = [0.55, 0.3, 0.15, 0.07, 0.02];
|
|
44
|
+
const aggression = [0.1, 0.3, 0.5, 0.7, 0.9];
|
|
45
|
+
const positioning = [0.1, 0.35, 0.5, 0.75, 0.95];
|
|
46
|
+
if (self.noticed === 0 || world.tick - self.noticed >= reactions[level] / 1000 / world.dt) {
|
|
47
|
+
let best = self.pos, nearest = 10000;
|
|
48
|
+
self.targetGem = '';
|
|
49
|
+
const rivals = world.near(self.pos, 64, 'runner');
|
|
50
|
+
for (const gem of world.near(self.pos, 64, 'gem')) {
|
|
51
|
+
if (rivals.some(p => p.id !== self.id && p.noticed === world.tick && p.targetGem === gem.id)) continue;
|
|
52
|
+
const hot = M.dist(gem.pos, world.shared.zone) < 3;
|
|
53
|
+
const distance = M.dist(gem.pos, self.pos) * (hot ? 1.5 - positioning[level] : 1);
|
|
54
|
+
if (distance < nearest) { nearest = distance; best = gem.pos; self.targetGem = gem.id; }
|
|
55
|
+
}
|
|
56
|
+
self.target = { x: best.x + (world.random() - 0.5) * 4 * noise[level], y: best.y + (world.random() - 0.5) * 4 * noise[level], z: 0 };
|
|
57
|
+
self.noticed = world.tick; self.arrived = false;
|
|
58
|
+
}
|
|
59
|
+
const d = M.sub(self.target, self.pos), distance = M.len(d);
|
|
60
|
+
if (distance < 0.12 && !self.arrived) { self.arrived = true; self.noticed = world.tick; }
|
|
61
|
+
const v = distance < 0.12 ? { x: 0, y: 0, z: 0 } : M.norm(d);
|
|
62
|
+
let rival = false;
|
|
63
|
+
for (const p of world.near(self.pos, world.tune.knockRange, 'runner')) if (p.id !== self.id) rival = true;
|
|
64
|
+
return { ax: Math.round(v.x * 127), ay: Math.round(v.y * 127), wave: rival && world.round.phase === 'live' && world.random() < Math.min(world.kids ? 0.3 : 1, aggression[level]) * 0.8 * world.dt };
|
|
65
|
+
},
|
|
66
|
+
on: {
|
|
67
|
+
score(world, self, e) { self.score += e.value; self.pickups += 1; },
|
|
68
|
+
takeover(world, self) { self.motion.speed = 6.8; self.motion.accel = 14; self.motion.knockAt = 0; self.motion.knockUntil = 0; self.vel = { x: 0, y: 0, z: 0 }; },
|
|
69
|
+
arrive(world, self) {
|
|
70
|
+
if (world.stage === 'dummy') {
|
|
71
|
+
let index = 0;
|
|
72
|
+
for (const p of world.near(self.pos, 64, 'runner')) if (p.driver === 'bot' && p.seat < self.seat) index++;
|
|
73
|
+
world.place(self, self.driver === 'person' ? { x: 9.8, y: 8, z: 0 } : index === 0 ? { x: 11.64, y: 8, z: 0 } : { x: 22.4, y: 3 + (index - 1) * 10, z: 0 });
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
hit(world, self, e) {
|
|
77
|
+
if (e.by === self.id) return;
|
|
78
|
+
const M = world.math, m = self.motion, delta = M.sub(self.pos, e.from);
|
|
79
|
+
m.knockDir = M.len(delta) < 0.02 ? M.dir(world.random() * M.TAU) : M.norm(delta);
|
|
80
|
+
m.knockFrom = self.pos;
|
|
81
|
+
m.knockAt = world.tick + world.tune.hitStopMs / 1000 / world.dt;
|
|
82
|
+
m.knockUntil = m.knockAt + world.tune.knockMs / 1000 / world.dt;
|
|
83
|
+
world.emit('knock', self.id, { dir: m.knockDir, by: e.by });
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
onRoom: {
|
|
87
|
+
roundStart(world, self) {
|
|
88
|
+
self.score = 0; self.pickups = 0; self.noticed = 0;
|
|
89
|
+
self.motion.knockAt = 0; self.motion.knockUntil = 0;
|
|
90
|
+
const spots = world.map.spots('start');
|
|
91
|
+
if (world.stage !== 'dummy') world.place(self, spots[self.seat % spots.length]);
|
|
92
|
+
else {
|
|
93
|
+
let index = 0;
|
|
94
|
+
for (const p of world.near(self.pos, 64, 'runner')) if (p.driver === 'bot' && p.seat < self.seat) index++;
|
|
95
|
+
world.place(self, self.driver === 'person' ? { x: 9.8, y: 8, z: 0 } : index === 0 ? { x: 11.64, y: 8, z: 0 } : { x: 22.4, y: 3 + (index - 1) * 10, z: 0 });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
gem: {
|
|
101
|
+
fields: { takenAt: f.tick(), serial: f.u32() },
|
|
102
|
+
on: {
|
|
103
|
+
take(world, self, e) {
|
|
104
|
+
if (self.takenAt === world.tick || world.math.dist(e.at, self.pos) >= 0.7) return;
|
|
105
|
+
self.takenAt = world.tick;
|
|
106
|
+
world.send(e.by, 'score', { value: world.math.dist(self.pos, world.shared.zone) < 3 ? 2 : 1 });
|
|
107
|
+
world.place(self, gemPoint(world));
|
|
108
|
+
self.serial += 14;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
onRoom: { roundOver(world, self) { world.despawn(self); } },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
room: {
|
|
115
|
+
rounds: { seconds: 60, breakSeconds: 7 }, bots: { keep: 3 },
|
|
116
|
+
join(ctx, player) { const spots = ctx.map.spots('start'); return { kind: 'runner', at: spots[player.seat % spots.length] }; },
|
|
117
|
+
on: {
|
|
118
|
+
roundStart(world) {
|
|
119
|
+
for (let i = 0; i < 14; i++) { world.shared.gemSeq += 1; world.spawn('gem', gemPoint(world), { serial: world.shared.gemSeq }); }
|
|
120
|
+
world.shared.zone = { x: 4.4 + world.random() * 17.2, y: 4 + world.random() * 8, z: 0 };
|
|
121
|
+
world.shared.zoneN += 1; world.shared.zoneUntil = world.tick + world.ticks(12);
|
|
122
|
+
world.after(world.ticks(12), 'zone', {});
|
|
123
|
+
},
|
|
124
|
+
zone(world) {
|
|
125
|
+
if (world.round.phase !== 'live') return;
|
|
126
|
+
world.shared.zone = { x: 4.4 + world.random() * 17.2, y: 4 + world.random() * 8, z: 0 };
|
|
127
|
+
world.shared.zoneN += 1; world.shared.zoneUntil = world.tick + world.ticks(12);
|
|
128
|
+
world.after(world.ticks(12), 'zone', {});
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
map: './map',
|
|
133
|
+
});
|