Huge refactoring of the whole placement logic

This commit is contained in:
tobspr 2020-07-05 17:56:54 +02:00
parent ca8745b1c0
commit d09bd0bcd7
7 changed files with 215 additions and 244 deletions

View File

@ -90,17 +90,7 @@ export class Blueprint {
const rect = staticComp.getTileSpaceBounds(); const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y); rect.moveBy(tile.x, tile.y);
let placeable = true; if (!parameters.root.logic.checkCanPlaceEntity(entity, tile)) {
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) {
parameters.context.globalAlpha = 0.3; parameters.context.globalAlpha = 0.3;
} else { } else {
parameters.context.globalAlpha = 1; parameters.context.globalAlpha = 1;
@ -150,21 +140,8 @@ export class Blueprint {
let anyPlaceable = false; let anyPlaceable = false;
for (let i = 0; i < this.entities.length; ++i) { for (let i = 0; i < this.entities.length; ++i) {
let placeable = true;
const entity = this.entities[i]; const entity = this.entities[i];
const staticComp = entity.components.StaticMapEntity; if (root.logic.checkCanPlaceEntity(entity, tile)) {
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) {
anyPlaceable = true; anyPlaceable = true;
} }
} }
@ -188,48 +165,17 @@ export class Blueprint {
return root.logic.performBulkOperation(() => { return root.logic.performBulkOperation(() => {
let anyPlaced = false; let anyPlaced = false;
for (let i = 0; i < this.entities.length; ++i) { for (let i = 0; i < this.entities.length; ++i) {
let placeable = true;
const entity = this.entities[i]; const entity = this.entities[i];
const staticComp = entity.components.StaticMapEntity; if (!root.logic.checkCanPlaceEntity(entity, tile)) {
const rect = staticComp.getTileSpaceBounds(); continue;
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 (placeable) { const clone = entity.duplicateWithoutContents();
for (let x = rect.x; x < rect.right(); ++x) { clone.components.StaticMapEntity.origin.addInplace(tile);
for (let y = rect.y; y < rect.bottom(); ++y) { root.logic.freeEntityAreaBeforeBuild(clone);
const contents = root.map.getLayerContentXY(x, y, entity.layer); root.map.placeStaticEntity(clone);
if (contents) { root.entityMgr.registerEntity(clone);
assert( anyPlaced = true;
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;
}
} }
return anyPlaced; return anyPlaced;
}); });

View File

@ -130,7 +130,8 @@ export class GameCore {
this.root.gameIsFresh = true; this.root.gameIsFresh = true;
this.root.map.seed = randomInt(0, 100000); this.root.map.seed = randomInt(0, 100000);
gMetaBuildingRegistry.findByClass(MetaHubBuilding).createAndPlaceEntity({ // Place the hub
const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({
root: this.root, root: this.root,
origin: new Vector(-2, -2), origin: new Vector(-2, -2),
rotation: 0, rotation: 0,
@ -138,6 +139,8 @@ export class GameCore {
rotationVariant: 0, rotationVariant: 0,
variant: defaultBuildingVariant, variant: defaultBuildingVariant,
}); });
this.root.map.placeStaticEntity(hub);
this.root.entityMgr.registerEntity(hub);
} }
/** /**

View File

@ -276,14 +276,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
staticComp.tileSize = metaBuilding.getDimensions(this.currentVariant.get()); staticComp.tileSize = metaBuilding.getDimensions(this.currentVariant.get());
metaBuilding.updateVariants(this.fakeEntity, rotationVariant, this.currentVariant.get()); metaBuilding.updateVariants(this.fakeEntity, rotationVariant, this.currentVariant.get());
// Check if we could place the buildnig const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity);
const canBuild = this.root.logic.checkCanPlaceBuilding({
origin: mouseTile,
rotation,
rotationVariant,
building: metaBuilding,
variant: this.currentVariant.get(),
});
// Fade in / out // Fade in / out
parameters.context.lineWidth = 1; parameters.context.lineWidth = 1;

View File

@ -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 { createLogger } from "../core/logging";
import { MetaBeltBaseBuilding, arrayBeltVariantToRotation } from "./buildings/belt_base";
import { SOUNDS } from "../platform/sound";
import { round2Digits } from "../core/utils"; 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"); const logger = createLogger("ingame/logic");
@ -47,125 +45,41 @@ export class GameLogic {
} }
/** /**
* @param {object} param0 * Checks if the given entity can be placed
* @param {Vector} param0.origin * @param {Entity} entity
* @param {number} param0.rotation * @param {Vector=} offset Optional, move the entity by the given offset first
* @param {number} param0.rotationVariant * @returns {boolean} true if the entity could be placed there
* @param {string} param0.variant
* @param {MetaBuilding} param0.building
* @returns {boolean}
*/ */
isAreaFreeToBuild({ origin, rotation, rotationVariant, variant, building }) { checkCanPlaceEntity(entity, offset = null) {
const checker = new StaticMapEntityComponent({ // Compute area of the building
origin, const rect = entity.components.StaticMapEntity.getTileSpaceBounds();
tileSize: building.getDimensions(variant), if (offset) {
rotation, rect.x += offset.x;
blueprintSpriteKey: "", rect.y += offset.y;
}); }
const layer = building.getLayer();
const rect = checker.getTileSpaceBounds();
// Check the whole area of the building
for (let x = rect.x; x < rect.x + rect.w; ++x) { for (let x = rect.x; x < rect.x + rect.w; ++x) {
for (let y = rect.y; y < rect.y + rect.h; ++y) { for (let y = rect.y; y < rect.y + rect.h; ++y) {
const contents = this.root.map.getLayerContentXY(x, y, layer); // Check if there is any direct collision
if (contents) { const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer);
if ( if (otherEntity && !otherEntity.components.ReplaceableMapEntity) {
!this.checkCanReplaceBuilding({ // This one is a direct blocker
original: contents, return false;
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;
}
}
}
}
} }
} }
} }
return true;
}
/** // Perform additional placement checks
* Checks if the given building can be replaced by another if (this.root.signals.prePlacementCheck.dispatch(entity, offset) === STOP_PROPAGATION) {
* @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
return false; 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; return true;
} }
/** /**
* @param {object} param0 * Attempts to place the given building
* @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,
});
}
/**
*
* @param {object} param0 * @param {object} param0
* @param {Vector} param0.origin * @param {Vector} param0.origin
* @param {number} param0.rotation * @param {number} param0.rotation
@ -176,44 +90,51 @@ export class GameLogic {
* @returns {Entity} * @returns {Entity}
*/ */
tryPlaceBuilding({ origin, rotation, rotationVariant, originalRotation, variant, building }) { tryPlaceBuilding({ origin, rotation, rotationVariant, originalRotation, variant, building }) {
if (this.checkCanPlaceBuilding({ origin, rotation, rotationVariant, variant, building })) { const entity = building.createEntity({
// Remove any removeable entities below root: this.root,
const checker = new StaticMapEntityComponent({ origin,
origin, rotation,
tileSize: building.getDimensions(variant), originalRotation,
rotation, rotationVariant,
blueprintSpriteKey: "", variant,
}); });
if (this.checkCanPlaceEntity(entity)) {
const layer = building.getLayer(); this.freeEntityAreaBeforeBuild(entity);
this.root.map.placeStaticEntity(entity);
const rect = checker.getTileSpaceBounds(); this.root.entityMgr.registerEntity(entity);
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,
});
return entity; return entity;
} }
return null; 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 * Performs a bulk operation, not updating caches in the meantime
* @param {function} operation * @param {function} operation
@ -266,6 +187,7 @@ export class GameLogic {
/** @type {AcceptorsAffectingTile} */ /** @type {AcceptorsAffectingTile} */
let acceptors = []; let acceptors = [];
// Well .. please ignore this code! :D
for (let dx = -1; dx <= 1; ++dx) { for (let dx = -1; dx <= 1; ++dx) {
for (let dy = -1; dy <= 1; ++dy) { for (let dy = -1; dy <= 1; ++dy) {
if (Math.abs(dx) + Math.abs(dy) !== 1) { if (Math.abs(dx) + Math.abs(dy) !== 1) {

View File

@ -144,30 +144,6 @@ export class MetaBuilding {
return null; 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 * Creates the entity without placing it
* @param {object} param0 * @param {object} param0

View File

@ -27,6 +27,7 @@ import { ShapeDefinition } from "./shape_definition";
import { BaseItem } from "./base_item"; import { BaseItem } from "./base_item";
import { DynamicTickrate } from "./dynamic_tickrate"; import { DynamicTickrate } from "./dynamic_tickrate";
import { KeyActionMapper } from "./key_action_mapper"; import { KeyActionMapper } from "./key_action_mapper";
import { Vector } from "../core/vector";
/* typehints:end */ /* typehints:end */
const logger = createLogger("game/root"); const logger = createLogger("game/root");
@ -169,6 +170,14 @@ export class GameRoot {
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
editModeChanged: /** @type {TypedSignal<[enumLayer]>} */ (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 // RNG's

View File

@ -1,10 +1,11 @@
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters"; 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 { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view"; import { enumLayer } from "../root";
import { Loader } from "../../core/loader";
export class WiredPinsSystem extends GameSystemWithFilter { export class WiredPinsSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
@ -20,6 +21,127 @@ export class WiredPinsSystem extends GameSystemWithFilter {
"sprites/wires/pin_negative_accept.png" "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() { update() {