From c6d3a42ef24c2afbad6828befa814955b610fa1b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Tue, 23 Mar 2021 18:03:33 +1200 Subject: [PATCH] :sparkles: Optionally make a camera stay inside a specific rectangle with new rooms' settings. --- app/data/ct.release/camera.js | 694 ++++++++++++---------- app/data/ct.release/rooms.js | 6 + app/data/i18n/English.json | 7 +- src/node_requires/exporter/rooms.js | 25 + src/riotTags/app-view.tag | 1 - src/riotTags/rooms/room-editor.tag | 91 ++- src/riotTags/shared/extensions-editor.tag | 2 +- 7 files changed, 482 insertions(+), 344 deletions(-) diff --git a/app/data/ct.release/camera.js b/app/data/ct.release/camera.js index 06b8b90ec..ffd30eef8 100644 --- a/app/data/ct.release/camera.js +++ b/app/data/ct.release/camera.js @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ /** * This class represents a camera that is used by ct.js' cameras. * Usually you won't create new instances of it, but if you need, you can substitute @@ -48,8 +47,10 @@ * relative to the screen's max side (100 is 100% of screen shake). * If set to 0 or less, it, disables the effect. * @property {number} shakePhase The current phase of screen shake oscillation. - * @property {number} shakeDecay The amount of `shake` units substracted in a second. Default is 5. - * @property {number} shakeFrequency The base frequency of the screen shake effect. Default is 50. + * @property {number} shakeDecay The amount of `shake` units substracted in a second. + * Default is 5. + * @property {number} shakeFrequency The base frequency of the screen shake effect. + * Default is 50. * @property {number} shakeX A multiplier applied to the horizontal screen shake effect. * Default is 1. * @property {number} shakeY A multiplier applied to the vertical screen shake effect. @@ -57,357 +58,398 @@ * @property {number} shakeMax The maximum possible value for the `shake` property * to protect players from losing their monitor, in `shake` units. Default is 10. */ -class Camera extends PIXI.DisplayObject { - constructor(x, y, w, h) { - super(); - this.follow = this.rotate = false; - this.followX = this.followY = true; - this.targetX = this.x = x; - this.targetY = this.y = y; - this.z = 500; - this.width = w || 1920; - this.height = h || 1080; - this.shiftX = this.shiftY = this.interpolatedShiftX = this.interpolatedShiftY = 0; - this.borderX = this.borderY = null; - this.drift = 0; - - this.shake = 0; - this.shakeDecay = 5; - this.shakeX = this.shakeY = 1; - this.shakeFrequency = 50; - this.shakePhase = this.shakePhaseX = this.shakePhaseY = 0; - this.shakeMax = 10; - - this.getBounds = this.getBoundingBox; - } - - get scale() { - return this.transform.scale; - } - set scale(value) { - if (typeof value === 'number') { - value = { - x: value, - y: value - }; +const Camera = (function Camera() { + const shakeCamera = function shakeCamera(camera, delta) { + const sec = delta / (PIXI.Ticker.shared.maxFPS || 60); + camera.shake -= sec * camera.shakeDecay; + camera.shake = Math.max(0, camera.shake); + if (camera.shakeMax) { + camera.shake = Math.min(camera.shake, camera.shakeMax); } - this.transform.scale.copyFrom(value); - } + const phaseDelta = sec * camera.shakeFrequency; + camera.shakePhase += phaseDelta; + // no logic in these constants + // They are used to desync fluctuations and remove repetitive circular movements + camera.shakePhaseX += phaseDelta * (1 + Math.sin(camera.shakePhase * 0.1489) * 0.25); + camera.shakePhaseY += phaseDelta * (1 + Math.sin(camera.shakePhase * 0.1734) * 0.25); + }; + const followCamera = function followCamera(camera) { + // eslint-disable-next-line max-len + const bx = camera.borderX === null ? camera.width / 2 : Math.min(camera.borderX, camera.width / 2), + // eslint-disable-next-line max-len + by = camera.borderY === null ? camera.height / 2 : Math.min(camera.borderY, camera.height / 2); + const tl = camera.uiToGameCoord(bx, by), + br = camera.uiToGameCoord(camera.width - bx, camera.height - by); - /** - * Moves the camera to a new position. It will have a smooth transition - * if a `drift` parameter is set. - * @param {number} x New x coordinate - * @param {number} y New y coordinate - * @returns {void} - */ - moveTo(x, y) { - this.targetX = x; - this.targetY = y; - } + if (camera.followX) { + if (camera.follow.x < tl[0] - camera.interpolatedShiftX) { + camera.targetX = camera.follow.x - bx + camera.width / 2; + } else if (camera.follow.x > br[0] - camera.interpolatedShiftX) { + camera.targetX = camera.follow.x + bx - camera.width / 2; + } + } + if (camera.followY) { + if (camera.follow.y < tl[1] - camera.interpolatedShiftY) { + camera.targetY = camera.follow.y - by + camera.height / 2; + } else if (camera.follow.y > br[1] - camera.interpolatedShiftY) { + camera.targetY = camera.follow.y + by - camera.height / 2; + } + } + }; + const restrictInRect = function restrictInRect(camera) { + if (camera.minX !== void 0) { + camera.x = Math.max(camera.minX + camera.width * camera.scale.x * 0.5, camera.x); + camera.targetX = Math.max(camera.minX, camera.targetX); + } + if (camera.maxX !== void 0) { + camera.x = Math.min(camera.maxX - camera.width * camera.scale.x * 0.5, camera.x); + camera.targetX = Math.min(camera.maxX, camera.targetX); + } + if (camera.minY !== void 0) { + camera.y = Math.max(camera.minY + camera.height * camera.scale.y * 0.5, camera.y); + camera.targetY = Math.max(camera.minY, camera.targetY); + } + if (camera.maxY !== void 0) { + camera.y = Math.min(camera.maxY - camera.height * camera.scale.y * 0.5, camera.y); + camera.targetY = Math.min(camera.maxY, camera.targetY); + } + }; + class Camera extends PIXI.DisplayObject { + constructor(x, y, w, h) { + super(); + this.follow = this.rotate = false; + this.followX = this.followY = true; + this.targetX = this.x = x; + this.targetY = this.y = y; + this.z = 500; + this.width = w || 1920; + this.height = h || 1080; + this.shiftX = this.shiftY = this.interpolatedShiftX = this.interpolatedShiftY = 0; + this.borderX = this.borderY = null; + this.drift = 0; - /** - * Moves the camera to a new position. Ignores the `drift` value. - * @param {number} x New x coordinate - * @param {number} y New y coordinate - * @returns {void} - */ - teleportTo(x, y) { - this.targetX = this.x = x; - this.targetY = this.y = y; - this.shakePhase = this.shakePhaseX = this.shakePhaseY = 0; - this.interpolatedShiftX = this.shiftX; - this.interpolatedShiftY = this.shiftY; - } + this.shake = 0; + this.shakeDecay = 5; + this.shakeX = this.shakeY = 1; + this.shakeFrequency = 50; + this.shakePhase = this.shakePhaseX = this.shakePhaseY = 0; + this.shakeMax = 10; - /** - * Updates the position of the camera - * @param {number} delta A delta value between the last two frames. This is usually ct.delta. - * @returns {void} - */ - update(delta) { - if (this.follow && this.follow.kill) { - this.follow = false; + this.getBounds = this.getBoundingBox; } - const sec = delta / (PIXI.Ticker.shared.maxFPS || 60); - this.shake -= sec * this.shakeDecay; - this.shake = Math.max(0, this.shake); - if (this.shakeMax) { - this.shake = Math.min(this.shake, this.shakeMax); + get scale() { + return this.transform.scale; + } + set scale(value) { + if (typeof value === 'number') { + value = { + x: value, + y: value + }; + } + this.transform.scale.copyFrom(value); } - const phaseDelta = sec * this.shakeFrequency; - this.shakePhase += phaseDelta; - // no logic in these constants - // They are used to desync fluctuations and remove repetitive circular movements - this.shakePhaseX += phaseDelta * (1 + Math.sin(this.shakePhase * 0.1489) * 0.25); - this.shakePhaseY += phaseDelta * (1 + Math.sin(this.shakePhase * 0.1734) * 0.25); - // The speed of drift movement - const speed = this.drift ? Math.min(1, (1 - this.drift) * delta) : 1; + /** + * Moves the camera to a new position. It will have a smooth transition + * if a `drift` parameter is set. + * @param {number} x New x coordinate + * @param {number} y New y coordinate + * @returns {void} + */ + moveTo(x, y) { + this.targetX = x; + this.targetY = y; + } - if (this.follow && ('x' in this.follow) && ('y' in this.follow)) { - // eslint-disable-next-line max-len - const bx = this.borderX === null ? this.width / 2 : Math.min(this.borderX, this.width / 2), - // eslint-disable-next-line max-len - by = this.borderY === null ? this.height / 2 : Math.min(this.borderY, this.height / 2); - const tl = this.uiToGameCoord(bx, by), - br = this.uiToGameCoord(this.width - bx, this.height - by); + /** + * Moves the camera to a new position. Ignores the `drift` value. + * @param {number} x New x coordinate + * @param {number} y New y coordinate + * @returns {void} + */ + teleportTo(x, y) { + this.targetX = this.x = x; + this.targetY = this.y = y; + this.shakePhase = this.shakePhaseX = this.shakePhaseY = 0; + this.interpolatedShiftX = this.shiftX; + this.interpolatedShiftY = this.shiftY; + } - if (this.followX) { - if (this.follow.x < tl[0] - this.interpolatedShiftX) { - this.targetX = this.follow.x - bx + this.width / 2; - } else if (this.follow.x > br[0] - this.interpolatedShiftX) { - this.targetX = this.follow.x + bx - this.width / 2; - } + /** + * Updates the position of the camera + * @param {number} delta A delta value between the last two frames. + * This is usually ct.delta. + * @returns {void} + */ + update(delta) { + shakeCamera(this, delta); + // Check if we've been following a copy that is now killed + if (this.follow && this.follow.kill) { + this.follow = false; } - if (this.followY) { - if (this.follow.y < tl[1] - this.interpolatedShiftY) { - this.targetY = this.follow.y - by + this.height / 2; - } else if (this.follow.y > br[1] - this.interpolatedShiftY) { - this.targetY = this.follow.y + by - this.height / 2; - } + // Follow copies around + if (this.follow && ('x' in this.follow) && ('y' in this.follow)) { + followCamera(this); } - } - this.x = this.targetX * speed + this.x * (1 - speed); - this.y = this.targetY * speed + this.y * (1 - speed); - this.interpolatedShiftX = this.shiftX * speed + this.interpolatedShiftX * (1 - speed); - this.interpolatedShiftY = this.shiftY * speed + this.interpolatedShiftY * (1 - speed); + // The speed of drift movement + const speed = this.drift ? Math.min(1, (1 - this.drift) * delta) : 1; + // Perform drift motion + this.x = this.targetX * speed + this.x * (1 - speed); + this.y = this.targetY * speed + this.y * (1 - speed); - this.x = this.x || 0; - this.y = this.y || 0; - } + // Off-center shifts drift, too + this.interpolatedShiftX = this.shiftX * speed + this.interpolatedShiftX * (1 - speed); + this.interpolatedShiftY = this.shiftY * speed + this.interpolatedShiftY * (1 - speed); - /** - * Returns the current camera position plus the screen shake effect. - * @type {number} - */ - get computedX() { - const dx = (Math.sin(this.shakePhaseX) + Math.sin(this.shakePhaseX * 3.1846) * 0.25) / 1.25; - const x = this.x + dx * this.shake * Math.max(this.width, this.height) / 100 * this.shakeX; - return x + this.interpolatedShiftX; - } - /** - * Returns the current camera position plus the screen shake effect. - * @type {number} - */ - get computedY() { - const dy = (Math.sin(this.shakePhaseY) + Math.sin(this.shakePhaseY * 2.8948) * 0.25) / 1.25; - const y = this.y + dy * this.shake * Math.max(this.width, this.height) / 100 * this.shakeY; - return y + this.interpolatedShiftY; - } + restrictInRect(this); - /** - * Returns the position of the left edge where the visible rectangle ends, in game coordinates. - * This can be used for UI positioning in game coordinates. - * This does not count for rotations, though. - * For rotated and/or scaled viewports, see `getTopLeftCorner` - * and `getBottomLeftCorner` methods. - * @returns {number} The location of the left edge. - * @type {number} - * @readonly - */ - get left() { - return this.computedX - (this.width / 2) * this.scale.x; - } - /** - * Returns the position of the top edge where the visible rectangle ends, in game coordinates. - * This can be used for UI positioning in game coordinates. - * This does not count for rotations, though. - * For rotated and/or scaled viewports, see `getTopLeftCorner` and `getTopRightCorner` methods. - * @returns {number} The location of the top edge. - * @type {number} - * @readonly - */ - get top() { - return this.computedY - (this.height / 2) * this.scale.y; - } - /** - * Returns the position of the right edge where the visible rectangle ends, in game coordinates. - * This can be used for UI positioning in game coordinates. - * This does not count for rotations, though. - * For rotated and/or scaled viewports, see `getTopRightCorner` - * and `getBottomRightCorner` methods. - * @returns {number} The location of the right edge. - * @type {number} - * @readonly - */ - get right() { - return this.computedX + (this.width / 2) * this.scale.x; - } - /** - * Returns the position of the bottom edge where the visible rectangle ends, - * in game coordinates. This can be used for UI positioning in game coordinates. - * This does not count for rotations, though. - * For rotated and/or scaled viewports, see `getBottomLeftCorner` - * and `getBottomRightCorner` methods. - * @returns {number} The location of the bottom edge. - * @type {number} - * @readonly - */ - get bottom() { - return this.computedY + (this.height / 2) * this.scale.y; - } + // Recover from possible calculation errors + this.x = this.x || 0; + this.y = this.y || 0; + } - /** - * Translates a point from UI space to game space. - * @param {number} x The x coordinate in UI space. - * @param {number} y The y coordinate in UI space. - * @returns {Array} A pair of new `x` and `y` coordinates. - */ - uiToGameCoord(x, y) { - const modx = (x - this.width / 2) * this.scale.x, - mody = (y - this.height / 2) * this.scale.y; - const result = ct.u.rotate(modx, mody, this.rotation); - return [result[0] + this.computedX, result[1] + this.computedY]; - } + /** + * Returns the current camera position plus the screen shake effect. + * @type {number} + */ + get computedX() { + // eslint-disable-next-line max-len + const dx = (Math.sin(this.shakePhaseX) + Math.sin(this.shakePhaseX * 3.1846) * 0.25) / 1.25; + // eslint-disable-next-line max-len + const x = this.x + dx * this.shake * Math.max(this.width, this.height) / 100 * this.shakeX; + return x + this.interpolatedShiftX; + } + /** + * Returns the current camera position plus the screen shake effect. + * @type {number} + */ + get computedY() { + // eslint-disable-next-line max-len + const dy = (Math.sin(this.shakePhaseY) + Math.sin(this.shakePhaseY * 2.8948) * 0.25) / 1.25; + // eslint-disable-next-line max-len + const y = this.y + dy * this.shake * Math.max(this.width, this.height) / 100 * this.shakeY; + return y + this.interpolatedShiftY; + } - /** - * Translates a point from game space to UI space. - * @param {number} x The x coordinate in game space. - * @param {number} y The y coordinate in game space. - * @returns {Array} A pair of new `x` and `y` coordinates. - */ - gameToUiCoord(x, y) { - const relx = x - this.computedX, - rely = y - this.computedY; - const unrotated = ct.u.rotate(relx, rely, -this.rotation); - return [ - unrotated[0] / this.scale.x + this.width / 2, - unrotated[1] / this.scale.y + this.height / 2 - ]; - } - /** - * Gets the position of the top-left corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, - * especially with rotated viewports. - * @returns {Array} A pair of `x` and `y` coordinates. - */ - getTopLeftCorner() { - return this.uiToGameCoord(0, 0); - } + /** + * Returns the position of the left edge where the visible rectangle ends, + * in game coordinates. + * This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. + * For rotated and/or scaled viewports, see `getTopLeftCorner` + * and `getBottomLeftCorner` methods. + * @returns {number} The location of the left edge. + * @type {number} + * @readonly + */ + get left() { + return this.computedX - (this.width / 2) * this.scale.x; + } + /** + * Returns the position of the top edge where the visible rectangle ends, + * in game coordinates. + * This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. + * For rotated and/or scaled viewports, see `getTopLeftCorner` + * and `getTopRightCorner` methods. + * @returns {number} The location of the top edge. + * @type {number} + * @readonly + */ + get top() { + return this.computedY - (this.height / 2) * this.scale.y; + } + /** + * Returns the position of the right edge where the visible rectangle ends, + * in game coordinates. + * This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. + * For rotated and/or scaled viewports, see `getTopRightCorner` + * and `getBottomRightCorner` methods. + * @returns {number} The location of the right edge. + * @type {number} + * @readonly + */ + get right() { + return this.computedX + (this.width / 2) * this.scale.x; + } + /** + * Returns the position of the bottom edge where the visible rectangle ends, + * in game coordinates. This can be used for UI positioning in game coordinates. + * This does not count for rotations, though. + * For rotated and/or scaled viewports, see `getBottomLeftCorner` + * and `getBottomRightCorner` methods. + * @returns {number} The location of the bottom edge. + * @type {number} + * @readonly + */ + get bottom() { + return this.computedY + (this.height / 2) * this.scale.y; + } - /** - * Gets the position of the top-right corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, - * especially with rotated viewports. - * @returns {Array} A pair of `x` and `y` coordinates. - */ - getTopRightCorner() { - return this.uiToGameCoord(this.width, 0); - } + /** + * Translates a point from UI space to game space. + * @param {number} x The x coordinate in UI space. + * @param {number} y The y coordinate in UI space. + * @returns {Array} A pair of new `x` and `y` coordinates. + */ + uiToGameCoord(x, y) { + const modx = (x - this.width / 2) * this.scale.x, + mody = (y - this.height / 2) * this.scale.y; + const result = ct.u.rotate(modx, mody, this.rotation); + return [result[0] + this.computedX, result[1] + this.computedY]; + } - /** - * Gets the position of the bottom-left corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, - * especially with rotated viewports. - * @returns {Array} A pair of `x` and `y` coordinates. - */ - getBottomLeftCorner() { - return this.uiToGameCoord(0, this.height); - } + /** + * Translates a point from game space to UI space. + * @param {number} x The x coordinate in game space. + * @param {number} y The y coordinate in game space. + * @returns {Array} A pair of new `x` and `y` coordinates. + */ + gameToUiCoord(x, y) { + const relx = x - this.computedX, + rely = y - this.computedY; + const unrotated = ct.u.rotate(relx, rely, -this.rotation); + return [ + unrotated[0] / this.scale.x + this.width / 2, + unrotated[1] / this.scale.y + this.height / 2 + ]; + } + /** + * Gets the position of the top-left corner of the viewport in game coordinates. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. + * @returns {Array} A pair of `x` and `y` coordinates. + */ + getTopLeftCorner() { + return this.uiToGameCoord(0, 0); + } - /** - * Gets the position of the bottom-right corner of the viewport in game coordinates. - * This is useful for positioning UI elements in game coordinates, - * especially with rotated viewports. - * @returns {Array} A pair of `x` and `y` coordinates. - */ - getBottomRightCorner() { - return this.uiToGameCoord(this.width, this.height); - } + /** + * Gets the position of the top-right corner of the viewport in game coordinates. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. + * @returns {Array} A pair of `x` and `y` coordinates. + */ + getTopRightCorner() { + return this.uiToGameCoord(this.width, 0); + } - /** - * Returns the bounding box of the camera. - * Useful for rotated viewports when something needs to be reliably covered by a rectangle. - * @returns {PIXI.Rectangle} The bounding box of the camera. - */ - getBoundingBox() { - const bb = new PIXI.Bounds(); - const tl = this.getTopLeftCorner(), - tr = this.getTopRightCorner(), - bl = this.getBottomLeftCorner(), - br = this.getBottomRightCorner(); - bb.addPoint(new PIXI.Point(tl[0], tl[1])); - bb.addPoint(new PIXI.Point(tr[0], tr[1])); - bb.addPoint(new PIXI.Point(bl[0], bl[1])); - bb.addPoint(new PIXI.Point(br[0], br[1])); - return bb.getRectangle(); - } + /** + * Gets the position of the bottom-left corner of the viewport in game coordinates. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. + * @returns {Array} A pair of `x` and `y` coordinates. + */ + getBottomLeftCorner() { + return this.uiToGameCoord(0, this.height); + } - get rotation() { - return this.transform.rotation / Math.PI * -180; - } - /** - * The rotation angle of a camera. - * @param {number} value New rotation value - * @type {number} - */ - set rotation(value) { - this.transform.rotation = value * Math.PI / -180; - return value; - } + /** + * Gets the position of the bottom-right corner of the viewport in game coordinates. + * This is useful for positioning UI elements in game coordinates, + * especially with rotated viewports. + * @returns {Array} A pair of `x` and `y` coordinates. + */ + getBottomRightCorner() { + return this.uiToGameCoord(this.width, this.height); + } - /** - * Checks whether a given object (or any Pixi's DisplayObject) - * is potentially visible, meaning that its bounding box intersects - * the camera's bounding box. - * @param {PIXI.DisplayObject} copy An object to check for. - * @returns {boolean} `true` if an object is visible, `false` otherwise. - */ - contains(copy) { - // `true` skips transforms recalculations, boosting performance - const bounds = copy.getBounds(true); - return bounds.right > 0 && - bounds.left < this.width * this.scale.x && - bounds.bottom > 0 && - bounds.top < this.width * this.scale.y; - } + /** + * Returns the bounding box of the camera. + * Useful for rotated viewports when something needs to be reliably covered by a rectangle. + * @returns {PIXI.Rectangle} The bounding box of the camera. + */ + getBoundingBox() { + const bb = new PIXI.Bounds(); + const tl = this.getTopLeftCorner(), + tr = this.getTopRightCorner(), + bl = this.getBottomLeftCorner(), + br = this.getBottomRightCorner(); + bb.addPoint(new PIXI.Point(tl[0], tl[1])); + bb.addPoint(new PIXI.Point(tr[0], tr[1])); + bb.addPoint(new PIXI.Point(bl[0], bl[1])); + bb.addPoint(new PIXI.Point(br[0], br[1])); + return bb.getRectangle(); + } - /** - * Realigns all the copies in a room so that they distribute proportionally - * to a new camera size based on their `xstart` and `ystart` coordinates. - * Will throw an error if the given room is not in UI space (if `room.isUi` is not `true`). - * You can skip the realignment for some copies - * if you set their `skipRealign` parameter to `true`. - * @param {Room} room The room which copies will be realigned. - * @returns {void} - */ - realign(room) { - if (!room.isUi) { - throw new Error('[ct.camera] An attempt to realing a room that is not in UI space. The room in question is', room); + get rotation() { + return this.transform.rotation / Math.PI * -180; + } + /** + * The rotation angle of a camera. + * @param {number} value New rotation value + * @type {number} + */ + set rotation(value) { + this.transform.rotation = value * Math.PI / -180; + return value; } - const w = (ct.rooms.templates[room.name].width || 1), - h = (ct.rooms.templates[room.name].height || 1); - for (const copy of room.children) { - if (!('xstart' in copy) || copy.skipRealign) { - continue; + + /** + * Checks whether a given object (or any Pixi's DisplayObject) + * is potentially visible, meaning that its bounding box intersects + * the camera's bounding box. + * @param {PIXI.DisplayObject} copy An object to check for. + * @returns {boolean} `true` if an object is visible, `false` otherwise. + */ + contains(copy) { + // `true` skips transforms recalculations, boosting performance + const bounds = copy.getBounds(true); + return bounds.right > 0 && + bounds.left < this.width * this.scale.x && + bounds.bottom > 0 && + bounds.top < this.width * this.scale.y; + } + + /** + * Realigns all the copies in a room so that they distribute proportionally + * to a new camera size based on their `xstart` and `ystart` coordinates. + * Will throw an error if the given room is not in UI space (if `room.isUi` is not `true`). + * You can skip the realignment for some copies + * if you set their `skipRealign` parameter to `true`. + * @param {Room} room The room which copies will be realigned. + * @returns {void} + */ + realign(room) { + if (!room.isUi) { + throw new Error('[ct.camera] An attempt to realing a room that is not in UI space. The room in question is', room); + } + const w = (ct.rooms.templates[room.name].width || 1), + h = (ct.rooms.templates[room.name].height || 1); + for (const copy of room.children) { + if (!('xstart' in copy) || copy.skipRealign) { + continue; + } + copy.x = copy.xstart / w * this.width; + copy.y = copy.ystart / h * this.height; } - copy.x = copy.xstart / w * this.width; - copy.y = copy.ystart / h * this.height; } - } - /** - * This will align all non-UI layers in the game according to the camera's transforms. - * This is automatically called internally, and you will hardly ever use it. - * @returns {void} - */ - manageStage() { - const px = this.computedX, - py = this.computedY, - sx = 1 / (isNaN(this.scale.x) ? 1 : this.scale.x), - sy = 1 / (isNaN(this.scale.y) ? 1 : this.scale.y); - for (const item of ct.stage.children) { - if (!item.isUi && item.pivot) { - item.x = -this.width / 2; - item.y = -this.height / 2; - item.pivot.x = px; - item.pivot.y = py; - item.scale.x = sx; - item.scale.y = sy; - item.angle = -this.angle; + /** + * This will align all non-UI layers in the game according to the camera's transforms. + * This is automatically called internally, and you will hardly ever use it. + * @returns {void} + */ + manageStage() { + const px = this.computedX, + py = this.computedY, + sx = 1 / (isNaN(this.scale.x) ? 1 : this.scale.x), + sy = 1 / (isNaN(this.scale.y) ? 1 : this.scale.y); + for (const item of ct.stage.children) { + if (!item.isUi && item.pivot) { + item.x = -this.width / 2; + item.y = -this.height / 2; + item.pivot.x = px; + item.pivot.y = py; + item.scale.x = sx; + item.scale.y = sy; + item.angle = -this.angle; + } } } } -} + return Camera; +})(ct); diff --git a/app/data/ct.release/rooms.js b/app/data/ct.release/rooms.js index 04360b1ce..33c214b6c 100644 --- a/app/data/ct.release/rooms.js +++ b/app/data/ct.release/rooms.js @@ -296,6 +296,12 @@ Room.roomId = 0; ct.roomWidth, ct.roomHeight ); + if (template.cameraConstraints) { + ct.camera.minX = template.cameraConstraints.x1; + ct.camera.maxX = template.cameraConstraints.x2; + ct.camera.minY = template.cameraConstraints.y1; + ct.camera.maxY = template.cameraConstraints.y2; + } ct.pixiApp.renderer.resize(template.width, template.height); ct.rooms.current = ct.room = new Room(template); ct.stage.addChild(ct.room); diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 8a029e148..6c0a14082 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -538,7 +538,12 @@ "position": "Position", "rotation": "Rotation", "scale": "Scale" - } + }, + "restrictCamera": "Keep camera in a rectangle", + "minimumX": "Min X", + "minimumY": "Min Y", + "maximumX": "Max X", + "maximumY": "Max Y" }, "notepad": { "local": "Project's notepad", diff --git a/src/node_requires/exporter/rooms.js b/src/node_requires/exporter/rooms.js index 504f9bb89..c44b61696 100644 --- a/src/node_requires/exporter/rooms.js +++ b/src/node_requires/exporter/rooms.js @@ -11,6 +11,28 @@ const getStartingRoom = proj => { } return startroom; }; +const getConstraints = r => { + if (r.restrictCamera) { + let x1 = r.restrictMinX || 0, + y1 = r.restrictMinY || 0, + x2 = r.restrictMaxX === void 0 ? r.width : r.restrictMaxX, + y2 = r.restrictMaxX === void 0 ? r.height : r.restrictMaxY; + if (x1 > x2) { + [x1, x2] = [x2, x1]; + } + if (y1 > y2) { + [y1, y2] = [y2, y1]; + } + return { + x1, + y1, + x2, + y2 + }; + } + return false; +}; + const stringifyRooms = proj => { let roomsCode = ''; for (const k in proj.rooms) { @@ -57,6 +79,8 @@ const stringifyRooms = proj => { } } + const constraints = getConstraints(r); + roomsCode += ` ct.rooms.templates['${r.name}'] = { name: '${r.name}', @@ -67,6 +91,7 @@ ct.rooms.templates['${r.name}'] = { bgs: JSON.parse('${JSON.stringify(bgsCopy)}'), tiles: JSON.parse('${JSON.stringify(tileLayers)}'), backgroundColor: '${r.backgroundColor || '#000000'}', + ${constraints ? 'cameraConstraints: ' + JSON.stringify(constraints) + ',' : ''} onStep() { ${proj.rooms[k].onstep} }, diff --git a/src/riotTags/app-view.tag b/src/riotTags/app-view.tag index ef7840011..1a6542811 100644 --- a/src/riotTags/app-view.tag +++ b/src/riotTags/app-view.tag @@ -78,7 +78,6 @@ app-view.flexcol const assetListener = asset => { const [assetType] = asset.split('/'); this.changeTab(assetType)(); - console.log(assetType, asset); this.update(); }; window.orders.on('openAsset', assetListener); diff --git a/src/riotTags/rooms/room-editor.tag b/src/riotTags/rooms/room-editor.tag index 107e8a233..3082ab3b7 100644 --- a/src/riotTags/rooms/room-editor.tag +++ b/src/riotTags/rooms/room-editor.tag @@ -33,22 +33,69 @@ room-editor.panel.view room-backgrounds-editor(show="{tab === 'roombackgrounds'}" room="{room}") room-tile-editor(show="{tab === 'roomtiles'}" room="{room}") .pad.panel(show="{tab === 'properties'}") - .fifty.npt.npb.npl - b {voc.width} - br - input.wide(type="number" value="{room.width}" onchange="{wire('this.room.width')}") - .fifty.npt.npb.npr - b {voc.height} + fieldset + .fifty.npt.npb.npl + b {voc.width} + br + input.wide(type="number" value="{room.width}" onchange="{wireAndRedraw('this.room.width')}") + .fifty.npt.npb.npr + b {voc.height} + br + input.wide(type="number" value="{room.height}" onchange="{wireAndRedraw('this.room.height')}") + .clear + fieldset + label.checkbox + input(type="checkbox" checked="{room.restrictCamera}" onchange="{wireAndRedraw('this.room.restrictCamera')}") + span {voc.restrictCamera} + .aPoint2DInput.compact.wide(if="{room.restrictCamera}") + label + span {voc.minimumX}: + | + input.compact( + step="{room.gridX}" type="number" + oninput="{wireAndRedraw('this.room.restrictMinX')}" + value="{room.restrictMinX === void 0 ? 0 : room.restrictMinX}" + ) + .spacer + label + span.nogrow {voc.minimumY}: + | + input.compact( + step="{room.gridY}" type="number" + oninput="{wireAndRedraw('this.room.restrictMinY')}" + value="{room.restrictMinY === void 0 ? 0 : room.restrictMinY}" + ) + .aPoint2DInput.compact.wide(if="{room.restrictCamera}") + label + span {voc.maximumX}: + | + input.compact( + step="{room.gridX}" type="number" + oninput="{wireAndRedraw('this.room.restrictMaxX')}" + value="{room.restrictMaxX === void 0 ? room.width : room.restrictMaxX}" + ) + .spacer + label + span.nogrow {voc.maximumY}: + | + input.compact( + step="{room.gridY}" type="number" + oninput="{wireAndRedraw('this.room.restrictMaxY')}" + value="{room.restrictMaxY === void 0 ? room.height : room.restrictMaxY}" + ) + + fieldset + b {voc.backgroundColor} br - input.wide(type="number" value="{room.height}" onchange="{wire('this.room.height')}") - .clear - b {voc.backgroundColor} - br - color-input.wide(onchange="{updateRoomBackground}" color="{room.backgroundColor || '#000000'}") - extensions-editor(entity="{room.extends}" type="room" wide="aye" compact="sure") - label.block.checkbox - input(type="checkbox" checked="{room.extends.isUi}" onchange="{wire('this.room.extends.isUi')}") - b {voc.isUi} + color-input.wide(onchange="{updateRoomBackground}" color="{room.backgroundColor || '#000000'}") + + fieldset + extensions-editor(entity="{room.extends}" type="room" wide="aye" compact="sure") + + fieldset + label.block.checkbox + input(type="checkbox" checked="{room.extends.isUi}" onchange="{wire('this.room.extends.isUi')}") + b {voc.isUi} .done.nogrow button.wide#roomviewdone(onclick="{roomSave}") @@ -157,6 +204,10 @@ room-editor.panel.view this.mixin(window.riotWired); this.mixin(window.roomCopyTools); this.mixin(window.roomTileTools); + this.wireAndRedraw = way => e => { + this.wire(way)(e); + this.refreshRoomCanvas(); + }; this.room = this.opts.room; if (!this.room.extends) { @@ -651,6 +702,16 @@ room-editor.panel.view // Outline the starting viewport frame this.drawSelection(-1.5, -1.5, this.room.width + 1.5, this.room.height + 1.5); + + // Outline room's limits + if (this.room.restrictCamera) { + this.drawSelection( + (this.room.restrictMinX || 0) - 1.5, + (this.room.restrictMinY || 0) - 1.5, + (this.room.restrictMaxX === void 0 ? this.room.width : this.room.restrictMaxX) + 1.5, + (this.room.restrictMaxY === void 0 ? this.room.height : this.room.restrictMaxY) + 1.5 + ); + } }; this.drawSelection = (x1, y1, x2, y2) => { diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index 3fad7a73c..e536a10a5 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -292,7 +292,7 @@ extensions-editor obj[field] = [...def]; } this.wire(way)(e); - } + }; this.addRow = e => { const {ext} = e.item;