minimojs 1.0.0-alpha.2 → 1.0.0-alpha.4
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/README.md +57 -292
- package/dist/minimo.d.ts +233 -43
- package/dist/minimo.js +337 -51
- package/package.json +1 -1
package/dist/minimo.js
CHANGED
|
@@ -25,10 +25,7 @@
|
|
|
25
25
|
* @example
|
|
26
26
|
* ```ts
|
|
27
27
|
* // Direct instantiation
|
|
28
|
-
* const coin = new Sprite("🪙");
|
|
29
|
-
* coin.x = 300;
|
|
30
|
-
* coin.y = 200;
|
|
31
|
-
* coin.size = 32;
|
|
28
|
+
* const coin = new Sprite("🪙", 300, 200, 32);
|
|
32
29
|
* game.add(coin);
|
|
33
30
|
* ```
|
|
34
31
|
*
|
|
@@ -39,10 +36,7 @@
|
|
|
39
36
|
* health = 3;
|
|
40
37
|
*
|
|
41
38
|
* constructor(x: number, y: number) {
|
|
42
|
-
* super("🐢");
|
|
43
|
-
* this.x = x;
|
|
44
|
-
* this.y = y;
|
|
45
|
-
* this.size = 48;
|
|
39
|
+
* super("🐢", x, y, 48);
|
|
46
40
|
* this.gravityScale = 1;
|
|
47
41
|
* }
|
|
48
42
|
* }
|
|
@@ -53,7 +47,25 @@
|
|
|
53
47
|
*/
|
|
54
48
|
export class Sprite {
|
|
55
49
|
/**
|
|
56
|
-
*
|
|
50
|
+
* Base width and height of this sprite in pixels.
|
|
51
|
+
*
|
|
52
|
+
* This value is read-only after construction and defines the glyph-cache
|
|
53
|
+
* render size. The visible and collision size during gameplay is exposed by
|
|
54
|
+
* {@link Sprite.displaySize}.
|
|
55
|
+
*/
|
|
56
|
+
get size() {
|
|
57
|
+
return this._size;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Effective rendered/collision size in pixels.
|
|
61
|
+
* Computed as `size * scale` with non-negative clamping.
|
|
62
|
+
*/
|
|
63
|
+
get displaySize() {
|
|
64
|
+
const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
|
|
65
|
+
return this._size * Math.max(0, safeScale);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new Sprite with the given emoji, optional position, and base size.
|
|
57
69
|
* All other properties use their defaults and can be set after construction.
|
|
58
70
|
*
|
|
59
71
|
* @param sprite - The emoji character to render. Must be a single emoji.
|
|
@@ -61,15 +73,15 @@ export class Sprite {
|
|
|
61
73
|
* @example "🔥", "⭐", "🐢", "💣", "👾"
|
|
62
74
|
* @param x - Initial X position in world space (center), in pixels. Default: `0`.
|
|
63
75
|
* @param y - Initial Y position in world space (center), in pixels. Default: `0`.
|
|
76
|
+
* @param size - Base sprite size in pixels. Default: `32`.
|
|
64
77
|
*
|
|
65
78
|
* @example
|
|
66
79
|
* ```ts
|
|
67
|
-
* const enemy = new Sprite("👾", 200, 100);
|
|
68
|
-
* enemy.size = 40;
|
|
80
|
+
* const enemy = new Sprite("👾", 200, 100, 40);
|
|
69
81
|
* game.add(enemy);
|
|
70
82
|
* ```
|
|
71
83
|
*/
|
|
72
|
-
constructor(sprite, x = 0, y = 0) {
|
|
84
|
+
constructor(sprite, x = 0, y = 0, size = 32) {
|
|
73
85
|
/**
|
|
74
86
|
* X position in world space (horizontal center of the sprite), in pixels.
|
|
75
87
|
* Positive X points right. Updated each frame by: `x += vx * dt`.
|
|
@@ -83,10 +95,34 @@ export class Sprite {
|
|
|
83
95
|
*/
|
|
84
96
|
this.y = 0;
|
|
85
97
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
98
|
+
* Visual scale multiplier applied to {@link Sprite.size}.
|
|
99
|
+
* Default: `1`.
|
|
100
|
+
*
|
|
101
|
+
* Set this at runtime to resize a sprite without changing its base cached
|
|
102
|
+
* glyph size.
|
|
103
|
+
*/
|
|
104
|
+
this.scale = 1;
|
|
105
|
+
/**
|
|
106
|
+
* CSS text color used when rendering this sprite.
|
|
107
|
+
*
|
|
108
|
+
* This mainly affects monochrome glyphs and symbol-style sprites.
|
|
109
|
+
* Full-color emoji may ignore this and render with their native colors,
|
|
110
|
+
* depending on browser behavior.
|
|
111
|
+
*/
|
|
112
|
+
this.color = "#000000";
|
|
113
|
+
/**
|
|
114
|
+
* Physics body type flag.
|
|
115
|
+
*
|
|
116
|
+
* - `false` (default): the sprite is dynamic and can be moved by velocity,
|
|
117
|
+
* gravity, and explicit collision resolution.
|
|
118
|
+
* - `true`: the sprite is static and is not moved by the engine's built-in
|
|
119
|
+
* velocity/gravity integration. Static sprites act as stable obstacles for
|
|
120
|
+
* simple platform collisions.
|
|
121
|
+
*
|
|
122
|
+
* Static sprites can still be repositioned manually by setting `x` and `y`
|
|
123
|
+
* directly in your own game code.
|
|
88
124
|
*/
|
|
89
|
-
this.
|
|
125
|
+
this.isStatic = false;
|
|
90
126
|
/**
|
|
91
127
|
* Visual rotation of the sprite in degrees.
|
|
92
128
|
* `0` = upright. Positive values rotate clockwise.
|
|
@@ -154,6 +190,8 @@ export class Sprite {
|
|
|
154
190
|
this.sprite = sprite;
|
|
155
191
|
this.x = x;
|
|
156
192
|
this.y = y;
|
|
193
|
+
const safeSize = Number.isFinite(size) ? size : 32;
|
|
194
|
+
this._size = Math.max(1, safeSize);
|
|
157
195
|
}
|
|
158
196
|
}
|
|
159
197
|
// ---------------------------------------------------------------------------
|
|
@@ -168,17 +206,63 @@ export class Sprite {
|
|
|
168
206
|
*
|
|
169
207
|
* ---
|
|
170
208
|
*
|
|
171
|
-
* ##
|
|
209
|
+
* ## ES Module Only
|
|
210
|
+
*
|
|
211
|
+
* MinimoJS is an **ES module only** package.
|
|
212
|
+
* You must import it with standard ESM syntax such as:
|
|
172
213
|
*
|
|
173
214
|
* ```ts
|
|
174
215
|
* import { Game, Sprite } from "minimojs";
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* In the browser, use it from a module script:
|
|
219
|
+
*
|
|
220
|
+
* ```html
|
|
221
|
+
* <script type="module">
|
|
222
|
+
* import { Game, Sprite } from "./dist/minimo.js";
|
|
223
|
+
* </script>
|
|
224
|
+
* ```
|
|
225
|
+
*
|
|
226
|
+
* Or from a CDN:
|
|
227
|
+
*
|
|
228
|
+
* ```ts
|
|
229
|
+
* import { Game, Sprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
|
|
230
|
+
* ```
|
|
231
|
+
*
|
|
232
|
+
* Do NOT use a classic `<script>` tag without `type="module"`.
|
|
233
|
+
* Do NOT expect a global `window.MinimoJS`.
|
|
234
|
+
* Do NOT use `require("minimojs")` (CommonJS is not supported).
|
|
235
|
+
*
|
|
236
|
+
* ---
|
|
237
|
+
*
|
|
238
|
+
* ## Required Font Setup
|
|
239
|
+
*
|
|
240
|
+
* `drawText()` renders with `"Press Start 2P", monospace`.
|
|
241
|
+
* You MUST load `Press Start 2P` yourself in your `index.html` before starting
|
|
242
|
+
* the game. MinimoJS does NOT download, inject, or manage web fonts for you.
|
|
243
|
+
* If the font is missing, the browser falls back to `monospace`.
|
|
244
|
+
*
|
|
245
|
+
* Example `index.html` `<head>` setup:
|
|
246
|
+
*
|
|
247
|
+
* ```html
|
|
248
|
+
* <link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
249
|
+
* <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
250
|
+
* <link
|
|
251
|
+
* href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
|
|
252
|
+
* rel="stylesheet"
|
|
253
|
+
* />
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* ---
|
|
257
|
+
*
|
|
258
|
+
* ## Quick Start
|
|
259
|
+
*
|
|
260
|
+
* ```ts
|
|
261
|
+
* import { Game, Sprite } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo.js";
|
|
175
262
|
*
|
|
176
263
|
* const game = new Game(800, 600);
|
|
177
264
|
*
|
|
178
|
-
* const player = new Sprite("🐢");
|
|
179
|
-
* player.x = 400;
|
|
180
|
-
* player.y = 500;
|
|
181
|
-
* player.size = 48;
|
|
265
|
+
* const player = new Sprite("🐢", 400, 500, 48);
|
|
182
266
|
* game.add(player);
|
|
183
267
|
*
|
|
184
268
|
* game.onUpdate = (dt) => {
|
|
@@ -205,7 +289,8 @@ export class Sprite {
|
|
|
205
289
|
* ## Flat API
|
|
206
290
|
*
|
|
207
291
|
* ALL engine functionality is on the `game` object.
|
|
208
|
-
* Do not look for
|
|
292
|
+
* Do not look for nested subsystem objects like `game.input.keyboard`,
|
|
293
|
+
* `game.physics.world`, etc. They do not exist in MinimoJS v1.
|
|
209
294
|
*
|
|
210
295
|
* ---
|
|
211
296
|
*
|
|
@@ -311,8 +396,7 @@ export class Sprite {
|
|
|
311
396
|
* ```ts
|
|
312
397
|
* game.onCreate = () => {
|
|
313
398
|
* // scene init: add sprites, setup timers
|
|
314
|
-
* const skull = new Sprite("💀");
|
|
315
|
-
* skull.x = 400; skull.y = 300; skull.size = 96;
|
|
399
|
+
* const skull = new Sprite("💀", 400, 300, 96);
|
|
316
400
|
* game.add(skull);
|
|
317
401
|
* };
|
|
318
402
|
*
|
|
@@ -349,7 +433,7 @@ export class Sprite {
|
|
|
349
433
|
* - Text input / HTML form elements
|
|
350
434
|
* - Parallax layers
|
|
351
435
|
* - Multiple cameras
|
|
352
|
-
* -
|
|
436
|
+
* - Full physics engine (only basic explicit AABB collision helpers are provided)
|
|
353
437
|
* - Image sprites (PNG, SVG, canvas, etc.)
|
|
354
438
|
* - `setTimeout` or `setInterval`
|
|
355
439
|
*/
|
|
@@ -372,6 +456,7 @@ export class Game {
|
|
|
372
456
|
* @example
|
|
373
457
|
* ```ts
|
|
374
458
|
* const game = new Game(800, 600);
|
|
459
|
+
* game.physics = true;
|
|
375
460
|
* ```
|
|
376
461
|
*/
|
|
377
462
|
constructor(width = 800, height = 600) {
|
|
@@ -428,6 +513,20 @@ export class Game {
|
|
|
428
513
|
* ```
|
|
429
514
|
*/
|
|
430
515
|
this.gravityY = 0;
|
|
516
|
+
/**
|
|
517
|
+
* Enables the basic collision-resolution helpers (`collide` / `collideAny`).
|
|
518
|
+
*
|
|
519
|
+
* When `false` (default), overlap detection still works, but collision
|
|
520
|
+
* resolution helpers are unavailable.
|
|
521
|
+
*
|
|
522
|
+
* Set this to `true` for simple platformer-style collision handling.
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```ts
|
|
526
|
+
* game.physics = true;
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
this.physics = false;
|
|
431
530
|
/**
|
|
432
531
|
* Horizontal scroll offset of the world camera, in pixels.
|
|
433
532
|
* The canvas viewport is shifted left by `scrollX` — sprites with higher `x`
|
|
@@ -582,8 +681,9 @@ export class Game {
|
|
|
582
681
|
// -------------------------------------------------------------------------
|
|
583
682
|
/**
|
|
584
683
|
* Registers a {@link Sprite} (or subclass instance) with the engine.
|
|
585
|
-
* After calling `add`, the sprite is rendered and
|
|
586
|
-
*
|
|
684
|
+
* After calling `add`, the sprite is rendered and, if dynamic
|
|
685
|
+
* (`isStatic = false`), receives built-in velocity/gravity integration every
|
|
686
|
+
* frame until {@link Game.destroySprite} or {@link Game.reset} is called.
|
|
587
687
|
*
|
|
588
688
|
* **Ownership:** The game instance takes ownership of the sprite from this
|
|
589
689
|
* point forward. It will appear in {@link Game.getSprites} on the same frame.
|
|
@@ -598,16 +698,14 @@ export class Game {
|
|
|
598
698
|
* @example
|
|
599
699
|
* ```ts
|
|
600
700
|
* // Plain sprite
|
|
601
|
-
* const coin = new Sprite("🪙");
|
|
602
|
-
* coin.x = 300; coin.y = 200; coin.size = 32;
|
|
701
|
+
* const coin = new Sprite("🪙", 300, 200, 32);
|
|
603
702
|
* game.add(coin);
|
|
604
703
|
*
|
|
605
704
|
* // Custom subclass
|
|
606
705
|
* class Enemy extends Sprite {
|
|
607
706
|
* speed = 150;
|
|
608
707
|
* constructor(x: number, y: number) {
|
|
609
|
-
* super("👾");
|
|
610
|
-
* this.x = x; this.y = y; this.size = 40;
|
|
708
|
+
* super("👾", x, y, 40);
|
|
611
709
|
* }
|
|
612
710
|
* }
|
|
613
711
|
* const enemy = game.add(new Enemy(600, 100));
|
|
@@ -666,10 +764,10 @@ export class Game {
|
|
|
666
764
|
* collision detection.
|
|
667
765
|
*
|
|
668
766
|
* Each sprite's bounding box is a square centered at `(x, y)` with side
|
|
669
|
-
* length `
|
|
767
|
+
* length `displaySize`. Rotation is **ignored** — the box is always axis-aligned.
|
|
670
768
|
*
|
|
671
769
|
* **No collision resolution is performed.** This method is detection-only.
|
|
672
|
-
*
|
|
770
|
+
* Use {@link Game.collide} for the engine's basic explicit push-out helper.
|
|
673
771
|
*
|
|
674
772
|
* @param a - First sprite.
|
|
675
773
|
* @param b - Second sprite.
|
|
@@ -683,8 +781,8 @@ export class Game {
|
|
|
683
781
|
* ```
|
|
684
782
|
*/
|
|
685
783
|
overlap(a, b) {
|
|
686
|
-
const halfA = a.
|
|
687
|
-
const halfB = b.
|
|
784
|
+
const halfA = a.displaySize / 2;
|
|
785
|
+
const halfB = b.displaySize / 2;
|
|
688
786
|
return (Math.abs(a.x - b.x) < halfA + halfB &&
|
|
689
787
|
Math.abs(a.y - b.y) < halfA + halfB);
|
|
690
788
|
}
|
|
@@ -720,6 +818,95 @@ export class Game {
|
|
|
720
818
|
}
|
|
721
819
|
return null;
|
|
722
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Tests and resolves a basic AABB collision between two sprites.
|
|
823
|
+
*
|
|
824
|
+
* This helper is intended for simple platformer-style collision response.
|
|
825
|
+
* It uses the same axis-aligned square bounds as {@link Game.overlap}, but
|
|
826
|
+
* additionally pushes one sprite out of the collision and zeroes velocity on
|
|
827
|
+
* the resolved axis.
|
|
828
|
+
*
|
|
829
|
+
* **Resolution rules:**
|
|
830
|
+
* - If the first sprite is dynamic (`isStatic = false`), the first sprite is resolved.
|
|
831
|
+
* - Otherwise, if the second sprite is dynamic, the second sprite is resolved.
|
|
832
|
+
* - If both sprites are static, collision is reported but no movement occurs.
|
|
833
|
+
*
|
|
834
|
+
* The returned flags are always reported relative to the **first** sprite.
|
|
835
|
+
*
|
|
836
|
+
* @param a - First sprite. Usually the moving actor (for example, the player).
|
|
837
|
+
* @param b - Second sprite. Usually a static obstacle or platform.
|
|
838
|
+
* @returns Collision details, or `null` if the sprites do not overlap.
|
|
839
|
+
* @throws Error if the game's physics helpers are not enabled.
|
|
840
|
+
*
|
|
841
|
+
* @example
|
|
842
|
+
* ```ts
|
|
843
|
+
* const hit = game.collide(player, floorTile);
|
|
844
|
+
* if (hit?.grounded) {
|
|
845
|
+
* canJump = true;
|
|
846
|
+
* }
|
|
847
|
+
* ```
|
|
848
|
+
*/
|
|
849
|
+
collide(a, b) {
|
|
850
|
+
this._assertPhysicsEnabled();
|
|
851
|
+
if (a === b)
|
|
852
|
+
return null;
|
|
853
|
+
const collision = this._getCollisionResolution(a, b);
|
|
854
|
+
if (!collision)
|
|
855
|
+
return null;
|
|
856
|
+
if (!a.isStatic) {
|
|
857
|
+
a.x += collision.separationX;
|
|
858
|
+
a.y += collision.separationY;
|
|
859
|
+
if (collision.axis === "x") {
|
|
860
|
+
a.vx = 0;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
a.vy = 0;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else if (!b.isStatic) {
|
|
867
|
+
b.x -= collision.separationX;
|
|
868
|
+
b.y -= collision.separationY;
|
|
869
|
+
if (collision.axis === "x") {
|
|
870
|
+
b.vx = 0;
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
b.vy = 0;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return collision.info;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Tests and resolves the first collision found between two groups of sprites.
|
|
880
|
+
*
|
|
881
|
+
* This behaves like {@link Game.overlapAny}, but uses the explicit collision
|
|
882
|
+
* rules from {@link Game.collide} and returns the collision details as the
|
|
883
|
+
* third tuple item.
|
|
884
|
+
*
|
|
885
|
+
* @param listA - First group of sprites.
|
|
886
|
+
* @param listB - Second group of sprites.
|
|
887
|
+
* @returns A `[Sprite, Sprite, CollisionInfo]` tuple for the first collision found,
|
|
888
|
+
* or `null` if no pair overlaps.
|
|
889
|
+
* @throws Error if the game's physics helpers are not enabled.
|
|
890
|
+
*
|
|
891
|
+
* @example
|
|
892
|
+
* ```ts
|
|
893
|
+
* const hit = game.collideAny([player], floorTiles);
|
|
894
|
+
* if (hit?.[2].grounded) {
|
|
895
|
+
* canJump = true;
|
|
896
|
+
* }
|
|
897
|
+
* ```
|
|
898
|
+
*/
|
|
899
|
+
collideAny(listA, listB) {
|
|
900
|
+
this._assertPhysicsEnabled();
|
|
901
|
+
for (const a of listA) {
|
|
902
|
+
for (const b of listB) {
|
|
903
|
+
const info = this.collide(a, b);
|
|
904
|
+
if (info)
|
|
905
|
+
return [a, b, info];
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
723
910
|
// -------------------------------------------------------------------------
|
|
724
911
|
// Input — Keyboard
|
|
725
912
|
// -------------------------------------------------------------------------
|
|
@@ -803,14 +990,14 @@ export class Game {
|
|
|
803
990
|
* Works with both mouse input and multiple simultaneous touches.
|
|
804
991
|
*
|
|
805
992
|
* Pointer hit testing uses a circular area centered on the sprite. The radius
|
|
806
|
-
* is `sprite.
|
|
993
|
+
* is `sprite.displaySize * radiusScale`. World-space sprites are tested against the
|
|
807
994
|
* current camera scroll. HUD sprites with `ignoreScroll = true` are tested in
|
|
808
995
|
* screen space.
|
|
809
996
|
*
|
|
810
997
|
* Use this for continuous virtual buttons such as touch movement controls.
|
|
811
998
|
*
|
|
812
999
|
* @param sprite - Target sprite to test. If `null` / `undefined`, returns `false`.
|
|
813
|
-
* @param radiusScale - Multiplier applied to `sprite.
|
|
1000
|
+
* @param radiusScale - Multiplier applied to `sprite.displaySize` to define the hit radius.
|
|
814
1001
|
* Default: `0.5`.
|
|
815
1002
|
* @returns `true` if any currently held pointer overlaps the sprite hit area.
|
|
816
1003
|
*
|
|
@@ -840,14 +1027,14 @@ export class Game {
|
|
|
840
1027
|
* sprite. Works with both mouse input and multiple simultaneous touches.
|
|
841
1028
|
*
|
|
842
1029
|
* Pointer hit testing uses a circular area centered on the sprite. The radius
|
|
843
|
-
* is `sprite.
|
|
1030
|
+
* is `sprite.displaySize * radiusScale`. World-space sprites are tested against the
|
|
844
1031
|
* current camera scroll. HUD sprites with `ignoreScroll = true` are tested in
|
|
845
1032
|
* screen space.
|
|
846
1033
|
*
|
|
847
1034
|
* Use this for one-shot virtual buttons such as menu taps.
|
|
848
1035
|
*
|
|
849
1036
|
* @param sprite - Target sprite to test. If `null` / `undefined`, returns `false`.
|
|
850
|
-
* @param radiusScale - Multiplier applied to `sprite.
|
|
1037
|
+
* @param radiusScale - Multiplier applied to `sprite.displaySize` to define the hit radius.
|
|
851
1038
|
* Default: `0.5`.
|
|
852
1039
|
* @returns `true` if any pointer began pressing this frame over the sprite hit area.
|
|
853
1040
|
*
|
|
@@ -890,7 +1077,7 @@ export class Game {
|
|
|
890
1077
|
* const pointers = game.getPointers();
|
|
891
1078
|
* if (pointers.length > 0) {
|
|
892
1079
|
* const first = pointers[0];
|
|
893
|
-
* game.
|
|
1080
|
+
* game.drawText(`Pointer: ${first.x}, ${first.y}`, 10, 10, 14);
|
|
894
1081
|
* }
|
|
895
1082
|
* ```
|
|
896
1083
|
*/
|
|
@@ -902,7 +1089,7 @@ export class Game {
|
|
|
902
1089
|
* the target sprite.
|
|
903
1090
|
*
|
|
904
1091
|
* Pointer hit testing uses a circular area centered on the sprite. The radius
|
|
905
|
-
* is `sprite.
|
|
1092
|
+
* is `sprite.displaySize * radiusScale`. World-space sprites are tested against the
|
|
906
1093
|
* current camera scroll. HUD sprites with `ignoreScroll = true` are tested in
|
|
907
1094
|
* screen space.
|
|
908
1095
|
*
|
|
@@ -910,7 +1097,7 @@ export class Game {
|
|
|
910
1097
|
* pointer position over a virtual joystick or draggable control.
|
|
911
1098
|
*
|
|
912
1099
|
* @param sprite - Target sprite to test. If `null` / `undefined`, returns an empty array.
|
|
913
|
-
* @param radiusScale - Multiplier applied to `sprite.
|
|
1100
|
+
* @param radiusScale - Multiplier applied to `sprite.displaySize` to define the hit radius.
|
|
914
1101
|
* Default: `0.5`.
|
|
915
1102
|
* @returns A read-only array of active pointer snapshots currently over the sprite.
|
|
916
1103
|
*
|
|
@@ -1142,8 +1329,11 @@ export class Game {
|
|
|
1142
1329
|
* The text overlay list is cleared after each render. Call this inside `onUpdate`.
|
|
1143
1330
|
*
|
|
1144
1331
|
* **Layer:** Text is always drawn on top of all sprites.
|
|
1145
|
-
* **Font:** Text
|
|
1146
|
-
*
|
|
1332
|
+
* **Font:** Text uses `"Press Start 2P", monospace`.
|
|
1333
|
+
* You MUST load `Press Start 2P` yourself in `index.html` (for example via
|
|
1334
|
+
* Google Fonts) before calling {@link Game.start}. MinimoJS does NOT load
|
|
1335
|
+
* external fonts for you. If the font is unavailable, the browser falls back
|
|
1336
|
+
* to `monospace`. Font family cannot be customized in MinimoJS v1.
|
|
1147
1337
|
*
|
|
1148
1338
|
* @param text - The string to render. Supports emoji and Unicode.
|
|
1149
1339
|
* @param x - X position in **screen space** (pixels from canvas left edge).
|
|
@@ -1169,6 +1359,24 @@ export class Game {
|
|
|
1169
1359
|
// -------------------------------------------------------------------------
|
|
1170
1360
|
// Misc
|
|
1171
1361
|
// -------------------------------------------------------------------------
|
|
1362
|
+
/**
|
|
1363
|
+
* Clears the internal sprite glyph cache.
|
|
1364
|
+
*
|
|
1365
|
+
* MinimoJS prerenders sprite glyphs into offscreen canvases for more stable
|
|
1366
|
+
* emoji rendering and better performance. In long-running sessions, you can
|
|
1367
|
+
* call this to release cached glyph variants and force them to be rebuilt on
|
|
1368
|
+
* the next render.
|
|
1369
|
+
*
|
|
1370
|
+
* This does not change any sprite state. It only clears cached render data.
|
|
1371
|
+
*
|
|
1372
|
+
* @example
|
|
1373
|
+
* ```ts
|
|
1374
|
+
* game.clearSpriteCache();
|
|
1375
|
+
* ```
|
|
1376
|
+
*/
|
|
1377
|
+
clearSpriteCache() {
|
|
1378
|
+
this._spriteGlyphCache.clear();
|
|
1379
|
+
}
|
|
1172
1380
|
/**
|
|
1173
1381
|
* Returns a pseudo-random floating-point number in the range `[0, 1)`.
|
|
1174
1382
|
* Delegates to `Math.random()`.
|
|
@@ -1219,8 +1427,7 @@ export class Game {
|
|
|
1219
1427
|
*
|
|
1220
1428
|
* game.onCreate = () => {
|
|
1221
1429
|
* // Scene init
|
|
1222
|
-
* const skull = new Sprite("💀");
|
|
1223
|
-
* skull.x = 400; skull.y = 300; skull.size = 96;
|
|
1430
|
+
* const skull = new Sprite("💀", 400, 300, 96);
|
|
1224
1431
|
* game.add(skull);
|
|
1225
1432
|
* game.addTimer(3000, false, () => game.reset()); // auto-restart
|
|
1226
1433
|
* };
|
|
@@ -1387,7 +1594,7 @@ export class Game {
|
|
|
1387
1594
|
/** @internal */
|
|
1388
1595
|
_isScreenPointOverSprite(x, y, sprite, radiusScale) {
|
|
1389
1596
|
const safeScale = Math.max(0, radiusScale);
|
|
1390
|
-
const radius = sprite.
|
|
1597
|
+
const radius = sprite.displaySize * safeScale;
|
|
1391
1598
|
const drawX = sprite.ignoreScroll ? sprite.x : sprite.x - this.scrollX;
|
|
1392
1599
|
const drawY = sprite.ignoreScroll ? sprite.y : sprite.y - this.scrollY;
|
|
1393
1600
|
const dx = x - drawX;
|
|
@@ -1448,6 +1655,78 @@ export class Game {
|
|
|
1448
1655
|
this._pointerDown = this._mouseDown;
|
|
1449
1656
|
this._pointerPressed = this._mousePressed;
|
|
1450
1657
|
}
|
|
1658
|
+
/** @internal */
|
|
1659
|
+
_assertPhysicsEnabled() {
|
|
1660
|
+
if (!this.physics) {
|
|
1661
|
+
throw new Error("MinimoJS: collide() and collideAny() require game.physics = true.");
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
/** @internal */
|
|
1665
|
+
_getCollisionResolution(a, b) {
|
|
1666
|
+
const halfA = a.displaySize / 2;
|
|
1667
|
+
const halfB = b.displaySize / 2;
|
|
1668
|
+
const dx = b.x - a.x;
|
|
1669
|
+
const dy = b.y - a.y;
|
|
1670
|
+
const overlapX = halfA + halfB - Math.abs(dx);
|
|
1671
|
+
const overlapY = halfA + halfB - Math.abs(dy);
|
|
1672
|
+
if (overlapX <= 0 || overlapY <= 0) {
|
|
1673
|
+
return null;
|
|
1674
|
+
}
|
|
1675
|
+
let axis;
|
|
1676
|
+
if (overlapX < overlapY) {
|
|
1677
|
+
axis = "x";
|
|
1678
|
+
}
|
|
1679
|
+
else if (overlapY < overlapX) {
|
|
1680
|
+
axis = "y";
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1683
|
+
const relVX = Math.abs(a.vx - b.vx);
|
|
1684
|
+
const relVY = Math.abs(a.vy - b.vy);
|
|
1685
|
+
axis = relVX > relVY ? "x" : "y";
|
|
1686
|
+
}
|
|
1687
|
+
const info = {
|
|
1688
|
+
left: false,
|
|
1689
|
+
right: false,
|
|
1690
|
+
top: false,
|
|
1691
|
+
bottom: false,
|
|
1692
|
+
grounded: false,
|
|
1693
|
+
};
|
|
1694
|
+
if (axis === "x") {
|
|
1695
|
+
if (dx >= 0) {
|
|
1696
|
+
info.right = true;
|
|
1697
|
+
return {
|
|
1698
|
+
info,
|
|
1699
|
+
axis,
|
|
1700
|
+
separationX: -overlapX,
|
|
1701
|
+
separationY: 0,
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
info.left = true;
|
|
1705
|
+
return {
|
|
1706
|
+
info,
|
|
1707
|
+
axis,
|
|
1708
|
+
separationX: overlapX,
|
|
1709
|
+
separationY: 0,
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
if (dy >= 0) {
|
|
1713
|
+
info.bottom = true;
|
|
1714
|
+
info.grounded = true;
|
|
1715
|
+
return {
|
|
1716
|
+
info,
|
|
1717
|
+
axis,
|
|
1718
|
+
separationX: 0,
|
|
1719
|
+
separationY: -overlapY,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
info.top = true;
|
|
1723
|
+
return {
|
|
1724
|
+
info,
|
|
1725
|
+
axis,
|
|
1726
|
+
separationX: 0,
|
|
1727
|
+
separationY: overlapY,
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1451
1730
|
// -------------------------------------------------------------------------
|
|
1452
1731
|
// Private — rAF loop
|
|
1453
1732
|
// -------------------------------------------------------------------------
|
|
@@ -1521,6 +1800,9 @@ export class Game {
|
|
|
1521
1800
|
/** @internal */
|
|
1522
1801
|
_updatePhysics(dt) {
|
|
1523
1802
|
for (const sprite of this._sprites) {
|
|
1803
|
+
if (sprite.isStatic) {
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1524
1806
|
if (sprite.gravityScale !== 0) {
|
|
1525
1807
|
sprite.vx += this.gravityX * sprite.gravityScale * dt;
|
|
1526
1808
|
sprite.vy += this.gravityY * sprite.gravityScale * dt;
|
|
@@ -1535,7 +1817,8 @@ export class Game {
|
|
|
1535
1817
|
/** @internal */
|
|
1536
1818
|
_getSpriteGlyphCanvas(sprite) {
|
|
1537
1819
|
const size = Math.max(1, Math.round(sprite.size));
|
|
1538
|
-
const
|
|
1820
|
+
const color = sprite.color;
|
|
1821
|
+
const cacheKey = `${sprite.sprite}::${size}::${color}`;
|
|
1539
1822
|
const cached = this._spriteGlyphCache.get(cacheKey);
|
|
1540
1823
|
if (cached)
|
|
1541
1824
|
return cached;
|
|
@@ -1552,7 +1835,7 @@ export class Game {
|
|
|
1552
1835
|
glyphCtx.shadowBlur = 0;
|
|
1553
1836
|
glyphCtx.shadowOffsetX = 0;
|
|
1554
1837
|
glyphCtx.shadowOffsetY = 0;
|
|
1555
|
-
glyphCtx.fillStyle =
|
|
1838
|
+
glyphCtx.fillStyle = color;
|
|
1556
1839
|
glyphCtx.font = `${size}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
|
1557
1840
|
glyphCtx.textAlign = "center";
|
|
1558
1841
|
glyphCtx.textBaseline = "middle";
|
|
@@ -1590,8 +1873,11 @@ export class Game {
|
|
|
1590
1873
|
if (sprite.rotation !== 0) {
|
|
1591
1874
|
ctx.rotate((sprite.rotation * Math.PI) / 180);
|
|
1592
1875
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1876
|
+
const safeRenderScale = Number.isFinite(sprite.scale)
|
|
1877
|
+
? Math.max(0, sprite.scale)
|
|
1878
|
+
: 1;
|
|
1879
|
+
if (sprite.flipX || sprite.flipY || safeRenderScale !== 1) {
|
|
1880
|
+
ctx.scale((sprite.flipX ? -1 : 1) * safeRenderScale, (sprite.flipY ? -1 : 1) * safeRenderScale);
|
|
1595
1881
|
}
|
|
1596
1882
|
ctx.shadowColor = "transparent";
|
|
1597
1883
|
ctx.shadowBlur = 0;
|
|
@@ -1603,7 +1889,7 @@ export class Game {
|
|
|
1603
1889
|
}
|
|
1604
1890
|
for (const entry of this._textOverlays) {
|
|
1605
1891
|
ctx.save();
|
|
1606
|
-
ctx.font = `${entry.fontSize}px monospace`;
|
|
1892
|
+
ctx.font = `${entry.fontSize}px "Press Start 2P", monospace`;
|
|
1607
1893
|
ctx.fillStyle = entry.color;
|
|
1608
1894
|
ctx.textAlign = entry.centered ? "center" : "left";
|
|
1609
1895
|
ctx.textBaseline = entry.centered ? "middle" : "top";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimojs",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.4",
|
|
4
4
|
"description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Emoji-only sprites, rAF loop, TypeScript-first, LLM-friendly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/minimo.js",
|