mirror of
https://github.com/tobspr/shapez.io.git
synced 2024-10-27 20:34:29 +00:00
831 lines
28 KiB
JavaScript
831 lines
28 KiB
JavaScript
import { globalConfig } from "../../../core/config";
|
|
import { gMetaBuildingRegistry } from "../../../core/global_registries";
|
|
import { Signal, STOP_PROPAGATION } from "../../../core/signal";
|
|
import { TrackedState } from "../../../core/tracked_state";
|
|
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";
|
|
import { SOUNDS } from "../../../platform/sound";
|
|
import { MetaMinerBuilding, enumMinerVariants } from "../../buildings/miner";
|
|
import { enumHubGoalRewards } from "../../tutorial_goals";
|
|
import { enumLayer } from "../../root";
|
|
|
|
/**
|
|
* 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(),
|
|
draggingStarted: new Signal(),
|
|
};
|
|
|
|
/**
|
|
* The current building
|
|
* @type {TypedTrackedState<MetaBuilding?>}
|
|
*/
|
|
this.currentMetaBuilding = new TrackedState(this.onSelectedMetaBuildingChanged, this);
|
|
|
|
/**
|
|
* The current rotation
|
|
* @type {number}
|
|
*/
|
|
this.currentBaseRotationGeneral = 0;
|
|
|
|
/**
|
|
* The current rotation preference for each building.
|
|
* @type{Object.<string,number>}
|
|
*/
|
|
this.preferredBaseRotations = {};
|
|
|
|
/**
|
|
* Whether we are currently dragging
|
|
* @type {boolean}
|
|
*/
|
|
this.currentlyDragging = false;
|
|
|
|
/**
|
|
* Current building variant
|
|
* @type {TypedTrackedState<string>}
|
|
*/
|
|
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.<string, string>}
|
|
*/
|
|
this.preferredVariants = {};
|
|
|
|
/**
|
|
* The tile we last dragged from
|
|
* @type {Vector}
|
|
*/
|
|
this.lastDragTile = null;
|
|
|
|
/**
|
|
* The side for direction lock
|
|
* @type {number} (0|1)
|
|
*/
|
|
this.currentDirectionLockSide = 0;
|
|
|
|
/**
|
|
* Whether the side for direction lock has not yet been determined.
|
|
* @type {boolean}
|
|
*/
|
|
this.currentDirectionLockSideIndeterminate = true;
|
|
|
|
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.switchDirectionLockSide)
|
|
.add(this.switchDirectionLockSide, this);
|
|
keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this);
|
|
keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.startPipette, this);
|
|
this.root.gameState.inputReciever.keyup.add(this.checkForDirectionLockSwitch, 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());
|
|
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* Called when the edit mode got changed
|
|
* @param {enumLayer} editMode
|
|
*/
|
|
onEditModeChanged(editMode) {
|
|
const metaBuilding = this.currentMetaBuilding.get();
|
|
if (metaBuilding) {
|
|
if (metaBuilding.getLayer() !== editMode) {
|
|
// This layer doesn't fit the edit mode anymore
|
|
this.currentMetaBuilding.set(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the current base rotation for the current meta-building.
|
|
* @returns {number}
|
|
*/
|
|
get currentBaseRotation() {
|
|
if (!this.root.app.settings.getAllSettings().rotationByBuilding) {
|
|
return this.currentBaseRotationGeneral;
|
|
}
|
|
const metaBuilding = this.currentMetaBuilding.get();
|
|
if (metaBuilding && this.preferredBaseRotations.hasOwnProperty(metaBuilding.getId())) {
|
|
return this.preferredBaseRotations[metaBuilding.getId()];
|
|
} else {
|
|
return this.currentBaseRotationGeneral;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the base rotation for the current meta-building.
|
|
* @param {number} rotation The new rotation/angle.
|
|
*/
|
|
set currentBaseRotation(rotation) {
|
|
if (!this.root.app.settings.getAllSettings().rotationByBuilding) {
|
|
this.currentBaseRotationGeneral = rotation;
|
|
} else {
|
|
const metaBuilding = this.currentMetaBuilding.get();
|
|
if (metaBuilding) {
|
|
this.preferredBaseRotations[metaBuilding.getId()] = rotation;
|
|
} else {
|
|
this.currentBaseRotationGeneral = rotation;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.placementModifiers.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();
|
|
|
|
// Figure initial direction
|
|
const dx = Math.abs(this.lastDragTile.x - mouseTile.x);
|
|
const dy = Math.abs(this.lastDragTile.y - mouseTile.y);
|
|
if (dx === 0 && dy === 0) {
|
|
// Back at the start. Try a new direction.
|
|
this.currentDirectionLockSideIndeterminate = true;
|
|
} else if (this.currentDirectionLockSideIndeterminate) {
|
|
this.currentDirectionLockSideIndeterminate = false;
|
|
this.currentDirectionLockSide = dx <= dy ? 0 : 1;
|
|
}
|
|
|
|
if (this.currentDirectionLockSide === 0) {
|
|
return new Vector(this.lastDragTile.x, mouseTile.y);
|
|
} else {
|
|
return new Vector(mouseTile.x, this.lastDragTile.y);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
// Make sure we have nothing selected while in overview mode
|
|
if (this.root.camera.getIsMapOverlayActive()) {
|
|
if (this.currentMetaBuilding.get()) {
|
|
this.currentMetaBuilding.set(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, this.root.currentLayer);
|
|
if (contents) {
|
|
if (this.root.logic.tryDeleteBuilding(contents)) {
|
|
this.root.soundProxy.playUi(SOUNDS.destroyBuilding);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the pipette function
|
|
*/
|
|
startPipette() {
|
|
// Disable in overview
|
|
if (this.root.camera.getIsMapOverlayActive()) {
|
|
return;
|
|
}
|
|
|
|
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, this.root.currentLayer);
|
|
if (!contents) {
|
|
const tileBelow = this.root.map.getLowerLayerContentXY(tile.x, tile.y);
|
|
|
|
// Check if there's a shape or color item below, if so select the miner
|
|
if (tileBelow) {
|
|
this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding));
|
|
|
|
// Select chained miner if available, since thats always desired once unlocked
|
|
if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) {
|
|
this.currentVariant.set(enumMinerVariants.chainable);
|
|
}
|
|
} else {
|
|
this.currentMetaBuilding.set(null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Try to extract the building
|
|
const extracted = this.hack_reconstructMetaBuildingAndVariantFromBuilding(contents);
|
|
|
|
// If the building we are picking is the same as the one we have, clear the cursor.
|
|
if (
|
|
!extracted ||
|
|
(extracted.metaBuilding === this.currentMetaBuilding.get() &&
|
|
extracted.variant === this.currentVariant.get())
|
|
) {
|
|
this.currentMetaBuilding.set(null);
|
|
return;
|
|
}
|
|
|
|
this.currentMetaBuilding.set(extracted.metaBuilding);
|
|
this.currentVariant.set(extracted.variant);
|
|
this.currentBaseRotation = contents.components.StaticMapEntity.rotation;
|
|
}
|
|
|
|
/**
|
|
* HACK!
|
|
*
|
|
* This attempts to reconstruct the meta building and its variant from a given entity
|
|
* @param {Entity} entity
|
|
* @returns {{ metaBuilding: MetaBuilding, variant: string }}
|
|
*/
|
|
hack_reconstructMetaBuildingAndVariantFromBuilding(entity) {
|
|
if (entity.components.Hub) {
|
|
// Hub is not copyable
|
|
return null;
|
|
}
|
|
|
|
const matches = [];
|
|
const metaBuildings = gMetaBuildingRegistry.entries;
|
|
for (let i = 0; i < metaBuildings.length; ++i) {
|
|
const metaBuilding = metaBuildings[i];
|
|
const availableVariants = metaBuilding.getAvailableVariants(this.root);
|
|
checkVariant: for (let k = 0; k < availableVariants.length; ++k) {
|
|
const variant = availableVariants[k];
|
|
let unplaced = metaBuilding.createEntity({
|
|
root: this.root,
|
|
variant,
|
|
origin: new Vector(0, 0),
|
|
rotation: 0,
|
|
originalRotation: 0,
|
|
rotationVariant: 0,
|
|
});
|
|
|
|
// Compare if both entities share the same components
|
|
for (let component in entity.components) {
|
|
if ((entity.components[component] == null) !== (unplaced.components[component] == null)) {
|
|
continue checkVariant;
|
|
}
|
|
}
|
|
|
|
// Check for same item processor
|
|
if (
|
|
entity.components.ItemProcessor &&
|
|
entity.components.ItemProcessor.type != unplaced.components.ItemProcessor.type
|
|
) {
|
|
continue checkVariant;
|
|
}
|
|
|
|
// Check for underground belt
|
|
if (
|
|
entity.components.UndergroundBelt &&
|
|
entity.components.UndergroundBelt.tier != unplaced.components.UndergroundBelt.tier
|
|
) {
|
|
continue checkVariant;
|
|
}
|
|
|
|
// Check for same sprite key - except for underground belts
|
|
// since the sprite may vary here
|
|
if (
|
|
!entity.components.UndergroundBelt &&
|
|
entity.components.StaticMapEntity.spriteKey !=
|
|
unplaced.components.StaticMapEntity.spriteKey
|
|
) {
|
|
continue checkVariant;
|
|
}
|
|
matches.push({ metaBuilding, variant });
|
|
}
|
|
}
|
|
|
|
if (matches.length == 1) {
|
|
const staticEntity = entity.components.StaticMapEntity;
|
|
const key = staticEntity.spriteKey || staticEntity.blueprintSpriteKey;
|
|
assert(
|
|
key &&
|
|
key.includes(matches[0].metaBuilding.id) &&
|
|
(matches[0].variant === defaultBuildingVariant || key.includes(matches[0].variant))
|
|
);
|
|
return matches[0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
switchDirectionLockSide() {
|
|
this.currentDirectionLockSide = 1 - this.currentDirectionLockSide;
|
|
}
|
|
|
|
/**
|
|
* Checks if the direction lock key got released and if such, resets the placement
|
|
* @param {any} args
|
|
*/
|
|
checkForDirectionLockSwitch({ keyCode }) {
|
|
if (
|
|
keyCode ===
|
|
this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.lockBeltDirection).keyCode
|
|
) {
|
|
this.abortDragging();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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({
|
|
root: this.root,
|
|
tile,
|
|
rotation: this.currentBaseRotation,
|
|
variant: this.currentVariant.get(),
|
|
layer: metaBuilding.getLayer(),
|
|
});
|
|
|
|
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 metaBuilding = this.currentMetaBuilding.get();
|
|
if (!metaBuilding) {
|
|
// No active building
|
|
return;
|
|
}
|
|
|
|
// Get path to place
|
|
const path = this.computeDirectionLockPath();
|
|
|
|
// Store if we placed anything
|
|
let anythingPlaced = false;
|
|
|
|
// Perform this in bulk to avoid recalculations
|
|
this.root.logic.performBulkOperation(() => {
|
|
for (let i = 0; i < path.length; ++i) {
|
|
const { rotation, tile } = path[i];
|
|
this.currentBaseRotation = rotation;
|
|
if (this.tryPlaceCurrentBuildingAt(tile)) {
|
|
anythingPlaced = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (anythingPlaced) {
|
|
this.root.soundProxy.playUi(metaBuilding.getPlacementSound());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the path which the current direction lock will use
|
|
* @returns {Array<{ tile: Vector, rotation: number }>}
|
|
*/
|
|
computeDirectionLockPath() {
|
|
const mousePosition = this.root.app.mousePosition;
|
|
if (!mousePosition) {
|
|
// Not on screen
|
|
return [];
|
|
}
|
|
|
|
let result = [];
|
|
|
|
// Figure which points the line visits
|
|
const worldPos = this.root.camera.screenToWorld(mousePosition);
|
|
let endTile = worldPos.toTileSpace();
|
|
let startTile = this.lastDragTile;
|
|
|
|
// if the alt key is pressed, reverse belt planner direction by switching start and end tile
|
|
if (this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse).pressed) {
|
|
let tmp = startTile;
|
|
startTile = endTile;
|
|
endTile = tmp;
|
|
}
|
|
|
|
// 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();
|
|
|
|
let rotation = (Math.round(Math.degrees(deltaToCorner.angle()) / 90) * 90 + 360) % 360;
|
|
|
|
if (lengthToCorner > 0) {
|
|
for (let i = 0; i < lengthToCorner; ++i) {
|
|
result.push({
|
|
tile: currentPos.copy(),
|
|
rotation,
|
|
});
|
|
currentPos.addInplace(deltaToCorner);
|
|
}
|
|
}
|
|
|
|
// Place from corner to end
|
|
const pathFromCorner = endTile.sub(this.currentDirectionLockCorner);
|
|
const deltaFromCorner = pathFromCorner.normalize().round();
|
|
const lengthFromCorner = Math.round(pathFromCorner.length());
|
|
|
|
if (lengthFromCorner > 0) {
|
|
rotation = (Math.round(Math.degrees(deltaFromCorner.angle()) / 90) * 90 + 360) % 360;
|
|
for (let i = 0; i < lengthFromCorner + 1; ++i) {
|
|
result.push({
|
|
tile: currentPos.copy(),
|
|
rotation,
|
|
});
|
|
currentPos.addInplace(deltaFromCorner);
|
|
}
|
|
} else {
|
|
// Finish last one
|
|
result.push({
|
|
tile: currentPos.copy(),
|
|
rotation,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
if (this.tryPlaceCurrentBuildingAt(this.lastDragTile)) {
|
|
this.root.soundProxy.playUi(metaBuilding.getPlacementSound());
|
|
}
|
|
}
|
|
return STOP_PROPAGATION;
|
|
}
|
|
|
|
// Deletion
|
|
if (button === enumMouseButton.right && !metaBuilding) {
|
|
this.currentlyDragging = true;
|
|
this.currentlyDeleting = true;
|
|
this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace();
|
|
this.deleteBelowCursor();
|
|
return STOP_PROPAGATION;
|
|
}
|
|
|
|
// Cancel placement
|
|
if (button === enumMouseButton.right && metaBuilding) {
|
|
this.currentMetaBuilding.set(null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
let anythingPlaced = false;
|
|
let anythingDeleted = false;
|
|
|
|
while (this.currentlyDeleting || this.currentMetaBuilding.get()) {
|
|
if (this.currentlyDeleting) {
|
|
// Deletion
|
|
const contents = this.root.map.getLayerContentXY(x0, y0, this.root.currentLayer);
|
|
if (contents && !contents.queuedForDestroy && !contents.destroyed) {
|
|
if (this.root.logic.tryDeleteBuilding(contents)) {
|
|
anythingDeleted = true;
|
|
}
|
|
}
|
|
} else {
|
|
// Placement
|
|
if (this.tryPlaceCurrentBuildingAt(new Vector(x0, y0))) {
|
|
anythingPlaced = true;
|
|
}
|
|
}
|
|
|
|
if (x0 === x1 && y0 === y1) break;
|
|
var e2 = 2 * err;
|
|
if (e2 > -dy) {
|
|
err -= dy;
|
|
x0 += sx;
|
|
}
|
|
if (e2 < dx) {
|
|
err += dx;
|
|
y0 += sy;
|
|
}
|
|
}
|
|
|
|
if (anythingPlaced) {
|
|
this.root.soundProxy.playUi(metaBuilding.getPlacementSound());
|
|
}
|
|
if (anythingDeleted) {
|
|
this.root.soundProxy.playUi(SOUNDS.destroyBuilding);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|