diff --git a/.gitignore b/.gitignore index cdade93f..1f7c68bc 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ config.local.js # Editor artifacts *.*.swp *.*.swo + +.history/ \ No newline at end of file diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 1d1b0b02..4efb747d 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -682,6 +682,44 @@ export function smoothPulse(time) { return Math.sin(time * 4) * 0.5 + 0.5; } +let logIntervals = {}; +const intervalStyle = "color: grey; font-style: inherit"; +const keyStyle = "color: purple; font-style: italic"; +const revertStyle = "color: inherit; font-style: inherit"; + +export function logInterval(key, frames, message, ...args) { + let interval = logIntervals[key] || 0; + if (++interval > frames) { + console.log( + `%clogInterval [%c${key}%c]: \t%c` + message, + intervalStyle, + keyStyle, + intervalStyle, + revertStyle, + ...args + ); + interval = 0; + } + logIntervals[key] = interval; +} + +export function dirInterval(key, frames, object, premessage, ...args) { + let interval = logIntervals[key] || 0; + if (++interval > frames) { + console.log( + `%cdirInterval [%c${key}%c]: \t%c` + (premessage || ""), + intervalStyle, + keyStyle, + intervalStyle, + revertStyle, + ...args + ); + console.dir(object); + interval = 0; + } + logIntervals[key] = interval; +} + /** * Fills in a tag * @param {string} translation diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 8ad4f7e3..73740e4b 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -630,9 +630,10 @@ export class BeltPath extends BasicSerializableObject { beltComp.assignedPath = null; const entityLength = beltComp.getEffectiveLengthTiles(); - assert(this.entityPath.indexOf(entity) >= 0, "Entity not contained for split"); - assert(this.entityPath.indexOf(entity) !== 0, "Entity is first"); - assert(this.entityPath.indexOf(entity) !== this.entityPath.length - 1, "Entity is last"); + const index = this.entityPath.indexOf(entity); + assert(index >= 0, "Entity not contained for split"); + assert(index !== 0, "Entity is first"); + assert(index !== this.entityPath.length - 1, "Entity is last"); let firstPathEntityCount = 0; let firstPathLength = 0; diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index 795b27c3..16e7f842 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -58,6 +58,37 @@ export class Blueprint { return new Blueprint(newEntities); } + /** + * Creates a new blueprint from the given entity uids + * @param {Array} entities + */ + static fromEntities(entities) { + const newEntities = []; + + let averagePosition = new Vector(); + + // First, create a copy + for (let i = entities.length - 1; i >= 0; --i) { + const entity = entities[i]; + + const clone = entity.clone(); + newEntities.push(clone); + + const pos = entity.components.StaticMapEntity.getTileSpaceBounds().getCenter(); + averagePosition.addInplace(pos); + } + + averagePosition.divideScalarInplace(entities.length); + const blueprintOrigin = averagePosition.subScalars(0.5, 0.5).floor(); + + for (let i = newEntities.length - 1; i >= 0; --i) { + newEntities[i].components.StaticMapEntity.origin.subInplace(blueprintOrigin); + } + + // Now, make sure the origin is 0,0 + return new Blueprint(newEntities); + } + /** * Returns the cost of this blueprint in shapes */ diff --git a/src/js/game/core.js b/src/js/game/core.js index a0ee3713..25df57d0 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -437,7 +437,7 @@ export class GameCore { this.overlayAlpha = lerp(this.overlayAlpha, desiredOverlayAlpha, 0.25); // On low performance, skip the fade - if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) { + if (this.root.entityMgr.entities.size > 5000 || this.root.dynamicTickrate.averageFps < 50) { this.overlayAlpha = desiredOverlayAlpha; } diff --git a/src/js/game/entity_manager.js b/src/js/game/entity_manager.js index b4101fc8..9c4a3dda 100644 --- a/src/js/game/entity_manager.js +++ b/src/js/game/entity_manager.js @@ -1,4 +1,3 @@ -import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils"; import { Component } from "./component"; import { GameRoot } from "./root"; import { Entity } from "./entity"; @@ -10,8 +9,8 @@ const logger = createLogger("entity_manager"); // Manages all entities -// NOTICE: We use arrayDeleteValue instead of fastArrayDeleteValue since that does not preserve the order -// This is slower but we need it for the street path generation +/** @typedef {number} EntityUid */ +/** @typedef {string} ComponentId */ export class EntityManager extends BasicSerializableObject { constructor(root) { @@ -20,19 +19,20 @@ export class EntityManager extends BasicSerializableObject { /** @type {GameRoot} */ this.root = root; - /** @type {Array} */ - this.entities = []; + /** @type {Set} */ + this.entities = new Set(); + + /** @type {Map} */ + this.entitiesByUid = new Map(); + + /** @type {Map>} */ + this.entitiesByComponent = new Map(); // We store a separate list with entities to destroy, since we don't destroy // them instantly /** @type {Array} */ this.destroyList = []; - // Store a map from componentid to entities - This is used by the game system - // for faster processing - /** @type {Object.>} */ - this.componentToEntity = newEmptyMap(); - // Store the next uid to use this.nextUid = 10000; } @@ -48,7 +48,7 @@ export class EntityManager extends BasicSerializableObject { } getStatsText() { - return this.entities.length + " entities [" + this.destroyList.length + " to kill]"; + return this.entities.size + " entities [" + this.destroyList.length + " to kill]"; } // Main update @@ -56,6 +56,19 @@ export class EntityManager extends BasicSerializableObject { this.processDestroyList(); } + /** + * @param {Entity} entity + * @param {ComponentId} componentId + */ + addToComponentMap(entity, componentId) { + let set; + if ((set = this.entitiesByComponent.get(componentId))) { + set.add(entity); + } else { + this.entitiesByComponent.set(componentId, new Set([entity])); + } + } + /** * Registers a new entity * @param {Entity} entity @@ -63,7 +76,7 @@ export class EntityManager extends BasicSerializableObject { */ registerEntity(entity, uid = null) { if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) { - assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`); + assert(!this.entities.has(entity), `RegisterEntity() called twice for entity ${entity}`); } assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`); @@ -72,21 +85,17 @@ export class EntityManager extends BasicSerializableObject { assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid); } - this.entities.push(entity); + // Give each entity a unique id + entity.uid = uid || this.generateUid(); + + this.entities.add(entity); + this.entitiesByUid.set(uid, entity); // Register into the componentToEntity map for (const componentId in entity.components) { - if (entity.components[componentId]) { - if (this.componentToEntity[componentId]) { - this.componentToEntity[componentId].push(entity); - } else { - this.componentToEntity[componentId] = [entity]; - } - } + this.addToComponentMap(entity, componentId); } - // Give each entity a unique id - entity.uid = uid ? uid : this.generateUid(); entity.registered = true; this.root.signals.entityAdded.dispatch(entity); @@ -108,11 +117,8 @@ export class EntityManager extends BasicSerializableObject { attachDynamicComponent(entity, component) { entity.addComponent(component, true); const componentId = /** @type {typeof Component} */ (component.constructor).getId(); - if (this.componentToEntity[componentId]) { - this.componentToEntity[componentId].push(entity); - } else { - this.componentToEntity[componentId] = [entity]; - } + + this.addToComponentMap(entity, componentId); this.root.signals.entityGotNewComponent.dispatch(entity); } @@ -125,7 +131,7 @@ export class EntityManager extends BasicSerializableObject { entity.removeComponent(component, true); const componentId = /** @type {typeof Component} */ (component.constructor).getId(); - fastArrayDeleteValue(this.componentToEntity[componentId], entity); + this.entitiesByComponent.get(componentId).delete(entity); this.root.signals.entityComponentRemoved.dispatch(entity); } @@ -136,18 +142,15 @@ export class EntityManager extends BasicSerializableObject { * @returns {Entity} */ findByUid(uid, errorWhenNotFound = true) { - const arr = this.entities; - for (let i = 0, len = arr.length; i < len; ++i) { - const entity = arr[i]; - if (entity.uid === uid) { - if (entity.queuedForDestroy || entity.destroyed) { - if (errorWhenNotFound) { - logger.warn("Entity with UID", uid, "not found (destroyed)"); - } - return null; + const entity = this.entitiesByUid.get(uid); + if (entity) { + if (entity.queuedForDestroy || entity.destroyed) { + if (errorWhenNotFound) { + logger.warn("Entity with UID", uid, "not found (destroyed)"); } - return entity; + return null; } + return entity; } if (errorWhenNotFound) { logger.warn("Entity with UID", uid, "not found"); @@ -162,15 +165,7 @@ export class EntityManager extends BasicSerializableObject { * @returns {Map} */ getFrozenUidSearchMap() { - const result = new Map(); - const array = this.entities; - for (let i = 0, len = array.length; i < len; ++i) { - const entity = array[i]; - if (!entity.queuedForDestroy && !entity.destroyed) { - result.set(entity.uid, entity); - } - } - return result; + return this.entitiesByUid; } /** @@ -179,7 +174,9 @@ export class EntityManager extends BasicSerializableObject { * @returns {Array} entities */ getAllWithComponent(componentHandle) { - return this.componentToEntity[componentHandle.getId()] || []; + const set = this.entitiesByComponent.get(componentHandle.getId()); + if (!set) return []; + else return [...set.values()]; } /** @@ -188,20 +185,20 @@ export class EntityManager extends BasicSerializableObject { */ unregisterEntityComponents(entity) { for (const componentId in entity.components) { - if (entity.components[componentId]) { - arrayDeleteValue(this.componentToEntity[componentId], entity); - } + const set = this.entitiesByComponent.get(componentId); + if (set) set.delete(entity); } } // Processes the entities to destroy and actually destroys them /* eslint-disable max-statements */ processDestroyList() { - for (let i = 0; i < this.destroyList.length; ++i) { + for (let i = this.destroyList.length - 1; i >= 0; --i) { const entity = this.destroyList[i]; // Remove from entities list - arrayDeleteValue(this.entities, entity); + this.entities.delete(entity); + this.entitiesByUid.delete(entity.uid); // Remove from componentToEntity list this.unregisterEntityComponents(entity); @@ -230,12 +227,8 @@ export class EntityManager extends BasicSerializableObject { return; } - if (this.destroyList.indexOf(entity) < 0) { - this.destroyList.push(entity); - entity.queuedForDestroy = true; - this.root.signals.entityQueuedForDestroy.dispatch(entity); - } else { - assert(false, "Trying to destroy entity twice"); - } + this.destroyList.push(entity); + entity.queuedForDestroy = true; + this.root.signals.entityQueuedForDestroy.dispatch(entity); } } diff --git a/src/js/game/game_system_with_filter.js b/src/js/game/game_system_with_filter.js index a6efeffd..32da0881 100644 --- a/src/js/game/game_system_with_filter.js +++ b/src/js/game/game_system_with_filter.js @@ -1,137 +1,134 @@ -/* typehints:start */ -import { Component } from "./component"; -import { Entity } from "./entity"; -/* typehints:end */ - -import { GameRoot } from "./root"; -import { GameSystem } from "./game_system"; -import { arrayDelete, arrayDeleteValue } from "../core/utils"; -import { globalConfig } from "../core/config"; - -export class GameSystemWithFilter extends GameSystem { - /** - * Constructs a new game system with the given component filter. It will process - * all entities which have *all* of the passed components - * @param {GameRoot} root - * @param {Array} requiredComponents - */ - constructor(root, requiredComponents) { - super(root); - this.requiredComponents = requiredComponents; - this.requiredComponentIds = requiredComponents.map(component => component.getId()); - - /** - * All entities which match the current components - * @type {Array} - */ - this.allEntities = []; - - this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this); - this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this); - this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this); - this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this); - - this.root.signals.postLoadHook.add(this.internalPostLoadHook, this); - this.root.signals.bulkOperationFinished.add(this.refreshCaches, this); - } - - /** - * @param {Entity} entity - */ - internalPushEntityIfMatching(entity) { - for (let i = 0; i < this.requiredComponentIds.length; ++i) { - if (!entity.components[this.requiredComponentIds[i]]) { - return; - } - } - - // This is slow! - if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) { - assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity); - } - - this.internalRegisterEntity(entity); - } - - /** - * - * @param {Entity} entity - */ - internalCheckEntityAfterComponentRemoval(entity) { - if (this.allEntities.indexOf(entity) < 0) { - // Entity wasn't interesting anyways - return; - } - - for (let i = 0; i < this.requiredComponentIds.length; ++i) { - if (!entity.components[this.requiredComponentIds[i]]) { - // Entity is not interesting anymore - arrayDeleteValue(this.allEntities, entity); - } - } - } - - /** - * - * @param {Entity} entity - */ - internalReconsiderEntityToAdd(entity) { - for (let i = 0; i < this.requiredComponentIds.length; ++i) { - if (!entity.components[this.requiredComponentIds[i]]) { - return; - } - } - if (this.allEntities.indexOf(entity) >= 0) { - return; - } - this.internalRegisterEntity(entity); - } - - refreshCaches() { - // Remove all entities which are queued for destroy - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - if (entity.queuedForDestroy || entity.destroyed) { - this.allEntities.splice(i, 1); - i -= 1; - } - } - - this.allEntities.sort((a, b) => a.uid - b.uid); - } - - /** - * Recomputes all target entities after the game has loaded - */ - internalPostLoadHook() { - this.refreshCaches(); - } - - /** - * - * @param {Entity} entity - */ - internalRegisterEntity(entity) { - this.allEntities.push(entity); - - if (this.root.gameInitialized && !this.root.bulkOperationRunning) { - // Sort entities by uid so behaviour is predictable - this.allEntities.sort((a, b) => a.uid - b.uid); - } - } - - /** - * - * @param {Entity} entity - */ - internalPopEntityIfMatching(entity) { - if (this.root.bulkOperationRunning) { - // We do this in refreshCaches afterwards - return; - } - const index = this.allEntities.indexOf(entity); - if (index >= 0) { - arrayDelete(this.allEntities, index); - } - } -} +/* typehints:start */ +import { Component } from "./component"; +import { Entity } from "./entity"; +/* typehints:end */ + +import { GameRoot } from "./root"; +import { GameSystem } from "./game_system"; + +export class GameSystemWithFilter extends GameSystem { + /** + * Constructs a new game system with the given component filter. It will process + * all entities which have *all* of the passed components + * @param {GameRoot} root + * @param {Array} requiredComponents + */ + constructor(root, requiredComponents) { + super(root); + this.requiredComponents = requiredComponents; + this.requiredComponentIds = requiredComponents.map(component => component.getId()); + + /** + * All entities which match the current components + * @type {Set} + */ + this.allEntitiesSet = new Set(); + this.allEntitiesArray = []; + this.allEntitiesArrayIsOutdated = true; + this.entitiesQueuedToDelete = []; + + this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this); + this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this); + this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this); + this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this); + + this.root.signals.postLoadHook.add(this.internalPostLoadHook, this); + this.root.signals.bulkOperationFinished.add(this.refreshCaches, this); + } + + tryUpdateEntitiesArray() { + if (this.allEntitiesArrayIsOutdated) { + this.allEntitiesArray = [...this.allEntitiesSet.values()]; + this.allEntitiesArrayIsOutdated = false; + } + } + + /** + * @param {Entity} entity + */ + internalPushEntityIfMatching(entity) { + for (let i = 0; i < this.requiredComponentIds.length; ++i) { + if (!entity.components[this.requiredComponentIds[i]]) { + return; + } + } + + assert(!this.allEntitiesSet.has(entity), "entity already in list: " + entity); + this.internalRegisterEntity(entity); + } + + /** + * + * @param {Entity} entity + */ + internalCheckEntityAfterComponentRemoval(entity) { + if (!this.allEntitiesSet.has(entity)) { + // Entity wasn't interesting anyways + return; + } + + for (let i = 0; i < this.requiredComponentIds.length; ++i) { + if (!entity.components[this.requiredComponentIds[i]]) { + // Entity is not interesting anymore + this.allEntitiesArrayIsOutdated = this.allEntitiesSet.delete(entity); + } + } + } + + /** + * + * @param {Entity} entity + */ + internalReconsiderEntityToAdd(entity) { + for (let i = 0; i < this.requiredComponentIds.length; ++i) { + if (!entity.components[this.requiredComponentIds[i]]) { + return; + } + } + if (this.allEntitiesSet.has(entity)) { + return; + } + this.internalRegisterEntity(entity); + } + + refreshCaches() { + // Remove all entities which are queued for destroy + if (this.entitiesQueuedToDelete.length > 0) { + for (let i = this.entitiesQueuedToDelete.length - 1; i >= 0; --i) { + this.allEntitiesSet.delete(this.entitiesQueuedToDelete[i]); + } + this.entitiesQueuedToDelete = []; + } + + // called here in case a delete executed mid frame + this.tryUpdateEntitiesArray(); + } + + /** + * Recomputes all target entities after the game has loaded + */ + internalPostLoadHook() { + this.refreshCaches(); + } + + /** + * + * @param {Entity} entity + */ + internalRegisterEntity(entity) { + this.allEntitiesSet.add(entity); + this.allEntitiesArray.push(entity); + } + + /** + * + * @param {Entity} entity + */ + internalPopEntityIfMatching(entity) { + if (this.root.bulkOperationRunning) { + this.entitiesQueuedToDelete.push(entity); + return; + } + this.allEntitiesArrayIsOutdated = this.allEntitiesSet.delete(entity); + } +} diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 3d22787c..ddb705e5 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -38,7 +38,7 @@ export class GameHUD { shapePinRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()), notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), - buildingsSelectedForCopy: /** @type {TypedSignal<[Array]>} */ (new Signal()), + buildingsSelectedForCopy: /** @type {TypedSignal<[Array]>} */ (new Signal()), pasteBlueprintRequested: /** @type {TypedSignal<[]>} */ (new Signal()), viewShapeDetailsRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), unlockNotificationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), diff --git a/src/js/game/hud/parts/blueprint_placer.js b/src/js/game/hud/parts/blueprint_placer.js index 4b2bafb2..37fe8cee 100644 --- a/src/js/game/hud/parts/blueprint_placer.js +++ b/src/js/game/hud/parts/blueprint_placer.js @@ -1,212 +1,213 @@ -import { DrawParameters } from "../../../core/draw_parameters"; -import { STOP_PROPAGATION } from "../../../core/signal"; -import { TrackedState } from "../../../core/tracked_state"; -import { makeDiv } from "../../../core/utils"; -import { Vector } from "../../../core/vector"; -import { SOUNDS } from "../../../platform/sound"; -import { T } from "../../../translations"; -import { Blueprint } from "../../blueprint"; -import { enumMouseButton } from "../../camera"; -import { KEYMAPPINGS } from "../../key_action_mapper"; -import { BaseHUDPart } from "../base_hud_part"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; - -export class HUDBlueprintPlacer extends BaseHUDPart { - createElements(parent) { - const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey( - this.root.gameMode.getBlueprintShapeKey() - ); - const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80); - - this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``); - - makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost); - const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], ""); - this.costDisplayText = makeDiv(costContainer, null, ["costText"], ""); - costContainer.appendChild(blueprintCostShapeCanvas); - } - - initialize() { - this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this); - - /** @type {TypedTrackedState} */ - this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this); - /** @type {Blueprint?} */ - this.lastBlueprintUsed = null; - - const keyActionMapper = this.root.keyMapper; - keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); - keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this); - keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this); - keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this); - - this.root.camera.downPreHandler.add(this.onMouseDown, this); - this.root.camera.movePreHandler.add(this.onMouseMove, this); - - this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this); - this.root.signals.editModeChanged.add(this.onEditModeChanged, this); - - this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent); - this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this); - } - - getHasFreeCopyPaste() { - return this.root.gameMode.getHasFreeCopyPaste(); - } - - abortPlacement() { - if (this.currentBlueprint.get()) { - this.currentBlueprint.set(null); - - return STOP_PROPAGATION; - } - } - - /** - * Called when the layer was changed - * @param {Layer} layer - */ - onEditModeChanged(layer) { - // Check if the layer of the blueprint differs and thus we have to deselect it - const blueprint = this.currentBlueprint.get(); - if (blueprint) { - if (blueprint.layer !== layer) { - this.currentBlueprint.set(null); - } - } - } - - /** - * Called when the blueprint is now affordable or not - * @param {boolean} canAfford - */ - onCanAffordChanged(canAfford) { - this.costDisplayParent.classList.toggle("canAfford", canAfford); - } - - update() { - const currentBlueprint = this.currentBlueprint.get(); - this.domAttach.update( - !this.getHasFreeCopyPaste() && currentBlueprint && currentBlueprint.getCost() > 0 - ); - this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root)); - } - - /** - * Called when the blueprint was changed - * @param {Blueprint} blueprint - */ - onBlueprintChanged(blueprint) { - if (blueprint) { - this.lastBlueprintUsed = blueprint; - this.costDisplayText.innerText = "" + blueprint.getCost(); - } - } - - /** - * mouse down pre handler - * @param {Vector} pos - * @param {enumMouseButton} button - */ - onMouseDown(pos, button) { - if (button === enumMouseButton.right) { - if (this.currentBlueprint.get()) { - this.abortPlacement(); - return STOP_PROPAGATION; - } - } else if (button === enumMouseButton.left) { - const blueprint = this.currentBlueprint.get(); - if (!blueprint) { - return; - } - - if (!this.getHasFreeCopyPaste() && !blueprint.canAfford(this.root)) { - this.root.soundProxy.playUiError(); - return; - } - - const worldPos = this.root.camera.screenToWorld(pos); - const tile = worldPos.toTileSpace(); - if (blueprint.tryPlace(this.root, tile)) { - if (!this.getHasFreeCopyPaste()) { - const cost = blueprint.getCost(); - this.root.hubGoals.takeShapeByKey(this.root.gameMode.getBlueprintShapeKey(), cost); - } - this.root.soundProxy.playUi(SOUNDS.placeBuilding); - } - return STOP_PROPAGATION; - } - } - - /** - * Mouse move handler - */ - onMouseMove() { - // Prevent movement while blueprint is selected - if (this.currentBlueprint.get()) { - return STOP_PROPAGATION; - } - } - - /** - * Called when an array of bulidings was selected - * @param {Array} uids - */ - createBlueprintFromBuildings(uids) { - if (uids.length === 0) { - return; - } - this.currentBlueprint.set(Blueprint.fromUids(this.root, uids)); - } - - /** - * Attempts to rotate the current blueprint - */ - rotateBlueprint() { - if (this.currentBlueprint.get()) { - if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { - this.currentBlueprint.get().rotateCcw(); - } else { - this.currentBlueprint.get().rotateCw(); - } - } - } - - /** - * Attempts to paste the last blueprint - */ - pasteBlueprint() { - if (this.lastBlueprintUsed !== null) { - if (this.lastBlueprintUsed.layer !== this.root.currentLayer) { - // Not compatible - this.root.soundProxy.playUiError(); - return; - } - - this.root.hud.signals.pasteBlueprintRequested.dispatch(); - this.currentBlueprint.set(this.lastBlueprintUsed); - } else { - this.root.soundProxy.playUiError(); - } - } - - /** - * - * @param {DrawParameters} parameters - */ - draw(parameters) { - const blueprint = this.currentBlueprint.get(); - if (!blueprint) { - return; - } - const mousePosition = this.root.app.mousePosition; - if (!mousePosition) { - // Not on screen - return; - } - - const worldPos = this.root.camera.screenToWorld(mousePosition); - const tile = worldPos.toTileSpace(); - blueprint.draw(parameters, tile); - } -} +import { DrawParameters } from "../../../core/draw_parameters"; +import { STOP_PROPAGATION } from "../../../core/signal"; +import { TrackedState } from "../../../core/tracked_state"; +import { makeDiv } from "../../../core/utils"; +import { Vector } from "../../../core/vector"; +import { SOUNDS } from "../../../platform/sound"; +import { T } from "../../../translations"; +import { Blueprint } from "../../blueprint"; +import { enumMouseButton } from "../../camera"; +import { KEYMAPPINGS } from "../../key_action_mapper"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { Entity } from "../../entity"; + +export class HUDBlueprintPlacer extends BaseHUDPart { + createElements(parent) { + const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey( + this.root.gameMode.getBlueprintShapeKey() + ); + const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80); + + this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``); + + makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost); + const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], ""); + this.costDisplayText = makeDiv(costContainer, null, ["costText"], ""); + costContainer.appendChild(blueprintCostShapeCanvas); + } + + initialize() { + this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this); + + /** @type {TypedTrackedState} */ + this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this); + /** @type {Blueprint?} */ + this.lastBlueprintUsed = null; + + const keyActionMapper = this.root.keyMapper; + keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this); + keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this); + + this.root.camera.downPreHandler.add(this.onMouseDown, this); + this.root.camera.movePreHandler.add(this.onMouseMove, this); + + this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this); + this.root.signals.editModeChanged.add(this.onEditModeChanged, this); + + this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent); + this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this); + } + + getHasFreeCopyPaste() { + return this.root.gameMode.getHasFreeCopyPaste(); + } + + abortPlacement() { + if (this.currentBlueprint.get()) { + this.currentBlueprint.set(null); + + return STOP_PROPAGATION; + } + } + + /** + * Called when the layer was changed + * @param {Layer} layer + */ + onEditModeChanged(layer) { + // Check if the layer of the blueprint differs and thus we have to deselect it + const blueprint = this.currentBlueprint.get(); + if (blueprint) { + if (blueprint.layer !== layer) { + this.currentBlueprint.set(null); + } + } + } + + /** + * Called when the blueprint is now affordable or not + * @param {boolean} canAfford + */ + onCanAffordChanged(canAfford) { + this.costDisplayParent.classList.toggle("canAfford", canAfford); + } + + update() { + const currentBlueprint = this.currentBlueprint.get(); + this.domAttach.update( + !this.getHasFreeCopyPaste() && currentBlueprint && currentBlueprint.getCost() > 0 + ); + this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root)); + } + + /** + * Called when the blueprint was changed + * @param {Blueprint} blueprint + */ + onBlueprintChanged(blueprint) { + if (blueprint) { + this.lastBlueprintUsed = blueprint; + this.costDisplayText.innerText = "" + blueprint.getCost(); + } + } + + /** + * mouse down pre handler + * @param {Vector} pos + * @param {enumMouseButton} button + */ + onMouseDown(pos, button) { + if (button === enumMouseButton.right) { + if (this.currentBlueprint.get()) { + this.abortPlacement(); + return STOP_PROPAGATION; + } + } else if (button === enumMouseButton.left) { + const blueprint = this.currentBlueprint.get(); + if (!blueprint) { + return; + } + + if (!this.getHasFreeCopyPaste() && !blueprint.canAfford(this.root)) { + this.root.soundProxy.playUiError(); + return; + } + + const worldPos = this.root.camera.screenToWorld(pos); + const tile = worldPos.toTileSpace(); + if (blueprint.tryPlace(this.root, tile)) { + if (!this.getHasFreeCopyPaste()) { + const cost = blueprint.getCost(); + this.root.hubGoals.takeShapeByKey(this.root.gameMode.getBlueprintShapeKey(), cost); + } + this.root.soundProxy.playUi(SOUNDS.placeBuilding); + } + return STOP_PROPAGATION; + } + } + + /** + * Mouse move handler + */ + onMouseMove() { + // Prevent movement while blueprint is selected + if (this.currentBlueprint.get()) { + return STOP_PROPAGATION; + } + } + + /** + * Called when an array of bulidings was selected + * @param {Array} uids + */ + createBlueprintFromBuildings(entities) { + if (entities.length === 0) { + return; + } + this.currentBlueprint.set(Blueprint.fromEntities(entities)); + } + + /** + * Attempts to rotate the current blueprint + */ + rotateBlueprint() { + if (this.currentBlueprint.get()) { + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { + this.currentBlueprint.get().rotateCcw(); + } else { + this.currentBlueprint.get().rotateCw(); + } + } + } + + /** + * Attempts to paste the last blueprint + */ + pasteBlueprint() { + if (this.lastBlueprintUsed !== null) { + if (this.lastBlueprintUsed.layer !== this.root.currentLayer) { + // Not compatible + this.root.soundProxy.playUiError(); + return; + } + + this.root.hud.signals.pasteBlueprintRequested.dispatch(); + this.currentBlueprint.set(this.lastBlueprintUsed); + } else { + this.root.soundProxy.playUiError(); + } + } + + /** + * + * @param {DrawParameters} parameters + */ + draw(parameters) { + const blueprint = this.currentBlueprint.get(); + if (!blueprint) { + return; + } + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return; + } + + const worldPos = this.root.camera.screenToWorld(mousePosition); + const tile = worldPos.toTileSpace(); + blueprint.draw(parameters, tile); + } +} diff --git a/src/js/game/hud/parts/keybinding_overlay.js b/src/js/game/hud/parts/keybinding_overlay.js index 2384ab84..d689fe8f 100644 --- a/src/js/game/hud/parts/keybinding_overlay.js +++ b/src/js/game/hud/parts/keybinding_overlay.js @@ -101,7 +101,7 @@ export class HUDKeybindingOverlay extends BaseHUDPart { */ get anythingSelectedOnMap() { const selector = this.root.hud.parts.massSelector; - return selector && selector.selectedUids.size > 0; + return selector && selector.selectedEntities.size > 0; } /** diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index b8283d55..aaf762bb 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -1,6 +1,5 @@ import { globalConfig } from "../../../core/config"; import { DrawParameters } from "../../../core/draw_parameters"; -import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { createLogger } from "../../../core/logging"; import { STOP_PROPAGATION } from "../../../core/signal"; import { formatBigNumberFull } from "../../../core/utils"; @@ -8,15 +7,12 @@ import { Vector } from "../../../core/vector"; import { ACHIEVEMENTS } from "../../../platform/achievement_provider"; import { T } from "../../../translations"; import { Blueprint } from "../../blueprint"; -import { MetaBlockBuilding } from "../../buildings/block"; -import { MetaConstantProducerBuilding } from "../../buildings/constant_producer"; -import { enumMouseButton } from "../../camera"; -import { Component } from "../../component"; import { Entity } from "../../entity"; import { KEYMAPPINGS } from "../../key_action_mapper"; import { THEME } from "../../theme"; import { enumHubGoalRewards } from "../../tutorial_goals"; import { BaseHUDPart } from "../base_hud_part"; +import { enumMouseButton } from "../../camera"; const logger = createLogger("hud/mass_selector"); @@ -26,7 +22,9 @@ export class HUDMassSelector extends BaseHUDPart { initialize() { this.currentSelectionStartWorld = null; this.currentSelectionEnd = null; - this.selectedUids = new Set(); + + /** @type {Set} */ + this.selectedEntities = new Set(); this.root.signals.entityQueuedForDestroy.add(this.onEntityDestroyed, this); this.root.hud.signals.pasteBlueprintRequested.add(this.clearSelection, this); @@ -47,6 +45,10 @@ export class HUDMassSelector extends BaseHUDPart { this.root.signals.editModeChanged.add(this.clearSelection, this); } + clear() { + this.selectedEntities.clear(); + } + /** * Handles the destroy callback and makes sure we clean our list * @param {Entity} entity @@ -55,7 +57,7 @@ export class HUDMassSelector extends BaseHUDPart { if (this.root.bulkOperationRunning) { return; } - this.selectedUids.delete(entity.uid); + this.selectedEntities.delete(entity); } /** @@ -63,8 +65,8 @@ export class HUDMassSelector extends BaseHUDPart { */ onBack() { // Clear entities on escape - if (this.selectedUids.size > 0) { - this.selectedUids = new Set(); + if (this.selectedEntities.size > 0) { + this.clear(); return STOP_PROPAGATION; } } @@ -73,19 +75,19 @@ export class HUDMassSelector extends BaseHUDPart { * Clears the entire selection */ clearSelection() { - this.selectedUids = new Set(); + this.clear(); } confirmDelete() { if ( !this.root.app.settings.getAllSettings().disableCutDeleteWarnings && - this.selectedUids.size > 100 + this.selectedEntities.size > 100 ) { const { ok } = this.root.hud.parts.dialogs.showWarning( T.dialogs.massDeleteConfirm.title, T.dialogs.massDeleteConfirm.desc.replace( "", - "" + formatBigNumberFull(this.selectedUids.size) + "" + formatBigNumberFull(this.selectedEntities.size) ), ["cancel:good:escape", "ok:bad:enter"] ); @@ -96,25 +98,16 @@ export class HUDMassSelector extends BaseHUDPart { } doDelete() { - const entityUids = Array.from(this.selectedUids); - // Build mapping from uid to entity /** * @type {Map} */ - const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap(); let count = 0; this.root.logic.performBulkOperation(() => { - for (let i = 0; i < entityUids.length; ++i) { - const uid = entityUids[i]; - const entity = mapUidToEntity.get(uid); - if (!entity) { - logger.error("Entity not found by uid:", uid); - continue; - } - - if (!this.root.logic.tryDeleteBuilding(entity)) { + const arr = [...this.selectedEntities.values()]; + for (let i = arr.length - 1; i >= 0; --i) { + if (!this.root.logic.tryDeleteBuilding(arr[i])) { logger.error("Error in mass delete, could not remove building"); } else { count++; @@ -124,12 +117,11 @@ export class HUDMassSelector extends BaseHUDPart { this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.destroy1000, count); }); - // Clear uids later - this.selectedUids = new Set(); + this.clear(); } startCopy() { - if (this.selectedUids.size > 0) { + if (this.selectedEntities.size > 0) { if (!this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { this.root.hud.parts.dialogs.showInfo( T.dialogs.blueprintsNotUnlocked.title, @@ -137,8 +129,10 @@ export class HUDMassSelector extends BaseHUDPart { ); return; } - this.root.hud.signals.buildingsSelectedForCopy.dispatch(Array.from(this.selectedUids)); - this.selectedUids = new Set(); + + // @ts-ignore + this.root.hud.signals.buildingsSelectedForCopy.dispatch([...this.selectedEntities.values()]); + this.selectedEntities.clear(); this.root.soundProxy.playUiClick(); } else { this.root.soundProxy.playUiError(); @@ -146,13 +140,12 @@ export class HUDMassSelector extends BaseHUDPart { } clearBelts() { - for (const uid of this.selectedUids) { - const entity = this.root.entityMgr.findByUid(uid); + for (const entity of this.selectedEntities) { for (const component of Object.values(entity.components)) { /** @type {Component} */ (component).clear(); } } - this.selectedUids = new Set(); + this.selectedEntities = new Set(); } confirmCut() { @@ -163,13 +156,13 @@ export class HUDMassSelector extends BaseHUDPart { ); } else if ( !this.root.app.settings.getAllSettings().disableCutDeleteWarnings && - this.selectedUids.size > 100 + this.selectedEntities.size > 100 ) { const { ok } = this.root.hud.parts.dialogs.showWarning( T.dialogs.massCutConfirm.title, T.dialogs.massCutConfirm.desc.replace( "", - "" + formatBigNumberFull(this.selectedUids.size) + "" + formatBigNumberFull(this.selectedEntities.size) ), ["cancel:good:escape", "ok:bad:enter"] ); @@ -180,26 +173,26 @@ export class HUDMassSelector extends BaseHUDPart { } doCut() { - if (this.selectedUids.size > 0) { - const entityUids = Array.from(this.selectedUids); - - const cutAction = () => { + if (this.selectedEntities.size > 0) { + const cutAction = argArray => { + const arr = argArray || [...this.selectedEntities.values()]; // copy code relies on entities still existing, so must copy before deleting. - this.root.hud.signals.buildingsSelectedForCopy.dispatch(entityUids); - for (let i = 0; i < entityUids.length; ++i) { - const uid = entityUids[i]; - const entity = this.root.entityMgr.findByUid(uid); + this.root.hud.signals.buildingsSelectedForCopy.dispatch(arr); + + for (let i = arr.length - 1; i >= 0; --i) { + const entity = arr[i]; if (!this.root.logic.tryDeleteBuilding(entity)) { logger.error("Error in mass cut, could not remove building"); - this.selectedUids.delete(uid); + this.selectedEntities.delete(entity); } } }; - const blueprint = Blueprint.fromUids(this.root, entityUids); + const arr = [...this.selectedEntities.values()]; + const blueprint = Blueprint.fromEntities(arr); if (blueprint.canAfford(this.root)) { - cutAction(); + cutAction(arr); } else { const { cancel, ok } = this.root.hud.parts.dialogs.showWarning( T.dialogs.massCutInsufficientConfirm.title, @@ -231,7 +224,7 @@ export class HUDMassSelector extends BaseHUDPart { if (!this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectSelectMultiple).pressed) { // Start new selection - this.selectedUids = new Set(); + this.clear(); } this.currentSelectionStartWorld = this.root.camera.screenToWorld(pos.copy()); @@ -270,8 +263,7 @@ export class HUDMassSelector extends BaseHUDPart { if (!staticComp.getMetaBuilding().getIsRemovable(this.root)) { continue; } - - this.selectedUids.add(contents.uid); + this.selectedEntities.add(contents); } } } @@ -350,18 +342,22 @@ export class HUDMassSelector extends BaseHUDPart { } parameters.context.fillStyle = THEME.map.selectionOverlay; - this.selectedUids.forEach(uid => { - const entity = this.root.entityMgr.findByUid(uid); - const staticComp = entity.components.StaticMapEntity; - const bounds = staticComp.getTileSpaceBounds(); - parameters.context.beginRoundedRect( - bounds.x * globalConfig.tileSize + boundsBorder, - bounds.y * globalConfig.tileSize + boundsBorder, - bounds.w * globalConfig.tileSize - 2 * boundsBorder, - bounds.h * globalConfig.tileSize - 2 * boundsBorder, - 2 - ); - parameters.context.fill(); - }); + + if (this.selectedEntities.size > 0) { + const arr = [...this.selectedEntities.values()]; + for (let i = arr.length - 1; i >= 0; --i) { + const entity = arr[i]; + const staticComp = entity.components.StaticMapEntity; + const bounds = staticComp.getTileSpaceBounds(); + parameters.context.beginRoundedRect( + bounds.x * globalConfig.tileSize + boundsBorder, + bounds.y * globalConfig.tileSize + boundsBorder, + bounds.w * globalConfig.tileSize - 2 * boundsBorder, + bounds.h * globalConfig.tileSize - 2 * boundsBorder, + 2 + ); + parameters.context.fill(); + } + } } } diff --git a/src/js/game/hud/parts/wires_overlay.js b/src/js/game/hud/parts/wires_overlay.js index 328d6689..0dd52db8 100644 --- a/src/js/game/hud/parts/wires_overlay.js +++ b/src/js/game/hud/parts/wires_overlay.js @@ -8,6 +8,7 @@ import { KEYMAPPINGS } from "../../key_action_mapper"; import { enumHubGoalRewards } from "../../tutorial_goals"; import { BaseHUDPart } from "../base_hud_part"; +// @ts-ignore const copy = require("clipboard-copy"); const wiresBackgroundDpi = 4; @@ -64,7 +65,7 @@ export class HUDWiresOverlay extends BaseHUDPart { const desiredAlpha = this.root.currentLayer === "wires" ? 1.0 : 0.0; // On low performance, skip the fade - if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) { + if (this.root.entityMgr.entities.size > 5000 || this.root.dynamicTickrate.averageFps < 50) { this.currentAlpha = desiredAlpha; } else { this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12); diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 00491eff..1163a7f1 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -425,8 +425,8 @@ export class BeltSystem extends GameSystemWithFilter { const result = []; - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; if (visitedUids.has(entity.uid)) { continue; } diff --git a/src/js/game/systems/belt_reader.js b/src/js/game/systems/belt_reader.js index 41211c05..b4b3bb81 100644 --- a/src/js/game/systems/belt_reader.js +++ b/src/js/game/systems/belt_reader.js @@ -12,8 +12,9 @@ export class BeltReaderSystem extends GameSystemWithFilter { const now = this.root.time.now(); const minimumTime = now - globalConfig.readerAnalyzeIntervalSeconds; const minimumTimeForThroughput = now - 1; - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const readerComp = entity.components.BeltReader; const pinsComp = entity.components.WiredPins; diff --git a/src/js/game/systems/constant_producer.js b/src/js/game/systems/constant_producer.js index 5c10b409..3060bf3d 100644 --- a/src/js/game/systems/constant_producer.js +++ b/src/js/game/systems/constant_producer.js @@ -14,8 +14,8 @@ export class ConstantProducerSystem extends GameSystemWithFilter { } update() { - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const signalComp = entity.components.ConstantSignal; const ejectorComp = entity.components.ItemEjector; if (!ejectorComp) { diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js index 29079825..b579ba02 100644 --- a/src/js/game/systems/constant_signal.js +++ b/src/js/game/systems/constant_signal.js @@ -24,8 +24,8 @@ export class ConstantSignalSystem extends GameSystemWithFilter { update() { // Set signals - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const signalComp = entity.components.ConstantSignal; const pinsComp = entity.components.WiredPins; diff --git a/src/js/game/systems/filter.js b/src/js/game/systems/filter.js index a6442b41..53675858 100644 --- a/src/js/game/systems/filter.js +++ b/src/js/game/systems/filter.js @@ -20,8 +20,8 @@ export class FilterSystem extends GameSystemWithFilter { const requiredProgress = 1 - progress; - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const filterComp = entity.components.Filter; const ejectorComp = entity.components.ItemEjector; diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js index 60d4a984..1e2d5220 100644 --- a/src/js/game/systems/goal_acceptor.js +++ b/src/js/game/systems/goal_acceptor.js @@ -20,8 +20,8 @@ export class GoalAcceptorSystem extends GameSystemWithFilter { let allAccepted = true; - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const goalComp = entity.components.GoalAcceptor; if (!goalComp.lastDelivery) { diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 2002b66e..748a4f7c 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -25,15 +25,15 @@ export class HubSystem extends GameSystemWithFilter { * @param {DrawParameters} parameters */ draw(parameters) { - for (let i = 0; i < this.allEntities.length; ++i) { - this.drawEntity(parameters, this.allEntities[i]); + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + this.drawEntity(parameters, this.allEntitiesArray[i]); } } update() { - for (let i = 0; i < this.allEntities.length; ++i) { + for (let i = 0; i < this.allEntitiesArray.length; ++i) { // Set hub goal - const entity = this.allEntities[i]; + const entity = this.allEntitiesArray[i]; const pinsComp = entity.components.WiredPins; pinsComp.slots[0].value = this.root.shapeDefinitionMgr.getShapeItemFromDefinition( this.root.hubGoals.currentGoal.definition diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 780b4abd..8d0977d4 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -39,8 +39,8 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { // Reset accumulated ticks this.accumulatedTicksWhileInMapOverview = 0; - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const aceptorComp = entity.components.ItemAcceptor; const animations = aceptorComp.itemConsumptionAnimations; diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index db37455a..be8e9399 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -3,6 +3,7 @@ import { DrawParameters } from "../../core/draw_parameters"; import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; import { StaleAreaDetector } from "../../core/stale_area_detector"; +import { dirInterval } from "../../core/utils"; import { enumDirection, enumDirectionToVector } from "../../core/vector"; import { ACHIEVEMENTS } from "../../platform/achievement_provider"; import { BaseItem } from "../base_item"; @@ -61,8 +62,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter { */ recomputeCacheFull() { logger.log("Full cache recompute in post load hook"); - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; this.recomputeSingleEntityCache(entity); } } @@ -147,8 +148,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } // Go over all cache entries - for (let i = 0; i < this.allEntities.length; ++i) { - const sourceEntity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const sourceEntity = this.allEntitiesArray[i]; const sourceEjectorComp = sourceEntity.components.ItemEjector; const slots = sourceEjectorComp.slots; diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 525c242c..b8053170 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -70,9 +70,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } update() { - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const processorComp = entity.components.ItemProcessor; const ejectorComp = entity.components.ItemEjector; diff --git a/src/js/game/systems/item_producer.js b/src/js/game/systems/item_producer.js index 0a385907..8f4d5845 100644 --- a/src/js/game/systems/item_producer.js +++ b/src/js/game/systems/item_producer.js @@ -13,9 +13,8 @@ export class ItemProducerSystem extends GameSystemWithFilter { } update() { - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - const ejectorComp = entity.components.ItemEjector; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const pinsComp = entity.components.WiredPins; if (!pinsComp) { continue; diff --git a/src/js/game/systems/lever.js b/src/js/game/systems/lever.js index 75b6cf28..0997f6a1 100644 --- a/src/js/game/systems/lever.js +++ b/src/js/game/systems/lever.js @@ -14,9 +14,8 @@ export class LeverSystem extends GameSystemWithFilter { } update() { - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const leverComp = entity.components.Lever; const pinsComp = entity.components.WiredPins; diff --git a/src/js/game/systems/logic_gate.js b/src/js/game/systems/logic_gate.js index 4545a331..d426e537 100644 --- a/src/js/game/systems/logic_gate.js +++ b/src/js/game/systems/logic_gate.js @@ -30,8 +30,8 @@ export class LogicGateSystem extends GameSystemWithFilter { } update() { - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const logicComp = entity.components.LogicGate; const slotComp = entity.components.WiredPins; diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index cd478be3..1b4734df 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -36,8 +36,8 @@ export class MinerSystem extends GameSystemWithFilter { miningSpeed *= 100; } - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const minerComp = entity.components.Miner; // Reset everything on recompute diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index 80affac9..4b300ea0 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -26,8 +26,8 @@ export class StorageSystem extends GameSystemWithFilter { } update() { - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; const storageComp = entity.components.Storage; const pinsComp = entity.components.WiredPins; diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index 9b31eec1..78b348e5 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -1,353 +1,353 @@ -import { globalConfig } from "../../core/config"; -import { Loader } from "../../core/loader"; -import { createLogger } from "../../core/logging"; -import { Rectangle } from "../../core/rectangle"; -import { StaleAreaDetector } from "../../core/stale_area_detector"; -import { fastArrayDelete } from "../../core/utils"; -import { - enumAngleToDirection, - enumDirection, - enumDirectionToAngle, - enumDirectionToVector, - enumInvertedDirections, -} from "../../core/vector"; -import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt"; -import { Entity } from "../entity"; -import { GameSystemWithFilter } from "../game_system_with_filter"; - -const logger = createLogger("tunnels"); - -export class UndergroundBeltSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [UndergroundBeltComponent]); - - this.beltSprites = { - [enumUndergroundBeltMode.sender]: Loader.getSprite( - "sprites/buildings/underground_belt_entry.png" - ), - [enumUndergroundBeltMode.receiver]: Loader.getSprite( - "sprites/buildings/underground_belt_exit.png" - ), - }; - - this.staleAreaWatcher = new StaleAreaDetector({ - root: this.root, - name: "underground-belt", - recomputeMethod: this.recomputeArea.bind(this), - }); - - this.root.signals.entityManuallyPlaced.add(this.onEntityManuallyPlaced, this); - - // NOTICE: Once we remove a tunnel, we need to update the whole area to - // clear outdated handles - this.staleAreaWatcher.recomputeOnComponentsChanged( - [UndergroundBeltComponent], - globalConfig.undergroundBeltMaxTilesByTier[globalConfig.undergroundBeltMaxTilesByTier.length - 1] - ); - } - - /** - * Callback when an entity got placed, used to remove belts between underground belts - * @param {Entity} entity - */ - onEntityManuallyPlaced(entity) { - if (!this.root.app.settings.getAllSettings().enableTunnelSmartplace) { - // Smart-place disabled - return; - } - - const undergroundComp = entity.components.UndergroundBelt; - if (undergroundComp && undergroundComp.mode === enumUndergroundBeltMode.receiver) { - const staticComp = entity.components.StaticMapEntity; - const tile = staticComp.origin; - - const direction = enumAngleToDirection[staticComp.rotation]; - const inverseDirection = enumInvertedDirections[direction]; - const offset = enumDirectionToVector[inverseDirection]; - - let currentPos = tile.copy(); - - const tier = undergroundComp.tier; - const range = globalConfig.undergroundBeltMaxTilesByTier[tier]; - - // FIND ENTRANCE - // Search for the entrance which is farthest apart (this is why we can't reuse logic here) - let matchingEntrance = null; - for (let i = 0; i < range; ++i) { - currentPos.addInplace(offset); - const contents = this.root.map.getTileContent(currentPos, entity.layer); - if (!contents) { - continue; - } - - const contentsUndergroundComp = contents.components.UndergroundBelt; - const contentsStaticComp = contents.components.StaticMapEntity; - if ( - contentsUndergroundComp && - contentsUndergroundComp.tier === undergroundComp.tier && - contentsUndergroundComp.mode === enumUndergroundBeltMode.sender && - enumAngleToDirection[contentsStaticComp.rotation] === direction - ) { - matchingEntrance = { - entity: contents, - range: i, - }; - } - } - - if (!matchingEntrance) { - // Nothing found - return; - } - - // DETECT OBSOLETE BELTS BETWEEN - // Remove any belts between entrance and exit which have the same direction, - // but only if they *all* have the right direction - currentPos = tile.copy(); - let allBeltsMatch = true; - for (let i = 0; i < matchingEntrance.range; ++i) { - currentPos.addInplace(offset); - - const contents = this.root.map.getTileContent(currentPos, entity.layer); - if (!contents) { - allBeltsMatch = false; - break; - } - - const contentsStaticComp = contents.components.StaticMapEntity; - const contentsBeltComp = contents.components.Belt; - if (!contentsBeltComp) { - allBeltsMatch = false; - break; - } - - // It's a belt - if ( - contentsBeltComp.direction !== enumDirection.top || - enumAngleToDirection[contentsStaticComp.rotation] !== direction - ) { - allBeltsMatch = false; - break; - } - } - - currentPos = tile.copy(); - if (allBeltsMatch) { - // All belts between this are obsolete, so drop them - for (let i = 0; i < matchingEntrance.range; ++i) { - currentPos.addInplace(offset); - const contents = this.root.map.getTileContent(currentPos, entity.layer); - assert(contents, "Invalid smart underground belt logic"); - this.root.logic.tryDeleteBuilding(contents); - } - } - - // REMOVE OBSOLETE TUNNELS - // Remove any double tunnels, by checking the tile plus the tile above - currentPos = tile.copy().add(offset); - for (let i = 0; i < matchingEntrance.range - 1; ++i) { - const posBefore = currentPos.copy(); - currentPos.addInplace(offset); - - const entityBefore = this.root.map.getTileContent(posBefore, entity.layer); - const entityAfter = this.root.map.getTileContent(currentPos, entity.layer); - - if (!entityBefore || !entityAfter) { - continue; - } - - const undergroundBefore = entityBefore.components.UndergroundBelt; - const undergroundAfter = entityAfter.components.UndergroundBelt; - - if (!undergroundBefore || !undergroundAfter) { - // Not an underground belt - continue; - } - - if ( - // Both same tier - undergroundBefore.tier !== undergroundAfter.tier || - // And same tier as our original entity - undergroundBefore.tier !== undergroundComp.tier - ) { - // Mismatching tier - continue; - } - - if ( - undergroundBefore.mode !== enumUndergroundBeltMode.sender || - undergroundAfter.mode !== enumUndergroundBeltMode.receiver - ) { - // Not the right mode - continue; - } - - // Check rotations - const staticBefore = entityBefore.components.StaticMapEntity; - const staticAfter = entityAfter.components.StaticMapEntity; - - if ( - enumAngleToDirection[staticBefore.rotation] !== direction || - enumAngleToDirection[staticAfter.rotation] !== direction - ) { - // Wrong rotation - continue; - } - - // All good, can remove - this.root.logic.tryDeleteBuilding(entityBefore); - this.root.logic.tryDeleteBuilding(entityAfter); - } - } - } - - /** - * Recomputes the cache in the given area, invalidating all entries there - * @param {Rectangle} area - */ - recomputeArea(area) { - for (let x = area.x; x < area.right(); ++x) { - for (let y = area.y; y < area.bottom(); ++y) { - const entities = this.root.map.getLayersContentsMultipleXY(x, y); - for (let i = 0; i < entities.length; ++i) { - const entity = entities[i]; - const undergroundComp = entity.components.UndergroundBelt; - if (!undergroundComp) { - continue; - } - undergroundComp.cachedLinkedEntity = null; - } - } - } - } - - update() { - this.staleAreaWatcher.update(); - - const sender = enumUndergroundBeltMode.sender; - const now = this.root.time.now(); - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - const undergroundComp = entity.components.UndergroundBelt; - if (undergroundComp.mode === sender) { - this.handleSender(entity); - } else { - this.handleReceiver(entity, now); - } - } - } - - /** - * Finds the receiver for a given sender - * @param {Entity} entity - * @returns {import("../components/underground_belt").LinkedUndergroundBelt} - */ - findRecieverForSender(entity) { - const staticComp = entity.components.StaticMapEntity; - const undergroundComp = entity.components.UndergroundBelt; - const searchDirection = staticComp.localDirectionToWorld(enumDirection.top); - const searchVector = enumDirectionToVector[searchDirection]; - const targetRotation = enumDirectionToAngle[searchDirection]; - let currentTile = staticComp.origin; - - // Search in the direction of the tunnel - for ( - let searchOffset = 0; - searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; - ++searchOffset - ) { - currentTile = currentTile.add(searchVector); - - const potentialReceiver = this.root.map.getTileContent(currentTile, "regular"); - if (!potentialReceiver) { - // Empty tile - continue; - } - const receiverUndergroundComp = potentialReceiver.components.UndergroundBelt; - if (!receiverUndergroundComp || receiverUndergroundComp.tier !== undergroundComp.tier) { - // Not a tunnel, or not on the same tier - continue; - } - - const receiverStaticComp = potentialReceiver.components.StaticMapEntity; - if (receiverStaticComp.rotation !== targetRotation) { - // Wrong rotation - continue; - } - - if (receiverUndergroundComp.mode !== enumUndergroundBeltMode.receiver) { - // Not a receiver, but a sender -> Abort to make sure we don't deliver double - break; - } - - return { entity: potentialReceiver, distance: searchOffset }; - } - - // None found - return { entity: null, distance: 0 }; - } - - /** - * - * @param {Entity} entity - */ - handleSender(entity) { - const undergroundComp = entity.components.UndergroundBelt; - - // Find the current receiver - let cacheEntry = undergroundComp.cachedLinkedEntity; - if (!cacheEntry) { - // Need to recompute cache - cacheEntry = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity); - } - - if (!cacheEntry.entity) { - // If there is no connection to a receiver, ignore this one - return; - } - - // Check if we have any items to eject - const nextItemAndDuration = undergroundComp.pendingItems[0]; - if (nextItemAndDuration) { - assert(undergroundComp.pendingItems.length === 1, "more than 1 pending"); - - // Check if the receiver can accept it - if ( - cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem( - nextItemAndDuration[0], - cacheEntry.distance, - this.root.hubGoals.getUndergroundBeltBaseSpeed(), - this.root.time.now() - ) - ) { - // Drop this item - fastArrayDelete(undergroundComp.pendingItems, 0); - } - } - } - - /** - * - * @param {Entity} entity - * @param {number} now - */ - handleReceiver(entity, now) { - const undergroundComp = entity.components.UndergroundBelt; - - // Try to eject items, we only check the first one because it is sorted by remaining time - const nextItemAndDuration = undergroundComp.pendingItems[0]; - if (nextItemAndDuration) { - if (now > nextItemAndDuration[1]) { - const ejectorComp = entity.components.ItemEjector; - - const nextSlotIndex = ejectorComp.getFirstFreeSlot(); - if (nextSlotIndex !== null) { - if (ejectorComp.tryEject(nextSlotIndex, nextItemAndDuration[0])) { - undergroundComp.pendingItems.shift(); - } - } - } - } - } -} +import { globalConfig } from "../../core/config"; +import { Loader } from "../../core/loader"; +import { createLogger } from "../../core/logging"; +import { Rectangle } from "../../core/rectangle"; +import { StaleAreaDetector } from "../../core/stale_area_detector"; +import { fastArrayDelete } from "../../core/utils"; +import { + enumAngleToDirection, + enumDirection, + enumDirectionToAngle, + enumDirectionToVector, + enumInvertedDirections, +} from "../../core/vector"; +import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt"; +import { Entity } from "../entity"; +import { GameSystemWithFilter } from "../game_system_with_filter"; + +const logger = createLogger("tunnels"); + +export class UndergroundBeltSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [UndergroundBeltComponent]); + + this.beltSprites = { + [enumUndergroundBeltMode.sender]: Loader.getSprite( + "sprites/buildings/underground_belt_entry.png" + ), + [enumUndergroundBeltMode.receiver]: Loader.getSprite( + "sprites/buildings/underground_belt_exit.png" + ), + }; + + this.staleAreaWatcher = new StaleAreaDetector({ + root: this.root, + name: "underground-belt", + recomputeMethod: this.recomputeArea.bind(this), + }); + + this.root.signals.entityManuallyPlaced.add(this.onEntityManuallyPlaced, this); + + // NOTICE: Once we remove a tunnel, we need to update the whole area to + // clear outdated handles + this.staleAreaWatcher.recomputeOnComponentsChanged( + [UndergroundBeltComponent], + globalConfig.undergroundBeltMaxTilesByTier[globalConfig.undergroundBeltMaxTilesByTier.length - 1] + ); + } + + /** + * Callback when an entity got placed, used to remove belts between underground belts + * @param {Entity} entity + */ + onEntityManuallyPlaced(entity) { + if (!this.root.app.settings.getAllSettings().enableTunnelSmartplace) { + // Smart-place disabled + return; + } + + const undergroundComp = entity.components.UndergroundBelt; + if (undergroundComp && undergroundComp.mode === enumUndergroundBeltMode.receiver) { + const staticComp = entity.components.StaticMapEntity; + const tile = staticComp.origin; + + const direction = enumAngleToDirection[staticComp.rotation]; + const inverseDirection = enumInvertedDirections[direction]; + const offset = enumDirectionToVector[inverseDirection]; + + let currentPos = tile.copy(); + + const tier = undergroundComp.tier; + const range = globalConfig.undergroundBeltMaxTilesByTier[tier]; + + // FIND ENTRANCE + // Search for the entrance which is farthest apart (this is why we can't reuse logic here) + let matchingEntrance = null; + for (let i = 0; i < range; ++i) { + currentPos.addInplace(offset); + const contents = this.root.map.getTileContent(currentPos, entity.layer); + if (!contents) { + continue; + } + + const contentsUndergroundComp = contents.components.UndergroundBelt; + const contentsStaticComp = contents.components.StaticMapEntity; + if ( + contentsUndergroundComp && + contentsUndergroundComp.tier === undergroundComp.tier && + contentsUndergroundComp.mode === enumUndergroundBeltMode.sender && + enumAngleToDirection[contentsStaticComp.rotation] === direction + ) { + matchingEntrance = { + entity: contents, + range: i, + }; + } + } + + if (!matchingEntrance) { + // Nothing found + return; + } + + // DETECT OBSOLETE BELTS BETWEEN + // Remove any belts between entrance and exit which have the same direction, + // but only if they *all* have the right direction + currentPos = tile.copy(); + let allBeltsMatch = true; + for (let i = 0; i < matchingEntrance.range; ++i) { + currentPos.addInplace(offset); + + const contents = this.root.map.getTileContent(currentPos, entity.layer); + if (!contents) { + allBeltsMatch = false; + break; + } + + const contentsStaticComp = contents.components.StaticMapEntity; + const contentsBeltComp = contents.components.Belt; + if (!contentsBeltComp) { + allBeltsMatch = false; + break; + } + + // It's a belt + if ( + contentsBeltComp.direction !== enumDirection.top || + enumAngleToDirection[contentsStaticComp.rotation] !== direction + ) { + allBeltsMatch = false; + break; + } + } + + currentPos = tile.copy(); + if (allBeltsMatch) { + // All belts between this are obsolete, so drop them + for (let i = 0; i < matchingEntrance.range; ++i) { + currentPos.addInplace(offset); + const contents = this.root.map.getTileContent(currentPos, entity.layer); + assert(contents, "Invalid smart underground belt logic"); + this.root.logic.tryDeleteBuilding(contents); + } + } + + // REMOVE OBSOLETE TUNNELS + // Remove any double tunnels, by checking the tile plus the tile above + currentPos = tile.copy().add(offset); + for (let i = 0; i < matchingEntrance.range - 1; ++i) { + const posBefore = currentPos.copy(); + currentPos.addInplace(offset); + + const entityBefore = this.root.map.getTileContent(posBefore, entity.layer); + const entityAfter = this.root.map.getTileContent(currentPos, entity.layer); + + if (!entityBefore || !entityAfter) { + continue; + } + + const undergroundBefore = entityBefore.components.UndergroundBelt; + const undergroundAfter = entityAfter.components.UndergroundBelt; + + if (!undergroundBefore || !undergroundAfter) { + // Not an underground belt + continue; + } + + if ( + // Both same tier + undergroundBefore.tier !== undergroundAfter.tier || + // And same tier as our original entity + undergroundBefore.tier !== undergroundComp.tier + ) { + // Mismatching tier + continue; + } + + if ( + undergroundBefore.mode !== enumUndergroundBeltMode.sender || + undergroundAfter.mode !== enumUndergroundBeltMode.receiver + ) { + // Not the right mode + continue; + } + + // Check rotations + const staticBefore = entityBefore.components.StaticMapEntity; + const staticAfter = entityAfter.components.StaticMapEntity; + + if ( + enumAngleToDirection[staticBefore.rotation] !== direction || + enumAngleToDirection[staticAfter.rotation] !== direction + ) { + // Wrong rotation + continue; + } + + // All good, can remove + this.root.logic.tryDeleteBuilding(entityBefore); + this.root.logic.tryDeleteBuilding(entityAfter); + } + } + } + + /** + * Recomputes the cache in the given area, invalidating all entries there + * @param {Rectangle} area + */ + recomputeArea(area) { + for (let x = area.x; x < area.right(); ++x) { + for (let y = area.y; y < area.bottom(); ++y) { + const entities = this.root.map.getLayersContentsMultipleXY(x, y); + for (let i = 0; i < entities.length; ++i) { + const entity = entities[i]; + const undergroundComp = entity.components.UndergroundBelt; + if (!undergroundComp) { + continue; + } + undergroundComp.cachedLinkedEntity = null; + } + } + } + } + + update() { + this.staleAreaWatcher.update(); + + const sender = enumUndergroundBeltMode.sender; + const now = this.root.time.now(); + + for (let i = this.allEntitiesArray.length - 1; i >= 0; --i) { + const entity = this.allEntitiesArray[i]; + const undergroundComp = entity.components.UndergroundBelt; + if (undergroundComp.mode === sender) { + this.handleSender(entity); + } else { + this.handleReceiver(entity, now); + } + } + } + + /** + * Finds the receiver for a given sender + * @param {Entity} entity + * @returns {import("../components/underground_belt").LinkedUndergroundBelt} + */ + findRecieverForSender(entity) { + const staticComp = entity.components.StaticMapEntity; + const undergroundComp = entity.components.UndergroundBelt; + const searchDirection = staticComp.localDirectionToWorld(enumDirection.top); + const searchVector = enumDirectionToVector[searchDirection]; + const targetRotation = enumDirectionToAngle[searchDirection]; + let currentTile = staticComp.origin; + + // Search in the direction of the tunnel + for ( + let searchOffset = 0; + searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; + ++searchOffset + ) { + currentTile = currentTile.add(searchVector); + + const potentialReceiver = this.root.map.getTileContent(currentTile, "regular"); + if (!potentialReceiver) { + // Empty tile + continue; + } + const receiverUndergroundComp = potentialReceiver.components.UndergroundBelt; + if (!receiverUndergroundComp || receiverUndergroundComp.tier !== undergroundComp.tier) { + // Not a tunnel, or not on the same tier + continue; + } + + const receiverStaticComp = potentialReceiver.components.StaticMapEntity; + if (receiverStaticComp.rotation !== targetRotation) { + // Wrong rotation + continue; + } + + if (receiverUndergroundComp.mode !== enumUndergroundBeltMode.receiver) { + // Not a receiver, but a sender -> Abort to make sure we don't deliver double + break; + } + + return { entity: potentialReceiver, distance: searchOffset }; + } + + // None found + return { entity: null, distance: 0 }; + } + + /** + * + * @param {Entity} entity + */ + handleSender(entity) { + const undergroundComp = entity.components.UndergroundBelt; + + // Find the current receiver + let cacheEntry = undergroundComp.cachedLinkedEntity; + if (!cacheEntry) { + // Need to recompute cache + cacheEntry = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity); + } + + if (!cacheEntry.entity) { + // If there is no connection to a receiver, ignore this one + return; + } + + // Check if we have any items to eject + const nextItemAndDuration = undergroundComp.pendingItems[0]; + if (nextItemAndDuration) { + assert(undergroundComp.pendingItems.length === 1, "more than 1 pending"); + + // Check if the receiver can accept it + if ( + cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem( + nextItemAndDuration[0], + cacheEntry.distance, + this.root.hubGoals.getUndergroundBeltBaseSpeed(), + this.root.time.now() + ) + ) { + // Drop this item + fastArrayDelete(undergroundComp.pendingItems, 0); + } + } + } + + /** + * + * @param {Entity} entity + * @param {number} now + */ + handleReceiver(entity, now) { + const undergroundComp = entity.components.UndergroundBelt; + + // Try to eject items, we only check the first one because it is sorted by remaining time + const nextItemAndDuration = undergroundComp.pendingItems[0]; + if (nextItemAndDuration) { + if (now > nextItemAndDuration[1]) { + const ejectorComp = entity.components.ItemEjector; + + const nextSlotIndex = ejectorComp.getFirstFreeSlot(); + if (nextSlotIndex !== null) { + if (ejectorComp.tryEject(nextSlotIndex, nextItemAndDuration[0])) { + undergroundComp.pendingItems.shift(); + } + } + } + } + } +} diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 36ed884f..c67fd5e2 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -1,3 +1,4 @@ +// @ts-nocheck import { ReadWriteProxy } from "../core/read_write_proxy"; import { ExplainedResult } from "../core/explained_result"; import { SavegameSerializer } from "./savegame_serializer"; @@ -208,7 +209,12 @@ export class Savegame extends ReadWriteProxy { * Returns if this game has a serialized game dump */ hasGameDump() { - return !!this.currentData.dump && this.currentData.dump.entities.length > 0; + if (!this.currentData.dump) return false; + if (Array.isArray(this.currentData.dump.entities)) { + return this.currentData.dump.entities.length; + } else { + return this.currentData.dump.entities.size; + } } /** diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index 3230cdd5..508596c7 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -70,9 +70,10 @@ export class SavegameSerializer { const seenUids = new Set(); // Check for duplicate UIDS - for (let i = 0; i < savegame.entities.length; ++i) { + const entities = [...savegame.entities.values()]; + for (let i = entities.length - 1; i >= 0; --i) { /** @type {Entity} */ - const entity = savegame.entities[i]; + const entity = entities[i]; const uid = entity.uid; if (!Number.isInteger(uid)) { diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index c5e0e5c5..75e3ff06 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -16,7 +16,7 @@ * hubGoals: any, * pinnedShapes: any, * waypoints: any, - * entities: Array, + * entities: Array|Set, * beltPaths: Array * }} SerializedGame * diff --git a/src/js/savegame/schemas/1001.js b/src/js/savegame/schemas/1001.js index af86b09d..658a5a58 100644 --- a/src/js/savegame/schemas/1001.js +++ b/src/js/savegame/schemas/1001.js @@ -40,7 +40,7 @@ export class SavegameInterface_V1001 extends SavegameInterface_V1000 { ], }; - const entities = dump.entities; + const entities = Array.isArray(dump.entities) ? dump.entities : [...dump.entities.values()]; for (let i = 0; i < entities.length; ++i) { const entity = entities[i]; diff --git a/src/js/savegame/schemas/1002.js b/src/js/savegame/schemas/1002.js index 866bc1e8..e2351582 100644 --- a/src/js/savegame/schemas/1002.js +++ b/src/js/savegame/schemas/1002.js @@ -24,7 +24,7 @@ export class SavegameInterface_V1002 extends SavegameInterface_V1001 { return true; } - const entities = dump.entities; + const entities = Array.isArray(dump.entities) ? dump.entities : [...dump.entities.values()]; for (let i = 0; i < entities.length; ++i) { const entity = entities[i]; const beltComp = entity.components.Belt; diff --git a/src/js/savegame/schemas/1005.js b/src/js/savegame/schemas/1005.js index 0380f8eb..1949ee0e 100644 --- a/src/js/savegame/schemas/1005.js +++ b/src/js/savegame/schemas/1005.js @@ -26,7 +26,7 @@ export class SavegameInterface_V1005 extends SavegameInterface_V1004 { // just reset belt paths for now dump.beltPaths = []; - const entities = dump.entities; + const entities = Array.isArray(dump.entities) ? dump.entities : [...dump.entities.values()]; // clear ejector slots for (let i = 0; i < entities.length; ++i) { diff --git a/src/js/savegame/schemas/1006.js b/src/js/savegame/schemas/1006.js index 79226772..5a9fd4d8 100644 --- a/src/js/savegame/schemas/1006.js +++ b/src/js/savegame/schemas/1006.js @@ -173,7 +173,8 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 { dump.hubGoals.level = levelMapping[level] || level; // Update entities - const entities = dump.entities; + const entities = Array.isArray(dump.entities) ? dump.entities : [...dump.entities.values()]; + for (let i = 0; i < entities.length; ++i) { const entity = entities[i]; const components = entity.components; @@ -269,8 +270,17 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 { newStaticComp.originalRotation = staticComp.originalRotation; newStaticComp.rotation = staticComp.rotation; + /** + * in one of our files: + * we dont seem to actually have a blueprintspritekey + * but we do have this attribute called code + */ + // @ts-ignore - newStaticComp.code = spriteMapping[staticComp.blueprintSpriteKey]; + if (staticComp.blueprintSpriteKey) { + // @ts-ignore + newStaticComp.code = spriteMapping[staticComp.blueprintSpriteKey]; + } else newStaticComp.code = staticComp.code; // Hub special case if (entity.components.Hub) { @@ -293,9 +303,11 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 { } if (!newStaticComp.code) { + console.dir(entity); + console.dir(staticComp); throw new Error( // @ts-ignore - "1006 Migration: Could not reconstruct code for " + staticComp.blueprintSpriteKey + "1006 Migration: Could not reconstruct code for " + code ); } diff --git a/src/js/savegame/serializer_internal.js b/src/js/savegame/serializer_internal.js index c75cebad..7ed1ee32 100644 --- a/src/js/savegame/serializer_internal.js +++ b/src/js/savegame/serializer_internal.js @@ -11,12 +11,15 @@ const logger = createLogger("serializer_internal"); export class SerializerInternal { /** * Serializes an array of entities - * @param {Array} array + * @param {Array|Set} array */ serializeEntityArray(array) { const serialized = []; - for (let i = 0; i < array.length; ++i) { - const entity = array[i]; + + const arr = Array.isArray(array) ? array : [...array.values()]; + + for (let i = 0; i < arr.length; ++i) { + const entity = arr[i]; if (!entity.queuedForDestroy && !entity.destroyed) { serialized.push(entity.serialize()); } diff --git a/src/js/tslint.json b/src/js/tslint.json index c89e7770..4d554235 100644 --- a/src/js/tslint.json +++ b/src/js/tslint.json @@ -10,7 +10,8 @@ "no-console": false, "forin": false, "no-empty": false, - "space-before-function-paren": ["always"] + "space-before-function-paren": ["always"], + "no-unused-declaration": true }, "rulesDirectory": [] } diff --git a/src/js/webworkers/tsconfig.json b/src/js/webworkers/tsconfig.json index dce06856..3a428cfc 100644 --- a/src/js/webworkers/tsconfig.json +++ b/src/js/webworkers/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": ["ES2018","WebWorker"] + "lib": ["ES2018", "WebWorker"] }, "exclude": [], "extends": "../tsconfig",