diff --git a/src/js/game/buildings/belt.js b/src/js/game/buildings/belt.js index 1fb80b88..c2194acd 100644 --- a/src/js/game/buildings/belt.js +++ b/src/js/game/buildings/belt.js @@ -1,52 +1,230 @@ -import { Loader } from "../../core/loader"; -import { enumDirection } from "../../core/vector"; -import { SOUNDS } from "../../platform/sound"; -import { arrayBeltVariantToRotation, MetaBeltBaseBuilding } from "./belt_base"; - -export class MetaBeltBuilding extends MetaBeltBaseBuilding { - constructor() { - super("belt"); - } - - getSilhouetteColor() { - return "#777"; - } - - getPlacementSound() { - return SOUNDS.placeBelt; - } - - getPreviewSprite(rotationVariant) { - switch (arrayBeltVariantToRotation[rotationVariant]) { - case enumDirection.top: { - return Loader.getSprite("sprites/buildings/belt_top.png"); - } - case enumDirection.left: { - return Loader.getSprite("sprites/buildings/belt_left.png"); - } - case enumDirection.right: { - return Loader.getSprite("sprites/buildings/belt_right.png"); - } - default: { - assertAlways(false, "Invalid belt rotation variant"); - } - } - } - - getBlueprintSprite(rotationVariant) { - switch (arrayBeltVariantToRotation[rotationVariant]) { - case enumDirection.top: { - return Loader.getSprite("sprites/blueprints/belt_top.png"); - } - case enumDirection.left: { - return Loader.getSprite("sprites/blueprints/belt_left.png"); - } - case enumDirection.right: { - return Loader.getSprite("sprites/blueprints/belt_right.png"); - } - default: { - assertAlways(false, "Invalid belt rotation variant"); - } - } - } -} +import { Loader } from "../../core/loader"; +import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils"; +import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector"; +import { SOUNDS } from "../../platform/sound"; +import { T } from "../../translations"; +import { BeltComponent } from "../components/belt"; +import { Entity } from "../entity"; +import { MetaBuilding } from "../meta_building"; +import { GameRoot } from "../root"; + +export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right]; + +export const beltOverlayMatrices = { + [enumDirection.top]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]), + [enumDirection.left]: generateMatrixRotations([0, 0, 0, 1, 1, 0, 0, 1, 0]), + [enumDirection.right]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]), +}; + +export class MetaBeltBaseBuilding extends MetaBuilding {} + +export class MetaBeltBuilding extends MetaBuilding { + constructor() { + super("belt"); + } + + getSilhouetteColor() { + return "#777"; + } + + getPlacementSound() { + return SOUNDS.placeBelt; + } + + getHasDirectionLockAvailable() { + return true; + } + getStayInPlacementMode() { + return true; + } + + getRotateAutomaticallyWhilePlacing() { + return true; + } + + getSprite() { + return null; + } + + getIsReplaceable() { + return true; + } + + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const beltSpeed = root.hubGoals.getBeltBaseSpeed(); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]]; + } + + getPreviewSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/buildings/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/buildings/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/buildings/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + getBlueprintSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/blueprints/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/blueprints/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/blueprints/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + /** + * + * @param {number} rotation + * @param {number} rotationVariant + * @param {string} variant + * @param {Entity} entity + */ + getSpecialOverlayRenderMatrix(rotation, rotationVariant, variant, entity) { + return beltOverlayMatrices[entity.components.Belt.direction][rotation]; + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new BeltComponent({ + direction: enumDirection.top, // updated later + }) + ); + } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + */ + updateVariants(entity, rotationVariant) { + entity.components.Belt.direction = arrayBeltVariantToRotation[rotationVariant]; + } + + /** + * Should compute the optimal rotation variant on the given tile + * @param {object} param0 + * @param {GameRoot} param0.root + * @param {Vector} param0.tile + * @param {number} param0.rotation + * @param {string} param0.variant + * @param {Layer} param0.layer + * @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array }} + */ + computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }) { + const topDirection = enumAngleToDirection[rotation]; + const rightDirection = enumAngleToDirection[(rotation + 90) % 360]; + const bottomDirection = enumAngleToDirection[(rotation + 180) % 360]; + const leftDirection = enumAngleToDirection[(rotation + 270) % 360]; + + const { ejectors, acceptors } = root.logic.getEjectorsAndAcceptorsAtTile(tile); + + let hasBottomEjector = false; + let hasRightEjector = false; + let hasLeftEjector = false; + + let hasTopAcceptor = false; + let hasLeftAcceptor = false; + let hasRightAcceptor = false; + + // Check all ejectors + for (let i = 0; i < ejectors.length; ++i) { + const ejector = ejectors[i]; + + if (ejector.toDirection === topDirection) { + hasBottomEjector = true; + } else if (ejector.toDirection === leftDirection) { + hasRightEjector = true; + } else if (ejector.toDirection === rightDirection) { + hasLeftEjector = true; + } + } + + // Check all acceptors + for (let i = 0; i < acceptors.length; ++i) { + const acceptor = acceptors[i]; + if (acceptor.fromDirection === bottomDirection) { + hasTopAcceptor = true; + } else if (acceptor.fromDirection === rightDirection) { + hasLeftAcceptor = true; + } else if (acceptor.fromDirection === leftDirection) { + hasRightAcceptor = true; + } + } + + // Soo .. if there is any ejector below us we always prioritize + // this ejector + if (!hasBottomEjector) { + // When something ejects to us from the left and nothing from the right, + // do a curve from the left to the top + + if (hasRightEjector && !hasLeftEjector) { + return { + rotation: (rotation + 270) % 360, + rotationVariant: 2, + }; + } + + // When something ejects to us from the right and nothing from the left, + // do a curve from the right to the top + if (hasLeftEjector && !hasRightEjector) { + return { + rotation: (rotation + 90) % 360, + rotationVariant: 1, + }; + } + } + + // When there is a top acceptor, ignore sides + // NOTICE: This makes the belt prefer side turns *way* too much! + if (!hasTopAcceptor) { + // When there is an acceptor to the right but no acceptor to the left, + // do a turn to the right + if (hasRightAcceptor && !hasLeftAcceptor) { + return { + rotation, + rotationVariant: 2, + }; + } + + // When there is an acceptor to the left but no acceptor to the right, + // do a turn to the left + if (hasLeftAcceptor && !hasRightAcceptor) { + return { + rotation, + rotationVariant: 1, + }; + } + } + + return { + rotation, + rotationVariant: 0, + }; + } +} diff --git a/src/js/game/buildings/belt_base.js b/src/js/game/buildings/belt_base.js deleted file mode 100644 index 1aafa9e1..00000000 --- a/src/js/game/buildings/belt_base.js +++ /dev/null @@ -1,186 +0,0 @@ -import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils"; -import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector"; -import { SOUNDS } from "../../platform/sound"; -import { T } from "../../translations"; -import { BeltComponent } from "../components/belt"; -import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; -import { GameRoot } from "../root"; - -export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right]; - -export const beltOverlayMatrices = { - [enumDirection.top]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]), - [enumDirection.left]: generateMatrixRotations([0, 0, 0, 1, 1, 0, 0, 1, 0]), - [enumDirection.right]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]), -}; - -export class MetaBeltBaseBuilding extends MetaBuilding { - getHasDirectionLockAvailable() { - return true; - } - - /** - * @param {GameRoot} root - * @param {string} variant - * @returns {Array<[string, string]>} - */ - getAdditionalStatistics(root, variant) { - const beltSpeed = root.hubGoals.getBeltBaseSpeed(); - return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]]; - } - - getStayInPlacementMode() { - return true; - } - - getRotateAutomaticallyWhilePlacing() { - return true; - } - - getPlacementSound() { - return SOUNDS.placeBelt; - } - - getSprite() { - return null; - } - - getIsReplaceable() { - return true; - } - - /** - * - * @param {number} rotation - * @param {number} rotationVariant - * @param {string} variant - * @param {Entity} entity - */ - getSpecialOverlayRenderMatrix(rotation, rotationVariant, variant, entity) { - return beltOverlayMatrices[entity.components.Belt.direction][rotation]; - } - - /** - * Creates the entity at the given location - * @param {Entity} entity - */ - setupEntityComponents(entity) { - entity.addComponent( - new BeltComponent({ - direction: enumDirection.top, // updated later - }) - ); - } - - /** - * - * @param {Entity} entity - * @param {number} rotationVariant - */ - updateVariants(entity, rotationVariant) { - entity.components.Belt.direction = arrayBeltVariantToRotation[rotationVariant]; - } - - /** - * Should compute the optimal rotation variant on the given tile - * @param {object} param0 - * @param {GameRoot} param0.root - * @param {Vector} param0.tile - * @param {number} param0.rotation - * @param {string} param0.variant - * @param {Layer} param0.layer - * @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array }} - */ - computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }) { - const topDirection = enumAngleToDirection[rotation]; - const rightDirection = enumAngleToDirection[(rotation + 90) % 360]; - const bottomDirection = enumAngleToDirection[(rotation + 180) % 360]; - const leftDirection = enumAngleToDirection[(rotation + 270) % 360]; - - const { ejectors, acceptors } = root.logic.getEjectorsAndAcceptorsAtTile(tile); - - let hasBottomEjector = false; - let hasRightEjector = false; - let hasLeftEjector = false; - - let hasTopAcceptor = false; - let hasLeftAcceptor = false; - let hasRightAcceptor = false; - - // Check all ejectors - for (let i = 0; i < ejectors.length; ++i) { - const ejector = ejectors[i]; - - if (ejector.toDirection === topDirection) { - hasBottomEjector = true; - } else if (ejector.toDirection === leftDirection) { - hasRightEjector = true; - } else if (ejector.toDirection === rightDirection) { - hasLeftEjector = true; - } - } - - // Check all acceptors - for (let i = 0; i < acceptors.length; ++i) { - const acceptor = acceptors[i]; - if (acceptor.fromDirection === bottomDirection) { - hasTopAcceptor = true; - } else if (acceptor.fromDirection === rightDirection) { - hasLeftAcceptor = true; - } else if (acceptor.fromDirection === leftDirection) { - hasRightAcceptor = true; - } - } - - // Soo .. if there is any ejector below us we always prioritize - // this ejector - if (!hasBottomEjector) { - // When something ejects to us from the left and nothing from the right, - // do a curve from the left to the top - - if (hasRightEjector && !hasLeftEjector) { - return { - rotation: (rotation + 270) % 360, - rotationVariant: 2, - }; - } - - // When something ejects to us from the right and nothing from the left, - // do a curve from the right to the top - if (hasLeftEjector && !hasRightEjector) { - return { - rotation: (rotation + 90) % 360, - rotationVariant: 1, - }; - } - } - - // When there is a top acceptor, ignore sides - // NOTICE: This makes the belt prefer side turns *way* too much! - if (!hasTopAcceptor) { - // When there is an acceptor to the right but no acceptor to the left, - // do a turn to the right - if (hasRightAcceptor && !hasLeftAcceptor) { - return { - rotation, - rotationVariant: 2, - }; - } - - // When there is an acceptor to the left but no acceptor to the right, - // do a turn to the left - if (hasLeftAcceptor && !hasRightAcceptor) { - return { - rotation, - rotationVariant: 1, - }; - } - } - - return { - rotation, - rotationVariant: 0, - }; - } -} diff --git a/src/js/game/hud/parts/buildings_toolbar.js b/src/js/game/hud/parts/buildings_toolbar.js index f8953204..9b6a7901 100644 --- a/src/js/game/hud/parts/buildings_toolbar.js +++ b/src/js/game/hud/parts/buildings_toolbar.js @@ -1,21 +1,21 @@ -import { MetaBeltBaseBuilding } from "../../buildings/belt_base"; +import { MetaBeltBuilding } from "../../buildings/belt"; import { MetaCutterBuilding } from "../../buildings/cutter"; +import { MetaDisplayBuilding } from "../../buildings/display"; +import { MetaFilterBuilding } from "../../buildings/filter"; +import { MetaLeverBuilding } from "../../buildings/lever"; import { MetaMinerBuilding } from "../../buildings/miner"; import { MetaMixerBuilding } from "../../buildings/mixer"; import { MetaPainterBuilding } from "../../buildings/painter"; +import { MetaReaderBuilding } from "../../buildings/reader"; import { MetaRotaterBuilding } from "../../buildings/rotater"; import { MetaSplitterBuilding } from "../../buildings/splitter"; import { MetaStackerBuilding } from "../../buildings/stacker"; import { MetaTrashBuilding } from "../../buildings/trash"; import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; import { HUDBaseToolbar } from "./base_toolbar"; -import { MetaLeverBuilding } from "../../buildings/lever"; -import { MetaFilterBuilding } from "../../buildings/filter"; -import { MetaDisplayBuilding } from "../../buildings/display"; -import { MetaReaderBuilding } from "../../buildings/reader"; const supportedBuildings = [ - MetaBeltBaseBuilding, + MetaBeltBuilding, MetaSplitterBuilding, MetaUndergroundBeltBuilding, MetaMinerBuilding, diff --git a/src/js/game/hud/parts/miner_highlight.js b/src/js/game/hud/parts/miner_highlight.js index c2b23583..b398bbfd 100644 --- a/src/js/game/hud/parts/miner_highlight.js +++ b/src/js/game/hud/parts/miner_highlight.js @@ -67,7 +67,7 @@ export class HUDMinerHighlight extends BaseHUDPart { const maxThroughput = this.root.hubGoals.getBeltBaseSpeed(); - const screenPos = this.root.camera.screenToWorld(mousePos); + const tooltipLocation = this.root.camera.screenToWorld(mousePos); const scale = (1 / this.root.camera.zoomLevel) * this.root.app.getEffectiveUiScale(); @@ -76,8 +76,8 @@ export class HUDMinerHighlight extends BaseHUDPart { // Background parameters.context.fillStyle = THEME.map.connectedMiners.background; parameters.context.beginRoundedRect( - screenPos.x + 5 * scale, - screenPos.y - 3 * scale, + tooltipLocation.x + 5 * scale, + tooltipLocation.y - 3 * scale, (isCapped ? 100 : 65) * scale, (isCapped ? 45 : 30) * scale, 2 @@ -89,8 +89,8 @@ export class HUDMinerHighlight extends BaseHUDPart { parameters.context.font = "bold " + scale * 10 + "px GameFont"; parameters.context.fillText( formatItemsPerSecond(throughput), - screenPos.x + 10 * scale, - screenPos.y + 10 * scale + tooltipLocation.x + 10 * scale, + tooltipLocation.y + 10 * scale ); // Amount of miners @@ -100,8 +100,8 @@ export class HUDMinerHighlight extends BaseHUDPart { connectedEntities.length === 1 ? T.ingame.connectedMiners.one_miner : T.ingame.connectedMiners.n_miners.replace("", String(connectedEntities.length)), - screenPos.x + 10 * scale, - screenPos.y + 22 * scale + tooltipLocation.x + 10 * scale, + tooltipLocation.y + 22 * scale ); parameters.context.globalAlpha = 1; @@ -113,8 +113,8 @@ export class HUDMinerHighlight extends BaseHUDPart { "", formatItemsPerSecond(maxThroughput) ), - screenPos.x + 10 * scale, - screenPos.y + 34 * scale + tooltipLocation.x + 10 * scale, + tooltipLocation.y + 34 * scale ); } } diff --git a/src/js/game/hud/trailer_maker.js b/src/js/game/hud/trailer_maker.js index 8655def4..e9193a93 100644 --- a/src/js/game/hud/trailer_maker.js +++ b/src/js/game/hud/trailer_maker.js @@ -1,125 +1,122 @@ -import { GameRoot } from "../root"; -import { globalConfig } from "../../core/config"; -import { Vector, mixVector } from "../../core/vector"; -import { lerp } from "../../core/utils"; - -/* dev:start */ -import trailerPoints from "./trailer_points"; -import { gMetaBuildingRegistry } from "../../core/global_registries"; -import { MetaBeltBaseBuilding } from "../buildings/belt_base"; -import { MinerComponent } from "../components/miner"; - -const tickrate = 1 / 165; - -export class TrailerMaker { - /** - * - * @param {GameRoot} root - */ - constructor(root) { - this.root = root; - - this.markers = []; - this.playbackMarkers = null; - this.currentPlaybackOrigin = new Vector(); - this.currentPlaybackZoom = 3; - - window.addEventListener("keydown", ev => { - if (ev.key === "j") { - console.log("Record"); - this.markers.push({ - pos: this.root.camera.center.copy(), - zoom: this.root.camera.zoomLevel, - time: 1, - wait: 0, - }); - } else if (ev.key === "k") { - console.log("Export"); - const json = JSON.stringify(this.markers); - const handle = window.open("about:blank"); - handle.document.write(json); - } else if (ev.key === "u") { - if (this.playbackMarkers && this.playbackMarkers.length > 0) { - this.playbackMarkers = []; - return; - } - console.log("Playback"); - this.playbackMarkers = trailerPoints.map(p => Object.assign({}, p)); - this.playbackMarkers.unshift(this.playbackMarkers[0]); - this.currentPlaybackOrigin = Vector.fromSerializedObject(this.playbackMarkers[0].pos); - - this.currentPlaybackZoom = this.playbackMarkers[0].zoom; - this.root.camera.center = this.currentPlaybackOrigin.copy(); - this.root.camera.zoomLevel = this.currentPlaybackZoom; - console.log("STart at", this.currentPlaybackOrigin); - - // this.root.entityMgr.getAllWithComponent(MinerComponent).forEach(miner => { - // miner.components.Miner.itemChainBuffer = []; - // miner.components.Miner.lastMiningTime = this.root.time.now() + 5; - // miner.components.ItemEjector.slots.forEach(slot => (slot.item = null)); - // }); - - // this.root.logic.tryPlaceBuilding({ - // origin: new Vector(-428, -15), - // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), - // originalRotation: 0, - // rotation: 0, - // variant: "default", - // rotationVariant: 0, - // }); - - // this.root.logic.tryPlaceBuilding({ - // origin: new Vector(-427, -15), - // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), - // originalRotation: 0, - // rotation: 0, - // variant: "default", - // rotationVariant: 0, - // }); - } - }); - } - - update() { - if (this.playbackMarkers && this.playbackMarkers.length > 0) { - const nextMarker = this.playbackMarkers[0]; - - if (!nextMarker.startTime) { - console.log("Starting to approach", nextMarker.pos); - nextMarker.startTime = performance.now() / 1000.0; - } - - const speed = - globalConfig.tileSize * - globalConfig.beltSpeedItemsPerSecond * - globalConfig.itemSpacingOnBelts; - // let time = - // this.currentPlaybackOrigin.distance(Vector.fromSerializedObject(nextMarker.pos)) / speed; - const time = nextMarker.time; - - const progress = (performance.now() / 1000.0 - nextMarker.startTime) / time; - - if (progress > 1.0) { - if (nextMarker.wait > 0) { - nextMarker.wait -= tickrate; - } else { - console.log("Approached"); - this.currentPlaybackOrigin = this.root.camera.center.copy(); - this.currentPlaybackZoom = this.root.camera.zoomLevel; - this.playbackMarkers.shift(); - } - return; - } - - const targetPos = Vector.fromSerializedObject(nextMarker.pos); - const targetZoom = nextMarker.zoom; - - const pos = mixVector(this.currentPlaybackOrigin, targetPos, progress); - const zoom = lerp(this.currentPlaybackZoom, targetZoom, progress); - this.root.camera.zoomLevel = zoom; - this.root.camera.center = pos; - } - } -} - -/* dev:end */ +import { GameRoot } from "../root"; +import { globalConfig } from "../../core/config"; +import { Vector, mixVector } from "../../core/vector"; +import { lerp } from "../../core/utils"; + +/* dev:start */ +import trailerPoints from "./trailer_points"; + +const tickrate = 1 / 165; + +export class TrailerMaker { + /** + * + * @param {GameRoot} root + */ + constructor(root) { + this.root = root; + + this.markers = []; + this.playbackMarkers = null; + this.currentPlaybackOrigin = new Vector(); + this.currentPlaybackZoom = 3; + + window.addEventListener("keydown", ev => { + if (ev.key === "j") { + console.log("Record"); + this.markers.push({ + pos: this.root.camera.center.copy(), + zoom: this.root.camera.zoomLevel, + time: 1, + wait: 0, + }); + } else if (ev.key === "k") { + console.log("Export"); + const json = JSON.stringify(this.markers); + const handle = window.open("about:blank"); + handle.document.write(json); + } else if (ev.key === "u") { + if (this.playbackMarkers && this.playbackMarkers.length > 0) { + this.playbackMarkers = []; + return; + } + console.log("Playback"); + this.playbackMarkers = trailerPoints.map(p => Object.assign({}, p)); + this.playbackMarkers.unshift(this.playbackMarkers[0]); + this.currentPlaybackOrigin = Vector.fromSerializedObject(this.playbackMarkers[0].pos); + + this.currentPlaybackZoom = this.playbackMarkers[0].zoom; + this.root.camera.center = this.currentPlaybackOrigin.copy(); + this.root.camera.zoomLevel = this.currentPlaybackZoom; + console.log("STart at", this.currentPlaybackOrigin); + + // this.root.entityMgr.getAllWithComponent(MinerComponent).forEach(miner => { + // miner.components.Miner.itemChainBuffer = []; + // miner.components.Miner.lastMiningTime = this.root.time.now() + 5; + // miner.components.ItemEjector.slots.forEach(slot => (slot.item = null)); + // }); + + // this.root.logic.tryPlaceBuilding({ + // origin: new Vector(-428, -15), + // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), + // originalRotation: 0, + // rotation: 0, + // variant: "default", + // rotationVariant: 0, + // }); + + // this.root.logic.tryPlaceBuilding({ + // origin: new Vector(-427, -15), + // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), + // originalRotation: 0, + // rotation: 0, + // variant: "default", + // rotationVariant: 0, + // }); + } + }); + } + + update() { + if (this.playbackMarkers && this.playbackMarkers.length > 0) { + const nextMarker = this.playbackMarkers[0]; + + if (!nextMarker.startTime) { + console.log("Starting to approach", nextMarker.pos); + nextMarker.startTime = performance.now() / 1000.0; + } + + const speed = + globalConfig.tileSize * + globalConfig.beltSpeedItemsPerSecond * + globalConfig.itemSpacingOnBelts; + // let time = + // this.currentPlaybackOrigin.distance(Vector.fromSerializedObject(nextMarker.pos)) / speed; + const time = nextMarker.time; + + const progress = (performance.now() / 1000.0 - nextMarker.startTime) / time; + + if (progress > 1.0) { + if (nextMarker.wait > 0) { + nextMarker.wait -= tickrate; + } else { + console.log("Approached"); + this.currentPlaybackOrigin = this.root.camera.center.copy(); + this.currentPlaybackZoom = this.root.camera.zoomLevel; + this.playbackMarkers.shift(); + } + return; + } + + const targetPos = Vector.fromSerializedObject(nextMarker.pos); + const targetZoom = nextMarker.zoom; + + const pos = mixVector(this.currentPlaybackOrigin, targetPos, progress); + const zoom = lerp(this.currentPlaybackZoom, targetZoom, progress); + this.root.camera.zoomLevel = zoom; + this.root.camera.center = pos; + } + } +} + +/* dev:end */ diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 647e55f5..26870d2a 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -1,7 +1,6 @@ import { gMetaBuildingRegistry } from "../core/global_registries"; import { createLogger } from "../core/logging"; import { MetaBeltBuilding } from "./buildings/belt"; -import { MetaBeltBaseBuilding } from "./buildings/belt_base"; import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter"; import { MetaHubBuilding } from "./buildings/hub"; import { enumMinerVariants, MetaMinerBuilding } from "./buildings/miner"; @@ -49,9 +48,9 @@ export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaReaderBuilding); // Belt - registerBuildingVariant(1, MetaBeltBaseBuilding, defaultBuildingVariant, 0); - registerBuildingVariant(2, MetaBeltBaseBuilding, defaultBuildingVariant, 1); - registerBuildingVariant(3, MetaBeltBaseBuilding, defaultBuildingVariant, 2); + registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0); + registerBuildingVariant(2, MetaBeltBuilding, defaultBuildingVariant, 1); + registerBuildingVariant(3, MetaBeltBuilding, defaultBuildingVariant, 2); // Splitter registerBuildingVariant(4, MetaSplitterBuilding); diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 4d8151f6..62480caf 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -1,520 +1,520 @@ -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 { AtlasSprite } from "../../core/sprites"; -import { fastArrayDeleteValue } from "../../core/utils"; -import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../../core/vector"; -import { BeltPath } from "../belt_path"; -import { arrayBeltVariantToRotation, MetaBeltBaseBuilding } from "../buildings/belt_base"; -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 { getCodeFromBuildingData } from "../building_codes"; - -export const BELT_ANIM_COUNT = 14; - -const logger = createLogger("belt"); - -/** - * Manages all belts - */ -export class BeltSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [BeltComponent]); - /** - * @type {Object.>} - */ - this.beltSprites = { - [enumDirection.top]: Loader.getSprite("sprites/belt/built/forward_0.png"), - [enumDirection.left]: Loader.getSprite("sprites/belt/built/left_0.png"), - [enumDirection.right]: Loader.getSprite("sprites/belt/built/right_0.png"), - }; - - /** - * @type {Object.>} - */ - this.beltAnimations = { - [enumDirection.top]: [], - [enumDirection.left]: [], - [enumDirection.right]: [], - }; - - for (let i = 0; i < BELT_ANIM_COUNT; ++i) { - this.beltAnimations[enumDirection.top].push( - Loader.getSprite("sprites/belt/built/forward_" + i + ".png") - ); - this.beltAnimations[enumDirection.left].push( - Loader.getSprite("sprites/belt/built/left_" + i + ".png") - ); - this.beltAnimations[enumDirection.right].push( - Loader.getSprite("sprites/belt/built/right_" + i + ".png") - ); - } - - this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this); - this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); - - // Notice: These must come *after* the entity destroyed signals - this.root.signals.entityAdded.add(this.onEntityAdded, this); - this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); - - /** @type {Array} */ - this.beltPaths = []; - } - - /** - * Serializes all belt paths - * @returns {Array} - */ - serializePaths() { - let data = []; - for (let i = 0; i < this.beltPaths.length; ++i) { - data.push(this.beltPaths[i].serialize()); - } - return data; - } - - /** - * Deserializes all belt paths - * @param {Array} data - */ - deserializePaths(data) { - if (!Array.isArray(data)) { - return "Belt paths are not an array: " + typeof data; - } - - for (let i = 0; i < data.length; ++i) { - const path = BeltPath.fromSerialized(this.root, data[i]); - // If path is a string, that means its an error - if (!(path instanceof BeltPath)) { - return "Failed to create path from belt data: " + path; - } - this.beltPaths.push(path); - } - - if (this.beltPaths.length === 0) { - // Old savegames might not have paths yet - logger.warn("Recomputing belt paths (most likely the savegame is old or empty)"); - this.recomputeAllBeltPaths(); - } else { - logger.warn("Restored", this.beltPaths.length, "belt paths"); - } - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Updates the belt placement after an entity has been added / deleted - * @param {Entity} entity - */ - updateSurroundingBeltPlacement(entity) { - if (!this.root.gameInitialized) { - return; - } - - const staticComp = entity.components.StaticMapEntity; - if (!staticComp) { - return; - } - - const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); - // Compute affected area - const originalRect = staticComp.getTileSpaceBounds(); - const affectedArea = originalRect.expandedInAllDirections(1); - - /** @type {Set} */ - const changedPaths = new Set(); - - for (let x = affectedArea.x; x < affectedArea.right(); ++x) { - for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { - if (originalRect.containsPoint(x, y)) { - // Make sure we don't update the original entity - continue; - } - - const targetEntities = this.root.map.getLayersContentsMultipleXY(x, y); - for (let i = 0; i < targetEntities.length; ++i) { - const targetEntity = targetEntities[i]; - - const targetBeltComp = targetEntity.components.Belt; - const targetStaticComp = targetEntity.components.StaticMapEntity; - - if (!targetBeltComp) { - // Not a belt - continue; - } - - const { - rotation, - rotationVariant, - } = metaBelt.computeOptimalDirectionAndRotationVariantAtTile({ - root: this.root, - tile: new Vector(x, y), - rotation: targetStaticComp.originalRotation, - variant: defaultBuildingVariant, - layer: targetEntity.layer, - }); - - // Compute delta to see if anything changed - const newDirection = arrayBeltVariantToRotation[rotationVariant]; - - if (targetStaticComp.rotation !== rotation || newDirection !== targetBeltComp.direction) { - // Ok, first remove it from its current path - this.deleteEntityFromPath(targetBeltComp.assignedPath, targetEntity); - - // Change stuff - targetStaticComp.rotation = rotation; - metaBelt.updateVariants(targetEntity, rotationVariant, defaultBuildingVariant); - - // Update code as well - targetStaticComp.code = getCodeFromBuildingData( - metaBelt, - defaultBuildingVariant, - rotationVariant - ); - - // Now add it again - this.addEntityToPaths(targetEntity); - - // Sanity - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - - // Make sure the chunks know about the update - this.root.signals.entityChanged.dispatch(targetEntity); - } - - if (targetBeltComp.assignedPath) { - changedPaths.add(targetBeltComp.assignedPath); - } - } - } - } - - // notify all paths *afterwards* to avoid multi-updates - changedPaths.forEach(path => path.onSurroundingsChanged()); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Called when an entity got destroyed - * @param {Entity} entity - */ - onEntityDestroyed(entity) { - if (!this.root.gameInitialized) { - return; - } - - if (!entity.components.Belt) { - return; - } - - const assignedPath = entity.components.Belt.assignedPath; - assert(assignedPath, "Entity has no belt path assigned"); - this.deleteEntityFromPath(assignedPath, entity); - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Attempts to delete the belt from its current path - * @param {BeltPath} path - * @param {Entity} entity - */ - deleteEntityFromPath(path, entity) { - if (path.entityPath.length === 1) { - // This is a single entity path, easy to do, simply erase whole path - fastArrayDeleteValue(this.beltPaths, path); - return; - } - - // Notice: Since there might be circular references, it is important to check - // which role the entity has - if (path.isStartEntity(entity)) { - // We tried to delete the start - path.deleteEntityOnStart(entity); - } else if (path.isEndEntity(entity)) { - // We tried to delete the end - path.deleteEntityOnEnd(entity); - } else { - // We tried to delete something inbetween - const newPath = path.deleteEntityOnPathSplitIntoTwo(entity); - this.beltPaths.push(newPath); - } - - // Sanity - entity.components.Belt.assignedPath = null; - } - - /** - * Adds the given entity to the appropriate paths - * @param {Entity} entity - */ - addEntityToPaths(entity) { - const fromEntity = this.findSupplyingEntity(entity); - const toEntity = this.findFollowUpEntity(entity); - - // 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; - - if (fromPath === toPath) { - // This is a circular dependency -> Ignore - } else { - 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); - } - } - } - - /** - * Called when an entity got added - * @param {Entity} entity - */ - onEntityAdded(entity) { - if (!this.root.gameInitialized) { - return; - } - - if (!entity.components.Belt) { - return; - } - - this.addEntityToPaths(entity); - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Draws all belt paths - * @param {DrawParameters} parameters - */ - drawBeltItems(parameters) { - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].draw(parameters); - } - } - - /** - * Verifies all belt paths - */ - debug_verifyBeltPaths() { - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].debug_checkIntegrity("general-verify"); - } - - const belts = this.root.entityMgr.getAllWithComponent(BeltComponent); - for (let i = 0; i < belts.length; ++i) { - const path = belts[i].components.Belt.assignedPath; - if (!path) { - throw new Error("Belt has no path: " + belts[i].uid); - } - if (this.beltPaths.indexOf(path) < 0) { - throw new Error("Path of entity not contained: " + belts[i].uid); - } - } - } - - /** - * Finds the follow up entity for a given belt. Used for building the dependencies - * @param {Entity} entity - * @returns {Entity|null} - */ - findFollowUpEntity(entity) { - const staticComp = entity.components.StaticMapEntity; - const beltComp = entity.components.Belt; - - const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); - const followUpVector = enumDirectionToVector[followUpDirection]; - - const followUpTile = staticComp.origin.add(followUpVector); - const followUpEntity = this.root.map.getLayerContentXY(followUpTile.x, followUpTile.y, entity.layer); - - // Check if theres a belt at the tile we point to - if (followUpEntity) { - const followUpBeltComp = followUpEntity.components.Belt; - if (followUpBeltComp) { - const followUpStatic = followUpEntity.components.StaticMapEntity; - - const acceptedDirection = followUpStatic.localDirectionToWorld(enumDirection.top); - if (acceptedDirection === followUpDirection) { - return followUpEntity; - } - } - } - - return null; - } - - /** - * Finds the supplying belt for a given belt. Used for building the dependencies - * @param {Entity} entity - * @returns {Entity|null} - */ - 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.getLayerContentXY(supplyTile.x, supplyTile.y, entity.layer); - - // 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 otherDirection = supplyStatic.localDirectionToWorld( - enumInvertedDirections[supplyBeltComp.direction] - ); - - if (otherDirection === supplyDirection) { - return supplyEntity; - } - } - } - - return null; - } - - /** - * Recomputes the belt path network. Only required for old savegames - */ - recomputeAllBeltPaths() { - logger.warn("Recomputing all belt paths"); - const visitedUids = new Set(); - - const result = []; - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - if (visitedUids.has(entity.uid)) { - continue; - } - - // Mark entity as visited - visitedUids.add(entity.uid); - - // Compute path, start with entity and find precedors / successors - const path = [entity]; - - // Prevent infinite loops - let maxIter = 99999; - - // Find precedors - let prevEntity = this.findSupplyingEntity(entity); - while (prevEntity && --maxIter > 0) { - if (visitedUids.has(prevEntity.uid)) { - break; - } - path.unshift(prevEntity); - visitedUids.add(prevEntity.uid); - prevEntity = this.findSupplyingEntity(prevEntity); - } - - // Find succedors - let nextEntity = this.findFollowUpEntity(entity); - while (nextEntity && --maxIter > 0) { - if (visitedUids.has(nextEntity.uid)) { - break; - } - - path.push(nextEntity); - visitedUids.add(nextEntity.uid); - nextEntity = this.findFollowUpEntity(nextEntity); - } - - assert(maxIter > 1, "Ran out of iterations"); - result.push(new BeltPath(this.root, path)); - } - - logger.log("Found", this.beltPaths.length, "belt paths"); - this.beltPaths = result; - } - - /** - * Updates all belts - */ - update() { - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].update(); - } - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Draws a given chunk - * @param {DrawParameters} parameters - * @param {MapChunkView} chunk - */ - drawChunk(parameters, chunk) { - // Limit speed to avoid belts going backwards - const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10); - - // SYNC with systems/item_acceptor.js:drawEntityUnderlays! - // 126 / 42 is the exact animation speed of the png animation - const animationIndex = Math.floor( - ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * - globalConfig.itemSpacingOnBelts - ); - const contents = chunk.containedEntitiesByLayer.regular; - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - if (entity.components.Belt) { - const direction = entity.components.Belt.direction; - const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT]; - - // Culling happens within the static map entity component - entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); - } - } - } - - /** - * Draws the belt path debug overlays - * @param {DrawParameters} parameters - */ - drawBeltPathDebug(parameters) { - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].drawDebug(parameters); - } - } -} +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 { AtlasSprite } from "../../core/sprites"; +import { fastArrayDeleteValue } from "../../core/utils"; +import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } 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 { getCodeFromBuildingData } from "../building_codes"; +import { arrayBeltVariantToRotation, MetaBeltBuilding } from "../buildings/belt"; + +export const BELT_ANIM_COUNT = 14; + +const logger = createLogger("belt"); + +/** + * Manages all belts + */ +export class BeltSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [BeltComponent]); + /** + * @type {Object.>} + */ + this.beltSprites = { + [enumDirection.top]: Loader.getSprite("sprites/belt/built/forward_0.png"), + [enumDirection.left]: Loader.getSprite("sprites/belt/built/left_0.png"), + [enumDirection.right]: Loader.getSprite("sprites/belt/built/right_0.png"), + }; + + /** + * @type {Object.>} + */ + this.beltAnimations = { + [enumDirection.top]: [], + [enumDirection.left]: [], + [enumDirection.right]: [], + }; + + for (let i = 0; i < BELT_ANIM_COUNT; ++i) { + this.beltAnimations[enumDirection.top].push( + Loader.getSprite("sprites/belt/built/forward_" + i + ".png") + ); + this.beltAnimations[enumDirection.left].push( + Loader.getSprite("sprites/belt/built/left_" + i + ".png") + ); + this.beltAnimations[enumDirection.right].push( + Loader.getSprite("sprites/belt/built/right_" + i + ".png") + ); + } + + this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this); + this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); + + // Notice: These must come *after* the entity destroyed signals + this.root.signals.entityAdded.add(this.onEntityAdded, this); + this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); + + /** @type {Array} */ + this.beltPaths = []; + } + + /** + * Serializes all belt paths + * @returns {Array} + */ + serializePaths() { + let data = []; + for (let i = 0; i < this.beltPaths.length; ++i) { + data.push(this.beltPaths[i].serialize()); + } + return data; + } + + /** + * Deserializes all belt paths + * @param {Array} data + */ + deserializePaths(data) { + if (!Array.isArray(data)) { + return "Belt paths are not an array: " + typeof data; + } + + for (let i = 0; i < data.length; ++i) { + const path = BeltPath.fromSerialized(this.root, data[i]); + // If path is a string, that means its an error + if (!(path instanceof BeltPath)) { + return "Failed to create path from belt data: " + path; + } + this.beltPaths.push(path); + } + + if (this.beltPaths.length === 0) { + // Old savegames might not have paths yet + logger.warn("Recomputing belt paths (most likely the savegame is old or empty)"); + this.recomputeAllBeltPaths(); + } else { + logger.warn("Restored", this.beltPaths.length, "belt paths"); + } + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Updates the belt placement after an entity has been added / deleted + * @param {Entity} entity + */ + updateSurroundingBeltPlacement(entity) { + if (!this.root.gameInitialized) { + return; + } + + const staticComp = entity.components.StaticMapEntity; + if (!staticComp) { + return; + } + + const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBuilding); + // Compute affected area + const originalRect = staticComp.getTileSpaceBounds(); + const affectedArea = originalRect.expandedInAllDirections(1); + + /** @type {Set} */ + const changedPaths = new Set(); + + for (let x = affectedArea.x; x < affectedArea.right(); ++x) { + for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { + if (originalRect.containsPoint(x, y)) { + // Make sure we don't update the original entity + continue; + } + + const targetEntities = this.root.map.getLayersContentsMultipleXY(x, y); + for (let i = 0; i < targetEntities.length; ++i) { + const targetEntity = targetEntities[i]; + + const targetBeltComp = targetEntity.components.Belt; + const targetStaticComp = targetEntity.components.StaticMapEntity; + + if (!targetBeltComp) { + // Not a belt + continue; + } + + const { + rotation, + rotationVariant, + } = metaBelt.computeOptimalDirectionAndRotationVariantAtTile({ + root: this.root, + tile: new Vector(x, y), + rotation: targetStaticComp.originalRotation, + variant: defaultBuildingVariant, + layer: targetEntity.layer, + }); + + // Compute delta to see if anything changed + const newDirection = arrayBeltVariantToRotation[rotationVariant]; + + if (targetStaticComp.rotation !== rotation || newDirection !== targetBeltComp.direction) { + // Ok, first remove it from its current path + this.deleteEntityFromPath(targetBeltComp.assignedPath, targetEntity); + + // Change stuff + targetStaticComp.rotation = rotation; + metaBelt.updateVariants(targetEntity, rotationVariant, defaultBuildingVariant); + + // Update code as well + targetStaticComp.code = getCodeFromBuildingData( + metaBelt, + defaultBuildingVariant, + rotationVariant + ); + + // Now add it again + this.addEntityToPaths(targetEntity); + + // Sanity + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + + // Make sure the chunks know about the update + this.root.signals.entityChanged.dispatch(targetEntity); + } + + if (targetBeltComp.assignedPath) { + changedPaths.add(targetBeltComp.assignedPath); + } + } + } + } + + // notify all paths *afterwards* to avoid multi-updates + changedPaths.forEach(path => path.onSurroundingsChanged()); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Called when an entity got destroyed + * @param {Entity} entity + */ + onEntityDestroyed(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + const assignedPath = entity.components.Belt.assignedPath; + assert(assignedPath, "Entity has no belt path assigned"); + this.deleteEntityFromPath(assignedPath, entity); + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Attempts to delete the belt from its current path + * @param {BeltPath} path + * @param {Entity} entity + */ + deleteEntityFromPath(path, entity) { + if (path.entityPath.length === 1) { + // This is a single entity path, easy to do, simply erase whole path + fastArrayDeleteValue(this.beltPaths, path); + return; + } + + // Notice: Since there might be circular references, it is important to check + // which role the entity has + if (path.isStartEntity(entity)) { + // We tried to delete the start + path.deleteEntityOnStart(entity); + } else if (path.isEndEntity(entity)) { + // We tried to delete the end + path.deleteEntityOnEnd(entity); + } else { + // We tried to delete something inbetween + const newPath = path.deleteEntityOnPathSplitIntoTwo(entity); + this.beltPaths.push(newPath); + } + + // Sanity + entity.components.Belt.assignedPath = null; + } + + /** + * Adds the given entity to the appropriate paths + * @param {Entity} entity + */ + addEntityToPaths(entity) { + const fromEntity = this.findSupplyingEntity(entity); + const toEntity = this.findFollowUpEntity(entity); + + // 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; + + if (fromPath === toPath) { + // This is a circular dependency -> Ignore + } else { + 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); + } + } + } + + /** + * Called when an entity got added + * @param {Entity} entity + */ + onEntityAdded(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + this.addEntityToPaths(entity); + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Draws all belt paths + * @param {DrawParameters} parameters + */ + drawBeltItems(parameters) { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].draw(parameters); + } + } + + /** + * Verifies all belt paths + */ + debug_verifyBeltPaths() { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].debug_checkIntegrity("general-verify"); + } + + const belts = this.root.entityMgr.getAllWithComponent(BeltComponent); + for (let i = 0; i < belts.length; ++i) { + const path = belts[i].components.Belt.assignedPath; + if (!path) { + throw new Error("Belt has no path: " + belts[i].uid); + } + if (this.beltPaths.indexOf(path) < 0) { + throw new Error("Path of entity not contained: " + belts[i].uid); + } + } + } + + /** + * Finds the follow up entity for a given belt. Used for building the dependencies + * @param {Entity} entity + * @returns {Entity|null} + */ + findFollowUpEntity(entity) { + const staticComp = entity.components.StaticMapEntity; + const beltComp = entity.components.Belt; + + const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); + const followUpVector = enumDirectionToVector[followUpDirection]; + + const followUpTile = staticComp.origin.add(followUpVector); + const followUpEntity = this.root.map.getLayerContentXY(followUpTile.x, followUpTile.y, entity.layer); + + // Check if theres a belt at the tile we point to + if (followUpEntity) { + const followUpBeltComp = followUpEntity.components.Belt; + if (followUpBeltComp) { + const followUpStatic = followUpEntity.components.StaticMapEntity; + + const acceptedDirection = followUpStatic.localDirectionToWorld(enumDirection.top); + if (acceptedDirection === followUpDirection) { + return followUpEntity; + } + } + } + + return null; + } + + /** + * Finds the supplying belt for a given belt. Used for building the dependencies + * @param {Entity} entity + * @returns {Entity|null} + */ + 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.getLayerContentXY(supplyTile.x, supplyTile.y, entity.layer); + + // 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 otherDirection = supplyStatic.localDirectionToWorld( + enumInvertedDirections[supplyBeltComp.direction] + ); + + if (otherDirection === supplyDirection) { + return supplyEntity; + } + } + } + + return null; + } + + /** + * Recomputes the belt path network. Only required for old savegames + */ + recomputeAllBeltPaths() { + logger.warn("Recomputing all belt paths"); + const visitedUids = new Set(); + + const result = []; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + if (visitedUids.has(entity.uid)) { + continue; + } + + // Mark entity as visited + visitedUids.add(entity.uid); + + // Compute path, start with entity and find precedors / successors + const path = [entity]; + + // Prevent infinite loops + let maxIter = 99999; + + // Find precedors + let prevEntity = this.findSupplyingEntity(entity); + while (prevEntity && --maxIter > 0) { + if (visitedUids.has(prevEntity.uid)) { + break; + } + path.unshift(prevEntity); + visitedUids.add(prevEntity.uid); + prevEntity = this.findSupplyingEntity(prevEntity); + } + + // Find succedors + let nextEntity = this.findFollowUpEntity(entity); + while (nextEntity && --maxIter > 0) { + if (visitedUids.has(nextEntity.uid)) { + break; + } + + path.push(nextEntity); + visitedUids.add(nextEntity.uid); + nextEntity = this.findFollowUpEntity(nextEntity); + } + + assert(maxIter > 1, "Ran out of iterations"); + result.push(new BeltPath(this.root, path)); + } + + logger.log("Found", this.beltPaths.length, "belt paths"); + this.beltPaths = result; + } + + /** + * Updates all belts + */ + update() { + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].update(); + } + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Draws a given chunk + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawChunk(parameters, chunk) { + // Limit speed to avoid belts going backwards + const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10); + + // SYNC with systems/item_acceptor.js:drawEntityUnderlays! + // 126 / 42 is the exact animation speed of the png animation + const animationIndex = Math.floor( + ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * + globalConfig.itemSpacingOnBelts + ); + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + if (entity.components.Belt) { + const direction = entity.components.Belt.direction; + const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT]; + + // Culling happens within the static map entity component + entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); + } + } + } + + /** + * Draws the belt path debug overlays + * @param {DrawParameters} parameters + */ + drawBeltPathDebug(parameters) { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].drawDebug(parameters); + } + } +} diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index bea209a8..d8d81001 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -133,7 +133,7 @@ export class MainMenuState extends GameState { !this.app.platformWrapper.getHasUnlimitedSavegames() ) { this.app.analytics.trackUiClick("importgame_slot_limit_show"); - this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); + this.showSavegameSlotLimit(); return; } @@ -522,6 +522,21 @@ export class MainMenuState extends GameState { }); } + /** + * Shows a hint that the slot limit has been reached + */ + showSavegameSlotLimit() { + const { getStandalone } = this.dialogs.showWarning( + T.dialogs.oneSavegameLimit.title, + T.dialogs.oneSavegameLimit.desc, + ["cancel:bad", "getStandalone:good"] + ); + getStandalone.add(() => { + this.app.analytics.trackUiClick("visit_steampage_from_slot_limit"); + this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage); + }); + } + onSettingsButtonClicked() { this.moveToState("SettingsState"); } @@ -540,7 +555,7 @@ export class MainMenuState extends GameState { !this.app.platformWrapper.getHasUnlimitedSavegames() ) { this.app.analytics.trackUiClick("startgame_slot_limit_show"); - this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); + this.showSavegameSlotLimit(); return; }