From acc4b1790b52f2a4af726f755879b8fa1bf7e8e5 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Fri, 3 Apr 2026 07:47:15 +0800 Subject: [PATCH 1/6] Remove game import from Container, default dimensions to Infinity Container constructor no longer imports or references the global game singleton. Default width/height changed from game.viewport dimensions to Infinity (no clipping). This is the same effective behavior since all actual Container creations pass explicit dimensions. Updated JSDoc to document Infinity defaults. Added tests: default dimensions, position defaults, explicit dimensions, children with Infinity container, no-clip behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/melonjs/src/renderable/container.js | 22 +++--------- packages/melonjs/tests/container.spec.js | 38 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/packages/melonjs/src/renderable/container.js b/packages/melonjs/src/renderable/container.js index c13fbfbb6..87fc8590f 100644 --- a/packages/melonjs/src/renderable/container.js +++ b/packages/melonjs/src/renderable/container.js @@ -1,4 +1,3 @@ -import { game } from "../application/application.ts"; import { colorPool } from "../math/color.ts"; import Body from "../physics/body.js"; import state from "../state/state.ts"; @@ -49,25 +48,12 @@ export default class Container extends Renderable { /** * @param {number} [x=0] - position of the container (accessible via the inherited pos.x property) * @param {number} [y=0] - position of the container (accessible via the inherited pos.y property) - * @param {number} [width=game.viewport.width] - width of the container - * @param {number} [height=game.viewport.height] - height of the container + * @param {number} [width=Infinity] - width of the container (Infinity means no clipping) + * @param {number} [height=Infinity] - height of the container (Infinity means no clipping) */ - constructor(x = 0, y = 0, width, height, root = false) { + constructor(x = 0, y = 0, width = Infinity, height = Infinity, root = false) { // call the super constructor - super( - x, - y, - typeof width === "undefined" - ? typeof game.viewport !== "undefined" - ? game.viewport.width - : Infinity - : width, - typeof height === "undefined" - ? typeof game.viewport !== "undefined" - ? game.viewport.height - : Infinity - : height, - ); + super(x, y, width, height); /** * keep track of pending sort diff --git a/packages/melonjs/tests/container.spec.js b/packages/melonjs/tests/container.spec.js index 8231b3865..1a4d6d981 100644 --- a/packages/melonjs/tests/container.spec.js +++ b/packages/melonjs/tests/container.spec.js @@ -970,4 +970,42 @@ describe("Container", () => { expect(child.visibleInAllCameras).toEqual(true); }); }); + + describe("default dimensions", () => { + it("should default to Infinity when no dimensions are provided", () => { + const c = new Container(); + expect(c.width).toEqual(Infinity); + expect(c.height).toEqual(Infinity); + }); + + it("should default to (0, 0) position when no position is provided", () => { + const c = new Container(); + expect(c.pos.x).toEqual(0); + expect(c.pos.y).toEqual(0); + }); + + it("should use explicit dimensions when provided", () => { + const c = new Container(10, 20, 200, 150); + expect(c.pos.x).toEqual(10); + expect(c.pos.y).toEqual(20); + expect(c.width).toEqual(200); + expect(c.height).toEqual(150); + }); + + it("should accept children when dimensions are Infinity", () => { + const c = new Container(); + const child = new Renderable(50, 50, 32, 32); + c.addChild(child); + expect(c.getChildren().length).toEqual(1); + }); + + it("should not clip children when dimensions are Infinity", () => { + const c = new Container(); + const farChild = new Renderable(99999, 99999, 10, 10); + c.addChild(farChild); + // child bounds should extend beyond any finite container + const bounds = farChild.getBounds(); + expect(bounds.left).toBeGreaterThan(0); + }); + }); }); From 963192c64de9ba7f5279d732506dee5b1d031e79 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Sat, 4 Apr 2026 11:55:28 +0800 Subject: [PATCH 2/6] Decouple internal modules from game singleton, deprecate video.init - Container: Infinity defaults, isFinite() guard in updateBounds, anchorPoint (0,0) - ImageLayer: uses parentApp for viewport/renderer, deferred to onActivateEvent - TMXTileMap: resolves viewport from container tree via getRootAncestor().app - Input: pointer/pointerevent receive app via GAME_INIT instead of game singleton - Stage: onDestroyEvent(app) now passes Application instance - video.js: DOM event listeners moved to Application.init() - video.renderer, video.init(), video.getParent(), device.onReady() deprecated - Application: improved JSDoc, DOM event bridge for resize/orientation/scroll - BitmapTextData: fix circular import (pool.ts <-> bitmaptextdata.ts) - All me.game.* JSDoc references updated to app.* - README hello world updated to use new Application() pattern - Platformer and UI examples migrated to Application entry point - New tests: container (Infinity/isFinite/clipping), ImageLayer (parentApp) Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 43 +++-- .../src/examples/platformer/createGame.ts | 47 +++--- .../examples/platformer/entities/player.ts | 28 ++-- .../examples/src/examples/platformer/play.ts | 16 +- .../examples/src/examples/ui/ExampleUI.tsx | 26 ++-- packages/melonjs/CHANGELOG.md | 8 + .../melonjs/src/application/application.ts | 61 +++++++- packages/melonjs/src/camera/camera2d.ts | 18 +-- packages/melonjs/src/input/pointer.ts | 7 +- packages/melonjs/src/input/pointerevent.ts | 35 +++-- packages/melonjs/src/level/level.js | 4 +- packages/melonjs/src/level/tiled/TMXGroup.js | 2 +- packages/melonjs/src/level/tiled/TMXLayer.js | 4 +- packages/melonjs/src/level/tiled/TMXObject.js | 2 +- .../melonjs/src/level/tiled/TMXTileMap.js | 11 +- packages/melonjs/src/particles/emitter.ts | 4 +- packages/melonjs/src/physics/collision.js | 2 +- packages/melonjs/src/physics/detector.js | 2 +- packages/melonjs/src/physics/world.js | 6 +- packages/melonjs/src/plugin/plugin.ts | 6 +- packages/melonjs/src/renderable/container.js | 24 ++- packages/melonjs/src/renderable/imagelayer.js | 29 ++-- packages/melonjs/src/renderable/renderable.js | 4 +- .../melonjs/src/renderable/text/bitmaptext.js | 2 +- .../src/renderable/text/bitmaptextdata.ts | 2 +- packages/melonjs/src/state/stage.js | 11 +- packages/melonjs/src/state/state.ts | 6 +- packages/melonjs/src/system/bootstrap.ts | 8 +- packages/melonjs/src/system/device.js | 9 +- packages/melonjs/src/system/legacy_pool.js | 4 +- packages/melonjs/src/video/video.js | 80 +++------- packages/melonjs/tests/container.spec.js | 147 +++++++++++++++++- packages/melonjs/tests/eventEmitter.spec.ts | 6 +- packages/melonjs/tests/font.spec.js | 1 - packages/melonjs/tests/imagelayer.spec.js | 83 ++++++++++ 35 files changed, 505 insertions(+), 243 deletions(-) create mode 100644 packages/melonjs/tests/imagelayer.spec.js diff --git a/README.md b/README.md index 489569478..9dd8e5346 100644 --- a/README.md +++ b/README.md @@ -163,30 +163,29 @@ Browse all examples [here](https://melonjs.github.io/melonJS/examples/) ### Basic Hello World Example ```JavaScript -import * as me from "https://cdn.jsdelivr.net/npm/melonjs/+esm"; - -me.device.onReady(function () { - // initialize the display canvas once the device/browser is ready - if (!me.video.init(1218, 562, {parent : "screen", scale : "auto"})) { - alert("Your browser does not support HTML5 canvas."); - return; - } - - // set a gray background color - me.game.world.backgroundColor.parseCSS("#202020"); - - // add a font text display object - me.game.world.addChild(new me.Text(609, 281, { - font: "Arial", - size: 160, - fillStyle: "#FFFFFF", - textBaseline : "middle", - textAlign : "center", - text : "Hello World !" - })); +import { Application, Text } from "https://cdn.jsdelivr.net/npm/melonjs/+esm"; + +// create a new melonJS application +const app = new Application(1218, 562, { + parent: "screen", + scale: "auto", + backgroundColor: "#202020", }); + +// set a gray background color +app.world.backgroundColor.parseCSS("#202020"); + +// add a font text display object +app.world.addChild(new Text(609, 281, { + font: "Arial", + size: 160, + fillStyle: "#FFFFFF", + textBaseline: "middle", + textAlign: "center", + text: "Hello World !", +})); ``` -> Simple hello world using melonJS 2 +> Simple hello world using melonJS Documentation ------------------------------------------------------------------------------- diff --git a/packages/examples/src/examples/platformer/createGame.ts b/packages/examples/src/examples/platformer/createGame.ts index 136c5e468..1cd697599 100644 --- a/packages/examples/src/examples/platformer/createGame.ts +++ b/packages/examples/src/examples/platformer/createGame.ts @@ -1,5 +1,6 @@ import { DebugPanelPlugin } from "@melonjs/debug-plugin"; import { + Application, audio, device, event, @@ -9,7 +10,6 @@ import { pool, state, TextureAtlas, - video, } from "melonjs"; import { CoinEntity } from "./entities/coin.js"; import { FlyEnemyEntity, SlimeEnemyEntity } from "./entities/enemies.js"; @@ -19,63 +19,52 @@ import { PlayScreen } from "./play.js"; import { resources } from "./resources.js"; export const createGame = () => { - // init the video - if ( - !video.init(800, 600, { - parent: "screen", - scaleMethod: "flex-width", - renderer: video.AUTO, - preferWebGL1: false, - depthTest: "z-buffer", - subPixel: false, - }) - ) { - alert("Your browser does not support HTML5 canvas."); - return; - } + // create a new melonJS Application + const app = new Application(800, 600, { + parent: "screen", + scaleMethod: "flex-width", + renderer: 2, // AUTO + preferWebGL1: false, + depthTest: "z-buffer", + subPixel: false, + }); // register the debug plugin plugin.register(DebugPanelPlugin, "debugPanel"); - // initialize the "sound engine" + // initialize the sound engine audio.init("mp3,ogg"); // allow cross-origin for image/texture loading loader.setOptions({ crossOrigin: "anonymous" }); - // set all ressources to be loaded + // preload all resources loader.preload(resources, () => { - // set the "Play/Ingame" Screen Object + // set the Play screen state.set(state.PLAY, new PlayScreen()); // set the fade transition effect state.transition("fade", "#FFFFFF", 250); - // register our objects entity in the object pool + // register entity classes in the object pool pool.register("mainPlayer", PlayerEntity); pool.register("SlimeEntity", SlimeEnemyEntity); pool.register("FlyEntity", FlyEnemyEntity); pool.register("CoinEntity", CoinEntity, true); - // load the texture atlas file - // this will be used by renderable object later + // load the texture atlas gameState.texture = new TextureAtlas( loader.getJSON("texture"), loader.getImage("texture"), ); - // add some keyboard shortcuts - event.on(event.KEYDOWN, (_action, keyCode /*, edge */) => { - // change global volume setting + // keyboard shortcuts for volume and fullscreen + event.on(event.KEYDOWN, (_action, keyCode) => { if (keyCode === input.KEY.PLUS) { - // increase volume audio.setVolume(audio.getVolume() + 0.1); } else if (keyCode === input.KEY.MINUS) { - // decrease volume audio.setVolume(audio.getVolume() - 0.1); } - - // toggle fullscreen on/off if (keyCode === input.KEY.F) { if (!device.isFullscreen()) { device.requestFullscreen(); @@ -85,7 +74,7 @@ export const createGame = () => { } }); - // switch to PLAY state + // switch to the Play state state.change(state.PLAY); }); }; diff --git a/packages/examples/src/examples/platformer/entities/player.ts b/packages/examples/src/examples/platformer/entities/player.ts index 7ecfde607..7b7c9291d 100644 --- a/packages/examples/src/examples/platformer/entities/player.ts +++ b/packages/examples/src/examples/platformer/entities/player.ts @@ -2,7 +2,6 @@ import { audio, Body, collision, - game, input, level, Rect, @@ -61,9 +60,6 @@ export class PlayerEntity extends Sprite { this.multipleJump = 1; - // set the viewport to follow this renderable on both axis, and enable damping - game.viewport.follow(this, game.viewport.AXIS.BOTH, 0.1); - // enable keyboard input.bindKey(input.KEY.LEFT, "left"); input.bindKey(input.KEY.RIGHT, "right"); @@ -150,7 +146,16 @@ export class PlayerEntity extends Sprite { } /** - ** update the force applied + * called when added to the game world + */ + onActivateEvent() { + const app = this.parentApp; + // set the viewport to follow this renderable on both axis, and enable damping + app.viewport.follow(this, app.viewport.AXIS.BOTH, 0.1); + } + + /** + * update the force applied */ update(dt) { if (input.isKeyPressed("left")) { @@ -192,12 +197,13 @@ export class PlayerEntity extends Sprite { // check if we fell into a hole if (!this.inViewport && this.getBounds().top > video.renderer.height) { + const app = this.parentApp; // if yes reset the game - game.world.removeChild(this); - game.viewport.fadeIn("#fff", 150, () => { + app.world.removeChild(this); + app.viewport.fadeIn("#fff", 150, () => { audio.play("die", false); level.reload(); - game.viewport.fadeOut("#fff", 150); + app.viewport.fadeOut("#fff", 150); }); return true; } @@ -211,7 +217,7 @@ export class PlayerEntity extends Sprite { } /** - * colision handler + * collision handler */ onCollision(response, other) { switch (other.body.collisionType) { @@ -228,7 +234,7 @@ export class PlayerEntity extends Sprite { ) { // Disable collision on the x axis response.overlapV.x = 0; - // Repond to the platform (it is solid) + // Respond to the platform (it is solid) return true; } // Do not respond to the platform (pass through) @@ -286,7 +292,7 @@ export class PlayerEntity extends Sprite { }); // flash the screen - game.viewport.fadeIn("#FFFFFF", 75); + this.parentApp.viewport.fadeIn("#FFFFFF", 75); audio.play("die", false); } } diff --git a/packages/examples/src/examples/platformer/play.ts b/packages/examples/src/examples/platformer/play.ts index 28b0d5dec..cf003527b 100644 --- a/packages/examples/src/examples/platformer/play.ts +++ b/packages/examples/src/examples/platformer/play.ts @@ -1,4 +1,4 @@ -import { audio, device, game, level, plugin, Stage } from "melonjs"; +import { type Application, audio, device, level, plugin, Stage } from "melonjs"; import { VirtualJoypad } from "./entities/controls"; import UIContainer from "./entities/HUD"; import { MinimapCamera } from "./entities/minimap"; @@ -11,7 +11,7 @@ export class PlayScreen extends Stage { /** * action to perform on state change */ - override onResetEvent() { + override onResetEvent(app: Application) { // load a level level.load("map1"); @@ -27,14 +27,14 @@ export class PlayScreen extends Stage { if (typeof this.HUD === "undefined") { this.HUD = new UIContainer(); } - game.world.addChild(this.HUD); + app.world.addChild(this.HUD); // display if debugPanel is enabled or on mobile if (plugin.cache.debugPanel?.panel.visible || device.touch) { if (typeof this.virtualJoypad === "undefined") { this.virtualJoypad = new VirtualJoypad(); } - game.world.addChild(this.virtualJoypad); + app.world.addChild(this.virtualJoypad); } // play some music @@ -44,15 +44,15 @@ export class PlayScreen extends Stage { /** * action to perform on state change */ - override onDestroyEvent() { + override onDestroyEvent(app: Application) { // remove the HUD from the game world if (this.HUD) { - game.world.removeChild(this.HUD); + app.world.removeChild(this.HUD); } // remove the joypad if initially added - if (this.virtualJoypad && game.world.hasChild(this.virtualJoypad)) { - game.world.removeChild(this.virtualJoypad); + if (this.virtualJoypad && app.world.hasChild(this.virtualJoypad)) { + app.world.removeChild(this.virtualJoypad); } // stop some music diff --git a/packages/examples/src/examples/ui/ExampleUI.tsx b/packages/examples/src/examples/ui/ExampleUI.tsx index 480263259..af1dfb6ec 100644 --- a/packages/examples/src/examples/ui/ExampleUI.tsx +++ b/packages/examples/src/examples/ui/ExampleUI.tsx @@ -1,6 +1,7 @@ import { + Application as App, + type Application, ColorLayer, - game, loader, Stage, state, @@ -41,7 +42,6 @@ class ButtonUI extends UISpriteElement { fillStyle: "black", textAlign: "center", textBaseline: "middle", - offScreenCanvas: video.renderer.WebGLVersion >= 1, }); } @@ -123,7 +123,6 @@ class CheckBoxUI extends UISpriteElement { textAlign: "left", textBaseline: "middle", text: offLabel, - offScreenCanvas: true, }); this.getBounds().width += this.font.measureText().width; @@ -204,8 +203,8 @@ class UIContainer extends UIBaseElement { } class PlayScreen extends Stage { - override onResetEvent() { - game.world.addChild( + override onResetEvent(app: Application) { + app.world.addChild( new ColorLayer("background", "rgba(248, 194, 40, 1.0)"), 0, ); @@ -243,21 +242,16 @@ class PlayScreen extends Stage { panel.addChild(new ButtonUI(30, 250, "green", "Accept")); panel.addChild(new ButtonUI(230, 250, "yellow", "Cancel")); - game.world.addChild(panel, 1); + app.world.addChild(panel, 1); } } const createGame = () => { - if ( - !video.init(800, 600, { - parent: "screen", - scale: "auto", - scaleMethod: "flex-width", - }) - ) { - alert("Your browser does not support HTML5 canvas."); - return; - } + const app = new App(800, 600, { + parent: "screen", + scale: "auto", + scaleMethod: "flex-width", + }); const resources = [ { name: "UI_Assets-0", type: "image", src: `${base}img/UI_Assets-0.png` }, diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 5925ecfe8..9e75a793f 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -14,6 +14,12 @@ - Application: new `canvas` getter, `resize()`, and `destroy()` convenience methods - Application: `GAME_INIT` event now passes the Application instance as parameter - Stage: `onResetEvent(app, ...args)` now receives the Application instance as first parameter, followed by any extra arguments from `state.change()` +- Stage: `onDestroyEvent(app)` now receives the Application instance as parameter +- Container: default dimensions are now `Infinity` (no intrinsic size, no clipping) — removes dependency on `game.viewport`. `anchorPoint` is always `(0, 0)` as containers act as grouping/transform nodes +- ImageLayer: decoupled from `game` singleton — uses `parentApp` for viewport and renderer access; `resize`, `createPattern` and event listeners deferred to `onActivateEvent` +- TMXTileMap: `addTo()` resolves viewport from the container tree via `getRootAncestor().app` instead of `game.viewport` +- Input: pointer and pointerevent modules now receive the Application instance via `GAME_INIT` event instead of importing the `game` singleton +- video: `video.renderer` and `video.init()` are now deprecated — use `new Application(width, height, options)` and `app.renderer` instead. `video.renderer` is kept in sync via `VIDEO_INIT` for backward compatibility - EventEmitter: native context parameter support — `addListener(event, fn, context)` and `addListenerOnce(event, fn, context)` now accept an optional context, eliminating `.bind()` closure overhead and enabling proper `removeListener()` by original function reference - EventEmitter: `event.on()` and `event.once()` no longer create `.bind()` closures when a context is provided @@ -25,6 +31,8 @@ - Application: prevent white flash on load by setting a black background on the parent element when no background is defined - WebGLRenderer: `setBlendMode()` now tracks the `premultipliedAlpha` flag — previously only the mode name was checked, causing incorrect GL blend function when mixing PMA and non-PMA textures with the same blend mode - TMX: fix crash in `getObjects(false)` when a map contains an empty object group (Container.children lazily initialized) +- Container: fix `updateBounds()` producing NaN when container has Infinity dimensions (skip parent bounds computation for non-finite containers, derive bounds from children only) +- Container: fix circular import in `BitmapTextData` pool registration (`pool.ts` ↔ `bitmaptextdata.ts`) - EventEmitter: `removeAllListeners()` now correctly clears once-listeners (previously only cleared regular listeners) - Loader: fix undefined `crossOrigin` variable in script parser, unsafe regex match in video parser, missing error parameter in video/fontface error callbacks, `fetchData` Promise constructor antipattern and silent error swallowing diff --git a/packages/melonjs/src/application/application.ts b/packages/melonjs/src/application/application.ts index 53540f21b..3f9f7e14d 100644 --- a/packages/melonjs/src/application/application.ts +++ b/packages/melonjs/src/application/application.ts @@ -25,6 +25,7 @@ import { VIDEO_INIT, WINDOW_ONORIENTATION_CHANGE, WINDOW_ONRESIZE, + WINDOW_ONSCROLL, } from "../system/event.ts"; import timer from "../system/timer.ts"; import { getUriFragment } from "../utils/utils.ts"; @@ -41,9 +42,32 @@ import type { } from "./settings.ts"; /** - * An Application represents a single melonJS game, and is responsible for updating (each frame) all the related object status and draw them. + * The Application class is the main entry point for creating a melonJS game. + * It initializes the renderer, creates the game world and viewport, registers DOM event + * listeners (resize, orientation, scroll), and starts the game loop. + * + * The Application instance provides access to the core game systems: + * - {@link Application#renderer renderer} — the active Canvas or WebGL renderer + * - {@link Application#world world} — the root container for all game objects + * - {@link Application#viewport viewport} — the default camera / viewport + * + * The app instance is automatically passed to {@link Stage#onResetEvent} and + * {@link Stage#onDestroyEvent}, and is accessible from any renderable via + * {@link Renderable#parentApp parentApp}. * @category Application - * @see {@link game} + * @example + * // create a new melonJS Application + * const app = new Application(800, 600, { + * parent: "screen", + * scaleMethod: "flex-width", + * renderer: 2, // AUTO + * }); + * + * // add objects to the world + * app.world.addChild(new Sprite(0, 0, { image: "player" })); + * + * // access the viewport + * app.viewport.follow(player, app.viewport.AXIS.BOTH); */ export default class Application { /** @@ -98,7 +122,7 @@ export default class Application { * @default true * @example * // keep the default game instance running even when losing focus - * me.game.pauseOnBlur = false; + * app.pauseOnBlur = false; */ pauseOnBlur: boolean; @@ -136,10 +160,19 @@ export default class Application { updateAverageDelta: number; /** + * Create and initialize a new melonJS Application. + * This is the recommended way to start a melonJS game. * @param width - The width of the canvas viewport * @param height - The height of the canvas viewport * @param options - The optional parameters for the application and default renderer * @throws {Error} Will throw an exception if it fails to instantiate a renderer + * @example + * const app = new Application(1024, 768, { + * parent: "game-container", + * scale: "auto", + * scaleMethod: "fit", + * renderer: 2, // AUTO + * }); */ constructor( width: number, @@ -279,7 +312,23 @@ export default class Application { // make this the active game instance for modules that reference the global setDefaultGame(this); - // register to the channel + // bridge DOM events to the melonJS event system + globalThis.addEventListener("resize", (e) => { + emit(WINDOW_ONRESIZE, e); + }); + globalThis.addEventListener("orientationchange", (e) => { + emit(WINDOW_ONORIENTATION_CHANGE, e); + }); + if (device.screenOrientation) { + globalThis.screen.orientation.onchange = (e) => { + emit(WINDOW_ONORIENTATION_CHANGE, e); + }; + } + globalThis.addEventListener("scroll", (e) => { + emit(WINDOW_ONSCROLL, e); + }); + + // react to resize/orientation changes on(WINDOW_ONRESIZE, () => { onresize(this); }); @@ -392,7 +441,7 @@ export default class Application { * Additionally the level id will also be passed to the called function. * @example * // call myFunction () everytime a level is loaded - * me.game.onLevelLoaded = this.myFunction.bind(this); + * app.onLevelLoaded = this.myFunction.bind(this); */ onLevelLoaded(): void {} @@ -596,6 +645,8 @@ export default class Application { /** * The default game application instance. * Set via {@link setDefaultGame} during engine initialization. + * When using {@link Application} directly, prefer using the app instance + * (e.g. from {@link Stage#onResetEvent} or {@link Renderable#parentApp}). */ export let game: Application; diff --git a/packages/melonjs/src/camera/camera2d.ts b/packages/melonjs/src/camera/camera2d.ts index b90a61f16..14a7868a8 100644 --- a/packages/melonjs/src/camera/camera2d.ts +++ b/packages/melonjs/src/camera/camera2d.ts @@ -56,7 +56,7 @@ const targetV = new Vector2d(); * // create a minimap camera in the top-right corner showing the full level * const minimap = new Camera2d(0, 0, 180, 100); * minimap.name = "minimap"; - * minimap.screenX = game.viewport.width - 190; + * minimap.screenX = app.viewport.width - 190; * minimap.screenY = 10; * minimap.autoResize = false; * minimap.setBounds(0, 0, levelWidth, levelHeight); @@ -495,11 +495,11 @@ export default class Camera2d extends Renderable { * set the camera to follow the specified renderable.
* (this will put the camera center around the given target) * @param target - renderable or position vector to follow - * @param [axis=me.game.viewport.AXIS.BOTH] - Which axis to follow (see {@link Camera2d.AXIS}) + * @param [axis=app.viewport.AXIS.BOTH] - Which axis to follow (see {@link Camera2d.AXIS}) * @param [damping=1] - default damping value * @example * // set the camera to follow this renderable on both axis, and enable damping - * me.game.viewport.follow(this, me.game.viewport.AXIS.BOTH, 0.1); + * app.viewport.follow(this, app.viewport.AXIS.BOTH, 0.1); */ follow( target: Renderable | Vector2d | Vector3d, @@ -545,7 +545,7 @@ export default class Camera2d extends Renderable { * @param y - vertical offset * @example * // Move the camera up by four pixels - * me.game.viewport.move(0, -4); + * app.viewport.move(0, -4); */ move(x: number, y: number): void { this.moveTo(this.pos.x + x, this.pos.y + y); @@ -675,12 +675,12 @@ export default class Camera2d extends Renderable { * @param intensity - maximum offset that the screen can be moved * while shaking * @param duration - expressed in milliseconds - * @param [axis=me.game.viewport.AXIS.BOTH] - specify on which axis to apply the shake effect (see {@link Camera2d.AXIS}) + * @param [axis=app.viewport.AXIS.BOTH] - specify on which axis to apply the shake effect (see {@link Camera2d.AXIS}) * @param [onComplete] - callback once shaking effect is over * @param [force] - if true this will override the current effect * @example * // shake it baby ! - * me.game.viewport.shake(10, 500, me.game.viewport.AXIS.BOTH); + * app.viewport.shake(10, 500, app.viewport.AXIS.BOTH); */ shake( intensity: number, @@ -706,10 +706,10 @@ export default class Camera2d extends Renderable { * @param [onComplete] - callback once effect is over * @example * // fade the camera to white upon dying, reload the level, and then fade out back - * me.game.viewport.fadeIn("#fff", 150, function() { + * app.viewport.fadeIn("#fff", 150, function() { * me.audio.play("die", false); * me.level.reload(); - * me.game.viewport.fadeOut("#fff", 150); + * app.viewport.fadeOut("#fff", 150); * }); */ fadeOut( @@ -736,7 +736,7 @@ export default class Camera2d extends Renderable { * @param [onComplete] - callback once effect is over * @example * // flash the camera to white for 75ms - * me.game.viewport.fadeIn("#FFFFFF", 75); + * app.viewport.fadeIn("#FFFFFF", 75); */ fadeIn( color: Color | string, diff --git a/packages/melonjs/src/input/pointer.ts b/packages/melonjs/src/input/pointer.ts index bdabfb7b8..a32e863ff 100644 --- a/packages/melonjs/src/input/pointer.ts +++ b/packages/melonjs/src/input/pointer.ts @@ -1,8 +1,7 @@ -import { game } from "../application/application.ts"; import { Vector2d } from "../math/vector2d.ts"; import { Bounds } from "./../physics/bounds.ts"; import { globalToLocal } from "./input.ts"; -import { locked } from "./pointerevent.ts"; +import { _app, locked } from "./pointerevent.ts"; /** * a temporary vector object @@ -284,8 +283,8 @@ class Pointer extends Bounds { this.type = event.type; // get the current screen to game world offset - if (typeof game.viewport !== "undefined") { - game.viewport.localToWorld(this.gameScreenX, this.gameScreenY, tmpVec); + if (typeof _app?.viewport !== "undefined") { + _app.viewport.localToWorld(this.gameScreenX, this.gameScreenY, tmpVec); } /* Initialize the two coordinate space properties. */ diff --git a/packages/melonjs/src/input/pointerevent.ts b/packages/melonjs/src/input/pointerevent.ts index e9dbbf436..1bd422913 100644 --- a/packages/melonjs/src/input/pointerevent.ts +++ b/packages/melonjs/src/input/pointerevent.ts @@ -1,9 +1,15 @@ -import { game } from "../application/application.ts"; +import type Application from "../application/application.ts"; import { Rect } from "./../geometries/rectangle.ts"; import type { Vector2d } from "../math/vector2d.ts"; import { vector2dPool } from "../math/vector2d.ts"; import * as device from "./../system/device.js"; -import { emit, POINTERLOCKCHANGE, POINTERMOVE } from "../system/event.ts"; +import { + emit, + GAME_INIT, + on, + POINTERLOCKCHANGE, + POINTERMOVE, +} from "../system/event.ts"; import timer from "./../system/timer.ts"; import { remove } from "./../utils/array.ts"; import { throttle } from "./../utils/function.ts"; @@ -29,6 +35,15 @@ const eventHandlers: Map = new Map(); // a cache rect represeting the current pointer area let currentPointer: Rect; +/** + * reference to the active application instance + * @ignore + */ +export let _app: Application; +on(GAME_INIT, (app: Application) => { + _app = app; +}); + // some useful flags let pointerInitialized = false; @@ -133,7 +148,7 @@ function enablePointerEvent(): void { if (pointerEventTarget === null || pointerEventTarget === undefined) { // default pointer event target - pointerEventTarget = game.renderer.getCanvas(); + pointerEventTarget = _app.renderer.getCanvas(); } if (device.pointerEvent) { @@ -202,7 +217,7 @@ function enablePointerEvent(): void { () => { // change the locked status accordingly locked = - globalThis.document.pointerLockElement === game.getParentElement(); + globalThis.document.pointerLockElement === _app.getParentElement(); // emit the corresponding internal event emit(POINTERLOCKCHANGE, locked); }, @@ -309,14 +324,14 @@ function dispatchEvent(normalizedEvents: Pointer[]): boolean { } // fetch valid candiates from the game world container - let candidates = game.world.broadphase.retrieve( + let candidates = _app.world.broadphase.retrieve( currentPointer, - (a: any, b: any) => game.world._sortReverseZ(a, b), + (a: any, b: any) => _app.world._sortReverseZ(a, b), undefined, ); // add the main game viewport to the list of candidates - candidates = candidates.concat([game.viewport]); + candidates = candidates.concat([_app.viewport]); for ( let c = candidates.length, candidate; @@ -594,11 +609,11 @@ export function hasRegisteredEvents(): boolean { */ export function globalToLocal(x: number, y: number, v?: Vector2d): Vector2d { v = v || vector2dPool.get(); - const rect = device.getElementBounds(game.renderer.getCanvas()); + const rect = device.getElementBounds(_app.renderer.getCanvas()); const pixelRatio = globalThis.devicePixelRatio || 1; x -= rect.left + (globalThis.pageXOffset || 0); y -= rect.top + (globalThis.pageYOffset || 0); - const scale = game.renderer.scaleRatio; + const scale = _app.renderer.scaleRatio; if (scale.x !== 1.0 || scale.y !== 1.0) { x /= scale.x; y /= scale.y; @@ -817,7 +832,7 @@ export function releaseAllPointerEvents(region: any): void { */ export function requestPointerLock(): boolean { if (device.hasPointerLockSupport) { - const element = game.getParentElement(); + const element = _app.getParentElement(); void element.requestPointerLock(); return true; } diff --git a/packages/melonjs/src/level/level.js b/packages/melonjs/src/level/level.js index c48bf57b4..270defb3a 100644 --- a/packages/melonjs/src/level/level.js +++ b/packages/melonjs/src/level/level.js @@ -150,7 +150,7 @@ export const level = { * levelContainer.currentTransform.rotate(0.05); * levelContainer.currentTransform.translate(-levelContainer.width / 2, -levelContainer.height / 2 ); * // add it to the game world - * me.game.world.addChild(levelContainer); + * app.world.addChild(levelContainer); */ load(levelId, options) { options = Object.assign( @@ -203,7 +203,7 @@ export const level = { /** * return the current level definition. * for a reference to the live instantiated level, - * rather use the container in which it was loaded (e.g. me.game.world) + * rather use the container in which it was loaded (e.g. app.world) * @name getCurrentLevel * @memberof level * @public diff --git a/packages/melonjs/src/level/tiled/TMXGroup.js b/packages/melonjs/src/level/tiled/TMXGroup.js index 9e40ae36b..0aedfa891 100644 --- a/packages/melonjs/src/level/tiled/TMXGroup.js +++ b/packages/melonjs/src/level/tiled/TMXGroup.js @@ -5,7 +5,7 @@ import { applyTMXProperties, tiledBlendMode } from "./TMXUtils.js"; /** * object group definition as defined in Tiled. - * (group definition is translated into the virtual `me.game.world` using `me.Container`) + * (group definition is translated into the virtual `app.world` using `me.Container`) * @ignore */ export default class TMXGroup { diff --git a/packages/melonjs/src/level/tiled/TMXLayer.js b/packages/melonjs/src/level/tiled/TMXLayer.js index 16212008e..7f572cb14 100644 --- a/packages/melonjs/src/level/tiled/TMXLayer.js +++ b/packages/melonjs/src/level/tiled/TMXLayer.js @@ -291,7 +291,7 @@ export default class TMXLayer extends Renderable { * @returns {Tile} corresponding tile or null if there is no defined tile at the coordinate or if outside of the layer bounds * @example * // get the TMX Map Layer called "Front layer" - * let layer = me.game.world.getChildByName("Front Layer")[0]; + * let layer = app.world.getChildByName("Front Layer")[0]; * // get the tile object corresponding to the latest pointer position * let tile = layer.getTile(me.input.pointer.x, me.input.pointer.y); */ @@ -369,7 +369,7 @@ export default class TMXLayer extends Renderable { * @param {number} x - X coordinate (in map coordinates: row/column) * @param {number} y - Y coordinate (in map coordinates: row/column) * @example - * me.game.world.getChildByType(me.TMXLayer).forEach(function(layer) { + * app.world.getChildByType(me.TMXLayer).forEach(function(layer) { * // clear all tiles at the given x,y coordinates * layer.clearTile(x, y); * }); diff --git a/packages/melonjs/src/level/tiled/TMXObject.js b/packages/melonjs/src/level/tiled/TMXObject.js index 4a97817a3..9d2dd7c94 100644 --- a/packages/melonjs/src/level/tiled/TMXObject.js +++ b/packages/melonjs/src/level/tiled/TMXObject.js @@ -36,7 +36,7 @@ function detectShape(settings) { /** * a TMX Object defintion, as defined in Tiled - * (Object definition is translated into the virtual `me.game.world` using `me.Renderable`) + * (Object definition is translated into the virtual `app.world` using `me.Renderable`) * @ignore */ export default class TMXObject { diff --git a/packages/melonjs/src/level/tiled/TMXTileMap.js b/packages/melonjs/src/level/tiled/TMXTileMap.js index 55a59e342..cc3a51851 100644 --- a/packages/melonjs/src/level/tiled/TMXTileMap.js +++ b/packages/melonjs/src/level/tiled/TMXTileMap.js @@ -1,4 +1,3 @@ -import { game } from "../../application/application.ts"; import { warning } from "../../lang/console.js"; import { vector2dPool } from "../../math/vector2d.ts"; import { collision } from "../../physics/collision.js"; @@ -129,7 +128,7 @@ export default class TMXTileMap { * // create a new level object based on the TMX JSON object * let level = new me.TMXTileMap(levelId, me.loader.getTMX(levelId)); * // add the level to the game world container - * level.addTo(me.game.world, true); + * level.addTo(app.world, true); */ constructor(levelId, data) { /** @@ -378,7 +377,7 @@ export default class TMXTileMap { * // create a new level object based on the TMX JSON object * let level = new me.TMXTileMap(levelId, me.loader.getTMX(levelId)); * // add the level to the game world container - * level.addTo(me.game.world, true, true); + * level.addTo(app.world, true, true); */ addTo(container, flatten, setViewportBounds) { const _sort = container.autoSort; @@ -414,9 +413,11 @@ export default class TMXTileMap { * callback funtion for the viewport resize event * @ignore */ + const app = container.getRootAncestor().app; + function _setBounds(width, height) { // adjust the viewport bounds if level is smaller - game.viewport.setBounds( + app.viewport.setBounds( 0, 0, Math.max(levelBounds.width, width), @@ -434,7 +435,7 @@ export default class TMXTileMap { if (setViewportBounds === true) { off(VIEWPORT_ONRESIZE, _setBounds); // force viewport bounds update - _setBounds(game.viewport.width, game.viewport.height); + _setBounds(app.viewport.width, app.viewport.height); // Replace the resize handler on(VIEWPORT_ONRESIZE, _setBounds); } diff --git a/packages/melonjs/src/particles/emitter.ts b/packages/melonjs/src/particles/emitter.ts index 47194a503..44b0a33e7 100644 --- a/packages/melonjs/src/particles/emitter.ts +++ b/packages/melonjs/src/particles/emitter.ts @@ -70,7 +70,7 @@ export default class ParticleEmitter extends Container { * }); * * // Add the emitter to the game world - * me.game.world.addChild(emitter); + * app.world.addChild(emitter); * * // Launch all particles one time and stop, like an explosion * emitter.burstParticles(); @@ -80,7 +80,7 @@ export default class ParticleEmitter extends Container { * * // At the end, remove emitter from the game world * // call this in onDestroyEvent function - * me.game.world.removeChild(emitter); + * app.world.removeChild(emitter); */ constructor(x: number, y: number, settings: Record = {}) { // call the super constructor diff --git a/packages/melonjs/src/physics/collision.js b/packages/melonjs/src/physics/collision.js index 4ca520217..9d61ec786 100644 --- a/packages/melonjs/src/physics/collision.js +++ b/packages/melonjs/src/physics/collision.js @@ -111,7 +111,7 @@ export const collision = { * // starting point relative to the initial position * new me.Vector2d(0, 0), * // ending point - * new me.Vector2d(me.game.viewport.width, me.game.viewport.height) + * new me.Vector2d(app.viewport.width, app.viewport.height) * ]); * * // check for collition diff --git a/packages/melonjs/src/physics/detector.js b/packages/melonjs/src/physics/detector.js index 6dddb1b0f..a51b265f7 100644 --- a/packages/melonjs/src/physics/detector.js +++ b/packages/melonjs/src/physics/detector.js @@ -258,7 +258,7 @@ class Detector { * // starting point relative to the initial position * new Vector2d(0, 0), * // ending point - * new Vector2d(me.game.viewport.width, me.game.viewport.height) + * new Vector2d(app.viewport.width, app.viewport.height) * ]); * * // check for collition diff --git a/packages/melonjs/src/physics/world.js b/packages/melonjs/src/physics/world.js index d0ee0cd9f..cbd6c022c 100644 --- a/packages/melonjs/src/physics/world.js +++ b/packages/melonjs/src/physics/world.js @@ -26,8 +26,8 @@ export default class World extends Container { /** * @param {number} [x=0] - position of the container (accessible via the inherited pos.x property) * @param {number} [y=0] - position of the container (accessible via the inherited pos.y property) - * @param {number} [width=game.viewport.width] - width of the container - * @param {number} [height=game.viewport.height] - height of the container + * @param {number} [width=Infinity] - width of the world container + * @param {number} [height=Infinity] - height of the world container */ constructor(x = 0, y = 0, width = Infinity, height = Infinity) { // call the super constructor @@ -52,7 +52,7 @@ export default class World extends Container { * @default "builtin" * @example * // disable builtin physic - * me.game.world.physic = "none"; + * app.world.physic = "none"; */ this.physic = "builtin"; diff --git a/packages/melonjs/src/plugin/plugin.ts b/packages/melonjs/src/plugin/plugin.ts index 1a08f41fd..159cf8301 100644 --- a/packages/melonjs/src/plugin/plugin.ts +++ b/packages/melonjs/src/plugin/plugin.ts @@ -40,11 +40,11 @@ export class BasePlugin { * @param name - target function * @param fn - replacement function * @example - * // redefine the me.game.update function with a new one - * me.plugin.patch(me.game, "update", function () { + * // redefine the app.update function with a new one + * me.plugin.patch(app, "update", function () { * // display something in the console * console.log("duh"); - * // call the original me.game.update function + * // call the original app.update function * this._patched(); * }); */ diff --git a/packages/melonjs/src/renderable/container.js b/packages/melonjs/src/renderable/container.js index 87fc8590f..80fc13827 100644 --- a/packages/melonjs/src/renderable/container.js +++ b/packages/melonjs/src/renderable/container.js @@ -41,15 +41,23 @@ let globalFloatingCounter = 0; */ /** - * Container represents a collection of child objects + * Container represents a collection of child objects. + * When no explicit dimensions are given, width and height default to Infinity, + * meaning the container has no intrinsic size, no clipping, and acts as a pure + * grouping/transform node (similar to PixiJS or Phaser containers). + * In this case, anchorPoint is treated as (0, 0) since there is no meaningful + * center for an infinite area. Bounds are then derived entirely from children + * when {@link Container#enableChildBoundsUpdate} is enabled. * @category Container */ export default class Container extends Renderable { /** * @param {number} [x=0] - position of the container (accessible via the inherited pos.x property) * @param {number} [y=0] - position of the container (accessible via the inherited pos.y property) - * @param {number} [width=Infinity] - width of the container (Infinity means no clipping) - * @param {number} [height=Infinity] - height of the container (Infinity means no clipping) + * @param {number} [width=Infinity] - width of the container. Defaults to Infinity (no intrinsic size, no clipping). + * @param {number} [height=Infinity] - height of the container. Defaults to Infinity (no intrinsic size, no clipping). + * @param {boolean} [root=false] - internal flag, true for the world root container + * @ignore root */ constructor(x = 0, y = 0, width = Infinity, height = Infinity, root = false) { // call the super constructor @@ -142,6 +150,10 @@ export default class Container extends Renderable { // enable collision and event detection this.isKinematic = false; + // container anchorPoint is always (0, 0) — children position from the + // container's origin (top-left), matching the convention used by other engines + // (PixiJS, Phaser). This also avoids Infinity * 0.5 = Infinity issues + // when the container has no explicit size. this.anchorPoint.set(0, 0); // subscribe on the canvas resize event @@ -555,8 +567,10 @@ export default class Container extends Renderable { updateBounds(absolute = true) { const bounds = this.getBounds(); - // call parent method - super.updateBounds(absolute); + if (this.isFinite()) { + // call parent method only when container has finite dimensions + super.updateBounds(absolute); + } if (this.enableChildBoundsUpdate === true) { this.forEach((child) => { diff --git a/packages/melonjs/src/renderable/imagelayer.js b/packages/melonjs/src/renderable/imagelayer.js index 603fe762f..40f154a90 100644 --- a/packages/melonjs/src/renderable/imagelayer.js +++ b/packages/melonjs/src/renderable/imagelayer.js @@ -1,4 +1,3 @@ -import { game } from "../application/application.ts"; import { vector2dPool } from "../math/vector2d.ts"; import { LEVEL_LOADED, @@ -33,7 +32,7 @@ export default class ImageLayer extends Sprite { * @param {number|Vector2d} [settings.anchorPoint=<0.0,0.0>] - Define how the image is anchored to the viewport bound. By default, its upper-left corner is anchored to the viewport bounds upper left corner. * @example * // create a repetitive background pattern on the X axis using the citycloud image asset - * me.game.world.addChild(new me.ImageLayer(0, 0, { + * app.world.addChild(new me.ImageLayer(0, 0, { * image:"citycloud", * repeat :"repeat-x" * }), 1); @@ -83,9 +82,6 @@ export default class ImageLayer extends Sprite { } this.repeat = settings.repeat || "repeat"; - - // on context lost, all previous textures are destroyed - on(ONCONTEXT_RESTORED, this.createPattern, this); } /** @@ -123,15 +119,18 @@ export default class ImageLayer extends Sprite { this.repeatY = true; break; } - this.resize(game.viewport.width, game.viewport.height); - this.createPattern(); } // called when the layer is added to the game world or a container onActivateEvent() { - // register to the viewport change notification + const viewport = this.parentApp.viewport; + // set the initial size to match the viewport + this.resize(viewport.width, viewport.height); + this.createPattern(); + // register to the viewport change notification and context restore on(VIEWPORT_ONCHANGE, this.updateLayer, this); on(VIEWPORT_ONRESIZE, this.resize, this); + on(ONCONTEXT_RESTORED, this.createPattern, this); // force a first refresh when the level is loaded on(LEVEL_LOADED, this.updateLayer, this); // in case the level is not added to the root container, @@ -159,10 +158,10 @@ export default class ImageLayer extends Sprite { * @ignore */ createPattern() { - const renderer = this.parentApp?.renderer ?? game.renderer; - if (renderer) { - this._pattern = renderer.createPattern(this.image, this._repeat); - } + this._pattern = this.parentApp.renderer.createPattern( + this.image, + this._repeat, + ); } /** @@ -173,7 +172,7 @@ export default class ImageLayer extends Sprite { const rx = this.ratio.x; const ry = this.ratio.y; - const viewport = game.viewport; + const viewport = this.parentApp.viewport; if (rx === 0 && ry === 0) { // static image @@ -263,7 +262,7 @@ export default class ImageLayer extends Sprite { x = this.pos.x + ax * (bw - width); y = this.pos.y + ay * (bh - height); } else { - // parallax — compute position from the current viewport, not game.viewport + // parallax — compute position from the current viewport passed to draw() x = ax * (rx - 1) * (bw - viewport.width) + this.offset.x - @@ -300,6 +299,7 @@ export default class ImageLayer extends Sprite { off(VIEWPORT_ONCHANGE, this.updateLayer, this); off(VIEWPORT_ONRESIZE, this.resize, this); off(LEVEL_LOADED, this.updateLayer, this); + off(ONCONTEXT_RESTORED, this.createPattern, this); } /** @@ -309,7 +309,6 @@ export default class ImageLayer extends Sprite { destroy() { vector2dPool.release(this.ratio); this.ratio = undefined; - off(ONCONTEXT_RESTORED, this.createPattern, this); super.destroy(); } } diff --git a/packages/melonjs/src/renderable/renderable.js b/packages/melonjs/src/renderable/renderable.js index 58bbbb5a8..6368bbeb0 100644 --- a/packages/melonjs/src/renderable/renderable.js +++ b/packages/melonjs/src/renderable/renderable.js @@ -104,7 +104,7 @@ export default class Renderable extends Rect { * this.isKinematic = false; * * // set the display to follow our position on both axis - * me.game.viewport.follow(this.pos, me.game.viewport.AXIS.BOTH); + * app.viewport.follow(this.pos, app.viewport.AXIS.BOTH); * } * * ... @@ -116,7 +116,7 @@ export default class Renderable extends Rect { /** * (G)ame (U)nique (Id)entifier"
* a GUID will be allocated for any renderable object added
- * to an object container (including the `me.game.world` container) + * to an object container (including the `app.world` container) * @type {string} */ this.GUID = undefined; diff --git a/packages/melonjs/src/renderable/text/bitmaptext.js b/packages/melonjs/src/renderable/text/bitmaptext.js index f8bbb43a0..f22e2cccf 100644 --- a/packages/melonjs/src/renderable/text/bitmaptext.js +++ b/packages/melonjs/src/renderable/text/bitmaptext.js @@ -37,7 +37,7 @@ export default class BitmapText extends Renderable { * // either call the draw function from your Renderable draw function * myFont.draw(renderer, "Hello!", 0, 0); * // or just add it to the world container - * me.game.world.addChild(myFont); + * app.world.addChild(myFont); */ constructor(x, y, settings) { // call the parent constructor diff --git a/packages/melonjs/src/renderable/text/bitmaptextdata.ts b/packages/melonjs/src/renderable/text/bitmaptextdata.ts index 789ef3282..026c7f180 100644 --- a/packages/melonjs/src/renderable/text/bitmaptextdata.ts +++ b/packages/melonjs/src/renderable/text/bitmaptextdata.ts @@ -1,4 +1,4 @@ -import { createPool } from "../../pool.ts"; +import { createPool } from "../../system/pool.ts"; import Glyph from "./glyph.ts"; // bitmap constants diff --git a/packages/melonjs/src/state/stage.js b/packages/melonjs/src/state/stage.js index 59d6a675c..d963e9610 100644 --- a/packages/melonjs/src/state/stage.js +++ b/packages/melonjs/src/state/stage.js @@ -53,7 +53,7 @@ export default class Stage { * // set a dark ambient light * this.ambientLight.parseCSS("#1117"); * // make the light follow the mouse - * me.input.registerPointerEvent("pointermove", me.game.viewport, (event) => { + * me.input.registerPointerEvent("pointermove", app.viewport, (event) => { * whiteLight.centerOn(event.gameX, event.gameY); * }); */ @@ -181,7 +181,7 @@ export default class Stage { * destroy function * @ignore */ - destroy() { + destroy(app) { // clear all cameras this.cameras.clear(); // clear all lights @@ -190,7 +190,7 @@ export default class Stage { }); this.lights.clear(); // notify the object - this.onDestroyEvent.apply(this, arguments); + this.onDestroyEvent(app); } /** @@ -215,11 +215,12 @@ export default class Stage { * called by the state manager before switching to another state * @name onDestroyEvent * @memberof Stage + * @param {Application} [app] - the current application instance */ - onDestroyEvent() { + onDestroyEvent(app) { // execute onDestroyEvent function if given through the constructor if (typeof this.settings.onDestroyEvent === "function") { - this.settings.onDestroyEvent.apply(this, arguments); + this.settings.onDestroyEvent.call(this, app); } } } diff --git a/packages/melonjs/src/state/state.ts b/packages/melonjs/src/state/state.ts index 64203927a..7a85ec3d3 100644 --- a/packages/melonjs/src/state/state.ts +++ b/packages/melonjs/src/state/state.ts @@ -120,7 +120,7 @@ function _switchState(stateId: number): void { // call the stage destroy method if (_stages[_state]) { // just notify the object - _stages[_state].stage.destroy(); + _stages[_state].stage.destroy(_app); } if (_stages[stateId]) { @@ -352,7 +352,7 @@ const state = { * class MenuScreen extends me.Stage { * onResetEvent() { * // Load background image - * me.game.world.addChild( + * app.world.addChild( * new me.ImageLayer(0, 0, { * image : "bg", * z: 0 // z-index @@ -360,7 +360,7 @@ const state = { * ); * * // Add a button - * me.game.world.addChild( + * app.world.addChild( * new MenuButton(350, 200, { "image" : "start" }), * 1 // z-index * ); diff --git a/packages/melonjs/src/system/bootstrap.ts b/packages/melonjs/src/system/bootstrap.ts index 41ca21c56..2401a30bc 100644 --- a/packages/melonjs/src/system/bootstrap.ts +++ b/packages/melonjs/src/system/bootstrap.ts @@ -27,12 +27,10 @@ export let initialized = false; /** * Initialize the melonJS library. - * This is called automatically in two cases: - * - On DOMContentLoaded, unless {@link skipAutoInit} is set to true - * - By {@link Application.init} when creating a new game instance - * + * This is called automatically by the {@link Application} constructor. * Multiple calls are safe — boot() is idempotent. - * @see {@link skipAutoInit} + * When using {@link Application} directly, calling boot() manually is not needed. + * @see {@link Application} */ export function boot() { // don't do anything if already initialized (should not happen anyway) diff --git a/packages/melonjs/src/system/device.js b/packages/melonjs/src/system/device.js index 8b2951249..e8a90dcdd 100644 --- a/packages/melonjs/src/system/device.js +++ b/packages/melonjs/src/system/device.js @@ -388,6 +388,7 @@ export let autoFocus = true; * me.device.onReady(function () { * game.onload(); * }); + * @deprecated since 18.3.0 — no longer needed when using {@link Application} as entry point. * @category Application */ export function onReady(fn) { @@ -771,10 +772,10 @@ export function focus() { * @returns {boolean} false if not supported or permission not granted by the user * @example * // try to enable device accelerometer event on user gesture - * me.input.registerPointerEvent("pointerleave", me.game.viewport, function() { + * me.input.registerPointerEvent("pointerleave", app.viewport, function() { * if (me.device.watchAccelerometer() === true) { * // Success - * me.input.releasePointerEvent("pointerleave", me.game.viewport); + * me.input.releasePointerEvent("pointerleave", app.viewport); * } else { * // ... fail at enabling the device accelerometer event * } @@ -828,10 +829,10 @@ export function unwatchAccelerometer() { * @returns {boolean} false if not supported or permission not granted by the user * @example * // try to enable device orientation event on user gesture - * me.input.registerPointerEvent("pointerleave", me.game.viewport, function() { + * me.input.registerPointerEvent("pointerleave", app.viewport, function() { * if (me.device.watchDeviceOrientation() === true) { * // Success - * me.input.releasePointerEvent("pointerleave", me.game.viewport); + * me.input.releasePointerEvent("pointerleave", app.viewport); * } else { * // ... fail at enabling the device orientation event * } diff --git a/packages/melonjs/src/system/legacy_pool.js b/packages/melonjs/src/system/legacy_pool.js index b69947a42..36658ce8c 100644 --- a/packages/melonjs/src/system/legacy_pool.js +++ b/packages/melonjs/src/system/legacy_pool.js @@ -119,8 +119,8 @@ class ObjectPool { * // ... * // when we want to destroy existing object, the remove * // function will ensure the object can then be reallocated later - * me.game.world.removeChild(enemy); - * me.game.world.removeChild(bullet); + * app.world.removeChild(enemy); + * app.world.removeChild(bullet); */ pull(name, ...args) { const className = this.objectClass[name]; diff --git a/packages/melonjs/src/video/video.js b/packages/melonjs/src/video/video.js index 1fc55fd80..b9ca5c0b7 100644 --- a/packages/melonjs/src/video/video.js +++ b/packages/melonjs/src/video/video.js @@ -2,13 +2,7 @@ import { game } from "../application/application.ts"; import { defaultApplicationSettings } from "../application/defaultApplicationSettings.ts"; import { initialized } from "../system/bootstrap.ts"; import * as device from "./../system/device.js"; -import { - emit, - VIDEO_INIT, - WINDOW_ONORIENTATION_CHANGE, - WINDOW_ONRESIZE, - WINDOW_ONSCROLL, -} from "../system/event.ts"; +import { on, VIDEO_INIT } from "../system/event.ts"; /** * @namespace video @@ -27,13 +21,19 @@ export { AUTO, CANVAS, WEBGL } from "../const"; /** * A reference to the active Canvas or WebGL renderer. - * Only available after calling {@link video.init}. - * When using {@link Application} directly, use `app.renderer` instead. * @memberof video * @type {CanvasRenderer|WebGLRenderer} + * @deprecated since 18.3.0 — use {@link Application#renderer app.renderer} instead. + * @see Application#renderer */ export let renderer = null; +// backward compatibility: keep video.renderer in sync +// when Application is used directly instead of video.init() +on(VIDEO_INIT, (r) => { + renderer = r; +}); + /** * Initialize the "video" system (create a canvas based on the given arguments, and the related renderer).
* @memberof video @@ -41,8 +41,18 @@ export let renderer = null; * @param {number} height - The height of the canvas viewport * @param {ApplicationSettings} [options] - optional parameters for the renderer * @returns {boolean} false if initialization failed (canvas not supported) + * @deprecated since 18.3.0 — use {@link Application} constructor instead: + * `const app = new Application(width, height, options)` + * @see Application * @example - * // init the video with a 640x480 canvas + * // using the new Application entry point (recommended) + * const app = new Application(640, 480, { + * parent : "screen", + * scale : "auto", + * scaleMethod : "fit" + * }); + * + * // legacy usage (still supported) * me.video.init(640, 480, { * parent : "screen", * renderer : me.video.AUTO, @@ -68,53 +78,7 @@ export function init(width, height, options) { return false; } - // set the public renderer reference - renderer = game.renderer; - - //add a channel for the onresize/onorientationchange event - globalThis.addEventListener( - "resize", - (e) => { - emit(WINDOW_ONRESIZE, e); - }, - false, - ); - - // Screen Orientation API - globalThis.addEventListener( - "orientationchange", - (e) => { - emit(WINDOW_ONORIENTATION_CHANGE, e); - }, - false, - ); - - // pre-fixed implementation on mozzila - globalThis.addEventListener( - "onmozorientationchange", - (e) => { - emit(WINDOW_ONORIENTATION_CHANGE, e); - }, - false, - ); - - if (device.screenOrientation === true) { - globalThis.screen.orientation.onchange = (e) => { - emit(WINDOW_ONORIENTATION_CHANGE, e); - }; - } - - // Automatically update relative canvas position on scroll - globalThis.addEventListener( - "scroll", - (e) => { - emit(WINDOW_ONSCROLL, e); - }, - false, - ); - - // notify the video has been initialized - emit(VIDEO_INIT, game.renderer); + // DOM event listeners and VIDEO_INIT are now handled by Application.init() return true; } @@ -157,6 +121,8 @@ export function createCanvas(width, height, returnOffscreenCanvas = false) { * return a reference to the parent DOM element holding the main canvas * @memberof video * @returns {HTMLElement} the HTML parent element + * @deprecated since 18.3.0 — use {@link Application#getParentElement app.getParentElement()} instead. + * @see Application#getParentElement */ export function getParent() { return game.getParentElement(); diff --git a/packages/melonjs/tests/container.spec.js b/packages/melonjs/tests/container.spec.js index 1a4d6d981..a8059c267 100644 --- a/packages/melonjs/tests/container.spec.js +++ b/packages/melonjs/tests/container.spec.js @@ -1001,11 +1001,152 @@ describe("Container", () => { it("should not clip children when dimensions are Infinity", () => { const c = new Container(); + c.clipping = true; const farChild = new Renderable(99999, 99999, 10, 10); c.addChild(farChild); - // child bounds should extend beyond any finite container - const bounds = farChild.getBounds(); - expect(bounds.left).toBeGreaterThan(0); + // draw() guards clipRect with bounds.isFinite(), so even with + // clipping enabled, Infinity bounds prevent actual clipping + const bounds = c.getBounds(); + expect(bounds.isFinite()).toEqual(false); + // child bounds should still be valid and reachable + const childBounds = farChild.getBounds(); + expect(childBounds.isFinite()).toEqual(true); + expect(childBounds.left).toBeGreaterThan(0); + }); + + it("getBounds() should return valid bounds with Infinity dimensions", () => { + const c = new Container(); + const bounds = c.getBounds(); + expect(bounds).toBeDefined(); + expect(typeof bounds.width).toEqual("number"); + expect(typeof bounds.height).toEqual("number"); + }); + + it("getBounds() should grow to fit children with explicit dimensions", () => { + const c = new Container(0, 0, 100, 100); + c.enableChildBoundsUpdate = true; + const child = new Renderable(10, 20, 50, 60); + c.addChild(child); + c.updateBounds(); + const bounds = c.getBounds(); + expect(bounds.width).toBeGreaterThanOrEqual(50); + expect(bounds.height).toBeGreaterThanOrEqual(60); + }); + + it("getChildByName should work with Infinity-sized container", () => { + const c = new Container(); + const child = new Renderable(0, 0, 32, 32); + child.name = "testChild"; + c.addChild(child); + const found = c.getChildByName("testChild"); + expect(found.length).toEqual(1); + expect(found[0]).toBe(child); + }); + + it("sort should work with Infinity-sized container", () => { + const c = new Container(); + c.autoSort = true; + const child1 = new Renderable(0, 0, 10, 10); + child1.pos.z = 5; + const child2 = new Renderable(0, 0, 10, 10); + child2.pos.z = 1; + c.addChild(child1); + c.addChild(child2); + c.sort(); + expect(c.getChildAt(0).pos.z).toBeLessThanOrEqual(c.getChildAt(1).pos.z); + }); + + it("isFinite() should return false for Infinity-sized container", () => { + const c = new Container(); + expect(c.isFinite()).toEqual(false); + }); + + it("isFinite() should return true for explicitly-sized container", () => { + const c = new Container(0, 0, 100, 100); + expect(c.isFinite()).toEqual(true); + }); + + it("getBounds().isFinite() should return false for Infinity-sized container", () => { + const c = new Container(); + const bounds = c.getBounds(); + expect(bounds.isFinite()).toEqual(false); + }); + + it("getBounds().isFinite() should return true for explicitly-sized container", () => { + const c = new Container(0, 0, 200, 150); + const bounds = c.getBounds(); + expect(bounds.isFinite()).toEqual(true); + }); + + it("updateBounds should not produce NaN for Infinity-sized container", () => { + const c = new Container(); + c.enableChildBoundsUpdate = true; + const child = new Renderable(10, 10, 50, 50); + c.addChild(child); + const bounds = c.updateBounds(); + expect(Number.isNaN(bounds.width)).toEqual(false); + expect(Number.isNaN(bounds.height)).toEqual(false); + }); + + it("updateBounds should return finite child bounds for Infinity container", () => { + const c = new Container(); + c.enableChildBoundsUpdate = true; + const child = new Renderable(10, 10, 50, 50); + c.addChild(child); + const bounds = c.updateBounds(); + expect(bounds.isFinite()).toEqual(true); + expect(bounds.width).toBeGreaterThanOrEqual(50); + expect(bounds.height).toBeGreaterThanOrEqual(50); + }); + + it("clipping should be skipped for Infinity-sized container", () => { + const c = new Container(); + // clipping requires finite bounds — verify the guard condition + expect(c.clipping).toEqual(false); + c.clipping = true; + const bounds = c.getBounds(); + // draw() checks bounds.isFinite() before clipping + expect(bounds.isFinite()).toEqual(false); + }); + + it("clipping preconditions should be met for finite container", () => { + const c = new Container(0, 0, 200, 150); + c.clipping = true; + const bounds = c.getBounds(); + // all three guard conditions in draw() should pass + expect(c.root).toEqual(false); + expect(c.clipping).toEqual(true); + expect(bounds.isFinite()).toEqual(true); + expect(bounds.width).toEqual(200); + expect(bounds.height).toEqual(150); + }); + + it("clipping defaults to false", () => { + expect(new Container().clipping).toEqual(false); + expect(new Container(0, 0, 100, 100).clipping).toEqual(false); + }); + + it("nested Infinity containers should not produce NaN bounds", () => { + const parent = new Container(); + const child = new Container(); + child.enableChildBoundsUpdate = true; + const renderable = new Renderable(5, 5, 20, 20); + child.addChild(renderable); + parent.addChild(child); + parent.enableChildBoundsUpdate = true; + const bounds = parent.updateBounds(); + expect(Number.isNaN(bounds.width)).toEqual(false); + expect(Number.isNaN(bounds.height)).toEqual(false); + }); + + it("anchorPoint should always be (0,0) for containers", () => { + const infinite = new Container(); + expect(infinite.anchorPoint.x).toEqual(0); + expect(infinite.anchorPoint.y).toEqual(0); + + const finite = new Container(0, 0, 200, 100); + expect(finite.anchorPoint.x).toEqual(0); + expect(finite.anchorPoint.y).toEqual(0); }); }); }); diff --git a/packages/melonjs/tests/eventEmitter.spec.ts b/packages/melonjs/tests/eventEmitter.spec.ts index 06dc9ceef..cda159745 100644 --- a/packages/melonjs/tests/eventEmitter.spec.ts +++ b/packages/melonjs/tests/eventEmitter.spec.ts @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, it, test, vi } from "vitest"; -import { boot, event, video } from "../src/index.js"; +import { Application, event } from "../src/index.js"; import { EventEmitter } from "../src/system/eventEmitter"; test("addListener()", () => { @@ -302,11 +302,9 @@ test("listener without context has undefined this", () => { // --------------------------------------------------------------- describe("event.ts public API", () => { beforeAll(() => { - boot(); - video.init(64, 64, { + new Application(64, 64, { parent: "screen", scale: "auto", - renderer: video.AUTO, }); }); diff --git a/packages/melonjs/tests/font.spec.js b/packages/melonjs/tests/font.spec.js index 06eaa00ad..cb6f09043 100644 --- a/packages/melonjs/tests/font.spec.js +++ b/packages/melonjs/tests/font.spec.js @@ -17,7 +17,6 @@ describe("Font : Text", () => { size: 8, fillStyle: "white", text: "test", - offScreenCanvas: false, }); }); diff --git a/packages/melonjs/tests/imagelayer.spec.js b/packages/melonjs/tests/imagelayer.spec.js new file mode 100644 index 000000000..ef3af9225 --- /dev/null +++ b/packages/melonjs/tests/imagelayer.spec.js @@ -0,0 +1,83 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { boot, game, ImageLayer, video } from "../src/index.js"; + +describe("ImageLayer", () => { + let testImage; + + beforeAll(async () => { + boot(); + video.init(800, 600, { + parent: "screen", + scale: "auto", + renderer: video.AUTO, + }); + + // create a small canvas to use as the image source + testImage = document.createElement("canvas"); + testImage.width = 64; + testImage.height = 64; + }); + + describe("createPattern", () => { + it("should create pattern when added to game world", () => { + const layer = new ImageLayer(0, 0, { + image: testImage, + name: "test", + repeat: "repeat", + }); + game.world.addChild(layer); + expect(layer._pattern).toBeDefined(); + game.world.removeChildNow(layer); + }); + }); + + describe("parentApp usage", () => { + it("should access viewport via parentApp when in world", () => { + const layer = new ImageLayer(0, 0, { + image: testImage, + name: "test", + repeat: "no-repeat", + }); + game.world.addChild(layer); + expect(layer.parentApp).toBeDefined(); + expect(layer.parentApp.viewport).toBe(game.viewport); + game.world.removeChildNow(layer); + }); + + it("should resize to viewport dimensions on activate", () => { + const layer = new ImageLayer(0, 0, { + image: testImage, + name: "test", + repeat: "no-repeat", + }); + game.world.addChild(layer); + expect(layer.width).toEqual(game.viewport.width); + expect(layer.height).toEqual(game.viewport.height); + game.world.removeChildNow(layer); + }); + + it("repeat-x should set width to Infinity on activate", () => { + const layer = new ImageLayer(0, 0, { + image: testImage, + name: "test", + repeat: "repeat-x", + }); + game.world.addChild(layer); + expect(layer.width).toEqual(Infinity); + expect(layer.height).toEqual(game.viewport.height); + game.world.removeChildNow(layer); + }); + + it("repeat-y should set height to Infinity on activate", () => { + const layer = new ImageLayer(0, 0, { + image: testImage, + name: "test", + repeat: "repeat-y", + }); + game.world.addChild(layer); + expect(layer.width).toEqual(game.viewport.width); + expect(layer.height).toEqual(Infinity); + game.world.removeChildNow(layer); + }); + }); +}); From aae62c19e3f9620decc9a8b9bc46b789ddf119d7 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Sat, 4 Apr 2026 12:14:33 +0800 Subject: [PATCH 3/6] Fix type error in eventEmitter test: parent expects HTMLElement, not string Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/melonjs/tests/eventEmitter.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/melonjs/tests/eventEmitter.spec.ts b/packages/melonjs/tests/eventEmitter.spec.ts index cda159745..aebdd808c 100644 --- a/packages/melonjs/tests/eventEmitter.spec.ts +++ b/packages/melonjs/tests/eventEmitter.spec.ts @@ -303,7 +303,7 @@ test("listener without context has undefined this", () => { describe("event.ts public API", () => { beforeAll(() => { new Application(64, 64, { - parent: "screen", + parent: document.getElementById("screen") || document.body, scale: "auto", }); }); From 64bb8062e51bb7e667d280971c86ccd47fd68e84 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Sat, 4 Apr 2026 12:17:41 +0800 Subject: [PATCH 4/6] Fix ApplicationSettings parent type to accept string ID or HTMLElement Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/melonjs/src/application/settings.ts | 4 ++-- packages/melonjs/tests/eventEmitter.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/melonjs/src/application/settings.ts b/packages/melonjs/src/application/settings.ts index 5c42b2cae..37c5e0059 100644 --- a/packages/melonjs/src/application/settings.ts +++ b/packages/melonjs/src/application/settings.ts @@ -107,8 +107,8 @@ export type ApplicationSettings = { batcher?: (new (renderer: any) => Batcher) | undefined; } & ( | { - // the DOM parent element to hold the canvas in the HTML file - parent: HTMLElement; + // the DOM parent element (or its string ID) to hold the canvas in the HTML file + parent: string | HTMLElement; canvas?: never; } | { diff --git a/packages/melonjs/tests/eventEmitter.spec.ts b/packages/melonjs/tests/eventEmitter.spec.ts index aebdd808c..cda159745 100644 --- a/packages/melonjs/tests/eventEmitter.spec.ts +++ b/packages/melonjs/tests/eventEmitter.spec.ts @@ -303,7 +303,7 @@ test("listener without context has undefined this", () => { describe("event.ts public API", () => { beforeAll(() => { new Application(64, 64, { - parent: document.getElementById("screen") || document.body, + parent: "screen", scale: "auto", }); }); From 9fb16ee7d514815a34a4770152784fce4b61887b Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Sat, 4 Apr 2026 12:21:27 +0800 Subject: [PATCH 5/6] Address Copilot review: guards, bounds clear, DOM listener cleanup - Container: clear bounds before child aggregation for infinite containers - ImageLayer: throw if parentApp unavailable in onActivateEvent - Input: throw if Application not initialized when enabling pointer events - Application: store DOM event handlers, remove them in destroy() - TMXTileMap: move app resolution inside setViewportBounds branch Co-Authored-By: Claude Opus 4.6 (1M context) --- .../melonjs/src/application/application.ts | 36 ++++++++++++------ packages/melonjs/src/input/pointerevent.ts | 3 ++ .../melonjs/src/level/tiled/TMXTileMap.js | 38 +++++++++---------- packages/melonjs/src/renderable/container.js | 3 ++ packages/melonjs/src/renderable/imagelayer.js | 5 +++ 5 files changed, 55 insertions(+), 30 deletions(-) diff --git a/packages/melonjs/src/application/application.ts b/packages/melonjs/src/application/application.ts index 3f9f7e14d..63bccd906 100644 --- a/packages/melonjs/src/application/application.ts +++ b/packages/melonjs/src/application/application.ts @@ -155,6 +155,11 @@ export default class Application { // min update step size stepSize: number; + + // DOM event handlers (stored for cleanup in destroy) + private _onResize?: (e: Event) => void; + private _onOrientationChange?: (e: Event) => void; + private _onScroll?: (e: Event) => void; updateDelta: number; lastUpdateStart: number | null; updateAverageDelta: number; @@ -313,20 +318,16 @@ export default class Application { setDefaultGame(this); // bridge DOM events to the melonJS event system - globalThis.addEventListener("resize", (e) => { - emit(WINDOW_ONRESIZE, e); - }); - globalThis.addEventListener("orientationchange", (e) => { + this._onResize = (e: Event) => emit(WINDOW_ONRESIZE, e); + this._onOrientationChange = (e: Event) => emit(WINDOW_ONORIENTATION_CHANGE, e); - }); + this._onScroll = (e: Event) => emit(WINDOW_ONSCROLL, e); + globalThis.addEventListener("resize", this._onResize); + globalThis.addEventListener("orientationchange", this._onOrientationChange); if (device.screenOrientation) { - globalThis.screen.orientation.onchange = (e) => { - emit(WINDOW_ONORIENTATION_CHANGE, e); - }; + globalThis.screen.orientation.onchange = this._onOrientationChange; } - globalThis.addEventListener("scroll", (e) => { - emit(WINDOW_ONSCROLL, e); - }); + globalThis.addEventListener("scroll", this._onScroll); // react to resize/orientation changes on(WINDOW_ONRESIZE, () => { @@ -516,6 +517,19 @@ export default class Application { off(STAGE_RESET, this.reset, this); /* eslint-enable @typescript-eslint/unbound-method */ + // remove DOM event listeners + if (this._onResize) { + globalThis.removeEventListener("resize", this._onResize); + globalThis.removeEventListener( + "orientationchange", + this._onOrientationChange!, + ); + globalThis.removeEventListener("scroll", this._onScroll!); + if (device.screenOrientation) { + globalThis.screen.orientation.onchange = null; + } + } + // destroy the world and all its children if (this.world) { this.world.destroy(); diff --git a/packages/melonjs/src/input/pointerevent.ts b/packages/melonjs/src/input/pointerevent.ts index 1bd422913..9338d6278 100644 --- a/packages/melonjs/src/input/pointerevent.ts +++ b/packages/melonjs/src/input/pointerevent.ts @@ -138,6 +138,9 @@ function registerEventListener( */ function enablePointerEvent(): void { if (!pointerInitialized) { + if (!_app) { + throw new Error("Pointer events require an initialized Application"); + } // the current pointer area currentPointer = new Rect(0, 0, 1, 1); diff --git a/packages/melonjs/src/level/tiled/TMXTileMap.js b/packages/melonjs/src/level/tiled/TMXTileMap.js index cc3a51851..615cf021c 100644 --- a/packages/melonjs/src/level/tiled/TMXTileMap.js +++ b/packages/melonjs/src/level/tiled/TMXTileMap.js @@ -413,26 +413,26 @@ export default class TMXTileMap { * callback funtion for the viewport resize event * @ignore */ - const app = container.getRootAncestor().app; - - function _setBounds(width, height) { - // adjust the viewport bounds if level is smaller - app.viewport.setBounds( - 0, - 0, - Math.max(levelBounds.width, width), - Math.max(levelBounds.height, height), - ); - // center the map if smaller than the current viewport - container.pos.set( - Math.max(0, ~~((width - levelBounds.width) / 2)), - Math.max(0, ~~((height - levelBounds.height) / 2)), - // don't change the container z position if defined - container.pos.z, - ); - } - if (setViewportBounds === true) { + const app = container.getRootAncestor().app; + + function _setBounds(width, height) { + // adjust the viewport bounds if level is smaller + app.viewport.setBounds( + 0, + 0, + Math.max(levelBounds.width, width), + Math.max(levelBounds.height, height), + ); + // center the map if smaller than the current viewport + container.pos.set( + Math.max(0, ~~((width - levelBounds.width) / 2)), + Math.max(0, ~~((height - levelBounds.height) / 2)), + // don't change the container z position if defined + container.pos.z, + ); + } + off(VIEWPORT_ONRESIZE, _setBounds); // force viewport bounds update _setBounds(app.viewport.width, app.viewport.height); diff --git a/packages/melonjs/src/renderable/container.js b/packages/melonjs/src/renderable/container.js index 80fc13827..7db5d191e 100644 --- a/packages/melonjs/src/renderable/container.js +++ b/packages/melonjs/src/renderable/container.js @@ -570,6 +570,9 @@ export default class Container extends Renderable { if (this.isFinite()) { // call parent method only when container has finite dimensions super.updateBounds(absolute); + } else if (this.enableChildBoundsUpdate === true) { + // clear bounds so child aggregation starts fresh + bounds.clear(); } if (this.enableChildBoundsUpdate === true) { diff --git a/packages/melonjs/src/renderable/imagelayer.js b/packages/melonjs/src/renderable/imagelayer.js index 40f154a90..78cccb12a 100644 --- a/packages/melonjs/src/renderable/imagelayer.js +++ b/packages/melonjs/src/renderable/imagelayer.js @@ -123,6 +123,11 @@ export default class ImageLayer extends Sprite { // called when the layer is added to the game world or a container onActivateEvent() { + if (!this.parentApp) { + throw new Error( + "ImageLayer requires a parent Application (must be added to an app's world container)", + ); + } const viewport = this.parentApp.viewport; // set the initial size to match the viewport this.resize(viewport.width, viewport.height); From a361299109d7d9ebc4b0999ca858fad7bb8b3f74 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Sat, 4 Apr 2026 14:08:03 +0800 Subject: [PATCH 6/6] Fix no-confusing-void-expression lint errors in DOM event handlers Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/melonjs/src/application/application.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/melonjs/src/application/application.ts b/packages/melonjs/src/application/application.ts index 63bccd906..273780381 100644 --- a/packages/melonjs/src/application/application.ts +++ b/packages/melonjs/src/application/application.ts @@ -318,10 +318,15 @@ export default class Application { setDefaultGame(this); // bridge DOM events to the melonJS event system - this._onResize = (e: Event) => emit(WINDOW_ONRESIZE, e); - this._onOrientationChange = (e: Event) => + this._onResize = (e: Event) => { + emit(WINDOW_ONRESIZE, e); + }; + this._onOrientationChange = (e: Event) => { emit(WINDOW_ONORIENTATION_CHANGE, e); - this._onScroll = (e: Event) => emit(WINDOW_ONSCROLL, e); + }; + this._onScroll = (e: Event) => { + emit(WINDOW_ONSCROLL, e); + }; globalThis.addEventListener("resize", this._onResize); globalThis.addEventListener("orientationchange", this._onOrientationChange); if (device.screenOrientation) {