diff --git a/src/js/game/buildings/balancer.js b/src/js/game/buildings/balancer.js index 38a568e1..35402d15 100644 --- a/src/js/game/buildings/balancer.js +++ b/src/js/game/buildings/balancer.js @@ -8,7 +8,7 @@ import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; import { T } from "../../translations"; import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils"; -import { BeltUnderlaysComponent } from "../components/belt_underlays"; +import { BELT_BORDER } from "../systems/belt"; /** @enum {string} */ export const enumBalancerVariants = { @@ -138,8 +138,6 @@ export class MetaBalancerBuilding extends MetaBuilding { renderFloatingItems: false, }) ); - - entity.addComponent(new BeltUnderlaysComponent({ underlays: [] })); } /** @@ -155,23 +153,20 @@ export class MetaBalancerBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [enumDirection.bottom], + beltLength: 0.5, }, { pos: new Vector(1, 0), directions: [enumDirection.bottom], + beltLength: 0.5, }, ]); entity.components.ItemEjector.setSlots([ - { pos: new Vector(0, 0), direction: enumDirection.top }, - { pos: new Vector(1, 0), direction: enumDirection.top }, + { pos: new Vector(0, 0), direction: enumDirection.top, beltLength: 0.5 }, + { pos: new Vector(1, 0), direction: enumDirection.top, beltLength: 0.5 }, ]); - entity.components.BeltUnderlays.underlays = [ - { pos: new Vector(0, 0), direction: enumDirection.top }, - { pos: new Vector(1, 0), direction: enumDirection.top }, - ]; - break; } case enumBalancerVariants.merger: @@ -180,6 +175,7 @@ export class MetaBalancerBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [enumDirection.bottom], + beltLength: 0.5, }, { pos: new Vector(0, 0), @@ -188,17 +184,14 @@ export class MetaBalancerBuilding extends MetaBuilding { ? enumDirection.left : enumDirection.right, ], + beltLength: BELT_BORDER, }, ]); entity.components.ItemEjector.setSlots([ - { pos: new Vector(0, 0), direction: enumDirection.top }, + { pos: new Vector(0, 0), direction: enumDirection.top, beltLength: 0.5 }, ]); - entity.components.BeltUnderlays.underlays = [ - { pos: new Vector(0, 0), direction: enumDirection.top }, - ]; - break; } case enumBalancerVariants.splitter: @@ -207,6 +200,7 @@ export class MetaBalancerBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [enumDirection.bottom], + beltLength: 0.5, }, ]); @@ -214,6 +208,7 @@ export class MetaBalancerBuilding extends MetaBuilding { { pos: new Vector(0, 0), direction: enumDirection.top, + beltLength: 0.5, }, { pos: new Vector(0, 0), @@ -221,13 +216,10 @@ export class MetaBalancerBuilding extends MetaBuilding { variant === enumBalancerVariants.splitterInverse ? enumDirection.left : enumDirection.right, + beltLength: BELT_BORDER, }, ]); - entity.components.BeltUnderlays.underlays = [ - { pos: new Vector(0, 0), direction: enumDirection.top }, - ]; - break; } default: diff --git a/src/js/game/buildings/hub.js b/src/js/game/buildings/hub.js index b9929b31..95647006 100644 --- a/src/js/game/buildings/hub.js +++ b/src/js/game/buildings/hub.js @@ -67,71 +67,85 @@ export class MetaHubBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [enumDirection.top, enumDirection.left], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(1, 0), directions: [enumDirection.top], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(2, 0), directions: [enumDirection.top], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(3, 0), directions: [enumDirection.top, enumDirection.right], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(0, 3), directions: [enumDirection.bottom, enumDirection.left], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(1, 3), directions: [enumDirection.bottom], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(2, 3), directions: [enumDirection.bottom], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(3, 3), directions: [enumDirection.bottom, enumDirection.right], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(0, 1), directions: [enumDirection.left], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(0, 2), directions: [enumDirection.left], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(0, 3), directions: [enumDirection.left], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(3, 1), directions: [enumDirection.right], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(3, 2), directions: [enumDirection.right], + beltLength: 0.5, filter: "shape", }, { pos: new Vector(3, 3), directions: [enumDirection.right], + beltLength: 0.5, filter: "shape", }, ], diff --git a/src/js/game/buildings/mixer.js b/src/js/game/buildings/mixer.js index e572bbba..b61ac0ed 100644 --- a/src/js/game/buildings/mixer.js +++ b/src/js/game/buildings/mixer.js @@ -7,6 +7,7 @@ import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/it import { Entity } from "../entity"; import { MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; +import { BELT_BORDER } from "../systems/belt"; import { enumHubGoalRewards } from "../tutorial_goals"; export class MetaMixerBuilding extends MetaBuilding { @@ -65,11 +66,13 @@ export class MetaMixerBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [enumDirection.bottom], + beltLength: BELT_BORDER, filter: "color", }, { pos: new Vector(1, 0), directions: [enumDirection.bottom], + beltLength: BELT_BORDER, filter: "color", }, ], diff --git a/src/js/game/buildings/painter.js b/src/js/game/buildings/painter.js index e7a0b72d..eddffcb5 100644 --- a/src/js/game/buildings/painter.js +++ b/src/js/game/buildings/painter.js @@ -13,6 +13,7 @@ import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins"; +import { BELT_BORDER } from "../systems/belt"; /** @enum {string} */ export const enumPainterVariants = { mirrored: "mirrored", double: "double", quad: "quad" }; @@ -231,6 +232,7 @@ export class MetaPainterBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [enumDirection.left], + beltLength: BELT_BORDER, filter: "shape", }, { diff --git a/src/js/game/buildings/reader.js b/src/js/game/buildings/reader.js index 006d6582..dfbb7b03 100644 --- a/src/js/game/buildings/reader.js +++ b/src/js/game/buildings/reader.js @@ -1,115 +1,105 @@ -import { enumDirection, Vector } from "../../core/vector"; -import { ItemAcceptorComponent } from "../components/item_acceptor"; -import { ItemEjectorComponent } from "../components/item_ejector"; -import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; -import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; -import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; -import { GameRoot } from "../root"; -import { BeltUnderlaysComponent } from "../components/belt_underlays"; -import { BeltReaderComponent } from "../components/belt_reader"; -import { enumHubGoalRewards } from "../tutorial_goals"; -import { generateMatrixRotations } from "../../core/utils"; - -const overlayMatrix = generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]); - -export class MetaReaderBuilding extends MetaBuilding { - constructor() { - super("reader"); - } - - getSilhouetteColor() { - return "#25fff2"; - } - - /** - * @param {GameRoot} root - */ - getIsUnlocked(root) { - return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_belt_reader); - } - - getDimensions() { - return new Vector(1, 1); - } - - getShowWiresLayerPreview() { - return true; - } - - /** - * @param {number} rotation - * @param {number} rotationVariant - * @param {string} variant - * @param {Entity} entity - * @returns {Array|null} - */ - getSpecialOverlayRenderMatrix(rotation, rotationVariant, variant, entity) { - return overlayMatrix[rotation]; - } - - /** - * Creates the entity at the given location - * @param {Entity} entity - */ - setupEntityComponents(entity) { - entity.addComponent( - new WiredPinsComponent({ - slots: [ - { - pos: new Vector(0, 0), - direction: enumDirection.right, - type: enumPinSlotType.logicalEjector, - }, - { - pos: new Vector(0, 0), - direction: enumDirection.left, - type: enumPinSlotType.logicalEjector, - }, - ], - }) - ); - - entity.addComponent( - new ItemAcceptorComponent({ - slots: [ - { - pos: new Vector(0, 0), - directions: [enumDirection.bottom], - }, - ], - }) - ); - - entity.addComponent( - new ItemEjectorComponent({ - slots: [ - { - pos: new Vector(0, 0), - direction: enumDirection.top, - }, - ], - }) - ); - - entity.addComponent( - new ItemProcessorComponent({ - processorType: enumItemProcessorTypes.reader, - inputsPerCharge: 1, - }) - ); - - entity.addComponent( - new BeltUnderlaysComponent({ - underlays: [ - { - pos: new Vector(0, 0), - direction: enumDirection.top, - }, - ], - }) - ); - - entity.addComponent(new BeltReaderComponent()); - } -} +import { enumDirection, Vector } from "../../core/vector"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; +import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; +import { Entity } from "../entity"; +import { MetaBuilding } from "../meta_building"; +import { GameRoot } from "../root"; +import { BeltReaderComponent } from "../components/belt_reader"; +import { enumHubGoalRewards } from "../tutorial_goals"; +import { generateMatrixRotations } from "../../core/utils"; + +const overlayMatrix = generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]); + +export class MetaReaderBuilding extends MetaBuilding { + constructor() { + super("reader"); + } + + getSilhouetteColor() { + return "#25fff2"; + } + + /** + * @param {GameRoot} root + */ + getIsUnlocked(root) { + return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_belt_reader); + } + + getDimensions() { + return new Vector(1, 1); + } + + getShowWiresLayerPreview() { + return true; + } + + /** + * @param {number} rotation + * @param {number} rotationVariant + * @param {string} variant + * @param {Entity} entity + * @returns {Array|null} + */ + getSpecialOverlayRenderMatrix(rotation, rotationVariant, variant, entity) { + return overlayMatrix[rotation]; + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new WiredPinsComponent({ + slots: [ + { + pos: new Vector(0, 0), + direction: enumDirection.right, + type: enumPinSlotType.logicalEjector, + }, + { + pos: new Vector(0, 0), + direction: enumDirection.left, + type: enumPinSlotType.logicalEjector, + }, + ], + }) + ); + + entity.addComponent( + new ItemAcceptorComponent({ + slots: [ + { + pos: new Vector(0, 0), + directions: [enumDirection.bottom], + beltLength: 0.5, + }, + ], + }) + ); + + entity.addComponent( + new ItemEjectorComponent({ + slots: [ + { + pos: new Vector(0, 0), + direction: enumDirection.top, + beltLength: 0.5, + }, + ], + }) + ); + + entity.addComponent( + new ItemProcessorComponent({ + processorType: enumItemProcessorTypes.reader, + inputsPerCharge: 1, + }) + ); + + entity.addComponent(new BeltReaderComponent()); + } +} diff --git a/src/js/game/buildings/stacker.js b/src/js/game/buildings/stacker.js index 6b70365d..35d2b64e 100644 --- a/src/js/game/buildings/stacker.js +++ b/src/js/game/buildings/stacker.js @@ -7,6 +7,7 @@ import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/it import { Entity } from "../entity"; import { MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; +import { BELT_BORDER } from "../systems/belt"; import { enumHubGoalRewards } from "../tutorial_goals"; export class MetaStackerBuilding extends MetaBuilding { @@ -65,11 +66,13 @@ export class MetaStackerBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [enumDirection.bottom], + beltLength: BELT_BORDER, filter: "shape", }, { pos: new Vector(1, 0), directions: [enumDirection.bottom], + beltLength: BELT_BORDER, filter: "shape", }, ], diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index 9c9247e6..daba7987 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -9,7 +9,6 @@ import { UndergroundBeltComponent } from "./components/underground_belt"; import { HubComponent } from "./components/hub"; import { StorageComponent } from "./components/storage"; import { WiredPinsComponent } from "./components/wired_pins"; -import { BeltUnderlaysComponent } from "./components/belt_underlays"; import { WireComponent } from "./components/wire"; import { ConstantSignalComponent } from "./components/constant_signal"; import { LogicGateComponent } from "./components/logic_gate"; @@ -32,7 +31,6 @@ export function initComponentRegistry() { gComponentRegistry.register(HubComponent); gComponentRegistry.register(StorageComponent); gComponentRegistry.register(WiredPinsComponent); - gComponentRegistry.register(BeltUnderlaysComponent); gComponentRegistry.register(WireComponent); gComponentRegistry.register(ConstantSignalComponent); gComponentRegistry.register(LogicGateComponent); diff --git a/src/js/game/components/belt_underlays.js b/src/js/game/components/belt_underlays.js deleted file mode 100644 index 63b265d0..00000000 --- a/src/js/game/components/belt_underlays.js +++ /dev/null @@ -1,41 +0,0 @@ -import { enumDirection, Vector } from "../../core/vector"; -import { Component } from "../component"; - -/** - * Store which type an underlay is, this is cached so we can easily - * render it. - * - * Full: Render underlay at top and bottom of tile - * Bottom Only: Only render underlay at the bottom half - * Top Only: - * @enum {string} - */ -export const enumClippedBeltUnderlayType = { - full: "full", - bottomOnly: "bottomOnly", - topOnly: "topOnly", - none: "none", -}; - -/** - * @typedef {{ - * pos: Vector, - * direction: enumDirection, - * cachedType?: enumClippedBeltUnderlayType - * }} BeltUnderlayTile - */ - -export class BeltUnderlaysComponent extends Component { - static getId() { - return "BeltUnderlays"; - } - - /** - * @param {object} param0 - * @param {Array=} param0.underlays Where to render belt underlays - */ - constructor({ underlays = [] }) { - super(); - this.underlays = underlays; - } -} diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index 354f9024..3827f08f 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -6,6 +6,7 @@ import { Component } from "../component"; /** @typedef {{ * pos: Vector, * directions: enumDirection[], + * beltLength?: number, * filter?: ItemType * }} ItemAcceptorSlot */ @@ -20,6 +21,7 @@ import { Component } from "../component"; /** @typedef {{ * pos: Vector, * directions: enumDirection[], + * beltLength?: number, * filter?: ItemType * }} ItemAcceptorSlotConfig */ @@ -65,6 +67,7 @@ export class ItemAcceptorComponent extends Component { this.slots.push({ pos: slot.pos, directions: slot.directions, + beltLength: slot.beltLength, // Which type of item to accept (shape | color | all) @see ItemType filter: slot.filter, diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index bfc54cd8..1a993dd0 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -10,6 +10,7 @@ import { typeItemSingleton } from "../item_resolver"; * @typedef {{ * pos: Vector, * direction: enumDirection, + * beltLength?: number, * item: BaseItem, * lastItem: BaseItem, * progress: number?, @@ -39,7 +40,7 @@ export class ItemEjectorComponent extends Component { /** * * @param {object} param0 - * @param {Array<{pos: Vector, direction: enumDirection }>=} param0.slots The slots to eject on + * @param {Array<{pos: Vector, direction: enumDirection, beltLength?: number }>=} param0.slots The slots to eject on * @param {boolean=} param0.renderFloatingItems Whether to render items even if they are not connected */ constructor({ slots = [], renderFloatingItems = true }) { @@ -58,7 +59,7 @@ export class ItemEjectorComponent extends Component { } /** - * @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on + * @param {Array<{pos: Vector, direction: enumDirection, beltLength?: number }>} slots The slots to eject on */ setSlots(slots) { /** @type {Array} */ @@ -68,6 +69,7 @@ export class ItemEjectorComponent extends Component { this.slots.push({ pos: slot.pos, direction: slot.direction, + beltLength: slot.beltLength, item: null, lastItem: null, progress: 0, diff --git a/src/js/game/entity_components.js b/src/js/game/entity_components.js index 163be9f9..f0bd2a06 100644 --- a/src/js/game/entity_components.js +++ b/src/js/game/entity_components.js @@ -1,6 +1,5 @@ /* typehints:start */ import { BeltComponent } from "./components/belt"; -import { BeltUnderlaysComponent } from "./components/belt_underlays"; import { HubComponent } from "./components/hub"; import { ItemAcceptorComponent } from "./components/item_acceptor"; import { ItemEjectorComponent } from "./components/item_ejector"; @@ -60,9 +59,6 @@ export class EntityComponentStorage { /** @type {WiredPinsComponent} */ this.WiredPins; - /** @type {BeltUnderlaysComponent} */ - this.BeltUnderlays; - /** @type {WireComponent} */ this.Wire; diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index 08609f89..ef1e53fc 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -14,7 +14,6 @@ import { StaticMapEntitySystem } from "./systems/static_map_entity"; import { ItemAcceptorSystem } from "./systems/item_acceptor"; import { StorageSystem } from "./systems/storage"; import { WiredPinsSystem } from "./systems/wired_pins"; -import { BeltUnderlaysSystem } from "./systems/belt_underlays"; import { WireSystem } from "./systems/wire"; import { ConstantSignalSystem } from "./systems/constant_signal"; import { LogicGateSystem } from "./systems/logic_gate"; @@ -27,6 +26,8 @@ import { ItemProducerSystem } from "./systems/item_producer"; import { ConstantProducerSystem } from "./systems/constant_producer"; import { GoalAcceptorSystem } from "./systems/goal_acceptor"; import { ZoneSystem } from "./systems/zone"; +import { AcceptorBeltSystem } from "./systems/acceptor_belt"; +import { EjectorBeltSystem } from "./systems/ejector_belt"; const logger = createLogger("game_system_manager"); @@ -73,8 +74,11 @@ export class GameSystemManager { /** @type {WiredPinsSystem} */ wiredPins: null, - /** @type {BeltUnderlaysSystem} */ - beltUnderlays: null, + /** @type {AcceptorBeltSystem} */ + acceptorBelt: null, + + /** @type {EjectorBeltSystem} */ + ejectorBelt: null, /** @type {WireSystem} */ wire: null, @@ -160,7 +164,9 @@ export class GameSystemManager { add("wiredPins", WiredPinsSystem); - add("beltUnderlays", BeltUnderlaysSystem); + add("acceptorBelt", AcceptorBeltSystem); + + add("ejectorBelt", EjectorBeltSystem); add("constantSignal", ConstantSignalSystem); diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 947b7a9f..f81add4b 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -50,7 +50,6 @@ export class MapChunkView extends MapChunk { systems.mapResources.drawChunk(parameters, this); } - systems.beltUnderlays.drawChunk(parameters, this); systems.belt.drawChunk(parameters, this); } diff --git a/src/js/game/systems/acceptor_belt.js b/src/js/game/systems/acceptor_belt.js new file mode 100644 index 00000000..718ccdec --- /dev/null +++ b/src/js/game/systems/acceptor_belt.js @@ -0,0 +1,172 @@ +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { Loader } from "../../core/loader"; +import { Rectangle } from "../../core/rectangle"; +import { + enumDirection, + enumDirectionToAngle, + enumDirectionToVector, + enumInvertedDirections, + Vector, +} from "../../core/vector"; +import { BeltPath } from "../belt_path"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { Entity } from "../entity"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { MapChunkView } from "../map_chunk_view"; +import { BELT_ANIM_COUNT } from "./belt"; + +// nearly identical to systems/ejector_belt.js +export class AcceptorBeltSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [ItemAcceptorComponent]); + + this.underlayBeltSprites = []; + + for (let i = 0; i < BELT_ANIM_COUNT; ++i) { + this.underlayBeltSprites.push(Loader.getSprite("sprites/belt/built/forward_" + i + ".png")); + } + } + + /** + * Gets the adjacent entity that ejects to a tile + * @param {Vector} toTile + * @param {enumDirection} toDirection + * @returns {Entity} + */ + getSourceEntity(toTile, toDirection) { + const toDirectionVector = enumDirectionToVector[toDirection]; + const tile = toTile.sub(toDirectionVector); + + const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); + if (!contents) { + return null; + } + + const staticComp = contents.components.StaticMapEntity; + + // Check if its a belt, since then its simple + const beltComp = contents.components.Belt; + if (beltComp) { + return staticComp.localDirectionToWorld(beltComp.direction) === toDirection ? contents : null; + } + + // Check for an ejector + const ejectorComp = contents.components.ItemEjector; + if (ejectorComp) { + // Check each slot to see if its connected + for (let i = 0; i < ejectorComp.slots.length; ++i) { + const slot = ejectorComp.slots[i]; + const slotTile = staticComp.localTileToWorld(slot.pos); + + // Step 1: Check if the tile matches + if (!slotTile.equals(tile)) { + continue; + } + + // Step 2: Check if the direction matches + const slotDirection = staticComp.localDirectionToWorld(slot.direction); + if (slotDirection === toDirection) { + return contents; + } + } + } + + return null; + } + + /** + * Draws a given chunk + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + * @param {object} param0 + * @param {number} param0.animationIndex + * @param {boolean} param0.simplifiedBelts + * @param {BeltPath} param0.hoveredBeltPath + */ + internalDrawChunk(parameters, chunk, { animationIndex, simplifiedBelts, hoveredBeltPath }) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const acceptorComp = entity.components.ItemAcceptor; + if (!acceptorComp) { + continue; + } + + const staticComp = entity.components.StaticMapEntity; + for (let i = 0; i < acceptorComp.slots.length; ++i) { + // Extract underlay parameters + const { pos, directions, beltLength } = acceptorComp.slots[i]; + + // skips both missing and 0 belt lengths + if (!beltLength) { + continue; + } + + const transformedPos = staticComp.localTileToWorld(pos); + const destX = transformedPos.x * globalConfig.tileSize; + const destY = transformedPos.y * globalConfig.tileSize; + + // Culling, Part 1: Check if the chunk contains the tile + if (!chunk.tileSpaceRectangle.containsPoint(transformedPos.x, transformedPos.y)) { + continue; + } + + // Culling, Part 2: Check if the overlay is visible + if ( + !parameters.visibleRect.containsRect4Params( + destX, + destY, + globalConfig.tileSize, + globalConfig.tileSize + ) + ) { + continue; + } + + for (let j = 0; j < directions.length; ++j) { + const direction = directions[j]; + + // Extract direction and angle + const worldDirection = + enumInvertedDirections[staticComp.localDirectionToWorld(direction)]; + const worldDirectionVector = enumDirectionToVector[worldDirection]; + const angle = enumDirectionToAngle[worldDirection]; + + // check if connected + const sourceEntity = this.getSourceEntity(transformedPos, worldDirection); + if (!sourceEntity) { + continue; + } + + const sourceBeltComp = sourceEntity.components.Belt; + const sourceBeltPath = sourceBeltComp ? sourceBeltComp.assignedPath : null; + + const clipRect = new Rectangle(0, 1 - beltLength, 1, beltLength); + + // Actually draw the sprite + const x = destX + globalConfig.halfTileSize; + const y = destY + globalConfig.halfTileSize; + const angleRadians = Math.radians(angle); + + parameters.context.translate(x, y); + parameters.context.rotate(angleRadians); + this.underlayBeltSprites[ + !simplifiedBelts || (sourceBeltPath && sourceBeltPath === hoveredBeltPath) + ? animationIndex % BELT_ANIM_COUNT + : 0 + ].drawCachedWithClipRect( + parameters, + -globalConfig.halfTileSize, + -globalConfig.halfTileSize, + globalConfig.tileSize, + globalConfig.tileSize, + clipRect + ); + parameters.context.rotate(-angleRadians); + parameters.context.translate(-x, -y); + } + } + } + } +} diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 00491eff..0f17cdf2 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -17,6 +17,10 @@ import { defaultBuildingVariant } from "../meta_building"; export const BELT_ANIM_COUNT = 14; +// width of the empty space to the side of the belt sprite, ignoring border width, in tiles +// see generate_belt_sprites.js +export const BELT_BORDER = 23.5 / 192; + const logger = createLogger("belt"); /** @@ -492,57 +496,69 @@ export class BeltSystem extends GameSystemWithFilter { * Draws a given chunk * @param {DrawParameters} parameters * @param {MapChunkView} chunk + * @param {object} param0 + * @param {number} param0.animationIndex + * @param {boolean} param0.simplifiedBelts + * @param {BeltPath} param0.hoveredBeltPath + * + */ + internalDrawChunk(parameters, chunk, { animationIndex, simplifiedBelts, hoveredBeltPath }) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + if (entity.components.Belt) { + const { direction, assignedPath } = entity.components.Belt; + const sprite = this.beltAnimations[direction][ + !simplifiedBelts || assignedPath === hoveredBeltPath ? animationIndex : 0 + ]; + + // Culling happens within the static map entity component + entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); + } + } + } + + /** + * Draws a given chunk, including acceptor/ejector belts + * @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; + const animationIndex = + Math.floor( + ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * + globalConfig.itemSpacingOnBelts + ) % BELT_ANIM_COUNT; - if (this.root.app.settings.getAllSettings().simplifiedBelts) { + const simplifiedBelts = this.root.app.settings.getAllSettings().simplifiedBelts; + + let hoveredBeltPath = null; + if (simplifiedBelts) { // POTATO Mode: Only show items when belt is hovered - let hoveredBeltPath = null; const mousePos = this.root.app.mousePosition; if (mousePos && this.root.currentLayer === "regular") { const tile = this.root.camera.screenToWorld(mousePos).toTileSpace(); - const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); - if (contents && contents.components.Belt) { - hoveredBeltPath = contents.components.Belt.assignedPath; - } - } - - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - if (entity.components.Belt) { - const direction = entity.components.Belt.direction; - let sprite = this.beltAnimations[direction][0]; - - if (entity.components.Belt.assignedPath === hoveredBeltPath) { - sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT]; - } - - // Culling happens within the static map entity component - entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); - } - } - } else { - 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); + const entity = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); + if (entity && entity.components.Belt) { + hoveredBeltPath = entity.components.Belt.assignedPath; } } } + + this.internalDrawChunk(parameters, chunk, { animationIndex, simplifiedBelts, hoveredBeltPath }); + this.root.systemMgr.systems.acceptorBelt.internalDrawChunk(parameters, chunk, { + animationIndex, + simplifiedBelts, + hoveredBeltPath, + }); + this.root.systemMgr.systems.ejectorBelt.internalDrawChunk(parameters, chunk, { + animationIndex, + simplifiedBelts, + hoveredBeltPath, + }); } /** diff --git a/src/js/game/systems/belt_underlays.js b/src/js/game/systems/belt_underlays.js deleted file mode 100644 index c5c69d26..00000000 --- a/src/js/game/systems/belt_underlays.js +++ /dev/null @@ -1,300 +0,0 @@ -import { globalConfig } from "../../core/config"; -import { DrawParameters } from "../../core/draw_parameters"; -import { Loader } from "../../core/loader"; -import { Rectangle } from "../../core/rectangle"; -import { FULL_CLIP_RECT } from "../../core/sprites"; -import { StaleAreaDetector } from "../../core/stale_area_detector"; -import { - enumDirection, - enumDirectionToAngle, - enumDirectionToVector, - enumInvertedDirections, - Vector, -} from "../../core/vector"; -import { BeltComponent } from "../components/belt"; -import { BeltUnderlaysComponent, enumClippedBeltUnderlayType } from "../components/belt_underlays"; -import { ItemAcceptorComponent } from "../components/item_acceptor"; -import { ItemEjectorComponent } from "../components/item_ejector"; -import { Entity } from "../entity"; -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { MapChunkView } from "../map_chunk_view"; -import { BELT_ANIM_COUNT } from "./belt"; - -/** - * Mapping from underlay type to clip rect - * @type {Object} - */ -const enumUnderlayTypeToClipRect = { - [enumClippedBeltUnderlayType.none]: null, - [enumClippedBeltUnderlayType.full]: FULL_CLIP_RECT, - [enumClippedBeltUnderlayType.topOnly]: new Rectangle(0, 0, 1, 0.5), - [enumClippedBeltUnderlayType.bottomOnly]: new Rectangle(0, 0.5, 1, 0.5), -}; - -export class BeltUnderlaysSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [BeltUnderlaysComponent]); - - this.underlayBeltSprites = []; - - for (let i = 0; i < BELT_ANIM_COUNT; ++i) { - this.underlayBeltSprites.push(Loader.getSprite("sprites/belt/built/forward_" + i + ".png")); - } - - // Automatically recompute areas - this.staleArea = new StaleAreaDetector({ - root, - name: "belt-underlay", - recomputeMethod: this.recomputeStaleArea.bind(this), - }); - - this.staleArea.recomputeOnComponentsChanged( - [BeltUnderlaysComponent, BeltComponent, ItemAcceptorComponent, ItemEjectorComponent], - 1 - ); - } - - update() { - this.staleArea.update(); - } - - /** - * Called when an area changed - Resets all caches in the given area - * @param {Rectangle} area - */ - recomputeStaleArea(area) { - for (let x = 0; x < area.w; ++x) { - for (let y = 0; y < area.h; ++y) { - const tileX = area.x + x; - const tileY = area.y + y; - const entity = this.root.map.getLayerContentXY(tileX, tileY, "regular"); - if (entity) { - const underlayComp = entity.components.BeltUnderlays; - if (underlayComp) { - for (let i = 0; i < underlayComp.underlays.length; ++i) { - underlayComp.underlays[i].cachedType = null; - } - } - } - } - } - } - - /** - * Checks if a given tile is connected and has an acceptor - * @param {Vector} tile - * @param {enumDirection} fromDirection - * @returns {boolean} - */ - checkIsAcceptorConnected(tile, fromDirection) { - const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); - if (!contents) { - return false; - } - - const staticComp = contents.components.StaticMapEntity; - - // Check if its a belt, since then its simple - const beltComp = contents.components.Belt; - if (beltComp) { - return staticComp.localDirectionToWorld(enumDirection.bottom) === fromDirection; - } - - // Check if there's an item acceptor - const acceptorComp = contents.components.ItemAcceptor; - if (acceptorComp) { - // Check each slot to see if its connected - for (let i = 0; i < acceptorComp.slots.length; ++i) { - const slot = acceptorComp.slots[i]; - const slotTile = staticComp.localTileToWorld(slot.pos); - - // Step 1: Check if the tile matches - if (!slotTile.equals(tile)) { - continue; - } - - // Step 2: Check if any of the directions matches - for (let j = 0; j < slot.directions.length; ++j) { - const slotDirection = staticComp.localDirectionToWorld(slot.directions[j]); - if (slotDirection === fromDirection) { - return true; - } - } - } - } - - return false; - } - - /** - * Checks if a given tile is connected and has an ejector - * @param {Vector} tile - * @param {enumDirection} toDirection - * @returns {boolean} - */ - checkIsEjectorConnected(tile, toDirection) { - const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); - if (!contents) { - return false; - } - - const staticComp = contents.components.StaticMapEntity; - - // Check if its a belt, since then its simple - const beltComp = contents.components.Belt; - if (beltComp) { - return staticComp.localDirectionToWorld(beltComp.direction) === toDirection; - } - - // Check for an ejector - const ejectorComp = contents.components.ItemEjector; - if (ejectorComp) { - // Check each slot to see if its connected - for (let i = 0; i < ejectorComp.slots.length; ++i) { - const slot = ejectorComp.slots[i]; - const slotTile = staticComp.localTileToWorld(slot.pos); - - // Step 1: Check if the tile matches - if (!slotTile.equals(tile)) { - continue; - } - - // Step 2: Check if the direction matches - const slotDirection = staticComp.localDirectionToWorld(slot.direction); - if (slotDirection === toDirection) { - return true; - } - } - } - - return false; - } - - /** - * Computes the flag for a given tile - * @param {Entity} entity - * @param {import("../components/belt_underlays").BeltUnderlayTile} underlayTile - * @returns {enumClippedBeltUnderlayType} The type of the underlay - */ - computeBeltUnderlayType(entity, underlayTile) { - if (underlayTile.cachedType) { - return underlayTile.cachedType; - } - - const staticComp = entity.components.StaticMapEntity; - - const transformedPos = staticComp.localTileToWorld(underlayTile.pos); - const destX = transformedPos.x * globalConfig.tileSize; - const destY = transformedPos.y * globalConfig.tileSize; - - // Extract direction and angle - const worldDirection = staticComp.localDirectionToWorld(underlayTile.direction); - const worldDirectionVector = enumDirectionToVector[worldDirection]; - - // Figure out if there is anything connected at the top - const connectedTop = this.checkIsAcceptorConnected( - transformedPos.add(worldDirectionVector), - enumInvertedDirections[worldDirection] - ); - - // Figure out if there is anything connected at the bottom - const connectedBottom = this.checkIsEjectorConnected( - transformedPos.sub(worldDirectionVector), - worldDirection - ); - - let flag = enumClippedBeltUnderlayType.none; - - if (connectedTop && connectedBottom) { - flag = enumClippedBeltUnderlayType.full; - } else if (connectedTop) { - flag = enumClippedBeltUnderlayType.topOnly; - } else if (connectedBottom) { - flag = enumClippedBeltUnderlayType.bottomOnly; - } - - return (underlayTile.cachedType = flag); - } - - /** - * 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); - - const contents = chunk.containedEntitiesByLayer.regular; - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - const underlayComp = entity.components.BeltUnderlays; - if (!underlayComp) { - continue; - } - - const staticComp = entity.components.StaticMapEntity; - const underlays = underlayComp.underlays; - for (let i = 0; i < underlays.length; ++i) { - // Extract underlay parameters - const { pos, direction } = underlays[i]; - const transformedPos = staticComp.localTileToWorld(pos); - const destX = transformedPos.x * globalConfig.tileSize; - const destY = transformedPos.y * globalConfig.tileSize; - - // Culling, Part 1: Check if the chunk contains the tile - if (!chunk.tileSpaceRectangle.containsPoint(transformedPos.x, transformedPos.y)) { - continue; - } - - // Culling, Part 2: Check if the overlay is visible - if ( - !parameters.visibleRect.containsRect4Params( - destX, - destY, - globalConfig.tileSize, - globalConfig.tileSize - ) - ) { - continue; - } - - // Extract direction and angle - const worldDirection = staticComp.localDirectionToWorld(direction); - const angle = enumDirectionToAngle[worldDirection]; - - const underlayType = this.computeBeltUnderlayType(entity, underlays[i]); - const clipRect = enumUnderlayTypeToClipRect[underlayType]; - if (!clipRect) { - // Empty - continue; - } - - // Actually draw the sprite - const x = destX + globalConfig.halfTileSize; - const y = destY + globalConfig.halfTileSize; - const angleRadians = Math.radians(angle); - - // SYNC with systems/belt.js:drawSingleEntity! - const animationIndex = Math.floor( - ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * - globalConfig.itemSpacingOnBelts - ); - parameters.context.translate(x, y); - parameters.context.rotate(angleRadians); - this.underlayBeltSprites[ - animationIndex % this.underlayBeltSprites.length - ].drawCachedWithClipRect( - parameters, - -globalConfig.halfTileSize, - -globalConfig.halfTileSize, - globalConfig.tileSize, - globalConfig.tileSize, - clipRect - ); - parameters.context.rotate(-angleRadians); - parameters.context.translate(-x, -y); - } - } - } -} diff --git a/src/js/game/systems/ejector_belt.js b/src/js/game/systems/ejector_belt.js new file mode 100644 index 00000000..83fa95ed --- /dev/null +++ b/src/js/game/systems/ejector_belt.js @@ -0,0 +1,110 @@ +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { Loader } from "../../core/loader"; +import { Rectangle } from "../../core/rectangle"; +import { enumDirectionToAngle } from "../../core/vector"; +import { BeltPath } from "../belt_path"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { MapChunkView } from "../map_chunk_view"; +import { BELT_ANIM_COUNT } from "./belt"; + +// nearly identical to systems/acceptor_belt.js +export class EjectorBeltSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [ItemEjectorComponent]); + + this.underlayBeltSprites = []; + + for (let i = 0; i < BELT_ANIM_COUNT; ++i) { + this.underlayBeltSprites.push(Loader.getSprite("sprites/belt/built/forward_" + i + ".png")); + } + } + + /** + * Draws a given chunk + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + * @param {object} param0 + * @param {number} param0.animationIndex + * @param {boolean} param0.simplifiedBelts + * @param {BeltPath} param0.hoveredBeltPath + */ + internalDrawChunk(parameters, chunk, { animationIndex, simplifiedBelts, hoveredBeltPath }) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const ejectorComp = entity.components.ItemEjector; + if (!ejectorComp) { + continue; + } + + const staticComp = entity.components.StaticMapEntity; + for (let i = 0; i < ejectorComp.slots.length; ++i) { + // Extract underlay parameters + const { pos, direction, beltLength, cachedTargetEntity, cachedBeltPath } = ejectorComp.slots[ + i + ]; + + // skips both missing and 0 belt lengths + if (!beltLength) { + continue; + } + + // check if connected + if (!cachedTargetEntity) { + continue; + } + + const transformedPos = staticComp.localTileToWorld(pos); + const destX = transformedPos.x * globalConfig.tileSize; + const destY = transformedPos.y * globalConfig.tileSize; + + // Culling, Part 1: Check if the chunk contains the tile + if (!chunk.tileSpaceRectangle.containsPoint(transformedPos.x, transformedPos.y)) { + continue; + } + + // Culling, Part 2: Check if the overlay is visible + if ( + !parameters.visibleRect.containsRect4Params( + destX, + destY, + globalConfig.tileSize, + globalConfig.tileSize + ) + ) { + continue; + } + + // Extract direction and angle + const worldDirection = staticComp.localDirectionToWorld(direction); + const angle = enumDirectionToAngle[worldDirection]; + + const clipRect = new Rectangle(0, 0, 1, beltLength); + + // Actually draw the sprite + const x = destX + globalConfig.halfTileSize; + const y = destY + globalConfig.halfTileSize; + const angleRadians = Math.radians(angle); + + parameters.context.translate(x, y); + parameters.context.rotate(angleRadians); + this.underlayBeltSprites[ + !simplifiedBelts || (cachedBeltPath && cachedBeltPath === hoveredBeltPath) + ? animationIndex % BELT_ANIM_COUNT + : 0 + ].drawCachedWithClipRect( + parameters, + -globalConfig.halfTileSize, + -globalConfig.halfTileSize, + globalConfig.tileSize, + globalConfig.tileSize, + clipRect + ); + parameters.context.rotate(-angleRadians); + parameters.context.translate(-x, -y); + } + } + } +}