1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2026-03-02 03:39:21 +00:00

Initial commit

This commit is contained in:
Tobias Springer
2020-05-09 16:45:23 +02:00
commit 93c6ea683d
304 changed files with 56031 additions and 0 deletions

201
src/js/game/systems/belt.js Normal file
View File

@@ -0,0 +1,201 @@
import { Math_radians, Math_min } from "../../core/builtins";
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { Loader } from "../../core/loader";
import { AtlasSprite } from "../../core/sprites";
import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector";
import { MapChunkView } from "../map_chunk_view";
const BELT_ANIM_COUNT = 6;
export class BeltSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [BeltComponent]);
/**
* @type {Object.<enumDirection, Array<AtlasSprite>>}
*/
this.beltSprites = {
[enumDirection.top]: Loader.getSprite("sprites/belt/forward_0.png"),
[enumDirection.left]: Loader.getSprite("sprites/belt/left_0.png"),
[enumDirection.right]: Loader.getSprite("sprites/belt/right_0.png"),
};
/**
* @type {Object.<enumDirection, Array<AtlasSprite>>}
*/
this.beltAnimations = {
[enumDirection.top]: [
Loader.getSprite("sprites/belt/forward_0.png"),
Loader.getSprite("sprites/belt/forward_1.png"),
Loader.getSprite("sprites/belt/forward_2.png"),
Loader.getSprite("sprites/belt/forward_3.png"),
Loader.getSprite("sprites/belt/forward_4.png"),
Loader.getSprite("sprites/belt/forward_5.png"),
],
[enumDirection.left]: [
Loader.getSprite("sprites/belt/left_0.png"),
Loader.getSprite("sprites/belt/left_1.png"),
Loader.getSprite("sprites/belt/left_2.png"),
Loader.getSprite("sprites/belt/left_3.png"),
Loader.getSprite("sprites/belt/left_4.png"),
Loader.getSprite("sprites/belt/left_5.png"),
],
[enumDirection.right]: [
Loader.getSprite("sprites/belt/right_0.png"),
Loader.getSprite("sprites/belt/right_1.png"),
Loader.getSprite("sprites/belt/right_2.png"),
Loader.getSprite("sprites/belt/right_3.png"),
Loader.getSprite("sprites/belt/right_4.png"),
Loader.getSprite("sprites/belt/right_5.png"),
],
};
}
draw(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this));
}
update() {
const beltSpeed = this.root.hubGoals.getBeltBaseSpeed() * globalConfig.physicsDeltaSeconds;
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const beltComp = entity.components.Belt;
const staticComp = entity.components.StaticMapEntity;
const items = beltComp.sortedItems;
if (items.length === 0) {
// Fast out for performance
continue;
}
const ejectorComp = entity.components.ItemEjector;
let maxProgress = 1;
// When ejecting, we can not go further than the item spacing since it
// will be on the corner
if (ejectorComp.isAnySlotEjecting()) {
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// Find follow up belt to make sure we don't clash items
const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction);
const followUpVector = enumDirectionToVector[followUpDirection];
const followUpTile = staticComp.origin.add(followUpVector);
const followUpEntity = this.root.map.getTileContent(followUpTile);
if (followUpEntity) {
const followUpBeltComp = followUpEntity.components.Belt;
if (followUpBeltComp) {
const spacingOnBelt = followUpBeltComp.getDistanceToFirstItemCenter();
maxProgress = Math_min(1, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt);
}
}
}
let speedMultiplier = 1;
if (beltComp.direction !== enumDirection.top) {
// Shaped belts are longer, thus being quicker
speedMultiplier = 1.41;
}
for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) {
const itemAndProgress = items[itemIndex];
const newProgress = itemAndProgress[0] + speedMultiplier * beltSpeed;
if (newProgress >= 1.0) {
// Try to give this item to a new belt
const freeSlot = ejectorComp.getFirstFreeSlot();
if (freeSlot === null) {
// So, we don't have a free slot - damned!
itemAndProgress[0] = 1.0;
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// We got a free slot, remove this item and keep it on the ejector slot
if (!ejectorComp.tryEject(freeSlot, itemAndProgress[1])) {
assert(false, "Ejection failed");
}
items.splice(itemIndex, 1);
maxProgress = 1;
}
} else {
itemAndProgress[0] = Math_min(newProgress, maxProgress);
maxProgress = itemAndProgress[0] - globalConfig.itemSpacingOnBelts;
}
}
}
}
/**
*
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
if (parameters.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
return;
1;
}
const speedMultiplier = this.root.hubGoals.getBeltBaseSpeed();
// SYNC with systems/item_processor.js:drawEntityUnderlays!
// 126 / 42 is the exact animation speed of the png animation
const animationIndex = Math.floor(
(this.root.time.now() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42
);
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.Belt) {
const direction = entity.components.Belt.direction;
const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT];
entity.components.StaticMapEntity.drawSpriteOnFullEntityBounds(
parameters,
sprite,
0,
false
);
}
}
}
1;
}
/**
* @param {DrawParameters} parameters
* @param {Entity} entity
*/
drawEntityItems(parameters, entity) {
const beltComp = entity.components.Belt;
const staticComp = entity.components.StaticMapEntity;
const items = beltComp.sortedItems;
if (items.length === 0) {
// Fast out for performance
return;
}
for (let i = 0; i < items.length; ++i) {
const itemAndProgress = items[i];
// Nice would be const [pos, item] = itemAndPos; but that gets polyfilled and is super slow then
const progress = itemAndProgress[0];
const item = itemAndProgress[1];
const position = staticComp.applyRotationToVector(beltComp.transformBeltToLocalSpace(progress));
item.draw(
(staticComp.origin.x + position.x + 0.5) * globalConfig.tileSize,
(staticComp.origin.y + position.y + 0.5) * globalConfig.tileSize,
parameters
);
}
}
}

