1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2024-10-27 20:34:29 +00:00
tobspr_shapez.io/src/js/game/systems/belt.js

671 lines
24 KiB
JavaScript
Raw Normal View History

2020-06-26 11:57:07 +00:00
import { Math_sqrt } from "../../core/builtins";
2020-05-09 14:45:23 +00:00
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { Loader } from "../../core/loader";
2020-06-25 10:18:48 +00:00
import { createLogger } from "../../core/logging";
2020-05-09 14:45:23 +00:00
import { AtlasSprite } from "../../core/sprites";
2020-06-26 11:57:07 +00:00
import { enumDirection, enumDirectionToVector, enumInvertedDirections } from "../../core/vector";
import { BeltPath } from "../belt_path";
2020-05-09 14:45:23 +00:00
import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
2020-06-26 11:57:07 +00:00
import { fastArrayDeleteValue } from "../../core/utils";
2020-05-09 14:45:23 +00:00
2020-06-25 10:18:48 +00:00
export const BELT_ANIM_COUNT = 28;
2020-05-09 14:45:23 +00:00
const logger = createLogger("belt");
2020-05-09 14:45:23 +00:00
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"),
};
2020-06-25 10:18:48 +00:00
/**
2020-05-09 14:45:23 +00:00
* @type {Object.<enumDirection, Array<AtlasSprite>>}
*/
this.beltAnimations = {
2020-06-25 10:18:48 +00:00
[enumDirection.top]: [],
[enumDirection.left]: [],
[enumDirection.right]: [],
2020-05-09 14:45:23 +00:00
};
2020-05-10 15:00:02 +00:00
2020-06-25 10:18:48 +00:00
for (let i = 0; i < BELT_ANIM_COUNT; ++i) {
this.beltAnimations[enumDirection.top].push(
Loader.getSprite("sprites/belt/forward_" + i + ".png")
);
this.beltAnimations[enumDirection.left].push(Loader.getSprite("sprites/belt/left_" + i + ".png"));
this.beltAnimations[enumDirection.right].push(
Loader.getSprite("sprites/belt/right_" + i + ".png")
);
}
2020-05-10 15:00:02 +00:00
this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this);
this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this);
2020-06-26 11:57:07 +00:00
this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this);
this.root.signals.entityAdded.add(this.onEntityAdded, this);
this.root.signals.postLoadHook.add(this.computeBeltCache, this);
2020-06-26 11:57:07 +00:00
// /** @type {Rectangle} */
// this.areaToRecompute = null;
/** @type {Array<BeltPath>} */
this.beltPaths = [];
this.recomputePaths = true;
2020-05-10 15:00:02 +00:00
}
/**
* Updates the belt placement after an entity has been added / deleted
* @param {Entity} entity
*/
updateSurroundingBeltPlacement(entity) {
if (!this.root.gameInitialized) {
return;
}
2020-05-10 15:00:02 +00:00
const staticComp = entity.components.StaticMapEntity;
if (!staticComp) {
return;
}
2020-06-26 11:57:07 +00:00
// this.recomputePaths = true;
/*
2020-05-10 15:00:02 +00:00
const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding);
// Compute affected area
const originalRect = staticComp.getTileSpaceBounds();
const affectedArea = originalRect.expandedInAllDirections(1);
// Store if anything got changed, if so we need to queue a recompute
let anythingChanged = false;
2020-06-26 11:57:07 +00:00
anythingChanged = true; // TODO / FIXME
2020-05-10 15:00:02 +00:00
for (let x = affectedArea.x; x < affectedArea.right(); ++x) {
for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) {
if (!originalRect.containsPoint(x, y)) {
const targetEntity = this.root.map.getTileContentXY(x, y);
if (targetEntity) {
const targetBeltComp = targetEntity.components.Belt;
if (targetBeltComp) {
const targetStaticComp = targetEntity.components.StaticMapEntity;
const {
rotation,
rotationVariant,
} = metaBelt.computeOptimalDirectionAndRotationVariantAtTile(
this.root,
new Vector(x, y),
2020-05-16 21:48:56 +00:00
targetStaticComp.originalRotation,
defaultBuildingVariant
2020-05-10 15:00:02 +00:00
);
targetStaticComp.rotation = rotation;
2020-05-16 21:48:56 +00:00
metaBelt.updateVariants(targetEntity, rotationVariant, defaultBuildingVariant);
anythingChanged = true;
2020-05-10 15:00:02 +00:00
}
}
}
}
}
if (anythingChanged) {
if (this.areaToRecompute) {
this.areaToRecompute = this.areaToRecompute.getUnion(affectedArea);
} else {
this.areaToRecompute = affectedArea.clone();
}
2020-06-25 10:42:48 +00:00
if (G_IS_DEV) {
logger.log("Queuing recompute:", this.areaToRecompute);
}
}
2020-06-26 11:57:07 +00:00
// FIXME
this.areaToRecompute = new Rectangle(-1000, -1000, 2000, 2000);
*/
}
/**
* Called when an entity got destroyed
* @param {Entity} entity
*/
onEntityDestroyed(entity) {
if (!this.root.gameInitialized) {
return;
}
if (!entity.components.Belt) {
return;
}
const assignedPath = entity.components.Belt.assignedPath;
assert(assignedPath, "Entity has no belt path assigned");
// Find from and to entities
const fromEntity = this.findSupplyingEntity(entity);
const toEntity = this.findFollowUpEntity(entity);
// Check if the belt had a previous belt
if (fromEntity) {
const fromPath = fromEntity.components.Belt.assignedPath;
// Check if the entity had a followup - belt
if (toEntity) {
const toPath = toEntity.components.Belt.assignedPath;
assert(fromPath === toPath, "Invalid belt path layout (from path != to path)");
const newPath = fromPath.deleteEntityOnPathSplitIntoTwo(entity);
this.beltPaths.push(newPath);
} else {
2020-06-26 14:31:36 +00:00
fromPath.deleteEntityOnEnd(entity);
2020-06-26 11:57:07 +00:00
}
} else {
if (toEntity) {
2020-06-26 14:31:36 +00:00
// We need to remove the entity from the beginning of the other path
const toPath = toEntity.components.Belt.assignedPath;
toPath.deleteEntityOnStart(entity);
2020-06-26 11:57:07 +00:00
} else {
2020-06-26 14:31:36 +00:00
// This is a single entity path, easy to do
const path = entity.components.Belt.assignedPath;
fastArrayDeleteValue(this.beltPaths, path);
2020-06-26 11:57:07 +00:00
}
}
2020-06-26 14:31:36 +00:00
this.verifyBeltPaths();
2020-06-26 11:57:07 +00:00
}
/**
* Called when an entity got added
* @param {Entity} entity
*/
onEntityAdded(entity) {
if (!this.root.gameInitialized) {
return;
}
if (!entity.components.Belt) {
return;
}
console.log("ADD");
const fromEntity = this.findSupplyingEntity(entity);
const toEntity = this.findFollowUpEntity(entity);
console.log("From:", fromEntity, "to:", toEntity);
// Check if we can add the entity to the previous path
if (fromEntity) {
const fromPath = fromEntity.components.Belt.assignedPath;
fromPath.extendOnEnd(entity);
// Check if we now can extend the current path by the next path
if (toEntity) {
const toPath = toEntity.components.Belt.assignedPath;
2020-06-26 14:31:36 +00:00
if (fromPath === toPath) {
// This is a circular dependency -> Ignore
} else {
fromPath.extendByPath(toPath);
// Delete now obsolete path
fastArrayDeleteValue(this.beltPaths, toPath);
}
2020-06-26 11:57:07 +00:00
}
} else {
if (toEntity) {
// Prepend it to the other path
const toPath = toEntity.components.Belt.assignedPath;
toPath.extendOnBeginning(entity);
} else {
// This is an empty belt path
const path = new BeltPath(this.root, [entity]);
this.beltPaths.push(path);
}
}
2020-06-26 14:31:36 +00:00
this.verifyBeltPaths();
2020-05-09 14:45:23 +00:00
}
draw(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this));
}
2020-06-26 14:31:36 +00:00
/**
* Verifies all belt paths
*/
verifyBeltPaths() {
if (G_IS_DEV) {
for (let i = 0; i < this.beltPaths.length; ++i) {
this.beltPaths[i].debug_checkIntegrity("general-verify");
}
const belts = this.root.entityMgr.getAllWithComponent(BeltComponent);
for (let i = 0; i < belts.length; ++i) {
const path = belts[i].components.Belt.assignedPath;
if (!path) {
throw new Error("Belt has no path: " + belts[i].uid);
}
if (this.beltPaths.indexOf(path) < 0) {
throw new Error("Path of entity not contained: " + belts[i].uid);
}
}
}
}
2020-05-18 10:53:01 +00:00
/**
* Finds the follow up entity for a given belt. Used for building the dependencies
2020-05-18 10:53:01 +00:00
* @param {Entity} entity
*/
findFollowUpEntity(entity) {
const staticComp = entity.components.StaticMapEntity;
const beltComp = entity.components.Belt;
2020-05-18 10:53:01 +00:00
const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction);
const followUpVector = enumDirectionToVector[followUpDirection];
2020-05-18 10:53:01 +00:00
const followUpTile = staticComp.origin.add(followUpVector);
const followUpEntity = this.root.map.getTileContent(followUpTile);
2020-05-09 14:45:23 +00:00
2020-05-18 17:23:37 +00:00
// Check if theres a belt at the tile we point to
if (followUpEntity) {
const followUpBeltComp = followUpEntity.components.Belt;
if (followUpBeltComp) {
2020-05-18 17:23:37 +00:00
const followUpStatic = followUpEntity.components.StaticMapEntity;
const followUpAcceptor = followUpEntity.components.ItemAcceptor;
// Check if the belt accepts items from our direction
const acceptorSlots = followUpAcceptor.slots;
for (let i = 0; i < acceptorSlots.length; ++i) {
const slot = acceptorSlots[i];
for (let k = 0; k < slot.directions.length; ++k) {
const localDirection = followUpStatic.localDirectionToWorld(slot.directions[k]);
if (enumInvertedDirections[localDirection] === followUpDirection) {
return followUpEntity;
}
}
}
}
2020-05-18 10:53:01 +00:00
}
2020-05-09 14:45:23 +00:00
return null;
}
2020-06-26 11:57:07 +00:00
/**
* Finds the supplying belt for a given belt. Used for building the dependencies
* @param {Entity} entity
*/
findSupplyingEntity(entity) {
const staticComp = entity.components.StaticMapEntity;
const supplyDirection = staticComp.localDirectionToWorld(enumDirection.bottom);
const supplyVector = enumDirectionToVector[supplyDirection];
const supplyTile = staticComp.origin.add(supplyVector);
const supplyEntity = this.root.map.getTileContent(supplyTile);
// Check if theres a belt at the tile we point to
if (supplyEntity) {
const supplyBeltComp = supplyEntity.components.Belt;
if (supplyBeltComp) {
const supplyStatic = supplyEntity.components.StaticMapEntity;
const supplyEjector = supplyEntity.components.ItemEjector;
// Check if the belt accepts items from our direction
const ejectorSlots = supplyEjector.slots;
for (let i = 0; i < ejectorSlots.length; ++i) {
const slot = ejectorSlots[i];
const localDirection = supplyStatic.localDirectionToWorld(slot.direction);
if (enumInvertedDirections[localDirection] === supplyDirection) {
return supplyEntity;
}
}
}
}
return null;
}
/**
* Recomputes the belt cache
*/
computeBeltCache() {
2020-06-26 11:57:07 +00:00
this.recomputePaths = false;
/*
if (this.areaToRecompute) {
logger.log("Updating belt cache by updating area:", this.areaToRecompute);
2020-06-25 10:42:48 +00:00
if (G_IS_DEV && globalConfig.debug.renderChanges) {
this.root.hud.parts.changesDebugger.renderChange(
"belt-area",
this.areaToRecompute,
"#00fff6"
);
}
for (let x = this.areaToRecompute.x; x < this.areaToRecompute.right(); ++x) {
for (let y = this.areaToRecompute.y; y < this.areaToRecompute.bottom(); ++y) {
const tile = this.root.map.getTileContentXY(x, y);
if (tile && tile.components.Belt) {
tile.components.Belt.followUpCache = this.findFollowUpEntity(tile);
}
}
}
// Reset stale areas afterwards
this.areaToRecompute = null;
} else {
logger.log("Doing full belt recompute");
2020-06-25 10:42:48 +00:00
if (G_IS_DEV && globalConfig.debug.renderChanges) {
this.root.hud.parts.changesDebugger.renderChange(
"",
new Rectangle(-1000, -1000, 2000, 2000),
"#00fff6"
);
}
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
entity.components.Belt.followUpCache = this.findFollowUpEntity(entity);
}
2020-05-09 14:45:23 +00:00
}
2020-06-26 11:57:07 +00:00
*/
this.computeBeltPaths();
}
/**
* Computes the belt path network
*/
computeBeltPaths() {
const visitedUids = new Set();
console.log("Computing belt paths");
const debugEntity = e => e.components.StaticMapEntity.origin.toString();
// const stackToVisit = this.allEntities.slice();
const result = [];
const currentPath = null;
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
if (visitedUids.has(entity.uid)) {
continue;
}
// console.log("Starting at", debugEntity(entity));
// Mark entity as visited
visitedUids.add(entity.uid);
// Compute path, start with entity and find precedors / successors
const path = [entity];
2020-06-26 14:31:36 +00:00
let maxIter = 9999;
2020-06-26 11:57:07 +00:00
// Find precedors
let prevEntity = this.findSupplyingEntity(entity);
2020-06-26 14:31:36 +00:00
while (prevEntity && --maxIter > 0) {
2020-06-26 11:57:07 +00:00
if (visitedUids.has(prevEntity)) {
break;
}
// console.log(" -> precedor: ", debugEntity(prevEntity));
path.unshift(prevEntity);
visitedUids.add(prevEntity.uid);
prevEntity = this.findSupplyingEntity(prevEntity);
}
// Find succedors
let nextEntity = this.findFollowUpEntity(entity);
2020-06-26 14:31:36 +00:00
while (nextEntity && --maxIter > 0) {
2020-06-26 11:57:07 +00:00
if (visitedUids.has(nextEntity)) {
break;
}
// console.log(" -> succedor: ", debugEntity(nextEntity));
path.push(nextEntity);
visitedUids.add(nextEntity.uid);
nextEntity = this.findFollowUpEntity(nextEntity);
}
2020-06-26 14:31:36 +00:00
assert(maxIter !== 0, "Ran out of iterations");
2020-06-26 11:57:07 +00:00
// console.log(
// "Found path:",
// path.map(e => debugEntity(e))
// );
result.push(new BeltPath(this.root, path));
// let prevEntity = this.findSupplyingEntity(srcEntity);
}
logger.log("Found", this.beltPaths.length, "belt paths");
this.beltPaths = result;
2020-05-09 14:45:23 +00:00
}
2020-05-18 10:53:01 +00:00
update() {
2020-06-26 11:57:07 +00:00
if (this.recomputePaths) {
this.computeBeltCache();
}
2020-05-18 10:53:01 +00:00
2020-06-26 14:31:36 +00:00
this.verifyBeltPaths();
2020-06-26 11:57:07 +00:00
for (let i = 0; i < this.beltPaths.length; ++i) {
this.beltPaths[i].update();
}
2020-06-26 14:31:36 +00:00
this.verifyBeltPaths();
2020-06-26 11:57:07 +00:00
return;
/*
2020-05-30 15:50:29 +00:00
// Divide by item spacing on belts since we use throughput and not speed
let beltSpeed =
this.root.hubGoals.getBeltBaseSpeed() *
this.root.dynamicTickrate.deltaSeconds *
globalConfig.itemSpacingOnBelts;
2020-06-14 12:20:35 +00:00
2020-05-30 15:50:29 +00:00
if (G_IS_DEV && globalConfig.debug.instantBelts) {
beltSpeed *= 100;
}
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const beltComp = entity.components.Belt;
const items = beltComp.sortedItems;
if (items.length === 0) {
// Fast out for performance
continue;
}
const ejectorComp = entity.components.ItemEjector;
let maxProgress = 1;
2020-06-26 11:57:07 +00:00
// PERFORMANCE OPTIMIZATION
2020-06-14 12:20:35 +00:00
// Original:
// const isCurrentlyEjecting = ejectorComp.isAnySlotEjecting();
// Replaced (Since belts always have just one slot):
const ejectorSlot = ejectorComp.slots[0];
const isCurrentlyEjecting = ejectorSlot.item;
// When ejecting, we can not go further than the item spacing since it
// will be on the corner
2020-06-14 12:20:35 +00:00
if (isCurrentlyEjecting) {
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// Otherwise our progress depends on the follow up
if (beltComp.followUpCache) {
const spacingOnBelt = beltComp.followUpCache.components.Belt.getDistanceToFirstItemCenter();
2020-06-14 12:20:35 +00:00
maxProgress = Math.min(2, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt);
// Useful check, but hurts performance
2020-06-25 10:53:59 +00:00
// assert(maxProgress >= 0.0, "max progress < 0 (I) (" + maxProgress + ")");
}
}
let speedMultiplier = 1;
if (beltComp.direction !== enumDirection.top) {
2020-06-14 12:20:35 +00:00
// Curved belts are shorter, thus being quicker (Looks weird otherwise)
speedMultiplier = SQRT_2;
}
2020-06-25 10:42:48 +00:00
// How much offset we add when transferring to a new belt
// This substracts one tick because the belt will be updated directly
// afterwards anyways
const takeoverOffset = 1.0 + beltSpeed * speedMultiplier;
// Not really nice. haven't found the reason for this yet.
if (items.length > 2 / globalConfig.itemSpacingOnBelts) {
beltComp.sortedItems = [];
}
for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) {
const progressAndItem = items[itemIndex];
2020-06-14 12:20:35 +00:00
progressAndItem[0] = Math.min(maxProgress, progressAndItem[0] + speedMultiplier * beltSpeed);
2020-06-25 10:53:59 +00:00
assert(progressAndItem[0] >= 0, "Bad progress: " + progressAndItem[0]);
if (progressAndItem[0] >= 1.0) {
if (beltComp.followUpCache) {
const followUpBelt = beltComp.followUpCache.components.Belt;
2020-05-20 13:51:06 +00:00
if (followUpBelt.canAcceptItem()) {
2020-06-25 10:53:59 +00:00
followUpBelt.takeItem(
progressAndItem[1],
Math_max(0, progressAndItem[0] - takeoverOffset)
);
items.splice(itemIndex, 1);
} else {
// Well, we couldn't really take it to a follow up belt, keep it at
// max progress
progressAndItem[0] = 1.0;
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
}
} else {
// Try to give this item to a new belt
2020-06-14 12:20:35 +00:00
2020-06-26 11:57:07 +00:00
// PERFORMANCE OPTIMIZATION
2020-06-25 10:18:48 +00:00
2020-06-14 12:20:35 +00:00
// Original:
// const freeSlot = ejectorComp.getFirstFreeSlot();
2020-06-25 10:18:48 +00:00
2020-06-14 12:20:35 +00:00
// Replaced
if (ejectorSlot.item) {
// So, we don't have a free slot - damned!
progressAndItem[0] = 1.0;
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// We got a free slot, remove this item and keep it on the ejector slot
2020-06-14 12:20:35 +00:00
if (!ejectorComp.tryEject(0, progressAndItem[1])) {
assert(false, "Ejection failed");
}
items.splice(itemIndex, 1);
2020-05-18 17:23:37 +00:00
// NOTICE: Do not override max progress here at all, this leads to issues
}
}
} else {
// We just moved this item forward, so determine the maximum progress of other items
2020-06-14 12:20:35 +00:00
maxProgress = Math.max(0, progressAndItem[0] - globalConfig.itemSpacingOnBelts);
}
}
2020-05-18 10:53:01 +00:00
}
2020-06-26 11:57:07 +00:00
*/
2020-05-18 10:53:01 +00:00
}
2020-05-09 14:45:23 +00:00
/**
*
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
if (parameters.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
return;
}
const speedMultiplier = this.root.hubGoals.getBeltBaseSpeed();
2020-05-18 09:47:17 +00:00
// SYNC with systems/item_acceptor.js:drawEntityUnderlays!
2020-05-09 14:45:23 +00:00
// 126 / 42 is the exact animation speed of the png animation
const animationIndex = Math.floor(
2020-05-18 09:47:17 +00:00
((this.root.time.now() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) *
globalConfig.itemSpacingOnBelts
2020-05-09 14:45:23 +00:00
);
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) {
2020-06-26 11:57:07 +00:00
/*
2020-05-09 14:45:23 +00:00
const beltComp = entity.components.Belt;
const staticComp = entity.components.StaticMapEntity;
const items = beltComp.sortedItems;
if (items.length === 0) {
// Fast out for performance
return;
}
2020-05-18 15:40:20 +00:00
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
2020-05-09 14:45:23 +00:00
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
);
}
2020-06-26 11:57:07 +00:00
*/
}
/**
* Draws the belt parameters
* @param {DrawParameters} parameters
*/
drawBeltPathDebug(parameters) {
for (let i = 0; i < this.beltPaths.length; ++i) {
this.beltPaths[i].drawDebug(parameters);
}
2020-05-09 14:45:23 +00:00
}
}