minimojs 1.0.0-alpha.27 → 1.0.0-alpha.29

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/minimo.js CHANGED
@@ -22,6 +22,8 @@ import { TextSystem } from "./internal/TextSystem.js";
22
22
  import { TrailSystem } from "./internal/TrailSystem.js";
23
23
  import { TransitionSystem } from "./internal/TransitionSystem.js";
24
24
  import { TimerSystem } from "./internal/TimerSystem.js";
25
+ import { Design, findFirst, parseAssetFile, parseDesignObject } from "./minimo-format.js";
26
+ export { Design, parseAssetFile } from "./minimo-format.js";
25
27
  /** How long a scene may load before it dims and its progress ring appears, so a fast load shows neither. */
26
28
  const SCENE_LOADING_INDICATOR_DELAY_MS = 250;
27
29
  /** How long the dimming and the ring take to fade in once they appear. */
@@ -172,6 +174,35 @@ export class Sprite {
172
174
  * Values > 1 amplify gravity; negative values invert it.
173
175
  */
174
176
  this.gravityScale = 0;
177
+ /**
178
+ * Optional name, so code can ask an object what it is. Names may repeat,
179
+ * like a `name` attribute in the DOM. The engine never reads it.
180
+ * A sprite created from a design file takes the object's `name`.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * if (other.name === "door") openDoor();
185
+ * ```
186
+ */
187
+ this.name = undefined;
188
+ /**
189
+ * Tags for grouping, like CSS classes in the DOM. Empty by default. The
190
+ * engine never reads them. A sprite created from a design file takes the
191
+ * object's `tags`, which {@link Game.registerBuilderByTag} also matches.
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * if (other.tags.includes("coin")) collect(other);
196
+ * ```
197
+ */
198
+ this.tags = [];
199
+ /**
200
+ * The game's own values for this object, such as a button's action or an
201
+ * enemy's speed. An empty object by default, so `sprite.data.hp = 3` needs
202
+ * no check. The engine never reads it. A sprite created from a design file
203
+ * takes a copy of the object's `data`.
204
+ */
205
+ this.data = {};
175
206
  // `abstract` and `protected` are erased by the TypeScript compiler, and games
176
207
  // are written in JavaScript, so nothing but this check stops old code from
177
208
  // calling `new Sprite(glyph, x, y, size)`. Without it the glyph would be
@@ -823,6 +854,21 @@ export class BackgroundLayer {
823
854
  * Default: `"cover"`.
824
855
  */
825
856
  this.fit = "cover";
857
+ /**
858
+ * Optional name, so code can ask a layer what it is. Names may repeat. The
859
+ * engine never reads it. See {@link Sprite.name}.
860
+ */
861
+ this.name = undefined;
862
+ /**
863
+ * Tags for grouping. Empty by default. The engine never reads them. See
864
+ * {@link Sprite.tags}.
865
+ */
866
+ this.tags = [];
867
+ /**
868
+ * The game's own values for this layer. An empty object by default. The
869
+ * engine never reads it. See {@link Sprite.data}.
870
+ */
871
+ this.data = {};
826
872
  this.imageKey = imageKey;
827
873
  this.x = x;
828
874
  this.y = y;
@@ -830,6 +876,67 @@ export class BackgroundLayer {
830
876
  this.height = height;
831
877
  }
832
878
  }
879
+ /**
880
+ * The objects {@link Game.createObjectList} created, in the order of their
881
+ * data, and searches on them.
882
+ *
883
+ * An `ObjectList` only holds and finds its objects; it never changes them.
884
+ * Remove them through the game, as any other object:
885
+ *
886
+ * ```ts
887
+ * for (const obj of ui.objects) {
888
+ * if (obj instanceof BackgroundLayer) game.destroyBackgroundLayer(obj);
889
+ * else game.destroySprite(obj);
890
+ * }
891
+ * ```
892
+ *
893
+ * Searches read each object's current {@link Sprite.name} and
894
+ * {@link Sprite.tags}. In TypeScript, name the type you expect:
895
+ * `ui.findByName<ImageSprite>("play")`.
896
+ */
897
+ export class ObjectList {
898
+ /** @internal */
899
+ constructor(objects) {
900
+ this.objects = Object.freeze([...objects]);
901
+ }
902
+ /**
903
+ * Returns the first object whose `name` is `name`, in order.
904
+ * Throws an error naming `name` when there is none.
905
+ *
906
+ * @example
907
+ * ```ts
908
+ * const play = ui.findByName("play");
909
+ * ```
910
+ */
911
+ findByName(name) {
912
+ return findFirst(this.objects, (obj) => obj.name === name, `named "${name}"`, "the object list");
913
+ }
914
+ /** Returns every object whose `name` is `name`, in order; an empty list when there is none. */
915
+ findAllByName(name) {
916
+ return this.objects.filter((obj) => obj.name === name);
917
+ }
918
+ /**
919
+ * Returns the first object whose `tags` include `tag`, in order.
920
+ * Throws an error naming `tag` when there is none.
921
+ */
922
+ findByTag(tag) {
923
+ return findFirst(this.objects, (obj) => obj.tags.includes(tag), `tagged "${tag}"`, "the object list");
924
+ }
925
+ /** Returns every object whose `tags` include `tag`, in order; an empty list when there is none. */
926
+ findAllByTag(tag) {
927
+ return this.objects.filter((obj) => obj.tags.includes(tag));
928
+ }
929
+ }
930
+ /** The runtime class of each type of design object. */
931
+ const DESIGN_OBJECT_CLASSES = {
932
+ image: ImageSprite,
933
+ emoji: EmojiSprite,
934
+ text: TextSprite,
935
+ background: BackgroundLayer,
936
+ };
937
+ function describeDesignObject(obj) {
938
+ return obj.name === undefined ? `the object ${obj.id}` : `the object "${obj.name}" (${obj.id})`;
939
+ }
833
940
  // ---------------------------------------------------------------------------
834
941
  // Internal types (not part of the public API declarations)
835
942
  // ---------------------------------------------------------------------------
@@ -1267,6 +1374,7 @@ export class Game {
1267
1374
  /** @internal */ this._sceneLoad = null;
1268
1375
  /** @internal */ this._onCreate = null;
1269
1376
  /** @internal */ this._currentScene = null;
1377
+ /** @internal */ this._buildersByTag = new Map();
1270
1378
  /**
1271
1379
  * Horizontal scroll offset of the world camera, in pixels.
1272
1380
  * The canvas viewport is shifted left by `scrollX` — sprites with higher `x`
@@ -1341,7 +1449,7 @@ export class Game {
1341
1449
  * Asset registration callback invoked once before the first scene is created.
1342
1450
  *
1343
1451
  * This is the normal place to load a game's images. Use it to register them
1344
- * with {@link Game.loadImage}. The callback is synchronous: it should only
1452
+ * with {@link Game.loadAssets} or {@link Game.loadImage}. The callback is synchronous: it should only
1345
1453
  * queue assets, not await network work.
1346
1454
  *
1347
1455
  * After `onPreload` returns, MinimoJS loads the queued images, together with
@@ -1429,6 +1537,11 @@ export class Game {
1429
1537
  * in pixels. Updated every `mousemove` / `touchmove` event.
1430
1538
  * `(0, 0)` is the top-left corner of the canvas.
1431
1539
  * To convert to world space: `worldX = game.pointerX + game.scrollX`.
1540
+ *
1541
+ * On a touch screen the pointer is one finger: the first to touch while no
1542
+ * finger is followed. Other fingers never move it, and when the followed
1543
+ * finger lifts, the pointer stays where it left the screen even if others
1544
+ * are still down. Use {@link Game.getPointers} to follow several fingers.
1432
1545
  */
1433
1546
  get pointerX() {
1434
1547
  return this._inputSystem.pointerX;
@@ -1584,6 +1697,48 @@ export class Game {
1584
1697
  }
1585
1698
  this._assetSystem.queueImage(key, src);
1586
1699
  }
1700
+ /**
1701
+ * Queues every asset an asset file declares, as one {@link Game.loadImage}
1702
+ * call per `image` entry would.
1703
+ *
1704
+ * Pass the content of a `*.assets.json` file, obtained by JSON import. The
1705
+ * file is checked with {@link parseAssetFile}, and an invalid one throws an
1706
+ * error naming what is wrong. Like `loadImage`, `loadAssets` may only be
1707
+ * called from within {@link Game.onPreload} or a scene's
1708
+ * {@link IScene.onPreload}.
1709
+ *
1710
+ * - Keys are unique in the whole game. A key that another asset file
1711
+ * declares, or that {@link Game.loadImage} or {@link Game.createTexture}
1712
+ * registered, throws an error naming it, and so does a later
1713
+ * `loadImage` or `createTexture` of a key an asset file declares.
1714
+ * - Loading the same file again, as a scene's `onPreload()` does each time
1715
+ * the scene is entered, queues only its images that are not loaded.
1716
+ * - An entry of a type this engine does not support is skipped, with a
1717
+ * console warning naming it, so the game still loads every asset it can.
1718
+ *
1719
+ * @param assets - The content of a `*.assets.json` file. See {@link AssetFile}.
1720
+ *
1721
+ * @example
1722
+ * ```ts
1723
+ * import common from "./common.assets.json" with { type: "json" };
1724
+ *
1725
+ * game.onPreload = () => game.loadAssets(common);
1726
+ * ```
1727
+ */
1728
+ loadAssets(assets) {
1729
+ if (!this._isRegisteringPreloadAssets) {
1730
+ throw new Error("MinimoJS: loadAssets() may only be called inside game.onPreload or a scene's onPreload().");
1731
+ }
1732
+ const file = parseAssetFile(assets);
1733
+ const isFirstLoad = this._assetSystem.registerAssetFile(file, assets);
1734
+ if (!isFirstLoad)
1735
+ return;
1736
+ for (const entry of file.assets) {
1737
+ if (entry.type !== "image") {
1738
+ console.warn(`MinimoJS: Skipped the asset "${entry.key}", whose type "${entry.type}" this version of MinimoJS does not support.`);
1739
+ }
1740
+ }
1741
+ }
1587
1742
  /**
1588
1743
  * Creates a dynamic texture immediately and registers it under a stable key.
1589
1744
  *
@@ -1631,6 +1786,202 @@ export class Game {
1631
1786
  return this._assetSystem.hasImage(key);
1632
1787
  }
1633
1788
  // -------------------------------------------------------------------------
1789
+ // Designs
1790
+ // -------------------------------------------------------------------------
1791
+ /**
1792
+ * Creates one runtime object from object data, such as a template found in
1793
+ * a {@link Design}, and adds it to the game.
1794
+ *
1795
+ * The object is built through the same public constructors and
1796
+ * {@link Game.add} or {@link Game.addBackground} that code uses, so it
1797
+ * behaves as the equivalent code. Its type picks its class: `image` builds
1798
+ * an {@link ImageSprite}, `emoji` an {@link EmojiSprite}, `text` a
1799
+ * {@link TextSprite}, and `background` a {@link BackgroundLayer}, unless a
1800
+ * builder registered with {@link Game.registerBuilderByTag} matches one of
1801
+ * its tags. Then every property the data holds is applied, including
1802
+ * `name`, `tags`, and a copy of `data`.
1803
+ *
1804
+ * Each call creates a new object, so one template can be created any number
1805
+ * of times. Set its position afterwards to place it; the object is never
1806
+ * drawn at the template's position.
1807
+ *
1808
+ * Throws an error naming what is wrong when the data is not a valid design
1809
+ * object, or naming the key when it uses an image key that is not loaded.
1810
+ *
1811
+ * @param data - One design object, such as `design.findByName("player")`. Not a `Design`.
1812
+ * @param options - `add: false` creates the object without adding it to the game. Default: added.
1813
+ * @returns The created object. In TypeScript, name its type: `game.createObject<ImageSprite>(data)`.
1814
+ *
1815
+ * @example
1816
+ * ```ts
1817
+ * import charactersJson from "./characters.design.json" with { type: "json" };
1818
+ * const characters = new Design(charactersJson);
1819
+ *
1820
+ * const bat = game.createObject(characters.findByName("bat"));
1821
+ * bat.x = spawn.x;
1822
+ * bat.y = spawn.y;
1823
+ * ```
1824
+ */
1825
+ createObject(data, options = {}) {
1826
+ if (data instanceof Design) {
1827
+ throw new Error("MinimoJS: createObject() takes one object's data, not a Design. Pass a search's result, such as design.findByName(\"player\"), or use createObjectList(design.objects).");
1828
+ }
1829
+ const obj = parseDesignObject(data, "the object", "design object");
1830
+ this._assertDesignImageLoaded(obj);
1831
+ const created = this._buildDesignObject(obj);
1832
+ if (options.add !== false)
1833
+ this._addDesignObject(created);
1834
+ return created;
1835
+ }
1836
+ /**
1837
+ * Creates a runtime object from each object data, in order, and adds them
1838
+ * to the game, as {@link Game.createObject} does for one. Objects in the
1839
+ * same layer draw in the order of the data.
1840
+ *
1841
+ * Every object is checked before any is created, so invalid data or an
1842
+ * image key that is not loaded creates nothing.
1843
+ *
1844
+ * @param objects - Object data, such as `design.objects` or `design.findAllByTag("enemy")`. Not a `Design`.
1845
+ * @param options - `add: false` creates the objects without adding them to the game. Default: added.
1846
+ * @returns An {@link ObjectList} of the created objects, to find them by name or tag.
1847
+ *
1848
+ * @example
1849
+ * ```ts
1850
+ * import menuJson from "./menu.design.json" with { type: "json" };
1851
+ * const menu = new Design(menuJson);
1852
+ *
1853
+ * class MenuScene {
1854
+ * onCreate() {
1855
+ * const ui = game.createObjectList(menu.objects);
1856
+ * this.play = ui.findByName("play");
1857
+ * }
1858
+ * onUpdate() {
1859
+ * if (game.isPointerPressedOverSprite(this.play)) start(this.play.data.action);
1860
+ * }
1861
+ * }
1862
+ * ```
1863
+ */
1864
+ createObjectList(objects, options = {}) {
1865
+ if (!Array.isArray(objects)) {
1866
+ throw new Error("MinimoJS: createObjectList() takes a list of object data, such as design.objects or design.findAllByTag(\"enemy\").");
1867
+ }
1868
+ const parsed = objects.map((data, index) => parseDesignObject(data, `objects[${index}]`, "design object"));
1869
+ for (const obj of parsed)
1870
+ this._assertDesignImageLoaded(obj);
1871
+ const created = parsed.map((obj) => this._buildDesignObject(obj));
1872
+ if (options.add !== false) {
1873
+ for (const obj of created)
1874
+ this._addDesignObject(obj);
1875
+ }
1876
+ return new ObjectList(created);
1877
+ }
1878
+ /**
1879
+ * Makes {@link Game.createObject} and {@link Game.createObjectList} build
1880
+ * the design objects tagged `tag` with `builder`, such as an `Enemy` class
1881
+ * of the game's own.
1882
+ *
1883
+ * - The builder returns an instance of the object's type class or a
1884
+ * subclass of it: {@link ImageSprite} for `image`, {@link EmojiSprite} for
1885
+ * `emoji`, {@link TextSprite} for `text`, and {@link BackgroundLayer} for
1886
+ * `background`. Anything else throws an error naming the object.
1887
+ * - The engine then applies every property the data holds, so the file wins
1888
+ * over the constructor. Properties the data omits keep what the builder set.
1889
+ * An `emoji` object's `size` cannot be changed after construction, so a
1890
+ * builder must pass it on, or the creation throws.
1891
+ * - An object matching the builders of two of its tags throws an error
1892
+ * naming the object and both tags.
1893
+ * - Builders are scene state: {@link Game.reset} clears them, as it clears
1894
+ * timers, so each scene registers its own in `onCreate()`.
1895
+ *
1896
+ * @param tag - The tag whose objects the builder builds.
1897
+ * @param builder - Receives the object's data and the game, and returns the object, not yet added.
1898
+ *
1899
+ * @example
1900
+ * ```ts
1901
+ * class Enemy extends ImageSprite {
1902
+ * update(dt) { this.x += this.data.speed * dt; }
1903
+ * }
1904
+ *
1905
+ * class Level1Scene {
1906
+ * onCreate() {
1907
+ * game.registerBuilderByTag("enemy", (obj, game) => new Enemy(game, obj.imageKey));
1908
+ * this.enemies = game.createObjectList(level1.findAllByTag("enemy")).objects;
1909
+ * }
1910
+ * }
1911
+ * ```
1912
+ */
1913
+ registerBuilderByTag(tag, builder) {
1914
+ if (typeof tag !== "string" || tag.length === 0) {
1915
+ throw new Error("MinimoJS: registerBuilderByTag() requires a non-empty tag.");
1916
+ }
1917
+ if (typeof builder !== "function") {
1918
+ throw new Error(`MinimoJS: registerBuilderByTag("${tag}") requires a builder function.`);
1919
+ }
1920
+ if (this._buildersByTag.has(tag)) {
1921
+ throw new Error(`MinimoJS: A builder is already registered for the tag "${tag}". game.reset() clears builders, so register each in a scene's onCreate().`);
1922
+ }
1923
+ this._buildersByTag.set(tag, builder);
1924
+ }
1925
+ /** @internal */
1926
+ _assertDesignImageLoaded(obj) {
1927
+ if ((obj.type === "image" || obj.type === "background") && !this.hasImage(obj.imageKey)) {
1928
+ throw new Error(`MinimoJS: ${describeDesignObject(obj)} uses the image key "${obj.imageKey}", which is not loaded. Declare it in an asset file and load that file with game.loadAssets() in an onPreload hook.`);
1929
+ }
1930
+ }
1931
+ /** @internal */
1932
+ _buildDesignObject(obj) {
1933
+ const matchingTags = [...new Set(obj.tags ?? [])].filter((tag) => this._buildersByTag.has(tag));
1934
+ if (matchingTags.length > 1) {
1935
+ throw new Error(`MinimoJS: ${describeDesignObject(obj)} matches the builders of two of its tags, "${matchingTags[0]}" and "${matchingTags[1]}". An object may match at most one builder.`);
1936
+ }
1937
+ const tag = matchingTags[0];
1938
+ const builder = tag === undefined ? undefined : this._buildersByTag.get(tag);
1939
+ let created;
1940
+ if (builder === undefined) {
1941
+ created =
1942
+ obj.type === "image"
1943
+ ? new ImageSprite(this, obj.imageKey)
1944
+ : obj.type === "emoji"
1945
+ ? new EmojiSprite(obj.sprite, 0, 0, obj.size)
1946
+ : obj.type === "text"
1947
+ ? new TextSprite(obj.text, 0, 0, obj.fontSize)
1948
+ : new BackgroundLayer(obj.imageKey);
1949
+ }
1950
+ else {
1951
+ created = builder(obj, this);
1952
+ const expected = DESIGN_OBJECT_CLASSES[obj.type];
1953
+ if (!(created instanceof expected)) {
1954
+ throw new Error(`MinimoJS: The builder for the tag "${tag}" did not return an instance of ${expected.name} or of a subclass of it for ${describeDesignObject(obj)}, whose type is "${obj.type}".`);
1955
+ }
1956
+ if (obj.type === "emoji" && obj.size !== undefined && created.size !== obj.size) {
1957
+ throw new Error(`MinimoJS: The builder for the tag "${tag}" built ${describeDesignObject(obj)} with the size ${created.size}, but its data says ${obj.size}. An EmojiSprite's size is set only by its constructor, so pass obj.size to it.`);
1958
+ }
1959
+ }
1960
+ const target = created;
1961
+ for (const [field, value] of Object.entries(obj)) {
1962
+ if (field === "id" || field === "type")
1963
+ continue;
1964
+ if (field === "size" && created instanceof EmojiSprite)
1965
+ continue;
1966
+ if (field === "imageKey" && created instanceof ImageSprite) {
1967
+ created.setTexture(value);
1968
+ continue;
1969
+ }
1970
+ target[field] = value;
1971
+ }
1972
+ return created;
1973
+ }
1974
+ /** @internal */
1975
+ _addDesignObject(obj) {
1976
+ if (obj instanceof BackgroundLayer) {
1977
+ if (!this._backgroundSystem.getMutableLayers().includes(obj))
1978
+ this.addBackground(obj);
1979
+ }
1980
+ else if (!this._spriteSystem.getMutableSprites().includes(obj)) {
1981
+ this.add(obj);
1982
+ }
1983
+ }
1984
+ // -------------------------------------------------------------------------
1634
1985
  // Collision
1635
1986
  // -------------------------------------------------------------------------
1636
1987
  /**
@@ -1788,6 +2139,8 @@ export class Game {
1788
2139
  * Use this for continuous pointer actions.
1789
2140
  *
1790
2141
  * **Hold behavior:** returns `true` every frame from press until release.
2142
+ * On a touch screen it follows one finger, as {@link Game.pointerX} does,
2143
+ * so it turns `false` when that finger lifts, even if another is down.
1791
2144
  *
1792
2145
  * @returns `true` if the pointer is currently pressed.
1793
2146
  *
@@ -2649,6 +3002,7 @@ export class Game {
2649
3002
  this._trailSystem.clearAll();
2650
3003
  this._textSystem.clear();
2651
3004
  this._inputSystem.resetPressedState();
3005
+ this._buildersByTag.clear();
2652
3006
  this.scrollX = 0;
2653
3007
  this.scrollY = 0;
2654
3008
  this._loopSystem.invokeCreate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimojs",
3
- "version": "1.0.0-alpha.27",
3
+ "version": "1.0.0-alpha.29",
4
4
  "description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Image-first sprites with emoji as a zero-asset alternative, rAF loop, TypeScript-first, LLM-friendly.",
5
5
  "type": "module",
6
6
  "main": "dist/minimo.js",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "import": "./dist/minimo.js",
11
11
  "types": "./dist/minimo.d.ts"
12
+ },
13
+ "./format": {
14
+ "import": "./dist/minimo-format.js",
15
+ "types": "./dist/minimo-format.d.ts"
12
16
  }
13
17
  },
14
18
  "files": [
@@ -21,7 +25,8 @@
21
25
  "clean": "rm -rf dist",
22
26
  "rebuild": "npm run clean && npm run build",
23
27
  "prepack": "npm run build",
24
- "pack:check": "npm pack --dry-run"
28
+ "pack:check": "npm pack --dry-run",
29
+ "test": "npm run build && node --test \"test/*.test.mjs\""
25
30
  },
26
31
  "keywords": [
27
32
  "game",