2020-06-26 14:31:36 +00:00
|
|
|
import { Math_min } from "../core/builtins";
|
2020-06-26 11:57:07 +00:00
|
|
|
import { globalConfig } from "../core/config";
|
|
|
|
import { DrawParameters } from "../core/draw_parameters";
|
2020-06-26 14:31:36 +00:00
|
|
|
import { createLogger } from "../core/logging";
|
|
|
|
import { epsilonCompare, round4Digits } from "../core/utils";
|
2020-06-26 11:57:07 +00:00
|
|
|
import { Vector } from "../core/vector";
|
|
|
|
import { BaseItem } from "./base_item";
|
|
|
|
import { Entity } from "./entity";
|
|
|
|
import { GameRoot } from "./root";
|
|
|
|
|
|
|
|
const logger = createLogger("belt_path");
|
|
|
|
|
|
|
|
// Helpers for more semantic access into interleaved arrays
|
2020-06-26 14:31:36 +00:00
|
|
|
const _nextDistance = 0;
|
|
|
|
const _item = 1;
|
2020-06-26 11:57:07 +00:00
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
const DEBUG = G_IS_DEV && false;
|
|
|
|
|
2020-06-26 11:57:07 +00:00
|
|
|
/**
|
|
|
|
* Stores a path of belts, used for optimizing performance
|
|
|
|
*/
|
|
|
|
export class BeltPath {
|
|
|
|
/**
|
|
|
|
* @param {GameRoot} root
|
|
|
|
* @param {Array<Entity>} entityPath
|
|
|
|
*/
|
|
|
|
constructor(root, entityPath) {
|
|
|
|
this.root = root;
|
|
|
|
|
|
|
|
assert(entityPath.length > 0, "invalid entity path");
|
|
|
|
this.entityPath = entityPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stores the items sorted, and their distance to the previous item (or start)
|
|
|
|
* Layout: [distanceToNext, item]
|
|
|
|
* @type {Array<[number, BaseItem]>}
|
|
|
|
*/
|
|
|
|
this.items = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stores the spacing to the first item
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Find acceptor and ejector
|
|
|
|
|
|
|
|
this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector;
|
|
|
|
this.ejectorSlot = this.ejectorComp.slots[0];
|
|
|
|
this.initialBeltComponent = this.entityPath[0].components.Belt;
|
|
|
|
|
|
|
|
this.totalLength = this.computeTotalLength();
|
|
|
|
this.spacingToFirstItem = this.totalLength;
|
|
|
|
|
|
|
|
// Connect the belts
|
|
|
|
for (let i = 0; i < this.entityPath.length; ++i) {
|
|
|
|
this.entityPath[i].components.Belt.assignedPath = this;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.debug_checkIntegrity("constructor");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper to throw an error on mismatch
|
|
|
|
* @param {string} change
|
|
|
|
* @param {Array<any>} reason
|
|
|
|
*/
|
|
|
|
debug_failIntegrity(change, ...reason) {
|
|
|
|
throw new Error("belt path invalid (" + change + "): " + reason.map(i => "" + i).join(" "));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if this path is valid
|
|
|
|
*/
|
|
|
|
debug_checkIntegrity(currentChange = "change") {
|
|
|
|
if (!G_IS_DEV) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const fail = (...args) => this.debug_failIntegrity(currentChange, ...args);
|
|
|
|
|
|
|
|
// Check for empty path
|
|
|
|
if (this.entityPath.length === 0) {
|
|
|
|
return fail("Belt path is empty");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for mismatching length
|
|
|
|
const totalLength = this.computeTotalLength();
|
2020-06-26 14:31:36 +00:00
|
|
|
if (!epsilonCompare(this.totalLength, totalLength)) {
|
2020-06-26 11:57:07 +00:00
|
|
|
return this.debug_failIntegrity(
|
|
|
|
currentChange,
|
|
|
|
"Total length mismatch, stored =",
|
|
|
|
this.totalLength,
|
|
|
|
"but correct is",
|
|
|
|
totalLength
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for misconnected entities
|
|
|
|
for (let i = 0; i < this.entityPath.length - 1; ++i) {
|
|
|
|
const entity = this.entityPath[i];
|
2020-06-26 14:31:36 +00:00
|
|
|
if (entity.destroyed) {
|
|
|
|
return fail("Reference to destroyed entity " + entity.uid);
|
|
|
|
}
|
|
|
|
|
2020-06-26 11:57:07 +00:00
|
|
|
const followUp = this.root.systemMgr.systems.belt.findFollowUpEntity(entity);
|
|
|
|
if (!followUp) {
|
|
|
|
return fail(
|
|
|
|
"Follow up entity for the",
|
|
|
|
i,
|
|
|
|
"-th entity (total length",
|
|
|
|
this.entityPath.length,
|
|
|
|
") was null!"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (followUp !== this.entityPath[i + 1]) {
|
|
|
|
return fail(
|
|
|
|
"Follow up entity mismatch, stored is",
|
|
|
|
this.entityPath[i + 1].uid,
|
|
|
|
"but real one is",
|
|
|
|
followUp.uid
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (entity.components.Belt.assignedPath !== this) {
|
|
|
|
return fail(
|
|
|
|
"Entity with uid",
|
|
|
|
entity.uid,
|
|
|
|
"doesn't have this path assigned, but this path contains the entity."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for right ejector component and slot
|
|
|
|
if (this.ejectorComp !== this.entityPath[this.entityPath.length - 1].components.ItemEjector) {
|
|
|
|
return fail("Stale ejectorComp handle");
|
|
|
|
}
|
|
|
|
if (this.ejectorSlot !== this.ejectorComp.slots[0]) {
|
|
|
|
return fail("Stale ejector slot handle");
|
|
|
|
}
|
|
|
|
if (!this.ejectorComp) {
|
|
|
|
return fail("Ejector comp not set");
|
|
|
|
}
|
|
|
|
if (!this.ejectorSlot) {
|
|
|
|
return fail("Ejector slot not set");
|
|
|
|
}
|
|
|
|
if (this.initialBeltComponent !== this.entityPath[0].components.Belt) {
|
|
|
|
return fail("Stale initial belt component handle");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check spacing
|
|
|
|
if (this.spacingToFirstItem > this.totalLength + 0.005) {
|
|
|
|
return fail(
|
|
|
|
currentChange,
|
|
|
|
"spacing to first item (",
|
|
|
|
this.spacingToFirstItem,
|
|
|
|
") is greater than total length (",
|
|
|
|
this.totalLength,
|
|
|
|
")"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check distance if empty
|
|
|
|
if (this.items.length === 0 && !epsilonCompare(this.spacingToFirstItem, this.totalLength)) {
|
|
|
|
return fail(
|
|
|
|
currentChange,
|
|
|
|
"Path is empty but spacing to first item (",
|
|
|
|
this.spacingToFirstItem,
|
|
|
|
") does not equal total length (",
|
|
|
|
this.totalLength,
|
|
|
|
")"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check items etc
|
|
|
|
let currentPos = this.spacingToFirstItem;
|
|
|
|
for (let i = 0; i < this.items.length; ++i) {
|
|
|
|
const item = this.items[i];
|
|
|
|
|
2020-06-26 14:31:36 +00:00
|
|
|
if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength) {
|
2020-06-26 11:57:07 +00:00
|
|
|
return fail(
|
|
|
|
"Item has invalid offset to next item: ",
|
2020-06-26 14:31:36 +00:00
|
|
|
item[_nextDistance],
|
2020-06-26 11:57:07 +00:00
|
|
|
"(total length:",
|
|
|
|
this.totalLength,
|
|
|
|
")"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-26 14:31:36 +00:00
|
|
|
currentPos += item[_nextDistance];
|
2020-06-26 11:57:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check the total sum matches
|
|
|
|
if (!epsilonCompare(currentPos, this.totalLength)) {
|
|
|
|
return fail(
|
|
|
|
"total sum (",
|
|
|
|
currentPos,
|
|
|
|
") of first item spacing (",
|
|
|
|
this.spacingToFirstItem,
|
|
|
|
") and items does not match total length (",
|
|
|
|
this.totalLength,
|
2020-06-26 14:31:36 +00:00
|
|
|
") -> items: " + this.items.map(i => i[_nextDistance]).join("|")
|
2020-06-26 11:57:07 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extends the belt path by the given belt
|
|
|
|
* @param {Entity} entity
|
|
|
|
*/
|
|
|
|
extendOnEnd(entity) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Extending belt path by entity at", entity.components.StaticMapEntity.origin);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
const beltComp = entity.components.Belt;
|
|
|
|
|
|
|
|
// If the last belt has something on its ejector, put that into the path first
|
|
|
|
const pendingItem = this.ejectorComp.takeSlotItem(0);
|
|
|
|
if (pendingItem) {
|
|
|
|
// Ok, so we have a pending item
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Taking pending item and putting it back on the path");
|
2020-06-26 11:57:07 +00:00
|
|
|
this.items.push([0, pendingItem]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append the entity
|
|
|
|
this.entityPath.push(entity);
|
|
|
|
|
|
|
|
// Extend the path length
|
|
|
|
const additionalLength = beltComp.getEffectiveLengthTiles();
|
|
|
|
this.totalLength += additionalLength;
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Extended total length by", additionalLength, "to", this.totalLength);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// If we have no item, just update the distance to the first item
|
|
|
|
if (this.items.length === 0) {
|
|
|
|
this.spacingToFirstItem = this.totalLength;
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Extended spacing to first to", this.totalLength, "(= total length)");
|
2020-06-26 11:57:07 +00:00
|
|
|
} else {
|
|
|
|
// Otherwise, update the next-distance of the last item
|
|
|
|
const lastItem = this.items[this.items.length - 1];
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" Extended spacing of last item from",
|
|
|
|
lastItem[_nextDistance],
|
|
|
|
"to",
|
|
|
|
lastItem[_nextDistance] + additionalLength
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
lastItem[_nextDistance] += additionalLength;
|
2020-06-26 11:57:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update handles
|
|
|
|
this.ejectorComp = entity.components.ItemEjector;
|
|
|
|
this.ejectorSlot = this.ejectorComp.slots[0];
|
|
|
|
|
|
|
|
// Assign reference
|
|
|
|
beltComp.assignedPath = this;
|
|
|
|
|
|
|
|
this.debug_checkIntegrity("extend-on-end");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extends the path with the given entity on the beginning
|
|
|
|
* @param {Entity} entity
|
|
|
|
*/
|
|
|
|
extendOnBeginning(entity) {
|
|
|
|
const beltComp = entity.components.Belt;
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Extending the path on the beginning");
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// All items on that belt are simply lost (for now)
|
|
|
|
|
|
|
|
const length = beltComp.getEffectiveLengthTiles();
|
|
|
|
|
|
|
|
// Extend the length of this path
|
|
|
|
this.totalLength += length;
|
|
|
|
|
|
|
|
// Simply adjust the first item spacing cuz we have no items contained
|
|
|
|
this.spacingToFirstItem += length;
|
|
|
|
|
|
|
|
// Set handles and append entity
|
|
|
|
beltComp.assignedPath = this;
|
|
|
|
this.entityPath.unshift(entity);
|
2020-06-26 14:31:36 +00:00
|
|
|
this.initialBeltComponent = this.entityPath[0].components.Belt;
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
this.debug_checkIntegrity("extend-on-begin");
|
|
|
|
}
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
/**
|
|
|
|
* Returns if the given entity is the end entity of the path
|
|
|
|
* @param {Entity} entity
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
isEndEntity(entity) {
|
|
|
|
return this.entityPath[this.entityPath.length - 1] === entity;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns if the given entity is the start entity of the path
|
|
|
|
* @param {Entity} entity
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
isStartEntity(entity) {
|
|
|
|
return this.entityPath[0] === entity;
|
|
|
|
}
|
|
|
|
|
2020-06-26 11:57:07 +00:00
|
|
|
/**
|
|
|
|
* Splits this path at the given entity by removing it, and
|
|
|
|
* returning the new secondary paht
|
|
|
|
* @param {Entity} entity
|
|
|
|
* @returns {BeltPath}
|
|
|
|
*/
|
|
|
|
deleteEntityOnPathSplitIntoTwo(entity) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Splitting path at entity", entity.components.StaticMapEntity.origin);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// First, find where the current path ends
|
|
|
|
const beltComp = entity.components.Belt;
|
|
|
|
beltComp.assignedPath = null;
|
|
|
|
|
|
|
|
const entityLength = beltComp.getEffectiveLengthTiles();
|
|
|
|
assert(this.entityPath.indexOf(entity) >= 0, "Entity not contained for split");
|
|
|
|
assert(this.entityPath.indexOf(entity) !== 0, "Entity is first");
|
|
|
|
assert(this.entityPath.indexOf(entity) !== this.entityPath.length - 1, "Entity is last");
|
|
|
|
|
|
|
|
let firstPathEntityCount = 0;
|
|
|
|
let firstPathLength = 0;
|
|
|
|
let firstPathEndEntity = null;
|
|
|
|
|
|
|
|
for (let i = 0; i < this.entityPath.length; ++i) {
|
|
|
|
const otherEntity = this.entityPath[i];
|
|
|
|
if (otherEntity === entity) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Found entity at", i, "of length", firstPathLength);
|
2020-06-26 11:57:07 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
++firstPathEntityCount;
|
|
|
|
firstPathEndEntity = otherEntity;
|
|
|
|
firstPathLength += otherEntity.components.Belt.getEffectiveLengthTiles();
|
|
|
|
}
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"First path ends at",
|
|
|
|
firstPathLength,
|
|
|
|
"and entity",
|
|
|
|
firstPathEndEntity.components.StaticMapEntity.origin,
|
|
|
|
"and has",
|
|
|
|
firstPathEntityCount,
|
|
|
|
"entities"
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// Compute length of second path
|
|
|
|
const secondPathLength = this.totalLength - firstPathLength - entityLength;
|
|
|
|
const secondPathStart = firstPathLength + entityLength;
|
|
|
|
const secondEntities = this.entityPath.splice(firstPathEntityCount + 1);
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"Second path starts at",
|
|
|
|
secondPathStart,
|
|
|
|
"and has a length of ",
|
|
|
|
secondPathLength,
|
|
|
|
"with",
|
|
|
|
secondEntities.length,
|
|
|
|
"entities"
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// Remove the last item
|
|
|
|
this.entityPath.pop();
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Splitting", this.items.length, "items");
|
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"Old items are",
|
|
|
|
this.items.map(i => i[_nextDistance])
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// Create second path
|
|
|
|
const secondPath = new BeltPath(this.root, secondEntities);
|
|
|
|
|
|
|
|
// Remove all items which are no longer relevant and transfer them to the second path
|
|
|
|
let itemPos = this.spacingToFirstItem;
|
|
|
|
for (let i = 0; i < this.items.length; ++i) {
|
|
|
|
const item = this.items[i];
|
2020-06-26 14:31:36 +00:00
|
|
|
const distanceToNext = item[_nextDistance];
|
2020-06-26 11:57:07 +00:00
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next");
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// Check if this item is past the first path
|
|
|
|
if (itemPos >= firstPathLength) {
|
|
|
|
// Remove it from the first path
|
|
|
|
this.items.splice(i, 1);
|
|
|
|
i -= 1;
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(" Removed item from first path since its no longer contained @", itemPos);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// Check if its on the second path (otherwise its on the removed belt and simply lost)
|
|
|
|
if (itemPos >= secondPathStart) {
|
|
|
|
// Put item on second path
|
2020-06-26 14:31:36 +00:00
|
|
|
secondPath.items.push([distanceToNext, item[_item]]);
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" Put item to second path @",
|
|
|
|
itemPos,
|
|
|
|
"with distance to next =",
|
|
|
|
distanceToNext
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// If it was the first item, adjust the distance to the first item
|
|
|
|
if (secondPath.items.length === 1) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Sinc it was the first, set sapcing of first to", itemPos);
|
2020-06-26 11:57:07 +00:00
|
|
|
secondPath.spacingToFirstItem = itemPos - secondPathStart;
|
|
|
|
}
|
|
|
|
} else {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Item was on the removed belt, so its gone - forever!");
|
2020-06-26 11:57:07 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Seems this item is on the first path (so all good), so just make sure it doesn't
|
|
|
|
// have a nextDistance which is bigger than the total path length
|
|
|
|
const clampedDistanceToNext = Math_min(itemPos + distanceToNext, firstPathLength) - itemPos;
|
|
|
|
if (clampedDistanceToNext < distanceToNext) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"Correcting next distance (first path) from",
|
|
|
|
distanceToNext,
|
|
|
|
"to",
|
|
|
|
clampedDistanceToNext
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
item[_nextDistance] = clampedDistanceToNext;
|
2020-06-26 11:57:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Advance items
|
|
|
|
itemPos += distanceToNext;
|
|
|
|
}
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"New items are",
|
|
|
|
this.items.map(i => i[_nextDistance])
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"And second path items are",
|
|
|
|
secondPath.items.map(i => i[_nextDistance])
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
// Adjust our total length
|
|
|
|
this.totalLength = firstPathLength;
|
|
|
|
|
|
|
|
// Make sure that if we are empty, we set our first distance properly
|
|
|
|
if (this.items.length === 0) {
|
|
|
|
this.spacingToFirstItem = this.totalLength;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set new ejector and acceptor handles
|
|
|
|
this.ejectorComp = firstPathEndEntity.components.ItemEjector;
|
|
|
|
this.ejectorSlot = this.ejectorComp.slots[0];
|
|
|
|
|
|
|
|
this.debug_checkIntegrity("split-two-first");
|
|
|
|
secondPath.debug_checkIntegrity("split-two-second");
|
|
|
|
|
|
|
|
return secondPath;
|
|
|
|
}
|
|
|
|
|
2020-06-26 14:31:36 +00:00
|
|
|
/**
|
|
|
|
* Deletes the last entity
|
|
|
|
* @param {Entity} entity
|
|
|
|
*/
|
|
|
|
deleteEntityOnEnd(entity) {
|
2020-06-26 15:02:52 +00:00
|
|
|
assert(
|
|
|
|
this.entityPath[this.entityPath.length - 1] === entity,
|
|
|
|
"Not actually the last entity (instead " + this.entityPath.indexOf(entity) + ")"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// Ok, first remove the entity
|
|
|
|
const beltComp = entity.components.Belt;
|
|
|
|
const beltLength = beltComp.getEffectiveLengthTiles();
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"Deleting last entity on path with length",
|
|
|
|
this.entityPath.length,
|
|
|
|
"(reducing",
|
|
|
|
this.totalLength,
|
|
|
|
" by",
|
|
|
|
beltLength,
|
|
|
|
")"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
this.totalLength -= beltLength;
|
|
|
|
this.entityPath.pop();
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" New path has length of",
|
|
|
|
this.totalLength,
|
|
|
|
"with",
|
|
|
|
this.entityPath.length,
|
|
|
|
"entities"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// This is just for sanity
|
|
|
|
beltComp.assignedPath = null;
|
|
|
|
|
|
|
|
// Clean up items
|
|
|
|
if (this.items.length === 0) {
|
|
|
|
// Simple case with no items, just update the first item spacing
|
|
|
|
this.spacingToFirstItem = this.totalLength;
|
|
|
|
} else {
|
|
|
|
// Ok, make sure we simply drop all items which are no longer contained
|
|
|
|
let itemOffset = this.spacingToFirstItem;
|
|
|
|
let lastItemOffset = itemOffset;
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Adjusting", this.items.length, "items");
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
for (let i = 0; i < this.items.length; ++i) {
|
|
|
|
const item = this.items[i];
|
|
|
|
|
|
|
|
// Get rid of items past this path
|
|
|
|
if (itemOffset >= this.totalLength) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Dropping item (current index=", i, ")");
|
2020-06-26 14:31:36 +00:00
|
|
|
this.items.splice(i, 1);
|
|
|
|
i -= 1;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]);
|
2020-06-26 14:31:36 +00:00
|
|
|
lastItemOffset = itemOffset;
|
|
|
|
itemOffset += item[_nextDistance];
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we still have an item, make sure the last item matches
|
|
|
|
if (this.items.length > 0) {
|
|
|
|
// We can easily compute the next distance since we know where the last item is now
|
|
|
|
const lastDistance = this.totalLength - lastItemOffset;
|
|
|
|
assert(
|
|
|
|
lastDistance >= 0.0,
|
|
|
|
"Last item distance mismatch: " +
|
|
|
|
lastDistance +
|
|
|
|
" -> Total length was " +
|
|
|
|
this.totalLength +
|
|
|
|
" and lastItemOffset was " +
|
|
|
|
lastItemOffset
|
|
|
|
);
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"Adjusted distance of last item: it is at",
|
|
|
|
lastItemOffset,
|
|
|
|
"so it has a distance of",
|
|
|
|
lastDistance,
|
|
|
|
"to the end (",
|
|
|
|
this.totalLength,
|
|
|
|
")"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
this.items[this.items.length - 1][_nextDistance] = lastDistance;
|
|
|
|
} else {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Removed all items so we'll update spacing to total length");
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// We removed all items so update our spacing
|
|
|
|
this.spacingToFirstItem = this.totalLength;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update handles
|
|
|
|
this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector;
|
|
|
|
this.ejectorSlot = this.ejectorComp.slots[0];
|
|
|
|
|
|
|
|
this.debug_checkIntegrity("delete-on-end");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes the entity of the start of the path
|
|
|
|
* @see deleteEntityOnEnd
|
|
|
|
* @param {Entity} entity
|
|
|
|
*/
|
|
|
|
deleteEntityOnStart(entity) {
|
2020-06-26 15:02:52 +00:00
|
|
|
assert(
|
|
|
|
entity === this.entityPath[0],
|
|
|
|
"Not actually the start entity (instead " + this.entityPath.indexOf(entity) + ")"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// Ok, first remove the entity
|
|
|
|
const beltComp = entity.components.Belt;
|
|
|
|
const beltLength = beltComp.getEffectiveLengthTiles();
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
"Deleting first entity on path with length",
|
|
|
|
this.entityPath.length,
|
|
|
|
"(reducing",
|
|
|
|
this.totalLength,
|
|
|
|
" by",
|
|
|
|
beltLength,
|
|
|
|
")"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
this.totalLength -= beltLength;
|
|
|
|
this.entityPath.shift();
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" New path has length of",
|
|
|
|
this.totalLength,
|
|
|
|
"with",
|
|
|
|
this.entityPath.length,
|
|
|
|
"entities"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// This is just for sanity
|
|
|
|
beltComp.assignedPath = null;
|
|
|
|
|
|
|
|
// Clean up items
|
|
|
|
if (this.items.length === 0) {
|
|
|
|
// Simple case with no items, just update the first item spacing
|
|
|
|
this.spacingToFirstItem = this.totalLength;
|
|
|
|
} else {
|
|
|
|
// Simple case, we had no item on the beginning -> all good
|
|
|
|
if (this.spacingToFirstItem >= beltLength) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" No item on the first place, so we can just adjust the spacing (spacing=",
|
|
|
|
this.spacingToFirstItem,
|
|
|
|
") removed =",
|
|
|
|
beltLength
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
this.spacingToFirstItem -= beltLength;
|
|
|
|
} else {
|
|
|
|
// Welp, okay we need to drop all items which are < beltLength and adjust
|
|
|
|
// the other item offsets as well
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" We have at least one item in the beginning, drop those and adjust spacing (first item @",
|
|
|
|
this.spacingToFirstItem,
|
|
|
|
") since we removed",
|
|
|
|
beltLength,
|
|
|
|
"length from path"
|
|
|
|
);
|
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" Items:",
|
|
|
|
this.items.map(i => i[_nextDistance])
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// Find offset to first item
|
|
|
|
let itemOffset = this.spacingToFirstItem;
|
|
|
|
for (let i = 0; i < this.items.length; ++i) {
|
|
|
|
const item = this.items[i];
|
|
|
|
if (itemOffset <= beltLength) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" -> Dropping item with index",
|
|
|
|
i,
|
|
|
|
"at",
|
|
|
|
itemOffset,
|
|
|
|
"since it was on the removed belt"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
// This item must be dropped
|
|
|
|
this.items.splice(i, 1);
|
|
|
|
i -= 1;
|
|
|
|
itemOffset += item[_nextDistance];
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
// This item can be kept, thus its the first we know
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.items.length > 0) {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" Offset of first non-dropped item was at:",
|
|
|
|
itemOffset,
|
|
|
|
"-> setting spacing to it (total length=",
|
|
|
|
this.totalLength,
|
|
|
|
")"
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
this.spacingToFirstItem = itemOffset - beltLength;
|
|
|
|
assert(
|
|
|
|
this.spacingToFirstItem >= 0.0,
|
|
|
|
"Invalid spacing after delete on start: " + this.spacingToFirstItem
|
|
|
|
);
|
|
|
|
} else {
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" We dropped all items, simply set spacing to total length");
|
2020-06-26 14:31:36 +00:00
|
|
|
// We dropped all items, simple one
|
|
|
|
this.spacingToFirstItem = this.totalLength;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update handles
|
|
|
|
this.initialBeltComponent = this.entityPath[0].components.Belt;
|
|
|
|
|
|
|
|
this.debug_checkIntegrity("delete-on-start");
|
|
|
|
}
|
|
|
|
|
2020-06-26 11:57:07 +00:00
|
|
|
/**
|
|
|
|
* Extends the path by the given other path
|
|
|
|
* @param {BeltPath} otherPath
|
|
|
|
*/
|
|
|
|
extendByPath(otherPath) {
|
2020-06-26 14:31:36 +00:00
|
|
|
assert(otherPath !== this, "Circular path dependency");
|
|
|
|
|
2020-06-26 11:57:07 +00:00
|
|
|
const entities = otherPath.entityPath;
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log("Extending path by other path, starting to add entities");
|
2020-06-26 14:31:36 +00:00
|
|
|
|
2020-06-26 11:57:07 +00:00
|
|
|
const oldLength = this.totalLength;
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Adding", entities.length, "new entities, current length =", this.totalLength);
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// First, append entities
|
2020-06-26 11:57:07 +00:00
|
|
|
for (let i = 0; i < entities.length; ++i) {
|
2020-06-26 14:31:36 +00:00
|
|
|
const entity = entities[i];
|
|
|
|
const beltComp = entity.components.Belt;
|
|
|
|
|
|
|
|
// Add to path and update references
|
|
|
|
this.entityPath.push(entity);
|
|
|
|
beltComp.assignedPath = this;
|
|
|
|
|
|
|
|
// Update our length
|
|
|
|
const additionalLength = beltComp.getEffectiveLengthTiles();
|
|
|
|
this.totalLength += additionalLength;
|
2020-06-26 11:57:07 +00:00
|
|
|
}
|
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" Path is now",
|
|
|
|
this.entityPath.length,
|
|
|
|
"entities and has a length of",
|
|
|
|
this.totalLength
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
2020-06-26 14:31:36 +00:00
|
|
|
// Update handles
|
|
|
|
this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector;
|
|
|
|
this.ejectorSlot = this.ejectorComp.slots[0];
|
2020-06-26 11:57:07 +00:00
|
|
|
|
2020-06-26 14:31:36 +00:00
|
|
|
// Now, update the distance of our last item
|
|
|
|
if (this.items.length !== 0) {
|
|
|
|
const lastItem = this.items[this.items.length - 1];
|
|
|
|
lastItem[_nextDistance] += otherPath.spacingToFirstItem;
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now");
|
2020-06-26 11:57:07 +00:00
|
|
|
} else {
|
2020-06-26 14:31:36 +00:00
|
|
|
// Seems we have no items, update our first item distance
|
|
|
|
this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem;
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG &&
|
|
|
|
logger.log(
|
|
|
|
" We had no items, so our new spacing to first is old length (",
|
|
|
|
oldLength,
|
|
|
|
") plus others spacing to first (",
|
|
|
|
otherPath.spacingToFirstItem,
|
|
|
|
") =",
|
|
|
|
this.spacingToFirstItem
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
}
|
2020-06-26 11:57:07 +00:00
|
|
|
|
2020-06-26 15:02:52 +00:00
|
|
|
DEBUG && logger.log(" Pushing", otherPath.items.length, "items from other path");
|
2020-06-26 14:31:36 +00:00
|
|
|
|
|
|
|
// Aaand push the other paths items
|
|
|
|
for (let i = 0; i < otherPath.items.length; ++i) {
|
|
|
|
const item = otherPath.items[i];
|
|
|
|
this.items.push([item[_nextDistance], item[_item]]);
|
2020-06-26 11:57:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.debug_checkIntegrity("extend-by-path");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Computes the total length of the path
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
computeTotalLength() {
|
|
|
|
let length = 0;
|
|
|
|
for (let i = 0; i < this.entityPath.length; ++i) {
|
|
|
|
length += this.entityPath[i].components.Belt.getEffectiveLengthTiles();
|
|
|
|
}
|
|
|
|
return length;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs one tick
|
|
|
|
*/
|
|
|
|
update() {
|
|
|
|
this.debug_checkIntegrity("pre-update");
|
|
|
|
const firstBeltItems = this.initialBeltComponent.sortedItems;
|
|
|
|
const transferItemAndProgress = firstBeltItems[0];
|
|
|
|
|
|
|
|
// Check if the first belt took a new item
|
|
|
|
if (transferItemAndProgress) {
|
2020-06-26 14:31:36 +00:00
|
|
|
const transferItem = transferItemAndProgress[_item];
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) {
|
|
|
|
// Can take new item
|
|
|
|
firstBeltItems.splice(0, 1);
|
|
|
|
|
|
|
|
this.items.unshift([this.spacingToFirstItem, transferItem]);
|
|
|
|
this.spacingToFirstItem = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
if (G_IS_DEV && globalConfig.debug.instantBelts) {
|
|
|
|
beltSpeed *= 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
let minimumDistance = this.ejectorSlot.item ? globalConfig.itemSpacingOnBelts : 0;
|
|
|
|
|
|
|
|
// Try to reduce spacing
|
|
|
|
let remainingAmount = beltSpeed;
|
|
|
|
for (let i = this.items.length - 1; i >= 0; --i) {
|
|
|
|
const nextDistanceAndItem = this.items[i];
|
|
|
|
const minimumSpacing = minimumDistance;
|
|
|
|
|
2020-06-26 14:31:36 +00:00
|
|
|
const takeAway = Math.max(
|
|
|
|
0,
|
|
|
|
Math.min(remainingAmount, nextDistanceAndItem[_nextDistance] - minimumSpacing)
|
|
|
|
);
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
remainingAmount -= takeAway;
|
2020-06-26 14:31:36 +00:00
|
|
|
nextDistanceAndItem[_nextDistance] -= takeAway;
|
2020-06-26 11:57:07 +00:00
|
|
|
|
|
|
|
this.spacingToFirstItem += takeAway;
|
|
|
|
if (remainingAmount === 0.0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
minimumDistance = globalConfig.itemSpacingOnBelts;
|
|
|
|
}
|
|
|
|
|
|
|
|
const lastItem = this.items[this.items.length - 1];
|
2020-06-26 14:31:36 +00:00
|
|
|
if (lastItem && lastItem[_nextDistance] === 0) {
|
2020-06-26 11:57:07 +00:00
|
|
|
// Take over
|
2020-06-26 14:31:36 +00:00
|
|
|
if (this.ejectorComp.tryEject(0, lastItem[_item])) {
|
2020-06-26 11:57:07 +00:00
|
|
|
this.items.pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.debug_checkIntegrity("post-update");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Computes a world space position from the given progress
|
|
|
|
* @param {number} progress
|
|
|
|
* @returns {Vector}
|
|
|
|
*/
|
|
|
|
computePositionFromProgress(progress) {
|
|
|
|
let currentLength = 0;
|
|
|
|
|
|
|
|
// floating point issuses ..
|
|
|
|
assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress);
|
|
|
|
|
|
|
|
for (let i = 0; i < this.entityPath.length; ++i) {
|
|
|
|
const beltComp = this.entityPath[i].components.Belt;
|
|
|
|
const localLength = beltComp.getEffectiveLengthTiles();
|
|
|
|
|
|
|
|
if (currentLength + localLength >= progress || i === this.entityPath.length - 1) {
|
|
|
|
// Min required here due to floating point issues
|
|
|
|
const localProgress = Math_min(1.0, progress - currentLength);
|
|
|
|
|
|
|
|
assert(localProgress >= 0.0, "Invalid local progress: " + localProgress);
|
|
|
|
const localSpace = beltComp.transformBeltToLocalSpace(localProgress);
|
|
|
|
return this.entityPath[i].components.StaticMapEntity.localTileToWorld(localSpace);
|
|
|
|
}
|
|
|
|
currentLength += localLength;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(false, "invalid progress: " + progress + " (max: " + this.totalLength + ")");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {DrawParameters} parameters
|
|
|
|
*/
|
|
|
|
drawDebug(parameters) {
|
|
|
|
parameters.context.fillStyle = "#d79a25";
|
|
|
|
parameters.context.strokeStyle = "#d79a25";
|
|
|
|
parameters.context.beginPath();
|
|
|
|
|
|
|
|
for (let i = 0; i < this.entityPath.length; ++i) {
|
|
|
|
const entity = this.entityPath[i];
|
|
|
|
const pos = entity.components.StaticMapEntity;
|
|
|
|
const worldPos = pos.origin.toWorldSpaceCenterOfTile();
|
|
|
|
|
|
|
|
if (i === 0) {
|
|
|
|
parameters.context.moveTo(worldPos.x, worldPos.y);
|
|
|
|
} else {
|
|
|
|
parameters.context.lineTo(worldPos.x, worldPos.y);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
parameters.context.stroke();
|
|
|
|
|
|
|
|
// Items
|
|
|
|
let progress = this.spacingToFirstItem;
|
|
|
|
for (let i = 0; i < this.items.length; ++i) {
|
|
|
|
const nextDistanceAndItem = this.items[i];
|
|
|
|
const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile();
|
|
|
|
parameters.context.fillStyle = "#268e4d";
|
|
|
|
parameters.context.beginRoundedRect(worldPos.x - 5, worldPos.y - 5, 10, 10, 3);
|
|
|
|
parameters.context.fill();
|
|
|
|
parameters.context.font = "6px GameFont";
|
|
|
|
parameters.context.fillStyle = "#111";
|
|
|
|
parameters.context.fillText(
|
2020-06-26 14:31:36 +00:00
|
|
|
"" + round4Digits(nextDistanceAndItem[_nextDistance]),
|
2020-06-26 11:57:07 +00:00
|
|
|
worldPos.x + 5,
|
|
|
|
worldPos.y + 2
|
|
|
|
);
|
2020-06-26 14:31:36 +00:00
|
|
|
progress += nextDistanceAndItem[_nextDistance];
|
|
|
|
nextDistanceAndItem[_item].draw(worldPos.x, worldPos.y, parameters, 10);
|
2020-06-26 11:57:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < this.entityPath.length; ++i) {
|
|
|
|
const entity = this.entityPath[i];
|
|
|
|
parameters.context.fillStyle = "#d79a25";
|
|
|
|
const pos = entity.components.StaticMapEntity;
|
|
|
|
const worldPos = pos.origin.toWorldSpaceCenterOfTile();
|
|
|
|
parameters.context.beginCircle(worldPos.x, worldPos.y, i === 0 ? 5 : 3);
|
|
|
|
parameters.context.fill();
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let progress = 0; progress <= this.totalLength + 0.01; progress += 0.2) {
|
|
|
|
const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile();
|
|
|
|
parameters.context.fillStyle = "red";
|
|
|
|
parameters.context.beginCircle(worldPos.x, worldPos.y, 1);
|
|
|
|
parameters.context.fill();
|
|
|
|
}
|
|
|
|
|
|
|
|
const firstItemIndicator = this.computePositionFromProgress(
|
|
|
|
this.spacingToFirstItem
|
|
|
|
).toWorldSpaceCenterOfTile();
|
|
|
|
parameters.context.fillStyle = "purple";
|
|
|
|
parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2);
|
|
|
|
}
|
|
|
|
}
|