View File

@@ -0,0 +1,83 @@
import { GameSystemWithFilter } from "../game_system_with_filter";
import { HubComponent } from "../components/hub";
import { DrawParameters } from "../../core/draw_parameters";
import { Entity } from "../entity";
import { formatBigNumber } from "../../core/utils";
export class HubSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [HubComponent]);
}
draw(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this));
}
update() {
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const hubComponent = entity.components.Hub;
const queue = hubComponent.definitionsToAnalyze;
for (let k = 0; k < queue.length; ++k) {
const definition = queue[k];
this.root.hubGoals.handleDefinitionDelivered(definition);
}
hubComponent.definitionsToAnalyze = [];
}
}
/**
* @param {DrawParameters} parameters
* @param {Entity} entity
*/
drawEntity(parameters, entity) {
const context = parameters.context;
const staticComp = entity.components.StaticMapEntity;
const pos = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
const definition = this.root.hubGoals.currentGoal.definition;
definition.draw(pos.x - 25, pos.y - 10, parameters, 40);
const goals = this.root.hubGoals.currentGoal;
const textOffsetX = 2;
const textOffsetY = -6;
// Deliver count
context.font = "bold 25px GameFont";
context.fillStyle = "#64666e";
context.textAlign = "left";
context.fillText(
"" + formatBigNumber(this.root.hubGoals.getCurrentGoalDelivered()),
pos.x + textOffsetX,
pos.y + textOffsetY
);
// Required
context.font = "13px GameFont";
context.fillStyle = "#a4a6b0";
context.fillText(
"/ " + formatBigNumber(goals.required),
pos.x + textOffsetX,
pos.y + textOffsetY + 13
);
// Reward
context.font = "bold 11px GameFont";
context.fillStyle = "#fd0752";
context.textAlign = "center";
context.fillText(goals.reward.toUpperCase(), pos.x, pos.y + 46);
// Level
context.font = "bold 11px GameFont";
context.fillStyle = "#fff";
context.fillText("" + this.root.hubGoals.level, pos.x - 42, pos.y - 36);
context.textAlign = "left";
}
}

