pull/303/head
Pascal Grossé 4 years ago
commit 8f867016de

@ -1,8 +1,9 @@
export const CHANGELOG = [ export const CHANGELOG = [
{ {
version: "1.1.18", version: "1.1.18",
date: "24.06.2020", date: "26.06.2020",
entries: [ entries: [
"Huge belt performance improvements - up to 50% increase",
"Preparations for the wires update", "Preparations for the wires update",
"Update belt placement performance on huge factories (by Phlosioneer)", "Update belt placement performance on huge factories (by Phlosioneer)",
"Allow clicking on variants to select them", "Allow clicking on variants to select them",

@ -51,7 +51,7 @@ export const globalConfig = {
// Belt speeds // Belt speeds
// NOTICE: Update webpack.production.config too! // NOTICE: Update webpack.production.config too!
beltSpeedItemsPerSecond: 2, beltSpeedItemsPerSecond: 2,
itemSpacingOnBelts: 0.8, itemSpacingOnBelts: 0.63,
minerSpeedItemsPerSecond: 0, // COMPUTED minerSpeedItemsPerSecond: 0, // COMPUTED
undergroundBeltMaxTilesByTier: [5, 8], undergroundBeltMaxTilesByTier: [5, 8],

@ -19,7 +19,7 @@ export class FormElement {
abstract; abstract;
} }
focus(parent) {} focus() {}
isValid() { isValid() {
return true; return true;

@ -72,6 +72,7 @@ function objectPolyfills() {
} }
if (!Object.entries) { if (!Object.entries) {
// @ts-ignore
Object.entries = function entries(O) { Object.entries = function entries(O) {
return reduce( return reduce(
keys(O), keys(O),

@ -405,8 +405,6 @@ export function findNiceValue(num) {
return Math_round(niceValue * 100) / 100; return Math_round(niceValue * 100) / 100;
} }
window.fn = findNiceValue;
/** /**
* Finds a nice integer value * Finds a nice integer value
* @see findNiceValue * @see findNiceValue

File diff suppressed because it is too large Load Diff

@ -1,13 +1,13 @@
import { DrawParameters } from "../../../core/draw_parameters"; import { DrawParameters } from "../core/draw_parameters";
import { Loader } from "../../../core/loader"; import { Loader } from "../core/loader";
import { createLogger } from "../../../core/logging"; import { createLogger } from "../core/logging";
import { Vector } from "../../../core/vector"; import { Vector } from "../core/vector";
import { Entity } from "../../entity"; import { Entity } from "./entity";
import { GameRoot } from "../../root"; import { GameRoot } from "./root";
import { findNiceIntegerValue } from "../../../core/utils"; import { findNiceIntegerValue } from "../core/utils";
import { Math_pow } from "../../../core/builtins"; import { Math_pow } from "../core/builtins";
import { blueprintShape } from "../../upgrades"; import { blueprintShape } from "./upgrades";
import { globalConfig } from "../../../core/config"; import { globalConfig } from "../core/config";
const logger = createLogger("blueprint"); const logger = createLogger("blueprint");
@ -176,7 +176,6 @@ export class Blueprint {
tryPlace(root, tile) { tryPlace(root, tile) {
return root.logic.performBulkOperation(() => { return root.logic.performBulkOperation(() => {
let anyPlaced = false; let anyPlaced = false;
const beltsToRegisterLater = [];
for (let i = 0; i < this.entities.length; ++i) { for (let i = 0; i < this.entities.length; ++i) {
let placeable = true; let placeable = true;
const entity = this.entities[i]; const entity = this.entities[i];
@ -217,21 +216,10 @@ export class Blueprint {
root.map.placeStaticEntity(clone); root.map.placeStaticEntity(clone);
// Registering a belt immediately triggers a recalculation of surrounding belt root.entityMgr.registerEntity(clone);
// directions, which is no good when not all belts have been placed. To resolve
// this, only register belts after all entities have been placed.
if (!clone.components.Belt) {
root.entityMgr.registerEntity(clone);
} else {
beltsToRegisterLater.push(clone);
}
anyPlaced = true; anyPlaced = true;
} }
} }
for (let i = 0; i < beltsToRegisterLater.length; i++) {
root.entityMgr.registerEntity(beltsToRegisterLater[i]);
}
return anyPlaced; return anyPlaced;
}); });
} }

@ -1,12 +1,12 @@
import { Component } from "../component"; import { Math_cos, Math_PI, Math_sin } from "../../core/builtins";
import { enumDirection, Vector } from "../../core/vector";
import { types } from "../../savegame/serialization"; import { types } from "../../savegame/serialization";
import { gItemRegistry } from "../../core/global_registries"; import { BeltPath } from "../belt_path";
import { BaseItem } from "../base_item"; import { Component } from "../component";
import { Vector, enumDirection } from "../../core/vector";
import { Math_PI, Math_sin, Math_cos } from "../../core/builtins";
import { globalConfig } from "../../core/config";
import { Entity } from "../entity"; import { Entity } from "../entity";
export const curvedBeltLength = /* Math_PI / 4 */ 0.78;
export class BeltComponent extends Component { export class BeltComponent extends Component {
static getId() { static getId() {
return "Belt"; return "Belt";
@ -16,7 +16,6 @@ export class BeltComponent extends Component {
// The followUpCache field is not serialized. // The followUpCache field is not serialized.
return { return {
direction: types.string, direction: types.string,
sortedItems: types.array(types.pair(types.float, types.obj(gItemRegistry))),
}; };
} }
@ -34,11 +33,22 @@ export class BeltComponent extends Component {
this.direction = direction; this.direction = direction;
/** @type {Array<[number, BaseItem]>} */
this.sortedItems = [];
/** @type {Entity} */ /** @type {Entity} */
this.followUpCache = null; this.followUpCache = null;
/**
* The path this belt is contained in, not serialized
* @type {BeltPath}
*/
this.assignedPath = null;
}
/**
* Returns the effective length of this belt in tile space
* @returns {number}
*/
getEffectiveLengthTiles() {
return this.direction === enumDirection.top ? 1.0 : curvedBeltLength;
} }
/** /**
@ -50,14 +60,17 @@ export class BeltComponent extends Component {
transformBeltToLocalSpace(progress) { transformBeltToLocalSpace(progress) {
switch (this.direction) { switch (this.direction) {
case enumDirection.top: case enumDirection.top:
assert(progress <= 1.02, "Invalid progress: " + progress);
return new Vector(0, 0.5 - progress); return new Vector(0, 0.5 - progress);
case enumDirection.right: { case enumDirection.right: {
const arcProgress = progress * 0.5 * Math_PI; assert(progress <= curvedBeltLength + 0.02, "Invalid progress 2: " + progress);
const arcProgress = (progress / curvedBeltLength) * 0.5 * Math_PI;
return new Vector(0.5 - 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress)); return new Vector(0.5 - 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress));
} }
case enumDirection.left: { case enumDirection.left: {
const arcProgress = progress * 0.5 * Math_PI; assert(progress <= curvedBeltLength + 0.02, "Invalid progress 3: " + progress);
const arcProgress = (progress / curvedBeltLength) * 0.5 * Math_PI;
return new Vector(-0.5 + 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress)); return new Vector(-0.5 + 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress));
} }
default: default:
@ -65,46 +78,4 @@ export class BeltComponent extends Component {
return new Vector(0, 0); return new Vector(0, 0);
} }
} }
/**
* Returns if the belt can currently accept an item from the given direction
*/
canAcceptItem() {
const firstItem = this.sortedItems[0];
if (!firstItem) {
return true;
}
return firstItem[0] > globalConfig.itemSpacingOnBelts;
}
/**
* Pushes a new item to the belt
* @param {BaseItem} item
*/
takeItem(item, leftoverProgress = 0.0) {
if (G_IS_DEV) {
assert(
this.sortedItems.length === 0 ||
leftoverProgress <= this.sortedItems[0][0] - globalConfig.itemSpacingOnBelts + 0.001,
"Invalid leftover: " +
leftoverProgress +
" items are " +
this.sortedItems.map(item => item[0])
);
assert(leftoverProgress < 1.0, "Invalid leftover: " + leftoverProgress);
}
this.sortedItems.unshift([leftoverProgress, item]);
}
/**
* Returns how much space there is to the first item
*/
getDistanceToFirstItemCenter() {
const firstItem = this.sortedItems[0];
if (!firstItem) {
return 1;
}
return firstItem[0];
}
} }

@ -194,4 +194,17 @@ export class ItemEjectorComponent extends Component {
this.slots[slotIndex].progress = this.instantEject ? 1 : 0; this.slots[slotIndex].progress = this.instantEject ? 1 : 0;
return true; return true;
} }
/**
* Clears the given slot and returns the item it had
* @param {number} slotIndex
* @returns {BaseItem|null}
*/
takeSlotItem(slotIndex) {
const slot = this.slots[slotIndex];
const item = slot.item;
slot.item = null;
slot.progress = 0.0;
return item;
}
} }

@ -418,6 +418,10 @@ export class GameCore {
root.map.drawStaticEntityDebugOverlays(params); root.map.drawStaticEntityDebugOverlays(params);
} }
if (G_IS_DEV && globalConfig.debug.renderBeltPaths) {
systems.belt.drawBeltPathDebug(params);
}
// END OF GAME CONTENT // END OF GAME CONTENT
// ----- // -----

@ -74,8 +74,11 @@ export class GameHUD {
shapeViewer: new HUDShapeViewer(this.root), shapeViewer: new HUDShapeViewer(this.root),
wiresOverlay: new HUDWiresOverlay(this.root), wiresOverlay: new HUDWiresOverlay(this.root),
// Typing hints
/* typehints:start */
/** @type {HUDChangesDebugger} */ /** @type {HUDChangesDebugger} */
changesDebugger: null, changesDebugger: null,
/* typehints:end */
}; };
this.signals = { this.signals = {

@ -9,7 +9,7 @@ import { KEYMAPPINGS } from "../../key_action_mapper";
import { blueprintShape } from "../../upgrades"; import { blueprintShape } from "../../upgrades";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { Blueprint } from "./blueprint"; import { Blueprint } from "../../blueprint";
import { SOUNDS } from "../../../platform/sound"; import { SOUNDS } from "../../../platform/sound";
export class HUDBlueprintPlacer extends BaseHUDPart { export class HUDBlueprintPlacer extends BaseHUDPart {

@ -48,9 +48,9 @@ export class HUDColorBlindHelper extends BaseHUDPart {
// Check if the belt has a color item // Check if the belt has a color item
if (beltComp) { if (beltComp) {
const firstItem = beltComp.sortedItems[0]; const item = beltComp.assignedPath.findItemAtTile(tile);
if (firstItem && firstItem[1] instanceof ColorItem) { if (item && item instanceof ColorItem) {
return firstItem[1].color; return item.color;
} }
} }

@ -27,7 +27,7 @@ export class HUDChangesDebugger extends BaseHUDPart {
* @param {string} fillColor Color to display (Hex) * @param {string} fillColor Color to display (Hex)
* @param {number=} timeToDisplay How long to display the change * @param {number=} timeToDisplay How long to display the change
*/ */
renderChange(label, area, fillColor, timeToDisplay = 2) { renderChange(label, area, fillColor, timeToDisplay = 0.3) {
this.changes.push({ this.changes.push({
label, label,
area: area.clone(), area: area.clone(),

@ -196,7 +196,7 @@ export class GameLogic {
* @param {function} operation * @param {function} operation
*/ */
performBulkOperation(operation) { performBulkOperation(operation) {
logger.log("Running bulk operation ..."); logger.warn("Running bulk operation ...");
assert(!this.root.bulkOperationRunning, "Can not run two bulk operations twice"); assert(!this.root.bulkOperationRunning, "Can not run two bulk operations twice");
this.root.bulkOperationRunning = true; this.root.bulkOperationRunning = true;
const now = performanceNow(); const now = performanceNow();
@ -227,6 +227,7 @@ export class GameLogic {
} }
this.root.map.removeStaticEntity(building); this.root.map.removeStaticEntity(building);
this.root.entityMgr.destroyEntity(building); this.root.entityMgr.destroyEntity(building);
this.root.entityMgr.processDestroyList();
return true; return true;
} }

@ -1,21 +1,18 @@
import { Math_sqrt, Math_max } from "../../core/builtins"; import { Math_sqrt } from "../../core/builtins";
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters"; import { DrawParameters } from "../../core/draw_parameters";
import { gMetaBuildingRegistry } from "../../core/global_registries";
import { Loader } from "../../core/loader"; import { Loader } from "../../core/loader";
import { createLogger } from "../../core/logging"; import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle";
import { AtlasSprite } from "../../core/sprites"; import { AtlasSprite } from "../../core/sprites";
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../../core/vector"; import { enumDirection, enumDirectionToVector, enumInvertedDirections } from "../../core/vector";
import { MetaBeltBaseBuilding } from "../buildings/belt_base"; import { BeltPath } from "../belt_path";
import { BeltComponent } from "../components/belt"; import { BeltComponent } from "../components/belt";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view"; import { MapChunkView } from "../map_chunk_view";
import { defaultBuildingVariant } from "../meta_building"; import { fastArrayDeleteValue } from "../../core/utils";
export const BELT_ANIM_COUNT = 28; export const BELT_ANIM_COUNT = 28;
const SQRT_2 = Math_sqrt(2);
const logger = createLogger("belt"); const logger = createLogger("belt");
@ -52,10 +49,53 @@ export class BeltSystem extends GameSystemWithFilter {
this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this);
this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this);
this.root.signals.postLoadHook.add(this.computeBeltCache, this); this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this);
this.root.signals.entityAdded.add(this.onEntityAdded, this);
/** @type {Rectangle} */ // /** @type {Rectangle} */
this.areaToRecompute = null; // this.areaToRecompute = null;
/** @type {Array<BeltPath>} */
this.beltPaths = [];
}
/**
* Serializes all belt paths
*/
serializePaths() {
let data = [];
for (let i = 0; i < this.beltPaths.length; ++i) {
data.push(this.beltPaths[i].serialize());
}
return data;
}
/**
* Deserializes all belt paths
* @param {Array<any>} data
*/
deserializePaths(data) {
if (!Array.isArray(data)) {
return "Belt paths are not an array: " + typeof data;
}
for (let i = 0; i < data.length; ++i) {
const path = BeltPath.fromSerialized(this.root, data[i]);
if (!(path instanceof BeltPath)) {
return "Failed to create path from belt data: " + path;
}
this.beltPaths.push(path);
}
if (this.beltPaths.length === 0) {
logger.warn("Recomputing belt paths (most likely the savegame is old)");
this.recomputeAllBeltPaths();
} else {
logger.warn("Restored", this.beltPaths.length, "belt paths");
}
this.verifyBeltPaths();
} }
/** /**
@ -72,10 +112,7 @@ export class BeltSystem extends GameSystemWithFilter {
return; return;
} }
if (entity.components.Belt) { /*
this.cacheNeedsUpdate = true;
}
const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding);
// Compute affected area // Compute affected area
@ -85,6 +122,8 @@ export class BeltSystem extends GameSystemWithFilter {
// Store if anything got changed, if so we need to queue a recompute // Store if anything got changed, if so we need to queue a recompute
let anythingChanged = false; let anythingChanged = false;
anythingChanged = true; // TODO / FIXME
for (let x = affectedArea.x; x < affectedArea.right(); ++x) { for (let x = affectedArea.x; x < affectedArea.right(); ++x) {
for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) {
if (!originalRect.containsPoint(x, y)) { if (!originalRect.containsPoint(x, y)) {
@ -121,10 +160,133 @@ export class BeltSystem extends GameSystemWithFilter {
logger.log("Queuing recompute:", this.areaToRecompute); logger.log("Queuing recompute:", this.areaToRecompute);
} }
} }
// 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");
this.deleteEntityFromPath(assignedPath, entity);
this.verifyBeltPaths();
}
/**
* Attempts to delete the belt from its current path
* @param {BeltPath} path
* @param {Entity} entity
*/
deleteEntityFromPath(path, entity) {
if (path.entityPath.length === 1) {
// This is a single entity path, easy to do, simply erase whole path
fastArrayDeleteValue(this.beltPaths, path);
return;
}
// Notice: Since there might be circular references, it is important to check
// which role the entity has
if (path.isStartEntity(entity)) {
// We tried to delete the start
path.deleteEntityOnStart(entity);
} else if (path.isEndEntity(entity)) {
// We tried to delete the end
path.deleteEntityOnEnd(entity);
} else {
// We tried to delete something inbetween
const newPath = path.deleteEntityOnPathSplitIntoTwo(entity);
this.beltPaths.push(newPath);
}
}
/**
* Called when an entity got added
* @param {Entity} entity
*/
onEntityAdded(entity) {
if (!this.root.gameInitialized) {
return;
}
if (!entity.components.Belt) {
return;
}
const fromEntity = this.findSupplyingEntity(entity);
const toEntity = this.findFollowUpEntity(entity);
// 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;
if (fromPath === toPath) {
// This is a circular dependency -> Ignore
} else {
fromPath.extendByPath(toPath);
// Delete now obsolete path
fastArrayDeleteValue(this.beltPaths, toPath);
}
}
} 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);
}
}
this.verifyBeltPaths();
} }
draw(parameters) { draw(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this)); for (let i = 0; i < this.beltPaths.length; ++i) {
this.beltPaths[i].draw(parameters);
}
}
/**
* Verifies all belt paths
*/
verifyBeltPaths() {
if (G_IS_DEV && true) {
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);
}
}
}
} }
/** /**
@ -166,166 +328,102 @@ export class BeltSystem extends GameSystemWithFilter {
} }
/** /**
* Recomputes the belt cache * Finds the supplying belt for a given belt. Used for building the dependencies
* @param {Entity} entity
*/ */
computeBeltCache() { findSupplyingEntity(entity) {
if (this.areaToRecompute) { const staticComp = entity.components.StaticMapEntity;
logger.log("Updating belt cache by updating area:", this.areaToRecompute);
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) { const supplyDirection = staticComp.localDirectionToWorld(enumDirection.bottom);
for (let y = this.areaToRecompute.y; y < this.areaToRecompute.bottom(); ++y) { const supplyVector = enumDirectionToVector[supplyDirection];
const tile = this.root.map.getTileContentXY(x, y);
if (tile && tile.components.Belt) {
tile.components.Belt.followUpCache = this.findFollowUpEntity(tile);
}
}
}
// Reset stale areas afterwards const supplyTile = staticComp.origin.add(supplyVector);
this.areaToRecompute = null; const supplyEntity = this.root.map.getTileContent(supplyTile);
} else {
logger.log("Doing full belt recompute");
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) { // Check if theres a belt at the tile we point to
const entity = this.allEntities[i]; if (supplyEntity) {
entity.components.Belt.followUpCache = this.findFollowUpEntity(entity); 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;
}
}
} }
} }
}
update() { return null;
if (this.areaToRecompute) { }
this.computeBeltCache();
}
// Divide by item spacing on belts since we use throughput and not speed /**
let beltSpeed = * Computes the belt path network
this.root.hubGoals.getBeltBaseSpeed() * */
this.root.dynamicTickrate.deltaSeconds * recomputeAllBeltPaths() {
globalConfig.itemSpacingOnBelts; logger.warn("Recomputing all belt paths");
const visitedUids = new Set();
if (G_IS_DEV && globalConfig.debug.instantBelts) { const result = [];
beltSpeed *= 100;
}
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
if (visitedUids.has(entity.uid)) {
const beltComp = entity.components.Belt;
const items = beltComp.sortedItems;
if (items.length === 0) {
// Fast out for performance
continue; continue;
} }
const ejectorComp = entity.components.ItemEjector; // Mark entity as visited
let maxProgress = 1; visitedUids.add(entity.uid);
/* PERFORMANCE OPTIMIZATION */ // Compute path, start with entity and find precedors / successors
// Original: const path = [entity];
// 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 let maxIter = 9999;
// will be on the corner
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();
maxProgress = Math.min(2, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt);
// Useful check, but hurts performance // Find precedors
// assert(maxProgress >= 0.0, "max progress < 0 (I) (" + maxProgress + ")"); let prevEntity = this.findSupplyingEntity(entity);
while (prevEntity && --maxIter > 0) {
if (visitedUids.has(prevEntity.uid)) {
break;
} }
path.unshift(prevEntity);
visitedUids.add(prevEntity.uid);
prevEntity = this.findSupplyingEntity(prevEntity);
} }
let speedMultiplier = 1; // Find succedors
if (beltComp.direction !== enumDirection.top) { let nextEntity = this.findFollowUpEntity(entity);
// Curved belts are shorter, thus being quicker (Looks weird otherwise) while (nextEntity && --maxIter > 0) {
speedMultiplier = SQRT_2; if (visitedUids.has(nextEntity.uid)) {
} break;
}
// 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. path.push(nextEntity);
if (items.length > 2 / globalConfig.itemSpacingOnBelts) { visitedUids.add(nextEntity.uid);
beltComp.sortedItems = []; nextEntity = this.findFollowUpEntity(nextEntity);
} }
for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) { assert(maxIter > 1, "Ran out of iterations");
const progressAndItem = items[itemIndex]; result.push(new BeltPath(this.root, path));
}
progressAndItem[0] = Math.min(maxProgress, progressAndItem[0] + speedMultiplier * beltSpeed); logger.log("Found", this.beltPaths.length, "belt paths");
assert(progressAndItem[0] >= 0, "Bad progress: " + progressAndItem[0]); this.beltPaths = result;
}
if (progressAndItem[0] >= 1.0) { update() {
if (beltComp.followUpCache) { this.verifyBeltPaths();
const followUpBelt = beltComp.followUpCache.components.Belt;
if (followUpBelt.canAcceptItem()) { for (let i = 0; i < this.beltPaths.length; ++i) {
followUpBelt.takeItem( this.beltPaths[i].update();
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
/* PERFORMANCE OPTIMIZATION */
// Original:
// const freeSlot = ejectorComp.getFirstFreeSlot();
// 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
if (!ejectorComp.tryEject(0, progressAndItem[1])) {
assert(false, "Ejection failed");
}
items.splice(itemIndex, 1);
// 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
maxProgress = Math.max(0, progressAndItem[0] - globalConfig.itemSpacingOnBelts);
}
}
} }
this.verifyBeltPaths();
} }
/** /**
@ -368,38 +466,12 @@ export class BeltSystem extends GameSystemWithFilter {
} }
/** /**
* Draws the belt parameters
* @param {DrawParameters} parameters * @param {DrawParameters} parameters
* @param {Entity} entity
*/ */
drawEntityItems(parameters, entity) { drawBeltPathDebug(parameters) {
const beltComp = entity.components.Belt; for (let i = 0; i < this.beltPaths.length; ++i) {
const staticComp = entity.components.StaticMapEntity; this.beltPaths[i].drawDebug(parameters);
const items = beltComp.sortedItems;
if (items.length === 0) {
// Fast out for performance
return;
}
if (!staticComp.shouldBeDrawn(parameters)) {
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
);
} }
} }
} }

