@homie-rocks/studio 0.39.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 +28 -1
- package/lib/build.mjs +1 -0
- package/lib/port-check.mjs +6 -3
- package/lib/rules-build.mjs +8 -1
- package/lib/rules-check-worker.mjs +3 -3
- package/lib/rules-check.mjs +3 -1
- package/lib/template-history.json +41 -0
- package/netplay/NETPLAY.md +74 -2
- package/package.json +1 -1
- 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/worker/version.mjs +1 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { defineRules, f, type GameWorld, type GameSelf } from '@homie-rocks/studio/rules';
|
|
2
|
+
import { move } from './move';
|
|
3
|
+
|
|
4
|
+
type Hero = GameSelf<'hero'>;
|
|
5
|
+
type ReadWorld = Pick<GameWorld, 'near' | 'math' | 'map' | 'tick' | 'dt' | 'shared'>;
|
|
6
|
+
function slimes(world: ReadWorld) { return world.near({ x: 16, y: 10, z: 0 }, 64, 'slime'); }
|
|
7
|
+
function people(world: ReadWorld) { return world.near({ x: 16, y: 10, z: 0 }, 64, 'hero').filter(p => p.driver === 'person' && p.hp > 0); }
|
|
8
|
+
function nearest(world: ReadWorld, at: { x: number; y: number; z: number }, list = slimes(world)) {
|
|
9
|
+
let best = list[0], distance = 10000;
|
|
10
|
+
for (const s of list) { const d = world.math.dist(at, s.pos); if (d < distance) { distance = d; best = s; } }
|
|
11
|
+
return best;
|
|
12
|
+
}
|
|
13
|
+
function quests(world: ReadWorld) {
|
|
14
|
+
const list = slimes(world);
|
|
15
|
+
return ['slime-hunt', ...(list.some(s => s.size === 3) || (world.shared.kingAt - world.tick) * world.dt < 30 ? ['king-slime'] : []), ...(list.some(s => s.size === 2) ? ['big-slime'] : [])];
|
|
16
|
+
}
|
|
17
|
+
function skill(world: GameWorld, guide = false) {
|
|
18
|
+
const n = (world.kids ? Math.min(3, guide ? world.guideLevel : world.level) : guide ? world.guideLevel : world.level) - 1;
|
|
19
|
+
return { reaction: [650, 420, 250, 170, 110][n], noise: [0.55, 0.3, 0.15, 0.07, 0.02][n], positioning: [0.1, 0.35, 0.5, 0.75, 0.95][n], aggression: Math.min(world.kids ? 0.3 : 1, [0.1, 0.3, 0.5, 0.7, 0.9][n]) };
|
|
20
|
+
}
|
|
21
|
+
function reset(world: GameWorld, self: Hero) {
|
|
22
|
+
self.hp = self.maxHp; self.motion.downUntil = 0; self.motion.ready = 0; self.motion.attackTick = 0; self.motion.lungeDone = 1;
|
|
23
|
+
const spots = world.map.spots('start');
|
|
24
|
+
if (world.stage === 'dummy') world.place(self, self.driver === 'person' ? { x: 14.6, y: 10, z: 0 } : { x: self.seat % 2 ? 4 : 28, y: 3.2 + Math.floor(self.seat / 2) * 4, z: 0 }, { heading: { x: 1, y: 0, z: 0 } });
|
|
25
|
+
else world.place(self, spots[self.seat % spots.length]);
|
|
26
|
+
self.target = self.pos;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default defineRules({
|
|
30
|
+
contract: 2, space: { dims: 2 }, move, map: './map',
|
|
31
|
+
shapes: {
|
|
32
|
+
events: { beat: {}, director: {}, kingSlain: {}, hit: { by: f.ref(), from: f.vec3(), damage: f.u16() }, reward: { xp: f.u16(), gold: f.u16() }, bite: { damage: f.u16(), size: f.u8() } },
|
|
33
|
+
effects: { loot: { xp: f.u16(), gold: f.u16() }, down: { cause: f.text(24) }, king: {}, strike: {}, director: { tactic: f.text(12), pressure: f.u8(), by: f.text(8) } },
|
|
34
|
+
commands: { hero: { level: f.u8(), name: f.text(20) } },
|
|
35
|
+
view: {
|
|
36
|
+
me: f.struct({ hp: f.u8(), down: f.bit() }), zone: f.text(20), danger: f.text(20),
|
|
37
|
+
party: f.list(f.struct({ seat: f.u8(), dist: f.u16(), hp: f.u8(), down: f.bit() }), 6),
|
|
38
|
+
quests: f.list(f.text(16), 3), slimes: f.struct({ near: f.u8(), king: f.bit() }), round: f.struct({ phase: f.text(8), left: f.u16() }),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
shared: { serial: f.u32(), spawnAt: f.tick(), kingAt: f.tick(), kingSlainAt: f.tick(), tactic: f.text(12, { init: 'chase' }), pressure: f.u8({ init: 2 }), waveLeft: f.u8(), directorBy: f.text(8, { init: 'floor' }), quests: f.list(f.text(16), 3) },
|
|
42
|
+
asks: {
|
|
43
|
+
director: {
|
|
44
|
+
state: {
|
|
45
|
+
heroes: f.list(f.struct({ hp: f.u8(), down: f.bit(), level: f.u8(), nearKing: f.u16() }), 32),
|
|
46
|
+
slimes: f.struct({ count: f.u8(), big: f.u8(), king: f.u8() }),
|
|
47
|
+
night: f.struct({ secondsLeft: f.u16() }), now: f.struct({ tactic: f.text(12), pressure: f.u8() }), kids: f.bit(),
|
|
48
|
+
},
|
|
49
|
+
questions: {
|
|
50
|
+
tactic: { type: 'choice', instructions: 'How should the slimes hunt? Hurt heroes deserve a breather.', criteria: { chase: 'Rush the nearest hero', surround: 'Spread out and close in', weakest: 'Hunt the most hurt hero', regroup: 'Gather at the King' } },
|
|
51
|
+
wave: { type: 'noul', instructions: 'Should three extra slimes come?', criteria: { true: 'Healthy heroes and quiet woods', false: 'Hurt, down or few heroes' } },
|
|
52
|
+
pressure: { type: 'score', instructions: 'How hard should the vale push?', criteria: ['A breather', 'Easy', 'Steady', 'Hard', 'Fierce'] },
|
|
53
|
+
},
|
|
54
|
+
floor() { return { tactic: 'chase', wave: false, pressure: 2 }; },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
entities: {
|
|
58
|
+
hero: {
|
|
59
|
+
player: { away: 'think', leave: 'bot' }, body: { shape: 'circle', radius: 0.44, maxSpeed: 6 },
|
|
60
|
+
fields: { striking: f.bit(), hp: f.u16({ init: 72 }), maxHp: f.u16({ init: 72 }), level: f.u8({ init: 1 }), name: f.text(20), score: f.u32({ score: true }), target: f.vec3(), aim: f.vec3(), hunting: f.bit(), noticed: f.tick(), greeted: f.list(f.u8(), 32), warned: f.tick(), huntFrom: f.u32(), huntGoal: f.tick(), arrivedAt: f.tick(), leadGoal: f.tick() },
|
|
61
|
+
motion: { lunges: f.bit({ init: true }), speed: f.fix({ init: 6 }), downUntil: f.tick(), ready: f.tick(), attackTick: f.tick(), lungeDir: f.dir(), lungeDone: f.fix({ init: 1 }), live: f.bit({ init: true }) },
|
|
62
|
+
input: { ax: f.i8(), ay: f.i8(), strike: f.press() },
|
|
63
|
+
commands: {
|
|
64
|
+
hero(world, self, e) { if (self.driver !== 'person') return; self.level = world.math.clamp(e.level, 1, 99); self.maxHp = 60 + self.level * 12; self.hp = Math.min(self.hp, self.maxHp); self.name = world.kids ? '' : e.name; },
|
|
65
|
+
},
|
|
66
|
+
tick(world, self) {
|
|
67
|
+
self.motion.live = world.round.phase === 'live';
|
|
68
|
+
self.striking = self.motion.attackTick > 0 && world.tick - self.motion.attackTick < world.tune.flashMs / 1000 / world.dt;
|
|
69
|
+
if (world.kids && self.name) self.name = '';
|
|
70
|
+
if (self.driver === 'person' && !self.away) { self.motion.speed = 6; self.motion.lunges = true; }
|
|
71
|
+
if (self.hp === 0) {
|
|
72
|
+
if (world.tick >= self.motion.downUntil) { self.hp = self.maxHp; world.place(self, { x: 16 + (world.random() - 0.5) * 6, y: 10 + (world.random() - 0.5) * 4, z: 0 }); }
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (self.motion.attackTick === world.tick && world.round.phase === 'live') {
|
|
76
|
+
world.emit('strike', self.id, {});
|
|
77
|
+
for (const s of world.near(self.pos, world.tune.strikeRange + 0.6, 'slime'))
|
|
78
|
+
if (world.math.dist(self.pos, s.pos) <= world.tune.strikeRange + s.size * 0.2) world.send(s.id, 'hit', { by: self.id, from: self.pos, damage: 10 + self.level * 3 });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
think(world, self) {
|
|
82
|
+
self.motion.lunges = false;
|
|
83
|
+
const M = world.math, s = skill(world, world.guideSeats.includes(self.seat)), goal = self.goal, folk = people(world);
|
|
84
|
+
if (self.hp === 0 || world.stage === 'dummy') { self.motion.speed = 0; return { ax: 0, ay: 0 }; }
|
|
85
|
+
let target = self.target, hunt = undefined as ReturnType<typeof nearest> | undefined;
|
|
86
|
+
if (goal) {
|
|
87
|
+
const nearTo = (p: { x: number; y: number; z: number }, r: number) => { const n = nearest(world, p); return n && M.dist(n.pos, p) < r ? n : undefined; };
|
|
88
|
+
if (goal.goal === 'follow') {
|
|
89
|
+
const person = folk.find(p => p.seat === goal.args.seat);
|
|
90
|
+
if (!person) world.goalDone(false);
|
|
91
|
+
else {
|
|
92
|
+
const back = (70 + (150 - 70) * (1 - s.positioning)) / 50;
|
|
93
|
+
const side = (self.seat % 2 ? 1 : -1) * (0.5 + self.seat % 3 * 0.25), a = M.atan2(person.heading.y, person.heading.x) + side;
|
|
94
|
+
target = { x: person.pos.x - M.cos(a) * back, y: person.pos.y - M.sin(a) * back, z: 0 }; hunt = nearTo(person.pos, 4);
|
|
95
|
+
}
|
|
96
|
+
} else if (goal.goal === 'quest') {
|
|
97
|
+
if (goal.args.quest === 'king-slime') {
|
|
98
|
+
if (world.shared.kingSlainAt > goal.at) world.goalDone();
|
|
99
|
+
hunt = slimes(world).find(s => s.size === 3); target = { x: 16, y: 3.8, z: 0 };
|
|
100
|
+
} else if (goal.args.quest === 'big-slime') { hunt = nearest(world, self.pos, slimes(world).filter(s => s.size === 2)); if (!hunt) world.goalDone(); }
|
|
101
|
+
else {
|
|
102
|
+
if (self.huntGoal !== goal.at || self.huntFrom > self.score) { self.huntGoal = goal.at; self.huntFrom = self.score; }
|
|
103
|
+
if (self.score - self.huntFrom >= 18) world.goalDone();
|
|
104
|
+
const centre = folk.length ? M.scale(folk.reduce((p, h) => M.add(p, h.pos), { x: 0, y: 0, z: 0 }), 1 / folk.length) : self.pos;
|
|
105
|
+
hunt = nearTo(centre, 10.4) ?? nearest(world, self.pos);
|
|
106
|
+
}
|
|
107
|
+
} else if (goal.goal === 'lead') {
|
|
108
|
+
target = world.map.spot(goal.args.place) ?? world.map.spot('camp')!;
|
|
109
|
+
if (self.leadGoal !== goal.at) { self.leadGoal = goal.at; self.arrivedAt = 0; }
|
|
110
|
+
if (M.dist(target, self.pos) < 1.2) {
|
|
111
|
+
if (!self.arrivedAt) self.arrivedAt = world.tick;
|
|
112
|
+
if (folk.some(p => M.dist(p.pos, target) < 5.6)) world.goalDone();
|
|
113
|
+
else if (world.tick - self.arrivedAt > world.ticks(20)) world.goalDone(false);
|
|
114
|
+
} else self.arrivedAt = 0;
|
|
115
|
+
hunt = nearTo(self.pos, 2.2);
|
|
116
|
+
} else if (goal.goal === 'guard') {
|
|
117
|
+
const near = folk.filter(p => M.dist(p.pos, self.pos) < 14);
|
|
118
|
+
const centre = near.length ? M.scale(near.reduce((p, h) => M.add(p, h.pos), { x: 0, y: 0, z: 0 }), 1 / near.length) : self.pos;
|
|
119
|
+
target = M.add(centre, { x: 1, y: 0.8, z: 0 }); hunt = nearTo(centre, (260 + (160 - 260) * (1 - s.positioning)) / 50);
|
|
120
|
+
} else if (goal.goal === 'back') { target = world.map.spot('camp')!; if (M.dist(target, self.pos) < 1) world.goalDone(); }
|
|
121
|
+
if (!hunt && goal.goal !== 'follow') target = M.add(target, M.scale(M.dir(self.seat * 2.4), 0.92));
|
|
122
|
+
if (hunt) {
|
|
123
|
+
if (!self.noticed || world.tick - self.noticed >= s.reaction / 1000 / world.dt) { self.noticed = world.tick; self.aim = { x: hunt.pos.x + (world.random() - 0.5) * 1.6 * s.noise, y: hunt.pos.y + (world.random() - 0.5) * 1.6 * s.noise, z: 0 }; }
|
|
124
|
+
target = self.aim;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
hunt = nearest(world, self.pos);
|
|
128
|
+
if (!self.noticed || world.tick - self.noticed >= s.reaction / 1000 / world.dt) {
|
|
129
|
+
self.noticed = world.tick; self.hunting = Boolean(hunt && M.dist(hunt.pos, self.pos) < (460 + (220 - 460) * (1 - s.positioning)) / 50);
|
|
130
|
+
if (self.hunting && hunt) self.aim = { x: hunt.pos.x + (world.random() - 0.5) * 1.6 * s.noise, y: hunt.pos.y + (world.random() - 0.5) * 1.6 * s.noise, z: 0 };
|
|
131
|
+
}
|
|
132
|
+
target = self.hunting ? self.aim : self.target;
|
|
133
|
+
if (!self.hunting && M.dist(self.target, self.pos) < 0.6) self.target = { x: 4 + world.random() * 24, y: 3 + world.random() * 14, z: 0 };
|
|
134
|
+
if (!self.hunting) hunt = undefined;
|
|
135
|
+
}
|
|
136
|
+
const delta = M.sub(target, self.pos), distance = M.len(delta), stop = goal ? hunt ? 1 : 0.48 : 1.2;
|
|
137
|
+
self.motion.speed = distance > stop ? Math.min(goal ? 5.1 : 4.5, distance / world.dt) : 0;
|
|
138
|
+
const v = distance > stop ? M.norm(delta) : { x: 0, y: 0, z: 0 };
|
|
139
|
+
const strike = Boolean(hunt && M.dist(goal ? hunt.pos : self.aim, self.pos) < world.tune.strikeRange + (goal ? hunt.size * 0.2 : 0) && world.tick - self.motion.attackTick > (1400 - 900 * s.aggression) / 1000 / world.dt);
|
|
140
|
+
return { ax: Math.round(v.x * 127), ay: Math.round(v.y * 127), strike };
|
|
141
|
+
},
|
|
142
|
+
on: {
|
|
143
|
+
arrive(world, self) { if (self.driver !== 'person') { self.level = 1 + self.seat % 4; self.maxHp = 60 + self.level * 12; } reset(world, self); },
|
|
144
|
+
takeover(world, self) { self.score = 0; self.name = ''; self.level = 1; self.maxHp = 72; self.hp = 72; self.motion.downUntil = 0; self.motion.speed = 6; },
|
|
145
|
+
reward(world, self, e) { self.score += e.xp; if (self.driver === 'person') world.emit('loot', self.id, { xp: e.xp, gold: e.gold }); },
|
|
146
|
+
bite(world, self, e) {
|
|
147
|
+
if (self.hp === 0) return; self.hp = Math.max(0, self.hp - e.damage);
|
|
148
|
+
if (self.hp === 0) { self.motion.downUntil = world.tick + world.ticks(4); self.motion.lungeDone = 1; world.emit('down', self.id, { cause: e.size === 3 ? 'the King Slime' : e.size === 2 ? 'a big slime' : 'a slime' }); }
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
onRoom: { roundStart(world, self) { self.score = 0; reset(world, self); } },
|
|
152
|
+
guide: {
|
|
153
|
+
view(world, self) {
|
|
154
|
+
const list = slimes(world), king = list.find(s => s.size === 3), near = list.filter(s => world.math.dist(self.pos, s.pos) < 7).length;
|
|
155
|
+
const party = world.near(self.pos, 18, 'hero').filter(p => p.driver === 'person').map(p => ({ seat: p.seat, dist: Math.round(world.math.dist(self.pos, p.pos) * 50), hp: Math.round(100 * p.hp / p.maxHp), down: p.hp === 0 })).sort((a, b) => a.dist - b.dist).slice(0, 6);
|
|
156
|
+
let zone = 'vale', distance = 5.2;
|
|
157
|
+
for (const name of ['camp', 'king', 'east-woods']) { const at = world.map.spot(name)!; const d = world.math.dist(self.pos, at); if (d < distance) { zone = name; distance = d; } }
|
|
158
|
+
return { me: { hp: Math.round(100 * self.hp / self.maxHp), down: self.hp === 0 }, zone, danger: king && world.math.dist(king.pos, self.pos) < 9 ? 'the King Slime' : near >= 5 ? 'slimes' : '', party, quests: quests(world), slimes: { near, king: Boolean(king) }, round: { phase: world.round.phase, left: Math.max(0, Math.round((world.round.endsAt - world.tick) * world.dt)) } };
|
|
159
|
+
},
|
|
160
|
+
floor(world, self, v) {
|
|
161
|
+
const live = world.near(self.pos, 64, 'hero').filter(p => p.driver === 'person');
|
|
162
|
+
const party = v.party.filter(p => live.some(h => h.seat === p.seat));
|
|
163
|
+
const ask = v.asks[0];
|
|
164
|
+
if (ask?.k === 'ask_help') return { goal: 'quest', args: { quest: ask.args.quest }, say: 'quest_help', sayArgs: { quest: ask.args.quest } };
|
|
165
|
+
if (ask?.k === 'lead_me') return { goal: 'lead', args: { place: ask.args.place }, say: 'follow_me', sayArgs: { place: ask.args.place } };
|
|
166
|
+
if (ask?.k === 'no_thanks') return { goal: 'back', say: 'bye' };
|
|
167
|
+
if (v.goal?.state === 'active' && ['quest', 'lead', 'back'].includes(v.goal.goal)) return {};
|
|
168
|
+
const fresh = party.find(p => !self.greeted.includes(p.seat) && p.dist < 420);
|
|
169
|
+
if (fresh) { self.greeted = [...self.greeted, fresh.seat].slice(-32); return { goal: 'follow', args: { seat: fresh.seat }, say: 'hello', sayArgs: { player: fresh.seat } }; }
|
|
170
|
+
if (v.danger === 'the King Slime') {
|
|
171
|
+
const warn = world.tick - self.warned > world.ticks(30); if (warn) self.warned = world.tick;
|
|
172
|
+
return warn ? { goal: 'guard', say: 'careful', sayArgs: { thing: 'the King Slime' } } : { goal: 'guard' };
|
|
173
|
+
}
|
|
174
|
+
const hurt = party.find(p => !p.down && p.hp < 40), person = hurt ?? party[0];
|
|
175
|
+
return person ? { goal: 'follow', args: { seat: person.seat } } : { goal: 'lead', args: { place: 'camp' } };
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
slime: {
|
|
180
|
+
body: { shape: 'circle', radius: 0.4, maxSpeed: 1.7 },
|
|
181
|
+
fields: { serial: f.u32(), hp: f.u16({ init: 18 }), maxHp: f.u16({ init: 18 }), size: f.u8({ init: 1 }), biteAt: f.tick() },
|
|
182
|
+
motion: { velocity: f.vec3(), pushAt: f.fix(), pushFrom: f.vec3(), pushDir: f.dir() },
|
|
183
|
+
tick(world, self) {
|
|
184
|
+
if (world.stage === 'dummy' || world.round.phase !== 'live' || self.motion.pushAt > 0) { self.motion.velocity = { x: 0, y: 0, z: 0 }; return; }
|
|
185
|
+
const M = world.math, all = world.near(self.pos, 64, 'hero').filter(p => p.hp > 0), folk = all.filter(p => p.driver === 'person'), prey = folk.length ? folk : all;
|
|
186
|
+
let target = prey[0], distance = 10000;
|
|
187
|
+
for (const p of prey) { const d = M.dist(p.pos, self.pos); if (d < distance) { distance = d; target = p; } }
|
|
188
|
+
if (world.shared.tactic === 'weakest' && !world.kids) for (const p of prey) if (target && p.hp / p.maxHp < target.hp / target.maxHp) target = p;
|
|
189
|
+
if (target) {
|
|
190
|
+
let to = target.pos;
|
|
191
|
+
const king = slimes(world).find(s => s.size === 3), speed = self.size === 3 ? 1.4 : (95 - self.size * 10) / 50;
|
|
192
|
+
if (world.shared.tactic === 'surround' && distance > 1.8) to = M.add(to, M.scale(M.dir(self.serial * 2.39996), 1.6));
|
|
193
|
+
if (world.shared.tactic === 'regroup' && king && king.id !== self.id && distance > 2.8) to = M.add(king.pos, M.scale(M.dir(self.serial), 1.4));
|
|
194
|
+
const delta = M.sub(to, self.pos); self.motion.velocity = M.len(delta) > .08 ? M.scale(M.norm(delta), speed) : { x: 0, y: 0, z: 0 };
|
|
195
|
+
if (M.dist(target.pos, self.pos) < .64 + self.size * .2 && world.tick - self.biteAt > world.ticks(.8)) { self.biteAt = world.tick; world.send(target.id, 'bite', { damage: 5 * self.size + (self.size === 3 ? 10 : 0), size: self.size }); }
|
|
196
|
+
} else self.motion.velocity = { x: M.sin(world.tick * world.dt / .9 + self.serial) * .4, y: M.cos(world.tick * world.dt / 1.1 + self.serial) * .4, z: 0 };
|
|
197
|
+
},
|
|
198
|
+
on: {
|
|
199
|
+
hit(world, self, e) {
|
|
200
|
+
if (self.hp === 0 || world.math.dist(e.from, self.pos) > world.tune.strikeRange + self.size * .2) return;
|
|
201
|
+
self.hp = Math.max(0, self.hp - e.damage);
|
|
202
|
+
self.motion.pushFrom = self.pos; self.motion.pushDir = world.math.norm(world.math.sub(self.pos, e.from)); self.motion.pushAt = world.tick + world.tune.hitStopMs / 1000 / world.dt;
|
|
203
|
+
if (self.hp === 0) {
|
|
204
|
+
world.send(e.by, 'reward', { xp: self.size === 3 ? 60 : 6 * self.size, gold: self.size === 3 ? 50 : 2 + Math.floor(world.random() * 4) * self.size });
|
|
205
|
+
if (self.size === 3) world.sendRoom('kingSlain', {}); world.despawn(self);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
onRoom: { roundOver(world, self) { world.despawn(self); } },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
room: {
|
|
213
|
+
rounds: { seconds: 90, breakSeconds: 8 }, bots: { keep: 3 },
|
|
214
|
+
join(ctx, player) { const spots = ctx.map.spots('start'); return { kind: 'hero', at: spots[player.seat % spots.length] }; },
|
|
215
|
+
start(world) { world.after(1, 'beat', {}); world.after(1, 'director', {}); },
|
|
216
|
+
on: {
|
|
217
|
+
roundStart(world) { world.shared.spawnAt = 0; world.shared.kingAt = world.tick + world.ticks(45); if (world.stage === 'dummy') { world.shared.serial += 1; world.spawn('slime', { x: 16.6, y: 10, z: 0 }, { hp: 36, maxHp: 36, size: 2, serial: world.shared.serial }); } },
|
|
218
|
+
kingSlain(world) { world.shared.kingSlainAt = world.tick; },
|
|
219
|
+
beat(world) {
|
|
220
|
+
world.after(1, 'beat', {}); world.shared.quests = quests(world);
|
|
221
|
+
if (world.stage === 'dummy' || world.round.phase !== 'live' || world.shared.kingAt === 0) return;
|
|
222
|
+
const list = slimes(world), cap = Math.max(4, 6 + people(world).length * 2 + (world.shared.pressure - 2) * 2) + (world.shared.waveLeft > 0 ? 3 : 0);
|
|
223
|
+
if ((world.tick >= world.shared.spawnAt || world.shared.waveLeft > 0) && list.length < cap) {
|
|
224
|
+
world.shared.spawnAt = world.tick + world.ticks([1.9, 1.45, 1.1, .85, .7][world.shared.pressure]); if (world.shared.waveLeft > 0) world.shared.waveLeft -= 1;
|
|
225
|
+
const edge = Math.floor(world.random() * 4), x = edge === 0 ? .6 : edge === 1 ? 31.4 : world.random() * 32, y = edge === 2 ? .6 : edge === 3 ? 19.4 : world.random() * 20, size = world.random() < .25 ? 2 : 1;
|
|
226
|
+
world.shared.serial += 1; world.spawn('slime', { x, y, z: 0 }, { hp: 18 * size, maxHp: 18 * size, size, serial: world.shared.serial });
|
|
227
|
+
}
|
|
228
|
+
if (world.tick >= world.shared.kingAt && !list.some(s => s.size === 3)) { world.shared.kingAt = world.tick + world.ticks(60); world.shared.serial += 1; world.spawn('slime', { x: 16, y: .8, z: 0 }, { hp: 160, maxHp: 160, size: 3, serial: world.shared.serial }); world.emit('king', { x: 16, y: .8, z: 0 }, {}); }
|
|
229
|
+
},
|
|
230
|
+
director(world) {
|
|
231
|
+
world.after(world.ticks(6), 'director', {}); if (world.round.phase !== 'live') return;
|
|
232
|
+
const list = slimes(world), king = list.find(s => s.size === 3);
|
|
233
|
+
world.ask('director', { heroes: world.near({ x: 16, y: 10, z: 0 }, 64, 'hero').filter(p => p.driver === 'person').map(p => ({ hp: Math.round(100 * p.hp / p.maxHp), down: p.hp === 0, level: p.level, nearKing: king ? Math.round(world.math.dist(p.pos, king.pos) * 50) : 0 })), slimes: { count: list.length, big: list.filter(s => s.size === 2).length, king: king ? Math.round(100 * king.hp / king.maxHp) : 0 }, night: { secondsLeft: Math.max(0, Math.round((world.round.endsAt - world.tick) * world.dt)) }, now: { tactic: world.shared.tactic, pressure: world.shared.pressure }, kids: world.kids });
|
|
234
|
+
},
|
|
235
|
+
answer(world, e) {
|
|
236
|
+
if (e.ask !== 'director') return;
|
|
237
|
+
world.shared.tactic = world.kids && e.picks.tactic === 'weakest' ? 'chase' : e.picks.tactic;
|
|
238
|
+
world.shared.pressure = Math.min(world.kids ? 2 : 4, e.picks.pressure);
|
|
239
|
+
if (e.picks.wave && world.shared.waveLeft === 0) world.shared.waveLeft = 3;
|
|
240
|
+
world.shared.directorBy = e.by; world.emit('director', { x: 16, y: 10, z: 0 }, { tactic: world.shared.tactic, pressure: world.shared.pressure, by: e.by });
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|