diff --git a/res/ui/building_icons/buffer.png b/res/ui/building_icons/buffer.png new file mode 100644 index 00000000..e8304d30 Binary files /dev/null and b/res/ui/building_icons/buffer.png differ diff --git a/res_raw/sprites/blueprints/buffer.png b/res_raw/sprites/blueprints/buffer.png new file mode 100644 index 00000000..715d5315 Binary files /dev/null and b/res_raw/sprites/blueprints/buffer.png differ diff --git a/res_raw/sprites/buildings/buffer.png b/res_raw/sprites/buildings/buffer.png new file mode 100644 index 00000000..715d5315 Binary files /dev/null and b/res_raw/sprites/buildings/buffer.png differ diff --git a/src/js/game/buildings/buffer.js b/src/js/game/buildings/buffer.js index 5992f914..6a894471 100644 --- a/src/js/game/buildings/buffer.js +++ b/src/js/game/buildings/buffer.js @@ -1,6 +1,7 @@ import { enumDirection, Vector } from "../../core/vector"; import { ItemAcceptorComponent, enumItemAcceptorItemFilter } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; +import { BufferComponent } from "../components/item_buffer"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; import { MetaBuilding } from "../meta_building"; @@ -34,9 +35,11 @@ export class MetaBufferBuilding extends MetaBuilding { */ setupEntityComponents(entity) { // TODO: Use custom component here to allow for smooth output + entity.addComponent(new BufferComponent()); entity.addComponent( new ItemProcessorComponent({ inputsPerCharge: 1, + chargeWhenBlocked: true, processorType: enumItemProcessorTypes.buffer, }) ); @@ -47,6 +50,9 @@ export class MetaBufferBuilding extends MetaBuilding { }) ); + // We render the sprite our self + // entity.components.StaticMapEntity.spriteKey = null; + // TODO: Replace item filters with custom filter to only allow one type of item to be collected. entity.addComponent( new ItemAcceptorComponent({ diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index 16ae01d8..2ee4aa6d 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -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 { BufferComponent } from "./components/item_buffer"; export function initComponentRegistry() { gComponentRegistry.register(StaticMapEntityComponent); @@ -21,6 +22,7 @@ export function initComponentRegistry() { gComponentRegistry.register(UndergroundBeltComponent); gComponentRegistry.register(UnremovableComponent); gComponentRegistry.register(HubComponent); + gComponentRegistry.register(BufferComponent); // IMPORTANT ^^^^^ REGENERATE SAVEGAME SCHEMA AFTERWARDS // IMPORTANT ^^^^^ ALSO UPDATE ENTITY COMPONENT STORAG diff --git a/src/js/game/components/item_buffer.js b/src/js/game/components/item_buffer.js new file mode 100644 index 00000000..7a7b6a44 --- /dev/null +++ b/src/js/game/components/item_buffer.js @@ -0,0 +1,42 @@ +import { types } from "../../savegame/serialization"; +import { Component } from "../component"; +import { BaseItem } from "../base_item"; +import { ShapeDefinition } from "../shape_definition"; +import { gItemRegistry } from "../../core/global_registries"; + + +export class BufferComponent extends Component { + static getId() { + return "Buffer"; + } + + static getSchema() { + return { + itemCount: types.uint, + storageLimit: types.uint, + // TODO: Is this the right type for an item definition? + definition: types.nullable(types.knownType(ShapeDefinition)), + }; + } + + /** + */ + constructor() { + super(); + + this.itemCount = 0; + // TODO: Make buffer storage a level up reward + this.storageLimit = 5000; + + this.definition = null; + } + + // TODO: Is this function needed? + /** + * + * @param {BaseItem} item + */ + tryAcceptChainedItem(item) { + return false; + } +} diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index fd1b253d..597762c1 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -31,7 +31,7 @@ export class ItemProcessorComponent extends Component { nextOutputSlot: types.uint, type: types.enum(enumItemProcessorTypes), inputsPerCharge: types.uint, - + chargeWhenBlocked: types.bool, inputSlots: types.array( types.structured({ item: types.obj(gItemRegistry), @@ -54,9 +54,10 @@ export class ItemProcessorComponent extends Component { * @param {object} param0 * @param {enumItemProcessorTypes=} param0.processorType Which type of processor this is * @param {number=} param0.inputsPerCharge How many items this machine needs until it can start working + * @param {boolean=} param0.chargeWhenBlocked If charges should be preformed when the output buffer is full * */ - constructor({ processorType = enumItemProcessorTypes.splitter, inputsPerCharge = 1 }) { + constructor({ processorType = enumItemProcessorTypes.splitter, inputsPerCharge = 1, chargeWhenBlocked = false}) { super(); // Which slot to emit next, this is only a preference and if it can't emit @@ -70,6 +71,9 @@ export class ItemProcessorComponent extends Component { // How many inputs we need for one charge this.inputsPerCharge = inputsPerCharge; + // Should charges be performed when the output buffer still has items (output is blocked) + this.chargeWhenBlocked = chargeWhenBlocked; + /** * Our current inputs * @type {Array<{ item: BaseItem, sourceSlot: number }>} diff --git a/src/js/game/entity_components.js b/src/js/game/entity_components.js index 5b58264b..550f8c3a 100644 --- a/src/js/game/entity_components.js +++ b/src/js/game/entity_components.js @@ -9,6 +9,8 @@ import { ReplaceableMapEntityComponent } from "./components/replaceable_map_enti import { UndergroundBeltComponent } from "./components/underground_belt"; import { UnremovableComponent } from "./components/unremovable"; import { HubComponent } from "./components/hub"; +import { BufferComponent } from "./components/item_buffer"; + /* typehints:end */ /** @@ -52,6 +54,9 @@ export class EntityComponentStorage { /** @type {HubComponent} */ this.Hub; + /** @type {BufferComponent} */ + this.Buffer; + /* typehints:end */ } } diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index 7cde296e..0f33dc26 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -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 { BufferSystem } from "./systems/item_buffer"; const logger = createLogger("game_system_manager"); @@ -52,6 +53,9 @@ export class GameSystemManager { /** @type {ItemAcceptorSystem} */ itemAcceptor: null, + /** @type {BufferSystem} */ + itemBuffer: null, + /* typehints:end */ }; this.systemUpdateOrder = []; @@ -88,6 +92,8 @@ export class GameSystemManager { add("itemAcceptor", ItemAcceptorSystem); + add("itemBuffer", BufferSystem); + logger.log("📦 There are", this.systemUpdateOrder.length, "game systems"); } diff --git a/src/js/game/hud/parts/buildings_toolbar.js b/src/js/game/hud/parts/buildings_toolbar.js index f7a21a1b..79dc635e 100644 --- a/src/js/game/hud/parts/buildings_toolbar.js +++ b/src/js/game/hud/parts/buildings_toolbar.js @@ -12,6 +12,7 @@ import { MetaSplitterBuilding } from "../../buildings/splitter"; import { MetaStackerBuilding } from "../../buildings/stacker"; import { MetaTrashBuilding } from "../../buildings/trash"; import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; +import { MetaBufferBuilding } from "../../buildings/buffer"; import { MetaBuilding } from "../../meta_building"; import { BaseHUDPart } from "../base_hud_part"; import { KEYMAPPINGS } from "../../key_action_mapper"; @@ -27,6 +28,7 @@ const toolbarBuildings = [ MetaMixerBuilding, MetaPainterBuilding, MetaTrashBuilding, + MetaBufferBuilding, ]; export class HUDBuildingsToolbar extends BaseHUDPart { diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 6aff0e88..95faf099 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -44,6 +44,7 @@ export const KEYMAPPINGS = { mixer: { keyCode: key("8") }, painter: { keyCode: key("9") }, trash: { keyCode: key("0") }, + buffer: { keyCode: key("-") }, }, placement: { diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 450a9743..47b982b8 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -10,6 +10,7 @@ import { MetaStackerBuilding } from "./buildings/stacker"; import { MetaTrashBuilding } from "./buildings/trash"; import { MetaUndergroundBeltBuilding } from "./buildings/underground_belt"; import { MetaHubBuilding } from "./buildings/hub"; +import { MetaBufferBuilding } from "./buildings/buffer"; export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaSplitterBuilding); @@ -23,4 +24,5 @@ export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaBeltBaseBuilding); gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding); gMetaBuildingRegistry.register(MetaHubBuilding); + gMetaBuildingRegistry.register(MetaBufferBuilding); } diff --git a/src/js/game/systems/item_buffer.js b/src/js/game/systems/item_buffer.js new file mode 100644 index 00000000..30ef1770 --- /dev/null +++ b/src/js/game/systems/item_buffer.js @@ -0,0 +1,57 @@ +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { BufferComponent } from "../components/item_buffer"; +import { DrawParameters } from "../../core/draw_parameters"; +import { Entity } from "../entity"; +import { formatBigNumber } from "../../core/utils"; +import { Loader } from "../../core/loader"; + +export class BufferSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [BufferComponent]); + + this.bufferSprite = Loader.getSprite("sprites/buildings/buffer.png"); + } + + 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; + const bufferContents = entity.components.Buffer; + + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + + const pos = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + + // Background + staticComp.drawSpriteOnFullEntityBounds(parameters, this.bufferSprite, 2.2); + + bufferContents.definition.draw(pos.x, pos.y - 5, parameters, 20); + + const textOffsetX = 2; + const textOffsetY = -6; + + context.font = "bold 10px GameFont"; + context.fillStyle = "#64666e"; + context.textAlign = "left"; + context.fillText("" + formatBigNumber(bufferContents.itemCount), pos.x + textOffsetX, pos.y + textOffsetY); + + context.font = "10px GameFont"; + context.fillStyle = "#a4a6b0"; + context.fillText( + "/ " + formatBigNumber(bufferContents.storageLimit), + pos.x + textOffsetX, + pos.y + textOffsetY + 13 + ); + + context.textAlign = "left"; + } +} diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index d3f68c04..e3188d25 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -23,7 +23,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // First of all, process the current recipe processorComp.secondsUntilEject = Math_max( 0, - processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds + processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds, ); // Check if we have any finished items we can eject @@ -64,11 +64,11 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } + const inputs = processorComp.inputSlots.length >= processorComp.inputsPerCharge; + const outputs = processorComp.itemsToEject.length > 0; // Check if we have an empty queue and can start a new charge - if (processorComp.itemsToEject.length === 0) { - if (processorComp.inputSlots.length >= processorComp.inputsPerCharge) { - this.startNewCharge(entity); - } + if (inputs && !outputs || processorComp.chargeWhenBlocked && (inputs || !outputs)) { + this.startNewCharge(entity); } } } @@ -94,7 +94,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { processorComp.secondsUntilEject = 1 / baseSpeed; /** @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} */ - const outItems = []; + const outItems = processorComp.itemsToEject; // Whether to track the production towards the analytics let trackProduction = true; @@ -196,7 +196,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const stackedDefinition = this.root.shapeDefinitionMgr.shapeActionStack( lowerItem.definition, - upperItem.definition + upperItem.definition, ); outItems.push({ item: new ShapeItem(stackedDefinition), @@ -244,7 +244,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith( shapeItem.definition, - colorItem.color + colorItem.color, ); outItems.push({ @@ -267,12 +267,12 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const colorizedDefinition1 = this.root.shapeDefinitionMgr.shapeActionPaintWith( shapeItem1.definition, - colorItem.color + colorItem.color, ); const colorizedDefinition2 = this.root.shapeDefinitionMgr.shapeActionPaintWith( shapeItem2.definition, - colorItem.color + colorItem.color, ); outItems.push({ item: new ShapeItem(colorizedDefinition1), @@ -302,7 +302,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith4Colors( shapeItem.definition, - [colorItem2.color, colorItem3.color, colorItem4.color, colorItem1.color] + [colorItem2.color, colorItem3.color, colorItem4.color, colorItem1.color], ); outItems.push({ @@ -328,6 +328,38 @@ export class ItemProcessorSystem extends GameSystemWithFilter { break; } + case enumItemProcessorTypes.buffer: { + let buffer = entity.components.Buffer; + + for (let i = 0; i < items.length; ++i) { + const inputItem = /** @type {ShapeItem} */ (items[i].item); + assert(inputItem instanceof ShapeItem, "Input is not a shape"); + // TODO: Validate item type + + if (buffer.itemCount < buffer.storageLimit) { + buffer.itemCount++; + } + + if (buffer.definition == null) { + buffer.definition = inputItem.definition; + } + } + + // const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCW(inputDefinition); + + if (outItems.length === 0 && buffer.itemCount > 0) { + buffer.itemCount--; + outItems.push({ + item: new ShapeItem(buffer.definition), + }); + + if (buffer.itemCount === 0) { + buffer.definition = null; + } + } + break; + } + default: assertAlways(false, "Unkown item processor type: " + processorComp.type); }