@fluritheme/moon-explorer 0.0.0-stage → 0.1.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/dist/model.js ADDED
@@ -0,0 +1,429 @@
1
+ export const clamp = (n, a, b) => Math.min(b, Math.max(a, n));
2
+ export const lerp = (a, b, t) => a + (b - a) * t;
3
+ export const ease = (t) => {
4
+ t = clamp(t, 0, 1);
5
+ return t * t * (3 - 2 * t);
6
+ };
7
+ export function random(seed) {
8
+ let s = seed >>> 0;
9
+ return () => {
10
+ s += 0x6d2b79f5;
11
+ let t = s;
12
+ t = Math.imul(t ^ (t >>> 15), t | 1);
13
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
14
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
15
+ };
16
+ }
17
+ export class Expedition {
18
+ world;
19
+ size;
20
+ auto;
21
+ rover;
22
+ maxFlags;
23
+ phase = "landing";
24
+ mission = null;
25
+ phaseTime = 0;
26
+ elapsed = 0;
27
+ x = 0;
28
+ y = 0;
29
+ startX = 0;
30
+ startY = 0;
31
+ targetX = 0;
32
+ targetY = 0;
33
+ facing = 1;
34
+ surface = { id: "floor", x: 0, y: 0, width: 0 };
35
+ distance = 0;
36
+ roverDistance = 0;
37
+ samples = 0;
38
+ missionsCompleted = 0;
39
+ dust = [];
40
+ footprints = [];
41
+ flags = [];
42
+ rocks = [];
43
+ stars = [];
44
+ queue = [];
45
+ roverX = 0;
46
+ roverY = 0;
47
+ landerX = 0;
48
+ landerY = 0;
49
+ landingStartY = 0;
50
+ sampled = false;
51
+ flagged = false;
52
+ lastStep = 0;
53
+ nextAuto = 0;
54
+ rng;
55
+ onEvent = () => { };
56
+ constructor(world, size = 1, auto = true, rover = true, maxFlags = 5, seed = 42) {
57
+ this.world = world;
58
+ this.size = size;
59
+ this.auto = auto;
60
+ this.rover = rover;
61
+ this.maxFlags = maxFlags;
62
+ this.rng = random(seed);
63
+ this.land();
64
+ }
65
+ event(event) {
66
+ this.onEvent(event, {
67
+ phase: this.phase,
68
+ mission: this.mission,
69
+ samples: this.samples,
70
+ flags: this.flags.length,
71
+ surface: this.surface.id,
72
+ });
73
+ }
74
+ choose(target) {
75
+ const options = this.world.surfaces.filter((s) => s.width > 100 * this.size &&
76
+ s.y >= this.world.view.y + 80 &&
77
+ s.y <= this.world.view.y + this.world.view.height - 20);
78
+ return (target ??
79
+ options[Math.floor(this.rng() * options.length)] ??
80
+ this.world.surfaces[0] ?? {
81
+ id: "floor",
82
+ x: this.world.bounds.x + 20,
83
+ y: this.world.bounds.y + this.world.bounds.height - 30,
84
+ width: Math.max(120, this.world.bounds.width - 40),
85
+ });
86
+ }
87
+ setPhase(phase) {
88
+ this.phase = phase;
89
+ this.phaseTime = 0;
90
+ this.startX = this.x;
91
+ this.startY = this.y;
92
+ this.event("phase");
93
+ }
94
+ land(target) {
95
+ this.surface = this.choose(target);
96
+ this.x =
97
+ this.surface.x +
98
+ clamp(this.surface.width * 0.55, 65 * this.size, this.surface.width - 55 * this.size);
99
+ this.y = this.surface.y;
100
+ this.landerX = this.x;
101
+ this.landerY = this.y;
102
+ this.landingStartY = Math.min(this.world.view.y - 160, this.y - 400);
103
+ this.roverX = this.x - 76 * this.size;
104
+ this.roverY = this.y;
105
+ this.queue = [];
106
+ this.rocks = [];
107
+ this.dust = [];
108
+ this.mission = null;
109
+ this.setPhase("landing");
110
+ this.populateRocks();
111
+ }
112
+ populateRocks() {
113
+ for (let i = 0; i < 3; i++)
114
+ if (this.rocks.length < 18)
115
+ this.rocks.push({
116
+ x: this.surface.x +
117
+ 30 +
118
+ this.rng() * Math.max(20, this.surface.width - 60),
119
+ y: this.surface.y,
120
+ size: (5 + this.rng() * 6) * this.size,
121
+ collected: false,
122
+ surface: this.surface.id,
123
+ angle: this.rng() * 6,
124
+ });
125
+ }
126
+ enqueue(kind, target) {
127
+ if (this.queue.length >= 8)
128
+ return false;
129
+ if (kind === "rover" && !this.rover)
130
+ return false;
131
+ this.queue.push(target ? { kind, target } : { kind });
132
+ if (this.phase === "sleeping")
133
+ this.setPhase("idle");
134
+ return true;
135
+ }
136
+ startMission(kind, target) {
137
+ this.mission = kind;
138
+ this.event("mission");
139
+ this.sampled = false;
140
+ this.flagged = false;
141
+ if (kind === "explore") {
142
+ const s = this.choose(target);
143
+ if (s.id !== this.surface.id) {
144
+ this.surface = s;
145
+ this.targetX =
146
+ s.x +
147
+ clamp(s.width * 0.5, 35 * this.size, Math.max(40, s.width - 35 * this.size));
148
+ this.targetY = s.y;
149
+ this.setPhase("jumping");
150
+ this.populateRocks();
151
+ }
152
+ else {
153
+ this.targetX =
154
+ s.x +
155
+ 35 * this.size +
156
+ this.rng() * Math.max(1, s.width - 70 * this.size);
157
+ this.targetY = s.y;
158
+ this.setPhase("walking");
159
+ }
160
+ }
161
+ else if (kind === "rover") {
162
+ this.roverX = this.x - 45 * this.size;
163
+ this.roverY = this.y;
164
+ this.setPhase("boarding");
165
+ }
166
+ else if (kind === "jump") {
167
+ this.targetX = clamp(this.x + this.facing * 50 * this.size, this.surface.x + 30 * this.size, this.surface.x + this.surface.width - 30 * this.size);
168
+ this.targetY = this.surface.y;
169
+ this.setPhase("jumping");
170
+ }
171
+ else
172
+ this.setPhase(kind === "sample"
173
+ ? "sampling"
174
+ : kind === "flag"
175
+ ? "planting"
176
+ : kind === "observe"
177
+ ? "observing"
178
+ : kind === "rest"
179
+ ? "sleeping"
180
+ : "waving");
181
+ }
182
+ finish() {
183
+ if (this.mission)
184
+ this.missionsCompleted++;
185
+ this.mission = null;
186
+ this.setPhase("idle");
187
+ this.nextAuto = this.elapsed + 2.5 + this.rng() * 4;
188
+ }
189
+ burst(x = this.x, y = this.y, amount = 8, power = 1) {
190
+ for (let i = 0; i < amount && this.dust.length < 90; i++) {
191
+ const ttl = 0.5 + this.rng() * 0.7;
192
+ this.dust.push({
193
+ x,
194
+ y,
195
+ vx: (this.rng() - 0.5) * 65 * power,
196
+ vy: -this.rng() * 35 * power,
197
+ life: ttl,
198
+ ttl,
199
+ radius: (1 + this.rng() * 2.3) * this.size,
200
+ });
201
+ }
202
+ }
203
+ shootingStar() {
204
+ if (this.stars.length >= 5)
205
+ return;
206
+ const ttl = 1.1;
207
+ this.stars.push({
208
+ x: this.world.view.x + this.rng() * this.world.view.width * 0.7,
209
+ y: this.world.view.y + 30 + this.rng() * this.world.view.height * 0.25,
210
+ vx: 290 + this.rng() * 200,
211
+ vy: 100 + this.rng() * 90,
212
+ life: ttl,
213
+ ttl,
214
+ });
215
+ }
216
+ staticPose() {
217
+ this.phase = "idle";
218
+ this.phaseTime = 0;
219
+ this.mission = null;
220
+ this.x = this.landerX + 30 * this.size;
221
+ this.y = this.surface.y;
222
+ }
223
+ refresh(world) {
224
+ this.world = world;
225
+ const next = world.surfaces.find((s) => s.id === this.surface.id);
226
+ if (next) {
227
+ const delta = next.y - this.surface.y;
228
+ this.y += delta;
229
+ this.startY += delta;
230
+ this.targetY += delta;
231
+ this.landerY += delta;
232
+ this.roverY += delta;
233
+ for (const f of this.flags)
234
+ if (f.surface === next.id)
235
+ f.y += delta;
236
+ for (const r of this.rocks)
237
+ if (r.surface === next.id)
238
+ r.y += delta;
239
+ this.surface = next;
240
+ this.x = clamp(this.x, next.x + 22 * this.size, Math.max(next.x + 22 * this.size, next.x + next.width - 22 * this.size));
241
+ }
242
+ else {
243
+ this.surface = this.choose();
244
+ this.x = this.surface.x + this.surface.width * 0.5;
245
+ this.y = this.surface.y;
246
+ this.setPhase("idle");
247
+ }
248
+ }
249
+ step(dt) {
250
+ dt = clamp(dt, 0, 0.06);
251
+ this.elapsed += dt;
252
+ this.phaseTime += dt;
253
+ const t = this.phaseTime, s = this.size;
254
+ const before = this.x;
255
+ for (const d of this.dust) {
256
+ d.x += d.vx * dt;
257
+ d.y += d.vy * dt;
258
+ d.vy += 20 * dt;
259
+ d.life -= dt;
260
+ }
261
+ this.dust = this.dust.filter((d) => d.life > 0);
262
+ for (const f of this.footprints)
263
+ f.life -= dt;
264
+ this.footprints = this.footprints.filter((f) => f.life > 0).slice(-80);
265
+ for (const a of this.stars) {
266
+ a.x += a.vx * dt;
267
+ a.y += a.vy * dt;
268
+ a.life -= dt;
269
+ }
270
+ this.stars = this.stars.filter((a) => a.life > 0);
271
+ if (this.phase === "landing") {
272
+ this.landerY = lerp(this.landingStartY, this.surface.y, ease(t / 3.6));
273
+ this.y = this.landerY;
274
+ if (t > 2.4 && t < 3.6)
275
+ this.burst(this.x, this.surface.y, 2, 1 - t / 4);
276
+ if (t >= 3.6) {
277
+ this.burst(this.x, this.y, 24);
278
+ this.setPhase("egress");
279
+ this.event("landed");
280
+ }
281
+ }
282
+ else if (this.phase === "egress") {
283
+ this.x = lerp(this.startX, clamp(this.landerX + 65 * s, this.surface.x + 25 * s, this.surface.x + this.surface.width - 25 * s), ease(t / 1.9));
284
+ this.y =
285
+ this.surface.y - Math.sin(clamp(t / 1.9, 0, 1) * Math.PI) * 20 * s;
286
+ if (t >= 1.9) {
287
+ this.y = this.surface.y;
288
+ this.finish();
289
+ }
290
+ }
291
+ else if (this.phase === "idle") {
292
+ if (this.queue.length) {
293
+ const job = this.queue.shift();
294
+ this.startMission(job.kind, job.target);
295
+ }
296
+ else if (this.auto && this.elapsed >= this.nextAuto) {
297
+ const choices = [
298
+ "explore",
299
+ "sample",
300
+ "wave",
301
+ "observe",
302
+ "jump",
303
+ "flag",
304
+ ...(this.rover ? ["rover"] : []),
305
+ ];
306
+ this.startMission(choices[Math.floor(this.rng() * choices.length)]);
307
+ }
308
+ }
309
+ else if (this.phase === "walking") {
310
+ const delta = this.targetX - this.x;
311
+ this.facing = delta >= 0 ? 1 : -1;
312
+ this.x += Math.sign(delta) * Math.min(Math.abs(delta), 23 * s * dt);
313
+ this.y = this.surface.y - Math.max(0, Math.sin(t * 5)) * 2 * s;
314
+ if (t - this.lastStep > 0.48) {
315
+ this.lastStep = t;
316
+ this.footprints.push({
317
+ x: this.x,
318
+ y: this.surface.y,
319
+ facing: this.facing,
320
+ side: Math.sin(t * 5) > 0 ? 1 : -1,
321
+ life: 65,
322
+ });
323
+ this.burst(this.x, this.surface.y, 1, 0.3);
324
+ }
325
+ if (Math.abs(delta) < 1) {
326
+ this.y = this.surface.y;
327
+ this.finish();
328
+ }
329
+ }
330
+ else if (this.phase === "jumping") {
331
+ const u = clamp(t / 2.1, 0, 1);
332
+ this.facing = this.targetX >= this.startX ? 1 : -1;
333
+ this.x = lerp(this.startX, this.targetX, ease(u));
334
+ this.y =
335
+ lerp(this.startY, this.targetY, ease(u)) -
336
+ Math.sin(u * Math.PI) *
337
+ Math.min(115 * s, 65 + Math.abs(this.targetY - this.startY) * 0.25);
338
+ if (t >= 2.1) {
339
+ this.y = this.targetY;
340
+ this.burst(this.x, this.y, 14);
341
+ this.setPhase("celebrating");
342
+ }
343
+ }
344
+ else if (this.phase === "sampling") {
345
+ if (t > 2 && !this.sampled) {
346
+ this.sampled = true;
347
+ this.samples++;
348
+ const rock = this.rocks.find((r) => r.surface === this.surface.id && !r.collected);
349
+ if (rock)
350
+ rock.collected = true;
351
+ this.burst(this.x + 20 * s, this.y, 5, 0.4);
352
+ this.event("sample");
353
+ }
354
+ if (t > 4.2)
355
+ this.setPhase("celebrating");
356
+ }
357
+ else if (this.phase === "planting") {
358
+ if (t > 1.8 && !this.flagged) {
359
+ this.flagged = true;
360
+ this.flags.push({
361
+ x: this.x + 25 * s * this.facing,
362
+ y: this.y,
363
+ phase: this.rng() * 6,
364
+ surface: this.surface.id,
365
+ });
366
+ this.flags = this.flags.slice(-this.maxFlags);
367
+ this.burst(this.x + 25 * s, this.y, 9, 0.4);
368
+ this.event("flag");
369
+ }
370
+ if (t > 3.6)
371
+ this.setPhase("celebrating");
372
+ }
373
+ else if (this.phase === "waving") {
374
+ if (t > 3.2)
375
+ this.finish();
376
+ }
377
+ else if (this.phase === "observing") {
378
+ if (t > 2.5 && t < 2.5 + dt)
379
+ this.shootingStar();
380
+ if (t > 5)
381
+ this.finish();
382
+ }
383
+ else if (this.phase === "boarding") {
384
+ this.x = lerp(this.startX, this.roverX, ease(t / 1.3));
385
+ this.y =
386
+ this.surface.y - Math.sin(clamp(t / 1.3, 0, 1) * Math.PI) * 20 * s;
387
+ if (t >= 1.3) {
388
+ this.targetX =
389
+ this.surface.x +
390
+ 40 * s +
391
+ this.rng() * Math.max(1, this.surface.width - 80 * s);
392
+ this.setPhase("driving");
393
+ }
394
+ }
395
+ else if (this.phase === "driving") {
396
+ const d = this.targetX - this.x;
397
+ this.facing = d >= 0 ? 1 : -1;
398
+ this.x += Math.sign(d) * Math.min(Math.abs(d), 38 * s * dt);
399
+ this.roverX = this.x;
400
+ this.y = this.surface.y - 3 * s + Math.sin(t * 9) * 1.2 * s;
401
+ this.roverDistance += Math.abs(this.x - before);
402
+ if (this.rng() < 0.4)
403
+ this.burst(this.x - 30 * s * this.facing, this.surface.y, 1, 0.35);
404
+ if (Math.abs(d) < 1) {
405
+ this.y = this.surface.y;
406
+ this.setPhase("dismounting");
407
+ }
408
+ }
409
+ else if (this.phase === "dismounting") {
410
+ this.x = lerp(this.startX, clamp(this.roverX + 55 * s * this.facing, this.surface.x + 25 * s, this.surface.x + this.surface.width - 25 * s), ease(t / 1.2));
411
+ this.y =
412
+ this.surface.y - Math.sin(clamp(t / 1.2, 0, 1) * Math.PI) * 18 * s;
413
+ if (t > 1.2) {
414
+ this.y = this.surface.y;
415
+ this.finish();
416
+ }
417
+ }
418
+ else if (this.phase === "celebrating") {
419
+ if (t > 1.4)
420
+ this.finish();
421
+ }
422
+ else if (this.phase === "sleeping") {
423
+ if (t > 7)
424
+ this.finish();
425
+ }
426
+ if (this.phase !== "driving")
427
+ this.distance += Math.abs(this.x - before);
428
+ }
429
+ }
@@ -0,0 +1,13 @@
1
+ import { type Expedition } from "./model.js";
2
+ export interface PaintOptions {
3
+ accent: string;
4
+ lander: boolean;
5
+ rover: boolean;
6
+ pointer: {
7
+ x: number;
8
+ y: number;
9
+ active: boolean;
10
+ };
11
+ reduced: boolean;
12
+ }
13
+ export declare function paint(c: CanvasRenderingContext2D, m: Expedition, o: PaintOptions): void;