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

Fix buildings not working at their advertised speed, closes #440, closes #442, closes #437, closes #449

This commit is contained in:
tobspr
2020-08-29 22:35:30 +02:00
parent 12892dcf54
commit 78fe34840a
17 changed files with 750 additions and 806 deletions

View File

@@ -109,6 +109,11 @@ export class ItemProcessorComponent extends Component {
* How long it takes until we are done with the current items
*/
this.secondsUntilEject = 0;
/**
* How much processing time we have lest from the last tick
*/
this.bonusFromLastTick = 0;
}
/**

View File

@@ -1,58 +1,58 @@
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
const chainBufferSize = 3;
export class MinerComponent extends Component {
static getId() {
return "Miner";
}
static getSchema() {
// cachedMinedItem is not serialized.
return {
lastMiningTime: types.ufloat,
itemChainBuffer: types.array(typeItemSingleton),
};
}
duplicateWithoutContents() {
return new MinerComponent({
chainable: this.chainable,
});
}
constructor({ chainable = false }) {
super();
this.lastMiningTime = 0;
this.chainable = chainable;
/**
* Stores items from other miners which were chained to this
* miner.
* @type {Array<BaseItem>}
*/
this.itemChainBuffer = [];
/**
* @type {BaseItem}
*/
this.cachedMinedItem = null;
}
/**
*
* @param {BaseItem} item
*/
tryAcceptChainedItem(item) {
if (this.itemChainBuffer.length > chainBufferSize) {
// Well, this one is full
return false;
}
this.itemChainBuffer.push(item);
return true;
}
}
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
const chainBufferSize = 6;
export class MinerComponent extends Component {
static getId() {
return "Miner";
}
static getSchema() {
// cachedMinedItem is not serialized.
return {
lastMiningTime: types.ufloat,
itemChainBuffer: types.array(typeItemSingleton),
};
}
duplicateWithoutContents() {
return new MinerComponent({
chainable: this.chainable,
});
}
constructor({ chainable = false }) {
super();
this.lastMiningTime = 0;
this.chainable = chainable;
/**
* Stores items from other miners which were chained to this
* miner.
* @type {Array<BaseItem>}
*/
this.itemChainBuffer = [];
/**
* @type {BaseItem}
*/
this.cachedMinedItem = null;
}
/**
*
* @param {BaseItem} item
*/
tryAcceptChainedItem(item) {
if (this.itemChainBuffer.length > chainBufferSize) {
// Well, this one is full
return false;
}
this.itemChainBuffer.push(item);
return true;
}
}

View File

@@ -25,10 +25,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
const ejectorComp = entity.components.ItemEjector;
// First of all, process the current recipe
processorComp.secondsUntilEject = Math.max(
0,
processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds
);
const newSecondsUntilEject =
processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds;
processorComp.secondsUntilEject = Math.max(0, newSecondsUntilEject);
if (newSecondsUntilEject < 0) {
processorComp.bonusFromLastTick -= newSecondsUntilEject;
}
if (G_IS_DEV && globalConfig.debug.instantProcessors) {
processorComp.secondsUntilEject = 0;
@@ -233,7 +237,10 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
}
const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type);
processorComp.secondsUntilEject = 1 / baseSpeed;
// Substract one tick because we already process it this frame
processorComp.secondsUntilEject = Math.max(0, 1 / baseSpeed - processorComp.bonusFromLastTick);
processorComp.bonusFromLastTick = 0;
/** @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} */
const outItems = [];

View File

