From b5a2e77db4129155444532a3b450cc9c019800ba Mon Sep 17 00:00:00 2001 From: tobspr Date: Sat, 19 Sep 2020 07:49:17 +0200 Subject: [PATCH] Fix multiple typos, closes #627 --- README.md | 4 +- res_raw/atlas.tps | 2 + src/css/ingame_hud/shape_viewer.scss | 315 ++- src/js/changelog.js | 2 +- src/js/core/modal_dialog_elements.js | 4 +- src/js/core/read_write_proxy.js | 2 +- src/js/core/sprites.js | 2 +- src/js/game/belt_path.js | 4 +- src/js/game/camera.js | 1884 ++++++++--------- src/js/game/entity_manager.js | 2 +- .../game/hud/parts/building_placer_logic.js | 2 +- src/js/game/systems/belt.js | 4 +- src/js/game/systems/miner.js | 2 +- src/js/game/systems/underground_belt.js | 2 +- src/js/game/systems/wire.js | 4 +- translations/README.md | 162 +- 16 files changed, 1197 insertions(+), 1200 deletions(-) diff --git a/README.md b/README.md index 32e09d59..cc3e06bc 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ This project is based on ES5. Some ES2015 features are used but most of them are 5. Add a constructor. **The constructor must be called with optional parameters only!** `new MyFancyComponent({})` should always work. 6. Add any props you need in the constructor. 7. Add the component in `src/js/game/component_registry.js` -8. Add the componetn in `src/js/game/entity_components.js` +8. Add the component in `src/js/game/entity_components.js` 9. Done! You can use your component now #### Adding a new building @@ -96,6 +96,6 @@ This project is based on ES5. Some ES2015 features are used but most of them are For most assets I use Adobe Photoshop, you can find them in `assets/`. -You will need a Texture Packer license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when thats done. +You will need a Texture Packer license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when that's done. shapez.io Screenshot diff --git a/res_raw/atlas.tps b/res_raw/atlas.tps index 947bdb04..50c27d8b 100644 --- a/res_raw/atlas.tps +++ b/res_raw/atlas.tps @@ -283,6 +283,7 @@ sprites/blueprints/virtual_processor-analyzer.png sprites/blueprints/virtual_processor-rotater.png sprites/blueprints/virtual_processor-shapecompare.png + sprites/blueprints/virtual_processor-stacker.png sprites/blueprints/virtual_processor-unstacker.png sprites/blueprints/virtual_processor.png sprites/blueprints/wire_tunnel-coating.png @@ -310,6 +311,7 @@ sprites/buildings/virtual_processor-analyzer.png sprites/buildings/virtual_processor-rotater.png sprites/buildings/virtual_processor-shapecompare.png + sprites/buildings/virtual_processor-stacker.png sprites/buildings/virtual_processor-unstacker.png sprites/buildings/virtual_processor.png sprites/buildings/wire_tunnel-coating.png diff --git a/src/css/ingame_hud/shape_viewer.scss b/src/css/ingame_hud/shape_viewer.scss index 65491a5a..9ece9f35 100644 --- a/src/css/ingame_hud/shape_viewer.scss +++ b/src/css/ingame_hud/shape_viewer.scss @@ -1,161 +1,154 @@ -#ingame_HUD_ShapeViewer { - $dims: 170px; - - .content { - display: flex; - @include S(width, $dims); - width: 100%; - flex-direction: column; - overflow-x: hidden; - - &[data-layers="3"], - &[data-layers="4"] { - @include S(width, 2 * $dims); - .renderArea { - display: grid; - grid-template-columns: 1fr 1fr; - @include S(grid-row-gap, 15px); - } - } - - .renderArea { - display: grid; - width: 100%; - @include S(grid-row-gap, 10px); - align-items: center; - justify-items: center; - } - - .infoArea { - align-self: flex-end; - @include S(margin-top, 10px); - display: flex; - flex-direction: column; - overflow: hidden; - - button { - @include S(margin, 0); - @include PlainText; - } - } - - .seperator { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - } - - .layer { - position: relative; - background: #eee; - - @include DarkThemeOverride { - background: rgba(0, 10, 20, 0.2); - } - @include S(width, 150px); - @include S(height, 100px); - display: flex; - align-items: center; - justify-content: center; - - > canvas { - @include S(width, 50px); - @include S(height, 50px); - } - - .quad { - position: absolute; - width: 50%; - height: 50%; - display: flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - - $arrowDims: 23px; - $spacing: 9px; - @include S(padding, 6px); - - .colorLabel { - text-transform: uppercase; - @include SuperSmallText; - @include S(font-size, 9px); - } - - .emptyLabel { - text-transform: uppercase; - @include SuperSmallText; - @include S(font-size, 9px); - } - - &::after { - content: " "; - background: rgba(0, 10, 20, 0.5); - @include S(width, $arrowDims); - @include S(height, 1px); - position: absolute; - transform: rotate(45deg); - transform-origin: 50% 50%; - } - @include DarkThemeOverride { - &::after { - background: rgba(255, 255, 255, 0.5); - } - } - - &.quad-0 { - right: 0; - top: 0; - align-items: flex-start; - justify-content: flex-end; - - &::after { - @include S(left, $spacing); - @include S(bottom, $arrowDims / 2 + $spacing); - transform: rotate(-45deg); - } - } - &.quad-1 { - bottom: 0; - right: 0; - - align-items: flex-end; - justify-content: flex-end; - - &::after { - @include S(left, $spacing); - @include S(top, $arrowDims / 2 + $spacing); - transform: rotate(45deg); - } - } - &.quad-2 { - bottom: 0; - left: 0; - - align-items: flex-end; - justify-content: flex-start; - - &::after { - @include S(right, $spacing); - @include S(top, $arrowDims / 2 + $spacing); - transform: rotate(135deg); - } - } - &.quad-3 { - top: 0; - left: 0; - - align-items: flex-start; - justify-content: flex-start; - - &::after { - @include S(right, $spacing); - @include S(bottom, $arrowDims / 2 + $spacing); - transform: rotate(225deg); - } - } - } - } - } -} +#ingame_HUD_ShapeViewer { + $dims: 170px; + + .content { + display: flex; + @include S(width, $dims); + width: 100%; + flex-direction: column; + overflow-x: hidden; + + &[data-layers="3"], + &[data-layers="4"] { + @include S(width, 2 * $dims); + .renderArea { + display: grid; + grid-template-columns: 1fr 1fr; + @include S(grid-row-gap, 15px); + } + } + + .renderArea { + display: grid; + width: 100%; + @include S(grid-row-gap, 10px); + align-items: center; + justify-items: center; + } + + .infoArea { + align-self: flex-end; + @include S(margin-top, 10px); + display: flex; + flex-direction: column; + overflow: hidden; + + button { + @include S(margin, 0); + @include PlainText; + } + } + + .layer { + position: relative; + background: #eee; + + @include DarkThemeOverride { + background: rgba(0, 10, 20, 0.2); + } + @include S(width, 150px); + @include S(height, 100px); + display: flex; + align-items: center; + justify-content: center; + + > canvas { + @include S(width, 50px); + @include S(height, 50px); + } + + .quad { + position: absolute; + width: 50%; + height: 50%; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + + $arrowDims: 23px; + $spacing: 9px; + @include S(padding, 6px); + + .colorLabel { + text-transform: uppercase; + @include SuperSmallText; + @include S(font-size, 9px); + } + + .emptyLabel { + text-transform: uppercase; + @include SuperSmallText; + @include S(font-size, 9px); + } + + &::after { + content: " "; + background: rgba(0, 10, 20, 0.5); + @include S(width, $arrowDims); + @include S(height, 1px); + position: absolute; + transform: rotate(45deg); + transform-origin: 50% 50%; + } + @include DarkThemeOverride { + &::after { + background: rgba(255, 255, 255, 0.5); + } + } + + &.quad-0 { + right: 0; + top: 0; + align-items: flex-start; + justify-content: flex-end; + + &::after { + @include S(left, $spacing); + @include S(bottom, $arrowDims / 2 + $spacing); + transform: rotate(-45deg); + } + } + &.quad-1 { + bottom: 0; + right: 0; + + align-items: flex-end; + justify-content: flex-end; + + &::after { + @include S(left, $spacing); + @include S(top, $arrowDims / 2 + $spacing); + transform: rotate(45deg); + } + } + &.quad-2 { + bottom: 0; + left: 0; + + align-items: flex-end; + justify-content: flex-start; + + &::after { + @include S(right, $spacing); + @include S(top, $arrowDims / 2 + $spacing); + transform: rotate(135deg); + } + } + &.quad-3 { + top: 0; + left: 0; + + align-items: flex-start; + justify-content: flex-start; + + &::after { + @include S(right, $spacing); + @include S(bottom, $arrowDims / 2 + $spacing); + transform: rotate(225deg); + } + } + } + } + } +} diff --git a/src/js/changelog.js b/src/js/changelog.js index bbc4a4fa..4165714d 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -109,7 +109,7 @@ export const CHANGELOG = [ date: "17.06.2020", entries: [ "You can now place straight belts (and tunnels) by holding SHIFT! (For you, @giantwaffle ❤️)", - "Added continue button to main menu and add seperate 'New game' button (by jaysc)", + "Added continue button to main menu and add separate 'New game' button (by jaysc)", "Added setting to disable smart tunnel placement introduced with the last update", "Added setting to disable vignette", "Update translations", diff --git a/src/js/core/modal_dialog_elements.js b/src/js/core/modal_dialog_elements.js index 54b69402..3acd2673 100644 --- a/src/js/core/modal_dialog_elements.js +++ b/src/js/core/modal_dialog_elements.js @@ -30,10 +30,10 @@ export class Dialog { * @param {string} param0.title Title of the dialog * @param {string} param0.contentHTML Inner dialog html * @param {Array} param0.buttons - * Button list, each button contains of up to 3 parts seperated by ':'. + * Button list, each button contains of up to 3 parts separated by ':'. * Part 0: The id, one of the one defined in dialog_buttons.yaml * Part 1: The style, either good, bad or misc - * Part 2 (optional): Additional parameters seperated by '/', available are: + * Part 2 (optional): Additional parameters separated by '/', available are: * timeout: This button is only available after some waiting time * kb_enter: This button is triggered by the enter key * kb_escape This button is triggered by the escape key diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 74b13efa..6d26fa2b 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -224,7 +224,7 @@ export class ReadWriteProxy { return rawData; }) - // Parse JSON, this could throw but thats fine + // Parse JSON, this could throw but that's fine .then(res => { try { return JSON.parse(res); diff --git a/src/js/core/sprites.js b/src/js/core/sprites.js index 928a9dcc..a338a5a3 100644 --- a/src/js/core/sprites.js +++ b/src/js/core/sprites.js @@ -206,7 +206,7 @@ export class AtlasSprite extends BaseSprite { srcX, srcY, - // atlas src siize + // atlas src size srcW, srcH, diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index b999e0ee..07440f80 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1091,7 +1091,7 @@ export class BeltPath extends BasicSerializableObject { computePositionFromProgress(progress) { let currentLength = 0; - // floating point issuses .. + // floating point issues .. assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress); for (let i = 0; i < this.entityPath.length; ++i) { @@ -1206,7 +1206,7 @@ export class BeltPath extends BasicSerializableObject { // Check if the current items are on the belt while (trackPos + beltLength >= currentItemPos - 1e-5) { - // Its on the belt, render it now + // It's on the belt, render it now const staticComp = entity.components.StaticMapEntity; assert( currentItemPos - trackPos >= 0, diff --git a/src/js/game/camera.js b/src/js/game/camera.js index ab73fc83..044ffeb4 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -1,942 +1,942 @@ -import { clickDetectorGlobals } from "../core/click_detector"; -import { globalConfig, SUPPORT_TOUCH } from "../core/config"; -import { createLogger } from "../core/logging"; -import { Rectangle } from "../core/rectangle"; -import { Signal, STOP_PROPAGATION } from "../core/signal"; -import { clamp } from "../core/utils"; -import { mixVector, Vector } from "../core/vector"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { KEYMAPPINGS } from "./key_action_mapper"; -import { GameRoot } from "./root"; - -const logger = createLogger("camera"); - -export const USER_INTERACT_MOVE = "move"; -export const USER_INTERACT_ZOOM = "zoom"; -export const USER_INTERACT_TOUCHEND = "touchend"; - -const velocitySmoothing = 0.5; -const velocityFade = 0.98; -const velocityStrength = 0.4; -const velocityMax = 20; -const ticksBeforeErasingVelocity = 10; - -/** - * @enum {string} - */ -export const enumMouseButton = { - left: "left", - middle: "middle", - right: "right", -}; - -export class Camera extends BasicSerializableObject { - constructor(root) { - super(); - - /** @type {GameRoot} */ - this.root = root; - - // Zoom level, 2 means double size - - // Find optimal initial zoom - - this.zoomLevel = this.findInitialZoom(); - this.clampZoomLevel(); - - /** @type {Vector} */ - this.center = new Vector(0, 0); - - // Input handling - this.currentlyMoving = false; - this.lastMovingPosition = null; - this.lastMovingPositionLastTick = null; - this.numTicksStandingStill = null; - this.cameraUpdateTimeBucket = 0.0; - this.didMoveSinceTouchStart = false; - this.currentlyPinching = false; - this.lastPinchPositions = null; - - this.keyboardForce = new Vector(); - - // Signal which gets emitted once the user changed something - this.userInteraction = new Signal(); - - /** @type {Vector} */ - this.currentShake = new Vector(0, 0); - - /** @type {Vector} */ - this.currentPan = new Vector(0, 0); - - // Set desired pan (camera movement) - /** @type {Vector} */ - this.desiredPan = new Vector(0, 0); - - // Set desired camera center - /** @type {Vector} */ - this.desiredCenter = null; - - // Set desired camera zoom - /** @type {number} */ - this.desiredZoom = null; - - /** @type {Vector} */ - this.touchPostMoveVelocity = new Vector(0, 0); - - // Handlers - this.downPreHandler = /** @type {TypedSignal<[Vector, enumMouseButton]>} */ (new Signal()); - this.movePreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); - // this.pinchPreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); - this.upPostHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); - - this.internalInitEvents(); - this.clampZoomLevel(); - this.bindKeys(); - if (G_IS_DEV) { - window.addEventListener("keydown", ev => { - if (ev.key === "i") { - this.zoomLevel = 3; - } - }); - } - } - - // Serialization - static getId() { - return "Camera"; - } - - static getSchema() { - return { - zoomLevel: types.float, - center: types.vector, - }; - } - - deserialize(data) { - const errorCode = super.deserialize(data); - if (errorCode) { - return errorCode; - } - - // Safety - this.clampZoomLevel(); - } - - // Simple geters & setters - - addScreenShake(amount) { - const currentShakeAmount = this.currentShake.length(); - const scale = 1 / (1 + 3 * currentShakeAmount); - this.currentShake.x = this.currentShake.x + 2 * (Math.random() - 0.5) * scale * amount; - this.currentShake.y = this.currentShake.y + 2 * (Math.random() - 0.5) * scale * amount; - } - - /** - * Sets a point in world space to focus on - * @param {Vector} center - */ - setDesiredCenter(center) { - this.desiredCenter = center.copy(); - this.currentlyMoving = false; - } - - /** - * Sets a desired zoom level - * @param {number} zoom - */ - setDesiredZoom(zoom) { - this.desiredZoom = zoom; - } - - /** - * Returns if this camera is currently moving by a non-user interaction - */ - isCurrentlyMovingToDesiredCenter() { - return this.desiredCenter !== null; - } - - /** - * Sets the camera pan, every frame the camera will move by this amount - * @param {Vector} pan - */ - setPan(pan) { - this.desiredPan = pan.copy(); - } - - /** - * Finds a good initial zoom level - */ - findInitialZoom() { - const desiredWorldSpaceWidth = 15 * globalConfig.tileSize; - const zoomLevelX = this.root.gameWidth / desiredWorldSpaceWidth; - const zoomLevelY = this.root.gameHeight / desiredWorldSpaceWidth; - - const finalLevel = Math.min(zoomLevelX, zoomLevelY); - assert( - Number.isFinite(finalLevel) && finalLevel > 0, - "Invalid zoom level computed for initial zoom: " + finalLevel - ); - return finalLevel; - } - - /** - * Clears all animations - */ - clearAnimations() { - this.touchPostMoveVelocity.x = 0; - this.touchPostMoveVelocity.y = 0; - this.desiredCenter = null; - this.desiredPan.x = 0; - this.desiredPan.y = 0; - this.currentPan.x = 0; - this.currentPan.y = 0; - this.currentlyPinching = false; - this.currentlyMoving = false; - this.lastMovingPosition = null; - this.didMoveSinceTouchStart = false; - this.desiredZoom = null; - } - - /** - * Returns if the user is currently interacting with the camera - * @returns {boolean} true if the user interacts - */ - isCurrentlyInteracting() { - if (this.currentlyPinching) { - return true; - } - if (this.currentlyMoving) { - // Only interacting if moved at least once - return this.didMoveSinceTouchStart; - } - if (this.touchPostMoveVelocity.lengthSquare() > 1) { - return true; - } - return false; - } - - /** - * Returns if in the next frame the viewport will change - * @returns {boolean} true if it willchange - */ - viewportWillChange() { - return this.desiredCenter !== null || this.desiredZoom !== null || this.isCurrentlyInteracting(); - } - - /** - * Cancels all interactions, that is user interaction and non user interaction - */ - cancelAllInteractions() { - this.touchPostMoveVelocity = new Vector(0, 0); - this.desiredCenter = null; - this.currentlyMoving = false; - this.currentlyPinching = false; - this.desiredZoom = null; - } - - /** - * Returns effective viewport width - */ - getViewportWidth() { - return this.root.gameWidth / this.zoomLevel; - } - - /** - * Returns effective viewport height - */ - getViewportHeight() { - return this.root.gameHeight / this.zoomLevel; - } - - /** - * Returns effective world space viewport left - */ - getViewportLeft() { - return this.center.x - this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns effective world space viewport right - */ - getViewportRight() { - return this.center.x + this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns effective world space viewport top - */ - getViewportTop() { - return this.center.y - this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns effective world space viewport bottom - */ - getViewportBottom() { - return this.center.y + this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns the visible world space rect - * @returns {Rectangle} - */ - getVisibleRect() { - return Rectangle.fromTRBL( - Math.floor(this.getViewportTop()), - Math.ceil(this.getViewportRight()), - Math.ceil(this.getViewportBottom()), - Math.floor(this.getViewportLeft()) - ); - } - - getIsMapOverlayActive() { - return this.zoomLevel < globalConfig.mapChunkOverviewMinZoom; - } - - /** - * Attaches all event listeners - */ - internalInitEvents() { - this.eventListenerTouchStart = this.onTouchStart.bind(this); - this.eventListenerTouchEnd = this.onTouchEnd.bind(this); - this.eventListenerTouchMove = this.onTouchMove.bind(this); - this.eventListenerMousewheel = this.onMouseWheel.bind(this); - this.eventListenerMouseDown = this.onMouseDown.bind(this); - this.eventListenerMouseMove = this.onMouseMove.bind(this); - this.eventListenerMouseUp = this.onMouseUp.bind(this); - - if (SUPPORT_TOUCH) { - this.root.canvas.addEventListener("touchstart", this.eventListenerTouchStart); - this.root.canvas.addEventListener("touchend", this.eventListenerTouchEnd); - this.root.canvas.addEventListener("touchcancel", this.eventListenerTouchEnd); - this.root.canvas.addEventListener("touchmove", this.eventListenerTouchMove); - } - - this.root.canvas.addEventListener("wheel", this.eventListenerMousewheel); - this.root.canvas.addEventListener("mousedown", this.eventListenerMouseDown); - this.root.canvas.addEventListener("mousemove", this.eventListenerMouseMove); - window.addEventListener("mouseup", this.eventListenerMouseUp); - // this.root.canvas.addEventListener("mouseout", this.eventListenerMouseUp); - } - - /** - * Cleans up all event listeners - */ - cleanup() { - if (SUPPORT_TOUCH) { - this.root.canvas.removeEventListener("touchstart", this.eventListenerTouchStart); - this.root.canvas.removeEventListener("touchend", this.eventListenerTouchEnd); - this.root.canvas.removeEventListener("touchcancel", this.eventListenerTouchEnd); - this.root.canvas.removeEventListener("touchmove", this.eventListenerTouchMove); - } - - this.root.canvas.removeEventListener("wheel", this.eventListenerMousewheel); - this.root.canvas.removeEventListener("mousedown", this.eventListenerMouseDown); - this.root.canvas.removeEventListener("mousemove", this.eventListenerMouseMove); - window.removeEventListener("mouseup", this.eventListenerMouseUp); - // this.root.canvas.removeEventListener("mouseout", this.eventListenerMouseUp); - } - - /** - * Binds the arrow keys - */ - bindKeys() { - const mapper = this.root.keyMapper; - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).add(() => (this.keyboardForce.y = -1)); - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).add(() => (this.keyboardForce.y = 1)); - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).add(() => (this.keyboardForce.x = 1)); - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).add(() => (this.keyboardForce.x = -1)); - - mapper - .getBinding(KEYMAPPINGS.navigation.mapZoomIn) - .add(() => (this.desiredZoom = this.zoomLevel * 1.2)); - mapper - .getBinding(KEYMAPPINGS.navigation.mapZoomOut) - .add(() => (this.desiredZoom = this.zoomLevel * 0.8)); - - mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add(() => this.centerOnMap()); - } - - centerOnMap() { - this.desiredCenter = new Vector(0, 0); - } - - /** - * Converts from screen to world space - * @param {Vector} screen - * @returns {Vector} world space - */ - screenToWorld(screen) { - const centerSpace = screen.subScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); - return centerSpace.divideScalar(this.zoomLevel).add(this.center); - } - - /** - * Converts from world to screen space - * @param {Vector} world - * @returns {Vector} screen space - */ - worldToScreen(world) { - const screenSpace = world.sub(this.center).multiplyScalar(this.zoomLevel); - return screenSpace.addScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); - } - - /** - * Returns if a point is on screen - * @param {Vector} point - * @returns {boolean} true if its on screen - */ - isWorldPointOnScreen(point) { - const rect = this.getVisibleRect(); - return rect.containsPoint(point.x, point.y); - } - - /** - * Returns if we can further zoom in - * @returns {boolean} - */ - canZoomIn() { - const maxLevel = this.root.app.platformWrapper.getMaximumZoom(); - return this.zoomLevel <= maxLevel - 0.01; - } - - /** - * Returns if we can further zoom out - * @returns {boolean} - */ - canZoomOut() { - const minLevel = this.root.app.platformWrapper.getMinimumZoom(); - return this.zoomLevel >= minLevel + 0.01; - } - - // EVENTS - - /** - * Checks if the mouse event is too close after a touch event and thus - * should get ignored - */ - checkPreventDoubleMouse() { - if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) { - return false; - } - return true; - } - - /** - * Mousedown handler - * @param {MouseEvent} event - */ - onMouseDown(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - if (!this.checkPreventDoubleMouse()) { - return; - } - - this.touchPostMoveVelocity = new Vector(0, 0); - if (event.button === 0) { - this.combinedSingleTouchStartHandler(event.clientX, event.clientY); - } else if (event.button === 1) { - this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle); - } else if (event.button === 2) { - this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right); - } - return false; - } - - /** - * Mousemove handler - * @param {MouseEvent} event - */ - onMouseMove(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - if (!this.checkPreventDoubleMouse()) { - return; - } - - if (event.button === 0) { - this.combinedSingleTouchMoveHandler(event.clientX, event.clientY); - } - - // Clamp everything afterwards - this.clampZoomLevel(); - return false; - } - - /** - * Mouseup handler - * @param {MouseEvent=} event - */ - onMouseUp(event) { - if (event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - } - - if (!this.checkPreventDoubleMouse()) { - return; - } - - this.combinedSingleTouchStopHandler(event.clientX, event.clientY); - return false; - } - - /** - * Mousewheel event - * @param {WheelEvent} event - */ - onMouseWheel(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - const prevZoom = this.zoomLevel; - - const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity(); - assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY); - assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel); - this.zoomLevel *= 1 + delta; - assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel); - - this.clampZoomLevel(); - this.desiredZoom = null; - - const mousePosition = this.root.app.mousePosition; - if (mousePosition) { - const worldPos = this.root.camera.screenToWorld(mousePosition); - const worldDelta = worldPos.sub(this.center); - const actualDelta = this.zoomLevel / prevZoom - 1; - this.center = this.center.add(worldDelta.multiplyScalar(actualDelta)); - this.desiredCenter = null; - } - - return false; - } - - /** - * Touch start handler - * @param {TouchEvent} event - */ - onTouchStart(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - clickDetectorGlobals.lastTouchTime = performance.now(); - this.touchPostMoveVelocity = new Vector(0, 0); - - if (event.touches.length === 1) { - const touch = event.touches[0]; - this.combinedSingleTouchStartHandler(touch.clientX, touch.clientY); - } else if (event.touches.length === 2) { - // if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) { - // // Something prevented pinching - // return false; - // } - - const touch1 = event.touches[0]; - const touch2 = event.touches[1]; - this.currentlyMoving = false; - this.currentlyPinching = true; - this.lastPinchPositions = [ - new Vector(touch1.clientX, touch1.clientY), - new Vector(touch2.clientX, touch2.clientY), - ]; - } - return false; - } - - /** - * Touch move handler - * @param {TouchEvent} event - */ - onTouchMove(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - clickDetectorGlobals.lastTouchTime = performance.now(); - - if (event.touches.length === 1) { - const touch = event.touches[0]; - this.combinedSingleTouchMoveHandler(touch.clientX, touch.clientY); - } else if (event.touches.length === 2) { - if (this.currentlyPinching) { - const touch1 = event.touches[0]; - const touch2 = event.touches[1]; - - const newPinchPositions = [ - new Vector(touch1.clientX, touch1.clientY), - new Vector(touch2.clientX, touch2.clientY), - ]; - - // Get distance of taps last time and now - const lastDistance = this.lastPinchPositions[0].distance(this.lastPinchPositions[1]); - const thisDistance = newPinchPositions[0].distance(newPinchPositions[1]); - - // IMPORTANT to do math max here to avoid NaN and causing an invalid zoom level - const difference = thisDistance / Math.max(0.001, lastDistance); - - // Find old center of zoom - let oldCenter = this.lastPinchPositions[0].centerPoint(this.lastPinchPositions[1]); - - // Find new center of zoom - let center = newPinchPositions[0].centerPoint(newPinchPositions[1]); - - // Compute movement - let movement = oldCenter.sub(center); - this.center.x += movement.x / this.zoomLevel; - this.center.y += movement.y / this.zoomLevel; - - // Compute zoom - center = center.sub(new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2)); - - // Apply zoom - assert( - Number.isFinite(difference), - "Invalid pinch difference: " + - difference + - "(last=" + - lastDistance + - ", new = " + - thisDistance + - ")" - ); - this.zoomLevel *= difference; - - // Stick to pivot point - const correcture = center.multiplyScalar(difference - 1).divideScalar(this.zoomLevel); - - this.center = this.center.add(correcture); - this.lastPinchPositions = newPinchPositions; - this.userInteraction.dispatch(USER_INTERACT_MOVE); - - // Since we zoomed, abort any programmed zooming - if (this.desiredZoom) { - this.desiredZoom = null; - } - } - } - - // Clamp everything afterwards - this.clampZoomLevel(); - return false; - } - - /** - * Touch end and cancel handler - * @param {TouchEvent=} event - */ - onTouchEnd(event) { - if (event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - } - - clickDetectorGlobals.lastTouchTime = performance.now(); - if (event.changedTouches.length === 0) { - logger.warn("Touch end without changed touches"); - } - - const touch = event.changedTouches[0]; - this.combinedSingleTouchStopHandler(touch.clientX, touch.clientY); - return false; - } - - /** - * Internal touch start handler - * @param {number} x - * @param {number} y - */ - combinedSingleTouchStartHandler(x, y) { - const pos = new Vector(x, y); - if (this.downPreHandler.dispatch(pos, enumMouseButton.left) === STOP_PROPAGATION) { - // Somebody else captured it - return; - } - - this.touchPostMoveVelocity = new Vector(0, 0); - this.currentlyMoving = true; - this.lastMovingPosition = pos; - this.lastMovingPositionLastTick = null; - this.numTicksStandingStill = 0; - this.didMoveSinceTouchStart = false; - } - - /** - * Internal touch move handler - * @param {number} x - * @param {number} y - */ - combinedSingleTouchMoveHandler(x, y) { - const pos = new Vector(x, y); - if (this.movePreHandler.dispatch(pos) === STOP_PROPAGATION) { - // Somebody else captured it - return; - } - - if (!this.currentlyMoving) { - return false; - } - - let delta = this.lastMovingPosition.sub(pos).divideScalar(this.zoomLevel); - if (G_IS_DEV && globalConfig.debug.testCulling) { - // When testing culling, we see everything from the same distance - delta = delta.multiplyScalar(this.zoomLevel * -2); - } - - this.didMoveSinceTouchStart = this.didMoveSinceTouchStart || delta.length() > 0; - this.center = this.center.add(delta); - - this.touchPostMoveVelocity = this.touchPostMoveVelocity - .multiplyScalar(velocitySmoothing) - .add(delta.multiplyScalar(1 - velocitySmoothing)); - - this.lastMovingPosition = pos; - this.userInteraction.dispatch(USER_INTERACT_MOVE); - - // Since we moved, abort any programmed moving - if (this.desiredCenter) { - this.desiredCenter = null; - } - } - - /** - * Internal touch stop handler - */ - combinedSingleTouchStopHandler(x, y) { - if (this.currentlyMoving || this.currentlyPinching) { - this.currentlyMoving = false; - this.currentlyPinching = false; - this.lastMovingPosition = null; - this.lastMovingPositionLastTick = null; - this.numTicksStandingStill = 0; - this.lastPinchPositions = null; - this.userInteraction.dispatch(USER_INTERACT_TOUCHEND); - this.didMoveSinceTouchStart = false; - } - this.upPostHandler.dispatch(new Vector(x, y)); - } - - /** - * Clamps the camera zoom level within the allowed range - */ - clampZoomLevel() { - if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { - return; - } - const wrapper = this.root.app.platformWrapper; - - assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); - this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); - assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel); - - if (this.desiredZoom) { - this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); - } - } - - /** - * Updates the camera - * @param {number} dt Delta time in milliseconds - */ - update(dt) { - dt = Math.min(dt, 33); - this.cameraUpdateTimeBucket += dt; - - // Simulate movement of N FPS - const updatesPerFrame = 4; - const physicsStepSizeMs = 1000.0 / (60.0 * updatesPerFrame); - - let now = this.root.time.systemNow() - 3 * physicsStepSizeMs; - - while (this.cameraUpdateTimeBucket > physicsStepSizeMs) { - now += physicsStepSizeMs; - this.cameraUpdateTimeBucket -= physicsStepSizeMs; - - this.internalUpdatePanning(now, physicsStepSizeMs); - this.internalUpdateZooming(now, physicsStepSizeMs); - this.internalUpdateCentering(now, physicsStepSizeMs); - this.internalUpdateShake(now, physicsStepSizeMs); - this.internalUpdateKeyboardForce(now, physicsStepSizeMs); - } - this.clampZoomLevel(); - } - - /** - * Prepares a context to transform it - * @param {CanvasRenderingContext2D} context - */ - transform(context) { - if (G_IS_DEV && globalConfig.debug.testCulling) { - context.transform(1, 0, 0, 1, 100, 100); - return; - } - - this.clampZoomLevel(); - const zoom = this.zoomLevel; - - context.transform( - // Scale, skew, rotate - zoom, - 0, - 0, - zoom, - - // Translate - -zoom * this.getViewportLeft(), - -zoom * this.getViewportTop() - ); - } - - /** - * Internal shake handler - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdateShake(now, dt) { - this.currentShake = this.currentShake.multiplyScalar(0.92); - } - - /** - * Internal pan handler - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdatePanning(now, dt) { - const baseStrength = velocityStrength * this.root.app.platformWrapper.getTouchPanStrength(); - - this.touchPostMoveVelocity = this.touchPostMoveVelocity.multiplyScalar(velocityFade); - - // Check if the camera is being dragged but standing still: if not, zero out `touchPostMoveVelocity`. - if (this.currentlyMoving && this.desiredCenter === null) { - if ( - this.lastMovingPositionLastTick !== null && - this.lastMovingPositionLastTick.equalsEpsilon(this.lastMovingPosition) - ) { - this.numTicksStandingStill++; - } else { - this.numTicksStandingStill = 0; - } - this.lastMovingPositionLastTick = this.lastMovingPosition.copy(); - - if (this.numTicksStandingStill >= ticksBeforeErasingVelocity) { - this.touchPostMoveVelocity.x = 0; - this.touchPostMoveVelocity.y = 0; - } - } - // Check influence of past points - if (!this.currentlyMoving && !this.currentlyPinching) { - const len = this.touchPostMoveVelocity.length(); - if (len >= velocityMax) { - this.touchPostMoveVelocity.x = (this.touchPostMoveVelocity.x * velocityMax) / len; - this.touchPostMoveVelocity.y = (this.touchPostMoveVelocity.y * velocityMax) / len; - } - - this.center = this.center.add(this.touchPostMoveVelocity.multiplyScalar(baseStrength)); - - // Panning - this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); - this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); - } - } - - /** - * Updates the non user interaction zooming - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdateZooming(now, dt) { - if (!this.currentlyPinching && this.desiredZoom !== null) { - const diff = this.zoomLevel - this.desiredZoom; - if (Math.abs(diff) > 0.0001) { - let fade = 0.94; - if (diff > 0) { - // Zoom out faster than in - fade = 0.9; - } - - assert(Number.isFinite(this.desiredZoom), "Desired zoom is NaN: " + this.desiredZoom); - assert(Number.isFinite(fade), "Zoom fade is NaN: " + fade); - this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade); - assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel); - } else { - this.desiredZoom = null; - } - } - } - - /** - * Updates the non user interaction centering - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdateCentering(now, dt) { - if (!this.currentlyMoving && this.desiredCenter !== null) { - const diff = this.center.direction(this.desiredCenter); - const length = diff.length(); - const tolerance = 1 / this.zoomLevel; - if (length > tolerance) { - const movement = diff.multiplyScalar(Math.min(1, dt * 0.008)); - this.center.x += movement.x; - this.center.y += movement.y; - } else { - this.desiredCenter = null; - } - } - } - - /** - * Updates the keyboard forces - * @param {number} now - * @param {number} dt Delta time - */ - internalUpdateKeyboardForce(now, dt) { - if (!this.currentlyMoving && this.desiredCenter == null) { - const limitingDimension = Math.min(this.root.gameWidth, this.root.gameHeight); - - const moveAmount = ((limitingDimension / 2048) * dt) / this.zoomLevel; - - let forceX = 0; - let forceY = 0; - - const actionMapper = this.root.keyMapper; - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).pressed) { - forceY -= 1; - } - - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).pressed) { - forceY += 1; - } - - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).pressed) { - forceX -= 1; - } - - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).pressed) { - forceX += 1; - } - - let movementSpeed = - this.root.app.settings.getMovementSpeed() * - (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveFaster).pressed ? 4 : 1); - - this.center.x += moveAmount * forceX * movementSpeed; - this.center.y += moveAmount * forceY * movementSpeed; - } - } -} +import { clickDetectorGlobals } from "../core/click_detector"; +import { globalConfig, SUPPORT_TOUCH } from "../core/config"; +import { createLogger } from "../core/logging"; +import { Rectangle } from "../core/rectangle"; +import { Signal, STOP_PROPAGATION } from "../core/signal"; +import { clamp } from "../core/utils"; +import { mixVector, Vector } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { KEYMAPPINGS } from "./key_action_mapper"; +import { GameRoot } from "./root"; + +const logger = createLogger("camera"); + +export const USER_INTERACT_MOVE = "move"; +export const USER_INTERACT_ZOOM = "zoom"; +export const USER_INTERACT_TOUCHEND = "touchend"; + +const velocitySmoothing = 0.5; +const velocityFade = 0.98; +const velocityStrength = 0.4; +const velocityMax = 20; +const ticksBeforeErasingVelocity = 10; + +/** + * @enum {string} + */ +export const enumMouseButton = { + left: "left", + middle: "middle", + right: "right", +}; + +export class Camera extends BasicSerializableObject { + constructor(root) { + super(); + + /** @type {GameRoot} */ + this.root = root; + + // Zoom level, 2 means double size + + // Find optimal initial zoom + + this.zoomLevel = this.findInitialZoom(); + this.clampZoomLevel(); + + /** @type {Vector} */ + this.center = new Vector(0, 0); + + // Input handling + this.currentlyMoving = false; + this.lastMovingPosition = null; + this.lastMovingPositionLastTick = null; + this.numTicksStandingStill = null; + this.cameraUpdateTimeBucket = 0.0; + this.didMoveSinceTouchStart = false; + this.currentlyPinching = false; + this.lastPinchPositions = null; + + this.keyboardForce = new Vector(); + + // Signal which gets emitted once the user changed something + this.userInteraction = new Signal(); + + /** @type {Vector} */ + this.currentShake = new Vector(0, 0); + + /** @type {Vector} */ + this.currentPan = new Vector(0, 0); + + // Set desired pan (camera movement) + /** @type {Vector} */ + this.desiredPan = new Vector(0, 0); + + // Set desired camera center + /** @type {Vector} */ + this.desiredCenter = null; + + // Set desired camera zoom + /** @type {number} */ + this.desiredZoom = null; + + /** @type {Vector} */ + this.touchPostMoveVelocity = new Vector(0, 0); + + // Handlers + this.downPreHandler = /** @type {TypedSignal<[Vector, enumMouseButton]>} */ (new Signal()); + this.movePreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + // this.pinchPreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + this.upPostHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + + this.internalInitEvents(); + this.clampZoomLevel(); + this.bindKeys(); + if (G_IS_DEV) { + window.addEventListener("keydown", ev => { + if (ev.key === "i") { + this.zoomLevel = 3; + } + }); + } + } + + // Serialization + static getId() { + return "Camera"; + } + + static getSchema() { + return { + zoomLevel: types.float, + center: types.vector, + }; + } + + deserialize(data) { + const errorCode = super.deserialize(data); + if (errorCode) { + return errorCode; + } + + // Safety + this.clampZoomLevel(); + } + + // Simple getters & setters + + addScreenShake(amount) { + const currentShakeAmount = this.currentShake.length(); + const scale = 1 / (1 + 3 * currentShakeAmount); + this.currentShake.x = this.currentShake.x + 2 * (Math.random() - 0.5) * scale * amount; + this.currentShake.y = this.currentShake.y + 2 * (Math.random() - 0.5) * scale * amount; + } + + /** + * Sets a point in world space to focus on + * @param {Vector} center + */ + setDesiredCenter(center) { + this.desiredCenter = center.copy(); + this.currentlyMoving = false; + } + + /** + * Sets a desired zoom level + * @param {number} zoom + */ + setDesiredZoom(zoom) { + this.desiredZoom = zoom; + } + + /** + * Returns if this camera is currently moving by a non-user interaction + */ + isCurrentlyMovingToDesiredCenter() { + return this.desiredCenter !== null; + } + + /** + * Sets the camera pan, every frame the camera will move by this amount + * @param {Vector} pan + */ + setPan(pan) { + this.desiredPan = pan.copy(); + } + + /** + * Finds a good initial zoom level + */ + findInitialZoom() { + const desiredWorldSpaceWidth = 15 * globalConfig.tileSize; + const zoomLevelX = this.root.gameWidth / desiredWorldSpaceWidth; + const zoomLevelY = this.root.gameHeight / desiredWorldSpaceWidth; + + const finalLevel = Math.min(zoomLevelX, zoomLevelY); + assert( + Number.isFinite(finalLevel) && finalLevel > 0, + "Invalid zoom level computed for initial zoom: " + finalLevel + ); + return finalLevel; + } + + /** + * Clears all animations + */ + clearAnimations() { + this.touchPostMoveVelocity.x = 0; + this.touchPostMoveVelocity.y = 0; + this.desiredCenter = null; + this.desiredPan.x = 0; + this.desiredPan.y = 0; + this.currentPan.x = 0; + this.currentPan.y = 0; + this.currentlyPinching = false; + this.currentlyMoving = false; + this.lastMovingPosition = null; + this.didMoveSinceTouchStart = false; + this.desiredZoom = null; + } + + /** + * Returns if the user is currently interacting with the camera + * @returns {boolean} true if the user interacts + */ + isCurrentlyInteracting() { + if (this.currentlyPinching) { + return true; + } + if (this.currentlyMoving) { + // Only interacting if moved at least once + return this.didMoveSinceTouchStart; + } + if (this.touchPostMoveVelocity.lengthSquare() > 1) { + return true; + } + return false; + } + + /** + * Returns if in the next frame the viewport will change + * @returns {boolean} true if it willchange + */ + viewportWillChange() { + return this.desiredCenter !== null || this.desiredZoom !== null || this.isCurrentlyInteracting(); + } + + /** + * Cancels all interactions, that is user interaction and non user interaction + */ + cancelAllInteractions() { + this.touchPostMoveVelocity = new Vector(0, 0); + this.desiredCenter = null; + this.currentlyMoving = false; + this.currentlyPinching = false; + this.desiredZoom = null; + } + + /** + * Returns effective viewport width + */ + getViewportWidth() { + return this.root.gameWidth / this.zoomLevel; + } + + /** + * Returns effective viewport height + */ + getViewportHeight() { + return this.root.gameHeight / this.zoomLevel; + } + + /** + * Returns effective world space viewport left + */ + getViewportLeft() { + return this.center.x - this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns effective world space viewport right + */ + getViewportRight() { + return this.center.x + this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns effective world space viewport top + */ + getViewportTop() { + return this.center.y - this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns effective world space viewport bottom + */ + getViewportBottom() { + return this.center.y + this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns the visible world space rect + * @returns {Rectangle} + */ + getVisibleRect() { + return Rectangle.fromTRBL( + Math.floor(this.getViewportTop()), + Math.ceil(this.getViewportRight()), + Math.ceil(this.getViewportBottom()), + Math.floor(this.getViewportLeft()) + ); + } + + getIsMapOverlayActive() { + return this.zoomLevel < globalConfig.mapChunkOverviewMinZoom; + } + + /** + * Attaches all event listeners + */ + internalInitEvents() { + this.eventListenerTouchStart = this.onTouchStart.bind(this); + this.eventListenerTouchEnd = this.onTouchEnd.bind(this); + this.eventListenerTouchMove = this.onTouchMove.bind(this); + this.eventListenerMousewheel = this.onMouseWheel.bind(this); + this.eventListenerMouseDown = this.onMouseDown.bind(this); + this.eventListenerMouseMove = this.onMouseMove.bind(this); + this.eventListenerMouseUp = this.onMouseUp.bind(this); + + if (SUPPORT_TOUCH) { + this.root.canvas.addEventListener("touchstart", this.eventListenerTouchStart); + this.root.canvas.addEventListener("touchend", this.eventListenerTouchEnd); + this.root.canvas.addEventListener("touchcancel", this.eventListenerTouchEnd); + this.root.canvas.addEventListener("touchmove", this.eventListenerTouchMove); + } + + this.root.canvas.addEventListener("wheel", this.eventListenerMousewheel); + this.root.canvas.addEventListener("mousedown", this.eventListenerMouseDown); + this.root.canvas.addEventListener("mousemove", this.eventListenerMouseMove); + window.addEventListener("mouseup", this.eventListenerMouseUp); + // this.root.canvas.addEventListener("mouseout", this.eventListenerMouseUp); + } + + /** + * Cleans up all event listeners + */ + cleanup() { + if (SUPPORT_TOUCH) { + this.root.canvas.removeEventListener("touchstart", this.eventListenerTouchStart); + this.root.canvas.removeEventListener("touchend", this.eventListenerTouchEnd); + this.root.canvas.removeEventListener("touchcancel", this.eventListenerTouchEnd); + this.root.canvas.removeEventListener("touchmove", this.eventListenerTouchMove); + } + + this.root.canvas.removeEventListener("wheel", this.eventListenerMousewheel); + this.root.canvas.removeEventListener("mousedown", this.eventListenerMouseDown); + this.root.canvas.removeEventListener("mousemove", this.eventListenerMouseMove); + window.removeEventListener("mouseup", this.eventListenerMouseUp); + // this.root.canvas.removeEventListener("mouseout", this.eventListenerMouseUp); + } + + /** + * Binds the arrow keys + */ + bindKeys() { + const mapper = this.root.keyMapper; + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).add(() => (this.keyboardForce.y = -1)); + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).add(() => (this.keyboardForce.y = 1)); + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).add(() => (this.keyboardForce.x = 1)); + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).add(() => (this.keyboardForce.x = -1)); + + mapper + .getBinding(KEYMAPPINGS.navigation.mapZoomIn) + .add(() => (this.desiredZoom = this.zoomLevel * 1.2)); + mapper + .getBinding(KEYMAPPINGS.navigation.mapZoomOut) + .add(() => (this.desiredZoom = this.zoomLevel * 0.8)); + + mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add(() => this.centerOnMap()); + } + + centerOnMap() { + this.desiredCenter = new Vector(0, 0); + } + + /** + * Converts from screen to world space + * @param {Vector} screen + * @returns {Vector} world space + */ + screenToWorld(screen) { + const centerSpace = screen.subScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); + return centerSpace.divideScalar(this.zoomLevel).add(this.center); + } + + /** + * Converts from world to screen space + * @param {Vector} world + * @returns {Vector} screen space + */ + worldToScreen(world) { + const screenSpace = world.sub(this.center).multiplyScalar(this.zoomLevel); + return screenSpace.addScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); + } + + /** + * Returns if a point is on screen + * @param {Vector} point + * @returns {boolean} true if its on screen + */ + isWorldPointOnScreen(point) { + const rect = this.getVisibleRect(); + return rect.containsPoint(point.x, point.y); + } + + /** + * Returns if we can further zoom in + * @returns {boolean} + */ + canZoomIn() { + const maxLevel = this.root.app.platformWrapper.getMaximumZoom(); + return this.zoomLevel <= maxLevel - 0.01; + } + + /** + * Returns if we can further zoom out + * @returns {boolean} + */ + canZoomOut() { + const minLevel = this.root.app.platformWrapper.getMinimumZoom(); + return this.zoomLevel >= minLevel + 0.01; + } + + // EVENTS + + /** + * Checks if the mouse event is too close after a touch event and thus + * should get ignored + */ + checkPreventDoubleMouse() { + if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) { + return false; + } + return true; + } + + /** + * Mousedown handler + * @param {MouseEvent} event + */ + onMouseDown(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + if (!this.checkPreventDoubleMouse()) { + return; + } + + this.touchPostMoveVelocity = new Vector(0, 0); + if (event.button === 0) { + this.combinedSingleTouchStartHandler(event.clientX, event.clientY); + } else if (event.button === 1) { + this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle); + } else if (event.button === 2) { + this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right); + } + return false; + } + + /** + * Mousemove handler + * @param {MouseEvent} event + */ + onMouseMove(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + if (!this.checkPreventDoubleMouse()) { + return; + } + + if (event.button === 0) { + this.combinedSingleTouchMoveHandler(event.clientX, event.clientY); + } + + // Clamp everything afterwards + this.clampZoomLevel(); + return false; + } + + /** + * Mouseup handler + * @param {MouseEvent=} event + */ + onMouseUp(event) { + if (event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + } + + if (!this.checkPreventDoubleMouse()) { + return; + } + + this.combinedSingleTouchStopHandler(event.clientX, event.clientY); + return false; + } + + /** + * Mousewheel event + * @param {WheelEvent} event + */ + onMouseWheel(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + const prevZoom = this.zoomLevel; + + const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity(); + assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY); + assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel); + this.zoomLevel *= 1 + delta; + assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel); + + this.clampZoomLevel(); + this.desiredZoom = null; + + const mousePosition = this.root.app.mousePosition; + if (mousePosition) { + const worldPos = this.root.camera.screenToWorld(mousePosition); + const worldDelta = worldPos.sub(this.center); + const actualDelta = this.zoomLevel / prevZoom - 1; + this.center = this.center.add(worldDelta.multiplyScalar(actualDelta)); + this.desiredCenter = null; + } + + return false; + } + + /** + * Touch start handler + * @param {TouchEvent} event + */ + onTouchStart(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + clickDetectorGlobals.lastTouchTime = performance.now(); + this.touchPostMoveVelocity = new Vector(0, 0); + + if (event.touches.length === 1) { + const touch = event.touches[0]; + this.combinedSingleTouchStartHandler(touch.clientX, touch.clientY); + } else if (event.touches.length === 2) { + // if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) { + // // Something prevented pinching + // return false; + // } + + const touch1 = event.touches[0]; + const touch2 = event.touches[1]; + this.currentlyMoving = false; + this.currentlyPinching = true; + this.lastPinchPositions = [ + new Vector(touch1.clientX, touch1.clientY), + new Vector(touch2.clientX, touch2.clientY), + ]; + } + return false; + } + + /** + * Touch move handler + * @param {TouchEvent} event + */ + onTouchMove(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + clickDetectorGlobals.lastTouchTime = performance.now(); + + if (event.touches.length === 1) { + const touch = event.touches[0]; + this.combinedSingleTouchMoveHandler(touch.clientX, touch.clientY); + } else if (event.touches.length === 2) { + if (this.currentlyPinching) { + const touch1 = event.touches[0]; + const touch2 = event.touches[1]; + + const newPinchPositions = [ + new Vector(touch1.clientX, touch1.clientY), + new Vector(touch2.clientX, touch2.clientY), + ]; + + // Get distance of taps last time and now + const lastDistance = this.lastPinchPositions[0].distance(this.lastPinchPositions[1]); + const thisDistance = newPinchPositions[0].distance(newPinchPositions[1]); + + // IMPORTANT to do math max here to avoid NaN and causing an invalid zoom level + const difference = thisDistance / Math.max(0.001, lastDistance); + + // Find old center of zoom + let oldCenter = this.lastPinchPositions[0].centerPoint(this.lastPinchPositions[1]); + + // Find new center of zoom + let center = newPinchPositions[0].centerPoint(newPinchPositions[1]); + + // Compute movement + let movement = oldCenter.sub(center); + this.center.x += movement.x / this.zoomLevel; + this.center.y += movement.y / this.zoomLevel; + + // Compute zoom + center = center.sub(new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2)); + + // Apply zoom + assert( + Number.isFinite(difference), + "Invalid pinch difference: " + + difference + + "(last=" + + lastDistance + + ", new = " + + thisDistance + + ")" + ); + this.zoomLevel *= difference; + + // Stick to pivot point + const correcture = center.multiplyScalar(difference - 1).divideScalar(this.zoomLevel); + + this.center = this.center.add(correcture); + this.lastPinchPositions = newPinchPositions; + this.userInteraction.dispatch(USER_INTERACT_MOVE); + + // Since we zoomed, abort any programmed zooming + if (this.desiredZoom) { + this.desiredZoom = null; + } + } + } + + // Clamp everything afterwards + this.clampZoomLevel(); + return false; + } + + /** + * Touch end and cancel handler + * @param {TouchEvent=} event + */ + onTouchEnd(event) { + if (event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + } + + clickDetectorGlobals.lastTouchTime = performance.now(); + if (event.changedTouches.length === 0) { + logger.warn("Touch end without changed touches"); + } + + const touch = event.changedTouches[0]; + this.combinedSingleTouchStopHandler(touch.clientX, touch.clientY); + return false; + } + + /** + * Internal touch start handler + * @param {number} x + * @param {number} y + */ + combinedSingleTouchStartHandler(x, y) { + const pos = new Vector(x, y); + if (this.downPreHandler.dispatch(pos, enumMouseButton.left) === STOP_PROPAGATION) { + // Somebody else captured it + return; + } + + this.touchPostMoveVelocity = new Vector(0, 0); + this.currentlyMoving = true; + this.lastMovingPosition = pos; + this.lastMovingPositionLastTick = null; + this.numTicksStandingStill = 0; + this.didMoveSinceTouchStart = false; + } + + /** + * Internal touch move handler + * @param {number} x + * @param {number} y + */ + combinedSingleTouchMoveHandler(x, y) { + const pos = new Vector(x, y); + if (this.movePreHandler.dispatch(pos) === STOP_PROPAGATION) { + // Somebody else captured it + return; + } + + if (!this.currentlyMoving) { + return false; + } + + let delta = this.lastMovingPosition.sub(pos).divideScalar(this.zoomLevel); + if (G_IS_DEV && globalConfig.debug.testCulling) { + // When testing culling, we see everything from the same distance + delta = delta.multiplyScalar(this.zoomLevel * -2); + } + + this.didMoveSinceTouchStart = this.didMoveSinceTouchStart || delta.length() > 0; + this.center = this.center.add(delta); + + this.touchPostMoveVelocity = this.touchPostMoveVelocity + .multiplyScalar(velocitySmoothing) + .add(delta.multiplyScalar(1 - velocitySmoothing)); + + this.lastMovingPosition = pos; + this.userInteraction.dispatch(USER_INTERACT_MOVE); + + // Since we moved, abort any programmed moving + if (this.desiredCenter) { + this.desiredCenter = null; + } + } + + /** + * Internal touch stop handler + */ + combinedSingleTouchStopHandler(x, y) { + if (this.currentlyMoving || this.currentlyPinching) { + this.currentlyMoving = false; + this.currentlyPinching = false; + this.lastMovingPosition = null; + this.lastMovingPositionLastTick = null; + this.numTicksStandingStill = 0; + this.lastPinchPositions = null; + this.userInteraction.dispatch(USER_INTERACT_TOUCHEND); + this.didMoveSinceTouchStart = false; + } + this.upPostHandler.dispatch(new Vector(x, y)); + } + + /** + * Clamps the camera zoom level within the allowed range + */ + clampZoomLevel() { + if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { + return; + } + const wrapper = this.root.app.platformWrapper; + + assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); + this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel); + + if (this.desiredZoom) { + this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + } + } + + /** + * Updates the camera + * @param {number} dt Delta time in milliseconds + */ + update(dt) { + dt = Math.min(dt, 33); + this.cameraUpdateTimeBucket += dt; + + // Simulate movement of N FPS + const updatesPerFrame = 4; + const physicsStepSizeMs = 1000.0 / (60.0 * updatesPerFrame); + + let now = this.root.time.systemNow() - 3 * physicsStepSizeMs; + + while (this.cameraUpdateTimeBucket > physicsStepSizeMs) { + now += physicsStepSizeMs; + this.cameraUpdateTimeBucket -= physicsStepSizeMs; + + this.internalUpdatePanning(now, physicsStepSizeMs); + this.internalUpdateZooming(now, physicsStepSizeMs); + this.internalUpdateCentering(now, physicsStepSizeMs); + this.internalUpdateShake(now, physicsStepSizeMs); + this.internalUpdateKeyboardForce(now, physicsStepSizeMs); + } + this.clampZoomLevel(); + } + + /** + * Prepares a context to transform it + * @param {CanvasRenderingContext2D} context + */ + transform(context) { + if (G_IS_DEV && globalConfig.debug.testCulling) { + context.transform(1, 0, 0, 1, 100, 100); + return; + } + + this.clampZoomLevel(); + const zoom = this.zoomLevel; + + context.transform( + // Scale, skew, rotate + zoom, + 0, + 0, + zoom, + + // Translate + -zoom * this.getViewportLeft(), + -zoom * this.getViewportTop() + ); + } + + /** + * Internal shake handler + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdateShake(now, dt) { + this.currentShake = this.currentShake.multiplyScalar(0.92); + } + + /** + * Internal pan handler + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdatePanning(now, dt) { + const baseStrength = velocityStrength * this.root.app.platformWrapper.getTouchPanStrength(); + + this.touchPostMoveVelocity = this.touchPostMoveVelocity.multiplyScalar(velocityFade); + + // Check if the camera is being dragged but standing still: if not, zero out `touchPostMoveVelocity`. + if (this.currentlyMoving && this.desiredCenter === null) { + if ( + this.lastMovingPositionLastTick !== null && + this.lastMovingPositionLastTick.equalsEpsilon(this.lastMovingPosition) + ) { + this.numTicksStandingStill++; + } else { + this.numTicksStandingStill = 0; + } + this.lastMovingPositionLastTick = this.lastMovingPosition.copy(); + + if (this.numTicksStandingStill >= ticksBeforeErasingVelocity) { + this.touchPostMoveVelocity.x = 0; + this.touchPostMoveVelocity.y = 0; + } + } + // Check influence of past points + if (!this.currentlyMoving && !this.currentlyPinching) { + const len = this.touchPostMoveVelocity.length(); + if (len >= velocityMax) { + this.touchPostMoveVelocity.x = (this.touchPostMoveVelocity.x * velocityMax) / len; + this.touchPostMoveVelocity.y = (this.touchPostMoveVelocity.y * velocityMax) / len; + } + + this.center = this.center.add(this.touchPostMoveVelocity.multiplyScalar(baseStrength)); + + // Panning + this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); + this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); + } + } + + /** + * Updates the non user interaction zooming + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdateZooming(now, dt) { + if (!this.currentlyPinching && this.desiredZoom !== null) { + const diff = this.zoomLevel - this.desiredZoom; + if (Math.abs(diff) > 0.0001) { + let fade = 0.94; + if (diff > 0) { + // Zoom out faster than in + fade = 0.9; + } + + assert(Number.isFinite(this.desiredZoom), "Desired zoom is NaN: " + this.desiredZoom); + assert(Number.isFinite(fade), "Zoom fade is NaN: " + fade); + this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade); + assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel); + } else { + this.desiredZoom = null; + } + } + } + + /** + * Updates the non user interaction centering + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdateCentering(now, dt) { + if (!this.currentlyMoving && this.desiredCenter !== null) { + const diff = this.center.direction(this.desiredCenter); + const length = diff.length(); + const tolerance = 1 / this.zoomLevel; + if (length > tolerance) { + const movement = diff.multiplyScalar(Math.min(1, dt * 0.008)); + this.center.x += movement.x; + this.center.y += movement.y; + } else { + this.desiredCenter = null; + } + } + } + + /** + * Updates the keyboard forces + * @param {number} now + * @param {number} dt Delta time + */ + internalUpdateKeyboardForce(now, dt) { + if (!this.currentlyMoving && this.desiredCenter == null) { + const limitingDimension = Math.min(this.root.gameWidth, this.root.gameHeight); + + const moveAmount = ((limitingDimension / 2048) * dt) / this.zoomLevel; + + let forceX = 0; + let forceY = 0; + + const actionMapper = this.root.keyMapper; + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).pressed) { + forceY -= 1; + } + + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).pressed) { + forceY += 1; + } + + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).pressed) { + forceX -= 1; + } + + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).pressed) { + forceX += 1; + } + + let movementSpeed = + this.root.app.settings.getMovementSpeed() * + (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveFaster).pressed ? 4 : 1); + + this.center.x += moveAmount * forceX * movementSpeed; + this.center.y += moveAmount * forceY * movementSpeed; + } + } +} diff --git a/src/js/game/entity_manager.js b/src/js/game/entity_manager.js index c76bd46d..334f4e28 100644 --- a/src/js/game/entity_manager.js +++ b/src/js/game/entity_manager.js @@ -23,7 +23,7 @@ export class EntityManager extends BasicSerializableObject { /** @type {Array} */ this.entities = []; - // We store a seperate list with entities to destroy, since we don't destroy + // We store a separate list with entities to destroy, since we don't destroy // them instantly /** @type {Array} */ this.destroyList = []; diff --git a/src/js/game/hud/parts/building_placer_logic.js b/src/js/game/hud/parts/building_placer_logic.js index 6031e555..5b31f3cd 100644 --- a/src/js/game/hud/parts/building_placer_logic.js +++ b/src/js/game/hud/parts/building_placer_logic.js @@ -330,7 +330,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { if (tileBelow && this.root.app.settings.getAllSettings().pickMinerOnPatch) { this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding)); - // Select chained miner if available, since thats always desired once unlocked + // Select chained miner if available, since that's always desired once unlocked if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) { this.currentVariant.set(enumMinerVariants.chainable); } diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 62480caf..bf02d518 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -360,7 +360,7 @@ export class BeltSystem extends GameSystemWithFilter { const followUpTile = staticComp.origin.add(followUpVector); const followUpEntity = this.root.map.getLayerContentXY(followUpTile.x, followUpTile.y, entity.layer); - // Check if theres a belt at the tile we point to + // Check if there's a belt at the tile we point to if (followUpEntity) { const followUpBeltComp = followUpEntity.components.Belt; if (followUpBeltComp) { @@ -390,7 +390,7 @@ export class BeltSystem extends GameSystemWithFilter { const supplyTile = staticComp.origin.add(supplyVector); const supplyEntity = this.root.map.getLayerContentXY(supplyTile.x, supplyTile.y, entity.layer); - // Check if theres a belt at the tile we point to + // Check if there's a belt at the tile we point to if (supplyEntity) { const supplyBeltComp = supplyEntity.components.Belt; if (supplyBeltComp) { diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index 55254e51..feda1594 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -170,7 +170,7 @@ export class MinerSystem extends GameSystemWithFilter { } // Draw the item background - this is to hide the ejected item animation from - // the item ejecto + // the item ejector const padding = 3; const destX = staticComp.origin.x * globalConfig.tileSize + padding; diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index 0cbd52b4..ff437173 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -71,7 +71,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { const range = globalConfig.undergroundBeltMaxTilesByTier[tier]; // FIND ENTRANCE - // Search for the entrance which is furthes apart (this is why we can't reuse logic here) + // Search for the entrance which is farthest apart (this is why we can't reuse logic here) let matchingEntrance = null; for (let i = 0; i < range; ++i) { currentPos.addInplace(offset); diff --git a/src/js/game/systems/wire.js b/src/js/game/systems/wire.js index fa9287e1..79f8a780 100644 --- a/src/js/game/systems/wire.js +++ b/src/js/game/systems/wire.js @@ -162,7 +162,7 @@ export class WireSystem extends GameSystemWithFilter { const tunnelEntities = this.root.entityMgr.getAllWithComponent(WireTunnelComponent); const pinEntities = this.root.entityMgr.getAllWithComponent(WiredPinsComponent); - // Clear all network references, but not on the first update since thats the deserializing one + // Clear all network references, but not on the first update since that's the deserializing one if (!this.isFirstRecompute) { for (let i = 0; i < wireEntities.length; ++i) { wireEntities[i].components.Wire.linkedNetwork = null; @@ -432,7 +432,7 @@ export class WireSystem extends GameSystemWithFilter { continue; } - // Check if its a tunnel, if so, go to the forwarded item + // Check if it's a tunnel, if so, go to the forwarded item const tunnelComp = entity.components.WireTunnel; if (tunnelComp) { if (visitedTunnels.has(entity.uid)) { diff --git a/translations/README.md b/translations/README.md index 7695f022..596da8d8 100644 --- a/translations/README.md +++ b/translations/README.md @@ -1,80 +1,82 @@ -# Translations - -The base language is English and can be found [here](base-en.yaml). - -## Languages - -- [German](base-de.yaml) -- [French](base-fr.yaml) -- [Korean](base-kor.yaml) -- [Dutch](base-nl.yaml) -- [Polish](base-pl.yaml) -- [Portuguese (Brazil)](base-pt-BR.yaml) -- [Portuguese (Portugal)](base-pt-PT.yaml) -- [Russian](base-ru.yaml) -- [Greek](base-el.yaml) -- [Italian](base-it.yaml) -- [Romanian](base-ro.yaml) -- [Swedish](base-sv.yaml) -- [Chinese (Simplified)](base-zh-CN.yaml) -- [Chinese (Traditional)](base-zh-TW.yaml) -- [Spanish](base-es.yaml) -- [Hungarian](base-hu.yaml) -- [Turkish](base-tr.yaml) -- [Japanese](base-ja.yaml) -- [Lithuanian](base-lt.yaml) -- [Arabic](base-ar.yaml) -- [Norwegian](base-no.yaml) -- [Kroatian](base-hr.yaml) -- [Danish](base-da.yaml) -- [Finnish](base-fi.yaml) -- [Catalan](base-cat.yaml) -- [Slovenian](base-sl.yaml) -- [Ukrainian](base-uk.yaml) -- [Indonesian](base-ind.yaml) -- [Serbian](base-sr.yaml) - -(If you want to translate into a new language, see below!) - -## Editing existing translations - -If you want to edit an existing translation (Fixing typos, Updating it to a newer version, etc), you can just use the github file editor to edit the file. - -- Click the language you want to edit from the list above -- Click the small "edit" symbol on the top right - -edit symbol - -- Do the changes you wish to do (Be sure **not** to translate placeholders! For example, ` minutes` should get ` Minuten` and **not** ` Minuten`!) - -- Click "Propose Changes" - -propose changes - -- Click "Create pull request" - -create pull request - -- I will review your changes and make comments, and eventually merge them so they will be in the next release! Be sure to regulary check the created pull request for comments. - -## Adding a new language - -Please DM me on Discord (tobspr#5407), so I can add the language template for you. - -Please use the following template: - -``` -Hey, could you add a new translation? - -Language: -Short code: -Local Name: -``` - -You can find the short code [here](https://www.science.co.il/language/Codes.php) (In column `Code 2`). - -PS: I'm super busy, but I'll give my best to do it quickly! - -## Updating a language to the latest version - -Run `yarn syncTranslations` in the root directory to synchronize all translations to the latest version! This will remove obsolete keys and add newly added keys. (Run `yarn` before to install packes). +# Translations + +The base language is English and can be found [here](base-en.yaml). + +## Languages + +- [German](base-de.yaml) +- [French](base-fr.yaml) +- [Korean](base-kor.yaml) +- [Dutch](base-nl.yaml) +- [Polish](base-pl.yaml) +- [Portuguese (Brazil)](base-pt-BR.yaml) +- [Portuguese (Portugal)](base-pt-PT.yaml) +- [Russian](base-ru.yaml) +- [Greek](base-el.yaml) +- [Italian](base-it.yaml) +- [Romanian](base-ro.yaml) +- [Swedish](base-sv.yaml) +- [Chinese (Simplified)](base-zh-CN.yaml) +- [Chinese (Traditional)](base-zh-TW.yaml) +- [Spanish](base-es.yaml) +- [Hungarian](base-hu.yaml) +- [Turkish](base-tr.yaml) +- [Japanese](base-ja.yaml) +- [Lithuanian](base-lt.yaml) +- [Arabic](base-ar.yaml) +- [Norwegian](base-no.yaml) +- [Kroatian](base-hr.yaml) +- [Danish](base-da.yaml) +- [Finnish](base-fi.yaml) +- [Catalan](base-cat.yaml) +- [Slovenian](base-sl.yaml) +- [Ukrainian](base-uk.yaml) +- [Indonesian](base-ind.yaml) +- [Serbian](base-sr.yaml) + +(If you want to translate into a new language, see below!) + +## Editing existing translations + +If you want to edit an existing translation (Fixing typos, updating it to a newer version, etc), you can just use the github file editor to edit the file. + +- Click the language you want to edit from the list above +- Click the small "edit" symbol on the top right + +edit symbol + +- Do the changes you wish to do (Be sure **not** to translate placeholders! For example, ` minutes` should get ` Minuten` and **not** ` Minuten`!) + +- Click "Propose Changes" + +propose changes + +- Click "Create pull request" + +create pull request + +- I will review your changes and make comments, and eventually merge them so they will be in the next release! Be sure to regulary check the created pull request for comments. + +## Adding a new language + +Please DM me on Discord (tobspr#5407), so I can add the language template for you. + +**Important: I am currently not accepting new languages until the wires update is out!** + +Please use the following template: + +``` +Hey, could you add a new translation? + +Language: +Short code: +Local Name: +``` + +You can find the short code [here](https://www.science.co.il/language/Codes.php) (In column `Code 2`). + +PS: I'm super busy, but I'll give my best to do it quickly! + +## Updating a language to the latest version + +Run `yarn syncTranslations` in the root directory to synchronize all translations to the latest version! This will remove obsolete keys and add newly added keys. (Run `yarn` before to install packages).