1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-17 12:11:51 +00:00
tobspr_shapez.io/src/js/game/systems/item_ejector.js

325 lines
13 KiB
JavaScript
Raw Normal View History

import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle";
2020-09-18 16:32:53 +00:00
import { StaleAreaDetector } from "../../core/stale_area_detector";
2020-08-29 08:45:38 +00:00
import { enumDirection, enumDirectionToVector } from "../../core/vector";
Achievements (#1087) * [WIP] Add boilerplate for achievement implementation * Add config.local.template.js and rm cached copy of config.local.js * [WIP] Implement painting, cutting, rotating achievements (to log only) * [WIP] Refactor achievements, jsdoc fixes, add npm script - Refactor achievements to make use of Signals - Move implemented achievement interfaces to appropriate platform folders (SteamAchievements in currently in use in browser wrapper for testing) - Fix invalid jsdocs - Add dev-standalone script to package.json scripts * Add steam/greenworks IPC calls and optional private-artifact dependency * Include private artifacts in standalone builds * Uncomment appid include * [WIP] Add steam overlay fix, add hash to artifact dependency * Update electron, greenworks. Add task to add local config if not present * Add more achievements, refactor achievement code * Add receiver flexibility and more achievements - Add check to see if necessary to create achievement and add receiver - Add remove receiver functionality when achievement is unlocked * Add achievements and accommodations for switching states - Fix startup code to avoid clobbering achievements on state switch - Add a few more achievements * Add achievements, ids. Update names, keys for consistency * Add play time achievements * [WIP] Add more achievements * Add more achievements. Add bulk achievement check signal * [WIP] Add achievements. Start savefile migration * Add achievements. Add savefile migration * Remove superfluous achievement stat * Update lock files, fix merge conflict
2021-03-10 06:33:39 +00:00
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { BaseItem } from "../base_item";
2020-09-18 16:32:53 +00:00
import { BeltComponent } from "../components/belt";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
const logger = createLogger("systems/ejector");
export class ItemEjectorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemEjectorComponent]);
2020-09-18 16:32:53 +00:00
this.staleAreaDetector = new StaleAreaDetector({
root: this.root,
name: "item-ejector",
recomputeMethod: this.recomputeArea.bind(this),
});
2020-09-18 16:32:53 +00:00
this.staleAreaDetector.recomputeOnComponentsChanged(
[ItemEjectorComponent, ItemAcceptorComponent, BeltComponent],
1
);
2020-09-18 16:32:53 +00:00
this.root.signals.postLoadHook.add(this.recomputeCacheFull, this);
}
/**
2020-09-18 16:32:53 +00:00
* Recomputes an area after it changed
* @param {Rectangle} area
*/
2020-09-18 16:32:53 +00:00
recomputeArea(area) {
/** @type {Set<number>} */
const seenUids = new Set();
for (let x = 0; x < area.w; ++x) {
for (let y = 0; y < area.h; ++y) {
const tileX = area.x + x;
const tileY = area.y + y;
// @NOTICE: Item ejector currently only supports regular layer
const contents = this.root.map.getLayerContentXY(tileX, tileY, "regular");
if (contents && contents.components.ItemEjector) {
if (!seenUids.has(contents.uid)) {
seenUids.add(contents.uid);
this.recomputeSingleEntityCache(contents);
}
}
}
}
}
/**
2020-09-18 16:32:53 +00:00
* Recomputes the whole cache after the game has loaded
*/
2020-09-18 16:32:53 +00:00
recomputeCacheFull() {
logger.log("Full cache recompute in post load hook");
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
this.recomputeSingleEntityCache(entity);
}
}
/**
* @param {Entity} entity
*/
recomputeSingleEntityCache(entity) {
const ejectorComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity;
for (let slotIndex = 0; slotIndex < ejectorComp.slots.length; ++slotIndex) {
const ejectorSlot = ejectorComp.slots[slotIndex];
// Clear the old cache.
ejectorSlot.cachedDestSlot = null;
ejectorSlot.cachedTargetEntity = null;
ejectorSlot.cachedBeltPath = null;
// Figure out where and into which direction we eject items
const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos);
const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction);
const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection];
const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector);
// Try to find the given acceptor component to take the item
// Since there can be cross layer dependencies, check on all layers
const targetEntities = this.root.map.getLayersContentsMultipleXY(
ejectSlotTargetWsTile.x,
ejectSlotTargetWsTile.y
);
for (let i = 0; i < targetEntities.length; ++i) {
const targetEntity = targetEntities[i];
const targetStaticComp = targetEntity.components.StaticMapEntity;
const targetBeltComp = targetEntity.components.Belt;
// Check for belts (special case)
if (targetBeltComp) {
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
if (ejectSlotWsDirection === beltAcceptingDirection) {
ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedBeltPath = targetBeltComp.assignedPath;
break;
}
}
// Check for item acceptors
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
if (!targetAcceptorComp) {
// Entity doesn't accept items
continue;
}
const matchingSlot = targetAcceptorComp.findMatchingSlot(
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection)
);
if (!matchingSlot) {
// No matching slot found
continue;
}
// A slot can always be connected to one other slot only
ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedDestSlot = matchingSlot;
break;
}
}
}
update() {
2020-09-18 16:32:53 +00:00
this.staleAreaDetector.update();
2022-01-18 17:39:02 +00:00
// * 2 because its only a half tile - (same code as acceptor)
const progressGrowth =
2 *
this.root.dynamicTickrate.deltaSeconds *
this.root.hubGoals.getBeltBaseSpeed() *
globalConfig.itemSpacingOnBelts;
// Go over all cache entries
for (let i = 0; i < this.allEntities.length; ++i) {
2022-01-18 17:39:02 +00:00
const entity = this.allEntities[i];
const ejectorComp = entity.components.ItemEjector;
const slots = ejectorComp.slots;
for (let j = 0; j < slots.length; ++j) {
2022-01-18 17:39:02 +00:00
const slot = slots[j];
const item = slot.item;
if (!item) {
2022-01-18 17:39:02 +00:00
// No output in progress
continue;
}
if (slot.progress < 1) {
// Advance items on the slot
slot.progress += progressGrowth;
if (G_IS_DEV && globalConfig.debug.disableEjectorProcessing) {
slot.progress = 1;
}
2020-08-29 09:08:30 +00:00
}
// Check if we are still in the process of ejecting, can't proceed then
if (slot.progress < 1) continue;
const extraProgress = slot.progress - 1;
// Check if we are ejecting to a belt path
2022-01-18 17:39:02 +00:00
const destPath = slot.cachedBeltPath;
if (destPath) {
// Try passing the item over - extraProgress / 2 because the progress there is for double the distance
if (destPath.tryAcceptItem(item, extraProgress / 2)) {
2022-01-18 17:39:02 +00:00
slot.item = null;
}
2022-01-18 17:39:02 +00:00
// Always stop here, since there can *either* be a belt path *or* an acceptor
continue;
}
// Check if the target acceptor can actually accept this item
2022-01-18 17:39:02 +00:00
const destEntity = slot.cachedTargetEntity;
const destSlot = slot.cachedDestSlot;
if (destEntity && destSlot) {
2020-09-18 16:18:38 +00:00
const targetAcceptorComp = destEntity.components.ItemAcceptor;
2022-01-18 17:39:02 +00:00
if (
targetAcceptorComp.tryAcceptItem(
destSlot.index,
destSlot.acceptedDirection,
item,
extraProgress
)
) {
// Handover successful, clear slot
2022-01-18 17:39:02 +00:00
slot.item = null;
}
}
Puzzle DLC (#1172) * Puzzle mode (#1135) * Add mode button to main menu * [WIP] Add mode menu. Add factory-based gameMode creation * Add savefile migration, serialize, deserialize * Add hidden HUD elements, zone, and zoom, boundary constraints * Clean up lint issues * Add building, HUD exclusion, building exclusion, and refactor - [WIP] Add ConstantProducer building that combines ConstantSignal and ItemProducer functionality. Currently using temp assets. - Add pre-placement check to the zone - Use Rectangles for zone and boundary - Simplify zone drawing - Account for exclusion in savegame data - [WIP] Add puzzle play and edit buttons in puzzle mode menu * [WIP] Add building, component, and systems for producing and accepting user-specified items and checking goal criteria * Add ingame puzzle mode UI elements - Add minimal menus in puzzle mode for back, next navigation - Add lower menu for changing zone dimenensions Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com> * Performance optimizations (#1154) * 1.3.1 preparations * Minor fixes, update translations * Fix achievements not working * Lots of belt optimizations, ~15% performance boost * Puzzle mode, part 1 * Puzzle mode, part 2 * Fix missing import * Puzzle mode, part 3 * Fix typo * Puzzle mode, part 4 * Puzzle Mode fixes: Correct zone restrictions and more (#1155) * Hide Puzzle Editor Controls in regular game mode, fix typo * Disallow shrinking zone if there are buildings * Fix multi-tile buildings for shrinking * Puzzle mode, Refactor hud * Puzzle mode * Fixed typo in latest puzzle commit (#1156) * Allow completing puzzles * Puzzle mode, almost done * Bump version to 1.4.0 * Fixes * [puzzle] Prevent pipette cheats (miners, emitters) (#1158) * Puzzle mode, almost done * Allow clearing belts with 'B' * Multiple users for the puzzle dlc * Bump api key * Minor adjustments * Update * Minor fixes * Fix throughput * Fix belts * Minor puzzle adjustments * New difficulty * Minor puzzle improvements * Fix belt path * Update translations * Added a button to return to the menu after a puzzle is completed (#1170) * added another button to return to the menu * improved menu return * fixed continue button to not go back to menu * [Puzzle] Added ability to lock buildings in the puzzle editor! (#1164) * initial test * tried to get it to work * added icon * added test exclusion * reverted css * completed flow for building locking * added lock option * finalized look and changed locked building to same sprite * removed unused art * added clearing every goal acceptor on lock to prevent creating impossible puzzles * heavily improved validation and prevented autocompletion * validation only checks every 100 ticks to improve performance * validation only checks every 100 ticks to improve performance * removed clearing goal acceptors as it isn't needed because of validation * Add soundtrack, puzzle dlc fixes Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com> Co-authored-by: dengr1065 <dengr1065@gmail.com> Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
2021-05-23 14:32:05 +00:00
2022-01-18 17:39:02 +00:00
//@SENSETODO deal with other buildings - acceptor code on them needs to be different!
}
}
}
/**
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
2020-09-19 08:34:46 +00:00
if (this.root.app.settings.getAllSettings().simplifiedBelts) {
// Disabled in potato mode
return;
}
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const ejectorComp = entity.components.ItemEjector;
if (!ejectorComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
for (let i = 0; i < ejectorComp.slots.length; ++i) {
const slot = ejectorComp.slots[i];
const ejectedItem = slot.item;
if (!ejectedItem) {
// No item
continue;
}
2020-09-18 16:18:38 +00:00
if (!ejectorComp.renderFloatingItems && !slot.cachedTargetEntity) {
// Not connected to any building
continue;
}
// Limit the progress to the maximum available space on the next belt (also see #1000)
let progress = Math.min(1, slot.progress);
const nextBeltPath = slot.cachedBeltPath;
if (nextBeltPath) {
/*
If you imagine the track between the center of the building and the center of the first belt as
a range from 0 to 1:
Building Belt
| X | X |
| 0...................1 |
And for example the first item on belt has a distance of 0.4 to the beginning of the belt:
Building Belt
| X | X |
| 0...................1 |
^ item
Then the space towards this first item is always 0.5 (the distance from the center of the building to the beginning of the belt)
PLUS the spacing to the item, so in this case 0.5 + 0.4 = 0.9:
Building Belt
| X | X |
| 0...................1 |
^ item @ 0.9
Since items must not get clashed, we need to substract some spacing (lets assume it is 0.6, exact value see globalConfig.itemSpacingOnBelts),
So we do 0.9 - globalConfig.itemSpacingOnBelts = 0.3
Building Belt
| X | X |
| 0...................1 |
^ ^ item @ 0.9
^ max progress = 0.3
Because now our range actually only goes to the end of the building, and not towards the center of the building, we need to multiply
all values by 2:
Building Belt
| X | X |
| 0.........1.........2 |
^ ^ item @ 1.8
^ max progress = 0.6
And that's it! If you summarize the calculations from above into a formula, you get the one below.
*/
const maxProgress =
(0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts) * 2;
progress = Math.min(maxProgress, progress);
}
const realPosition = staticComp.localTileToWorld(slot.pos);
if (!chunk.tileSpaceRectangle.containsPoint(realPosition.x, realPosition.y)) {
// Not within this chunk
continue;
}
const realDirection = staticComp.localDirectionToWorld(slot.direction);
const realDirectionVector = enumDirectionToVector[realDirection];
const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * progress;
const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * progress;
const worldX = tileX * globalConfig.tileSize;
const worldY = tileY * globalConfig.tileSize;
ejectedItem.drawItemCenteredClipped(
worldX,
worldY,
parameters,
globalConfig.defaultItemDiameter
);
}
}
}
}