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

Add storage building

This commit is contained in:
tobspr
2020-05-20 15:51:06 +02:00
parent faf16e6ba4
commit 1577ebe48c
37 changed files with 857 additions and 376 deletions

View File

@@ -130,10 +130,12 @@
&[data-tile-w="4"] {
@include S(width, 4 * $iconSize);
}
&[data-tile-h="2"] {
@include S(height, 2 * $iconSize);
}
&[data-tile-h="3"] {
@include S(height, 3 * $iconSize);
}
}
.label {

View File

@@ -1,5 +1,8 @@
import { Loader } from "../../core/loader";
import { formatItemsPerSecond } from "../../core/utils";
import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector";
import { SOUNDS } from "../../platform/sound";
import { T } from "../../translations";
import { BeltComponent } from "../components/belt";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
@@ -7,10 +10,6 @@ import { ReplaceableMapEntityComponent } from "../components/replaceable_map_ent
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { SOUNDS } from "../../platform/sound";
import { T } from "../../translations";
import { round1Digit, formatItemsPerSecond } from "../../core/utils";
import { globalConfig } from "../../core/config";
export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right];

View File

@@ -3,23 +3,64 @@ import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
import { enumHubGoalRewards } from "../tutorial_goals";
import { GameRoot } from "../root";
import { StorageComponent } from "../components/storage";
import { T } from "../../translations";
import { formatBigNumber } from "../../core/utils";
/** @enum {string} */
export const enumTrashVariants = { storage: "storage" };
const trashSize = 5000;
export class MetaTrashBuilding extends MetaBuilding {
constructor() {
super("trash");
}
isRotateable() {
return false;
isRotateable(variant) {
return variant !== defaultBuildingVariant;
}
getSilhouetteColor() {
return "#cd7d86";
}
/**
* @param {GameRoot} root
* @param {string} variant
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (variant === enumTrashVariants.storage) {
return [[T.ingame.buildingPlacement.infoTexts.storage, formatBigNumber(trashSize)]];
}
return [];
}
getDimensions(variant) {
switch (variant) {
case defaultBuildingVariant:
return new Vector(1, 1);
case enumTrashVariants.storage:
return new Vector(2, 2);
default:
assertAlways(false, "Unknown trash variant: " + variant);
}
}
/**
* @param {GameRoot} root
*/
getAvailableVariants(root) {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_storage)) {
return [defaultBuildingVariant, enumTrashVariants.storage];
}
return super.getAvailableVariants(root);
}
/**
* @param {GameRoot} root
*/
@@ -32,13 +73,6 @@ export class MetaTrashBuilding extends MetaBuilding {
* @param {Entity} entity
*/
setupEntityComponents(entity) {
entity.addComponent(
new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.trash,
})
);
// Required, since the item processor needs this.
entity.addComponent(
new ItemEjectorComponent({
@@ -62,4 +96,77 @@ export class MetaTrashBuilding extends MetaBuilding {
})
);
}
/**
*
* @param {Entity} entity
* @param {number} rotationVariant
* @param {string} variant
*/
updateVariants(entity, rotationVariant, variant) {
switch (variant) {
case defaultBuildingVariant: {
if (!entity.components.ItemProcessor) {
entity.addComponent(
new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.trash,
})
);
}
if (entity.components.Storage) {
entity.removeComponent(StorageComponent);
}
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [
enumDirection.top,
enumDirection.right,
enumDirection.bottom,
enumDirection.left,
],
},
]);
entity.components.ItemEjector.setSlots([]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.trash;
break;
}
case enumTrashVariants.storage: {
if (entity.components.ItemProcessor) {
entity.removeComponent(ItemProcessorComponent);
}
if (!entity.components.Storage) {
entity.addComponent(new StorageComponent({}));
}
entity.components.Storage.maximumStorage = trashSize;
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 1),
directions: [enumDirection.bottom],
},
{
pos: new Vector(1, 1),
directions: [enumDirection.bottom],
},
]);
entity.components.ItemEjector.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(1, 0),
direction: enumDirection.top,
},
]);
break;
}
default:
assertAlways(false, "Unknown trash variant: " + variant);
}
}
}

View File