@ -239,9 +239,9 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
const beltComp = receiver.components.Belt; const beltComp = receiver.components.Belt;
if (beltComp) { if (beltComp) {
// Ayy, its a belt! const path = beltComp.assignedPath;
if (beltComp.canAcceptItem()) { assert(path, "belt has no path");
beltComp.takeItem(item); if (path.tryAcceptItem(item)) {
return true; return true;
} }
} }

@ -14,6 +14,7 @@ import { SavegameInterface_V1001 } from "./schemas/1001";
import { SavegameInterface_V1002 } from "./schemas/1002"; import { SavegameInterface_V1002 } from "./schemas/1002";
import { SavegameInterface_V1003 } from "./schemas/1003"; import { SavegameInterface_V1003 } from "./schemas/1003";
import { SavegameInterface_V1004 } from "./schemas/1004"; import { SavegameInterface_V1004 } from "./schemas/1004";
import { SavegameInterface_V1005 } from "./schemas/1005";
const logger = createLogger("savegame"); const logger = createLogger("savegame");
@ -45,7 +46,7 @@ export class Savegame extends ReadWriteProxy {
* @returns {number} * @returns {number}
*/ */
static getCurrentVersion() { static getCurrentVersion() {
return 1004; return 1005;
} }
/** /**
@ -104,6 +105,11 @@ export class Savegame extends ReadWriteProxy {
data.version = 1004; data.version = 1004;
} }
if (data.version === 1004) {
SavegameInterface_V1005.migrate1004to1005(data);
data.version = 1005;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }

@ -5,6 +5,7 @@ import { SavegameInterface_V1001 } from "./schemas/1001";
import { SavegameInterface_V1002 } from "./schemas/1002"; import { SavegameInterface_V1002 } from "./schemas/1002";
import { SavegameInterface_V1003 } from "./schemas/1003"; import { SavegameInterface_V1003 } from "./schemas/1003";
import { SavegameInterface_V1004 } from "./schemas/1004"; import { SavegameInterface_V1004 } from "./schemas/1004";
import { SavegameInterface_V1005 } from "./schemas/1005";
/** @type {Object.<number, typeof BaseSavegameInterface>} */ /** @type {Object.<number, typeof BaseSavegameInterface>} */
export const savegameInterfaces = { export const savegameInterfaces = {
@ -13,6 +14,7 @@ export const savegameInterfaces = {
1002: SavegameInterface_V1002, 1002: SavegameInterface_V1002,
1003: SavegameInterface_V1003, 1003: SavegameInterface_V1003,
1004: SavegameInterface_V1004, 1004: SavegameInterface_V1004,
1005: SavegameInterface_V1005,
}; };
const logger = createLogger("savegame_interface_registry"); const logger = createLogger("savegame_interface_registry");

@ -40,6 +40,7 @@ export class SavegameSerializer {
hubGoals: root.hubGoals.serialize(), hubGoals: root.hubGoals.serialize(),
pinnedShapes: root.hud.parts.pinnedShapes.serialize(), pinnedShapes: root.hud.parts.pinnedShapes.serialize(),
waypoints: root.hud.parts.waypoints.serialize(), waypoints: root.hud.parts.waypoints.serialize(),
beltPaths: root.systemMgr.systems.belt.serializePaths(),
}; };
data.entities = this.internal.serializeEntityArray(root.entityMgr.entities); data.entities = this.internal.serializeEntityArray(root.entityMgr.entities);
@ -140,6 +141,7 @@ export class SavegameSerializer {
errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes);
errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints);
errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities);
errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths);
// Check for errors // Check for errors
if (errorReason) { if (errorReason) {

@ -5,6 +5,10 @@ import { Entity } from "../game/entity";
* }} SavegameStats * }} SavegameStats
*/ */
/**
*
*/
/** /**
* @typedef {{ * @typedef {{
* camera: any, * camera: any,
@ -14,7 +18,8 @@ import { Entity } from "../game/entity";
* hubGoals: any, * hubGoals: any,
* pinnedShapes: any, * pinnedShapes: any,
* waypoints: any, * waypoints: any,
* entities: Array<Entity> * entities: Array<Entity>,
* beltPaths: Array<any>
* }} SerializedGame * }} SerializedGame
*/ */

@ -0,0 +1,29 @@
import { createLogger } from "../../core/logging.js";
import { SavegameInterface_V1004 } from "./1004.js";
const schema = require("./1005.json");
const logger = createLogger("savegame_interface/1005");
export class SavegameInterface_V1005 extends SavegameInterface_V1004 {
getVersion() {
return 1005;
}
getSchemaUncached() {
return schema;
}
/**
* @param {import("../savegame_typedefs.js").SavegameData} data
*/
static migrate1004to1005(data) {
logger.log("Migrating 1004 to 1005");
const dump = data.dump;
if (!dump) {
return true;
}
// just reset belt paths for now
dump.beltPaths = [];
}
}

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}
Loading…
Cancel
Save