@@ -1,140 +1,144 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { enumDirectionToVector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { MinerComponent } from "../components/miner";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
export class MinerSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [MinerComponent]);
}
update() {
let miningSpeed = this.root.hubGoals.getMinerBaseSpeed();
if (G_IS_DEV && globalConfig.debug.instantMiners) {
miningSpeed *= 100;
}
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
// Check if miner is above an actual tile
const minerComp = entity.components.Miner;
if (!minerComp.cachedMinedItem) {
const staticComp = entity.components.StaticMapEntity;
const tileBelow = this.root.map.getLowerLayerContentXY(
staticComp.origin.x,
staticComp.origin.y
);
if (!tileBelow) {
continue;
}
minerComp.cachedMinedItem = tileBelow;
}
// First, try to get rid of chained items
if (minerComp.itemChainBuffer.length > 0) {
if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) {
minerComp.itemChainBuffer.shift();
continue;
}
}
if (this.root.time.isIngameTimerExpired(minerComp.lastMiningTime, 1 / miningSpeed)) {
if (this.tryPerformMinerEject(entity, minerComp.cachedMinedItem)) {
// Analytics hook
this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem);
// Actually mine
minerComp.lastMiningTime = this.root.time.now();
}
}
}
}
/**
*
* @param {Entity} entity
* @param {BaseItem} item
*/
tryPerformMinerEject(entity, item) {
const minerComp = entity.components.Miner;
const ejectComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity;
// Check if we are a chained miner
if (minerComp.chainable) {
const ejectingSlot = ejectComp.slots[0];
const ejectingPos = staticComp.localTileToWorld(ejectingSlot.pos);
const ejectingDirection = staticComp.localDirectionToWorld(ejectingSlot.direction);
const targetTile = ejectingPos.add(enumDirectionToVector[ejectingDirection]);
const targetContents = this.root.map.getTileContent(targetTile, "regular");
// Check if we are connected to another miner and thus do not eject directly
if (targetContents) {
const targetMinerComp = targetContents.components.Miner;
if (targetMinerComp) {
if (targetMinerComp.tryAcceptChainedItem(item)) {
return true;
} else {
return false;
}
}
}
}
// Seems we are a regular miner or at the end of a row, try actually ejecting
if (ejectComp.tryEject(0, item)) {
return true;
}
return false;
}
/**
*
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const minerComp = entity.components.Miner;
if (!minerComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
if (!minerComp.cachedMinedItem) {
continue;
}
// Draw the item background - this is to hide the ejected item animation from
// the item ejecto
const padding = 3;
const destX = staticComp.origin.x * globalConfig.tileSize + padding;
const destY = staticComp.origin.y * globalConfig.tileSize + padding;
const dimensions = globalConfig.tileSize - 2 * padding;
if (parameters.visibleRect.containsRect4Params(destX, destY, dimensions, dimensions)) {
parameters.context.fillStyle = minerComp.cachedMinedItem.getBackgroundColorAsResource();
parameters.context.fillRect(destX, destY, dimensions, dimensions);
}
minerComp.cachedMinedItem.drawItemCenteredClipped(
(0.5 + staticComp.origin.x) * globalConfig.tileSize,
(0.5 + staticComp.origin.y) * globalConfig.tileSize,
parameters,
globalConfig.defaultItemDiameter
);
}
}
}
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { enumDirectionToVector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { MinerComponent } from "../components/miner";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
export class MinerSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [MinerComponent]);
}
update() {
let miningSpeed = this.root.hubGoals.getMinerBaseSpeed();
if (G_IS_DEV && globalConfig.debug.instantMiners) {
miningSpeed *= 100;
}
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
// Check if miner is above an actual tile
const minerComp = entity.components.Miner;
if (!minerComp.cachedMinedItem) {
const staticComp = entity.components.StaticMapEntity;
const tileBelow = this.root.map.getLowerLayerContentXY(
staticComp.origin.x,
staticComp.origin.y
);
if (!tileBelow) {
continue;
}
minerComp.cachedMinedItem = tileBelow;
}
// First, try to get rid of chained items
if (minerComp.itemChainBuffer.length > 0) {
if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) {
minerComp.itemChainBuffer.shift();
continue;
}
}
const mineDuration = 1 / miningSpeed;
const timeSinceMine = this.root.time.now() - minerComp.lastMiningTime;
if (timeSinceMine > mineDuration) {
// Store how much we overflowed
const buffer = Math.min(timeSinceMine - mineDuration, this.root.dynamicTickrate.deltaSeconds);
if (this.tryPerformMinerEject(entity, minerComp.cachedMinedItem)) {
// Analytics hook
this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem);
// Store mining time
minerComp.lastMiningTime = this.root.time.now() - buffer;
}
}
}
}
/**
*
* @param {Entity} entity
* @param {BaseItem} item
*/
tryPerformMinerEject(entity, item) {
const minerComp = entity.components.Miner;
const ejectComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity;
// Check if we are a chained miner
if (minerComp.chainable) {
const ejectingSlot = ejectComp.slots[0];
const ejectingPos = staticComp.localTileToWorld(ejectingSlot.pos);
const ejectingDirection = staticComp.localDirectionToWorld(ejectingSlot.direction);
const targetTile = ejectingPos.add(enumDirectionToVector[ejectingDirection]);
const targetContents = this.root.map.getTileContent(targetTile, "regular");
// Check if we are connected to another miner and thus do not eject directly
if (targetContents) {
const targetMinerComp = targetContents.components.Miner;
if (targetMinerComp) {
if (targetMinerComp.tryAcceptChainedItem(item)) {
return true;
} else {
return false;
}
}
}
}
// Seems we are a regular miner or at the end of a row, try actually ejecting
if (ejectComp.tryEject(0, item)) {
return true;
}
return false;
}
/**
*
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const minerComp = entity.components.Miner;
if (!minerComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
if (!minerComp.cachedMinedItem) {
continue;
}
// Draw the item background - this is to hide the ejected item animation from
// the item ejecto
const padding = 3;
const destX = staticComp.origin.x * globalConfig.tileSize + padding;
const destY = staticComp.origin.y * globalConfig.tileSize + padding;
const dimensions = globalConfig.tileSize - 2 * padding;
if (parameters.visibleRect.containsRect4Params(destX, destY, dimensions, dimensions)) {
parameters.context.fillStyle = minerComp.cachedMinedItem.getBackgroundColorAsResource();
parameters.context.fillRect(destX, destY, dimensions, dimensions);
}
minerComp.cachedMinedItem.drawItemCenteredClipped(
(0.5 + staticComp.origin.x) * globalConfig.tileSize,
(0.5 + staticComp.origin.y) * globalConfig.tileSize,
parameters,
globalConfig.defaultItemDiameter
);
}
}
}

View File

@@ -1,214 +1,200 @@
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { types, BasicSerializableObject } from "../../savegame/serialization";
import { RegularGameSpeed } from "./regular_game_speed";
import { BaseGameSpeed } from "./base_game_speed";
import { PausedGameSpeed } from "./paused_game_speed";
import { FastForwardGameSpeed } from "./fast_forward_game_speed";
import { gGameSpeedRegistry } from "../../core/global_registries";
import { globalConfig } from "../../core/config";
import { checkTimerExpired, quantizeFloat } from "../../core/utils";
import { createLogger } from "../../core/logging";
const logger = createLogger("game_time");
export class GameTime extends BasicSerializableObject {
/**
* @param {GameRoot} root
*/
constructor(root) {
super();
this.root = root;
// Current ingame time seconds, not incremented while paused
this.timeSeconds = 0;
// Current "realtime", a timer which always is incremented no matter whether the game is paused or no
this.realtimeSeconds = 0;
// The adjustment, used when loading savegames so we can continue where we were
this.realtimeAdjust = 0;
/** @type {BaseGameSpeed} */
this.speed = new RegularGameSpeed(this.root);
// Store how much time we have in bucket
this.logicTimeBudget = 0;
}
static getId() {
return "GameTime";
}
static getSchema() {
return {
timeSeconds: types.float,
speed: types.obj(gGameSpeedRegistry),
realtimeSeconds: types.float,
};
}
/**
* Fetches the new "real" time, called from the core once per frame, since performance now() is kinda slow
*/
updateRealtimeNow() {
this.realtimeSeconds = performance.now() / 1000.0 + this.realtimeAdjust;
}
/**
* Returns the ingame time in milliseconds
*/
getTimeMs() {
return this.timeSeconds * 1000.0;
}
/**
* Safe check to check if a timer is expired. quantizes numbers
* @param {number} lastTick Last tick of the timer
* @param {number} tickRateSeconds Interval of the timer in seconds
*/
isIngameTimerExpired(lastTick, tickRateSeconds) {
return checkTimerExpired(this.timeSeconds, lastTick, tickRateSeconds);
}
/**
* Returns how many seconds we are in the grace period
* @returns {number}
*/
getRemainingGracePeriodSeconds() {
return 0;
}
/**
* Returns if we are currently in the grace period
* @returns {boolean}
*/
getIsWithinGracePeriod() {
return this.getRemainingGracePeriodSeconds() > 0;
}
/**
* Internal method to generate new logic time budget
* @param {number} deltaMs
*/
internalAddDeltaToBudget(deltaMs) {
// Only update if game is supposed to update
if (this.root.hud.shouldPauseGame()) {
this.logicTimeBudget = 0;
} else {
const multiplier = this.getSpeed().getTimeMultiplier();
this.logicTimeBudget += deltaMs * multiplier;
}
// Check for too big pile of updates -> reduce it to 1
let maxLogicSteps = Math.max(
3,
(this.speed.getMaxLogicStepsInQueue() * this.root.dynamicTickrate.currentTickRate) / 60
);
if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) {
maxLogicSteps *= 1 + globalConfig.debug.framePausesBetweenTicks;
}
if (this.logicTimeBudget > this.root.dynamicTickrate.deltaMs * maxLogicSteps) {
this.logicTimeBudget = this.root.dynamicTickrate.deltaMs * maxLogicSteps;
}
}
/**
* Performs update ticks based on the queued logic budget
* @param {number} deltaMs
* @param {function():boolean} updateMethod
*/
performTicks(deltaMs, updateMethod) {
this.internalAddDeltaToBudget(deltaMs);
const speedAtStart = this.root.time.getSpeed();
let effectiveDelta = this.root.dynamicTickrate.deltaMs;
if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) {
effectiveDelta += globalConfig.debug.framePausesBetweenTicks * this.root.dynamicTickrate.deltaMs;
}
// Update physics & logic
while (this.logicTimeBudget >= effectiveDelta) {
this.logicTimeBudget -= effectiveDelta;
if (!updateMethod()) {
// Gameover happened or so, do not update anymore
return;
}
// Step game time
this.timeSeconds = quantizeFloat(this.timeSeconds + this.root.dynamicTickrate.deltaSeconds);
// Game time speed changed, need to abort since our logic steps are no longer valid
if (speedAtStart.getId() !== this.speed.getId()) {
logger.warn(
"Skipping update because speed changed from",
speedAtStart.getId(),
"to",
this.speed.getId()
);
break;
}
}
}
/**
* Returns ingame time in seconds
* @returns {number} seconds
*/
now() {
return this.timeSeconds;
}
/**
* Returns "real" time in seconds
* @returns {number} seconds
*/
realtimeNow() {
return this.realtimeSeconds;
}
/**
* Returns "real" time in seconds
* @returns {number} seconds
*/
systemNow() {
return (this.realtimeSeconds - this.realtimeAdjust) * 1000.0;
}
getIsPaused() {
return this.speed.getId() === PausedGameSpeed.getId();
}
getSpeed() {
return this.speed;
}
setSpeed(speed) {
assert(speed instanceof BaseGameSpeed, "Not a valid game speed");
if (this.speed.getId() === speed.getId()) {
logger.warn("Same speed set than current one:", speed.constructor.getId());
}
this.speed = speed;
}
deserialize(data) {
const errorCode = super.deserialize(data);
if (errorCode) {
return errorCode;
}
// Adjust realtime now difference so they match
this.realtimeAdjust = this.realtimeSeconds - performance.now() / 1000.0;
this.updateRealtimeNow();
// Make sure we have a quantizied time
this.timeSeconds = quantizeFloat(this.timeSeconds);
this.speed.initializeAfterDeserialize(this.root);
}
}
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { types, BasicSerializableObject } from "../../savegame/serialization";
import { RegularGameSpeed } from "./regular_game_speed";
import { BaseGameSpeed } from "./base_game_speed";
import { PausedGameSpeed } from "./paused_game_speed";
import { gGameSpeedRegistry } from "../../core/global_registries";
import { globalConfig } from "../../core/config";
import { createLogger } from "../../core/logging";
const logger = createLogger("game_time");
export class GameTime extends BasicSerializableObject {
/**
* @param {GameRoot} root
*/
constructor(root) {
super();
this.root = root;
// Current ingame time seconds, not incremented while paused
this.timeSeconds = 0;
// Current "realtime", a timer which always is incremented no matter whether the game is paused or no
this.realtimeSeconds = 0;
// The adjustment, used when loading savegames so we can continue where we were
this.realtimeAdjust = 0;
/** @type {BaseGameSpeed} */
this.speed = new RegularGameSpeed(this.root);
// Store how much time we have in bucket
this.logicTimeBudget = 0;
}
static getId() {
return "GameTime";
}
static getSchema() {
return {
timeSeconds: types.float,
speed: types.obj(gGameSpeedRegistry),
realtimeSeconds: types.float,
};
}
/**
* Fetches the new "real" time, called from the core once per frame, since performance now() is kinda slow
*/
updateRealtimeNow() {
this.realtimeSeconds = performance.now() / 1000.0 + this.realtimeAdjust;
}
/**
* Returns the ingame time in milliseconds
*/
getTimeMs() {
return this.timeSeconds * 1000.0;
}
/**
* Returns how many seconds we are in the grace period
* @returns {number}
*/
getRemainingGracePeriodSeconds() {
return 0;
}
/**
* Returns if we are currently in the grace period
* @returns {boolean}
*/
getIsWithinGracePeriod() {
return this.getRemainingGracePeriodSeconds() > 0;
}
/**
* Internal method to generate new logic time budget
* @param {number} deltaMs
*/
internalAddDeltaToBudget(deltaMs) {
// Only update if game is supposed to update
if (this.root.hud.shouldPauseGame()) {
this.logicTimeBudget = 0;
} else {
const multiplier = this.getSpeed().getTimeMultiplier();
this.logicTimeBudget += deltaMs * multiplier;
}
// Check for too big pile of updates -> reduce it to 1
let maxLogicSteps = Math.max(
3,
(this.speed.getMaxLogicStepsInQueue() * this.root.dynamicTickrate.currentTickRate) / 60
);
if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) {
maxLogicSteps *= 1 + globalConfig.debug.framePausesBetweenTicks;
}
if (this.logicTimeBudget > this.root.dynamicTickrate.deltaMs * maxLogicSteps) {
this.logicTimeBudget = this.root.dynamicTickrate.deltaMs * maxLogicSteps;
}
}
/**
* Performs update ticks based on the queued logic budget
* @param {number} deltaMs
* @param {function():boolean} updateMethod
*/
performTicks(deltaMs, updateMethod) {
this.internalAddDeltaToBudget(deltaMs);
const speedAtStart = this.root.time.getSpeed();
let effectiveDelta = this.root.dynamicTickrate.deltaMs;
if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) {
effectiveDelta += globalConfig.debug.framePausesBetweenTicks * this.root.dynamicTickrate.deltaMs;
}
// Update physics & logic
while (this.logicTimeBudget >= effectiveDelta) {
this.logicTimeBudget -= effectiveDelta;
if (!updateMethod()) {
// Gameover happened or so, do not update anymore
return;
}
// Step game time
this.timeSeconds += this.root.dynamicTickrate.deltaSeconds;
// Game time speed changed, need to abort since our logic steps are no longer valid
if (speedAtStart.getId() !== this.speed.getId()) {
logger.warn(
"Skipping update because speed changed from",
speedAtStart.getId(),
"to",
this.speed.getId()
);
break;
}
}
}
/**
* Returns ingame time in seconds
* @returns {number} seconds
*/
now() {
return this.timeSeconds;
}
/**
* Returns "real" time in seconds
* @returns {number} seconds
*/
realtimeNow() {
return this.realtimeSeconds;
}
/**
* Returns "real" time in seconds
* @returns {number} seconds
*/
systemNow() {
return (this.realtimeSeconds - this.realtimeAdjust) * 1000.0;
}
getIsPaused() {
return this.speed.getId() === PausedGameSpeed.getId();
}
getSpeed() {
return this.speed;
}
setSpeed(speed) {
assert(speed instanceof BaseGameSpeed, "Not a valid game speed");
if (this.speed.getId() === speed.getId()) {
logger.warn("Same speed set than current one:", speed.constructor.getId());
}
this.speed = speed;
}
deserialize(data) {
const errorCode = super.deserialize(data);
if (errorCode) {
return errorCode;
}
// Adjust realtime now difference so they match
this.realtimeAdjust = this.realtimeSeconds - performance.now() / 1000.0;
this.updateRealtimeNow();
this.speed.initializeAfterDeserialize(this.root);
}
}