View File

@@ -0,0 +1,173 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { enumDirectionToVector, Vector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { ItemEjectorComponent } from "../components/item_ejector";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { Math_min } from "../../core/builtins";
export class ItemEjectorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemEjectorComponent]);
}
update() {
const effectiveBeltSpeed = this.root.hubGoals.getBeltBaseSpeed();
const progressGrowth = (effectiveBeltSpeed / 0.5) * globalConfig.physicsDeltaSeconds;
// Try to find acceptors for every ejector
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const ejectorComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity;
// For every ejector slot, try to find an acceptor
for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) {
const ejectorSlot = ejectorComp.slots[ejectorSlotIndex];
const ejectingItem = ejectorSlot.item;
if (!ejectingItem) {
// No item ejected
continue;
}
ejectorSlot.progress = Math_min(1, ejectorSlot.progress + progressGrowth);
if (ejectorSlot.progress < 1.0) {
// Still ejecting
continue;
}
// Figure out where and into which direction we eject items
const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos);
const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction);
const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection];
const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector);
// Try to find the given acceptor component to take the item
const targetEntity = this.root.map.getTileContent(ejectSlotTargetWsTile);
if (!targetEntity) {
// No consumer for item
continue;
}
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
const targetStaticComp = targetEntity.components.StaticMapEntity;
if (!targetAcceptorComp) {
// Entity doesn't accept items
continue;
}
const matchingSlot = targetAcceptorComp.findMatchingSlot(
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection)
);
if (!matchingSlot) {
// No matching slot found
continue;
}
if (!targetAcceptorComp.canAcceptItem(matchingSlot.index, ejectingItem)) {
// Can not accept item
continue;
}
if (
this.tryPassOverItem(
ejectingItem,
targetEntity,
matchingSlot.index,
matchingSlot.acceptedDirection
)
) {
ejectorSlot.item = null;
continue;
}
}
}
}
/**
*
* @param {BaseItem} item
* @param {Entity} receiver
* @param {number} slotIndex
* @param {string} localDirection
*/
tryPassOverItem(item, receiver, slotIndex, localDirection) {
// Try figuring out how what to do with the item
// TODO: Kinda hacky. How to solve this properly? Don't want to go through inheritance hell.
// Also its just a few cases (hope it stays like this .. :x).
const beltComp = receiver.components.Belt;
if (beltComp) {
// Ayy, its a belt!
if (beltComp.canAcceptNewItem(localDirection)) {
beltComp.takeNewItem(item, localDirection);
return true;
}
}
const itemProcessorComp = receiver.components.ItemProcessor;
if (itemProcessorComp) {
// Its an item processor ..
if (itemProcessorComp.tryTakeItem(item, slotIndex, localDirection)) {
return true;
}
}
const undergroundBeltCmop = receiver.components.UndergroundBelt;
if (undergroundBeltCmop) {
// Its an underground belt. yay.
if (
undergroundBeltCmop.tryAcceptExternalItem(
item,
this.root.hubGoals.getUndergroundBeltBaseSpeed()
)
) {
return true;
}
}
return false;
}
draw(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawSingleEntity.bind(this));
}
/**
* @param {DrawParameters} parameters
* @param {Entity} entity
*/
drawSingleEntity(parameters, entity) {
const ejectorComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity;
for (let i = 0; i < ejectorComp.slots.length; ++i) {
const slot = ejectorComp.slots[i];
const ejectedItem = slot.item;
if (!ejectedItem) {
// No item
continue;
}
const realPosition = slot.pos.rotateFastMultipleOf90(staticComp.rotationDegrees);
const realDirection = Vector.transformDirectionFromMultipleOf90(
slot.direction,
staticComp.rotationDegrees
);
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 worldX = tileX * globalConfig.tileSize;
const worldY = tileY * globalConfig.tileSize;
ejectedItem.draw(worldX, worldY, parameters);
}
}
}

