diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index fe6e5323..4e322eb0 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -90,17 +90,7 @@ export class Blueprint { const rect = staticComp.getTileSpaceBounds(); rect.moveBy(tile.x, tile.y); - let placeable = true; - placementCheck: for (let x = rect.x; x < rect.right(); ++x) { - for (let y = rect.y; y < rect.bottom(); ++y) { - if (parameters.root.map.isTileUsedXY(x, y, entity.layer)) { - placeable = false; - break placementCheck; - } - } - } - - if (!placeable) { + if (!parameters.root.logic.checkCanPlaceEntity(entity, tile)) { parameters.context.globalAlpha = 0.3; } else { parameters.context.globalAlpha = 1; @@ -150,21 +140,8 @@ export class Blueprint { let anyPlaceable = false; for (let i = 0; i < this.entities.length; ++i) { - let placeable = true; const entity = this.entities[i]; - const staticComp = entity.components.StaticMapEntity; - const rect = staticComp.getTileSpaceBounds(); - rect.moveBy(tile.x, tile.y); - placementCheck: for (let x = rect.x; x < rect.right(); ++x) { - for (let y = rect.y; y < rect.bottom(); ++y) { - if (root.map.isTileUsedXY(x, y, entity.layer)) { - placeable = false; - break placementCheck; - } - } - } - - if (placeable) { + if (root.logic.checkCanPlaceEntity(entity, tile)) { anyPlaceable = true; } } @@ -188,48 +165,17 @@ export class Blueprint { return root.logic.performBulkOperation(() => { let anyPlaced = false; for (let i = 0; i < this.entities.length; ++i) { - let placeable = true; const entity = this.entities[i]; - const staticComp = entity.components.StaticMapEntity; - const rect = staticComp.getTileSpaceBounds(); - rect.moveBy(tile.x, tile.y); - placementCheck: for (let x = rect.x; x < rect.right(); ++x) { - for (let y = rect.y; y < rect.bottom(); ++y) { - const contents = root.map.getLayerContentXY(x, y, entity.layer); - if (contents && !contents.components.ReplaceableMapEntity) { - placeable = false; - break placementCheck; - } - } + if (!root.logic.checkCanPlaceEntity(entity, tile)) { + continue; } - if (placeable) { - for (let x = rect.x; x < rect.right(); ++x) { - for (let y = rect.y; y < rect.bottom(); ++y) { - const contents = root.map.getLayerContentXY(x, y, entity.layer); - if (contents) { - assert( - contents.components.ReplaceableMapEntity, - "Can not delete entity for blueprint" - ); - if (!root.logic.tryDeleteBuilding(contents)) { - assertAlways( - false, - "Building has replaceable component but is also unremovable in blueprint" - ); - } - } - } - } - - const clone = entity.duplicateWithoutContents(); - clone.components.StaticMapEntity.origin.addInplace(tile); - - root.map.placeStaticEntity(clone); - - root.entityMgr.registerEntity(clone); - anyPlaced = true; - } + const clone = entity.duplicateWithoutContents(); + clone.components.StaticMapEntity.origin.addInplace(tile); + root.logic.freeEntityAreaBeforeBuild(clone); + root.map.placeStaticEntity(clone); + root.entityMgr.registerEntity(clone); + anyPlaced = true; } return anyPlaced; }); diff --git a/src/js/game/core.js b/src/js/game/core.js index 3c35beb5..b3f09c59 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -130,7 +130,8 @@ export class GameCore { this.root.gameIsFresh = true; this.root.map.seed = randomInt(0, 100000); - gMetaBuildingRegistry.findByClass(MetaHubBuilding).createAndPlaceEntity({ + // Place the hub + const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({ root: this.root, origin: new Vector(-2, -2), rotation: 0, @@ -138,6 +139,8 @@ export class GameCore { rotationVariant: 0, variant: defaultBuildingVariant, }); + this.root.map.placeStaticEntity(hub); + this.root.entityMgr.registerEntity(hub); } /** diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index ad630ab9..9dae6ba0 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -276,14 +276,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { 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: mouseTile, - rotation, - rotationVariant, - building: metaBuilding, - variant: this.currentVariant.get(), - }); + const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity); // Fade in / out parameters.context.lineWidth = 1; diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 075c27fe..403fa15b 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -1,12 +1,10 @@ -import { GameRoot, enumLayer, arrayLayers } from "./root"; -import { Entity } from "./entity"; -import { Vector, enumDirectionToVector, enumDirection } from "../core/vector"; -import { MetaBuilding } from "./meta_building"; -import { StaticMapEntityComponent } from "./components/static_map_entity"; import { createLogger } from "../core/logging"; -import { MetaBeltBaseBuilding, arrayBeltVariantToRotation } from "./buildings/belt_base"; -import { SOUNDS } from "../platform/sound"; import { round2Digits } from "../core/utils"; +import { enumDirection, enumDirectionToVector, Vector } from "../core/vector"; +import { Entity } from "./entity"; +import { MetaBuilding } from "./meta_building"; +import { enumLayer, GameRoot } from "./root"; +import { STOP_PROPAGATION } from "../core/signal"; const logger = createLogger("ingame/logic"); @@ -47,125 +45,41 @@ export class GameLogic { } /** - * @param {object} param0 - * @param {Vector} param0.origin - * @param {number} param0.rotation - * @param {number} param0.rotationVariant - * @param {string} param0.variant - * @param {MetaBuilding} param0.building - * @returns {boolean} + * Checks if the given entity can be placed + * @param {Entity} entity + * @param {Vector=} offset Optional, move the entity by the given offset first + * @returns {boolean} true if the entity could be placed there */ - isAreaFreeToBuild({ origin, rotation, rotationVariant, variant, building }) { - const checker = new StaticMapEntityComponent({ - origin, - tileSize: building.getDimensions(variant), - rotation, - blueprintSpriteKey: "", - }); - - const layer = building.getLayer(); - const rect = checker.getTileSpaceBounds(); + checkCanPlaceEntity(entity, offset = null) { + // Compute area of the building + const rect = entity.components.StaticMapEntity.getTileSpaceBounds(); + if (offset) { + rect.x += offset.x; + rect.y += offset.y; + } + // Check the whole area of the building for (let x = rect.x; x < rect.x + rect.w; ++x) { for (let y = rect.y; y < rect.y + rect.h; ++y) { - const contents = this.root.map.getLayerContentXY(x, y, layer); - if (contents) { - if ( - !this.checkCanReplaceBuilding({ - original: contents, - building, - rotation, - rotationVariant, - }) - ) { - // Content already has same rotation - return false; - } - } - - // Check for any pins which are in the way - if (layer === enumLayer.wires) { - const regularContents = this.root.map.getLayerContentXY(x, y, enumLayer.regular); - if (regularContents) { - const staticComp = regularContents.components.StaticMapEntity; - const pinComponent = regularContents.components.WiredPins; - if (pinComponent) { - const pins = pinComponent.slots; - for (let i = 0; i < pins.length; ++i) { - const pos = staticComp.localTileToWorld(pins[i].pos); - // Occupied by a pin - if (pos.x === x && pos.y === y) { - return false; - } - } - } - } + // Check if there is any direct collision + const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer); + if (otherEntity && !otherEntity.components.ReplaceableMapEntity) { + // This one is a direct blocker + return false; } } } - return true; - } - /** - * Checks if the given building can be replaced by another - * @param {object} param0 - * @param {Entity} param0.original - * @param {number} param0.rotation - * @param {number} param0.rotationVariant - * @param {MetaBuilding} param0.building - * @returns {boolean} - */ - checkCanReplaceBuilding({ original, building, rotation, rotationVariant }) { - if (!original.components.ReplaceableMapEntity) { - // Can not get replaced at all + // Perform additional placement checks + if (this.root.signals.prePlacementCheck.dispatch(entity, offset) === STOP_PROPAGATION) { return false; } - if (building.getLayer() !== original.layer) { - // Layer mismatch - return false; - } - - const staticComp = original.components.StaticMapEntity; - assert(staticComp, "Building is not static"); - const beltComp = original.components.Belt; - if (beltComp && building instanceof MetaBeltBaseBuilding) { - // Its a belt, check if it differs in either rotation or rotation variant - if (staticComp.rotation !== rotation) { - return true; - } - if (beltComp.direction !== arrayBeltVariantToRotation[rotationVariant]) { - return true; - } - } - return true; } /** - * @param {object} param0 - * @param {Vector} param0.origin - * @param {number} param0.rotation - * @param {number} param0.rotationVariant - * @param {string} param0.variant - * @param {MetaBuilding} param0.building - */ - checkCanPlaceBuilding({ origin, rotation, rotationVariant, variant, building }) { - if (!building.getIsUnlocked(this.root)) { - return false; - } - - return this.isAreaFreeToBuild({ - origin, - rotation, - rotationVariant, - variant, - building, - }); - } - - /** - * + * Attempts to place the given building * @param {object} param0 * @param {Vector} param0.origin * @param {number} param0.rotation @@ -176,44 +90,51 @@ export class GameLogic { * @returns {Entity} */ tryPlaceBuilding({ origin, rotation, rotationVariant, originalRotation, variant, building }) { - if (this.checkCanPlaceBuilding({ origin, rotation, rotationVariant, variant, building })) { - // Remove any removeable entities below - const checker = new StaticMapEntityComponent({ - origin, - tileSize: building.getDimensions(variant), - rotation, - blueprintSpriteKey: "", - }); - - const layer = building.getLayer(); - - const rect = checker.getTileSpaceBounds(); - for (let x = rect.x; x < rect.x + rect.w; ++x) { - for (let y = rect.y; y < rect.y + rect.h; ++y) { - const contents = this.root.map.getLayerContentXY(x, y, layer); - if (contents) { - if (!this.tryDeleteBuilding(contents)) { - logger.error("Building has replaceable component but is also unremovable"); - return null; - } - } - } - } - - const entity = building.createAndPlaceEntity({ - root: this.root, - origin, - rotation, - rotationVariant, - originalRotation, - variant, - }); - + const entity = building.createEntity({ + root: this.root, + origin, + rotation, + originalRotation, + rotationVariant, + variant, + }); + if (this.checkCanPlaceEntity(entity)) { + this.freeEntityAreaBeforeBuild(entity); + this.root.map.placeStaticEntity(entity); + this.root.entityMgr.registerEntity(entity); return entity; } return null; } + /** + * Removes all entities with a RemovableMapEntityComponent which need to get + * removed before placing this entity + * @param {Entity} entity + */ + freeEntityAreaBeforeBuild(entity) { + const staticComp = entity.components.StaticMapEntity; + const rect = staticComp.getTileSpaceBounds(); + // Remove any removeable colliding entities on the same layer + for (let x = rect.x; x < rect.x + rect.w; ++x) { + for (let y = rect.y; y < rect.y + rect.h; ++y) { + const contents = this.root.map.getLayerContentXY(x, y, entity.layer); + if (contents) { + assertAlways( + contents.components.ReplaceableMapEntity, + "Tried to replace non-repleaceable entity" + ); + if (!this.tryDeleteBuilding(contents)) { + assertAlways(false, "Tried to replace non-repleaceable entity #2"); + } + } + } + } + + // Perform other callbacks + this.root.signals.freeEntityAreaBeforeBuild.dispatch(entity); + } + /** * Performs a bulk operation, not updating caches in the meantime * @param {function} operation @@ -266,6 +187,7 @@ export class GameLogic { /** @type {AcceptorsAffectingTile} */ let acceptors = []; + // Well .. please ignore this code! :D for (let dx = -1; dx <= 1; ++dx) { for (let dy = -1; dy <= 1; ++dy) { if (Math.abs(dx) + Math.abs(dy) !== 1) { diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js index 15919d02..514474c9 100644 --- a/src/js/game/meta_building.js +++ b/src/js/game/meta_building.js @@ -144,30 +144,6 @@ export class MetaBuilding { return null; } - /** - * Creates the entity at the given location - * @param {object} param0 - * @param {GameRoot} param0.root - * @param {Vector} param0.origin Origin tile - * @param {number=} param0.rotation Rotation - * @param {number} param0.originalRotation Original Rotation - * @param {number} param0.rotationVariant Rotation variant - * @param {string} param0.variant - */ - createAndPlaceEntity({ root, origin, rotation, originalRotation, rotationVariant, variant }) { - const entity = this.createEntity({ - root, - origin, - rotation, - originalRotation, - rotationVariant, - variant, - }); - root.map.placeStaticEntity(entity); - root.entityMgr.registerEntity(entity); - return entity; - } - /** * Creates the entity without placing it * @param {object} param0 diff --git a/src/js/game/root.js b/src/js/game/root.js index 50c87072..7bd42b4d 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -27,6 +27,7 @@ import { ShapeDefinition } from "./shape_definition"; import { BaseItem } from "./base_item"; import { DynamicTickrate } from "./dynamic_tickrate"; import { KeyActionMapper } from "./key_action_mapper"; +import { Vector } from "../core/vector"; /* typehints:end */ const logger = createLogger("game/root"); @@ -169,6 +170,14 @@ export class GameRoot { bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), editModeChanged: /** @type {TypedSignal<[enumLayer]>} */ (new Signal()), + + // Called to check if an entity can be placed, second parameter is an additional offset. + // Use to introduce additional placement checks + prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()), + + // Called before actually placing an entity, use to perform additional logic + // for freeing space before actually placing. + freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()), }; // RNG's diff --git a/src/js/game/systems/wired_pins.js b/src/js/game/systems/wired_pins.js index 4161e4f3..54651aba 100644 --- a/src/js/game/systems/wired_pins.js +++ b/src/js/game/systems/wired_pins.js @@ -1,10 +1,11 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; -import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins"; +import { Loader } from "../../core/loader"; +import { Vector } from "../../core/vector"; +import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; -import { MapChunkView } from "../map_chunk_view"; -import { Loader } from "../../core/loader"; +import { enumLayer } from "../root"; export class WiredPinsSystem extends GameSystemWithFilter { constructor(root) { @@ -20,6 +21,127 @@ export class WiredPinsSystem extends GameSystemWithFilter { "sprites/wires/pin_negative_accept.png" ), }; + + this.root.signals.prePlacementCheck.add(this.prePlacementCheck, this); + this.root.signals.freeEntityAreaBeforeBuild.add(this.freeEntityAreaBeforeBuild, this); + } + + /** + * Performs pre-placement checks + * @param {Entity} entity + * @param {Vector} offset + */ + prePlacementCheck(entity, offset) { + // Compute area of the building + const rect = entity.components.StaticMapEntity.getTileSpaceBounds(); + if (offset) { + rect.x += offset.x; + rect.y += offset.y; + } + + // If this entity is placed on the wires layer, make sure we don't + // place it above a pin + if (entity.layer === enumLayer.wires) { + for (let x = rect.x; x < rect.x + rect.w; ++x) { + for (let y = rect.y; y < rect.y + rect.h; ++y) { + // Find which entities are in same tiles of both layers + const entities = this.root.map.getLayersContentsMultipleXY(x, y); + for (let i = 0; i < entities.length; ++i) { + const otherEntity = entities[i]; + + // Check if entity has a wired component + const pinComponent = otherEntity.components.WiredPins; + const staticComp = otherEntity.components.StaticMapEntity; + if (!pinComponent) { + continue; + } + + if (otherEntity.components.ReplaceableMapEntity) { + // Don't mind here, even if there would be a collision we + // could replace it + continue; + } + + // Go over all pins and check if they are blocking + const pins = pinComponent.slots; + for (let pinSlot = 0; pinSlot < pins.length; ++pinSlot) { + const pos = staticComp.localTileToWorld(pins[pinSlot].pos); + // Occupied by a pin + if (pos.x === x && pos.y === y) { + return STOP_PROPAGATION; + } + } + } + } + } + } + + // Check for collisions on the wires layer + if (this.checkEntityPinsCollide(entity, offset)) { + return STOP_PROPAGATION; + } + } + + /** + * Checks if the pins of the given entity collide on the wires layer + * @param {Entity} entity + * @param {Vector=} offset Optional, move the entity by the given offset first + * @returns {boolean} True if the pins collide + */ + checkEntityPinsCollide(entity, offset) { + const pinsComp = entity.components.WiredPins; + if (!pinsComp) { + return false; + } + + // Go over all slots + for (let slotIndex = 0; slotIndex < pinsComp.slots.length; ++slotIndex) { + const slot = pinsComp.slots[slotIndex]; + + // Figure out which tile this slot is on + const worldPos = entity.components.StaticMapEntity.localTileToWorld(slot.pos); + if (offset) { + worldPos.x += offset.x; + worldPos.y += offset.y; + } + + // Check if there is any entity on that tile (Wired pins are always on the wires layer) + const collidingEntity = this.root.map.getLayerContentXY(worldPos.x, worldPos.y, enumLayer.wires); + + // If there's an entity, and it can't get removed -> That's a collision + if (collidingEntity && !collidingEntity.components.ReplaceableMapEntity) { + return true; + } + } + return false; + } + + /** + * Called to free space for the given entity + * @param {Entity} entity + */ + freeEntityAreaBeforeBuild(entity) { + const pinsComp = entity.components.WiredPins; + if (!pinsComp) { + // Entity has no pins + return; + } + + // Remove any stuff which collides with the pins + for (let i = 0; i < pinsComp.slots.length; ++i) { + const slot = pinsComp.slots[i]; + const worldPos = entity.components.StaticMapEntity.localTileToWorld(slot.pos); + const collidingEntity = this.root.map.getLayerContentXY(worldPos.x, worldPos.y, enumLayer.wires); + if (collidingEntity) { + assertAlways( + collidingEntity.components.ReplaceableMapEntity, + "Tried to replace non-repleaceable entity for pins" + ); + if (!this.root.logic.tryDeleteBuilding(collidingEntity)) { + assertAlways(false, "Tried to replace non-repleaceable entity for pins #2"); + } + } + } } update() {