Huge rendering performance improvements and minor other changes, lots of refactorings

pull/573/head
tobspr 4 years ago
parent d1a5dd8c9e
commit b2880700e8

@ -27,6 +27,8 @@ export class BufferMaintainer {
this.iterationIndex = 1;
this.lastIteration = 0;
this.root.signals.gameFrameStarted.add(this.update, this);
}
/**

@ -53,6 +53,8 @@ export const globalConfig = {
beltSpeedItemsPerSecond: 2,
minerSpeedItemsPerSecond: 0, // COMPUTED
defaultItemDiameter: 20,
itemSpacingOnBelts: 0.63,
wiresSpeedItemsPerSecond: 6,

@ -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;

@ -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];

@ -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;
}

@ -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;
}

@ -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

@ -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);
}
}

@ -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
*/

@ -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

@ -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
);
}
}
}

@ -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");

@ -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,
});
}
/**
*

@ -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);
}
}

@ -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);
}
}

@ -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()),

@ -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";

@ -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);
}
}
}

@ -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,
});

@ -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,

@ -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,
});
}
}

@ -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
);
}
}
}
}

@ -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
);
}
}
}
}

@ -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,

@ -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
);
}
}
}

@ -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
);
}
}
}

@ -6,6 +6,18 @@ import { MapChunkView } from "../map_chunk_view";
export class StaticMapEntitySystem extends GameSystem {
constructor(root) {
super(root);
/** @type {Set<number>} */
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);
}
}
}

@ -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<number>}
*/
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;
}
}

@ -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) {

@ -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";
}
}
}
}

Loading…
Cancel
Save