@@ -9,6 +9,7 @@ import { ReplaceableMapEntityComponent } from "./components/replaceable_map_enti
import { UndergroundBeltComponent } from "./components/underground_belt";
import { UnremovableComponent } from "./components/unremovable";
import { HubComponent } from "./components/hub";
import { StorageComponent } from "./components/storage";
export function initComponentRegistry() {
gComponentRegistry.register(StaticMapEntityComponent);
@@ -21,9 +22,9 @@ export function initComponentRegistry() {
gComponentRegistry.register(UndergroundBeltComponent);
gComponentRegistry.register(UnremovableComponent);
gComponentRegistry.register(HubComponent);
gComponentRegistry.register(StorageComponent);
// IMPORTANT ^^^^^ REGENERATE SAVEGAME SCHEMA AFTERWARDS
// IMPORTANT ^^^^^ ALSO UPDATE ENTITY COMPONENT STORAG
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
// Sanity check - If this is thrown, you (=me, lol) forgot to add a new component here

View File

@@ -60,7 +60,7 @@ export class BeltComponent extends Component {
/**
* Returns if the belt can currently accept an item from the given direction
*/
canAcceptNewItem(leftoverProgress = 0.0) {
canAcceptItem(leftoverProgress = 0.0) {
const firstItem = this.sortedItems[0];
if (!firstItem) {
return true;
@@ -73,7 +73,7 @@ export class BeltComponent extends Component {
* Pushes a new item to the belt
* @param {BaseItem} item
*/
takeNewItem(item, leftoverProgress = 0.0) {
takeItem(item, leftoverProgress = 0.0) {
if (G_IS_DEV) {
assert(
this.sortedItems.length === 0 ||

View File

@@ -0,0 +1,80 @@
import { Component } from "../component";
import { types } from "../../savegame/serialization";
import { gItemRegistry } from "../../core/global_registries";
import { BaseItem } from "../base_item";
import { ColorItem } from "../items/color_item";
import { ShapeItem } from "../items/shape_item";
export class StorageComponent extends Component {
static getId() {
return "Storage";
}
static getSchema() {
return {
maximumStorage: types.uint,
storedCount: types.uint,
storedItem: types.nullable(types.obj(gItemRegistry)),
overlayOpacity: types.ufloat,
};
}
/**
* @param {object} param0
* @param {number=} param0.maximumStorage How much this storage can hold
*/
constructor({ maximumStorage = 1e20 }) {
super();
this.maximumStorage = maximumStorage;
/**
* Currently stored item
* @type {BaseItem}
*/
this.storedItem = null;
/**
* How many of this item we have stored
*/
this.storedCount = 0;
/**
* We compute an opacity to make sure it doesn't flicker
*/
this.overlayOpacity = 0;
}
/**
* Returns whether this storage can accept the item
* @param {BaseItem} item
*/
canAcceptItem(item) {
if (this.storedCount >= this.maximumStorage) {
return false;
}
if (!this.storedItem || this.storedCount === 0) {
return true;
}
if (item instanceof ColorItem) {
return this.storedItem instanceof ColorItem && this.storedItem.color === item.color;
}
if (item instanceof ShapeItem) {
return (
this.storedItem instanceof ShapeItem &&
this.storedItem.definition.getHash() === item.definition.getHash()
);
}
return false;
}
/**
* @param {BaseItem} item
*/
takeItem(item) {
this.storedItem = item;
this.storedCount++;
}
}

View File

@@ -404,6 +404,7 @@ export class GameCore {
root.map.drawForeground(params);
if (!this.root.camera.getIsMapOverlayActive()) {
systems.hub.draw(params);
systems.storage.draw(params);
}
if (G_IS_DEV) {

View File

@@ -99,6 +99,10 @@ export class Entity extends BasicSerializableObject {
* @param {boolean} force Used by the entity manager. Internal parameter, do not change
*/
addComponent(componentInstance, force = false) {
if (!force && this.registered) {
this.root.entityMgr.attachDynamicComponent(this, componentInstance);
return;
}
assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent");
const id = /** @type {typeof Component} */ (componentInstance.constructor).getId();
assert(!this.components[id], "Component already present");
@@ -109,9 +113,17 @@ export class Entity extends BasicSerializableObject {
* Removes a given component, only possible until the entity is registered on the entity manager,
* after that use @see EntityManager.removeDynamicComponent
* @param {typeof Component} componentClass
* @param {boolean} force
*/
removeComponent(componentClass) {
assert(!this.registered, "Entity already registered, use EntityManager.removeDynamicComponent");
removeComponent(componentClass, force = false) {
if (!force && this.registered) {
this.root.entityMgr.removeDynamicComponent(this, componentClass);
return;
}
assert(
force || !this.registered,
"Entity already registered, use EntityManager.removeDynamicComponent"
);
const id = componentClass.getId();
assert(this.components[id], "Component does not exist on entity");
delete this.components[id];

View File

@@ -9,6 +9,7 @@ import { ReplaceableMapEntityComponent } from "./components/replaceable_map_enti
import { UndergroundBeltComponent } from "./components/underground_belt";
import { UnremovableComponent } from "./components/unremovable";
import { HubComponent } from "./components/hub";
import { StorageComponent } from "./components/storage";
/* typehints:end */
/**
@@ -52,6 +53,9 @@ export class EntityComponentStorage {
/** @type {HubComponent} */
this.Hub;
/** @type {StorageComponent} */
this.Storage;
/* typehints:end */
}
}

View File

@@ -1,4 +1,4 @@
import { arrayDeleteValue, newEmptyMap } from "../core/utils";
import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils";
import { Component } from "./component";
import { GameRoot } from "./root";
import { Entity } from "./entity";
@@ -128,6 +128,19 @@ export class EntityManager extends BasicSerializableObject {
this.root.signals.entityGotNewComponent.dispatch(entity);
}
/**
* Call to remove a component after the creation of the entity
* @param {Entity} entity
* @param {typeof Component} component
*/
removeDynamicComponent(entity, component) {
entity.removeComponent(component, true);
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
fastArrayDeleteValue(this.componentToEntity[componentId], entity);
this.root.signals.entityComponentRemoved.dispatch(entity);
}
/**
* Finds an entity buy its uid, kinda slow since it loops over all entities
* @param {number} uid

View File

@@ -12,6 +12,7 @@ import { UndergroundBeltSystem } from "./systems/underground_belt";
import { HubSystem } from "./systems/hub";
import { StaticMapEntitySystem } from "./systems/static_map_entity";
import { ItemAcceptorSystem } from "./systems/item_acceptor";
import { StorageSystem } from "./systems/storage";
const logger = createLogger("game_system_manager");
@@ -52,6 +53,9 @@ export class GameSystemManager {
/** @type {ItemAcceptorSystem} */
itemAcceptor: null,
/** @type {StorageSystem} */
storage: null,
/* typehints:end */
};
this.systemUpdateOrder = [];
@@ -72,15 +76,17 @@ export class GameSystemManager {
add("belt", BeltSystem);
add("itemEjector", ItemEjectorSystem);
add("undergroundBelt", UndergroundBeltSystem);
add("miner", MinerSystem);
add("mapResources", MapResourcesSystem);
add("storage", StorageSystem);
add("itemProcessor", ItemProcessorSystem);
add("undergroundBelt", UndergroundBeltSystem);
add("itemEjector", ItemEjectorSystem);
add("mapResources", MapResourcesSystem);
add("hub", HubSystem);

View File

@@ -5,7 +5,7 @@ import { Entity } from "./entity";
/* typehints:end */
import { GameSystem } from "./game_system";
import { arrayDelete } from "../core/utils";
import { arrayDelete, arrayDeleteValue } from "../core/utils";
import { DrawParameters } from "../core/draw_parameters";
import { globalConfig } from "../core/config";
import { Math_floor, Math_ceil } from "../core/builtins";
@@ -30,6 +30,7 @@ export class GameSystemWithFilter extends GameSystem {
this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this);
this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this);
this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this);
this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this);
this.root.signals.postLoadHook.add(this.internalPostLoadHook, this);
@@ -122,6 +123,24 @@ export class GameSystemWithFilter extends GameSystem {
this.internalRegisterEntity(entity);
}
/**
*
* @param {Entity} entity
*/
internalCheckEntityAfterComponentRemoval(entity) {
if (this.allEntities.indexOf(entity) < 0) {
// Entity wasn't interesting anyways
return;
}
for (let i = 0; i < this.requiredComponentIds.length; ++i) {
if (!entity.components[this.requiredComponentIds[i]]) {
// Entity is not interesting anymore
arrayDeleteValue(this.allEntities, entity);
}
}
}
/**
*
* @param {Entity} entity

View File

@@ -107,9 +107,10 @@ export class MetaBuilding {
/**
* Returns whether this building is rotateable
* @param {string} variant
* @returns {boolean}
*/
isRotateable() {
isRotateable(variant) {
return true;
}
@@ -185,7 +186,7 @@ export class MetaBuilding {
* @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array<Entity> }}
*/
computeOptimalDirectionAndRotationVariantAtTile(root, tile, rotation, variant) {
if (!this.isRotateable()) {
if (!this.isRotateable(variant)) {
return {
rotation: 0,
rotationVariant: 0,

View File

@@ -123,6 +123,7 @@ export class GameRoot {
// Entities
entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()),

View File

@@ -258,8 +258,8 @@ export class BeltSystem extends GameSystemWithFilter {
if (progressAndItem[0] >= 1.0) {
if (followUp) {
const followUpBelt = followUp.components.Belt;
if (followUpBelt.canAcceptNewItem()) {
followUpBelt.takeNewItem(progressAndItem[1], progressAndItem[0] - 1.0);
if (followUpBelt.canAcceptItem()) {
followUpBelt.takeItem(progressAndItem[1], progressAndItem[0] - 1.0);
items.splice(itemIndex, 1);
} else {
// Well, we couldn't really take it to a follow up belt, keep it at

View File

@@ -99,8 +99,17 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
const beltComp = receiver.components.Belt;
if (beltComp) {
// Ayy, its a belt!
if (beltComp.canAcceptNewItem()) {
beltComp.takeNewItem(item);
if (beltComp.canAcceptItem()) {
beltComp.takeItem(item);
return true;
}
}
const storageComp = receiver.components.Storage;
if (storageComp) {
// It's a storage
if (storageComp.canAcceptItem(item)) {
storageComp.takeItem(item);
return true;
}
}

View File

@@ -28,12 +28,19 @@ export class StaticMapEntitySystem extends GameSystem {
const drawOutlinesOnly = parameters.zoomLevel < globalConfig.mapChunkOverviewMinZoom;
const drawnUids = new Set();
const contents = chunk.contents;
for (let y = 0; y < globalConfig.mapChunkSize; ++y) {
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
const entity = contents[x][y];
if (entity) {
if (drawnUids.has(entity.uid)) {
continue;
}
drawnUids.add(entity.uid);
const staticComp = entity.components.StaticMapEntity;
if (drawOutlinesOnly) {
const rect = staticComp.getTileSpaceBounds();

View File

@@ -0,0 +1,75 @@
import { GameSystemWithFilter } from "../game_system_with_filter";
import { StorageComponent } from "../components/storage";
import { Entity } from "../entity";
import { DrawParameters } from "../../core/draw_parameters";
import { formatBigNumber, lerp } from "../../core/utils";
import { Loader } from "../../core/loader";
export class StorageSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [StorageComponent]);
this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png");
}
update() {
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const storageComp = entity.components.Storage;
// Eject from storage
if (storageComp.storedItem && storageComp.storedCount > 0) {
const ejectorComp = entity.components.ItemEjector;
const nextSlot = ejectorComp.getFirstFreeSlot();
if (nextSlot !== null) {
if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) {
storageComp.storedCount--;
if (storageComp.storedCount === 0) {
storageComp.storedItem = null;
}
}
}
}
let targetAlpha = storageComp.storedCount > 0 ? 1 : 0;
storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05);
}
}
draw(parameters) {
this.forEachMatchingEntityOnScreen(parameters, this.drawEntity.bind(this));
}
/**
* @param {DrawParameters} parameters
* @param {Entity} entity
*/
drawEntity(parameters, entity) {
const context = parameters.context;
const staticComp = entity.components.StaticMapEntity;
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
const storageComp = entity.components.Storage;
const storedItem = storageComp.storedItem;
if (storedItem !== null) {
context.globalAlpha = storageComp.overlayOpacity;
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
storedItem.draw(center.x, center.y, parameters, 30);
this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15);
context.font = "bold 10px GameFont";
context.textAlign = "center";
context.fillStyle = "#64666e";
context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5);
context.textAlign = "left";
context.globalAlpha = 1;
}
}
}

View File

@@ -20,6 +20,7 @@ export const enumHubGoalRewards = {
reward_cutter_quad: "reward_cutter_quad",
reward_painter_double: "reward_painter_double",
reward_painter_quad: "reward_painter_quad",
reward_storage: "reward_storage",
reward_freeplay: "reward_freeplay",
@@ -107,6 +108,12 @@ export const tutorialGoals = [
reward: enumHubGoalRewards.reward_underground_belt_tier_2,
},
{
shape: "SrSrSrSr:CyCyCyCy", // unused
required: 7850,
reward: enumHubGoalRewards.reward_storage,
},
{
shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants)
required: 8000,