import { shapes } from "./collision.js"
import { docs } from "./docs.js"
import { sprites } from "./sprite.js"
import { scenes } from "./scene.js"
import { vectors } from "./vector.js"
import { text } from "./text.js"
import { audio } from "./audio.js"
import { camera } from "./camera.js"
import { animation } from "./animation.js"
import { images } from "./image.js"
"use strict"
console.log("Welcome to Gameify");
/** Access engine objects in your code
* @example import {$get} from './_out.js';
* $get('Tilemap::Dungeon Map');
* @arg {String} sel - The object selector ( Type::Name )
* @return The object, if it exists (undefined if it doesn't)
*/
let $get = (sel) => { throw 'How\'d you access this?? (Bad $get)'; }
/** This is the main gameify object. All other things are contained within it.
* @global
*/
export let gameify = {
getDocs: docs.getDocs,
Animation: animation.Animation,
Animator: animation.Animator,
Camera: camera.Camera,
Vector2d: vectors.Vector2d,
vectors: new Proxy(vectors, { get: () => {
console.warn("gameify.vectors is deprecated. Use static methods/properties of gameify.Vector2d instead");
return vectors;
} }), // alias, for compatibility for now
Image: images.Image,
Sprite: sprites.Sprite,
Scene: scenes.Scene,
Text: text.Text,
TextStyle: text.TextStyle,
shapes: shapes,
audio: audio,
/** Manages keyboard events. One is created automatically for every screen, you can access it as shown in the example.
* @constructor
* @example // make a new screen and scene
* let myScreen = new gameify.Screen(document.querySelector("#my-canvas"), 600, 400);
* let myScene = new gameify.Scene(myScreen);
* myScene.onUpdate(() => {
* if (myScreen.keyboard.keyIsPressed("Escape")) {
* myScreen.setScene(pause_menu);
* }
* });
* @arg {HTMLElement} scope - What parts of the screen the KeyboardEventManager looks for.
*/
KeyboardEventManager: function (captureScope) {
/** The element that input events are captured from
* @private
*/
this.captureScope = captureScope;
// A list of the keys that are currently pressed
this.pressedKeys = [];
// A list of keys that were just pressed. Cleared after a few updates or at the end of an update in which it's queried for.
this.justPressedKeys = [];
// Control, shift, alt, meta
this.modifierKeys = {
Control: false,
Shift: false,
Alt: false,
Meta: false
}
/** Makes pressed keys nicer, for simpler queries (KeyH --> H)
* @package
* @arg {String} key - The key to make nicer
* @returns {String} The input key, with extra text stripped away.
*/
this.makeKeyNicer = (key) => {
// If it has a direction (Control, Shift, Alt, OS all have left and right variants)
// and is not the arrow keys, remove the direction.
if (!key.startsWith("Arrow")) {
key = key.replace("Left", "")
.replace("Right", "");
}
// Get rid of prefixes
return key.replace("Digit", "")
.replace("Numpad", "")
.replace("Key", "")
.replace("Arrow", "")
}
/** Called when a key is pressed down
* @private
*/
this.onKeyDown = (event) => {
this.checkEventModifierKeys(event);
let key = event.code;
// If the key is already in the array, don't add it again.
// This is normal, the OS might fire the keydown event repeatedly for held keys
if (this.pressedKeys.indexOf(key) >= 0) {
return;
}
// Push both the niceified and unique key, in case someone wants more precise control.
// This does mean that there could be duplicates of the niceified key (Numpad8 and Digit8 are both 8),
// but that's fine since the niceified one is always next to the unique one
this.pressedKeys.push(key);
this.pressedKeys.push(this.makeKeyNicer(key));
}
/** Force clear pressed keys
*/
this.forceClearPressedKeys = () => {
this.pressedKeys = [];
}
window.addEventListener('blur', this.forceClearPressedKeys);
this.captureScope.addEventListener('blur', this.forceClearPressedKeys);
/** Called when a key is released
* @private
*/
this.onKeyUp = (event) => {
event.preventDefault();
this.checkEventModifierKeys(event);
let key = event.code;
// Remove the keys from the pressedkeys array
this.pressedKeys.splice(this.pressedKeys.indexOf(key), 2);
// Add keys to just pressed list
this.justPressedKeys.push([0, key, this.makeKeyNicer(key)]);
}
/** Called when any mouse event happens, to grab ctrl,shift,alt keys
* @private
*/
this.checkEventModifierKeys = (event) => {
this.modifierKeys.Control = event.ctrlKey;
this.modifierKeys.Shift = event.shiftKey;
this.modifierKeys.Alt = event.altKey;
this.modifierKeys.Meta = event.metaKey;
}
/** Check if a key is currently pressed down
* @arg {String} key - What key do you want to check
* @returns {Boolean} if the key is pressed down
* @example // see if the right arrow is pressed
* if (myGame.keyboard.keyIsPressed("Right")) {
* // set the player motion
* player.velocity.x = 5;
* }
*/
this.keyIsPressed = (key) => {
// The modifier keys list is more accurate (updated on any event), so check it
if (this.modifierKeys[key]) {
return true;
}
// then check the rest of the list
return (this.pressedKeys.indexOf(key) >= 0);
}
/** Check if a key was just pressed and let up
* @arg {String} key - What key do you want to check
* @returns {Boolean} if the key was just pressed
* @example // See if the player pressed the Escape key.
* if (myScreen.keyboard.keyWasJustPressed("Escape")) {
* myScreen.setScene(mainMenu);
* }
*/
this.keyWasJustPressed = (key) => {
for (const i in this.justPressedKeys) {
let jpk = this.justPressedKeys[i];
if (jpk[1] == key || jpk[2] == key) {
this.justPressedKeys[i][0] = 99999;
return true;
}
}
return false;
}
this.firstFocusFunction = undefined;
this.firstFocusHappened = false;
/** Run a callback when the game is first focused (Useful for starting audio, etc)
* @arg {Function} callback - The callback to run
*/
this.onFirstFocus = (callback) => {
this.firstFocusFunction = callback;
}
// How long before "just pressed" keys are removed from the justPressedKeys list
this.clearJpkTimeout = 1;
/** Sets how long before "just pressed" keys are removed from the just pressed list.
* By default, keys are removed from the list after one frame.
* @arg {Number} timeout - How many frames/updates keys should be considered "just pressed"
*/
this.setJustPressedTimeout = (timeout) => {
if (timeout < 1) {
console.warn(`Setting the timeout to 0 or less means keys will NEVER be considered "just pressed", meaning keyWasJustPressed will always return false`);
return;
}
this.clearJpkTimeout = timeout;
}
/** Removes stale keys from the just pressed list.
* @private
*/
this.clearJustPressed = () => {
for (const i in this.justPressedKeys) {
if (this.justPressedKeys[i][0] >= this.clearJpkTimeout) {
this.justPressedKeys.splice(i, 1);
} else {
this.justPressedKeys[i][0] += 1;
}
}
}
/** Sets up the event manager.
* @package
*/
this.setup = () => {
this.firstFocusHappened = false;
this.captureScope.setAttribute("tabindex", 1);
this.captureScope.addEventListener("focusin", () => {
if (!this.firstFocusHappened) {
this.firstFocusHappened = true;
if (this.firstFocusFunction) this.firstFocusFunction();
}
});
this.captureScope.addEventListener("keydown", this.onKeyDown);
this.captureScope.addEventListener("keyup", this.onKeyUp);
this.captureScope.addEventListener("mousedown", this.checkEventModifierKeys);
this.captureScope.addEventListener("mouseup", this.checkEventModifierKeys);
this.captureScope.addEventListener("mouseout", this.checkEventModifierKeys);
this.captureScope.addEventListener("mousemove", this.checkEventModifierKeys);
this.captureScope.addEventListener("wheel", this.checkEventModifierKeys);
this.captureScope.addEventListener("contextmenu", this.checkEventModifierKeys);
}
/** Destructs the event manager (clears events)
* @package
*/
this.destruct = () => {
this.captureScope.removeEventListener("keydown", this.onkeyDown);
this.captureScope.removeEventListener("keyup", this.onKeyUp);
this.captureScope.removeEventListener("mousedown", this.checkEventModifierKeys);
this.captureScope.removeEventListener("mouseup", this.checkEventModifierKeys);
this.captureScope.removeEventListener("mouseout", this.checkEventModifierKeys);
this.captureScope.removeEventListener("mousemove", this.checkEventModifierKeys);
this.captureScope.removeEventListener("wheel", this.checkEventModifierKeys);
this.captureScope.removeEventListener("contextmenu", this.checkEventModifierKeys);
}
/** Changes the scope that the KeyboardInputManager looks at
* @arg {HTMLElement} scope - What parts of the screen the KeyboardEventManager looks for.
*/
this.setCaptureScope = (scope) => {
this.destruct();
this.captureScope = scope;
this.setup();
}
},
/** Manages mouse events. One is created automatically for every screen, you can access it as shown in the example.
* @constructor
* @example // make a new screen and scene
* let myScreen = new gameify.Screen(document.querySelector("#my-canvas"), 600, 400);
* let myScene = new gameify.Scene(myScreen);
* myScene.onUpdate(() => {
* if (myScreen.mouse.buttonIsPressed()) {
* myScreen.setScene(pause_menu);
* }
* });
* @arg {HTMLElement} scope - What parts of the screen the MouseEventManager looks at.
*/
MouseEventManager: function (captureScope, canvasElement, defaultCamera) {
/** The element that input events are captured from
* @private
*/
this.captureScope = captureScope;
this.canvasElement = canvasElement;
// A list of the keys that are currently pressed
this.pressedButtons = [];
// A list of keys that were just pressed. Cleared after a few updates or at the end of an update in which it's queried for.
this.eventsJustHappened = [];
// Current mouse position
this.cursorPosition = {x: 0, y: 0};
this.camera = defaultCamera;
/** Returns the name of the button given the number. <br> 0 = left, 1 = middle (wheel), 2 = right
* @param {Number} button - The numerical button to get the name of
* @private
*/
this.getButtonName = (button) => {
switch (button) {
case 0: return "left";
case 1: return "middle";
case 2: return "right";
default: throw new Error(`Invalid mouse button ${button}`);
}
}
/** Get the x and y position of the mouse cursor on the creen
* @example
* if (myScreen.mouse.getPosition().x < 50) {
* // do something
* }
* @returns {gameify.Vector2d} The mouse position on the Screen
*/
this.getPosition = () => {
// copy values, because we don't want to return a reference.
return new vectors.Vector2d(this.cursorPosition.x, this.cursorPosition.y);
}
/** Get the x and y position of the mouse cursor in the world
* @returns {gameify.Vector2d} The mouse position in the world
*/
this.worldPosition = () => {
return this.camera.screenToWorld(this.cursorPosition);
}
/** Called when a mouse button is pressed down
* @private
*/
this.onMouseDown = (event) => {
this.pressedButtons.push(event.button);
this.pressedButtons.push(this.getButtonName(event.button));
}
/** Called when a mouse button is pressed down
* @private
*/
this.onDoubleClick = (event) => {
this.eventsJustHappened.push([0, "dblclick", "doubleclick"]);
}
/** Called when a key is released
* @private
*/
this.onMouseUp = (event) => {
event.preventDefault();
let button = event.button;
// Remove the keys from the pressedkeys array
this.pressedButtons.splice(this.pressedButtons.indexOf(button), 2);
// Add keys to just pressed list
this.eventsJustHappened.push([0, button, this.getButtonName(button)]);
}
this.onMouseMove = (event) => {
const widthRatio = this.canvasElement.width / this.canvasElement.getBoundingClientRect().width;
this.cursorPosition.x = event.offsetX * widthRatio;
this.cursorPosition.y = event.offsetY * widthRatio;
this.eventsJustHappened.push([0, "mousemove", "move"]);
}
/** Called when the mouse leaves the canvas
* @private
*/
this.onMouseOut = (event) => {
this.pressedButtons = [];
this.eventsJustHappened.push([0, "mouseout", "leave"]);
}
/** Called when the mouse wheel is moved
* @private
*/
this.onMouseWheel = (event) => {
event.preventDefault();
if (event.deltaY > 0) {
this.eventsJustHappened.push([0, "wheeldown", "wheel"]);
} else {
this.eventsJustHappened.push([0, "wheelup", "wheel"]);
}
}
this.onContextMenu = (event) => {
event.preventDefault();
}
/** Check if a button is currently pressed down
* @arg {String} button - The button you want to check
* @returns {Boolean} if the button is pressed
* @example // see if the left button is pressed
* if (myGame.mouse.buttonIsPressed("left")) {
* // do something
* }
*/
this.buttonIsPressed = (button) => {
return (this.pressedButtons.indexOf(button) >= 0);
}
/** Check if a mouse event just happened (eg a button press or scroll)
* @arg {String} event - The event you want to check
* @arg {Boolean} [capture=false] - Capture the event, and stop other checks from seeing it
* @returns {Boolean} if the event just happened
* @example // See if the player clicked.
* if (myScreen.mouse.eventJustHappened("click")) {
* // do something
* }
*/
this.eventJustHappened = (event, capture) => {
for (const i in this.eventsJustHappened) {
let evt = this.eventsJustHappened[i];
if (evt[1] == event || evt[2] == event) {
this.eventsJustHappened[i][0] = 99999;
if (capture) {
this.eventsJustHappened.splice(i, 1);
}
return true;
}
}
return false;
}
// How long before "just pressed" keys are removed from the eventsJustHappened list (in frames)
this.clearEventTimeout = 1;
/** Sets how long before "just pressed" keys are removed from the just pressed list.
* By default, keys are removed from the list after one frame.
* @arg {Number} timeout - How many frames/updates keys should be considered "just pressed"
*/
this.setRecentEventTimeout = (timeout) => {
if (timeout < 1) {
console.warn(`Setting the timeout to 0 or less means events will NEVER be considered "just happened", meaning eventJustHappened will always return false`);
return;
}
this.clearEventTimeout = timeout;
}
/** Removes stale keys from the just pressed list.
* @private
*/
this.clearRecentEvents = () => {
for (const i in this.eventsJustHappened) {
if (this.eventsJustHappened[i][0] >= this.clearEventTimeout) {
this.eventsJustHappened.splice(i, 1);
} else {
this.eventsJustHappened[i][0] += 1;
}
}
}
/** Sets up the event manager.
* @package
*/
this.setup = () => {
this.captureScope.setAttribute("tabindex", 1);
this.captureScope.addEventListener("mousedown", this.onMouseDown);
this.captureScope.addEventListener("dblclick", this.onDoubleClick);
this.captureScope.addEventListener("mouseup", this.onMouseUp);
this.captureScope.addEventListener("mouseout", this.onMouseOut);
this.captureScope.addEventListener("mousemove", this.onMouseMove);
this.captureScope.addEventListener("wheel", this.onMouseWheel);
this.captureScope.addEventListener("contextmenu", this.onContextMenu);
}
/** Destructs the event manager (clears events)
* @package
*/
this.destruct = () => {
this.captureScope.removeEventListener("mousedown", this.onMouseDown);
this.captureScope.removeEventListener("dblclick", this.onDoubleClick);
this.captureScope.removeEventListener("mouseup", this.onMouseUp);
this.captureScope.removeEventListener("mouseout", this.onMouseOut);
this.captureScope.removeEventListener("mousemove", this.onMouseMove);
this.captureScope.removeEventListener("wheel", this.onMouseWheel);
this.captureScope.removeEventListener("contextmenu", this.onContextMenu);
}
/** Changes the scope that the KeyboardInputManager looks at
* @arg {HTMLElement} scope - What parts of the screen the KeyboardEventManager looks for.
*/
this.setCaptureScope = (scope) => {
this.destruct();
this.captureScope = scope;
this.setup();
}
},
/** A Screen to draw things to and get events from, the base of every game.
* @example // Get the canvas element
* let canvas = document.querySelector("#my-canvas");
* // Create a Screen that is 600 by 400
* let myScreen = new gameify.Screen(canvas, 600, 400);
* @arg {HTMLElement} element - The canvas to draw the screen to
* @arg {number} width - The width of the Screen
* @arg {number} height - The height of the Screen
*/
Screen: class {
constructor(element, width, height) {
// Error if not given the correct parameters
if (!element) {
throw new Error(`You need to specify a canvas element to create a Screen. See ${gameify.getDocs("gameify.Screen")} for details`);
}
if (!width || !height) {
throw new Error(`You need to specify a width and height to create a Screen. See ${gameify.getDocs("gameify.Screen")} for details`);
}
this.element = element;
this.width = width;
this.height = height;
this.element.width = this.width;
this.element.height = this.height;
this.context = this.element.getContext("2d");
this.camera = new gameify.Camera(this.context);
this.keyboard = new gameify.KeyboardEventManager(this.element);
this.keyboard.setup();
this.mouse = new gameify.MouseEventManager(this.element, this.element, this.camera);
this.mouse.setup();
this.audio = new gameify.audio.AudioManager();
this.audio.setVolume(0.5);
}
/** The HTML5 Canvas element the Screen is attached to
* @type HTMLElement
*/
element;
/** The width of the Screen
* @type Number
*/
width;
/** The height of the Screen
* @type Number
*/
height;
/** The Canvas Context */
context;
/** Keyboard events for the Screen. Used to see what keys are pressed.
* @type {gameify.KeyboardEventManager}
*/
keyboard;
/** Mouse events for the Screen. Used to what mouse buttons are pressed, and other mouse events (eg scroll)
* @type {gameify.MouseEventManager}
*/
mouse;
/** This screen's default AudioManager.
* @type {gameify.audio.AudioManager}
*/
audio;
/** This screen's default Camera.
* @type {gameify.Camera}
*/
camera;
/** The current game scene */
currentScene = null;
/** The game's update interval */
updateInterval = null;
// Track this seperately (detatched from canvas el), so that if the
// canvas for some reason loses its status, it can be restored
// (it does this w/ the engine!)
#antialiasingEnabled = true;
// Timestamp of the last update
#lastUpdate = 0;
#gameActive = false;
/** Creates a object from JSON data
* @method
* @arg {Object|Array} data - Serialized object data (from object.toJSON)
* @arg {Function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {gameify.Screen}
*/
static fromJSON = (data, find) => {
if (Array.isArray(data)) {
// Be backwards compatible
console.warn('Save is using the old (de)serialization format for Screen.');
const obj = new gameify.Screen(document.getElementById(data[0]), data[1], data[2]);
if (data[3]) obj.setScene(find(data[3]));
obj.setAntialiasing(data[4]);
return obj;
}
const obj = new gameify.Screen(document.getElementById(data.elementId), data.width, data.height);
if (data.currentScene) obj.setScene(find(data.currentScene));
obj.setAntialiasing(data.antialiasing);
return obj;
}
/** Convert the object to JSON
* @method
* @arg {string} [key] - Key object is stored under (unused, here for consistency with e.g. Date.toJSON, etc.)
* @arg {function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {Object}
*/
toJSON = (key, ref) => {
return {
elementId: this.element.id,
width: this.width,
height: this.height,
currentScene: ref(this.currentScene),
antialiasing: this.getAntialiasing()
}
}
/** Get the screen's HTML5 canvas context
* @method
* @returns {CanvasRenderingContext2D} - The canvas context
*/
getContext = () => {
return this.context;
}
/** Alias for setAntialiasing
* @see {gameify.Screen.setAntialiasing}
* @method
* @param {Boolean} enable - Whether smoothing should be enabled or not (true/false)
* @deprecated
*/
setSmoothImages = (value) => {
this.context.imageSmoothingEnabled = value;
}
/** Turn antialiasing on or off (set to off for pixel art)
* @method
* @param {Boolean} enable - Whether antialiasing should be enabled.
*/
setAntialiasing = (value) => {
this.#antialiasingEnabled = value;
this.context.imageSmoothingEnabled = value;
}
/** Check if antialising is enabled (Note, also checks and corrects if
* the canvas element has the correct antialiasing setting)
* @method
* @returns {Boolean} - Whether antialising is enabled or not
*/
getAntialiasing = () => {
this.context.imageSmoothingEnabled = this.#antialiasingEnabled;
return this.#antialiasingEnabled;
}
/** Clear the screen
* @method
* @arg {String} [color] - The color to clear to, e.g. #472d3c or rgb(123, 123, 123). Default is transparent
*/
clear = (color) => {
this.context.save();
this.context.setTransform(1, 0, 0, 1, 0, 0);
this.context.clearRect(0, 0, this.width, this.height);
if (color) {
this.context.beginPath();
this.context.rect(0, 0, this.width, this.height);
this.context.fillStyle = color;
this.context.fill();
}
// Restore transformations
this.context.restore();
}
/** Changes the width of the Screen
* @method
* @param {Number} width - The new width of the Screen
*/
setWidth = (width) => {
width = Number(width);
this.width = width;
this.element.width = width;
}
/** Changes the height of the Screen
* @method
* @param {Number} height - The new height of the screen
*/
setHeight = (height) => {
height = Number(height);
this.height = height;
this.element.height = height;
}
/** Changes the size of the Screen
* @method
* @param {Number} width - The new width of the screen
* @param {Number} height - The new height of the screen
*//** Changes the size of the Screen
* @method
* @param {gameify.Vector2d} size - The new size of the screen
*/
setSize = (width, height) => {
if (width.x != undefined && width.y != undefined) {
// Convert vector to
height = width.y;
width = width.x;
}
this.setWidth(width);
this.setHeight(height);
}
/** Get the width and height of the screen
* @method
* @returns {gameify.Vector2d} A vector representing the size of the screen
*/
getSize = () => {
return new vectors.Vector2d(this.width, this.height);
}
/** Sets the game's scene
* @method
* @param {gameify.Scene} scene - The scene to set the game to.
*/
setScene = (scene) => {
if (this.currentScene && this.currentScene.locked) {
console.warn("The current scene is locked and cannot be changed: " + this.currentScene.locked);
return;
}
if (this.currentScene) this.currentScene.onUnload();
this.currentScene = scene;
this.currentScene.onLoad(this);
}
/** Returns the game's active scene
* @method
* @returns {gameify.Scene} The active scene
*/
getScene = () => { return this.currentScene; }
/** Add a Sprite to the Screen. This makes it so that sprite.draw(); draws to this screen.
* @method
* @param {gameify.Sprite | gameify.Tilemap} obj - The object to add to the screen
*/
add = (obj) => {
obj.setContext(this.getContext(), this);
}
/** Starts the game.
* @method
*/
startGame = () => {
if (this.currentScene == null) {
throw new Error(`You need to set a Scene before you can start the game. See ${gameify.getDocs("gameify.Scene")} for details`);
}
if (this.#gameActive) {
console.warn('The game is already started!');
return;
}
this.#gameActive = true;
this.#lastUpdate = 0;
const eachFrame = async (time) => {
if (!this.#lastUpdate) {
this.#lastUpdate = time;
}
// max delta of 1000/5 = 200ms (5 fps)
const delta = Math.min(200, time - this.#lastUpdate);
this.#lastUpdate = time;
// if delta is zero, pass one instead (bc of div-by-zero errors)
this.currentScene.update(delta || 1);
this.camera.update(delta || 1);
this.currentScene.draw();
if (this.#gameActive) {
window.requestAnimationFrame(eachFrame);
}
}
window.requestAnimationFrame(eachFrame);
}
/** Stops (pauses) the game
* @method
*/
stopGame = () => {
this.#gameActive = false;
}
},
/** A Tileset for use with Tilemaps, Sprites, etc
* @example let myTileset = new gameify.Tileset("images/tileset.png");
* // Give the coordinates of a tile to retrieve it
* let grassTile = myTileset.getTile(3, 2);
* @arg {String} path - The image/tileset filepath
* @arg {Number} twidth - The width of each tile
* @arg {Number} theight - The height of each tile
*/
Tileset: class {
constructor(path, twidth, theight) {
this.path = path;
this.#twidth = Number(twidth);
this.#theight = Number(theight);
this.texture = document.createElement("img");
this.texture.src = path;
this.#pathName = path;
if (path.length > 50) {
this.#pathName = this.#pathName = path.slice(0, 40) + '...';
}
this.texture.onerror = () => {
throw new Error(`Your image "${this.#pathName}" couldn't be loaded. Check the path, and make sure you don't have any typos.`);
}
this.texture.onload = () => {
console.info(`Loaded image "${this.#pathName}"`)
this.loaded = true;
if (this.#loadFunction) { this.#loadFunction(); }
}
}
path;
#twidth;
#theight;
/** The Tilemap's tile width
* @readonly
* @member
* @name gameify.Tileset#twidth
*/
get twidth() { return this.#twidth; }
set twidth(value) {
console.warn('Setting Tilemap.twidth may not update immediately. It is not reccomended that you hot-update the tile size.')
this.#twidth = value;
}
/** The Tilemap's tile height
* @readonly
* @member
* @name gameify.Tileset#theight
*/
get theight() { return this.#theight; }
set theight(value) {
console.warn('Setting Tilemap.theight may not update immediately. It is not reccomended that you hot-update the tile size.')
this.#theight = value;
}
loaded = false;
/** The tileset's image/texture
* @package
*/
texture;
#pathName;
#loadFunction = undefined;
/** Creates a object from JSON data
* @method
* @arg {Object|Array} data - Serialized object data (from object.toJSON)
* @arg {Function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {gameify.Tileset}
*/
static fromJSON = (data, find) => {
if (Array.isArray(data)) {
// Be backwards compatible
console.warn('Save is using the old (de)serialization format for Tileset.');
const obj = new gameify.Tileset(...data);
return obj;
}
const obj = new gameify.Tileset(data.path, data.twidth, data.theight);
obj.#setAllTags(data.tags || {});
for (const tile of data.collisionShapes || []) {
obj.setCollisionShape(shapes.Shape.fromJSON(tile.shape), tile.x, tile.y);
}
return obj;
}
/** Convert the object to JSON
* @method
* @arg {string} [key] - Key object is stored under (unused, here for consistency with e.g. Date.toJSON, etc.)
* @arg {function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {Object}
*/
toJSON = (key, ref) => {
const colShapes = [];
for (const tilex in this.#collisionShapes) {
for (const tiley in this.#collisionShapes[tilex]) {
if (!this.#collisionShapes[tilex][tiley]) continue;
colShapes.push({
x: Number(tilex),
y: Number(tiley),
shape: this.#collisionShapes[tilex][tiley].toJSON()
});
}
}
return {
path: this.path,
twidth: this.twidth,
theight: this.theight,
collisionShapes: colShapes,
tags: this.#tags
};
}
#collisionShapes = {};
#tags = {};
/** Add a collision shape to the tileset
* @method
* @arg {gameify.shapes.Shape} shape
* @arg {Number} tilex
* @arg {Number} tiley
*//** Add a collision shape to the tileset
* @method
* @arg {gameify.shapes.Shape} shape
* @arg {gameify.Vector2d} tilepos
*/
setCollisionShape = (shape, tilex, tiley) => {
if (tiley === undefined) {
tiley = tilex.y;
tilex = tilex.x;
}
if (!this.#collisionShapes[tilex]) {
this.#collisionShapes[tilex] = {};
}
this.#collisionShapes[tilex][tiley] = shape;
}
/** Remove a collision shape from the tileset
* @method
* @arg {Number} tilex
* @arg {Number} tiley
*//** Remove a collision shape from the tileset
* @method
* @arg {gameify.Vector2d} tilepos
*/
removeCollisionShape = (tilex, tiley) => {
if (tiley === undefined) {
tiley = tilex.y;
tilex = tilex.x;
}
if (this.#collisionShapes[tilex] && this.#collisionShapes[tilex][tiley]) {
delete this.#collisionShapes[tilex][tiley];
}
}
/** Add a tag to a tile in the tileset
* @method
* @arg {String} tag
* @arg {Number} tilex
* @arg {Number} tiley
*//** Add a tag to a tile in the tileset
* @method
* @arg {String} tag
* @arg {gameify.Vector2d} tilepos
*/
addTag(tag, tilex, tiley) {
if (tiley === undefined) {
tiley = tilex.y;
tilex = tilex.x;
}
if (!this.#tags[tilex]) {
this.#tags[tilex] = {};
}
if (!this.#tags[tilex][tiley]) {
this.#tags[tilex][tiley] = [];
}
// Don't add a tag twice
if (this.#tags[tilex][tiley].includes(tag)) return;
this.#tags[tilex][tiley].push(tag);
}
/** Set the entire tags object for the tileset
* @method
* @arg {Object} tags
* @private
*/
#setAllTags = (tags) => {
this.#tags = tags;
}
/** Remove a tag from a tile in the tileset
* @method
* @arg {String} tag
* @arg {Number} tilex
* @arg {Number} tiley
*//** Remove a tag from a tile in the tileset
* @method
* @arg {String} tag
* @arg {gameify.Vector2d} tilepos
*/
removeTag(tag, tilex, tiley) {
if (tiley === undefined) {
tiley = tilex.y;
tilex = tilex.x;
}
if (this.#tags[tilex] && this.#tags[tilex][tiley]) {
this.#tags[tilex][tiley] = this.#tags[tilex][tiley].filter(t => t !== tag);
}
}
/** Get a tile (or section of tiles) from the tileset. Returns a new Image object each time, so if you're getting
* the same tile a lot you might want to save it to a variable
* @method
* @param {Number} x - The x coordinate of the tile
* @param {Number} y - The y coordinate of the tile
* @param {Number} [width=1] - How many tiles wide
* @param {Number} [height=1] - How many tiles tall
* @returns {gameify.Image}
*/
getTile = (x, y, width = 1, height = 1) => {
const tile = new gameify.Image();
tile.tileData = {
tileset: this,
position: new vectors.Vector2d(x, y),
size: new vectors.Vector2d(width, height),
collisionShape: this.#collisionShapes[x]?.[y],
tags: this.#tags[x]?.[y]
}
tile.texture = this.texture;
tile.crop(x * this.twidth, y * this.theight, this.twidth*width, this.theight*height);
return tile;
}
/** Change and load a new image path. Please note this does not clear
* tilemaps' cached data, and it might retain its the original image.
* @method
* @param {string} path - The new tileset image path
*/
changePath = (path) => {
this.path = path;
const ni = new gameify.Tileset(path, this.twidth, this.theight);
ni.onLoad(() => {
this.texture = ni.texture;
if (this.#loadFunction) { this.#loadFunction(); }
});
}
/** Set a function to be run when the image is loaded
* @method
* @param {function} callback - The function to be called when the image is loaded.
*/
onLoad = (callback) => { this.#loadFunction = callback; }
},
/** A Tile as part of a Tilemap (not a tile from a Tileset)
* @arg {Number} x - The x coordinate of the tile
* @arg {Number} y - The y coordinate of the tile
* @arg {Number} sourcex - The source x coordinate of the tile
* @arg {Number} sourcey - The source y coordinate of the tile
* @arg {gameify.Image} image - The tile's Image (reference)
* @arg {Number} [rotation=0] - The rotation of the tile, in degrees
* @arg {Number} [width=1] - The width (in tiles) of the tile
* @arg {Number} [height=1] - The height (in tiles) of the tile
* @arg {Number} [twidth=1] - The width (in pixels) of the tile on the tilemap
*/
Tile: class {
constructor (x, y, sx, sy, image, r = 0, width = 1, height = 1, twidth = 1) {
this.image = image;
this.position = new gameify.Vector2d(x, y);
this.source = new gameify.Vector2d(sx, sy);
this.size = new gameify.Vector2d(width, height);
this.rotation = r;
this.tags = image.tileData?.tags || [];
// Be smart and extract the collision shape
// from the image tiledata
// TODO image.tileData really is a bad pattern and needs to go.
this.#shape = image.tileData?.collisionShape;
if (this.#shape) {
const tilesetSize = new vectors.Vector2d(
image.tileData.tileset.twidth,
image.tileData.tileset.theight
);
const tileCenter = tilesetSize.multiply(0.5);
const oldType = this.#shape.type;
this.#shape = this.#shape.rotated(this.rotation*(Math.PI/180), tileCenter);
this.#shape = this.#shape.scaled(twidth/image.tileData.tileset.twidth);
// This will not work if the size ratios of the tileset and map are different
this.#shape.position = this.#shape.position.add(this.position.multiply(twidth));
}
}
/** Creates a Tile from JSON data
* @method
* @arg {Object|Array} data - Serialized object data (from object.toJSON)
* @arg {Function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {gameify.Tile}
*/
static fromJSON = (data, find) => {
const obj = new gameify.Tile(
data.position.x, data.position.y,
data.source.x, data.source.y,
find(data.image),
data.rotation,
data.size.x || 1, data.size.y || 1
);
return obj;
}
/** Convert the object to JSON
* @method
* @arg {string} [key] - Key object is stored under (unused, here for consistency with e.g. Date.toJSON, etc.)
* @arg {function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {Object}
*/
toJSON = (key, ref) => {
return {
image: ref(this.image),
position: this.position.toJSON(),
source: this.source.toJSON(),
rotation: this.rotation
// tags and collisionshape are stored in the image
};
}
/** The tile's Image
* @type {gameify.Image}
*/
image;
/** The tile's position in the tilemap
* @type {gameify.Vector2d}
*/
position;
/** The tile's source coordinates
* @type {gameify.Vector2d}
*/
source;
/** The tile's size (in tiles)
* @type {gameify.Vector2d}
*/
size;
/** The tile's rotation
* @type {Number}
*/
rotation;
/** Any tags that the tile has
* @type {String[]}
*/
tags;
/** Check if the tile has a tag
* @method
* @arg {String} tag - The tag to check for
* @returns {boolean}
*/
hasTag = (tag) => { return this.tags.includes(tag); }
#shape;
/** The tile's collision shape
* @type {gameify.shapes.Shape}
* @name gameify.Tile.shape
* @readonly
*/
get shape() { return this.#shape; }
},
/** Class representing a Tilemap of rectangular tiles
* @example // ...
* // make a new tileset with 8x8 pixels
* let forestTileset = new gameify.Tileset("images/forest.png", 8, 8);
* // make a new tilemap with 16x16 tiles and no offset
* // anything that's not 16x16 will be scaled to match
* let forestMap = new gameify.Tilemap(16, 16, 0, 0);
* forsetMap.setTileset(forestTileset);
*
* // make sure to add it to the screen
* myScreen.add(tileset);
*
* forestScene.onDraw(() => {
* // ...
* // Draw the tilemap
* forsetMap.draw();
* });
* @arg {Number} twidth - The width of the tiles
* @arg {Number} theight - The height of the tiles
* @arg {Number} [offsetx=0] - X offset of the tiles
* @arg {Number} [offsety=0] - Y offset of the tiles
*/
// Dev's Note: There are a LOT of places in loops where row and col are reversed
// this.tiles.placed[x][y] means that looping through this.tiles.placed
// actually loops through each column, and I was dumb and got this backwards.
// Some are correct, because I realised it -- but be careful
Tilemap: class {
constructor (twidth, theight, offsetx, offsety) {
this.#twidth = Number(twidth);
this.#theight = Number(theight);
this.offset = new vectors.Vector2d(offsetx || 0, offsety || 0);
}
#twidth;
#theight;
// We'll pretend twidth/theight in maps is readonly for the sake of docs.
// In reality, it doesn't hurt anything, so I'm not adding a warning like in Tileset
/** The Tileset's tile width
* @readonly
* @member
* @name gameify.Tileset#twidth
*/
get twidth() { return this.#twidth; }
set twidth(value) { this.#twidth = value; }
/** The Tileset's tile height
* @readonly
* @member
* @name gameify.Tileset#theight
*/
get theight() { return this.#theight; }
set theight(value) { this.#theight = value; }
/** The tile offset (coordinates of tile <0, 0>). Used to translate the map
* @type {gameify.Vector2d}
*/
offset;
/** The Canvas context to draw to */
context = null;
/** The parent screen (not used directly) */
parent = null;
// placed is an object so there can be negative indexes
tiles = { placed: {} };
tileset = undefined;
#drawFunction = null;
#warnedNotIntegerCoordinates = false;
/** Creates a Tilemap from JSON data
* @method
* @arg {Object|Array} data - Serialized Tilemap data (from Tilemap.toJSON)
* @arg {Function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {gameify.Tilemap}
*/
static fromJSON = (data, find) => {
if (Array.isArray(data)) {
// Be backwards compatible
console.warn('Save is using the old (de)serialization format for Tilemap.');
const obj = new gameify.Tilemap(data[0], data[1], data[2].x, data[2].y);
if (data[3]) {
obj.setTileset(find(data[3]));
obj.loadMapData(data[4]);
}
if (data[5]) find(data[5]).add(obj); // Add to Screen
return obj;
}
const obj = new gameify.Tilemap(
data.twidth, data.theight,
data.offset.x, data.offset.y
);
if (data.tileset) {
obj.setTileset(find(data.tileset));
obj.loadMapData(data.mapData);
}
if (data.parent) find(data.parent).add(obj); // Add to Screen
return obj;
}
/** Convert the Tilemap to JSON
* @method
* @arg {string} [key] - Key object is stored under (unused, here for consistency with e.g. Date.toJSON, etc.)
* @arg {function} ref - A function that returns a name for other objects, so they can be restored later
* @returns {Object}
*/
toJSON = (key, ref) => {
return {
twidth: this.twidth,
theight: this.theight,
offset: this.offset.toJSON(),
tileset: ref(this.tileset),
mapData: this.exportMapData(),
parent: ref(this.parent),
};
}
/** Clear the tilemap. Removes all placed tiles and cached images
* @method
*/
clear = () => {
this.tiles = { placed: {} };
}
/** What tileset to use. This tileset must include anything you want to use in this tilemap.
* @method
* @param {gameify.Tileset} set - The tileset
*/
setTileset = (set) => {
this.tileset = set;
}
/** Get the tilemap's tileset
* @method
* @returns {gameify.Tileset} The tileset
*/
getTileset = () => {
return this.tileset;
}
/** Set the draw function for this tilemap
* @method
* @param {function} callback - The function to be called right before the tilemap is drawn
*/
onDraw = (callback) => {
this.#drawFunction = callback;
}
/** Convert world coordinates to map coordinates
* @method
* @param {Number} screenx - The screen x coordinate
* @param {Number} [screeny] - The screen y coordinate
* @returns {gameify.Vector2d} A vector representing the calculated position
*//** Convert world coordinates to map coordinates
* @method
* @param {Object | gameify.Vector2d} position - A vector OR an object containing both x any y coordinates
* @returns {gameify.Vector2d} A vector representing the calculated position
*/
worldToMap = (screenx, screeny) => {
// loose comparison because we don't want any null values
if (screenx.x != undefined && screenx.y != undefined) {
screeny = screenx.y;
screenx = screenx.x;
}
return new vectors.Vector2d(
Math.floor((screenx - this.offset.x) / this.twidth),
Math.floor((screeny - this.offset.y) / this.theight)
);
}
/** Convert world coordinates to map coordinates. Use worldToMap instead
* @method
* @deprecated
* @alias gameify.Tilemap.worldToMap
*/
screenToMap = (screenx, screeny) => {
console.warn('screenToMap is deprecated, use worldToMap instead.');
return this.worldToMap(screenx, screeny);
}
/** Convert map coordinates to world coordinates
* @method
* @param {Number} mapx - The map x coordinate
* @param {Number} [mapy] - The map y coordinate
* @returns {Object} {gameify.Vector2d} A vector representing the calculated position
*//** Convert map coordinates to world coordinates
* @method
* @param {Object | gameify.Vector2d} position - A vector OR an object containing both x any y coordinates
* @returns {gameify.Vector2d} A vector representing the calculated position
*/
mapToWorld = (mapx, mapy) => {
// loose comparison because we don't want any null values
if (mapx.x != undefined && mapx.y != undefined) {
mapy = mapx.y;
mapx = mapx.x;
}
return new vectors.Vector2d(
(mapx * this.twidth) + this.offset.x,
(mapy * this.theight) + this.offset.y
);
}
/** Converts map coordinates to world coordinates. Use mapToWorld instead
* @method
* @deprecated
* @alias gameify.Tilemap.mapToWorld
*/
mapToScreen = (mapx, mapy) => {
console.warn('mapToScreen is deprecated, use mapToWorld instead.');
return this.mapToWorld(screenx, screeny);
}
/** Place a tile on the tilemap
* @method
* @param {Number} originx - The x position of the tile on the tilesheet
* @param {Number} originy - The y position of the tile on the tilesheet
* @param {Number} destx - The x position to place the tile
* @param {Number} desty - The y position to place the tile
* @param {Number} [rotation=0] - Tile rotation, in degrees
* @param {Number} [width=1] - Tile width, relative to single tile width
* @param {Number} [height=1] - Tile height, relative to single tile height
*/
place = (originx, originy, destx, desty, rotation = 0, width = 1, height = 1) => {
if (!this.tileset) {
throw new Error("You can't place a tile before setting a tileset.");
}
const tileCacheString = `${originx},${originy}/${width},${height}`
// "cache" tiles as to not create a new Image for every single placed tile.
if (!this.tiles[tileCacheString]) {
this.tiles[tileCacheString] = this.tileset.getTile(originx, originy, width, height);
}
if (!this.tiles.placed[destx]) {
// an object so there can be negative indexes
this.tiles.placed[destx] = {};
}
// add the tile to the list of placed tiles
this.tiles.placed[destx][desty] = new gameify.Tile(
destx, desty, // destination position
originx, originy, // source position
this.tiles[tileCacheString], // gameify.Image
rotation || 0, // rotation
width, height , // size
this.twidth // tilemap size
)
}
/** Get the tile (if it exists) placed at a certain position
* @method
* @param {Number} x - X coordinate of the tile
* @param {Number} y - Y coordinate of the tile
* @return {gameify.Tile}
*/
get = (x, y) => {
if (this.tiles.placed[x] && this.tiles.placed[x][y]) {
return this.tiles.placed[x][y];
} else return undefined;
}
/** Get all tiles within x tiles (square, not radius) of the given tile
* @method
* @param {Number} x - X coordinate of the center tile
* @param {Number} y - Y coordinate of the center tile
* @param {Number} distance - radius of the square of tiles
* @returns {gameify.Tile[]}
*/
getTilesNear = (targetx, targety, distance) => {
let arr = [];
for (let x = targetx-distance; x < targetx+distance; x++) {
for (let y = targety-distance; y < targety+distance; y++) {
const tile = this.get(x, y);
// Only return tiles that exist
if (tile) arr.push(tile);
}
}
return arr;
}
/** Get an array of all the tiles in the map
* @method
* @return {gameify.Tile[]}
*/
listTiles = () => {
const out = [];
for (const x in this.tiles.placed) {
for (const y in this.tiles.placed[x]) {
out.push(this.tiles.placed[x][y]);
}
}
return out;
}
/** Remove a tile from the tilemap
* @method
* @param {Number} x - The x coord of the tile to remove
* @param {Number} y - The y coord of the tile to remove
*//** Remove a tile from the tilemap
* @method
* @param {gameify.Tile} tile - The tile to remove
*//** Remove a tile from the tilemap
* @method
* @param {gameify.Vector2d} position - The position (map coordinate) of the tile to remove
*/
remove = (x, y) => {
if (x.position) {
// Tile
// (technically also works on other things with a position property, oh well.)
y = x.position.y;
x = x.position.x;
} else if (x.x && x.y) {
// Vector2d
y = x.y;
x = x.x;
}
if (this.tiles.placed[x] && this.tiles.placed[x][y]) {
delete this.tiles.placed[x][y];
}
}
/** Draw the tilemap to the screen
* @param {Function} [check] - A function to check if the tile should be drawn (calls check(tile, x, y))
* @method
*/
draw = (check = (t, x, y) => true) => {
if (this.#drawFunction) {
this.#drawFunction();
}
if (!this.context) {
throw new Error(`You need to add this tilemap to a screen before you can draw it. See ${gameify.getDocs("gameify.Tilemap")} for more details`);
}
if (!this.#warnedNotIntegerCoordinates &&
( Math.round(this.offset.x) !== this.offset.x
|| Math.round(this.offset.y) !== this.offset.y)
) {
this.#warnedNotIntegerCoordinates = true;
console.warn(`Timemap offset is not an integer. This can cause images
to contain artifacts (eg lines along the edge)`);
}
for (const row in this.tiles.placed) {
for (const col in this.tiles.placed[row]) {
// row = x, col = y, yes it's backwards
const tile = this.tiles.placed[row][col];
if (!check(tile, Number(row), Number(col))) continue
tile.image.draw(this.context,
row * this.twidth + this.offset.x, col * this.theight + this.offset.y,
this.twidth*tile.size.x, this.theight*tile.size.y,
tile.rotation, /* ignoreOpacity= */ true );
}
}
}
/** <b>Deprecated.</b> Use the engine editor to build your tilemaps. This editor is no longer maintained.<br>
* Enable the map builder tool. This allows you to easily edit tilesets.<br>
* Controls are: Click to place, Right-click to delete, Middle-click to pick, Scroll and Ctrl+Scroll to switch tile, Shift+Scroll to rotate the tile.<br>
* Once you're finished, call <code>tilemap.exportMapData()</code> to export the map.
* @deprecated Use the engine editor to build and export your tilemaps. This editor is no longer maintained.
* @method
* @param {gameify.Screen} screen - The screen to show the map builder on. For best results, use the one you've already added it to.
*/
enableMapBuilder = (screen) => {
let mainScene = new gameify.Scene(screen);
mainScene.lock("The Tilemap builder is currently enabled.");
let selectedTile = {
x: 0, y: 0,
rotation: 0
};
let textureImage = new gameify.Image(this.tileset.texture.src);
// Start position for movement
let dragging = false;
let originalOffset = this.offset.copy();
let dragStartPos = new vectors.Vector2d(0, 0);
mainScene.onUpdate(() => {
if (screen.mouse.buttonIsPressed("left")) {
const pos = this.screenToMap(screen.mouse.getPosition());
this.place(selectedTile.x, selectedTile.y, pos.x, pos.y, selectedTile.rotation);
} else if (screen.mouse.buttonIsPressed("right")) {
const pos = this.screenToMap(screen.mouse.getPosition());
this.remove(pos.x, pos.y);
}
if (screen.mouse.buttonIsPressed("middle")) {
const pos = this.screenToMap(screen.mouse.getPosition());
if (!dragging) {
originalOffset = this.offset.copy();
dragStartPos = screen.mouse.getPosition();
}
dragging = true;
this.offset = originalOffset.add(screen.mouse.getPosition().subtract(dragStartPos)).truncated(2);
const tile = this.get(pos.x, pos.y);
if (tile) {
selectedTile.x = tile.source.x;
selectedTile.y = tile.source.y;
selectedTile.rotation = tile.rotation;
}
} else {
dragging = false;
}
if (screen.keyboard.keyWasJustPressed("Enter")) {
this.exportMapData();
mainScene.unlock();
}
if (screen.mouse.eventJustHappened("wheeldown")) {
if (screen.keyboard.keyIsPressed("Shift")) {
selectedTile.rotation += 45;
return;
} else if (screen.keyboard.keyIsPressed("Control")) {
selectedTile.y += 1;
// go to the next row if it's at the end
if (selectedTile.y * this.theight >= this.tileset.texture.height) {
selectedTile.y = 0;
selectedTile.x += 1;
// loop to the beginning
if (selectedTile.x * this.twidth >= this.tileset.texture.width) {
selectedTile.x = 0;
}
}
return;
}
selectedTile.x += 1;
// go to the next row if it's at the end
if (selectedTile.x * this.twidth >= this.tileset.texture.width) {
selectedTile.x = 0;
selectedTile.y += 1;
// loop to the beginning
if (selectedTile.y * this.theight >= this.tileset.texture.height) {
selectedTile.y = 0;
}
}
} else if (screen.mouse.eventJustHappened("wheelup")) {
if (screen.keyboard.keyIsPressed("Shift")) {
selectedTile.rotation -= 45;
return;
} else if (screen.keyboard.keyIsPressed("Control")) {
selectedTile.y -= 1;
// go to the previous row if it's at the beginning
if (selectedTile.y < 0) {
selectedTile.y = Math.floor(this.tileset.texture.height / this.theight) - 1;
selectedTile.x -= 1;
// loop to the end
if (selectedTile.x < 0) {
selectedTile.x = Math.floor(this.tileset.texture.width / this.twidth) - 1;
}
}
return;
}
selectedTile.x -= 1;
// go to the previous row if it's at the beginning
if (selectedTile.x < 0) {
selectedTile.x = Math.floor(this.tileset.texture.width / this.twidth) - 1;
selectedTile.y -= 1;
// loop to the end
if (selectedTile.y < 0) {
selectedTile.y = Math.floor(this.tileset.texture.height / this.theight) - 1;
}
}
}
});
mainScene.onDraw(() => {
screen.clear();
this.draw();
const pos = this.screenToMap(screen.mouse.getPosition());
const previewImage = this.tileset.getTile(selectedTile.x, selectedTile.y);
const crop = previewImage.getCrop();
// draw the preview transulcent
this.context.globalAlpha = 0.5;
previewImage.draw(this.context,
pos.x * this.twidth + this.offset.x,
pos.y * this.theight + this.offset.y,
this.twidth, this.theight,
selectedTile.rotation );
textureImage.draw( this.context, 0, 0,
textureImage.texture.width / this.tileset.twidth * 25,
textureImage.texture.height / this.tileset.theight * 25 );
this.context.globalAlpha = 1;
// highlight the hovered square
const padding = 2;
this.context.beginPath();
this.context.rect(pos.x * this.twidth - padding + this.offset.x,
pos.y * this.theight - padding + this.offset.y,
this.twidth + (padding*2),
this.theight + (padding*2));
// for the "minimap"
this.context.rect(selectedTile.x * 25, selectedTile.y * 25, 25, 25);
this.context.stroke();
});
screen.setScene(mainScene);
return mainScene;
}
/** Export this tilemap's map data and layout (load with loadMapData)
* @method
* @returns {object} The map data as JSON
*/
exportMapData = () => {
let output = [];
for (const col in this.tiles.placed) {
for (const row in this.tiles.placed[col]) {
const tile = this.tiles.placed[col][row];
// use as little text as possible, this will be saved to a JSON string
// Because this will be repeated possibly hundreds of times
output.push({
s: [tile.source.x, tile.source.y],
p: [Number(col), Number(row)],
r: tile.rotation
});
}
}
return output;
}
/** Download a .tilemapdata.js file of the tilemap. This file can be imported as an es6 module
* @example
* myMap.downloadMapData();
* @example
* import myMapData from './mymap.tilemapdata.js';
* myMap.loadMapData(myMapData);
* @method
*/
downloadMapData = () => {
const text = 'export default' + JSON.stringify(this.exportMapData(), null, 2);
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', `${this.__engine_name || 'mymap'}.tilemapdata.js`);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/** Load saved map data (export using exportMapData)
* @method
* @param {Object} data - The map data to load
*/
loadMapData = (data) => {
for (const tile of data) {
this.place(tile.s[0], tile.s[1], tile.p[0], tile.p[1], tile.r);
}
}
/** Clear cached images (use after updating a tileset).
* Clears all placed tiles, and re-adds them from the current tileset.
* This operation can be slow and is intended for infrequent changes (so don't run it every frame).
* @method
*/
refreshCachedImages = () => {
const mapData = this.exportMapData();
this.clear();
this.loadMapData(mapData);
}
/** Get the screen this sprite draws
* @method
* @returns {gameify.Screen}
*/
getParent = () => {
return this.parent;
}
/** Set the Canvas context to draw to. Should be called by a screen when the Tilemap is added to it.
* @method
*/
setContext = (context, parent) => {
this.context = context;
this.parent = parent;
}
}
};
/** This is a mostly complete list of mouse and keyboard input events supported by gameify. Most event names are case-sensitive
* @global
* @example // ----------------
* // Mouse Buttons
* // ----------------
* if (myScreen.mouse.buttonIsPressed( BUTTON_NAME )) {
* // do something
* }
* // Valid buttons are:
* 0 "left"
* 2 "right"
* 1 "middle"
* @example // ----------------
* // Mouse Events
* // ----------------
* if (myScreen.mouse.eventJustHappened( EVENT_NAME )) {
* // do something
* }
* // Valid events are:
* 0 "left"
* 2 "right"
* 1 "middle"
* "leave" "mouseout"
* "move" "movemove"
* "wheelup"
* "wheeldown"
* "wheel"
* "doubleclick"
* @example // ----------------
* // Keyboard Buttons
* // ----------------
* if (myScreen.keyboard.keyIsPressed( KEY_NAME )) {
* // do something
* }
* // Valid keys are: (On a standard US English keyboard)
* // Letter keys
* "A" "KeyA"
* // through
* "Z" "KeyZ"
*
* // Other keys
* "Backquote" "Minus" "Equal" "BracketLeft"
* "BracketRight" "Backslash" "Semicolon" "Quote"
* "Period" "Comma" "Slash"
*
* // Numbers
* "0" "Digit0" "Numpad0"
* // through
* "9" "Digit9" "Numpad9"
* // You can check if a key is upper or lowercase by checking for the Shift key
*
* // Arrow keys
* "Up" "ArrowUp"
* "Down" "ArrowDown"
* "Left" "ArrowLeft"
* "Right" "ArrowRight"
*
* // Special keys
* "Shift" "ShiftLeft" "ShiftRight"
* "Control" "ControlLeft" "ControlRight"
* "Alt" "AltLeft" "AltRight"
* "OS" "OSLeft" "OSRight"
* "Enter" "NumpadEnter" "Backspace" "CapsLock"
* "Tab" "PageUp" "PageDown" "Home"
* "End" "Delete" "Insert"
*
* // Function keys
* "F1"
* // through
* "F15" // most keyboards only have F1-F12
*
* // Numpad keys
* "NumpadDivide" "NumpadMultiply" "NumpadSubtract"
* "NumpadAdd" "NumpadDecimal"
*/
// This is an empty object, that only exists for the documentation.
export let inputEventsTables = {};