From 84969a9205f3fb9f69cda4412a90c4632f195ea7 Mon Sep 17 00:00:00 2001 From: tobspr Date: Wed, 17 Jun 2020 13:12:39 +0200 Subject: [PATCH] Refactor building placer --- src/js/core/utils.js | 14 + src/js/game/buildings/underground_belt.js | 4 - src/js/game/camera.js | 10 +- src/js/game/hud/parts/blueprint_placer.js | 8 +- src/js/game/hud/parts/building_placer.js | 509 +++-------------- .../game/hud/parts/building_placer_logic.js | 523 ++++++++++++++++++ src/js/game/hud/parts/mass_selector.js | 8 +- src/js/game/key_action_mapper.js | 4 +- src/js/game/logic.js | 10 +- src/js/game/themes/dark.json | 2 + src/js/game/themes/light.json | 2 + 11 files changed, 625 insertions(+), 469 deletions(-) create mode 100644 src/js/game/hud/parts/building_placer_logic.js diff --git a/src/js/core/utils.js b/src/js/core/utils.js index fdf97880..6c38ad98 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -978,3 +978,17 @@ export function formatItemsPerSecond(speed, double = false) { : T.ingame.buildingPlacement.infoTexts.itemsPerSecond.replace("", "" + round2Digits(speed)) + (double ? " " + T.ingame.buildingPlacement.infoTexts.itemsPerSecondDouble : ""); } + +/** + * Finds the corner point between two vectors + * @param {Vector} a + * @param {Vector} b + */ +export function findCornerBetweenPoints(a, b) { + const delta = b.sub(a); + if (Math_abs(delta.x) > Math_abs(delta.y)) { + return new Vector(a.x, b.y); + } else { + return new Vector(b.x, a.y); + } +} diff --git a/src/js/game/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 85679b05..6d24267b 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -38,10 +38,6 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { return true; } - getHasDirectionLockAvailable() { - return true; - } - getStayInPlacementMode() { return true; } diff --git a/src/js/game/camera.js b/src/js/game/camera.js index 0d3f285f..3e77c8f6 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -885,25 +885,25 @@ export class Camera extends BasicSerializableObject { let forceY = 0; const actionMapper = this.root.keyMapper; - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).isCurrentlyPressed()) { + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).pressed) { forceY -= 1; } - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).isCurrentlyPressed()) { + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).pressed) { forceY += 1; } - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).isCurrentlyPressed()) { + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).pressed) { forceX -= 1; } - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).isCurrentlyPressed()) { + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).pressed) { forceX += 1; } let movementSpeed = this.root.app.settings.getMovementSpeed() * - (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveFaster).isCurrentlyPressed() ? 4 : 1); + (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/hud/parts/blueprint_placer.js b/src/js/game/hud/parts/blueprint_placer.js index 0ffff9b4..fa9faca2 100644 --- a/src/js/game/hud/parts/blueprint_placer.js +++ b/src/js/game/hud/parts/blueprint_placer.js @@ -108,7 +108,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart { this.root.hubGoals.takeShapeByKey(blueprintShape, cost); // This actually feels weird - // if (!this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeMultiple).isCurrentlyPressed()) { + // if (!this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeMultiple).pressed) { // this.currentBlueprint.set(null); // } } @@ -133,11 +133,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart { rotateBlueprint() { if (this.currentBlueprint.get()) { - if ( - this.root.keyMapper - .getBinding(KEYMAPPINGS.placement.rotateInverseModifier) - .isCurrentlyPressed() - ) { + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { this.currentBlueprint.get().rotateCcw(); } else { this.currentBlueprint.get().rotateCw(); diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index 1dde75e8..01b6b797 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -1,87 +1,21 @@ -import { Math_abs, Math_degrees, Math_radians, Math_sign } from "../../../core/builtins"; +import { Math_radians } from "../../../core/builtins"; import { globalConfig } from "../../../core/config"; import { DrawParameters } from "../../../core/draw_parameters"; import { drawRotatedSprite } from "../../../core/draw_utils"; import { Loader } from "../../../core/loader"; -import { STOP_PROPAGATION } from "../../../core/signal"; -import { TrackedState } from "../../../core/tracked_state"; -import { makeDiv, removeAllChildren } from "../../../core/utils"; -import { - enumDirectionToAngle, - enumDirectionToVector, - enumInvertedDirections, - Vector, - enumAngleToDirection, -} from "../../../core/vector"; -import { enumMouseButton } from "../../camera"; -import { StaticMapEntityComponent } from "../../components/static_map_entity"; -import { Entity } from "../../entity"; -import { defaultBuildingVariant, MetaBuilding } from "../../meta_building"; -import { BaseHUDPart } from "../base_hud_part"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { findCornerBetweenPoints, makeDiv, removeAllChildren } from "../../../core/utils"; +import { enumDirectionToAngle, enumDirectionToVector, enumInvertedDirections } from "../../../core/vector"; import { T } from "../../../translations"; import { KEYMAPPINGS } from "../../key_action_mapper"; +import { defaultBuildingVariant } from "../../meta_building"; import { THEME } from "../../theme"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { HUDBuildingPlacerLogic } from "./building_placer_logic"; -export class HUDBuildingPlacer extends BaseHUDPart { - initialize() { - /** @type {TypedTrackedState} */ - this.currentMetaBuilding = new TrackedState(this.onSelectedMetaBuildingChanged, this); - this.currentBaseRotation = 0; - - /** @type {Entity} */ - this.fakeEntity = null; - - const keyActionMapper = this.root.keyMapper; - keyActionMapper - .getBinding(KEYMAPPINGS.placement.abortBuildingPlacement) - .add(this.abortPlacement, this); - keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); - - keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this); - keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this); - - this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this); - this.root.hud.signals.pasteBlueprintRequested.add(this.abortPlacement, this); - - this.domAttach = new DynamicDomAttach(this.root, this.element, {}); - - this.root.camera.downPreHandler.add(this.onMouseDown, this); - this.root.camera.movePreHandler.add(this.onMouseMove, this); - this.root.camera.upPostHandler.add(this.abortDragging, this); - - this.currentlyDragging = false; - this.currentVariant = new TrackedState(this.rerenderVariants, this); - - this.variantsAttach = new DynamicDomAttach(this.root, this.variantsElement, {}); - - /** - * Whether we are currently drag-deleting - */ - this.currentlyDeleting = false; - - /** - * Stores which variants for each building we prefer, this is based on what - * the user last selected - */ - this.preferredVariants = {}; - - /** - * The tile we last dragged onto - * @type {Vector} - * */ - this.lastDragTile = null; - - /** - * The tile we initially dragged from - * @type {Vector} - */ - this.initialDragTile = null; - - this.root.signals.storyGoalCompleted.add(this.rerenderVariants, this); - this.root.signals.upgradePurchased.add(this.rerenderVariants, this); - } - +export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { + /** + * @param {HTMLElement} parent + */ createElements(parent) { this.element = makeDiv(parent, "ingame_HUD_PlacementHints", [], ``); @@ -101,201 +35,15 @@ export class HUDBuildingPlacer extends BaseHUDPart { this.variantsElement = makeDiv(parent, "ingame_HUD_PlacerVariants"); } - abortPlacement() { - if (this.currentMetaBuilding.get()) { - this.currentMetaBuilding.set(null); - return STOP_PROPAGATION; - } - } + initialize() { + super.initialize(); - /** - * mouse down pre handler - * @param {Vector} pos - * @param {enumMouseButton} button - */ - onMouseDown(pos, button) { - if (this.root.camera.getIsMapOverlayActive()) { - return; - } + // Bind to signals + this.signals.variantChanged.add(this.rerenderVariants, this); - // Placement - if (button === enumMouseButton.left && this.currentMetaBuilding.get()) { - this.currentlyDragging = true; - this.currentlyDeleting = false; - this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); + this.domAttach = new DynamicDomAttach(this.root, this.element, {}); - // Place initial building - this.tryPlaceCurrentBuildingAt(this.lastDragTile); - - return STOP_PROPAGATION; - } - - // Deletion - if (button === enumMouseButton.right && !this.currentMetaBuilding.get()) { - this.currentlyDragging = true; - this.currentlyDeleting = true; - this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); - this.currentMetaBuilding.set(null); - return STOP_PROPAGATION; - } - } - - /** - * mouse move pre handler - * @param {Vector} pos - */ - onMouseMove(pos) { - if (this.root.camera.getIsMapOverlayActive()) { - return; - } - - const metaBuilding = this.currentMetaBuilding.get(); - if ((metaBuilding || this.currentlyDeleting) && this.lastDragTile) { - const oldPos = this.lastDragTile; - let newPos = this.root.camera.screenToWorld(pos).toTileSpace(); - - // Check if camera is moving, since then we do nothing - if (this.root.camera.desiredCenter) { - this.lastDragTile = newPos; - return; - } - - // Check for direction lock - if ( - metaBuilding && - metaBuilding.getHasDirectionLockAvailable() && - this.root.keyMapper.getBinding(KEYMAPPINGS.placement.lockBeltDirection).isCurrentlyPressed() - ) { - const vector = enumDirectionToVector[enumAngleToDirection[this.currentBaseRotation]]; - const delta = newPos.sub(oldPos); - delta.x *= Math_abs(vector.x); - delta.y *= Math_abs(vector.y); - newPos = oldPos.add(delta); - } - - // Check if anything changed - if (!oldPos.equals(newPos)) { - // Automatic Direction - if ( - metaBuilding && - metaBuilding.getRotateAutomaticallyWhilePlacing(this.currentVariant.get()) && - (!metaBuilding.getHasDirectionLockAvailable() || - !this.root.keyMapper - .getBinding(KEYMAPPINGS.placement.lockBeltDirection) - .isCurrentlyPressed()) && - !this.root.keyMapper - .getBinding(KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation) - .isCurrentlyPressed() - ) { - const delta = newPos.sub(oldPos); - const angleDeg = Math_degrees(delta.angle()); - this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360; - - // Holding alt inverts the placement - if ( - this.root.keyMapper - .getBinding(KEYMAPPINGS.placementModifiers.placeInverse) - .isCurrentlyPressed() - ) { - this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; - } - } - - // - Using bresenhams algorithmus - - let x0 = oldPos.x; - let y0 = oldPos.y; - let x1 = newPos.x; - let y1 = newPos.y; - - var dx = Math_abs(x1 - x0); - var dy = Math_abs(y1 - y0); - var sx = x0 < x1 ? 1 : -1; - var sy = y0 < y1 ? 1 : -1; - var err = dx - dy; - - while (this.currentlyDeleting || this.currentMetaBuilding.get()) { - if (this.currentlyDeleting) { - const contents = this.root.map.getTileContentXY(x0, y0); - if (contents && !contents.queuedForDestroy && !contents.destroyed) { - this.root.logic.tryDeleteBuilding(contents); - } - } else { - this.tryPlaceCurrentBuildingAt(new Vector(x0, y0)); - } - if (x0 === x1 && y0 === y1) break; - var e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x0 += sx; - } - if (e2 < dx) { - err += dx; - y0 += sy; - } - } - } - - this.lastDragTile = newPos; - - return STOP_PROPAGATION; - } - } - - update() { - // ALways update since the camera might have moved - const mousePos = this.root.app.mousePosition; - if (mousePos) { - this.onMouseMove(mousePos); - } - } - - /** - * aborts any dragging op - */ - abortDragging() { - this.currentlyDragging = true; - this.currentlyDeleting = false; - this.initialPlacementVector = null; - this.lastDragTile = null; - } - - /** - * - * @param {MetaBuilding} metaBuilding - */ - startSelection(metaBuilding) { - this.currentMetaBuilding.set(metaBuilding); - } - - /** - * @param {MetaBuilding} metaBuilding - */ - onSelectedMetaBuildingChanged(metaBuilding) { - this.abortDragging(); - this.root.hud.signals.selectedPlacementBuildingChanged.dispatch(metaBuilding); - if (metaBuilding) { - const variant = this.preferredVariants[metaBuilding.getId()] || defaultBuildingVariant; - this.currentVariant.set(variant); - - this.fakeEntity = new Entity(null); - metaBuilding.setupEntityComponents(this.fakeEntity, null); - - this.fakeEntity.addComponent( - new StaticMapEntityComponent({ - origin: new Vector(0, 0), - rotation: 0, - tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(), - blueprintSpriteKey: "", - }) - ); - metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); - } else { - this.fakeEntity = null; - } - - // Since it depends on both, rerender twice - this.rerenderVariants(); + this.variantsAttach = new DynamicDomAttach(this.root, this.variantsElement, {}); } /** @@ -390,151 +138,6 @@ export class HUDBuildingPlacer extends BaseHUDPart { } } - /** - * Cycles through the variants - */ - cycleVariants() { - const metaBuilding = this.currentMetaBuilding.get(); - if (!metaBuilding) { - this.currentVariant.set(defaultBuildingVariant); - } else { - const availableVariants = metaBuilding.getAvailableVariants(this.root); - const index = availableVariants.indexOf(this.currentVariant.get()); - assert( - index >= 0, - "Current variant was invalid: " + this.currentVariant.get() + " out of " + availableVariants - ); - const newIndex = (index + 1) % availableVariants.length; - const newVariant = availableVariants[newIndex]; - this.currentVariant.set(newVariant); - - this.preferredVariants[metaBuilding.getId()] = newVariant; - } - } - - /** - * Tries to rotate - */ - tryRotate() { - const selectedBuilding = this.currentMetaBuilding.get(); - if (selectedBuilding) { - if ( - this.root.keyMapper - .getBinding(KEYMAPPINGS.placement.rotateInverseModifier) - .isCurrentlyPressed() - ) { - this.currentBaseRotation = (this.currentBaseRotation + 270) % 360; - } else { - this.currentBaseRotation = (this.currentBaseRotation + 90) % 360; - } - - const staticComp = this.fakeEntity.components.StaticMapEntity; - staticComp.rotation = this.currentBaseRotation; - } - } - - /** - * Tries to delete the building under the mouse - */ - deleteBelowCursor() { - const mousePosition = this.root.app.mousePosition; - if (!mousePosition) { - // Not on screen - return; - } - - const worldPos = this.root.camera.screenToWorld(mousePosition); - const tile = worldPos.toTileSpace(); - const contents = this.root.map.getTileContent(tile); - if (contents) { - this.root.logic.tryDeleteBuilding(contents); - } - } - - /** - * Canvas click handler - * @param {Vector} mousePos - * @param {boolean} cancelAction - */ - onCanvasClick(mousePos, cancelAction = false) { - if (cancelAction) { - if (this.currentMetaBuilding.get()) { - this.currentMetaBuilding.set(null); - } else { - this.deleteBelowCursor(); - } - return STOP_PROPAGATION; - } - - if (!this.currentMetaBuilding.get()) { - return; - } - - return STOP_PROPAGATION; - } - - /** - * Tries to place the current building at the given tile - * @param {Vector} tile - */ - tryPlaceCurrentBuildingAt(tile) { - if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { - // Dont allow placing in overview mode - return; - } - // Transform to world space - - const metaBuilding = this.currentMetaBuilding.get(); - - const { rotation, rotationVariant } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile( - this.root, - tile, - this.currentBaseRotation, - this.currentVariant.get() - ); - - if ( - this.root.logic.tryPlaceBuilding({ - origin: tile, - rotation, - rotationVariant, - originalRotation: this.currentBaseRotation, - building: this.currentMetaBuilding.get(), - variant: this.currentVariant.get(), - }) - ) { - // Succesfully placed - - const entity = this.root.map.getTileContent(tile); - assert(entity, "Entity was not actually placed"); - this.root.signals.entityManuallyPlaced.dispatch(entity); - - if ( - metaBuilding.getFlipOrientationAfterPlacement() && - !this.root.keyMapper - .getBinding(KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation) - .isCurrentlyPressed() - ) { - this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; - } - - if ( - !metaBuilding.getStayInPlacementMode() && - !this.root.keyMapper - .getBinding(KEYMAPPINGS.placementModifiers.placeMultiple) - .isCurrentlyPressed() && - !this.root.app.settings.getAllSettings().alwaysMultiplace - ) { - // Stop placement - this.currentMetaBuilding.set(null); - } - - return true; - } else { - return false; - } - } - /** * * @param {DrawParameters} parameters @@ -555,14 +158,28 @@ export class HUDBuildingPlacer extends BaseHUDPart { return; } + // Draw direction lock + if (this.isDirectionLockActive) { + this.drawDirectionLock(parameters); + } else { + this.drawRegularPlacement(parameters); + } + } + + /** + * @param {DrawParameters} parameters + */ + drawRegularPlacement(parameters) { const mousePosition = this.root.app.mousePosition; if (!mousePosition) { // Not on screen return; } + const metaBuilding = this.currentMetaBuilding.get(); + const worldPos = this.root.camera.screenToWorld(mousePosition); - const tile = worldPos.toTileSpace(); + const mouseTile = worldPos.toTileSpace(); // Compute best rotation variant const { @@ -571,7 +188,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { connectedEntities, } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile( this.root, - tile, + mouseTile, this.currentBaseRotation, this.currentVariant.get() ); @@ -584,7 +201,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { .getCenter() .toWorldSpace(); - const startWsPoint = tile.toWorldSpaceCenterOfTile(); + const startWsPoint = mouseTile.toWorldSpaceCenterOfTile(); const startOffset = connectedWsPoint .sub(startWsPoint) @@ -609,14 +226,14 @@ export class HUDBuildingPlacer extends BaseHUDPart { // Synchronize rotation and origin const staticComp = this.fakeEntity.components.StaticMapEntity; - staticComp.origin = tile; + staticComp.origin = mouseTile; staticComp.rotation = rotation; staticComp.tileSize = metaBuilding.getDimensions(this.currentVariant.get()); metaBuilding.updateVariants(this.fakeEntity, rotationVariant, this.currentVariant.get()); // Check if we could place the buildnig const canBuild = this.root.logic.checkCanPlaceBuilding({ - origin: tile, + origin: mouseTile, rotation, rotationVariant, building: metaBuilding, @@ -653,45 +270,53 @@ export class HUDBuildingPlacer extends BaseHUDPart { const previewSprite = metaBuilding.getBlueprintSprite(rotationVariant, this.currentVariant.get()); staticComp.origin = worldPos.divideScalar(globalConfig.tileSize).subScalars(0.5, 0.5); staticComp.drawSpriteOnFullEntityBounds(parameters, previewSprite); - staticComp.origin = tile; + staticComp.origin = mouseTile; // Draw ejectors if (canBuild) { this.drawMatchingAcceptorsAndEjectors(parameters); } + } - // Draw direction lock + /** + * @param {DrawParameters} parameters + */ + drawDirectionLock(parameters) { + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return; + } - if ( - metaBuilding.getHasDirectionLockAvailable() && - this.root.keyMapper.getBinding(KEYMAPPINGS.placement.lockBeltDirection).isCurrentlyPressed() - ) { - if (this.lastDragTile) { - parameters.context.fillStyle = THEME.map.selectionBackground; - parameters.context.strokeStyle = THEME.map.selectionOverlay; - parameters.context.lineWidth = 3; + const mouseWorld = this.root.camera.screenToWorld(mousePosition); + const mouseTile = mouseWorld.toTileSpace(); + parameters.context.fillStyle = THEME.map.directionLock; + parameters.context.strokeStyle = THEME.map.directionLock; + parameters.context.lineWidth = 3; - const startLine = this.lastDragTile.toWorldSpaceCenterOfTile(); - const endLine = tile.toWorldSpaceCenterOfTile(); + parameters.context.beginCircle(mouseWorld.x, mouseWorld.y, 4); + parameters.context.fill(); - parameters.context.beginCircle(startLine.x, startLine.y, 7); - parameters.context.fill(); - parameters.context.stroke(); + if (this.lastDragTile) { + const startLine = this.lastDragTile.toWorldSpaceCenterOfTile(); + const endLine = mouseTile.toWorldSpaceCenterOfTile(); + const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile(); - parameters.context.beginPath(); - parameters.context.moveTo(startLine.x, startLine.y); - parameters.context.lineTo(endLine.x, endLine.y); - parameters.context.stroke(); + parameters.context.beginCircle(startLine.x, startLine.y, 7); + parameters.context.fill(); - parameters.context.beginCircle(endLine.x, endLine.y, 4); - parameters.context.fill(); - parameters.context.stroke(); - } + parameters.context.beginPath(); + parameters.context.moveTo(startLine.x, startLine.y); + parameters.context.lineTo(midLine.x, midLine.y); + parameters.context.lineTo(endLine.x, endLine.y); + parameters.context.stroke(); + + parameters.context.beginCircle(endLine.x, endLine.y, 4); + parameters.context.fill(); } } /** - * * @param {DrawParameters} parameters */ drawMatchingAcceptorsAndEjectors(parameters) { diff --git a/src/js/game/hud/parts/building_placer_logic.js b/src/js/game/hud/parts/building_placer_logic.js new file mode 100644 index 00000000..8131bf68 --- /dev/null +++ b/src/js/game/hud/parts/building_placer_logic.js @@ -0,0 +1,523 @@ +import { Math_abs, Math_degrees, Math_round } from "../../../core/builtins"; +import { globalConfig } from "../../../core/config"; +import { Signal, STOP_PROPAGATION } from "../../../core/signal"; +import { TrackedState } from "../../../core/tracked_state"; +import { findCornerBetweenPoints } from "../../../core/utils"; +import { Vector } from "../../../core/vector"; +import { enumMouseButton } from "../../camera"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { Entity } from "../../entity"; +import { KEYMAPPINGS } from "../../key_action_mapper"; +import { defaultBuildingVariant, MetaBuilding } from "../../meta_building"; +import { BaseHUDPart } from "../base_hud_part"; + +/** + * Contains all logic for the building placer - this doesn't include the rendering + * of info boxes or drawing. + */ +export class HUDBuildingPlacerLogic extends BaseHUDPart { + /** + * Initializes the logic + * @see BaseHUDPart.initialize + */ + initialize() { + /** + * We use a fake entity to get information about how a building will look + * once placed + * @type {Entity} + */ + this.fakeEntity = null; + + // Signals + this.signals = { + variantChanged: new Signal(), + }; + + /** + * The current building + * @type {TypedTrackedState} + */ + this.currentMetaBuilding = new TrackedState(this.onSelectedMetaBuildingChanged, this); + + /** + * The current rotation + * @type {number} + */ + this.currentBaseRotation = 0; + + /** + * Whether we are currently dragging + * @type {boolean} + */ + this.currentlyDragging = false; + + /** + * Current building variant + * @type {TypedTrackedState} + */ + this.currentVariant = new TrackedState(() => this.signals.variantChanged.dispatch()); + + /** + * Whether we are currently drag-deleting + * @type {boolean} + */ + this.currentlyDeleting = false; + + /** + * Stores which variants for each building we prefer, this is based on what + * the user last selected + * @type {Object.} + */ + this.preferredVariants = {}; + + /** + * The tile we last dragged from + * @type {Vector} + */ + this.lastDragTile = null; + + this.initializeBindings(); + } + + /** + * Initializes all bindings + */ + initializeBindings() { + // KEYBINDINGS + const keyActionMapper = this.root.keyMapper; + keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this); + keyActionMapper + .getBinding(KEYMAPPINGS.placement.abortBuildingPlacement) + .add(this.abortPlacement, this); + keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); + + // BINDINGS TO GAME EVENTS + this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this); + this.root.hud.signals.pasteBlueprintRequested.add(this.abortPlacement, this); + this.root.signals.storyGoalCompleted.add(() => this.signals.variantChanged.dispatch()); + this.root.signals.upgradePurchased.add(() => this.signals.variantChanged.dispatch()); + + // MOUSE BINDINGS + this.root.camera.downPreHandler.add(this.onMouseDown, this); + this.root.camera.movePreHandler.add(this.onMouseMove, this); + this.root.camera.upPostHandler.add(this.onMouseUp, this); + } + + /** + * Returns if the direction lock is currently active + * @returns {boolean} + */ + get isDirectionLockActive() { + const metaBuilding = this.currentMetaBuilding.get(); + return ( + metaBuilding && + metaBuilding.getHasDirectionLockAvailable() && + this.root.keyMapper.getBinding(KEYMAPPINGS.placement.lockBeltDirection).pressed + ); + } + + /** + * Returns the current direction lock corner, that is, the corner between + * mouse and original start point + * @returns {Vector|null} + */ + get currentDirectionLockCorner() { + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return null; + } + + if (!this.lastDragTile) { + // Haven't dragged yet + return null; + } + + // Figure which points the line visits + const worldPos = this.root.camera.screenToWorld(mousePosition); + const mouseTile = worldPos.toTileSpace(); + const cornerTile = findCornerBetweenPoints(this.lastDragTile, mouseTile); + return cornerTile; + } + + /** + * Aborts the placement + */ + abortPlacement() { + if (this.currentMetaBuilding.get()) { + this.currentMetaBuilding.set(null); + return STOP_PROPAGATION; + } + } + + /** + * Aborts any dragging + */ + abortDragging() { + this.currentlyDragging = true; + this.currentlyDeleting = false; + this.initialPlacementVector = null; + this.lastDragTile = null; + } + + /** + * @see BaseHUDPart.update + */ + update() { + // Always update since the camera might have moved + const mousePos = this.root.app.mousePosition; + if (mousePos) { + this.onMouseMove(mousePos); + } + } + + /** + * Tries to rotate the current building + */ + tryRotate() { + const selectedBuilding = this.currentMetaBuilding.get(); + if (selectedBuilding) { + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { + this.currentBaseRotation = (this.currentBaseRotation + 270) % 360; + } else { + this.currentBaseRotation = (this.currentBaseRotation + 90) % 360; + } + const staticComp = this.fakeEntity.components.StaticMapEntity; + staticComp.rotation = this.currentBaseRotation; + } + } + /** + * Tries to delete the building under the mouse + */ + deleteBelowCursor() { + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return; + } + + const worldPos = this.root.camera.screenToWorld(mousePosition); + const tile = worldPos.toTileSpace(); + const contents = this.root.map.getTileContent(tile); + if (contents) { + this.root.logic.tryDeleteBuilding(contents); + } + } + + /** + * Canvas click handler + * @param {Vector} mousePos + * @param {boolean} cancelAction + */ + onCanvasClick(mousePos, cancelAction = false) { + if (cancelAction) { + if (this.currentMetaBuilding.get()) { + this.currentMetaBuilding.set(null); + } else { + this.deleteBelowCursor(); + } + return STOP_PROPAGATION; + } + + if (!this.currentMetaBuilding.get()) { + return; + } + + return STOP_PROPAGATION; + } + + /** + * Tries to place the current building at the given tile + * @param {Vector} tile + */ + tryPlaceCurrentBuildingAt(tile) { + if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { + // Dont allow placing in overview mode + return; + } + + const metaBuilding = this.currentMetaBuilding.get(); + const { rotation, rotationVariant } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile( + this.root, + tile, + this.currentBaseRotation, + this.currentVariant.get() + ); + + const entity = this.root.logic.tryPlaceBuilding({ + origin: tile, + rotation, + rotationVariant, + originalRotation: this.currentBaseRotation, + building: this.currentMetaBuilding.get(), + variant: this.currentVariant.get(), + }); + + if (entity) { + // Succesfully placed, find which entity we actually placed + this.root.signals.entityManuallyPlaced.dispatch(entity); + + // Check if we should flip the orientation (used for tunnels) + if ( + metaBuilding.getFlipOrientationAfterPlacement() && + !this.root.keyMapper.getBinding( + KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation + ).pressed + ) { + this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; + } + + // Check if we should stop placement + if ( + !metaBuilding.getStayInPlacementMode() && + !this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeMultiple).pressed && + !this.root.app.settings.getAllSettings().alwaysMultiplace + ) { + // Stop placement + this.currentMetaBuilding.set(null); + } + return true; + } else { + return false; + } + } + + /** + * Cycles through the variants + */ + cycleVariants() { + const metaBuilding = this.currentMetaBuilding.get(); + if (!metaBuilding) { + this.currentVariant.set(defaultBuildingVariant); + } else { + const availableVariants = metaBuilding.getAvailableVariants(this.root); + const index = availableVariants.indexOf(this.currentVariant.get()); + assert( + index >= 0, + "Current variant was invalid: " + this.currentVariant.get() + " out of " + availableVariants + ); + const newIndex = (index + 1) % availableVariants.length; + const newVariant = availableVariants[newIndex]; + this.currentVariant.set(newVariant); + + this.preferredVariants[metaBuilding.getId()] = newVariant; + } + } + + /** + * Performs the direction locked placement between two points after + * releasing the mouse + */ + executeDirectionLockedPlacement() { + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return; + } + + // Figure which points the line visits + const worldPos = this.root.camera.screenToWorld(mousePosition); + const mouseTile = worldPos.toTileSpace(); + const startTile = this.lastDragTile; + + // Place from start to corner + const pathToCorner = this.currentDirectionLockCorner.sub(startTile); + const deltaToCorner = pathToCorner.normalize().round(); + const lengthToCorner = Math_round(pathToCorner.length()); + let currentPos = startTile.copy(); + + this.currentBaseRotation = (Math.round(Math_degrees(deltaToCorner.angle()) / 90) * 90 + 360) % 360; + + for (let i = 0; i < lengthToCorner; ++i) { + this.tryPlaceCurrentBuildingAt(currentPos); + currentPos.addInplace(deltaToCorner); + } + + // Place from corner to end + const pathFromCorner = mouseTile.sub(this.currentDirectionLockCorner); + const deltaFromCorner = pathFromCorner.normalize().round(); + const lengthFromCorner = Math_round(pathFromCorner.length()); + this.currentBaseRotation = (Math.round(Math_degrees(deltaFromCorner.angle()) / 90) * 90 + 360) % 360; + + for (let i = 0; i < lengthFromCorner + 1; ++i) { + this.tryPlaceCurrentBuildingAt(currentPos); + currentPos.addInplace(deltaFromCorner); + } + } + + /** + * Selects a given building + * @param {MetaBuilding} metaBuilding + */ + startSelection(metaBuilding) { + this.currentMetaBuilding.set(metaBuilding); + } + + /** + * Called when the selected buildings changed + * @param {MetaBuilding} metaBuilding + */ + onSelectedMetaBuildingChanged(metaBuilding) { + this.abortDragging(); + this.root.hud.signals.selectedPlacementBuildingChanged.dispatch(metaBuilding); + if (metaBuilding) { + const variant = this.preferredVariants[metaBuilding.getId()] || defaultBuildingVariant; + this.currentVariant.set(variant); + + this.fakeEntity = new Entity(null); + metaBuilding.setupEntityComponents(this.fakeEntity, null); + + this.fakeEntity.addComponent( + new StaticMapEntityComponent({ + origin: new Vector(0, 0), + rotation: 0, + tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(), + blueprintSpriteKey: "", + }) + ); + metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); + } else { + this.fakeEntity = null; + } + + // Since it depends on both, rerender twice + this.signals.variantChanged.dispatch(); + } + + /** + * mouse down pre handler + * @param {Vector} pos + * @param {enumMouseButton} button + */ + onMouseDown(pos, button) { + if (this.root.camera.getIsMapOverlayActive()) { + // We do not allow dragging if the overlay is active + return; + } + + const metaBuilding = this.currentMetaBuilding.get(); + + // Placement + if (button === enumMouseButton.left && metaBuilding) { + this.currentlyDragging = true; + this.currentlyDeleting = false; + this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); + + // Place initial building, but only if direction lock is not active + if (!this.isDirectionLockActive) { + this.tryPlaceCurrentBuildingAt(this.lastDragTile); + } + return STOP_PROPAGATION; + } + + // Deletion + if (button === enumMouseButton.right && !this.currentMetaBuilding.get()) { + this.currentlyDragging = true; + this.currentlyDeleting = true; + this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); + this.currentMetaBuilding.set(null); + return STOP_PROPAGATION; + } + } + + /** + * mouse move pre handler + * @param {Vector} pos + */ + onMouseMove(pos) { + if (this.root.camera.getIsMapOverlayActive()) { + return; + } + + // Check for direction lock + if (this.isDirectionLockActive) { + return; + } + + const metaBuilding = this.currentMetaBuilding.get(); + if ((metaBuilding || this.currentlyDeleting) && this.lastDragTile) { + const oldPos = this.lastDragTile; + let newPos = this.root.camera.screenToWorld(pos).toTileSpace(); + + // Check if camera is moving, since then we do nothing + if (this.root.camera.desiredCenter) { + this.lastDragTile = newPos; + return; + } + + // Check if anything changed + if (!oldPos.equals(newPos)) { + // Automatic Direction + if ( + metaBuilding && + metaBuilding.getRotateAutomaticallyWhilePlacing(this.currentVariant.get()) && + !this.root.keyMapper.getBinding( + KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation + ).pressed + ) { + const delta = newPos.sub(oldPos); + const angleDeg = Math_degrees(delta.angle()); + this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360; + + // Holding alt inverts the placement + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse).pressed) { + this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; + } + } + + // bresenham + let x0 = oldPos.x; + let y0 = oldPos.y; + let x1 = newPos.x; + let y1 = newPos.y; + + var dx = Math_abs(x1 - x0); + var dy = Math_abs(y1 - y0); + var sx = x0 < x1 ? 1 : -1; + var sy = y0 < y1 ? 1 : -1; + var err = dx - dy; + + while (this.currentlyDeleting || this.currentMetaBuilding.get()) { + if (this.currentlyDeleting) { + const contents = this.root.map.getTileContentXY(x0, y0); + if (contents && !contents.queuedForDestroy && !contents.destroyed) { + this.root.logic.tryDeleteBuilding(contents); + } + } else { + this.tryPlaceCurrentBuildingAt(new Vector(x0, y0)); + } + if (x0 === x1 && y0 === y1) break; + var e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + this.lastDragTile = newPos; + return STOP_PROPAGATION; + } + } + + /** + * Mouse up handler + */ + onMouseUp() { + if (this.root.camera.getIsMapOverlayActive()) { + return; + } + + // Check for direction lock + if (this.lastDragTile && this.currentlyDragging && this.isDirectionLockActive) { + this.executeDirectionLockedPlacement(); + } + + this.abortDragging(); + } +} diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index b89108c0..0dc872c5 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -177,7 +177,7 @@ export class HUDMassSelector extends BaseHUDPart { * @param {enumMouseButton} mouseButton */ onMouseDown(pos, mouseButton) { - if (!this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectStart).isCurrentlyPressed()) { + if (!this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectStart).pressed) { return; } @@ -185,11 +185,7 @@ export class HUDMassSelector extends BaseHUDPart { return; } - if ( - !this.root.keyMapper - .getBinding(KEYMAPPINGS.massSelect.massSelectSelectMultiple) - .isCurrentlyPressed() - ) { + if (!this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectSelectMultiple).pressed) { // Start new selection this.selectedUids = new Set(); } diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index da3db66d..7bdddd18 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -261,14 +261,16 @@ export class Keybinding { /** * Returns whether this binding is currently pressed + * @returns {boolean} */ - isCurrentlyPressed() { + get pressed() { // Check if the key is down if (this.app.inputMgr.keysDown.has(this.keyCode)) { // Check if it is the top reciever const reciever = this.keyMapper.inputReceiver; return this.app.inputMgr.getTopReciever() === reciever; } + return false; } /** diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 90597e52..408bf89d 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -151,6 +151,7 @@ export class GameLogic { * @param {number} param0.rotationVariant * @param {string} param0.variant * @param {MetaBuilding} param0.building + * @returns {Entity} */ tryPlaceBuilding({ origin, rotation, rotationVariant, originalRotation, variant, building }) { if (this.checkCanPlaceBuilding({ origin, rotation, rotationVariant, variant, building })) { @@ -170,13 +171,13 @@ export class GameLogic { if (contents) { if (!this.tryDeleteBuilding(contents)) { logger.error("Building has replaceable component but is also unremovable"); - return false; + return null; } } } } - building.createAndPlaceEntity({ + const entity = building.createAndPlaceEntity({ root: this.root, origin, rotation, @@ -186,10 +187,9 @@ export class GameLogic { }); this.root.soundProxy.playUi(building.getPlacementSound()); - - return true; + return entity; } - return false; + return null; } /** diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index 760ab510..78e21c1a 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -9,6 +9,8 @@ "selectionOutline": "rgba(74, 163, 223, 0.5)", "selectionBackground": "rgba(74, 163, 223, 0.2)", + "directionLock": "rgb(74, 237, 134)", + "resources": { "shape": "#3d3f4a", "red": "#4a3d3f", diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index 59e9e58f..1134ccb0 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -9,6 +9,8 @@ "selectionOutline": "rgba(74, 163, 223, 0.5)", "selectionBackground": "rgba(74, 163, 223, 0.2)", + "directionLock": "rgb(74, 237, 134)", + "resources": { "shape": "#eaebec", "red": "#ffbfc1",