@ludra/core 0.5.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/dist/ascii.js +2 -2
- package/dist/browser.js +151 -19
- package/dist/components.d.ts +7 -1
- package/dist/components.js +22 -1
- package/dist/drawcheck.d.ts +7 -0
- package/dist/drawcheck.js +81 -0
- package/dist/effects.d.ts +2 -1
- package/dist/effects.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/postfx.d.ts +18 -0
- package/dist/postfx.js +100 -0
- package/dist/script.d.ts +64 -2
- package/dist/script.js +22 -2
- package/dist/types.d.ts +14 -1
- package/dist/types.js +1 -1
- package/dist/validate.js +48 -4
- package/dist/world.d.ts +32 -4
- package/dist/world.js +148 -24
- package/package.json +1 -1
- package/src/ascii.ts +2 -2
- package/src/browser.ts +156 -17
- package/src/components.ts +30 -1
- package/src/drawcheck.ts +80 -0
- package/src/effects.ts +15 -1
- package/src/index.ts +1 -0
- package/src/postfx.ts +110 -0
- package/src/script.ts +80 -2
- package/src/types.ts +15 -1
- package/src/validate.ts +54 -4
- package/src/world.ts +144 -28
package/src/world.ts
CHANGED
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
type Platformer,
|
|
11
11
|
type ScriptComponent,
|
|
12
12
|
type Sprite,
|
|
13
|
+
type Topdown,
|
|
13
14
|
type Transform,
|
|
14
15
|
} from "./components.ts";
|
|
15
16
|
import {
|
|
16
17
|
getScript,
|
|
17
18
|
ScriptError,
|
|
19
|
+
type DrawContext,
|
|
18
20
|
type Script,
|
|
19
21
|
type ScriptContext,
|
|
20
22
|
type ScriptEntity,
|
|
@@ -38,8 +40,6 @@ export interface PlatformerState extends Platformer {
|
|
|
38
40
|
/** True from a player-initiated jump until the apex, so only those jumps are cut short on release. */
|
|
39
41
|
jumping: boolean;
|
|
40
42
|
bufferTimer: number;
|
|
41
|
-
spawnX: number;
|
|
42
|
-
spawnY: number;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface PatrolState extends Patrol {
|
|
@@ -65,6 +65,9 @@ export interface Entity {
|
|
|
65
65
|
body?: BodyState;
|
|
66
66
|
sprite?: Sprite;
|
|
67
67
|
platformer?: PlatformerState;
|
|
68
|
+
topdown?: Topdown;
|
|
69
|
+
/** Where a player respawns after dying. */
|
|
70
|
+
spawn?: { x: number; y: number };
|
|
68
71
|
collectible?: Collectible;
|
|
69
72
|
patrol?: PatrolState;
|
|
70
73
|
hazard?: Hazard;
|
|
@@ -81,6 +84,7 @@ export type GameEvent = { frame: number; time: number; x: number; y: number } &
|
|
|
81
84
|
| { type: "die"; entity: string; cause: string }
|
|
82
85
|
| { type: "stomp"; entity: string; enemy: string; score: number }
|
|
83
86
|
| { type: "win"; entity: string; goal: string }
|
|
87
|
+
| { type: "bounce"; entity: string; other: string }
|
|
84
88
|
/** Fired by a script with world.emit(name, data). */
|
|
85
89
|
| { type: "custom"; name: string; entity: string; data?: unknown }
|
|
86
90
|
);
|
|
@@ -133,6 +137,8 @@ const STOMP_TOLERANCE = 8;
|
|
|
133
137
|
const FALL_MARGIN = 100;
|
|
134
138
|
/** Bodies this close count as touching for `touch` hooks, so solids (doors, walls) can be touched too. */
|
|
135
139
|
const TOUCH_MARGIN = 1;
|
|
140
|
+
/** Impacts slower than this (px/s, after bounce) stop instead of bouncing. */
|
|
141
|
+
const MIN_BOUNCE_SPEED = 40;
|
|
136
142
|
const EPSILON = 1e-4;
|
|
137
143
|
|
|
138
144
|
/**
|
|
@@ -161,6 +167,8 @@ export class World {
|
|
|
161
167
|
private scriptInput: ScriptInput = { held: () => false, pressed: () => false };
|
|
162
168
|
/** The script hook currently running, for error messages and event attribution. */
|
|
163
169
|
private current: { entity: Entity; hook: string } | undefined;
|
|
170
|
+
/** True while a draw hook runs: the game can't be changed then. */
|
|
171
|
+
private drawing = false;
|
|
164
172
|
|
|
165
173
|
constructor(scene: SceneDef, options: WorldOptions = {}) {
|
|
166
174
|
this.width = scene.world?.width ?? 960;
|
|
@@ -195,6 +203,7 @@ export class World {
|
|
|
195
203
|
if (this.won) return;
|
|
196
204
|
|
|
197
205
|
this.updatePlatformers(input, pressed);
|
|
206
|
+
this.updateTopdown(input);
|
|
198
207
|
this.updateScripts(input, pressed);
|
|
199
208
|
if (this.won) return;
|
|
200
209
|
this.updatePatrols();
|
|
@@ -286,9 +295,36 @@ export class World {
|
|
|
286
295
|
const fn = script.module[hook];
|
|
287
296
|
if (!fn) return;
|
|
288
297
|
const ctx: ScriptContext = { self: this.handle(entity), world: this.api(), input: this.scriptInput, params: script.params, dt: FIXED_DT };
|
|
298
|
+
this.run(entity, hook, () => (fn as (ctx: ScriptContext, other?: ScriptEntity) => void).call(script.module, ctx, other));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Runs an entity's `draw` or `drawUI` hook. Called by the renderer, never by the
|
|
303
|
+
* simulation. `self` and `world` are read-only for the duration.
|
|
304
|
+
*/
|
|
305
|
+
callDrawHook(entity: Entity, hook: "draw" | "drawUI", extras: Omit<DrawContext, "self" | "world" | "params">): void {
|
|
306
|
+
const script = entity.script;
|
|
307
|
+
const fn = script?.module[hook];
|
|
308
|
+
if (!script || !fn || entity.removed) return;
|
|
309
|
+
const ctx: DrawContext = { ...extras, self: this.handle(entity), world: this.api(), params: script.params };
|
|
310
|
+
this.drawing = true;
|
|
311
|
+
try {
|
|
312
|
+
this.run(entity, hook, () => fn.call(script.module, ctx));
|
|
313
|
+
} finally {
|
|
314
|
+
this.drawing = false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** True when any live entity's script has the given hook. */
|
|
319
|
+
hasHook(hook: "draw" | "drawUI"): boolean {
|
|
320
|
+
return this.entities.some((e) => !e.removed && typeof e.script?.module[hook] === "function");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private run(entity: Entity, hook: string, call: () => void): void {
|
|
324
|
+
const script = entity.script!;
|
|
289
325
|
this.current = { entity, hook };
|
|
290
326
|
try {
|
|
291
|
-
|
|
327
|
+
call();
|
|
292
328
|
} catch (err) {
|
|
293
329
|
if (err instanceof ScriptError) throw err;
|
|
294
330
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -298,6 +334,13 @@ export class World {
|
|
|
298
334
|
}
|
|
299
335
|
}
|
|
300
336
|
|
|
337
|
+
/** Draw hooks only paint, so the browser can't drift from what tests verified. */
|
|
338
|
+
private guard(what: string): void {
|
|
339
|
+
if (this.drawing) {
|
|
340
|
+
throw this.misuse(`${what} changes the game, which a draw hook can't do. Change the game in update or touch, and keep visual memory in ctx.local.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
301
344
|
/** An error caused by how a script used the API, reported against the running script. */
|
|
302
345
|
private misuse(message: string): ScriptError {
|
|
303
346
|
const running = this.current;
|
|
@@ -320,24 +363,28 @@ export class World {
|
|
|
320
363
|
return e.transform.x;
|
|
321
364
|
},
|
|
322
365
|
set x(value: number) {
|
|
366
|
+
world.guard("Setting x");
|
|
323
367
|
e.transform.x = value;
|
|
324
368
|
},
|
|
325
369
|
get y() {
|
|
326
370
|
return e.transform.y;
|
|
327
371
|
},
|
|
328
372
|
set y(value: number) {
|
|
373
|
+
world.guard("Setting y");
|
|
329
374
|
e.transform.y = value;
|
|
330
375
|
},
|
|
331
376
|
get vx() {
|
|
332
377
|
return e.body?.vx ?? 0;
|
|
333
378
|
},
|
|
334
379
|
set vx(value: number) {
|
|
380
|
+
world.guard("Setting vx");
|
|
335
381
|
if (e.body) e.body.vx = value;
|
|
336
382
|
},
|
|
337
383
|
get vy() {
|
|
338
384
|
return e.body?.vy ?? 0;
|
|
339
385
|
},
|
|
340
386
|
set vy(value: number) {
|
|
387
|
+
world.guard("Setting vy");
|
|
341
388
|
if (e.body) e.body.vy = value;
|
|
342
389
|
},
|
|
343
390
|
get grounded() {
|
|
@@ -353,7 +400,8 @@ export class World {
|
|
|
353
400
|
return !e.removed;
|
|
354
401
|
},
|
|
355
402
|
get state() {
|
|
356
|
-
|
|
403
|
+
// A frozen copy while drawing, so a draw hook can't write game state by accident.
|
|
404
|
+
return world.drawing ? Object.freeze({ ...e.state }) : e.state;
|
|
357
405
|
},
|
|
358
406
|
has(component: string) {
|
|
359
407
|
return component === "transform" || (component in e && (e as unknown as Record<string, unknown>)[component] !== undefined);
|
|
@@ -391,7 +439,14 @@ export class World {
|
|
|
391
439
|
get score() {
|
|
392
440
|
return world.score;
|
|
393
441
|
},
|
|
442
|
+
get deaths() {
|
|
443
|
+
return world.deaths;
|
|
444
|
+
},
|
|
445
|
+
get won() {
|
|
446
|
+
return world.won;
|
|
447
|
+
},
|
|
394
448
|
set score(value: number) {
|
|
449
|
+
world.guard("Setting score");
|
|
395
450
|
world.score = value;
|
|
396
451
|
},
|
|
397
452
|
get: (id) => {
|
|
@@ -401,27 +456,34 @@ export class World {
|
|
|
401
456
|
all: (component) =>
|
|
402
457
|
world.entities.filter((e) => !component || world.handle(e).has(component)).map((e) => world.handle(e)),
|
|
403
458
|
player: () => {
|
|
404
|
-
const e = world.entities.find(
|
|
459
|
+
const e = world.entities.find(isPlayer);
|
|
405
460
|
return e ? world.handle(e) : undefined;
|
|
406
461
|
},
|
|
407
|
-
spawn: (def) =>
|
|
462
|
+
spawn: (def) => {
|
|
463
|
+
world.guard("world.spawn");
|
|
464
|
+
return world.spawn(def);
|
|
465
|
+
},
|
|
408
466
|
destroy: (target) => {
|
|
467
|
+
world.guard("world.destroy");
|
|
409
468
|
const e = world.resolve(target);
|
|
410
469
|
if (e) world.remove(e);
|
|
411
470
|
},
|
|
412
471
|
kill: (target) => {
|
|
472
|
+
world.guard("world.kill");
|
|
413
473
|
const e = world.resolve(target);
|
|
414
474
|
if (!e) return;
|
|
415
|
-
if (e
|
|
475
|
+
if (isPlayer(e)) world.kill(e, world.current?.entity.id ?? "script");
|
|
416
476
|
else world.remove(e);
|
|
417
477
|
},
|
|
418
478
|
win: () => {
|
|
479
|
+
world.guard("world.win");
|
|
419
480
|
if (world.won) return;
|
|
420
481
|
const by = world.current?.entity;
|
|
421
482
|
world.won = true;
|
|
422
483
|
world.emit({ type: "win", entity: by?.id ?? "script", goal: by?.id ?? "script", x: by?.transform.x ?? 0, y: by?.transform.y ?? 0 });
|
|
423
484
|
},
|
|
424
485
|
emit: (name, data) => {
|
|
486
|
+
world.guard("world.emit");
|
|
425
487
|
if (typeof name !== "string" || name === "") throw world.misuse("world.emit needs a non-empty event name.");
|
|
426
488
|
if ((EVENT_TYPES as readonly string[]).includes(name)) {
|
|
427
489
|
throw world.misuse(`"${name}" is a built-in event and can't be emitted by scripts. Pick another name, e.g. "${name}-custom".`);
|
|
@@ -429,7 +491,10 @@ export class World {
|
|
|
429
491
|
const by = world.current?.entity;
|
|
430
492
|
world.emit({ type: "custom", name, entity: by?.id ?? "script", data, x: by?.transform.x ?? 0, y: by?.transform.y ?? 0 });
|
|
431
493
|
},
|
|
432
|
-
random: () =>
|
|
494
|
+
random: () => {
|
|
495
|
+
world.guard("world.random (it advances the game's seed; use ctx.random in draw hooks)");
|
|
496
|
+
return world.random();
|
|
497
|
+
},
|
|
433
498
|
};
|
|
434
499
|
return this.scriptApi;
|
|
435
500
|
}
|
|
@@ -462,14 +527,33 @@ export class World {
|
|
|
462
527
|
|
|
463
528
|
// ------------------------------------------------------------ built-ins
|
|
464
529
|
|
|
530
|
+
/** Entities the player controls: platformers and topdown movers. */
|
|
465
531
|
private players(): Entity[] {
|
|
466
|
-
return this.entities.filter(
|
|
532
|
+
return this.entities.filter(isPlayer);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private updateTopdown(held: ReadonlySet<Action>): void {
|
|
536
|
+
for (const e of this.entities) {
|
|
537
|
+
const t = e.topdown;
|
|
538
|
+
const b = e.body;
|
|
539
|
+
if (!t || !b) continue;
|
|
540
|
+
let dx = (held.has("right") ? 1 : 0) - (held.has("left") ? 1 : 0);
|
|
541
|
+
let dy = (held.has("down") ? 1 : 0) - (held.has("up") ? 1 : 0);
|
|
542
|
+
if (t.directions === "x") dy = 0;
|
|
543
|
+
if (t.directions === "y") dx = 0;
|
|
544
|
+
if (t.directions === "4" && dx !== 0) dy = 0;
|
|
545
|
+
// Diagonals move at the same speed as straight lines.
|
|
546
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
547
|
+
b.vx = (dx / length) * t.speed;
|
|
548
|
+
b.vy = (dy / length) * t.speed;
|
|
549
|
+
}
|
|
467
550
|
}
|
|
468
551
|
|
|
469
552
|
private updatePlatformers(held: ReadonlySet<Action>, pressed: ReadonlySet<Action>): void {
|
|
470
|
-
for (const e of this.
|
|
471
|
-
const p = e.platformer
|
|
472
|
-
const b = e.body
|
|
553
|
+
for (const e of this.entities) {
|
|
554
|
+
const p = e.platformer;
|
|
555
|
+
const b = e.body;
|
|
556
|
+
if (!p || !b) continue;
|
|
473
557
|
|
|
474
558
|
const direction = (held.has("right") ? 1 : 0) - (held.has("left") ? 1 : 0);
|
|
475
559
|
b.vx = direction * p.speed;
|
|
@@ -517,13 +601,18 @@ export class World {
|
|
|
517
601
|
private updateBodies(): void {
|
|
518
602
|
// Kinematic bodies move first, so anything standing on them can follow.
|
|
519
603
|
const moved = new Map<Entity, { dx: number; dy: number }>();
|
|
604
|
+
const walls = this.entities.filter((e) => e.body?.type === "static" && !e.body.sensor);
|
|
520
605
|
for (const e of this.entities) {
|
|
521
606
|
const b = e.body;
|
|
522
607
|
if (!b || b.type !== "kinematic") continue;
|
|
523
608
|
const { x, y } = e.transform;
|
|
524
609
|
e.transform.x += b.vx * FIXED_DT;
|
|
610
|
+
// A player-steered kinematic body (a paddle) stops at walls; scripted movers pass through.
|
|
611
|
+
if (e.topdown) for (const wall of walls) if (overlaps(e, wall)) e.transform.x = wall.transform.x + (Math.sign(e.transform.x - wall.transform.x) * (wall.body!.width + b.width)) / 2;
|
|
525
612
|
e.transform.y += b.vy * FIXED_DT;
|
|
613
|
+
if (e.topdown) for (const wall of walls) if (overlaps(e, wall)) e.transform.y = wall.transform.y + (Math.sign(e.transform.y - wall.transform.y) * (wall.body!.height + b.height)) / 2;
|
|
526
614
|
if (e.patrol) clampToPatrol(e, e.patrol);
|
|
615
|
+
if (e.topdown?.clampToWorld) this.clampToWorld(e);
|
|
527
616
|
moved.set(e, { dx: e.transform.x - x, dy: e.transform.y - y });
|
|
528
617
|
}
|
|
529
618
|
|
|
@@ -539,7 +628,8 @@ export class World {
|
|
|
539
628
|
e.transform.y += carried.dy;
|
|
540
629
|
}
|
|
541
630
|
|
|
542
|
-
|
|
631
|
+
// Top-down movers ignore gravity; everything else falls.
|
|
632
|
+
if (!e.topdown) b.vy = Math.min(b.vy + this.gravity * b.gravityScale * FIXED_DT, MAX_FALL_SPEED);
|
|
543
633
|
const wasGrounded = b.grounded;
|
|
544
634
|
b.grounded = false;
|
|
545
635
|
b.standingOn = undefined;
|
|
@@ -551,27 +641,47 @@ export class World {
|
|
|
551
641
|
const half = (solid.body!.width + b.width) / 2;
|
|
552
642
|
if (b.vx > 0) e.transform.x = solid.transform.x - half;
|
|
553
643
|
else if (b.vx < 0) e.transform.x = solid.transform.x + half;
|
|
554
|
-
b.vx =
|
|
644
|
+
b.vx = this.rebound(e, solid, b.vx);
|
|
555
645
|
}
|
|
556
646
|
|
|
557
647
|
e.transform.y += b.vy * FIXED_DT;
|
|
558
648
|
for (const solid of solids) {
|
|
559
649
|
if (!overlaps(e, solid)) continue;
|
|
560
650
|
const half = (solid.body!.height + b.height) / 2;
|
|
561
|
-
|
|
562
|
-
|
|
651
|
+
const falling = b.vy > 0;
|
|
652
|
+
if (b.vy > 0) e.transform.y = solid.transform.y - half;
|
|
653
|
+
else if (b.vy < 0) e.transform.y = solid.transform.y + half;
|
|
654
|
+
b.vy = this.rebound(e, solid, b.vy);
|
|
655
|
+
// A body that stopped on top of something is standing on it; one that bounced off is airborne.
|
|
656
|
+
if (falling && b.vy === 0) {
|
|
563
657
|
b.grounded = true;
|
|
564
658
|
b.standingOn = solid;
|
|
565
|
-
} else if (b.vy < 0) {
|
|
566
|
-
e.transform.y = solid.transform.y + half;
|
|
567
659
|
}
|
|
568
|
-
b.vy = 0;
|
|
569
660
|
}
|
|
570
661
|
|
|
662
|
+
if (e.topdown?.clampToWorld) this.clampToWorld(e);
|
|
571
663
|
if (b.grounded && !wasGrounded) this.emit({ type: "land", entity: e.id, ...feet(e) });
|
|
572
664
|
}
|
|
573
665
|
}
|
|
574
666
|
|
|
667
|
+
/**
|
|
668
|
+
* The velocity along one axis after hitting `solid`: 0, or reversed and scaled by `bounce`.
|
|
669
|
+
* Slow impacts stop instead, so resting bodies don't jitter.
|
|
670
|
+
*/
|
|
671
|
+
private rebound(e: Entity, solid: Entity, velocity: number): number {
|
|
672
|
+
const bounce = e.body!.bounce;
|
|
673
|
+
if (bounce <= 0 || Math.abs(velocity) * bounce < MIN_BOUNCE_SPEED) return 0;
|
|
674
|
+
this.emit({ type: "bounce", entity: e.id, other: solid.id, ...e.transform });
|
|
675
|
+
return -velocity * bounce;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private clampToWorld(e: Entity): void {
|
|
679
|
+
const halfWidth = (e.body?.width ?? 0) / 2;
|
|
680
|
+
const halfHeight = (e.body?.height ?? 0) / 2;
|
|
681
|
+
e.transform.x = Math.min(this.width - halfWidth, Math.max(halfWidth, e.transform.x));
|
|
682
|
+
e.transform.y = Math.min(this.height - halfHeight, Math.max(halfHeight, e.transform.y));
|
|
683
|
+
}
|
|
684
|
+
|
|
575
685
|
private updateCollectibles(): void {
|
|
576
686
|
const players = this.players();
|
|
577
687
|
for (const item of this.entities.filter((e) => e.collectible && e.body)) {
|
|
@@ -592,7 +702,7 @@ export class World {
|
|
|
592
702
|
}
|
|
593
703
|
for (const hazard of this.entities.filter((e) => e.hazard && e.body)) {
|
|
594
704
|
if (!overlaps(player, hazard)) continue;
|
|
595
|
-
if (hazard.hazard!.stompable && isStomp(player, hazard)) {
|
|
705
|
+
if (hazard.hazard!.stompable && player.platformer && isStomp(player, hazard)) {
|
|
596
706
|
this.remove(hazard);
|
|
597
707
|
b.vy = -player.platformer!.jumpSpeed * STOMP_BOUNCE;
|
|
598
708
|
this.score += hazard.hazard!.score;
|
|
@@ -619,18 +729,20 @@ export class World {
|
|
|
619
729
|
/** Sends a player back to where it started. Collected items stay collected. */
|
|
620
730
|
private kill(player: Entity, cause: string): void {
|
|
621
731
|
const b = player.body!;
|
|
622
|
-
const p = player.platformer!;
|
|
623
732
|
this.deaths++;
|
|
624
733
|
this.emit({ type: "die", entity: player.id, cause, ...player.transform });
|
|
625
|
-
player.transform.x =
|
|
626
|
-
player.transform.y =
|
|
734
|
+
player.transform.x = player.spawn?.x ?? player.transform.x;
|
|
735
|
+
player.transform.y = player.spawn?.y ?? player.transform.y;
|
|
627
736
|
b.vx = 0;
|
|
628
737
|
b.vy = 0;
|
|
629
738
|
b.grounded = false;
|
|
630
739
|
b.standingOn = undefined;
|
|
631
|
-
p
|
|
632
|
-
p
|
|
633
|
-
|
|
740
|
+
const p = player.platformer;
|
|
741
|
+
if (p) {
|
|
742
|
+
p.coyoteTimer = 0;
|
|
743
|
+
p.bufferTimer = 0;
|
|
744
|
+
p.jumping = false;
|
|
745
|
+
}
|
|
634
746
|
}
|
|
635
747
|
}
|
|
636
748
|
|
|
@@ -646,10 +758,10 @@ function createEntity(def: EntityDef): Entity {
|
|
|
646
758
|
coyoteTimer: 0,
|
|
647
759
|
bufferTimer: 0,
|
|
648
760
|
jumping: false,
|
|
649
|
-
spawnX: transform.x,
|
|
650
|
-
spawnY: transform.y,
|
|
651
761
|
};
|
|
652
762
|
}
|
|
763
|
+
if (c.topdown) entity.topdown = resolveComponent<Topdown>("topdown", c.topdown);
|
|
764
|
+
if (entity.platformer || entity.topdown) entity.spawn = { x: transform.x, y: transform.y };
|
|
653
765
|
if (c.collectible) entity.collectible = resolveComponent<Collectible>("collectible", c.collectible);
|
|
654
766
|
if (c.patrol) entity.patrol = { ...resolveComponent<Patrol>("patrol", c.patrol), originX: transform.x, originY: transform.y };
|
|
655
767
|
if (c.hazard) entity.hazard = resolveComponent<Hazard>("hazard", c.hazard);
|
|
@@ -659,6 +771,10 @@ function createEntity(def: EntityDef): Entity {
|
|
|
659
771
|
return entity;
|
|
660
772
|
}
|
|
661
773
|
|
|
774
|
+
function isPlayer(e: Entity): boolean {
|
|
775
|
+
return !!(e.platformer || e.topdown) && !!e.body;
|
|
776
|
+
}
|
|
777
|
+
|
|
662
778
|
function feet(e: Entity): { x: number; y: number } {
|
|
663
779
|
return { x: e.transform.x, y: e.transform.y + (e.body?.height ?? 0) / 2 };
|
|
664
780
|
}
|