View File

@@ -0,0 +1,341 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { Loader } from "../../core/loader";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { ItemProcessorComponent, enumItemProcessorTypes } from "../components/item_processor";
import { Math_max, Math_radians } from "../../core/builtins";
import { BaseItem } from "../base_item";
import { ShapeItem } from "../items/shape_item";
import { enumDirectionToVector, enumDirection, enumDirectionToAngle } from "../../core/vector";
import { ColorItem } from "../items/color_item";
import { enumColorMixingResults } from "../colors";
import { drawRotatedSprite } from "../../core/draw_utils";
export class ItemProcessorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemProcessorComponent]);
this.sprites = {};
for (const key in enumItemProcessorTypes) {
this.sprites[key] = Loader.getSprite("sprites/buildings/" + key + ".png");
}
this.underlayBeltSprites = [
Loader.getSprite("sprites/belt/forward_0.png"),
Loader.getSprite("sprites/belt/forward_1.png"),
Loader.getSprite("sprites/belt/forward_2.png"),
Loader.getSprite("sprites/belt/forward_3.png"),
Loader.getSprite("sprites/belt/forward_4.png"),
Loader.getSprite("sprites/belt/forward_5.png"),
];
}
draw(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this));
}
drawUnderlays(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawEntityUnderlays.bind(this));
}
update() {
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const processorComp = entity.components.ItemProcessor;
const ejectorComp = entity.components.ItemEjector;
// First of all, process the current recipe
processorComp.secondsUntilEject = Math_max(
0,
processorComp.secondsUntilEject - globalConfig.physicsDeltaSeconds
);
// Also, process item consumption animations to avoid items popping from the belts
for (let animIndex = 0; animIndex < processorComp.itemConsumptionAnimations.length; ++animIndex) {
const anim = processorComp.itemConsumptionAnimations[animIndex];
anim.animProgress +=
globalConfig.physicsDeltaSeconds * this.root.hubGoals.getBeltBaseSpeed() * 2;
if (anim.animProgress > 1) {
processorComp.itemConsumptionAnimations.splice(animIndex, 1);
animIndex -= 1;
}
}
// Check if we have any finished items we can eject
if (
processorComp.secondsUntilEject === 0 && // it was processed in time
processorComp.itemsToEject.length > 0 // we have some items left to eject
) {
for (let itemIndex = 0; itemIndex < processorComp.itemsToEject.length; ++itemIndex) {
const { item, requiredSlot, preferredSlot } = processorComp.itemsToEject[itemIndex];
let slot = null;
if (requiredSlot !== null && requiredSlot !== undefined) {
// We have a slot override, check if that is free
if (ejectorComp.canEjectOnSlot(requiredSlot)) {
slot = requiredSlot;
}
} else if (preferredSlot !== null && preferredSlot !== undefined) {
// We have a slot preference, try using it but otherwise use a free slot
if (ejectorComp.canEjectOnSlot(preferredSlot)) {
slot = preferredSlot;
} else {
slot = ejectorComp.getFirstFreeSlot();
}
} else {
// We can eject on any slot
slot = ejectorComp.getFirstFreeSlot();
}
if (slot !== null) {
// Alright, we can actually eject
if (!ejectorComp.tryEject(slot, item)) {
assert(false, "Failed to eject");
} else {
processorComp.itemsToEject.splice(itemIndex, 1);
itemIndex -= 1;
}
}
}
}
// Check if we have an empty queue and can start a new charge
if (processorComp.itemsToEject.length === 0) {
if (processorComp.inputSlots.length === processorComp.inputsPerCharge) {
this.startNewCharge(entity);
}
}
}
}
/**
* Starts a new charge for the entity
* @param {Entity} entity
*/
startNewCharge(entity) {
const processorComp = entity.components.ItemProcessor;
// First, take items
const items = processorComp.inputSlots;
processorComp.inputSlots = [];
const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type);
processorComp.secondsUntilEject = 1 / baseSpeed;
/** @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} */
const outItems = [];
// DO SOME MAGIC
switch (processorComp.type) {
// SPLITTER
case enumItemProcessorTypes.splitter: {
let nextSlot = processorComp.nextOutputSlot++ % 2;
for (let i = 0; i < items.length; ++i) {
outItems.push({
item: items[i].item,
preferredSlot: (nextSlot + i) % 2,
});
}
break;
}
// CUTTER
case enumItemProcessorTypes.cutter: {
const inputItem = /** @type {ShapeItem} */ (items[0].item);
assert(inputItem instanceof ShapeItem, "Input for cut is not a shape");
const inputDefinition = inputItem.definition;
const [cutDefinition1, cutDefinition2] = this.root.shapeDefinitionMgr.shapeActionCutHalf(
inputDefinition
);
if (!cutDefinition1.isEntirelyEmpty()) {
outItems.push({
item: new ShapeItem(cutDefinition1),
requiredSlot: 0,
});
}
if (!cutDefinition2.isEntirelyEmpty()) {
outItems.push({
item: new ShapeItem(cutDefinition2),
requiredSlot: 1,
});
}
break;
}
// ROTATER
case enumItemProcessorTypes.rotater: {
const inputItem = /** @type {ShapeItem} */ (items[0].item);
assert(inputItem instanceof ShapeItem, "Input for cut is not a shape");
const inputDefinition = inputItem.definition;
const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCW(inputDefinition);
outItems.push({
item: new ShapeItem(rotatedDefinition),
});
break;
}
// STACKER
case enumItemProcessorTypes.stacker: {
const item1 = items[0];
const item2 = items[1];
const lowerItem = /** @type {ShapeItem} */ (item1.sourceSlot === 0 ? item1.item : item2.item);
const upperItem = /** @type {ShapeItem} */ (item1.sourceSlot === 1 ? item1.item : item2.item);
assert(lowerItem instanceof ShapeItem, "Input for lower stack is not a shape");
assert(upperItem instanceof ShapeItem, "Input for upper stack is not a shape");
const stackedDefinition = this.root.shapeDefinitionMgr.shapeActionStack(
lowerItem.definition,
upperItem.definition
);
outItems.push({
item: new ShapeItem(stackedDefinition),
});
break;
}
// TRASH
case enumItemProcessorTypes.trash: {
// Well this one is easy .. simply do nothing with the item
break;
}
// MIXER
case enumItemProcessorTypes.mixer: {
// Find both colors and combine them
const item1 = /** @type {ColorItem} */ (items[0].item);
const item2 = /** @type {ColorItem} */ (items[1].item);
assert(item1 instanceof ColorItem, "Input for color mixer is not a color");
assert(item2 instanceof ColorItem, "Input for color mixer is not a color");
const color1 = item1.color;
const color2 = item2.color;
// Try finding mixer color, and if we can't mix it we simply return the same color
const mixedColor = enumColorMixingResults[color1][color2];
let resultColor = color1;
if (mixedColor) {
resultColor = mixedColor;
}
outItems.push({
item: new ColorItem(resultColor),
});
break;
}
// PAINTER
case enumItemProcessorTypes.painter: {
const item1 = items[0];
const item2 = items[1];
const shapeItem = /** @type {ShapeItem} */ (item1.sourceSlot === 0 ? item1.item : item2.item);
const colorItem = /** @type {ColorItem} */ (item1.sourceSlot === 1 ? item1.item : item2.item);
const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith(
shapeItem.definition,
colorItem.color
);
outItems.push({
item: new ShapeItem(colorizedDefinition),
});
break;
}
// HUB
case enumItemProcessorTypes.hub: {
const shapeItem = /** @type {ShapeItem} */ (items[0].item);
const hubComponent = entity.components.Hub;
assert(hubComponent, "Hub item processor has no hub component");
hubComponent.queueShapeDefinition(shapeItem.definition);
break;
}
default:
assertAlways(false, "Unkown item processor type: " + processorComp.type);
}
processorComp.itemsToEject = outItems;
}
/**
* @param {DrawParameters} parameters
* @param {Entity} entity
*/
drawEntity(parameters, entity) {
const staticComp = entity.components.StaticMapEntity;
const processorComp = entity.components.ItemProcessor;
const acceptorComp = entity.components.ItemAcceptor;
for (let animIndex = 0; animIndex < processorComp.itemConsumptionAnimations.length; ++animIndex) {
const { item, slotIndex, animProgress, direction } = processorComp.itemConsumptionAnimations[
animIndex
];
const slotData = acceptorComp.slots[slotIndex];
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.draw(
(finalTile.x + 0.5) * globalConfig.tileSize,
(finalTile.y + 0.5) * globalConfig.tileSize,
parameters
);
}
}
/**
* @param {DrawParameters} parameters
* @param {Entity} entity
*/
drawEntityUnderlays(parameters, entity) {
const staticComp = entity.components.StaticMapEntity;
const processorComp = entity.components.ItemProcessor;
const underlays = processorComp.beltUnderlays;
for (let i = 0; i < underlays.length; ++i) {
const { pos, direction } = underlays[i];
const transformedPos = staticComp.localTileToWorld(pos);
const angle = enumDirectionToAngle[staticComp.localDirectionToWorld(direction)];
// SYNC with systems/belt.js:drawSingleEntity!
const animationIndex = Math.floor(
(this.root.time.now() *
this.root.hubGoals.getBeltBaseSpeed() *
this.underlayBeltSprites.length *
126) /
42
);
drawRotatedSprite({
parameters,
sprite: this.underlayBeltSprites[animationIndex % this.underlayBeltSprites.length],
x: (transformedPos.x + 0.5) * globalConfig.tileSize,
y: (transformedPos.y + 0.5) * globalConfig.tileSize,
angle: Math_radians(angle),
size: globalConfig.tileSize,
});
}
}
}

