diff --git a/src/css/ingame_hud/waypoints.scss b/src/css/ingame_hud/waypoints.scss new file mode 100644 index 00000000..2f5fe096 --- /dev/null +++ b/src/css/ingame_hud/waypoints.scss @@ -0,0 +1,95 @@ +#ingame_HUD_Waypoints { + .content { + @include S(width, 500px); + } + + .wizardWrap { + display: grid; + grid-template-columns: 1fr auto; + @include S(column-gap, 5px); + } + + .content { + @include S(margin-top, 10px); + @include S(height, 350px); + overflow-y: scroll; + display: flex; + flex-direction: column; + + justify-content: flex-start; + + @include S(padding-right, 4px); + + > .noWaypoints { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + @include PlainText; + color: #aaa; + } + + > div { + background: #f4f4f4; + @include S(border-radius, $globalBorderRadius); + @include S(margin-bottom, 4px); + display: grid; + + @include DarkThemeOverride { + background: #222428; + color: #efefef; + } + + grid-template-columns: 1fr auto; + @include S(padding, 5px); + @include S(padding-left, 10px); + &:last-child { + margin-bottom: 0; + } + + .title { + @include Text; + + @include S(padding-top, 10px); + } + + .position { + @include PlainText; + opacity: 0.7; + } + + button { + @include S(margin, 4px); + } + + .removeButton { + background: #df3f3d; + } + + .teleportButton { + background: #804db1; + } + } + } + + .dialogInner { + &[data-displaymode="detailed"] .content.hasEntries { + > div { + @include S(padding, 10px); + @include S(height, 40px); + grid-template-columns: auto 1fr auto; + @include S(grid-column-gap, 15px); + + .counter { + grid-column: 3 / 4; + grid-row: 1 / 2; + @include Heading; + align-self: center; + text-align: right; + color: #55595a; + } + } + } + } +} diff --git a/src/css/main.scss b/src/css/main.scss index 7a3c6df3..a46723cd 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -39,6 +39,7 @@ @import "ingame_hud/vignette_overlay"; @import "ingame_hud/statistics"; @import "ingame_hud/pinned_shapes"; +@import "ingame_hud/waypoints"; @import "ingame_hud/notifications"; @import "ingame_hud/settings_menu"; @import "ingame_hud/debug_info"; @@ -71,6 +72,7 @@ ingame_HUD_BetaOverlay, ingame_HUD_UnlockNotification, ingame_HUD_Shop, ingame_HUD_Statistics, +ingame_HUD_Waypoints, ingame_HUD_SettingsMenu, ingame_HUD_ModalDialogs; diff --git a/src/js/game/camera.js b/src/js/game/camera.js index d7608ce9..7c902a95 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -56,6 +56,9 @@ export class Camera extends BasicSerializableObject { /** @type {Vector} */ this.center = new Vector(0, 0); + /** @type {{ name: string, pos: Vector }[]} */ + this.waypoints = []; + // Input handling this.currentlyMoving = false; this.lastMovingPosition = null; @@ -117,6 +120,12 @@ export class Camera extends BasicSerializableObject { return { zoomLevel: types.float, center: types.vector, + waypoints: types.array( + types.structured({ + name: types.string, + pos: types.vector, + }) + ), }; } diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index b66c85e3..da3e1086 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -18,6 +18,7 @@ import { HUDVignetteOverlay } from "./parts/vignette_overlay"; import { HUDStatistics } from "./parts/statistics"; import { MetaBuilding } from "../meta_building"; import { HUDPinnedShapes } from "./parts/pinned_shapes"; +import { HUDWaypoints } from "./parts/waypoints"; import { ShapeDefinition } from "../shape_definition"; import { HUDNotifications, enumNotificationType } from "./parts/notifications"; import { HUDSettingsMenu } from "./parts/settings_menu"; @@ -26,6 +27,7 @@ import { HUDEntityDebugger } from "./parts/entity_debugger"; import { KEYMAPPINGS } from "../key_action_mapper"; import { HUDWatermark } from "./parts/watermark"; import { HUDModalDialogs } from "./parts/modal_dialogs"; +import { Vector } from "../../core/vector"; export class GameHUD { /** @@ -53,6 +55,7 @@ export class GameHUD { shop: new HUDShop(this.root), statistics: new HUDStatistics(this.root), + waypoints: new HUDWaypoints(this.root), vignetteOverlay: new HUDVignetteOverlay(this.root), diff --git a/src/js/game/hud/parts/game_menu.js b/src/js/game/hud/parts/game_menu.js index ef05bdbf..d0a85033 100644 --- a/src/js/game/hud/parts/game_menu.js +++ b/src/js/game/hud/parts/game_menu.js @@ -29,6 +29,12 @@ export class HUDGameMenu extends BaseHUDPart { handler: () => this.root.hud.parts.statistics.show(), keybinding: KEYMAPPINGS.ingame.menuOpenStats, }, + { + id: "waypoints", + label: "Waypoints", + handler: () => this.root.hud.parts.waypoints.show(), + keybinding: KEYMAPPINGS.ingame.menuOpenWaypoints, + }, ]; /** @type {Array<{ diff --git a/src/js/game/hud/parts/waypoints.js b/src/js/game/hud/parts/waypoints.js new file mode 100644 index 00000000..8067feb7 --- /dev/null +++ b/src/js/game/hud/parts/waypoints.js @@ -0,0 +1,215 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { DrawParameters } from "../../../core/draw_parameters"; +import { makeDiv, removeAllChildren, makeButton } from "../../../core/utils"; +import { T } from "../../../translations"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { InputReceiver } from "../../../core/input_receiver"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { createLogger } from "../../../core/logging"; + +const logger = createLogger("waypoints"); + +export class HUDWaypoints extends BaseHUDPart { + createElements(parent) { + this.background = makeDiv(parent, "ingame_HUD_Waypoints", ["ingameDialog"]); + + // DIALOG Inner / Wrapper + this.dialogInner = makeDiv(this.background, null, ["dialogInner"]); + this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.waypoints.title); + this.closeButton = makeDiv(this.title, null, ["closeButton"]); + this.trackClicks(this.closeButton, this.close); + + this.wizardWrap = makeDiv(this.dialogInner, null, ["wizardWrap"]); + + // FIXME: Make use of built-in methods + this.nameInput = document.createElement("input"); + this.nameInput.classList.add("findOrCreate"); + this.nameInput.placeholder = T.ingame.waypoints.findOrCreate; + + this.nameInput.addEventListener("focus", () => { + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.textInputReciever); + }); + + this.nameInput.addEventListener("blur", () => { + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + }); + + this.nameInput.addEventListener("keyup", ev => { + if (ev.keyCode == 13) { + ev.preventDefault(); + if (!this.newWaypoint()) { + return; + } + + this.nameInput.blur(); + return; + } + + this.rerenderFull(); + }); + + this.wizardWrap.appendChild(this.nameInput); + + this.newButton = makeButton(this.wizardWrap, ["newButton"], T.ingame.waypoints.buttonNew); + this.trackClicks(this.newButton, this.newWaypoint); + + this.contentDiv = makeDiv(this.dialogInner, null, ["content"]); + } + + initialize() { + this.domAttach = new DynamicDomAttach(this.root, this.background, { + attachClass: "visible", + }); + + this.textInputReciever = new InputReceiver("waypoints_text"); + + this.inputReciever = new InputReceiver("waypoints"); + this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); + + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuOpenWaypoints).add(this.close, this); + + this.close(); + this.rerenderFull(); + } + + getNextWaypointName() { + const inputName = this.nameInput.value.trim().substr(0, 32); + if (inputName !== "") return inputName; + + let counter = 0; + let autoName = "The BEST name for a WAYPOINT!"; + + do { + counter++; + autoName = T.ingame.waypoints.defaultName.replace("", counter.toString()); + } while (this.waypoints.find(w => w.name == autoName)); + + return autoName; + } + + newWaypoint() { + const vector = this.root.camera.center.round(); + if (this.waypoints.find(w => w.pos.distance(vector) < 2)) { + return false; + } + + this.waypoints.push({ + name: this.getNextWaypointName(), + pos: vector, + }); + + this.nameInput.value = ""; + this.rerenderFull(); + return true; + } + + cleanup() { + document.body.classList.remove("ingameDialogOpen"); + } + + show() { + this.visible = true; + document.body.classList.add("ingameDialogOpen"); + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.rerenderFull(); + this.update(); + } + + close() { + this.nameInput.value = ""; + + this.visible = false; + document.body.classList.remove("ingameDialogOpen"); + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + this.root.app.inputMgr.makeSureDetached(this.textInputReciever); + this.update(); + } + + update() { + this.domAttach.update(this.visible); + } + + /** + * @param {number} index + */ + removeWaypoint(index) { + if (this.waypoints[index] === undefined) { + logger.warn("Attempt to remove nonexisting waypoint", index); + return; + } + + this.waypoints.splice(index, 1); + this.rerenderFull(); + } + + /** + * @param {number} index + */ + waypointTeleport(index) { + if (this.waypoints[index] === undefined) { + logger.warn("Attempt to teleport to nonexisting waypoint", index); + return; + } + + this.close(); + this.root.camera.setDesiredCenter(this.waypoints[index].pos); + } + + rerenderFull() { + this.waypoints = this.root.camera.waypoints; + removeAllChildren(this.contentDiv); + + if (this.waypoints.length == 0) { + return (this.contentDiv.innerHTML = ` + + ${T.ingame.waypoints.noWaypoints.replace("", T.ingame.waypoints.buttonNew)} + + `); + } + + this.waypoints.forEach(waypoint => { + const term = this.nameInput.value.replaceAll(" ", ""); + if (term !== "") { + const simpleName = waypoint.name.toLowerCase().replaceAll(" ", ""); + if (!simpleName.includes(term.toLowerCase())) { + return; + } + } + + const tilePos = waypoint.pos.toTileSpace(); + + const wpContainer = makeDiv(this.contentDiv, null, ["waypoint"]); + const positionStr = T.ingame.waypoints.position + .replace("", tilePos.x) + .replace("", tilePos.y); + + const index = this.waypoints.indexOf(waypoint); + + // Waypoint name + makeDiv(wpContainer, null, ["title"], waypoint.name); + + // Remove button + const buttonRemove = makeButton(wpContainer, ["removeButton"], T.ingame.waypoints.buttonRemove); + this.trackClicks(buttonRemove, this.removeWaypoint.bind(this, index)); + + // Waypoint coords + makeDiv(wpContainer, null, ["position"], positionStr); + + // Teleport button + const buttonTeleport = makeButton( + wpContainer, + ["teleportButton"], + T.ingame.waypoints.buttonTeleport + ); + this.trackClicks(buttonTeleport, this.waypointTeleport.bind(this, index)); + }); + } + + /** + * @param {DrawParameters} parameters + */ + drawOverlays(parameters) { + // TODO: Draw tile overlays on existing waypoints + } +} diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 40774933..1e7048a4 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -29,6 +29,7 @@ export const KEYMAPPINGS = { menuOpenShop: { keyCode: key("F") }, menuOpenStats: { keyCode: key("G") }, + menuOpenWaypoints: { keyCode: key("H") }, toggleHud: { keyCode: 113 }, // F2 toggleFPSInfo: { keyCode: 115 }, // F1 diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 450a3c9b..432de691 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -239,6 +239,21 @@ ingame: # Displays the shapes per minute, e.g. '523 / m' shapesPerMinute: / m + # The "Waypoints" window + waypoints: + title: Waypoints + position: "X: ; Y: " + + findOrCreate: Find or create... + buttonNew: Add Waypoint + buttonTeleport: Teleport + buttonRemove: Remove + + # When a new waypoint is created, this name is used. + defaultName: Waypoint + noWaypoints: >- + There are no waypoints. Click "" to create a new waypoint. + # Settings menu, when you press "ESC" settingsMenu: playtime: Playtime