diff --git a/src/js/core/config.js b/src/js/core/config.js index 6825e762..0018b746 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -51,7 +51,7 @@ export const globalConfig = { // Belt speeds // NOTICE: Update webpack.production.config too! beltSpeedItemsPerSecond: 2, - itemSpacingOnBelts: 0.8, + itemSpacingOnBelts: 0.63, minerSpeedItemsPerSecond: 0, // COMPUTED undergroundBeltMaxTilesByTier: [5, 8], diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js new file mode 100644 index 00000000..04b74d11 --- /dev/null +++ b/src/js/game/belt_path.js @@ -0,0 +1,638 @@ +import { globalConfig } from "../core/config"; +import { DrawParameters } from "../core/draw_parameters"; +import { Vector } from "../core/vector"; +import { BaseItem } from "./base_item"; +import { Entity } from "./entity"; +import { GameRoot } from "./root"; +import { round4Digits, epsilonCompare } from "../core/utils"; +import { Math_min } from "../core/builtins"; +import { createLogger, logSection } from "../core/logging"; + +const logger = createLogger("belt_path"); + +// Helpers for more semantic access into interleaved arrays +const NEXT_ITEM_OFFSET_INDEX = 0; +const ITEM_INDEX = 1; + +/** + * Stores a path of belts, used for optimizing performance + */ +export class BeltPath { + /** + * @param {GameRoot} root + * @param {Array} entityPath + */ + constructor(root, entityPath) { + this.root = root; + + assert(entityPath.length > 0, "invalid entity path"); + this.entityPath = entityPath; + + /** + * Stores the items sorted, and their distance to the previous item (or start) + * Layout: [distanceToNext, item] + * @type {Array<[number, BaseItem]>} + */ + this.items = []; + + /** + * Stores the spacing to the first item + */ + + // Find acceptor and ejector + + this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; + this.initialBeltComponent = this.entityPath[0].components.Belt; + + this.totalLength = this.computeTotalLength(); + this.spacingToFirstItem = this.totalLength; + + // Connect the belts + for (let i = 0; i < this.entityPath.length; ++i) { + this.entityPath[i].components.Belt.assignedPath = this; + } + + this.debug_checkIntegrity("constructor"); + } + + /** + * Helper to throw an error on mismatch + * @param {string} change + * @param {Array} reason + */ + debug_failIntegrity(change, ...reason) { + throw new Error("belt path invalid (" + change + "): " + reason.map(i => "" + i).join(" ")); + } + + /** + * Checks if this path is valid + */ + debug_checkIntegrity(currentChange = "change") { + if (!G_IS_DEV) { + return; + } + + const fail = (...args) => this.debug_failIntegrity(currentChange, ...args); + + // Check for empty path + if (this.entityPath.length === 0) { + return fail("Belt path is empty"); + } + + // Check for mismatching length + const totalLength = this.computeTotalLength(); + if (this.totalLength !== totalLength) { + return this.debug_failIntegrity( + currentChange, + "Total length mismatch, stored =", + this.totalLength, + "but correct is", + totalLength + ); + } + + // Check for misconnected entities + for (let i = 0; i < this.entityPath.length - 1; ++i) { + const entity = this.entityPath[i]; + const followUp = this.root.systemMgr.systems.belt.findFollowUpEntity(entity); + if (!followUp) { + return fail( + "Follow up entity for the", + i, + "-th entity (total length", + this.entityPath.length, + ") was null!" + ); + } + if (followUp !== this.entityPath[i + 1]) { + return fail( + "Follow up entity mismatch, stored is", + this.entityPath[i + 1].uid, + "but real one is", + followUp.uid + ); + } + if (entity.components.Belt.assignedPath !== this) { + return fail( + "Entity with uid", + entity.uid, + "doesn't have this path assigned, but this path contains the entity." + ); + } + } + + // Check for right ejector component and slot + if (this.ejectorComp !== this.entityPath[this.entityPath.length - 1].components.ItemEjector) { + return fail("Stale ejectorComp handle"); + } + if (this.ejectorSlot !== this.ejectorComp.slots[0]) { + return fail("Stale ejector slot handle"); + } + if (!this.ejectorComp) { + return fail("Ejector comp not set"); + } + if (!this.ejectorSlot) { + return fail("Ejector slot not set"); + } + if (this.initialBeltComponent !== this.entityPath[0].components.Belt) { + return fail("Stale initial belt component handle"); + } + + // Check spacing + if (this.spacingToFirstItem > this.totalLength + 0.005) { + return fail( + currentChange, + "spacing to first item (", + this.spacingToFirstItem, + ") is greater than total length (", + this.totalLength, + ")" + ); + } + + // Check distance if empty + if (this.items.length === 0 && !epsilonCompare(this.spacingToFirstItem, this.totalLength)) { + return fail( + currentChange, + "Path is empty but spacing to first item (", + this.spacingToFirstItem, + ") does not equal total length (", + this.totalLength, + ")" + ); + } + + // Check items etc + let currentPos = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + + if (item[NEXT_ITEM_OFFSET_INDEX] < 0 || item[NEXT_ITEM_OFFSET_INDEX] > this.totalLength) { + return fail( + "Item has invalid offset to next item: ", + item[0], + "(total length:", + this.totalLength, + ")" + ); + } + + currentPos += item[0]; + } + + // Check the total sum matches + if (!epsilonCompare(currentPos, this.totalLength)) { + return fail( + "total sum (", + currentPos, + ") of first item spacing (", + this.spacingToFirstItem, + ") and items does not match total length (", + this.totalLength, + ")" + ); + } + } + + /** + * Extends the belt path by the given belt + * @param {Entity} entity + */ + extendOnEnd(entity) { + logger.log("Extending belt path by entity at", entity.components.StaticMapEntity.origin); + + const beltComp = entity.components.Belt; + + // If the last belt has something on its ejector, put that into the path first + const pendingItem = this.ejectorComp.takeSlotItem(0); + if (pendingItem) { + // Ok, so we have a pending item + logger.log("Taking pending item and putting it back on the path"); + this.items.push([0, pendingItem]); + } + + // Append the entity + this.entityPath.push(entity); + + // Extend the path length + const additionalLength = beltComp.getEffectiveLengthTiles(); + this.totalLength += additionalLength; + logger.log(" Extended total length by", additionalLength, "to", this.totalLength); + + // If we have no item, just update the distance to the first item + if (this.items.length === 0) { + this.spacingToFirstItem = this.totalLength; + logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); + } else { + // Otherwise, update the next-distance of the last item + const lastItem = this.items[this.items.length - 1]; + logger.log( + " Extended spacing of last item from", + lastItem[NEXT_ITEM_OFFSET_INDEX], + "to", + lastItem[NEXT_ITEM_OFFSET_INDEX] + additionalLength + ); + lastItem[NEXT_ITEM_OFFSET_INDEX] += additionalLength; + } + + // Update handles + this.ejectorComp = entity.components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; + + // Assign reference + beltComp.assignedPath = this; + + this.debug_checkIntegrity("extend-on-end"); + } + + /** + * Extends the path with the given entity on the beginning + * @param {Entity} entity + */ + extendOnBeginning(entity) { + const beltComp = entity.components.Belt; + + logger.log("Extending the path on the beginning"); + + // All items on that belt are simply lost (for now) + + const length = beltComp.getEffectiveLengthTiles(); + + // Extend the length of this path + this.totalLength += length; + + // Simply adjust the first item spacing cuz we have no items contained + this.spacingToFirstItem += length; + + // Set handles and append entity + beltComp.assignedPath = this; + this.initialBeltComponent = this.entityPath[0].components.Belt; + this.entityPath.unshift(entity); + + this.debug_checkIntegrity("extend-on-begin"); + } + + /** + * Splits this path at the given entity by removing it, and + * returning the new secondary paht + * @param {Entity} entity + * @returns {BeltPath} + */ + deleteEntityOnPathSplitIntoTwo(entity) { + logger.log("Splitting path at entity", entity.components.StaticMapEntity.origin); + + // First, find where the current path ends + const beltComp = entity.components.Belt; + 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"); + + let firstPathEntityCount = 0; + let firstPathLength = 0; + let firstPathEndEntity = null; + + for (let i = 0; i < this.entityPath.length; ++i) { + const otherEntity = this.entityPath[i]; + if (otherEntity === entity) { + logger.log("Found entity at", i, "of length", firstPathLength); + break; + } + + ++firstPathEntityCount; + firstPathEndEntity = otherEntity; + firstPathLength += otherEntity.components.Belt.getEffectiveLengthTiles(); + } + + logger.log( + "First path ends at", + firstPathLength, + "and entity", + firstPathEndEntity.components.StaticMapEntity.origin, + "and has", + firstPathEntityCount, + "entities" + ); + + // Compute length of second path + const secondPathLength = this.totalLength - firstPathLength - entityLength; + const secondPathStart = firstPathLength + entityLength; + const secondEntities = this.entityPath.splice(firstPathEntityCount + 1); + logger.log( + "Second path starts at", + secondPathStart, + "and has a length of ", + secondPathLength, + "with", + secondEntities.length, + "entities" + ); + + // Remove the last item + this.entityPath.pop(); + + logger.log("Splitting", this.items.length, "items"); + logger.log( + "Old items are", + this.items.map(i => i[NEXT_ITEM_OFFSET_INDEX]) + ); + + // Create second path + const secondPath = new BeltPath(this.root, secondEntities); + + // Remove all items which are no longer relevant and transfer them to the second path + let itemPos = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + const distanceToNext = item[NEXT_ITEM_OFFSET_INDEX]; + + logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); + + // Check if this item is past the first path + if (itemPos >= firstPathLength) { + // Remove it from the first path + this.items.splice(i, 1); + i -= 1; + logger.log(" Removed item from first path since its no longer contained @", itemPos); + + // Check if its on the second path (otherwise its on the removed belt and simply lost) + if (itemPos >= secondPathStart) { + // Put item on second path + secondPath.items.push([distanceToNext, item[ITEM_INDEX]]); + logger.log( + " Put item to second path @", + itemPos, + "with distance to next =", + distanceToNext + ); + + // If it was the first item, adjust the distance to the first item + if (secondPath.items.length === 1) { + logger.log(" Sinc it was the first, set sapcing of first to", itemPos); + secondPath.spacingToFirstItem = itemPos - secondPathStart; + } + } else { + logger.log(" Item was on the removed belt, so its gone - forever!"); + } + } else { + // Seems this item is on the first path (so all good), so just make sure it doesn't + // have a nextDistance which is bigger than the total path length + const clampedDistanceToNext = Math_min(itemPos + distanceToNext, firstPathLength) - itemPos; + if (clampedDistanceToNext < distanceToNext) { + logger.log( + "Correcting next distance (first path) from", + distanceToNext, + "to", + clampedDistanceToNext + ); + item[NEXT_ITEM_OFFSET_INDEX] = clampedDistanceToNext; + } + } + + // Advance items + itemPos += distanceToNext; + } + + logger.log( + "New items are", + this.items.map(i => i[0]) + ); + + logger.log( + "And second path items are", + secondPath.items.map(i => i[0]) + ); + + // Adjust our total length + this.totalLength = firstPathLength; + + // Make sure that if we are empty, we set our first distance properly + if (this.items.length === 0) { + this.spacingToFirstItem = this.totalLength; + } + + // Set new ejector and acceptor handles + this.ejectorComp = firstPathEndEntity.components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; + + this.debug_checkIntegrity("split-two-first"); + secondPath.debug_checkIntegrity("split-two-second"); + + return secondPath; + } + + /** + * Extends the path by the given other path + * @param {BeltPath} otherPath + */ + extendByPath(otherPath) { + const entities = otherPath.entityPath; + logger.log("Extending path by other path, starting to add entities"); + const oldLength = this.totalLength; + const oldLastItem = this.items[this.items.length - 1]; + + for (let i = 0; i < entities.length; ++i) { + this.extendOnEnd(entities[i]); + } + + logger.log(" Transferring new items:", otherPath.items); + + // Check if we have no items and thus need to adjust the spacing + if (this.items.length === 0) { + // This one is easy - Since our first path is empty, we can just + // set the spacing to the first one to the whole first part length + // and add the spacing on the second path (Which might be the whole second part + // length if its entirely empty, too) + this.spacingToFirstItem = this.totalLength + otherPath.spacingToFirstItem; + logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); + + // Simply copy over all items + for (let i = 0; i < otherPath.items.length; ++i) { + const item = otherPath.items[0]; + this.items.push([item[0], item[1]]); + } + } else { + console.error("TODO4"); + + // Adjust the distance from our last item to the first item of the second path. + // First, find the absolute position of the first item: + let itemPosition = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + itemPosition += this.items[i][0]; + } + } + + this.debug_checkIntegrity("extend-by-path"); + } + + /** + * Computes the total length of the path + * @returns {number} + */ + computeTotalLength() { + let length = 0; + for (let i = 0; i < this.entityPath.length; ++i) { + length += this.entityPath[i].components.Belt.getEffectiveLengthTiles(); + } + return length; + } + + /** + * Performs one tick + */ + update() { + this.debug_checkIntegrity("pre-update"); + const firstBeltItems = this.initialBeltComponent.sortedItems; + const transferItemAndProgress = firstBeltItems[0]; + + // Check if the first belt took a new item + if (transferItemAndProgress) { + const transferItem = transferItemAndProgress[1]; + + if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { + // Can take new item + firstBeltItems.splice(0, 1); + + this.items.unshift([this.spacingToFirstItem, transferItem]); + this.spacingToFirstItem = 0; + } + } + + // Divide by item spacing on belts since we use throughput and not speed + let beltSpeed = + this.root.hubGoals.getBeltBaseSpeed() * + this.root.dynamicTickrate.deltaSeconds * + globalConfig.itemSpacingOnBelts; + + if (G_IS_DEV && globalConfig.debug.instantBelts) { + beltSpeed *= 100; + } + + let minimumDistance = this.ejectorSlot.item ? globalConfig.itemSpacingOnBelts : 0; + + // Try to reduce spacing + let remainingAmount = beltSpeed; + for (let i = this.items.length - 1; i >= 0; --i) { + const nextDistanceAndItem = this.items[i]; + const minimumSpacing = minimumDistance; + + const takeAway = Math.max(0, Math.min(remainingAmount, nextDistanceAndItem[0] - minimumSpacing)); + + remainingAmount -= takeAway; + nextDistanceAndItem[0] -= takeAway; + + this.spacingToFirstItem += takeAway; + if (remainingAmount === 0.0) { + break; + } + + minimumDistance = globalConfig.itemSpacingOnBelts; + } + + const lastItem = this.items[this.items.length - 1]; + if (lastItem && lastItem[0] === 0.0) { + // Take over + if (this.ejectorComp.tryEject(0, lastItem[1])) { + this.items.pop(); + } + } + + this.debug_checkIntegrity("post-update"); + } + + /** + * Computes a world space position from the given progress + * @param {number} progress + * @returns {Vector} + */ + computePositionFromProgress(progress) { + let currentLength = 0; + + // floating point issuses .. + assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress); + + for (let i = 0; i < this.entityPath.length; ++i) { + const beltComp = this.entityPath[i].components.Belt; + const localLength = beltComp.getEffectiveLengthTiles(); + + if (currentLength + localLength >= progress || i === this.entityPath.length - 1) { + // Min required here due to floating point issues + const localProgress = Math_min(1.0, progress - currentLength); + + assert(localProgress >= 0.0, "Invalid local progress: " + localProgress); + const localSpace = beltComp.transformBeltToLocalSpace(localProgress); + return this.entityPath[i].components.StaticMapEntity.localTileToWorld(localSpace); + } + currentLength += localLength; + } + + assert(false, "invalid progress: " + progress + " (max: " + this.totalLength + ")"); + } + + /** + * + * @param {DrawParameters} parameters + */ + drawDebug(parameters) { + parameters.context.fillStyle = "#d79a25"; + parameters.context.strokeStyle = "#d79a25"; + parameters.context.beginPath(); + + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + const pos = entity.components.StaticMapEntity; + const worldPos = pos.origin.toWorldSpaceCenterOfTile(); + + if (i === 0) { + parameters.context.moveTo(worldPos.x, worldPos.y); + } else { + parameters.context.lineTo(worldPos.x, worldPos.y); + } + } + parameters.context.stroke(); + + // Items + let progress = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const nextDistanceAndItem = this.items[i]; + const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "#268e4d"; + parameters.context.beginRoundedRect(worldPos.x - 5, worldPos.y - 5, 10, 10, 3); + parameters.context.fill(); + parameters.context.font = "6px GameFont"; + parameters.context.fillStyle = "#111"; + parameters.context.fillText( + "" + round4Digits(nextDistanceAndItem[0]), + worldPos.x + 5, + worldPos.y + 2 + ); + progress += nextDistanceAndItem[0]; + nextDistanceAndItem[1].draw(worldPos.x, worldPos.y, parameters, 10); + } + + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + parameters.context.fillStyle = "#d79a25"; + const pos = entity.components.StaticMapEntity; + const worldPos = pos.origin.toWorldSpaceCenterOfTile(); + parameters.context.beginCircle(worldPos.x, worldPos.y, i === 0 ? 5 : 3); + parameters.context.fill(); + } + + for (let progress = 0; progress <= this.totalLength + 0.01; progress += 0.2) { + const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "red"; + parameters.context.beginCircle(worldPos.x, worldPos.y, 1); + parameters.context.fill(); + } + + const firstItemIndicator = this.computePositionFromProgress( + this.spacingToFirstItem + ).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "purple"; + parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2); + } +} diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index a9be5c99..4d5fa16c 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -6,6 +6,9 @@ import { Vector, enumDirection } from "../../core/vector"; import { Math_PI, Math_sin, Math_cos } from "../../core/builtins"; import { globalConfig } from "../../core/config"; import { Entity } from "../entity"; +import { BeltPath } from "../belt_path"; + +export const curvedBeltLength = /* Math_PI / 4 */ 0.78; export class BeltComponent extends Component { static getId() { @@ -39,6 +42,20 @@ export class BeltComponent extends Component { /** @type {Entity} */ this.followUpCache = null; + + /** + * The path this belt is contained in, not serialized + * @type {BeltPath} + */ + this.assignedPath = null; + } + + /** + * Returns the effective length of this belt in tile space + * @returns {number} + */ + getEffectiveLengthTiles() { + return this.direction === enumDirection.top ? 1.0 : curvedBeltLength; } /** @@ -50,14 +67,17 @@ export class BeltComponent extends Component { transformBeltToLocalSpace(progress) { switch (this.direction) { case enumDirection.top: + assert(progress <= 1.02, "Invalid progress: " + progress); return new Vector(0, 0.5 - progress); case enumDirection.right: { - const arcProgress = progress * 0.5 * Math_PI; + assert(progress <= curvedBeltLength + 0.02, "Invalid progress 2: " + progress); + const arcProgress = (progress / curvedBeltLength) * 0.5 * Math_PI; return new Vector(0.5 - 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress)); } case enumDirection.left: { - const arcProgress = progress * 0.5 * Math_PI; + assert(progress <= curvedBeltLength + 0.02, "Invalid progress 3: " + progress); + const arcProgress = (progress / curvedBeltLength) * 0.5 * Math_PI; return new Vector(-0.5 + 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress)); } default: diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index e78faa36..7ae97724 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -194,4 +194,17 @@ export class ItemEjectorComponent extends Component { this.slots[slotIndex].progress = this.instantEject ? 1 : 0; return true; } + + /** + * Clears the given slot and returns the item it had + * @param {number} slotIndex + * @returns {BaseItem|null} + */ + takeSlotItem(slotIndex) { + const slot = this.slots[slotIndex]; + const item = slot.item; + slot.item = null; + slot.progress = 0.0; + return item; + } } diff --git a/src/js/game/core.js b/src/js/game/core.js index 203e005b..e16a57a8 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -418,6 +418,10 @@ export class GameCore { root.map.drawStaticEntityDebugOverlays(params); } + if (G_IS_DEV && globalConfig.debug.renderBeltPaths) { + systems.belt.drawBeltPathDebug(params); + } + // END OF GAME CONTENT // ----- diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index f9c28d93..d7c2de51 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -74,8 +74,11 @@ export class GameHUD { shapeViewer: new HUDShapeViewer(this.root), wiresOverlay: new HUDWiresOverlay(this.root), + // Typing hints + /* typehints:start */ /** @type {HUDChangesDebugger} */ changesDebugger: null, + /* typehints:end */ }; this.signals = { diff --git a/src/js/game/hud/parts/debug_changes.js b/src/js/game/hud/parts/debug_changes.js index 46ad5234..1502afa2 100644 --- a/src/js/game/hud/parts/debug_changes.js +++ b/src/js/game/hud/parts/debug_changes.js @@ -27,7 +27,7 @@ export class HUDChangesDebugger extends BaseHUDPart { * @param {string} fillColor Color to display (Hex) * @param {number=} timeToDisplay How long to display the change */ - renderChange(label, area, fillColor, timeToDisplay = 2) { + renderChange(label, area, fillColor, timeToDisplay = 0.3) { this.changes.push({ label, area: area.clone(), diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 5c1bbf66..71c709dc 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -227,6 +227,7 @@ export class GameLogic { } this.root.map.removeStaticEntity(building); this.root.entityMgr.destroyEntity(building); + this.root.entityMgr.processDestroyList(); return true; } diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 5e4ac7f0..04220352 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -1,21 +1,18 @@ -import { Math_sqrt, Math_max } from "../../core/builtins"; +import { Math_sqrt } from "../../core/builtins"; import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; -import { gMetaBuildingRegistry } from "../../core/global_registries"; import { Loader } from "../../core/loader"; import { createLogger } from "../../core/logging"; -import { Rectangle } from "../../core/rectangle"; import { AtlasSprite } from "../../core/sprites"; -import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../../core/vector"; -import { MetaBeltBaseBuilding } from "../buildings/belt_base"; +import { enumDirection, enumDirectionToVector, enumInvertedDirections } from "../../core/vector"; +import { BeltPath } from "../belt_path"; import { BeltComponent } from "../components/belt"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunkView } from "../map_chunk_view"; -import { defaultBuildingVariant } from "../meta_building"; +import { fastArrayDeleteValue } from "../../core/utils"; export const BELT_ANIM_COUNT = 28; -const SQRT_2 = Math_sqrt(2); const logger = createLogger("belt"); @@ -52,10 +49,17 @@ export class BeltSystem extends GameSystemWithFilter { this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); + this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this); + this.root.signals.entityAdded.add(this.onEntityAdded, this); this.root.signals.postLoadHook.add(this.computeBeltCache, this); - /** @type {Rectangle} */ - this.areaToRecompute = null; + // /** @type {Rectangle} */ + // this.areaToRecompute = null; + + /** @type {Array} */ + this.beltPaths = []; + + this.recomputePaths = true; } /** @@ -72,10 +76,8 @@ export class BeltSystem extends GameSystemWithFilter { return; } - if (entity.components.Belt) { - this.cacheNeedsUpdate = true; - } - + // this.recomputePaths = true; + /* const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); // Compute affected area @@ -85,6 +87,8 @@ export class BeltSystem extends GameSystemWithFilter { // Store if anything got changed, if so we need to queue a recompute let anythingChanged = false; + anythingChanged = true; // TODO / FIXME + for (let x = affectedArea.x; x < affectedArea.right(); ++x) { for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { if (!originalRect.containsPoint(x, y)) { @@ -121,6 +125,105 @@ export class BeltSystem extends GameSystemWithFilter { logger.log("Queuing recompute:", this.areaToRecompute); } } + + // FIXME + this.areaToRecompute = new Rectangle(-1000, -1000, 2000, 2000); + */ + } + + /** + * Called when an entity got destroyed + * @param {Entity} entity + */ + onEntityDestroyed(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + console.log("DESTROY"); + + const assignedPath = entity.components.Belt.assignedPath; + assert(assignedPath, "Entity has no belt path assigned"); + + // Find from and to entities + const fromEntity = this.findSupplyingEntity(entity); + const toEntity = this.findFollowUpEntity(entity); + + // Check if the belt had a previous belt + if (fromEntity) { + const fromPath = fromEntity.components.Belt.assignedPath; + + // Check if the entity had a followup - belt + if (toEntity) { + const toPath = toEntity.components.Belt.assignedPath; + assert(fromPath === toPath, "Invalid belt path layout (from path != to path)"); + console.log("Remove inbetween"); + + const newPath = fromPath.deleteEntityOnPathSplitIntoTwo(entity); + this.beltPaths.push(newPath); + } else { + // TODO + console.error("TODO 1"); + } + } else { + if (toEntity) { + // TODO + console.error("TODO 2"); + } else { + // TODO + console.error("TODO 3"); + } + } + } + + /** + * Called when an entity got added + * @param {Entity} entity + */ + onEntityAdded(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + console.log("ADD"); + + const fromEntity = this.findSupplyingEntity(entity); + const toEntity = this.findFollowUpEntity(entity); + + console.log("From:", fromEntity, "to:", toEntity); + + // Check if we can add the entity to the previous path + if (fromEntity) { + const fromPath = fromEntity.components.Belt.assignedPath; + fromPath.extendOnEnd(entity); + + // Check if we now can extend the current path by the next path + if (toEntity) { + const toPath = toEntity.components.Belt.assignedPath; + fromPath.extendByPath(toPath); + + // Delete now obsolete path + fastArrayDeleteValue(this.beltPaths, toPath); + } + } else { + if (toEntity) { + // Prepend it to the other path + const toPath = toEntity.components.Belt.assignedPath; + toPath.extendOnBeginning(entity); + } else { + // This is an empty belt path + const path = new BeltPath(this.root, [entity]); + this.beltPaths.push(path); + } + } } draw(parameters) { @@ -165,10 +268,47 @@ export class BeltSystem extends GameSystemWithFilter { return null; } + /** + * Finds the supplying belt for a given belt. Used for building the dependencies + * @param {Entity} entity + */ + findSupplyingEntity(entity) { + const staticComp = entity.components.StaticMapEntity; + + const supplyDirection = staticComp.localDirectionToWorld(enumDirection.bottom); + const supplyVector = enumDirectionToVector[supplyDirection]; + + const supplyTile = staticComp.origin.add(supplyVector); + const supplyEntity = this.root.map.getTileContent(supplyTile); + + // Check if theres a belt at the tile we point to + if (supplyEntity) { + const supplyBeltComp = supplyEntity.components.Belt; + if (supplyBeltComp) { + const supplyStatic = supplyEntity.components.StaticMapEntity; + const supplyEjector = supplyEntity.components.ItemEjector; + + // Check if the belt accepts items from our direction + const ejectorSlots = supplyEjector.slots; + for (let i = 0; i < ejectorSlots.length; ++i) { + const slot = ejectorSlots[i]; + const localDirection = supplyStatic.localDirectionToWorld(slot.direction); + if (enumInvertedDirections[localDirection] === supplyDirection) { + return supplyEntity; + } + } + } + } + + return null; + } + /** * Recomputes the belt cache */ computeBeltCache() { + this.recomputePaths = false; + /* if (this.areaToRecompute) { logger.log("Updating belt cache by updating area:", this.areaToRecompute); @@ -207,13 +347,88 @@ export class BeltSystem extends GameSystemWithFilter { entity.components.Belt.followUpCache = this.findFollowUpEntity(entity); } } + */ + this.computeBeltPaths(); + } + + /** + * Computes the belt path network + */ + computeBeltPaths() { + const visitedUids = new Set(); + console.log("Computing belt paths"); + + const debugEntity = e => e.components.StaticMapEntity.origin.toString(); + + // const stackToVisit = this.allEntities.slice(); + const result = []; + + const currentPath = null; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + if (visitedUids.has(entity.uid)) { + continue; + } + + // console.log("Starting at", debugEntity(entity)); + // Mark entity as visited + visitedUids.add(entity.uid); + + // Compute path, start with entity and find precedors / successors + const path = [entity]; + + // Find precedors + let prevEntity = this.findSupplyingEntity(entity); + while (prevEntity) { + if (visitedUids.has(prevEntity)) { + break; + } + // console.log(" -> precedor: ", debugEntity(prevEntity)); + path.unshift(prevEntity); + visitedUids.add(prevEntity.uid); + prevEntity = this.findSupplyingEntity(prevEntity); + } + + // Find succedors + let nextEntity = this.findFollowUpEntity(entity); + while (nextEntity) { + if (visitedUids.has(nextEntity)) { + break; + } + + // console.log(" -> succedor: ", debugEntity(nextEntity)); + path.push(nextEntity); + visitedUids.add(nextEntity.uid); + nextEntity = this.findFollowUpEntity(nextEntity); + } + + // console.log( + // "Found path:", + // path.map(e => debugEntity(e)) + // ); + + result.push(new BeltPath(this.root, path)); + + // let prevEntity = this.findSupplyingEntity(srcEntity); + } + + logger.log("Found", this.beltPaths.length, "belt paths"); + this.beltPaths = result; } update() { - if (this.areaToRecompute) { + if (this.recomputePaths) { this.computeBeltCache(); } + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].update(); + } + + return; + /* + // Divide by item spacing on belts since we use throughput and not speed let beltSpeed = this.root.hubGoals.getBeltBaseSpeed() * @@ -238,7 +453,7 @@ export class BeltSystem extends GameSystemWithFilter { const ejectorComp = entity.components.ItemEjector; let maxProgress = 1; - /* PERFORMANCE OPTIMIZATION */ + // PERFORMANCE OPTIMIZATION // Original: // const isCurrentlyEjecting = ejectorComp.isAnySlotEjecting(); // Replaced (Since belts always have just one slot): @@ -300,7 +515,7 @@ export class BeltSystem extends GameSystemWithFilter { } else { // Try to give this item to a new belt - /* PERFORMANCE OPTIMIZATION */ + // PERFORMANCE OPTIMIZATION // Original: // const freeSlot = ejectorComp.getFirstFreeSlot(); @@ -326,6 +541,7 @@ export class BeltSystem extends GameSystemWithFilter { } } } + */ } /** @@ -372,6 +588,7 @@ export class BeltSystem extends GameSystemWithFilter { * @param {Entity} entity */ drawEntityItems(parameters, entity) { + /* const beltComp = entity.components.Belt; const staticComp = entity.components.StaticMapEntity; @@ -401,5 +618,16 @@ export class BeltSystem extends GameSystemWithFilter { parameters ); } + */ + } + + /** + * Draws the belt parameters + * @param {DrawParameters} parameters + */ + drawBeltPathDebug(parameters) { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].drawDebug(parameters); + } } }