View File

@@ -0,0 +1,56 @@
import { GameSystem } from "../game_system";
import { DrawParameters } from "../../core/draw_parameters";
import { globalConfig } from "../../core/config";
import { MapChunkView } from "../map_chunk_view";
export class MapResourcesSystem extends GameSystem {
/**
* Draws the map resources
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const renderItems = parameters.zoomLevel >= globalConfig.mapChunkOverviewMinZoom;
parameters.context.globalAlpha = 0.5;
const layer = chunk.lowerLayer;
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
const row = layer[x];
for (let y = 0; y < globalConfig.mapChunkSize; ++y) {
const lowerItem = row[y];
if (lowerItem) {
parameters.context.fillStyle = lowerItem.getBackgroundColorAsResource();
parameters.context.fillRect(
(chunk.tileX + x) * globalConfig.tileSize,
(chunk.tileY + y) * globalConfig.tileSize,
globalConfig.tileSize,
globalConfig.tileSize
);
if (renderItems) {
lowerItem.draw(
(chunk.tileX + x + 0.5) * globalConfig.tileSize,
(chunk.tileY + y + 0.5) * globalConfig.tileSize,
parameters
);
}
}
}
}
parameters.context.globalAlpha = 1;
if (!renderItems) {
// Render patches instead
const patches = chunk.patches;
for (let i = 0; i < patches.length; ++i) {
const { pos, item, size } = patches[i];
item.draw(
(chunk.tileX + pos.x + 0.5) * globalConfig.tileSize,
(chunk.tileY + pos.y + 0.5) * globalConfig.tileSize,
parameters,
80
);
}
}
}
}

View File

@@ -0,0 +1,87 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { MinerComponent } from "../components/miner";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
export class MinerSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [MinerComponent]);
}
update() {
const miningSpeed = this.root.hubGoals.getMinerBaseSpeed();
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const minerComp = entity.components.Miner;
const staticComp = entity.components.StaticMapEntity;
const ejectComp = entity.components.ItemEjector;
if (this.root.time.isIngameTimerExpired(minerComp.lastMiningTime, 1 / miningSpeed)) {
if (!ejectComp.canEjectOnSlot(0)) {
// We can't eject further
continue;
}
// Actually mine
minerComp.lastMiningTime = this.root.time.now();
const lowerLayerItem = this.root.map.getLowerLayerContentXY(
staticComp.origin.x,
staticComp.origin.y
);
if (!lowerLayerItem) {
// Nothing below;
continue;
}
// Try actually ejecting
if (!ejectComp.tryEject(0, lowerLayerItem)) {
assert(false, "Failed to eject");
}
}
}
}
/**
*
* @param {DrawParameters} parameters
* @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 lowerLayerItem = this.root.map.getLowerLayerContentXY(
staticComp.origin.x,
staticComp.origin.y
);
if (lowerLayerItem) {
const padding = 3;
parameters.context.fillStyle = lowerLayerItem.getBackgroundColorAsResource();
parameters.context.fillRect(
staticComp.origin.x * globalConfig.tileSize + padding,
staticComp.origin.y * globalConfig.tileSize + padding,
globalConfig.tileSize - 2 * padding,
globalConfig.tileSize - 2 * padding
);
}
if (lowerLayerItem) {
lowerLayerItem.draw(
(0.5 + staticComp.origin.x) * globalConfig.tileSize,
(0.5 + staticComp.origin.y) * globalConfig.tileSize,
parameters
);
}
}
}
}
}
}

View File

@@ -0,0 +1,72 @@
import { GameSystem } from "../game_system";
import { DrawParameters } from "../../core/draw_parameters";
import { globalConfig } from "../../core/config";
import { MapChunkView } from "../map_chunk_view";
import { Loader } from "../../core/loader";
import { enumDirection } from "../../core/vector";
export class StaticMapEntitySystem extends GameSystem {
constructor(root) {
super(root);
this.beltOverviewSprites = {
[enumDirection.top]: Loader.getSprite("sprites/map_overview/belt_forward.png"),
[enumDirection.right]: Loader.getSprite("sprites/map_overview/belt_right.png"),
[enumDirection.left]: Loader.getSprite("sprites/map_overview/belt_left.png"),
};
}
/**
* Draws the static entities
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) {
return;
}
const drawOutlinesOnly = parameters.zoomLevel < globalConfig.mapChunkOverviewMinZoom;
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) {
const staticComp = entity.components.StaticMapEntity;
if (drawOutlinesOnly) {
const rect = staticComp.getTileSpaceBounds();
parameters.context.fillStyle = staticComp.silhouetteColor || "#aaa";
const beltComp = entity.components.Belt;
if (beltComp) {
const sprite = this.beltOverviewSprites[beltComp.direction];
staticComp.drawSpriteOnFullEntityBounds(parameters, sprite, 0, false);
} else {
parameters.context.fillRect(
rect.x * globalConfig.tileSize,
rect.y * globalConfig.tileSize,
rect.w * globalConfig.tileSize,
rect.h * globalConfig.tileSize
);
}
} else {
const spriteKey = staticComp.spriteKey;
if (spriteKey) {
// Check if origin is contained to avoid drawing entities multiple times
if (
staticComp.origin.x >= chunk.tileX &&
staticComp.origin.x < chunk.tileX + globalConfig.mapChunkSize &&
staticComp.origin.y >= chunk.tileY &&
staticComp.origin.y < chunk.tileY + globalConfig.mapChunkSize
) {
const sprite = Loader.getSprite(spriteKey);
staticComp.drawSpriteOnFullEntityBounds(parameters, sprite, 2, false);
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,129 @@
import { GameSystemWithFilter } from "../game_system_with_filter";
import { UndergroundBeltComponent, enumUndergroundBeltMode } from "../components/underground_belt";
import { Entity } from "../entity";
import { Loader } from "../../core/loader";
import { Math_max } from "../../core/builtins";
import { globalConfig } from "../../core/config";
import { enumDirection, enumDirectionToVector, enumDirectionToAngle } from "../../core/vector";
import { MapChunkView } from "../map_chunk_view";
import { DrawParameters } from "../../core/draw_parameters";
export class UndergroundBeltSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [UndergroundBeltComponent]);
this.beltSprites = {
[enumUndergroundBeltMode.sender]: Loader.getSprite(
"sprites/buildings/underground_belt_entry.png"
),
[enumUndergroundBeltMode.receiver]: Loader.getSprite(
"sprites/buildings/underground_belt_exit.png"
),
};
}
update() {
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const undergroundComp = entity.components.UndergroundBelt;
// Decrease remaining time of all items in belt
for (let k = 0; k < undergroundComp.pendingItems.length; ++k) {
const item = undergroundComp.pendingItems[k];
item[1] = Math_max(0, item[1] - globalConfig.physicsDeltaSeconds);
}
if (undergroundComp.mode === enumUndergroundBeltMode.sender) {
this.handleSender(entity);
} else {
this.handleReceiver(entity);
}
}
}
/**
*
* @param {Entity} entity
*/
handleSender(entity) {
const staticComp = entity.components.StaticMapEntity;
const undergroundComp = entity.components.UndergroundBelt;
// Check if we have any item
if (undergroundComp.pendingItems.length > 0) {
const nextItemAndDuration = undergroundComp.pendingItems[0];
const remainingTime = nextItemAndDuration[1];
const nextItem = nextItemAndDuration[0];
if (remainingTime === 0) {
// Try to find a receiver
const searchDirection = staticComp.localDirectionToWorld(enumDirection.top);
const searchVector = enumDirectionToVector[searchDirection];
const targetRotation = enumDirectionToAngle[searchDirection];
let currentTile = staticComp.origin;
for (
let searchOffset = 0;
searchOffset < globalConfig.undergroundBeltMaxTiles;
++searchOffset
) {
currentTile = currentTile.add(searchVector);
const contents = this.root.map.getTileContent(currentTile);
if (contents) {
const receiverUndergroundComp = contents.components.UndergroundBelt;
if (receiverUndergroundComp) {
const receiverStaticComp = contents.components.StaticMapEntity;
if (receiverStaticComp.rotationDegrees === targetRotation) {
if (receiverUndergroundComp.mode === enumUndergroundBeltMode.receiver) {
// Try to pass over the item to the receiver
if (
receiverUndergroundComp.tryAcceptTunneledItem(
nextItem,
searchOffset,
this.root.hubGoals.getUndergroundBeltBaseSpeed()
)
) {
undergroundComp.pendingItems = [];
}
}
// When we hit some underground belt, always stop, no matter what
break;
}
}
}
}
}
}
}
/**
*
* @param {Entity} entity
*/
handleReceiver(entity) {
const undergroundComp = entity.components.UndergroundBelt;
// Try to eject items, we only check the first one cuz its sorted by remaining time
const items = undergroundComp.pendingItems;
if (items.length > 0) {
const nextItemAndDuration = undergroundComp.pendingItems[0];
const remainingTime = nextItemAndDuration[1];
const nextItem = nextItemAndDuration[0];
if (remainingTime <= 0) {
const ejectorComp = entity.components.ItemEjector;
const nextSlotIndex = ejectorComp.getFirstFreeSlot();
if (nextSlotIndex !== null) {
if (ejectorComp.tryEject(nextSlotIndex, nextItem)) {
items.shift();
}
}
}
}
}
}