From b2880700e87c7888db579451a55c7f3d02422622 Mon Sep 17 00:00:00 2001 From: tobspr Date: Sat, 15 Aug 2020 22:32:55 +0200 Subject: [PATCH] Huge rendering performance improvements and minor other changes, lots of refactorings --- src/js/core/buffer_maintainer.js | 2 + src/js/core/config.js | 2 + src/js/game/base_item.js | 18 ++- src/js/game/belt_path.js | 10 +- src/js/game/blueprint.js | 2 +- src/js/game/components/static_map_entity.js | 7 +- src/js/game/components/wired_pins.js | 12 ++ src/js/game/core.js | 16 +-- src/js/game/game_system_with_filter.js | 77 +----------- src/js/game/hud/parts/building_placer.js | 2 +- src/js/game/hud/parts/wire_info.js | 11 +- src/js/game/items/boolean_item.js | 3 +- src/js/game/items/color_item.js | 23 +++- src/js/game/items/shape_item.js | 3 +- src/js/game/map_chunk_view.js | 13 +- src/js/game/root.js | 2 + src/js/game/shape_definition.js | 1 - src/js/game/systems/belt.js | 4 +- src/js/game/systems/belt_underlays.js | 20 ++- src/js/game/systems/display.js | 4 +- src/js/game/systems/hub.js | 127 +++++++++++++------ src/js/game/systems/item_acceptor.js | 64 +++++----- src/js/game/systems/item_ejector.js | 71 +++++------ src/js/game/systems/lever.js | 5 +- src/js/game/systems/map_resources.js | 16 +-- src/js/game/systems/miner.js | 62 +++++----- src/js/game/systems/static_map_entity.js | 44 ++++--- src/js/game/systems/storage.js | 64 ++++++---- src/js/game/systems/wire.js | 2 +- src/js/game/systems/wired_pins.js | 129 +++++++++++--------- 30 files changed, 460 insertions(+), 356 deletions(-) diff --git a/src/js/core/buffer_maintainer.js b/src/js/core/buffer_maintainer.js index c28c0ee5..ad832b63 100644 --- a/src/js/core/buffer_maintainer.js +++ b/src/js/core/buffer_maintainer.js @@ -27,6 +27,8 @@ export class BufferMaintainer { this.iterationIndex = 1; this.lastIteration = 0; + + this.root.signals.gameFrameStarted.add(this.update, this); } /** diff --git a/src/js/core/config.js b/src/js/core/config.js index 3f6362c1..8d30aebb 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -53,6 +53,8 @@ export const globalConfig = { beltSpeedItemsPerSecond: 2, minerSpeedItemsPerSecond: 0, // COMPUTED + defaultItemDiameter: 20, + itemSpacingOnBelts: 0.63, wiresSpeedItemsPerSecond: 6, diff --git a/src/js/game/base_item.js b/src/js/game/base_item.js index 61dc3f34..cbd89e7f 100644 --- a/src/js/game/base_item.js +++ b/src/js/game/base_item.js @@ -1,3 +1,4 @@ +import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; import { BasicSerializableObject } from "../savegame/serialization"; @@ -57,7 +58,22 @@ export class BaseItem extends BasicSerializableObject { * @param {DrawParameters} parameters * @param {number=} diameter */ - drawCentered(x, y, parameters, diameter) {} + drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { + if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) { + this.drawItemCenteredImpl(x, y, parameters, diameter); + } + } + + /** + * INTERNAL + * @param {number} x + * @param {number} y + * @param {DrawParameters} parameters + * @param {number=} diameter + */ + drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { + abstract; + } getBackgroundColorAsResource() { abstract; diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 9e665487..0e4b7b79 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1194,9 +1194,13 @@ export class BeltPath extends BasicSerializableObject { const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile(); const distanceAndItem = this.items[currentItemIndex]; - if (parameters.visibleRect.containsCircle(worldPos.x, worldPos.y, 10)) { - distanceAndItem[_item].drawCentered(worldPos.x, worldPos.y, parameters); - } + + distanceAndItem[_item].drawItemCenteredClipped( + worldPos.x, + worldPos.y, + parameters, + globalConfig.defaultItemDiameter + ); // Check for the next item currentItemPos += distanceAndItem[_nextDistance]; diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index dc30554d..ccbbc248 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -92,7 +92,7 @@ export class Blueprint { parameters.context.globalAlpha = 1; } - staticComp.drawSpriteOnFullEntityBounds(parameters, staticComp.getBlueprintSprite(), 0, newPos); + staticComp.drawSpriteOnBoundsClipped(parameters, staticComp.getBlueprintSprite(), 0, newPos); } parameters.context.globalAlpha = 1; } diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index db4c834e..3d138e42 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -162,8 +162,9 @@ export class StaticMapEntityComponent extends Component { * @returns {Vector} */ localTileToWorld(localTile) { - const result = this.applyRotationToVector(localTile); - result.addInplace(this.origin); + const result = localTile.rotateFastMultipleOf90(this.rotation); + result.x += this.origin.x; + result.y += this.origin.y; return result; } @@ -235,7 +236,7 @@ export class StaticMapEntityComponent extends Component { * @param {number=} extrudePixels How many pixels to extrude the sprite * @param {Vector=} overridePosition Whether to drwa the entity at a different location */ - drawSpriteOnFullEntityBounds(parameters, sprite, extrudePixels = 0, overridePosition = null) { + drawSpriteOnBoundsClipped(parameters, sprite, extrudePixels = 0, overridePosition = null) { if (!this.shouldBeDrawn(parameters) && !overridePosition) { return; } diff --git a/src/js/game/components/wired_pins.js b/src/js/game/components/wired_pins.js index 2822ab57..9a19c2b0 100644 --- a/src/js/game/components/wired_pins.js +++ b/src/js/game/components/wired_pins.js @@ -1,6 +1,8 @@ import { enumDirection, Vector } from "../../core/vector"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { types } from "../../savegame/serialization"; +import { typeItemSingleton } from "../item_resolver"; /** @enum {string} */ export const enumPinSlotType = { @@ -27,6 +29,16 @@ export class WiredPinsComponent extends Component { return "WiredPins"; } + static getSchema() { + return { + slots: types.array( + types.structured({ + value: types.nullable(typeItemSingleton), + }) + ), + }; + } + /** * * @param {object} param0 diff --git a/src/js/game/core.js b/src/js/game/core.js index 70d69c2c..2f4dc046 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -329,8 +329,7 @@ export class GameCore { return; } - // Update buffers as the very first - root.buffers.update(); + this.root.signals.gameFrameStarted.dispatch(); root.queue.requireRedraw = false; @@ -390,33 +389,24 @@ export class GameCore { // Map overview root.map.drawOverlay(params); } else { + // Background (grid, resources, etc) root.map.drawBackground(params); // Belt items systems.belt.drawBeltItems(params); - // Items being ejected / accepted currently (animations) - systems.itemEjector.draw(params); - systems.itemAcceptor.draw(params); - - // Miner & Static map entities + // Miner & Static map entities etc. root.map.drawForeground(params); // HUB Overlay systems.hub.draw(params); - // Storage items - systems.storage.draw(params); - // Green wires overlay root.hud.parts.wiresOverlay.draw(params); if (this.root.currentLayer === "wires") { // Static map entities root.map.drawWiresForegroundLayer(params); - - // pins - systems.wiredPins.draw(params); } } diff --git a/src/js/game/game_system_with_filter.js b/src/js/game/game_system_with_filter.js index ef6dfd52..7b1ffbf0 100644 --- a/src/js/game/game_system_with_filter.js +++ b/src/js/game/game_system_with_filter.js @@ -6,8 +6,7 @@ import { Entity } from "./entity"; import { GameRoot } from "./root"; import { GameSystem } from "./game_system"; import { arrayDelete, arrayDeleteValue } from "../core/utils"; -import { DrawParameters } from "../core/draw_parameters"; -import { globalConfig } from "../core/config"; + export class GameSystemWithFilter extends GameSystem { /** * Constructs a new game system with the given component filter. It will process @@ -35,80 +34,6 @@ export class GameSystemWithFilter extends GameSystem { this.root.signals.bulkOperationFinished.add(this.refreshCaches, this); } - /** - * Calls a function for each matching entity on the screen, useful for drawing them - * @param {DrawParameters} parameters - * @param {function} callback - */ - forEachMatchingEntityOnScreen(parameters, callback) { - const cullRange = parameters.visibleRect.toTileCullRectangle(); - if (this.allEntities.length < 100) { - // So, its much quicker to simply perform per-entity checking - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - if (cullRange.containsRect(entity.components.StaticMapEntity.getTileSpaceBounds())) { - callback(parameters, entity); - } - } - return; - } - - const top = cullRange.top(); - const right = cullRange.right(); - const bottom = cullRange.bottom(); - const left = cullRange.left(); - - const border = 1; - const minY = top - border; - const maxY = bottom + border; - const minX = left - border; - const maxX = right + border - 1; - - const map = this.root.map; - - let seenUids = new Set(); - - const chunkStartX = Math.floor(minX / globalConfig.mapChunkSize); - const chunkStartY = Math.floor(minY / globalConfig.mapChunkSize); - - const chunkEndX = Math.ceil(maxX / globalConfig.mapChunkSize); - const chunkEndY = Math.ceil(maxY / globalConfig.mapChunkSize); - - const requiredComponents = this.requiredComponentIds; - - // Render y from top down for proper blending - for (let chunkX = chunkStartX; chunkX <= chunkEndX; ++chunkX) { - for (let chunkY = chunkStartY; chunkY <= chunkEndY; ++chunkY) { - const chunk = map.getChunk(chunkX, chunkY, false); - if (!chunk) { - continue; - } - - // BIG TODO: CULLING ON AN ENTITY BASIS - - const entities = chunk.containedEntities; - entityLoop: for (let i = 0; i < entities.length; ++i) { - const entity = entities[i]; - - // Avoid drawing twice - if (seenUids.has(entity.uid)) { - continue; - } - - seenUids.add(entity.uid); - - for (let i = 0; i < requiredComponents.length; ++i) { - if (!entity.components[requiredComponents[i]]) { - continue entityLoop; - } - } - callback(parameters, entity); - } - } - } - } - /** * @param {Entity} entity */ diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index 013fcde4..461a3431 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -374,7 +374,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { // HACK to draw the entity sprite const previewSprite = metaBuilding.getBlueprintSprite(rotationVariant, this.currentVariant.get()); staticComp.origin = worldPos.divideScalar(globalConfig.tileSize).subScalars(0.5, 0.5); - staticComp.drawSpriteOnFullEntityBounds(parameters, previewSprite); + staticComp.drawSpriteOnBoundsClipped(parameters, previewSprite); staticComp.origin = mouseTile; // Draw ejectors diff --git a/src/js/game/hud/parts/wire_info.js b/src/js/game/hud/parts/wire_info.js index f73d53d7..9394dcc2 100644 --- a/src/js/game/hud/parts/wire_info.js +++ b/src/js/game/hud/parts/wire_info.js @@ -64,11 +64,16 @@ export class HUDWireInfo extends BaseHUDPart { const network = networks[0]; if (network.valueConflict) { - this.spriteConflict.draw(parameters.context, mousePos.x + 10, mousePos.y - 10, 40, 40); + this.spriteConflict.draw(parameters.context, mousePos.x + 15, mousePos.y - 10, 60, 60); } else if (!network.currentValue) { - this.spriteEmpty.draw(parameters.context, mousePos.x + 10, mousePos.y - 10, 40, 40); + this.spriteEmpty.draw(parameters.context, mousePos.x + 15, mousePos.y - 10, 60, 60); } else { - network.currentValue.drawCentered(mousePos.x + 20, mousePos.y, parameters, 40); + network.currentValue.drawItemCenteredClipped( + mousePos.x + 40, + mousePos.y + 10, + parameters, + 60 + ); } } } diff --git a/src/js/game/items/boolean_item.js b/src/js/game/items/boolean_item.js index 57342bde..38422398 100644 --- a/src/js/game/items/boolean_item.js +++ b/src/js/game/items/boolean_item.js @@ -2,6 +2,7 @@ import { DrawParameters } from "../../core/draw_parameters"; import { Loader } from "../../core/loader"; import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; +import { globalConfig } from "../../core/config"; export class BooleanItem extends BaseItem { static getId() { @@ -46,7 +47,7 @@ export class BooleanItem extends BaseItem { * @param {number} diameter * @param {DrawParameters} parameters */ - drawCentered(x, y, parameters, diameter = 12) { + drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { let sprite; if (this.value) { sprite = Loader.getSprite("sprites/wires/boolean_true.png"); diff --git a/src/js/game/items/color_item.js b/src/js/game/items/color_item.js index 76a1e1d0..bed704a2 100644 --- a/src/js/game/items/color_item.js +++ b/src/js/game/items/color_item.js @@ -5,6 +5,7 @@ import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { enumColors, enumColorsToHexCode } from "../colors"; import { THEME } from "../theme"; +import { drawSpriteClipped } from "../../core/draw_utils"; export class ColorItem extends BaseItem { static getId() { @@ -54,23 +55,33 @@ export class ColorItem extends BaseItem { * @param {number} diameter * @param {DrawParameters} parameters */ - drawCentered(x, y, parameters, diameter = 12) { + drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { if (!this.bufferGenerator) { this.bufferGenerator = this.internalGenerateColorBuffer.bind(this); } + const realDiameter = diameter * 0.6; const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); - - const key = diameter + "/" + dpi; + const key = realDiameter + "/" + dpi; const canvas = parameters.root.buffers.getForKey({ key, subKey: this.color, - w: diameter, - h: diameter, + w: realDiameter, + h: realDiameter, dpi, redrawMethod: this.bufferGenerator, }); - parameters.context.drawImage(canvas, x - diameter / 2, y - diameter / 2, diameter, diameter); + + drawSpriteClipped({ + parameters, + sprite: canvas, + x: x - realDiameter / 2, + y: y - realDiameter / 2, + w: realDiameter, + h: realDiameter, + originalW: realDiameter * dpi, + originalH: realDiameter * dpi, + }); } /** * diff --git a/src/js/game/items/shape_item.js b/src/js/game/items/shape_item.js index 44081cab..d99a7251 100644 --- a/src/js/game/items/shape_item.js +++ b/src/js/game/items/shape_item.js @@ -3,6 +3,7 @@ import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { ShapeDefinition } from "../shape_definition"; import { THEME } from "../theme"; +import { globalConfig } from "../../core/config"; export class ShapeItem extends BaseItem { static getId() { @@ -55,7 +56,7 @@ export class ShapeItem extends BaseItem { * @param {DrawParameters} parameters * @param {number=} diameter */ - drawCentered(x, y, parameters, diameter) { + drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { this.definition.drawCentered(x, y, parameters, diameter); } } diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index c62a3401..0918e7af 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -52,10 +52,16 @@ export class MapChunkView extends MapChunk { */ drawForegroundLayer(parameters) { const systems = this.root.systemMgr.systems; + + systems.itemEjector.drawChunk(parameters, this); + systems.itemAcceptor.drawChunk(parameters, this); + systems.miner.drawChunk(parameters, this); + systems.staticMapEntities.drawChunk(parameters, this); systems.lever.drawChunk(parameters, this); systems.display.drawChunk(parameters, this); + systems.storage.drawChunk(parameters, this); } /** @@ -97,11 +103,9 @@ export class MapChunkView extends MapChunk { const destX = this.x * dims + patch.pos.x * globalConfig.tileSize; const destY = this.y * dims + patch.pos.y * globalConfig.tileSize; - const destSize = Math.min(80, 30 / parameters.zoomLevel); + const diameter = Math.min(80, 30 / parameters.zoomLevel); - if (parameters.visibleRect.containsCircle(destX, destY, destSize)) { - patch.item.drawCentered(destX, destY, parameters, destSize); - } + patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter); } } } @@ -265,5 +269,6 @@ export class MapChunkView extends MapChunk { const systems = this.root.systemMgr.systems; systems.wire.drawChunk(parameters, this); systems.staticMapEntities.drawWiresChunk(parameters, this); + systems.wiredPins.drawChunk(parameters, this); } } diff --git a/src/js/game/root.js b/src/js/game/root.js index dfb8025a..dd224dd8 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -149,6 +149,8 @@ export class GameRoot { gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored + gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame + storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()), upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()), diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index 7ddb029c..ad803881 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -2,7 +2,6 @@ import { makeOffscreenBuffer } from "../core/buffer_utils"; import { globalConfig } from "../core/config"; import { smoothenDpi } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; -import { createLogger } from "../core/logging"; import { Vector } from "../core/vector"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index d968e6e1..4d8151f6 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -501,7 +501,9 @@ export class BeltSystem extends GameSystemWithFilter { if (entity.components.Belt) { const direction = entity.components.Belt.direction; const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT]; - entity.components.StaticMapEntity.drawSpriteOnFullEntityBounds(parameters, sprite, 0); + + // Culling happens within the static map entity component + entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); } } } diff --git a/src/js/game/systems/belt_underlays.js b/src/js/game/systems/belt_underlays.js index 26c3e92c..61908045 100644 --- a/src/js/game/systems/belt_underlays.js +++ b/src/js/game/systems/belt_underlays.js @@ -39,10 +39,26 @@ export class BeltUnderlaysSystem extends GameSystemWithFilter { const { pos, direction } = underlays[i]; const transformedPos = staticComp.localTileToWorld(pos); + // Culling if (!chunk.tileSpaceRectangle.containsPoint(transformedPos.x, transformedPos.y)) { continue; } + const destX = transformedPos.x * globalConfig.tileSize; + const destY = transformedPos.y * globalConfig.tileSize; + + // Culling, #2 + if ( + parameters.visibleRect.containsRect4Params( + destX, + destY, + globalConfig.tileSize, + globalConfig.tileSize + ) + ) { + continue; + } + const angle = enumDirectionToAngle[staticComp.localDirectionToWorld(direction)]; // SYNC with systems/belt.js:drawSingleEntity! @@ -54,8 +70,8 @@ export class BeltUnderlaysSystem extends GameSystemWithFilter { drawRotatedSprite({ parameters, sprite: this.underlayBeltSprites[animationIndex % this.underlayBeltSprites.length], - x: (transformedPos.x + 0.5) * globalConfig.tileSize, - y: (transformedPos.y + 0.5) * globalConfig.tileSize, + x: destX + globalConfig.halfTileSize, + y: destY + globalConfig.halfTileSize, angle: Math.radians(angle), size: globalConfig.tileSize, }); diff --git a/src/js/game/systems/display.js b/src/js/game/systems/display.js index 85ce804c..b1bd9e69 100644 --- a/src/js/game/systems/display.js +++ b/src/js/game/systems/display.js @@ -66,9 +66,11 @@ export class DisplaySystem extends GameSystemWithFilter { if (entity && entity.components.Display) { const pinsComp = entity.components.WiredPins; const network = pinsComp.slots[0].linkedNetwork; + if (!network || !network.currentValue) { continue; } + const value = this.getDisplayItem(network.currentValue); if (!value) { @@ -84,7 +86,7 @@ export class DisplaySystem extends GameSystemWithFilter { globalConfig.tileSize ); } else if (value.getItemType() === "shape") { - value.drawCentered( + value.drawItemCenteredClipped( (origin.x + 0.5) * globalConfig.tileSize, (origin.y + 0.5) * globalConfig.tileSize, parameters, diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 3641d9d6..2270f941 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -5,6 +5,14 @@ import { T } from "../../translations"; import { HubComponent } from "../components/hub"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; +import { globalConfig } from "../../core/config"; +import { smoothenDpi } from "../../core/dpi_manager"; +import { drawSpriteClipped } from "../../core/draw_utils"; +import { Rectangle } from "../../core/rectangle"; +import { ORIGINAL_SPRITE_SCALE } from "../../core/sprites"; + +const HUB_SIZE_TILES = 4; +const HUB_SIZE_PIXELS = HUB_SIZE_TILES * globalConfig.tileSize; export class HubSystem extends GameSystemWithFilter { constructor(root) { @@ -13,8 +21,13 @@ export class HubSystem extends GameSystemWithFilter { this.hubSprite = Loader.getSprite("sprites/buildings/hub.png"); } + /** + * @param {DrawParameters} parameters + */ draw(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this)); + for (let i = 0; i < this.allEntities.length; ++i) { + this.drawEntity(parameters, this.allEntities[i]); + } } update() { @@ -27,35 +40,42 @@ export class HubSystem extends GameSystemWithFilter { ); } } - /** - * @param {DrawParameters} parameters - * @param {Entity} entity + * + * @param {HTMLCanvasElement} canvas + * @param {CanvasRenderingContext2D} context + * @param {number} w + * @param {number} h + * @param {number} dpi */ - drawEntity(parameters, entity) { - const context = parameters.context; - const staticComp = entity.components.StaticMapEntity; + redrawHubBaseTexture(canvas, context, w, h, dpi) { + // This method is quite ugly, please ignore it! - if (!staticComp.shouldBeDrawn(parameters)) { - return; - } + context.scale(dpi, dpi); - const pos = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + const parameters = new DrawParameters({ + context, + visibleRect: new Rectangle(0, 0, w, h), + desiredAtlasScale: ORIGINAL_SPRITE_SCALE, + zoomLevel: dpi * 0.75, + root: this.root, + }); - // Background - staticComp.drawSpriteOnFullEntityBounds(parameters, this.hubSprite, 2.2); + context.clearRect(0, 0, w, h); - const definition = this.root.hubGoals.currentGoal.definition; + this.hubSprite.draw(context, 0, 0, w, h); - definition.drawCentered(pos.x - 25, pos.y - 10, parameters, 40); + const definition = this.root.hubGoals.currentGoal.definition; + definition.drawCentered(45, 58, parameters, 36); const goals = this.root.hubGoals.currentGoal; - const textOffsetX = 2; - const textOffsetY = -6; + const textOffsetX = 70; + const textOffsetY = 61; // Deliver count const delivered = this.root.hubGoals.getCurrentGoalDelivered(); + const deliveredText = "" + formatBigNumber(delivered); if (delivered > 9999) { context.font = "bold 16px GameFont"; @@ -66,52 +86,87 @@ export class HubSystem extends GameSystemWithFilter { } context.fillStyle = "#64666e"; context.textAlign = "left"; - context.fillText("" + formatBigNumber(delivered), pos.x + textOffsetX, pos.y + textOffsetY); + context.fillText(deliveredText, textOffsetX, textOffsetY); // Required context.font = "13px GameFont"; context.fillStyle = "#a4a6b0"; - context.fillText( - "/ " + formatBigNumber(goals.required), - pos.x + textOffsetX, - pos.y + textOffsetY + 13 - ); + context.fillText("/ " + formatBigNumber(goals.required), textOffsetX, textOffsetY + 13); // Reward const rewardText = T.storyRewards[goals.reward].title.toUpperCase(); if (rewardText.length > 12) { - context.font = "bold 9px GameFont"; + context.font = "bold 8px GameFont"; } else { - context.font = "bold 11px GameFont"; + context.font = "bold 10px GameFont"; } context.fillStyle = "#fd0752"; context.textAlign = "center"; - context.fillText(rewardText, pos.x, pos.y + 46); + context.fillText(rewardText, HUB_SIZE_PIXELS / 2, 105); - // Level - context.font = "bold 11px GameFont"; + // Level "8" + context.font = "bold 10px GameFont"; context.fillStyle = "#fff"; - context.fillText("" + this.root.hubGoals.level, pos.x - 42, pos.y - 36); + context.fillText("" + this.root.hubGoals.level, 27, 32); - // Texts + // "LVL" context.textAlign = "center"; context.fillStyle = "#fff"; - context.font = "bold 7px GameFont"; - context.fillText(T.buildings.hub.levelShortcut, pos.x - 42, pos.y - 47); + context.font = "bold 6px GameFont"; + context.fillText(T.buildings.hub.levelShortcut, 27, 22); + // "Deliver" context.fillStyle = "#64666e"; - context.font = "bold 11px GameFont"; - context.fillText(T.buildings.hub.deliver.toUpperCase(), pos.x, pos.y - 40); + context.font = "bold 10px GameFont"; + context.fillText(T.buildings.hub.deliver.toUpperCase(), HUB_SIZE_PIXELS / 2, 30); + // "To unlock" const unlockText = T.buildings.hub.toUnlock.toUpperCase(); if (unlockText.length > 15) { context.font = "bold 8px GameFont"; } else { - context.font = "bold 11px GameFont"; + context.font = "bold 10px GameFont"; } - context.fillText(T.buildings.hub.toUnlock.toUpperCase(), pos.x, pos.y + 30); + context.fillText(T.buildings.hub.toUnlock.toUpperCase(), HUB_SIZE_PIXELS / 2, 92); context.textAlign = "left"; } + + /** + * @param {DrawParameters} parameters + * @param {Entity} entity + */ + drawEntity(parameters, entity) { + const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + + // Deliver count + const delivered = this.root.hubGoals.getCurrentGoalDelivered(); + const deliveredText = "" + formatBigNumber(delivered); + + const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); + const canvas = parameters.root.buffers.getForKey({ + key: "hub", + subKey: dpi + "/" + this.root.hubGoals.level + "/" + deliveredText, + w: globalConfig.tileSize * 4, + h: globalConfig.tileSize * 4, + dpi, + redrawMethod: this.redrawHubBaseTexture.bind(this), + }); + + const extrude = 8; + drawSpriteClipped({ + parameters, + sprite: canvas, + x: staticComp.origin.x * globalConfig.tileSize - extrude, + y: staticComp.origin.y * globalConfig.tileSize - extrude, + w: HUB_SIZE_PIXELS + 2 * extrude, + h: HUB_SIZE_PIXELS + 2 * extrude, + originalW: HUB_SIZE_PIXELS * dpi, + originalH: HUB_SIZE_PIXELS * dpi, + }); + } } diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index b67bd867..745da4bf 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -3,8 +3,8 @@ import { DrawParameters } from "../../core/draw_parameters"; import { fastArrayDelete } from "../../core/utils"; import { enumDirectionToVector } from "../../core/vector"; import { ItemAcceptorComponent } from "../components/item_acceptor"; -import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; +import { MapChunkView } from "../map_chunk_view"; export class ItemAcceptorSystem extends GameSystemWithFilter { constructor(root) { @@ -38,43 +38,45 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { } /** - * Draws the acceptor items * @param {DrawParameters} parameters + * @param {MapChunkView} chunk */ - draw(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawEntityRegularLayer.bind(this)); - } + drawChunk(parameters, chunk) { + 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; + } - /** - * @param {DrawParameters} parameters - * @param {Entity} entity - */ - drawEntityRegularLayer(parameters, entity) { - const staticComp = entity.components.StaticMapEntity; - const acceptorComp = entity.components.ItemAcceptor; + const staticComp = entity.components.StaticMapEntity; + for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { + const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ + animIndex + ]; - if (!staticComp.shouldBeDrawn(parameters)) { - return; - } + const slotData = acceptorComp.slots[slotIndex]; + const realSlotPos = staticComp.localTileToWorld(slotData.pos); - for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { - const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ - animIndex - ]; + if (!chunk.tileSpaceRectangle.containsPoint(realSlotPos.x, realSlotPos.y)) { + // Not within this chunk + continue; + } - const slotData = acceptorComp.slots[slotIndex]; + const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; + const finalTile = realSlotPos.subScalars( + fadeOutDirection.x * (animProgress / 2 - 0.5), + fadeOutDirection.y * (animProgress / 2 - 0.5) + ); - const slotWorldPos = staticComp.applyRotationToVector(slotData.pos).add(staticComp.origin); - const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; - const finalTile = slotWorldPos.subScalars( - fadeOutDirection.x * (animProgress / 2 - 0.5), - fadeOutDirection.y * (animProgress / 2 - 0.5) - ); - item.drawCentered( - (finalTile.x + 0.5) * globalConfig.tileSize, - (finalTile.y + 0.5) * globalConfig.tileSize, - parameters - ); + item.drawItemCenteredClipped( + (finalTile.x + 0.5) * globalConfig.tileSize, + (finalTile.y + 0.5) * globalConfig.tileSize, + parameters, + globalConfig.defaultItemDiameter + ); + } } } } diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 130c87f0..6804925d 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -8,6 +8,7 @@ import { ItemEjectorComponent } from "../components/item_ejector"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { enumItemProcessorTypes } from "../components/item_processor"; +import { MapChunkView } from "../map_chunk_view"; const logger = createLogger("systems/ejector"); @@ -336,50 +337,52 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } /** - * Draws everything * @param {DrawParameters} parameters + * @param {MapChunkView} chunk */ - draw(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawSingleEntity.bind(this)); - } + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; - /** - * @param {DrawParameters} parameters - * @param {Entity} entity - */ - drawSingleEntity(parameters, entity) { - const ejectorComp = entity.components.ItemEjector; - const staticComp = entity.components.StaticMapEntity; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const ejectorComp = entity.components.ItemEjector; + if (!ejectorComp) { + continue; + } - if (!staticComp.shouldBeDrawn(parameters)) { - return; - } + const staticComp = entity.components.StaticMapEntity; - for (let i = 0; i < ejectorComp.slots.length; ++i) { - const slot = ejectorComp.slots[i]; - const ejectedItem = slot.item; + for (let i = 0; i < ejectorComp.slots.length; ++i) { + const slot = ejectorComp.slots[i]; + const ejectedItem = slot.item; - if (!ejectedItem) { - // No item - continue; - } + if (!ejectedItem) { + // No item + continue; + } - const realPosition = slot.pos.rotateFastMultipleOf90(staticComp.rotation); - const realDirection = Vector.transformDirectionFromMultipleOf90( - slot.direction, - staticComp.rotation - ); - const realDirectionVector = enumDirectionToVector[realDirection]; + const realPosition = staticComp.localTileToWorld(slot.pos); + if (!chunk.tileSpaceRectangle.containsPoint(realPosition.x, realPosition.y)) { + // Not within this chunk + continue; + } + + const realDirection = staticComp.localDirectionToWorld(slot.direction); + const realDirectionVector = enumDirectionToVector[realDirection]; - const tileX = - staticComp.origin.x + realPosition.x + 0.5 + realDirectionVector.x * 0.5 * slot.progress; - const tileY = - staticComp.origin.y + realPosition.y + 0.5 + realDirectionVector.y * 0.5 * slot.progress; + const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * slot.progress; + const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * slot.progress; - const worldX = tileX * globalConfig.tileSize; - const worldY = tileY * globalConfig.tileSize; + const worldX = tileX * globalConfig.tileSize; + const worldY = tileY * globalConfig.tileSize; - ejectedItem.drawCentered(worldX, worldY, parameters); + ejectedItem.drawItemCenteredClipped( + worldX, + worldY, + parameters, + globalConfig.defaultItemDiameter + ); + } } } } diff --git a/src/js/game/systems/lever.js b/src/js/game/systems/lever.js index fa727535..0d538afc 100644 --- a/src/js/game/systems/lever.js +++ b/src/js/game/systems/lever.js @@ -34,8 +34,9 @@ export class LeverSystem extends GameSystemWithFilter { const contents = chunk.containedEntitiesByLayer.regular; for (let i = 0; i < contents.length; ++i) { const entity = contents[i]; - if (entity && entity.components.Lever) { - const sprite = entity.components.Lever.toggled ? this.spriteOn : this.spriteOff; + const leverComp = entity.components.Lever; + if (leverComp) { + const sprite = leverComp.toggled ? this.spriteOn : this.spriteOff; const origin = entity.components.StaticMapEntity.origin; sprite.drawCached( parameters, diff --git a/src/js/game/systems/map_resources.js b/src/js/game/systems/map_resources.js index 8a005a21..e5dcb5a0 100644 --- a/src/js/game/systems/map_resources.js +++ b/src/js/game/systems/map_resources.js @@ -42,10 +42,9 @@ export class MapResourcesSystem extends GameSystem { const patch = chunk.patches[i]; const destX = chunk.x * globalConfig.mapChunkWorldSize + patch.pos.x * globalConfig.tileSize; const destY = chunk.y * globalConfig.mapChunkWorldSize + patch.pos.y * globalConfig.tileSize; - const destSize = Math.min(80, 40 / parameters.zoomLevel); - if (parameters.visibleRect.containsCircle(destX, destY, destSize / 2)) { - patch.item.drawCentered(destX, destY, parameters, destSize); - } + const diameter = Math.min(80, 40 / parameters.zoomLevel); + + patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter); } } else { // HIGH QUALITY: Draw all items @@ -61,9 +60,12 @@ export class MapResourcesSystem extends GameSystem { const destX = worldX + globalConfig.halfTileSize; const destY = worldY + globalConfig.halfTileSize; - if (parameters.visibleRect.containsCircle(destX, destY, globalConfig.tileSize / 2)) { - lowerItem.drawCentered(destX, destY, parameters); - } + lowerItem.drawItemCenteredClipped( + destX, + destY, + parameters, + globalConfig.defaultItemDiameter + ); } } } diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index 0f1fbc45..deff1557 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -102,41 +102,39 @@ export class MinerSystem extends GameSystemWithFilter { * @param {MapChunkView} chunk */ drawChunk(parameters, chunk) { - const contents = chunk.contents; - for (let y = 0; y < globalConfig.mapChunkSize; ++y) { - for (let x = 0; x < globalConfig.mapChunkSize; ++x) { - const entity = contents[x][y]; - - if (entity && entity.components.Miner) { - const staticComp = entity.components.StaticMapEntity; - const minerComp = entity.components.Miner; - if (!staticComp.shouldBeDrawn(parameters)) { - continue; - } - if (!minerComp.cachedMinedItem) { - continue; - } + const contents = chunk.containedEntitiesByLayer.regular; - if (minerComp.cachedMinedItem) { - const padding = 3; - parameters.context.fillStyle = minerComp.cachedMinedItem.getBackgroundColorAsResource(); - parameters.context.fillRect( - staticComp.origin.x * globalConfig.tileSize + padding, - staticComp.origin.y * globalConfig.tileSize + padding, - globalConfig.tileSize - 2 * padding, - globalConfig.tileSize - 2 * padding - ); - } + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const minerComp = entity.components.Miner; + if (!minerComp) { + continue; + } - if (minerComp.cachedMinedItem) { - minerComp.cachedMinedItem.drawCentered( - (0.5 + staticComp.origin.x) * globalConfig.tileSize, - (0.5 + staticComp.origin.y) * globalConfig.tileSize, - parameters - ); - } - } + const staticComp = entity.components.StaticMapEntity; + if (!minerComp.cachedMinedItem) { + continue; + } + + // Draw the item background - this is to hide the ejected item animation from + // the item ejecto + + const padding = 3; + const destX = staticComp.origin.x * globalConfig.tileSize + padding; + const destY = staticComp.origin.y * globalConfig.tileSize + padding; + const dimensions = globalConfig.tileSize - 2 * padding; + + if (parameters.visibleRect.containsRect4Params(destX, destY, dimensions, dimensions)) { + parameters.context.fillStyle = minerComp.cachedMinedItem.getBackgroundColorAsResource(); + parameters.context.fillRect(destX, destY, dimensions, dimensions); } + + minerComp.cachedMinedItem.drawItemCenteredClipped( + (0.5 + staticComp.origin.x) * globalConfig.tileSize, + (0.5 + staticComp.origin.y) * globalConfig.tileSize, + parameters, + globalConfig.defaultItemDiameter + ); } } } diff --git a/src/js/game/systems/static_map_entity.js b/src/js/game/systems/static_map_entity.js index 00de6b5a..da6575a5 100644 --- a/src/js/game/systems/static_map_entity.js +++ b/src/js/game/systems/static_map_entity.js @@ -6,6 +6,18 @@ import { MapChunkView } from "../map_chunk_view"; export class StaticMapEntitySystem extends GameSystem { constructor(root) { super(root); + + /** @type {Set} */ + this.drawnUids = new Set(); + + this.root.signals.gameFrameStarted.add(this.clearUidList, this); + } + + /** + * Clears the uid list when a new frame started + */ + clearUidList() { + this.drawnUids.clear(); } /** @@ -18,25 +30,21 @@ export class StaticMapEntitySystem extends GameSystem { return; } - const drawnUids = new Set(); - - const contents = chunk.contents; - for (let y = 0; y < globalConfig.mapChunkSize; ++y) { - for (let x = 0; x < globalConfig.mapChunkSize; ++x) { - const entity = contents[x][y]; - - if (entity) { - if (drawnUids.has(entity.uid)) { - continue; - } - drawnUids.add(entity.uid); - const staticComp = entity.components.StaticMapEntity; + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; - const sprite = staticComp.getSprite(); - if (sprite) { - staticComp.drawSpriteOnFullEntityBounds(parameters, sprite, 2); - } + const staticComp = entity.components.StaticMapEntity; + const sprite = staticComp.getSprite(); + if (sprite) { + // Avoid drawing an entity twice which has been drawn for + // another chunk already + if (this.drawnUids.has(entity.uid)) { + continue; } + + this.drawnUids.add(entity.uid); + staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2); } } } @@ -65,7 +73,7 @@ export class StaticMapEntitySystem extends GameSystem { const sprite = staticComp.getSprite(); if (sprite) { - staticComp.drawSpriteOnFullEntityBounds(parameters, sprite, 2); + staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2); } } } diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index 5da8c9a4..5a2b57bb 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -1,16 +1,28 @@ import { GameSystemWithFilter } from "../game_system_with_filter"; import { StorageComponent } from "../components/storage"; -import { Entity } from "../entity"; import { DrawParameters } from "../../core/draw_parameters"; import { formatBigNumber, lerp } from "../../core/utils"; import { Loader } from "../../core/loader"; import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; +import { MapChunkView } from "../map_chunk_view"; export class StorageSystem extends GameSystemWithFilter { constructor(root) { super(root, [StorageComponent]); this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png"); + + /** + * Stores which uids were already drawn to avoid drawing entities twice + * @type {Set} + */ + this.drawnUids = new Set(); + + this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this); + } + + clearDrawnUids() { + this.drawnUids.clear(); } update() { @@ -43,38 +55,46 @@ export class StorageSystem extends GameSystemWithFilter { } } - draw(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this)); - } - /** * @param {DrawParameters} parameters - * @param {Entity} entity + * @param {MapChunkView} chunk */ - drawEntity(parameters, entity) { - const context = parameters.context; - const staticComp = entity.components.StaticMapEntity; + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const storageComp = entity.components.Storage; + if (!storageComp) { + continue; + } - if (!staticComp.shouldBeDrawn(parameters)) { - return; - } + const storedItem = storageComp.storedItem; + if (!storedItem) { + continue; + } + + if (this.drawnUids.has(entity.uid)) { + continue; + } - const storageComp = entity.components.Storage; + this.drawnUids.add(entity.uid); - const storedItem = storageComp.storedItem; - if (storedItem !== null) { + const staticComp = entity.components.StaticMapEntity; + + const context = parameters.context; context.globalAlpha = storageComp.overlayOpacity; const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); - storedItem.drawCentered(center.x, center.y, parameters, 30); + storedItem.drawItemCenteredClipped(center.x, center.y, parameters, 30); this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15); - context.font = "bold 10px GameFont"; - context.textAlign = "center"; - context.fillStyle = "#64666e"; - context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5); - - context.textAlign = "left"; + if (parameters.visibleRect.containsCircle(center.x, center.y + 25, 20)) { + context.font = "bold 10px GameFont"; + context.textAlign = "center"; + context.fillStyle = "#64666e"; + context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5); + context.textAlign = "left"; + } context.globalAlpha = 1; } } diff --git a/src/js/game/systems/wire.js b/src/js/game/systems/wire.js index 19a2d8be..a25f6040 100644 --- a/src/js/game/systems/wire.js +++ b/src/js/game/systems/wire.js @@ -624,7 +624,7 @@ export class WireSystem extends GameSystemWithFilter { assert(sprite, "Unknown wire type: " + wireType); const staticComp = entity.components.StaticMapEntity; parameters.context.globalAlpha = opacity; - staticComp.drawSpriteOnFullEntityBounds(parameters, sprite, 0); + staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 0); parameters.context.globalAlpha = 1; if (G_IS_DEV && globalConfig.debug.renderWireRotations) { diff --git a/src/js/game/systems/wired_pins.js b/src/js/game/systems/wired_pins.js index 02e84372..43771b49 100644 --- a/src/js/game/systems/wired_pins.js +++ b/src/js/game/systems/wired_pins.js @@ -1,13 +1,13 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; +import { drawRotatedSprite } from "../../core/draw_utils"; import { Loader } from "../../core/loader"; -import { Vector, enumDirectionToAngle } from "../../core/vector"; +import { STOP_PROPAGATION } from "../../core/signal"; +import { enumDirectionToAngle, Vector } from "../../core/vector"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; -import { STOP_PROPAGATION } from "../../core/signal"; -import { drawRotatedSprite } from "../../core/draw_utils"; -import { GLOBAL_APP } from "../../core/globals"; +import { MapChunkView } from "../map_chunk_view"; export class WiredPinsSystem extends GameSystemWithFilter { constructor(root) { @@ -146,65 +146,84 @@ export class WiredPinsSystem extends GameSystemWithFilter { // TODO } - /** - * Draws the pins - * @param {DrawParameters} parameters - */ - draw(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawSingleEntity.bind(this)); - } - /** * Draws a given entity * @param {DrawParameters} parameters - * @param {Entity} entity + * @param {MapChunkView} chunk */ - drawSingleEntity(parameters, entity) { - const staticComp = entity.components.StaticMapEntity; - const slots = entity.components.WiredPins.slots; - - for (let i = 0; i < slots.length; ++i) { - const slot = slots[i]; - const tile = staticComp.localTileToWorld(slot.pos); - - const worldPos = tile.toWorldSpaceCenterOfTile(); - const effectiveRotation = Math.radians( - staticComp.rotation + enumDirectionToAngle[slot.direction] - ); - - if (staticComp.getMetaBuilding().getRenderPins()) { - drawRotatedSprite({ - parameters, - sprite: this.pinSprites[slot.type], - x: worldPos.x, - y: worldPos.y, - angle: effectiveRotation, - size: globalConfig.tileSize + 2, - offsetX: 0, - offsetY: 0, - }); + drawChunk(parameters, chunk) { + const contents = chunk.containedEntities; + + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const pinsComp = entity.components.WiredPins; + if (!pinsComp) { + continue; } - // Draw contained item to visualize whats emitted - const value = slot.value; - if (value) { - const offset = new Vector(0, -9).rotated(effectiveRotation); - value.drawCentered(worldPos.x + offset.x, worldPos.y + offset.y, parameters, 9); - } + const staticComp = entity.components.StaticMapEntity; + const slots = pinsComp.slots; - // Debug view - if (G_IS_DEV && globalConfig.debug.renderWireNetworkInfos) { - const offset = new Vector(0, -10).rotated(effectiveRotation); - const network = slot.linkedNetwork; - parameters.context.fillStyle = "blue"; - parameters.context.font = "5px Tahoma"; - parameters.context.textAlign = "center"; - parameters.context.fillText( - network ? "S" + network.uid : "???", - (tile.x + 0.5) * globalConfig.tileSize + offset.x, - (tile.y + 0.5) * globalConfig.tileSize + offset.y + for (let j = 0; j < slots.length; ++j) { + const slot = slots[j]; + const tile = staticComp.localTileToWorld(slot.pos); + + if (!chunk.tileSpaceRectangle.containsPoint(tile.x, tile.y)) { + // Doesn't belong to this chunk + continue; + } + const worldPos = tile.toWorldSpaceCenterOfTile(); + + // Culling + if ( + !parameters.visibleRect.containsCircle(worldPos.x, worldPos.y, globalConfig.halfTileSize) + ) { + continue; + } + + const effectiveRotation = Math.radians( + staticComp.rotation + enumDirectionToAngle[slot.direction] ); - parameters.context.textAlign = "left"; + + if (staticComp.getMetaBuilding().getRenderPins()) { + drawRotatedSprite({ + parameters, + sprite: this.pinSprites[slot.type], + x: worldPos.x, + y: worldPos.y, + angle: effectiveRotation, + size: globalConfig.tileSize + 2, + offsetX: 0, + offsetY: 0, + }); + } + + // Draw contained item to visualize whats emitted + const value = slot.value; + if (value) { + const offset = new Vector(0, -9).rotated(effectiveRotation); + value.drawItemCenteredClipped( + worldPos.x + offset.x, + worldPos.y + offset.y, + parameters, + 9 + ); + } + + // Debug view + if (G_IS_DEV && globalConfig.debug.renderWireNetworkInfos) { + const offset = new Vector(0, -10).rotated(effectiveRotation); + const network = slot.linkedNetwork; + parameters.context.fillStyle = "blue"; + parameters.context.font = "5px Tahoma"; + parameters.context.textAlign = "center"; + parameters.context.fillText( + network ? "S" + network.uid : "???", + (tile.x + 0.5) * globalConfig.tileSize + offset.x, + (tile.y + 0.5) * globalConfig.tileSize + offset.y + ); + parameters.context.textAlign = "left"; + } } } }