@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,100 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"public": {
|
|
3
|
+
"knockRange": {
|
|
4
|
+
"value": 2.2,
|
|
5
|
+
"min": 1.2,
|
|
6
|
+
"max": 4,
|
|
7
|
+
"step": 0.02,
|
|
8
|
+
"unit": "m",
|
|
9
|
+
"group": "Knock",
|
|
10
|
+
"note": "How near a body must be for a wave to bump it"
|
|
11
|
+
},
|
|
12
|
+
"hitStopMs": {
|
|
13
|
+
"value": 70,
|
|
14
|
+
"min": 0,
|
|
15
|
+
"max": 200,
|
|
16
|
+
"step": 5,
|
|
17
|
+
"unit": "ms",
|
|
18
|
+
"group": "Knock",
|
|
19
|
+
"note": "The hit lands: the bumped body holds, squashed and shaking, before it flies"
|
|
20
|
+
},
|
|
21
|
+
"knockDistance": {
|
|
22
|
+
"value": 3.7,
|
|
23
|
+
"min": 0.8,
|
|
24
|
+
"max": 8,
|
|
25
|
+
"step": 0.1,
|
|
26
|
+
"unit": "m",
|
|
27
|
+
"group": "Knock",
|
|
28
|
+
"note": "How far a bump sends a body, the same at any frame rate"
|
|
29
|
+
},
|
|
30
|
+
"knockMs": {
|
|
31
|
+
"value": 420,
|
|
32
|
+
"min": 120,
|
|
33
|
+
"max": 1000,
|
|
34
|
+
"step": 10,
|
|
35
|
+
"unit": "ms",
|
|
36
|
+
"group": "Knock",
|
|
37
|
+
"note": "How long the slide takes"
|
|
38
|
+
},
|
|
39
|
+
"knockEase": {
|
|
40
|
+
"value": 4,
|
|
41
|
+
"min": 1,
|
|
42
|
+
"max": 6,
|
|
43
|
+
"step": 0.1,
|
|
44
|
+
"group": "Knock",
|
|
45
|
+
"note": "1 slides at one speed; higher leaves faster and eases into the stop"
|
|
46
|
+
},
|
|
47
|
+
"squash": {
|
|
48
|
+
"value": 0.35,
|
|
49
|
+
"min": 0,
|
|
50
|
+
"max": 0.8,
|
|
51
|
+
"step": 0.01,
|
|
52
|
+
"group": "Body",
|
|
53
|
+
"note": "How much a flying body stretches along the hit, and squashes when it stops"
|
|
54
|
+
},
|
|
55
|
+
"settleMs": {
|
|
56
|
+
"value": 260,
|
|
57
|
+
"min": 0,
|
|
58
|
+
"max": 700,
|
|
59
|
+
"step": 10,
|
|
60
|
+
"unit": "ms",
|
|
61
|
+
"group": "Body",
|
|
62
|
+
"note": "The wobble after the stop (overlap)"
|
|
63
|
+
},
|
|
64
|
+
"flashMs": {
|
|
65
|
+
"value": 110,
|
|
66
|
+
"min": 0,
|
|
67
|
+
"max": 300,
|
|
68
|
+
"step": 5,
|
|
69
|
+
"unit": "ms",
|
|
70
|
+
"group": "Hit",
|
|
71
|
+
"note": "How long the bumped body flashes white"
|
|
72
|
+
},
|
|
73
|
+
"shake": {
|
|
74
|
+
"value": 0.16,
|
|
75
|
+
"min": 0,
|
|
76
|
+
"max": 0.6,
|
|
77
|
+
"step": 0.01,
|
|
78
|
+
"unit": "m",
|
|
79
|
+
"group": "Hit",
|
|
80
|
+
"note": "The camera's kick when you bump or are bumped"
|
|
81
|
+
},
|
|
82
|
+
"sparks": {
|
|
83
|
+
"value": 9,
|
|
84
|
+
"min": 0,
|
|
85
|
+
"max": 24,
|
|
86
|
+
"step": 1,
|
|
87
|
+
"group": "Hit",
|
|
88
|
+
"note": "Sparks thrown off the hit"
|
|
89
|
+
},
|
|
90
|
+
"ringMs": {
|
|
91
|
+
"value": 900,
|
|
92
|
+
"min": 200,
|
|
93
|
+
"max": 2000,
|
|
94
|
+
"step": 10,
|
|
95
|
+
"unit": "ms",
|
|
96
|
+
"group": "Wave",
|
|
97
|
+
"note": "How long a wave's ring lasts"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
13
100
|
}
|
|
@@ -2,11 +2,29 @@
|
|
|
2
2
|
"id": "hero-rush-3d",
|
|
3
3
|
"name": "Hero Rush 3D",
|
|
4
4
|
"blurb": "Knights, mages and rogues race for the coins, jump each other's swings and knock the loot loose. Sixty-second rounds that never stop, in three.js with free CC0 animated characters.",
|
|
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 coins. Jump their swings. Knock the loot loose.",
|
|
22
|
+
"controls": {
|
|
23
|
+
"phone": "Drag anywhere to run; Jump and Swing at the bottom right.",
|
|
24
|
+
"computer": "WASD or arrows to run, Space to jump, F or a click to swing."
|
|
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,49 @@
|
|
|
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
|
+
const dt = ctx.dt, knocked = m.knockAt > 0;
|
|
17
|
+
const g0 = 2 * T.jumpHeight / (T.jumpRise * T.jumpRise);
|
|
18
|
+
let vz = body.vel.z, dz = 0;
|
|
19
|
+
if (input.jump && body.grounded && !knocked) { vz = g0 * T.jumpRise; m.jumpAt = ctx.tick; }
|
|
20
|
+
if (vz > 0 || !body.grounded) {
|
|
21
|
+
const gravity = g0 * (vz < 0 ? T.fallFaster : 1);
|
|
22
|
+
dz = vz * dt - gravity * dt * dt / 2;
|
|
23
|
+
vz -= gravity * dt;
|
|
24
|
+
if (Math.abs(vz) < 0.001) vz = 0;
|
|
25
|
+
}
|
|
26
|
+
let delta;
|
|
27
|
+
if (knocked) {
|
|
28
|
+
const last = ctx.tick + 1 >= m.knockUntil;
|
|
29
|
+
const u = M.clamp((ctx.tick - m.knockAt) / (m.knockUntil - m.knockAt), 0, 1);
|
|
30
|
+
const ease = last ? 1 : 1 - M.pow(1 - u, T.knockEase);
|
|
31
|
+
// A wall consumes blocked travel. Chasing the absolute endpoint stores up
|
|
32
|
+
// that travel and releases it as a burst when the body slides clear.
|
|
33
|
+
const previous = M.clamp((ctx.tick - 1 - m.knockAt) / (m.knockUntil - m.knockAt), 0, 1);
|
|
34
|
+
const before = 1 - M.pow(1 - previous, T.knockEase);
|
|
35
|
+
const travel = M.scale(m.knockDir, T.knockDistance * (ease - before));
|
|
36
|
+
delta = { x: travel.x, y: travel.y, z: dz };
|
|
37
|
+
if (last) m.knockAt = 0;
|
|
38
|
+
} else {
|
|
39
|
+
const want = M.scale(M.clampLen({ x: input.ax / 127, y: input.ay / 127, z: 0 }, 1), m.speed);
|
|
40
|
+
const keep = M.pow(1 - m.accel / 60, dt * 60);
|
|
41
|
+
const velocity = M.add(want, M.scale(M.sub({ x: body.vel.x, y: body.vel.y, z: 0 }, want), keep));
|
|
42
|
+
if (M.len(velocity) > 0.6 && !input.swing) body.heading = M.norm(velocity);
|
|
43
|
+
delta = { x: velocity.x * dt, y: velocity.y * dt, z: dz };
|
|
44
|
+
}
|
|
45
|
+
slide(body, delta, ctx);
|
|
46
|
+
body.vel = { x: knocked && m.knockAt === 0 ? 0 : delta.x / dt, y: knocked && m.knockAt === 0 ? 0 : delta.y / dt, z: body.grounded ? 0 : vz };
|
|
47
|
+
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { defineRules, f, type GameWorld } from '@homie-rocks/studio/rules';
|
|
2
|
+
import { move } from './move';
|
|
3
|
+
|
|
4
|
+
function flatDist(world: Pick<GameWorld, 'math'>, a: { x: number; y: number; z?: number }, b: { x: number; y: number; z?: number }) {
|
|
5
|
+
return world.math.len({ x: a.x - b.x, y: a.y - b.y, z: 0 });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function gemPoint(world: Pick<GameWorld, 'random' | 'map' | 'math'>) {
|
|
9
|
+
let p = { x: 1.2 + world.random() * 23.6, y: 1.2 + world.random() * 13.6, z: 0 };
|
|
10
|
+
for (let i = 0; i < 12; i++) {
|
|
11
|
+
let blocked = false;
|
|
12
|
+
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;
|
|
13
|
+
if (!blocked) break;
|
|
14
|
+
p = { x: 1.2 + world.random() * 23.6, y: 1.2 + world.random() * 13.6, z: 0 };
|
|
15
|
+
}
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineRules({
|
|
20
|
+
contract: 2, space: { dims: 3 }, move,
|
|
21
|
+
shapes: {
|
|
22
|
+
events: { hit: { from: f.vec3(), by: f.ref(), dir: f.dir() }, land: {}, notice: { by: f.ref() }, take: { by: f.ref(), at: f.vec3() }, score: { value: f.u8() }, zone: {} },
|
|
23
|
+
effects: { swing: { dir: f.dir() }, jump: {}, dodge: {}, knock: { dir: f.dir(), by: f.ref() } },
|
|
24
|
+
},
|
|
25
|
+
shared: { gemSeq: f.u32(), zone: f.vec3(), zoneN: f.u32(), zoneUntil: f.tick() },
|
|
26
|
+
entities: {
|
|
27
|
+
runner: {
|
|
28
|
+
player: { away: 'think', leave: 'bot' },
|
|
29
|
+
fields: { score: f.u16({ score: true }), pickups: f.u16(), target: f.vec3(), noticed: f.fix(), arrived: f.bit(), swingReady: f.tick(), dodgeAt: f.tick(), firstRound: f.u32(), soft: f.fix(), dial: f.u8({ init: 3 }), targetGem: f.ref() },
|
|
30
|
+
motion: { jumpAt: f.tick(), speed: f.fix({ init: 6.2 }), accel: f.fix({ init: 14 }), knockDir: f.dir(), knockFrom: f.vec3(), knockAt: f.fix(), knockUntil: f.fix() },
|
|
31
|
+
input: { ax: f.i8(), ay: f.i8(), jump: f.press(), swing: f.press() },
|
|
32
|
+
body: { shape: 'capsule', radius: 0.44, height: 1.7, maxSpeed: 6.2 },
|
|
33
|
+
tick(world, self) {
|
|
34
|
+
if (self.driver === 'person' && !self.away) { self.motion.speed = 6.2; self.motion.accel = 14; }
|
|
35
|
+
if (self.motion.jumpAt === world.tick) world.emit('jump', self.id, {});
|
|
36
|
+
if (self.input.swing && world.tick >= self.swingReady && self.motion.knockAt === 0) {
|
|
37
|
+
self.swingReady = world.tick + world.ticks((world.tune.windupMs + world.tune.swingRestMs) / 1000);
|
|
38
|
+
world.emit('swing', self.id, { dir: self.heading });
|
|
39
|
+
world.after(Math.max(1, world.ticks(world.tune.windupMs / 1000) - 1), 'land', {});
|
|
40
|
+
world.sendArea({ sphere: { at: self.pos, r: world.tune.knockRange + 0.6 } }, 'notice', { by: self.id });
|
|
41
|
+
}
|
|
42
|
+
if (world.round.phase === 'live') for (const gem of world.near(self.pos, 4, 'gem'))
|
|
43
|
+
if (flatDist(world, self.pos, gem.pos) < 0.74) world.send(gem.id, 'take', { by: self.id, at: self.pos });
|
|
44
|
+
},
|
|
45
|
+
think(world, self) {
|
|
46
|
+
const M = world.math, knocked = self.motion.knockAt > 0;
|
|
47
|
+
const jump = self.dodgeAt > 0 && world.tick >= self.dodgeAt && self.grounded && !knocked;
|
|
48
|
+
if (self.dodgeAt > 0 && world.tick >= self.dodgeAt) self.dodgeAt = 0;
|
|
49
|
+
self.motion.accel = 5;
|
|
50
|
+
if ((world.stage === 'dummy' || world.stage === 'jump') || knocked) { self.motion.speed = 0; return { ax: 0, ay: 0 }; }
|
|
51
|
+
const folk = world.near(self.pos, 64, 'runner').filter(p => p.driver === 'person');
|
|
52
|
+
const easy = folk.some(p => p.firstRound >= world.round.n);
|
|
53
|
+
const level = Math.max(1, (world.kids ? Math.min(3, world.level) : world.level) - (easy && !world.levelSet ? 1 : 0));
|
|
54
|
+
self.dial = level;
|
|
55
|
+
const reactions = [650, 420, 250, 170, 110], noise = [0.55, 0.3, 0.15, 0.07, 0.02];
|
|
56
|
+
const positioning = [0.1, 0.35, 0.5, 0.75, 0.95], aggression = [0.1, 0.3, 0.5, 0.7, 0.9];
|
|
57
|
+
const ix = level - 1;
|
|
58
|
+
let best = -1, near = folk[0], nearDist = 10000;
|
|
59
|
+
for (const person of folk) {
|
|
60
|
+
best = Math.max(best, person.score);
|
|
61
|
+
const distance = flatDist(world, person.pos, self.pos);
|
|
62
|
+
if (distance < nearDist) { near = person; nearDist = distance; }
|
|
63
|
+
}
|
|
64
|
+
const soft = level === 5 || best < 0 ? 0 : M.clamp((self.score - best - [-1, 1, 2, 5, 0][ix]) / 2, 0, 1);
|
|
65
|
+
self.soft = soft;
|
|
66
|
+
const gems = world.near(self.pos, 64, 'gem');
|
|
67
|
+
if (self.noticed === 0 || world.tick - self.noticed >= reactions[ix] / 1000 / world.dt * (soft >= 1 ? 1 : 1 + 2.5 * soft)) {
|
|
68
|
+
let target = self.target;
|
|
69
|
+
self.targetGem = '';
|
|
70
|
+
if (near && soft >= 1) {
|
|
71
|
+
for (let i = 0; i < 6; i++) {
|
|
72
|
+
const a = M.clamp(M.atan2(Math.min(self.pos.y - near.pos.y, -0.6), self.pos.x - near.pos.x) + (world.random() - 0.5) * (1.2 + i * 0.5), -M.PI + 0.7, -0.7);
|
|
73
|
+
const distance = 2.2 + world.random() * 0.8;
|
|
74
|
+
target = { x: M.clamp(near.pos.x + M.cos(a) * distance, 0.44, 25.56), y: M.clamp(near.pos.y + M.sin(a) * distance, 0.44, 15.56), z: 0 };
|
|
75
|
+
const dx = target.x - self.pos.x, dy = target.y - self.pos.y, len2 = dx * dx + dy * dy || 1;
|
|
76
|
+
let clear = true;
|
|
77
|
+
for (const gem of gems) {
|
|
78
|
+
const u = M.clamp(((gem.pos.x - self.pos.x) * dx + (gem.pos.y - self.pos.y) * dy) / len2, 0, 1);
|
|
79
|
+
if (flatDist(world, { x: self.pos.x + dx * u, y: self.pos.y + dy * u, z: 0 }, gem.pos) < 0.89) clear = false;
|
|
80
|
+
}
|
|
81
|
+
if (clear) break;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
let distance = 10000;
|
|
85
|
+
const rivals = world.near(self.pos, 64, 'runner');
|
|
86
|
+
for (const gem of gems) {
|
|
87
|
+
if (rivals.some(p => p.id !== self.id && p.noticed === world.tick && p.targetGem === gem.id)) continue;
|
|
88
|
+
const hot = flatDist(world, gem.pos, world.shared.zone) < 3;
|
|
89
|
+
const d = flatDist(world, gem.pos, self.pos) * (hot ? 1.5 - positioning[ix] : 1) + (near ? 2 * soft * flatDist(world, gem.pos, near.pos) : 0);
|
|
90
|
+
if (d < distance) { distance = d; target = gem.pos; self.targetGem = gem.id; }
|
|
91
|
+
}
|
|
92
|
+
if (self.targetGem) target = { x: target.x + (world.random() - 0.5) * 4 * noise[ix], y: target.y + (world.random() - 0.5) * 4 * noise[ix], z: 0 };
|
|
93
|
+
}
|
|
94
|
+
self.target = target; self.noticed = world.tick; self.arrived = false;
|
|
95
|
+
}
|
|
96
|
+
let v = M.sub(self.target, { x: self.pos.x, y: self.pos.y, z: 0 });
|
|
97
|
+
const distance = M.len(v);
|
|
98
|
+
if (distance < 0.12 && !self.arrived) { self.arrived = true; self.noticed = world.tick; }
|
|
99
|
+
self.motion.speed = distance < 0.12 ? 0 : 4.2 * (soft >= 1 && nearDist > 3.5 ? 1 : 1 - 0.5 * soft);
|
|
100
|
+
v = distance < 0.12 ? { x: 0, y: 0, z: 0 } : M.norm(v);
|
|
101
|
+
if (soft >= 1 && near) {
|
|
102
|
+
const look = Math.max(0.25, self.motion.speed * world.dt * 4);
|
|
103
|
+
let chosen = false;
|
|
104
|
+
for (const angle of [0, 1.05, -1.05, 2.1, -2.1]) {
|
|
105
|
+
const step = { x: v.x * M.cos(angle) - v.y * M.sin(angle), y: v.x * M.sin(angle) + v.y * M.cos(angle), z: 0 };
|
|
106
|
+
const p = M.add(self.pos, M.scale(step, look));
|
|
107
|
+
if (!gems.some(g => flatDist(world, p, g.pos) < 0.8 && flatDist(world, p, g.pos) < flatDist(world, self.pos, g.pos))) { v = step; chosen = true; break; }
|
|
108
|
+
}
|
|
109
|
+
if (!chosen) { v = { x: 0, y: 0, z: 0 }; self.vel = { x: 0, y: 0, z: self.vel.z }; }
|
|
110
|
+
}
|
|
111
|
+
let rival = null, rivalDist = world.tune.knockRange * 0.9;
|
|
112
|
+
for (const p of world.near(self.pos, 4, 'runner')) {
|
|
113
|
+
const d = flatDist(world, self.pos, p.pos);
|
|
114
|
+
if (p.id !== self.id && d < rivalDist) { rival = p; rivalDist = d; }
|
|
115
|
+
}
|
|
116
|
+
const attack = rival !== null && world.round.phase === 'live' && world.random() < (world.kids ? Math.min(0.3, aggression[ix]) : aggression[ix]) * (1 - 0.75 * soft) * 0.8 * world.dt;
|
|
117
|
+
if (attack && rival) self.heading = M.norm({ x: rival.pos.x - self.pos.x, y: rival.pos.y - self.pos.y, z: 0 });
|
|
118
|
+
return { ax: Math.round(v.x * 127), ay: Math.round(v.y * 127), jump, swing: attack };
|
|
119
|
+
},
|
|
120
|
+
on: {
|
|
121
|
+
land(world, self) {
|
|
122
|
+
if (self.motion.knockAt > 0) return;
|
|
123
|
+
world.sendArea({ box: { min: { x: self.pos.x - world.tune.knockRange, y: self.pos.y - world.tune.knockRange, z: 0 }, max: { x: self.pos.x + world.tune.knockRange, y: self.pos.y + world.tune.knockRange, z: 8 } } }, 'hit', { from: self.pos, by: self.id, dir: self.heading });
|
|
124
|
+
},
|
|
125
|
+
notice(world, self, e) {
|
|
126
|
+
if (e.by === self.id || self.driver !== 'bot' || (world.stage === 'dummy' || world.stage === 'jump')) return;
|
|
127
|
+
const ix = self.dial - 1, noise = [0.55, 0.3, 0.15, 0.07, 0.02], reaction = [650, 420, 250, 170, 110];
|
|
128
|
+
if (world.random() < (0.15 + 0.6 * (1 - noise[ix])) * (1 - 0.5 * self.soft)) self.dodgeAt = world.tick + world.ticks(Math.max(40, reaction[ix] * 0.5 - 60) / 1000);
|
|
129
|
+
},
|
|
130
|
+
score(world, self, e) { self.score += e.value; self.pickups += 1; },
|
|
131
|
+
takeover(world, self) { self.firstRound = world.round.n + (world.round.endsAt - world.tick < world.ticks(30) ? 1 : 0); self.motion.speed = 6.2; self.motion.accel = 14; self.motion.knockAt = 0; self.motion.knockUntil = 0; self.vel = { x: 0, y: 0, z: 0 }; },
|
|
132
|
+
arrive(world, self) {
|
|
133
|
+
self.firstRound = world.round.n + (world.round.endsAt - world.tick < world.ticks(30) ? 1 : 0);
|
|
134
|
+
if ((world.stage === 'dummy' || world.stage === 'jump')) {
|
|
135
|
+
let index = 0;
|
|
136
|
+
for (const p of world.near(self.pos, 64, 'runner')) if (p.driver === 'bot' && p.seat < self.seat) index++;
|
|
137
|
+
world.place(self, self.driver === 'person' ? { x: world.stage === 'jump' ? 13 : 9.8, y: 8, z: 0 } : index === 0 && world.stage !== 'jump' ? { x: 11.3, y: 8, z: 0 } : { x: 22.4, y: 3 + index * 5, z: 0 }, { heading: { x: self.driver === 'person' ? 1 : -1, y: 0, z: 0 } });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
hit(world, self, e) {
|
|
141
|
+
if (e.by === self.id) return;
|
|
142
|
+
const M = world.math, m = self.motion, delta = { x: self.pos.x - e.from.x, y: self.pos.y - e.from.y, z: 0 };
|
|
143
|
+
const distance = M.len(delta);
|
|
144
|
+
if (distance > world.tune.knockRange || distance > 0.66 && M.dot(M.norm(delta), e.dir) < M.cos(world.tune.swingArc * M.PI / 360)) return;
|
|
145
|
+
if (self.pos.z >= 0.3) { world.emit('dodge', self.id, {}); return; }
|
|
146
|
+
m.knockDir = M.len(delta) < 0.02 ? M.dir(world.random() * M.TAU) : M.norm(delta);
|
|
147
|
+
m.knockFrom = self.pos;
|
|
148
|
+
m.knockAt = world.tick + world.tune.hitStopMs / 1000 / world.dt;
|
|
149
|
+
m.knockUntil = m.knockAt + world.tune.knockMs / 1000 / world.dt;
|
|
150
|
+
world.emit('knock', self.id, { dir: m.knockDir, by: e.by });
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
onRoom: {
|
|
154
|
+
roundStart(world, self) {
|
|
155
|
+
self.score = 0; self.pickups = 0; self.noticed = 0; self.swingReady = 0; self.dodgeAt = 0;
|
|
156
|
+
self.motion.knockAt = 0; self.motion.knockUntil = 0;
|
|
157
|
+
const spots = world.map.spots('start');
|
|
158
|
+
if (world.stage !== 'dummy' && world.stage !== 'jump') world.place(self, spots[self.seat % spots.length]);
|
|
159
|
+
else {
|
|
160
|
+
let index = 0;
|
|
161
|
+
for (const p of world.near(self.pos, 64, 'runner')) if (p.driver === 'bot' && p.seat < self.seat) index++;
|
|
162
|
+
world.place(self, self.driver === 'person' ? { x: world.stage === 'jump' ? 13 : 9.8, y: 8, z: 0 } : index === 0 && world.stage !== 'jump' ? { x: 11.3, y: 8, z: 0 } : { x: 22.4, y: 3 + index * 5, z: 0 }, { heading: { x: self.driver === 'person' ? 1 : -1, y: 0, z: 0 } });
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
gem: {
|
|
168
|
+
fields: { takenAt: f.tick(), serial: f.u32() },
|
|
169
|
+
on: {
|
|
170
|
+
take(world, self, e) {
|
|
171
|
+
if (self.takenAt === world.tick || flatDist(world, e.at, self.pos) >= 0.74) return;
|
|
172
|
+
self.takenAt = world.tick;
|
|
173
|
+
world.send(e.by, 'score', { value: flatDist(world, self.pos, world.shared.zone) < 3 ? 2 : 1 });
|
|
174
|
+
world.place(self, gemPoint(world));
|
|
175
|
+
self.serial += 14;
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
onRoom: { roundOver(world, self) { world.despawn(self); } },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
room: {
|
|
182
|
+
rounds: { seconds: 60, breakSeconds: 7 }, bots: { keep: 3 },
|
|
183
|
+
join(ctx, player) { const spots = ctx.map.spots('start'); return { kind: 'runner', at: spots[player.seat % spots.length] }; },
|
|
184
|
+
on: {
|
|
185
|
+
roundStart(world) {
|
|
186
|
+
for (let i = 0; i < 14; i++) { world.shared.gemSeq += 1; world.spawn('gem', gemPoint(world), { serial: world.shared.gemSeq }); }
|
|
187
|
+
world.shared.zone = { x: 4.4 + world.random() * 17.2, y: 4 + world.random() * 8, z: 0 };
|
|
188
|
+
world.shared.zoneN += 1; world.shared.zoneUntil = world.tick + world.ticks(12);
|
|
189
|
+
world.after(world.ticks(12), 'zone', {});
|
|
190
|
+
},
|
|
191
|
+
zone(world) {
|
|
192
|
+
if (world.round.phase !== 'live') return;
|
|
193
|
+
world.shared.zone = { x: 4.4 + world.random() * 17.2, y: 4 + world.random() * 8, z: 0 };
|
|
194
|
+
world.shared.zoneN += 1; world.shared.zoneUntil = world.tick + world.ticks(12);
|
|
195
|
+
world.after(world.ticks(12), 'zone', {});
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
map: './map',
|
|
200
|
+
});
|