diff --git a/src/js/game/buildings/painter.js b/src/js/game/buildings/painter.js index 49c2ff69..aae3a79d 100644 --- a/src/js/game/buildings/painter.js +++ b/src/js/game/buildings/painter.js @@ -3,11 +3,12 @@ import { enumDirection, Vector } from "../../core/vector"; import { T } from "../../translations"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; -import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; +import { enumItemProcessorTypes, ItemProcessorComponent, enumItemProcessorRequirements } from "../components/item_processor"; import { Entity } from "../entity"; import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; +import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins"; /** @enum {string} */ export const enumPainterVariants = { mirrored: "mirrored", double: "double", quad: "quad" }; @@ -119,6 +120,10 @@ export class MetaPainterBuilding extends MetaBuilding { switch (variant) { case defaultBuildingVariant: case enumPainterVariants.mirrored: { + if (entity.components.WiredPins) { + entity.removeComponent(WiredPinsComponent) + } + entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), @@ -135,13 +140,19 @@ export class MetaPainterBuilding extends MetaBuilding { ]); entity.components.ItemProcessor.type = enumItemProcessorTypes.painter; + entity.components.ItemProcessor.processingRequirement = null; entity.components.ItemProcessor.inputsPerCharge = 2; + entity.components.ItemEjector.setSlots([ { pos: new Vector(1, 0), direction: enumDirection.right }, ]); break; } case enumPainterVariants.double: { + if (entity.components.WiredPins) { + entity.removeComponent(WiredPinsComponent) + } + entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), @@ -161,6 +172,7 @@ export class MetaPainterBuilding extends MetaBuilding { ]); entity.components.ItemProcessor.type = enumItemProcessorTypes.painterDouble; + entity.components.ItemProcessor.processingRequirement = null; entity.components.ItemProcessor.inputsPerCharge = 3; entity.components.ItemEjector.setSlots([ @@ -169,6 +181,33 @@ export class MetaPainterBuilding extends MetaBuilding { break; } case enumPainterVariants.quad: { + if (!entity.components.WiredPins) { + entity.addComponent(new WiredPinsComponent({ + slots: [ + { + pos: new Vector(0, 0), + direction: enumDirection.bottom, + type: enumPinSlotType.logicalAcceptor + }, + { + pos: new Vector(1, 0), + direction: enumDirection.bottom, + type: enumPinSlotType.logicalAcceptor + }, + { + pos: new Vector(2, 0), + direction: enumDirection.bottom, + type: enumPinSlotType.logicalAcceptor + }, + { + pos: new Vector(3, 0), + direction: enumDirection.bottom, + type: enumPinSlotType.logicalAcceptor + }, + ] + })); + } + entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), @@ -198,6 +237,7 @@ export class MetaPainterBuilding extends MetaBuilding { ]); entity.components.ItemProcessor.type = enumItemProcessorTypes.painterQuad; + entity.components.ItemProcessor.processingRequirement = enumItemProcessorRequirements.painterQuad; entity.components.ItemProcessor.inputsPerCharge = 5; entity.components.ItemEjector.setSlots([ diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index 84e6307a..65cee157 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -43,7 +43,7 @@ export function initComponentRegistry() { assert( // @ts-ignore require.context("./components", false, /.*\.js/i).keys().length === - gComponentRegistry.getNumEntries(), + gComponentRegistry.getNumEntries(), "Not all components are registered" ); diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index 02b742d4..cfc45ddb 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -22,6 +22,11 @@ export const enumItemProcessorTypes = { filter: "filter", }; +/** @enum {string} */ +export const enumItemProcessorRequirements = { + painterQuad: "painterQuad" +}; + export class ItemProcessorComponent extends Component { static getId() { return "ItemProcessor"; @@ -50,6 +55,7 @@ export class ItemProcessorComponent extends Component { duplicateWithoutContents() { return new ItemProcessorComponent({ processorType: this.type, + processingRequirement: this.processingRequirement, inputsPerCharge: this.inputsPerCharge, }); } @@ -58,10 +64,15 @@ export class ItemProcessorComponent extends Component { * * @param {object} param0 * @param {enumItemProcessorTypes=} param0.processorType Which type of processor this is + * @param {enumItemProcessorRequirements=} param0.processingRequirement Applied processing requirement * @param {number=} param0.inputsPerCharge How many items this machine needs until it can start working * */ - constructor({ processorType = enumItemProcessorTypes.splitter, inputsPerCharge = 1 }) { + constructor({ + processorType = enumItemProcessorTypes.splitter, + processingRequirement = null, + inputsPerCharge = 1 + }) { super(); // Which slot to emit next, this is only a preference and if it can't emit @@ -72,6 +83,9 @@ export class ItemProcessorComponent extends Component { // Type of the processor this.type = processorType; + // Type of processing requirement + this.processingRequirement = processingRequirement; + // How many inputs we need for one charge this.inputsPerCharge = inputsPerCharge; diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index 6ffb14a3..65b72a1a 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -1,610 +1,610 @@ -import { makeOffscreenBuffer } from "../core/buffer_utils"; -import { globalConfig } from "../core/config"; -import { smoothenDpi } from "../core/dpi_manager"; -import { DrawParameters } from "../core/draw_parameters"; -import { Vector } from "../core/vector"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; -import { THEME } from "./theme"; - -/** - * @typedef {{ - * subShape: enumSubShape, - * color: enumColors, - * }} ShapeLayerItem - */ - -/** - * Order is Q1 (tr), Q2(br), Q3(bl), Q4(tl) - * @typedef {[ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?]} ShapeLayer - */ - -const arrayQuadrantIndexToOffset = [ - new Vector(1, -1), // tr - new Vector(1, 1), // br - new Vector(-1, 1), // bl - new Vector(-1, -1), // tl -]; - -/** @enum {string} */ -export const enumSubShape = { - rect: "rect", - circle: "circle", - star: "star", - windmill: "windmill", -}; - -/** @enum {string} */ -export const enumSubShapeToShortcode = { - [enumSubShape.rect]: "R", - [enumSubShape.circle]: "C", - [enumSubShape.star]: "S", - [enumSubShape.windmill]: "W", -}; - -/** @enum {enumSubShape} */ -export const enumShortcodeToSubShape = {}; -for (const key in enumSubShapeToShortcode) { - enumShortcodeToSubShape[enumSubShapeToShortcode[key]] = key; -} - -/** - * Converts the given parameters to a valid shape definition - * @param {*} layers - * @returns {Array} - */ -export function createSimpleShape(layers) { - layers.forEach(layer => { - layer.forEach(item => { - if (item) { - item.color = item.color || enumColors.uncolored; - } - }); - }); - return layers; -} - -/** - * Cache which shapes are valid short keys and which not - * @type {Map} - */ -const SHORT_KEY_CACHE = new Map(); - -export class ShapeDefinition extends BasicSerializableObject { - static getId() { - return "ShapeDefinition"; - } - - static getSchema() { - return {}; - } - - deserialize(data) { - const errorCode = super.deserialize(data); - if (errorCode) { - return errorCode; - } - const definition = ShapeDefinition.fromShortKey(data); - this.layers = /** @type {Array} */ (definition.layers); - } - - serialize() { - return this.getHash(); - } - - /** - * - * @param {object} param0 - * @param {Array=} param0.layers - */ - constructor({ layers = [] }) { - super(); - - /** - * The layers from bottom to top - * @type {Array} - */ - this.layers = layers; - - /** @type {string} */ - this.cachedHash = null; - - // Set on demand - this.bufferGenerator = null; - } - - /** - * Generates the definition from the given short key - * @param {string} key - * @returns {ShapeDefinition} - */ - static fromShortKey(key) { - const sourceLayers = key.split(":"); - let layers = []; - for (let i = 0; i < sourceLayers.length; ++i) { - const text = sourceLayers[i]; - assert(text.length === 8, "Invalid shape short key: " + key); - - /** @type {ShapeLayer} */ - const quads = [null, null, null, null]; - for (let quad = 0; quad < 4; ++quad) { - const shapeText = text[quad * 2 + 0]; - const subShape = enumShortcodeToSubShape[shapeText]; - const color = enumShortcodeToColor[text[quad * 2 + 1]]; - if (subShape) { - assert(color, "Invalid shape short key:", key); - quads[quad] = { - subShape, - color, - }; - } else if (shapeText !== "-") { - assert(false, "Invalid shape key: " + shapeText); - } - } - layers.push(quads); - } - - const definition = new ShapeDefinition({ layers }); - // We know the hash so save some work - definition.cachedHash = key; - return definition; - } - - /** - * Checks if a given string is a valid short key - * @param {string} key - * @returns {boolean} - */ - static isValidShortKey(key) { - if (SHORT_KEY_CACHE.has(key)) { - return SHORT_KEY_CACHE.get(key); - } - - const result = ShapeDefinition.isValidShortKeyInternal(key); - SHORT_KEY_CACHE.set(key, result); - return result; - } - - /** - * INTERNAL - * Checks if a given string is a valid short key - * @param {string} key - * @returns {boolean} - */ - static isValidShortKeyInternal(key) { - const sourceLayers = key.split(":"); - let layers = []; - for (let i = 0; i < sourceLayers.length; ++i) { - const text = sourceLayers[i]; - if (text.length !== 8) { - return false; - } - - /** @type {ShapeLayer} */ - const quads = [null, null, null, null]; - let anyFilled = false; - for (let quad = 0; quad < 4; ++quad) { - const shapeText = text[quad * 2 + 0]; - const colorText = text[quad * 2 + 1]; - const subShape = enumShortcodeToSubShape[shapeText]; - const color = enumShortcodeToColor[colorText]; - - // Valid shape - if (subShape) { - if (!color) { - // Invalid color - return false; - } - quads[quad] = { - subShape, - color, - }; - anyFilled = true; - } else if (shapeText === "-") { - // Make sure color is empty then, too - if (colorText !== "-") { - return false; - } - } else { - // Invalid shape key - return false; - } - } - - if (!anyFilled) { - // Empty layer - return false; - } - layers.push(quads); - } - - if (layers.length === 0 || layers.length > 4) { - return false; - } - - return true; - } - - /** - * Internal method to clone the shape definition - * @returns {Array} - */ - internalCloneLayers() { - return JSON.parse(JSON.stringify(this.layers)); - } - - /** - * Returns if the definition is entirely empty^ - * @returns {boolean} - */ - isEntirelyEmpty() { - return this.layers.length === 0; - } - - /** - * Returns a unique id for this shape - * @returns {string} - */ - getHash() { - if (this.cachedHash) { - return this.cachedHash; - } - - let id = ""; - for (let layerIndex = 0; layerIndex < this.layers.length; ++layerIndex) { - const layer = this.layers[layerIndex]; - - for (let quadrant = 0; quadrant < layer.length; ++quadrant) { - const item = layer[quadrant]; - if (item) { - id += enumSubShapeToShortcode[item.subShape] + enumColorToShortcode[item.color]; - } else { - id += "--"; - } - } - - if (layerIndex < this.layers.length - 1) { - id += ":"; - } - } - this.cachedHash = id; - return id; - } - - /** - * Draws the shape definition - * @param {number} x - * @param {number} y - * @param {DrawParameters} parameters - * @param {number=} diameter - */ - drawCentered(x, y, parameters, diameter = 20) { - const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); - - if (!this.bufferGenerator) { - this.bufferGenerator = this.internalGenerateShapeBuffer.bind(this); - } - - const key = diameter + "/" + dpi + "/" + this.cachedHash; - const canvas = parameters.root.buffers.getForKey({ - key: "shapedef", - subKey: key, - w: diameter, - h: diameter, - dpi, - redrawMethod: this.bufferGenerator, - }); - parameters.context.drawImage(canvas, x - diameter / 2, y - diameter / 2, diameter, diameter); - } - - /** - * Generates this shape as a canvas - * @param {number} size - */ - generateAsCanvas(size = 120) { - const [canvas, context] = makeOffscreenBuffer(size, size, { - smooth: true, - label: "definition-canvas-cache-" + this.getHash(), - reusable: false, - }); - - this.internalGenerateShapeBuffer(canvas, context, size, size, 1); - return canvas; - } - - /** - * - * @param {HTMLCanvasElement} canvas - * @param {CanvasRenderingContext2D} context - * @param {number} w - * @param {number} h - * @param {number} dpi - */ - internalGenerateShapeBuffer(canvas, context, w, h, dpi) { - context.translate((w * dpi) / 2, (h * dpi) / 2); - context.scale((dpi * w) / 23, (dpi * h) / 23); - - context.fillStyle = "#e9ecf7"; - - const quadrantSize = 10; - const quadrantHalfSize = quadrantSize / 2; - - context.fillStyle = THEME.items.circleBackground; - context.beginCircle(0, 0, quadrantSize * 1.15); - context.fill(); - - for (let layerIndex = 0; layerIndex < this.layers.length; ++layerIndex) { - const quadrants = this.layers[layerIndex]; - - const layerScale = Math.max(0.1, 0.9 - layerIndex * 0.22); - - for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { - if (!quadrants[quadrantIndex]) { - continue; - } - const { subShape, color } = quadrants[quadrantIndex]; - - const quadrantPos = arrayQuadrantIndexToOffset[quadrantIndex]; - const centerQuadrantX = quadrantPos.x * quadrantHalfSize; - const centerQuadrantY = quadrantPos.y * quadrantHalfSize; - - const rotation = Math.radians(quadrantIndex * 90); - - context.translate(centerQuadrantX, centerQuadrantY); - context.rotate(rotation); - - context.fillStyle = enumColorsToHexCode[color]; - context.strokeStyle = THEME.items.outline; - context.lineWidth = THEME.items.outlineWidth; - - const insetPadding = 0.0; - - switch (subShape) { - case enumSubShape.rect: { - context.beginPath(); - const dims = quadrantSize * layerScale; - context.rect( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize - dims, - dims, - dims - ); - - break; - } - case enumSubShape.star: { - context.beginPath(); - const dims = quadrantSize * layerScale; - - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; - - const moveInwards = dims * 0.4; - context.moveTo(originX, originY + moveInwards); - context.lineTo(originX + dims, originY); - context.lineTo(originX + dims - moveInwards, originY + dims); - context.lineTo(originX, originY + dims); - context.closePath(); - break; - } - - case enumSubShape.windmill: { - context.beginPath(); - const dims = quadrantSize * layerScale; - - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; - const moveInwards = dims * 0.4; - context.moveTo(originX, originY + moveInwards); - context.lineTo(originX + dims, originY); - context.lineTo(originX + dims, originY + dims); - context.lineTo(originX, originY + dims); - context.closePath(); - break; - } - - case enumSubShape.circle: { - context.beginPath(); - context.moveTo(insetPadding + -quadrantHalfSize, -insetPadding + quadrantHalfSize); - context.arc( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize, - quadrantSize * layerScale, - -Math.PI * 0.5, - 0 - ); - context.closePath(); - break; - } - - default: { - assertAlways(false, "Unkown sub shape: " + subShape); - } - } - - context.fill(); - context.stroke(); - - context.rotate(-rotation); - context.translate(-centerQuadrantX, -centerQuadrantY); - } - } - } - - /** - * Returns a definition with only the given quadrants - * @param {Array} includeQuadrants - * @returns {ShapeDefinition} - */ - cloneFilteredByQuadrants(includeQuadrants) { - const newLayers = this.internalCloneLayers(); - for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { - const quadrants = newLayers[layerIndex]; - let anyContents = false; - for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { - if (includeQuadrants.indexOf(quadrantIndex) < 0) { - quadrants[quadrantIndex] = null; - } else if (quadrants[quadrantIndex]) { - anyContents = true; - } - } - - // Check if the layer is entirely empty - if (!anyContents) { - newLayers.splice(layerIndex, 1); - layerIndex -= 1; - } - } - return new ShapeDefinition({ layers: newLayers }); - } - - /** - * Returns a definition which was rotated clockwise - * @returns {ShapeDefinition} - */ - cloneRotateCW() { - const newLayers = this.internalCloneLayers(); - for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { - const quadrants = newLayers[layerIndex]; - quadrants.unshift(quadrants[3]); - quadrants.pop(); - } - return new ShapeDefinition({ layers: newLayers }); - } - - /** - * Returns a definition which was rotated counter clockwise - * @returns {ShapeDefinition} - */ - cloneRotateCCW() { - const newLayers = this.internalCloneLayers(); - for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { - const quadrants = newLayers[layerIndex]; - quadrants.push(quadrants[0]); - quadrants.shift(); - } - return new ShapeDefinition({ layers: newLayers }); - } - - /** - * Returns a definition which was rotated 180 degrees (flipped) - * @returns {ShapeDefinition} - */ - cloneRotateFL() { - const newLayers = this.internalCloneLayers(); - for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { - const quadrants = newLayers[layerIndex]; - quadrants.push(quadrants.shift(), quadrants.shift()); - } - return new ShapeDefinition({ layers: newLayers }); - } - - /** - * Stacks the given shape definition on top. - * @param {ShapeDefinition} definition - */ - cloneAndStackWith(definition) { - if (this.isEntirelyEmpty() || definition.isEntirelyEmpty()) { - assert(false, "Can not stack entirely empty definition"); - } - - const bottomShapeLayers = this.layers; - const bottomShapeHighestLayerByQuad = [-1, -1, -1, -1]; - - for (let layer = bottomShapeLayers.length - 1; layer >= 0; --layer) { - const shapeLayer = bottomShapeLayers[layer]; - for (let quad = 0; quad < 4; ++quad) { - const shapeQuad = shapeLayer[quad]; - if (shapeQuad !== null && bottomShapeHighestLayerByQuad[quad] < layer) { - bottomShapeHighestLayerByQuad[quad] = layer; - } - } - } - - const topShapeLayers = definition.layers; - const topShapeLowestLayerByQuad = [4, 4, 4, 4]; - - for (let layer = 0; layer < topShapeLayers.length; ++layer) { - const shapeLayer = topShapeLayers[layer]; - for (let quad = 0; quad < 4; ++quad) { - const shapeQuad = shapeLayer[quad]; - if (shapeQuad !== null && topShapeLowestLayerByQuad[quad] > layer) { - topShapeLowestLayerByQuad[quad] = layer; - } - } - } - - /** - * We want to find the number `layerToMergeAt` such that when the top shape is placed at that - * layer, the smallest gap between shapes is only 1. Instead of doing a guess-and-check method to - * find the appropriate layer, we just calculate all the gaps assuming a merge at layer 0, even - * though they go negative, and calculating the number to add to it so the minimum gap is 1 (ends - * up being 1 - minimum). - */ - const gapsBetweenShapes = []; - for (let quad = 0; quad < 4; ++quad) { - gapsBetweenShapes.push(topShapeLowestLayerByQuad[quad] - bottomShapeHighestLayerByQuad[quad]); - } - const smallestGapBetweenShapes = Math.min(...gapsBetweenShapes); - // Can't merge at a layer lower than 0 - const layerToMergeAt = Math.max(1 - smallestGapBetweenShapes, 0); - - const mergedLayers = this.internalCloneLayers(); - for (let layer = mergedLayers.length; layer < layerToMergeAt + topShapeLayers.length; ++layer) { - mergedLayers.push([null, null, null, null]); - } - - for (let layer = 0; layer < topShapeLayers.length; ++layer) { - const layerMergingAt = layerToMergeAt + layer; - const bottomShapeLayer = mergedLayers[layerMergingAt]; - const topShapeLayer = topShapeLayers[layer]; - for (let quad = 0; quad < 4; quad++) { - assert(!(bottomShapeLayer[quad] && topShapeLayer[quad]), "Shape merge: Sub shape got lost"); - bottomShapeLayer[quad] = bottomShapeLayer[quad] || topShapeLayer[quad]; - } - } - - // Limit to 4 layers at max - mergedLayers.splice(4); - - return new ShapeDefinition({ layers: mergedLayers }); - } - - /** - * Clones the shape and colors everything in the given color - * @param {enumColors} color - */ - cloneAndPaintWith(color) { - const newLayers = this.internalCloneLayers(); - - for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { - const quadrants = newLayers[layerIndex]; - for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { - const item = quadrants[quadrantIndex]; - if (item) { - item.color = color; - } - } - } - return new ShapeDefinition({ layers: newLayers }); - } - - /** - * Clones the shape and colors everything in the given colors - * @param {[enumColors, enumColors, enumColors, enumColors]} colors - */ - cloneAndPaintWith4Colors(colors) { - const newLayers = this.internalCloneLayers(); - - for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { - const quadrants = newLayers[layerIndex]; - for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { - const item = quadrants[quadrantIndex]; - if (item) { - item.color = colors[quadrantIndex]; - } - } - } - return new ShapeDefinition({ layers: newLayers }); - } -} +import { makeOffscreenBuffer } from "../core/buffer_utils"; +import { globalConfig } from "../core/config"; +import { smoothenDpi } from "../core/dpi_manager"; +import { DrawParameters } from "../core/draw_parameters"; +import { Vector } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; +import { THEME } from "./theme"; + +/** + * @typedef {{ + * subShape: enumSubShape, + * color: enumColors, + * }} ShapeLayerItem + */ + +/** + * Order is Q1 (tr), Q2(br), Q3(bl), Q4(tl) + * @typedef {[ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?]} ShapeLayer + */ + +const arrayQuadrantIndexToOffset = [ + new Vector(1, -1), // tr + new Vector(1, 1), // br + new Vector(-1, 1), // bl + new Vector(-1, -1), // tl +]; + +/** @enum {string} */ +export const enumSubShape = { + rect: "rect", + circle: "circle", + star: "star", + windmill: "windmill", +}; + +/** @enum {string} */ +export const enumSubShapeToShortcode = { + [enumSubShape.rect]: "R", + [enumSubShape.circle]: "C", + [enumSubShape.star]: "S", + [enumSubShape.windmill]: "W", +}; + +/** @enum {enumSubShape} */ +export const enumShortcodeToSubShape = {}; +for (const key in enumSubShapeToShortcode) { + enumShortcodeToSubShape[enumSubShapeToShortcode[key]] = key; +} + +/** + * Converts the given parameters to a valid shape definition + * @param {*} layers + * @returns {Array} + */ +export function createSimpleShape(layers) { + layers.forEach(layer => { + layer.forEach(item => { + if (item) { + item.color = item.color || enumColors.uncolored; + } + }); + }); + return layers; +} + +/** + * Cache which shapes are valid short keys and which not + * @type {Map} + */ +const SHORT_KEY_CACHE = new Map(); + +export class ShapeDefinition extends BasicSerializableObject { + static getId() { + return "ShapeDefinition"; + } + + static getSchema() { + return {}; + } + + deserialize(data) { + const errorCode = super.deserialize(data); + if (errorCode) { + return errorCode; + } + const definition = ShapeDefinition.fromShortKey(data); + this.layers = /** @type {Array} */ (definition.layers); + } + + serialize() { + return this.getHash(); + } + + /** + * + * @param {object} param0 + * @param {Array=} param0.layers + */ + constructor({ layers = [] }) { + super(); + + /** + * The layers from bottom to top + * @type {Array} + */ + this.layers = layers; + + /** @type {string} */ + this.cachedHash = null; + + // Set on demand + this.bufferGenerator = null; + } + + /** + * Generates the definition from the given short key + * @param {string} key + * @returns {ShapeDefinition} + */ + static fromShortKey(key) { + const sourceLayers = key.split(":"); + let layers = []; + for (let i = 0; i < sourceLayers.length; ++i) { + const text = sourceLayers[i]; + assert(text.length === 8, "Invalid shape short key: " + key); + + /** @type {ShapeLayer} */ + const quads = [null, null, null, null]; + for (let quad = 0; quad < 4; ++quad) { + const shapeText = text[quad * 2 + 0]; + const subShape = enumShortcodeToSubShape[shapeText]; + const color = enumShortcodeToColor[text[quad * 2 + 1]]; + if (subShape) { + assert(color, "Invalid shape short key:", key); + quads[quad] = { + subShape, + color, + }; + } else if (shapeText !== "-") { + assert(false, "Invalid shape key: " + shapeText); + } + } + layers.push(quads); + } + + const definition = new ShapeDefinition({ layers }); + // We know the hash so save some work + definition.cachedHash = key; + return definition; + } + + /** + * Checks if a given string is a valid short key + * @param {string} key + * @returns {boolean} + */ + static isValidShortKey(key) { + if (SHORT_KEY_CACHE.has(key)) { + return SHORT_KEY_CACHE.get(key); + } + + const result = ShapeDefinition.isValidShortKeyInternal(key); + SHORT_KEY_CACHE.set(key, result); + return result; + } + + /** + * INTERNAL + * Checks if a given string is a valid short key + * @param {string} key + * @returns {boolean} + */ + static isValidShortKeyInternal(key) { + const sourceLayers = key.split(":"); + let layers = []; + for (let i = 0; i < sourceLayers.length; ++i) { + const text = sourceLayers[i]; + if (text.length !== 8) { + return false; + } + + /** @type {ShapeLayer} */ + const quads = [null, null, null, null]; + let anyFilled = false; + for (let quad = 0; quad < 4; ++quad) { + const shapeText = text[quad * 2 + 0]; + const colorText = text[quad * 2 + 1]; + const subShape = enumShortcodeToSubShape[shapeText]; + const color = enumShortcodeToColor[colorText]; + + // Valid shape + if (subShape) { + if (!color) { + // Invalid color + return false; + } + quads[quad] = { + subShape, + color, + }; + anyFilled = true; + } else if (shapeText === "-") { + // Make sure color is empty then, too + if (colorText !== "-") { + return false; + } + } else { + // Invalid shape key + return false; + } + } + + if (!anyFilled) { + // Empty layer + return false; + } + layers.push(quads); + } + + if (layers.length === 0 || layers.length > 4) { + return false; + } + + return true; + } + + /** + * Internal method to clone the shape definition + * @returns {Array} + */ + internalCloneLayers() { + return JSON.parse(JSON.stringify(this.layers)); + } + + /** + * Returns if the definition is entirely empty^ + * @returns {boolean} + */ + isEntirelyEmpty() { + return this.layers.length === 0; + } + + /** + * Returns a unique id for this shape + * @returns {string} + */ + getHash() { + if (this.cachedHash) { + return this.cachedHash; + } + + let id = ""; + for (let layerIndex = 0; layerIndex < this.layers.length; ++layerIndex) { + const layer = this.layers[layerIndex]; + + for (let quadrant = 0; quadrant < layer.length; ++quadrant) { + const item = layer[quadrant]; + if (item) { + id += enumSubShapeToShortcode[item.subShape] + enumColorToShortcode[item.color]; + } else { + id += "--"; + } + } + + if (layerIndex < this.layers.length - 1) { + id += ":"; + } + } + this.cachedHash = id; + return id; + } + + /** + * Draws the shape definition + * @param {number} x + * @param {number} y + * @param {DrawParameters} parameters + * @param {number=} diameter + */ + drawCentered(x, y, parameters, diameter = 20) { + const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); + + if (!this.bufferGenerator) { + this.bufferGenerator = this.internalGenerateShapeBuffer.bind(this); + } + + const key = diameter + "/" + dpi + "/" + this.cachedHash; + const canvas = parameters.root.buffers.getForKey({ + key: "shapedef", + subKey: key, + w: diameter, + h: diameter, + dpi, + redrawMethod: this.bufferGenerator, + }); + parameters.context.drawImage(canvas, x - diameter / 2, y - diameter / 2, diameter, diameter); + } + + /** + * Generates this shape as a canvas + * @param {number} size + */ + generateAsCanvas(size = 120) { + const [canvas, context] = makeOffscreenBuffer(size, size, { + smooth: true, + label: "definition-canvas-cache-" + this.getHash(), + reusable: false, + }); + + this.internalGenerateShapeBuffer(canvas, context, size, size, 1); + return canvas; + } + + /** + * + * @param {HTMLCanvasElement} canvas + * @param {CanvasRenderingContext2D} context + * @param {number} w + * @param {number} h + * @param {number} dpi + */ + internalGenerateShapeBuffer(canvas, context, w, h, dpi) { + context.translate((w * dpi) / 2, (h * dpi) / 2); + context.scale((dpi * w) / 23, (dpi * h) / 23); + + context.fillStyle = "#e9ecf7"; + + const quadrantSize = 10; + const quadrantHalfSize = quadrantSize / 2; + + context.fillStyle = THEME.items.circleBackground; + context.beginCircle(0, 0, quadrantSize * 1.15); + context.fill(); + + for (let layerIndex = 0; layerIndex < this.layers.length; ++layerIndex) { + const quadrants = this.layers[layerIndex]; + + const layerScale = Math.max(0.1, 0.9 - layerIndex * 0.22); + + for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { + if (!quadrants[quadrantIndex]) { + continue; + } + const { subShape, color } = quadrants[quadrantIndex]; + + const quadrantPos = arrayQuadrantIndexToOffset[quadrantIndex]; + const centerQuadrantX = quadrantPos.x * quadrantHalfSize; + const centerQuadrantY = quadrantPos.y * quadrantHalfSize; + + const rotation = Math.radians(quadrantIndex * 90); + + context.translate(centerQuadrantX, centerQuadrantY); + context.rotate(rotation); + + context.fillStyle = enumColorsToHexCode[color]; + context.strokeStyle = THEME.items.outline; + context.lineWidth = THEME.items.outlineWidth; + + const insetPadding = 0.0; + + switch (subShape) { + case enumSubShape.rect: { + context.beginPath(); + const dims = quadrantSize * layerScale; + context.rect( + insetPadding + -quadrantHalfSize, + -insetPadding + quadrantHalfSize - dims, + dims, + dims + ); + + break; + } + case enumSubShape.star: { + context.beginPath(); + const dims = quadrantSize * layerScale; + + let originX = insetPadding - quadrantHalfSize; + let originY = -insetPadding + quadrantHalfSize - dims; + + const moveInwards = dims * 0.4; + context.moveTo(originX, originY + moveInwards); + context.lineTo(originX + dims, originY); + context.lineTo(originX + dims - moveInwards, originY + dims); + context.lineTo(originX, originY + dims); + context.closePath(); + break; + } + + case enumSubShape.windmill: { + context.beginPath(); + const dims = quadrantSize * layerScale; + + let originX = insetPadding - quadrantHalfSize; + let originY = -insetPadding + quadrantHalfSize - dims; + const moveInwards = dims * 0.4; + context.moveTo(originX, originY + moveInwards); + context.lineTo(originX + dims, originY); + context.lineTo(originX + dims, originY + dims); + context.lineTo(originX, originY + dims); + context.closePath(); + break; + } + + case enumSubShape.circle: { + context.beginPath(); + context.moveTo(insetPadding + -quadrantHalfSize, -insetPadding + quadrantHalfSize); + context.arc( + insetPadding + -quadrantHalfSize, + -insetPadding + quadrantHalfSize, + quadrantSize * layerScale, + -Math.PI * 0.5, + 0 + ); + context.closePath(); + break; + } + + default: { + assertAlways(false, "Unkown sub shape: " + subShape); + } + } + + context.fill(); + context.stroke(); + + context.rotate(-rotation); + context.translate(-centerQuadrantX, -centerQuadrantY); + } + } + } + + /** + * Returns a definition with only the given quadrants + * @param {Array} includeQuadrants + * @returns {ShapeDefinition} + */ + cloneFilteredByQuadrants(includeQuadrants) { + const newLayers = this.internalCloneLayers(); + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + let anyContents = false; + for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { + if (includeQuadrants.indexOf(quadrantIndex) < 0) { + quadrants[quadrantIndex] = null; + } else if (quadrants[quadrantIndex]) { + anyContents = true; + } + } + + // Check if the layer is entirely empty + if (!anyContents) { + newLayers.splice(layerIndex, 1); + layerIndex -= 1; + } + } + return new ShapeDefinition({ layers: newLayers }); + } + + /** + * Returns a definition which was rotated clockwise + * @returns {ShapeDefinition} + */ + cloneRotateCW() { + const newLayers = this.internalCloneLayers(); + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + quadrants.unshift(quadrants[3]); + quadrants.pop(); + } + return new ShapeDefinition({ layers: newLayers }); + } + + /** + * Returns a definition which was rotated counter clockwise + * @returns {ShapeDefinition} + */ + cloneRotateCCW() { + const newLayers = this.internalCloneLayers(); + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + quadrants.push(quadrants[0]); + quadrants.shift(); + } + return new ShapeDefinition({ layers: newLayers }); + } + + /** + * Returns a definition which was rotated 180 degrees (flipped) + * @returns {ShapeDefinition} + */ + cloneRotateFL() { + const newLayers = this.internalCloneLayers(); + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + quadrants.push(quadrants.shift(), quadrants.shift()); + } + return new ShapeDefinition({ layers: newLayers }); + } + + /** + * Stacks the given shape definition on top. + * @param {ShapeDefinition} definition + */ + cloneAndStackWith(definition) { + if (this.isEntirelyEmpty() || definition.isEntirelyEmpty()) { + assert(false, "Can not stack entirely empty definition"); + } + + const bottomShapeLayers = this.layers; + const bottomShapeHighestLayerByQuad = [-1, -1, -1, -1]; + + for (let layer = bottomShapeLayers.length - 1; layer >= 0; --layer) { + const shapeLayer = bottomShapeLayers[layer]; + for (let quad = 0; quad < 4; ++quad) { + const shapeQuad = shapeLayer[quad]; + if (shapeQuad !== null && bottomShapeHighestLayerByQuad[quad] < layer) { + bottomShapeHighestLayerByQuad[quad] = layer; + } + } + } + + const topShapeLayers = definition.layers; + const topShapeLowestLayerByQuad = [4, 4, 4, 4]; + + for (let layer = 0; layer < topShapeLayers.length; ++layer) { + const shapeLayer = topShapeLayers[layer]; + for (let quad = 0; quad < 4; ++quad) { + const shapeQuad = shapeLayer[quad]; + if (shapeQuad !== null && topShapeLowestLayerByQuad[quad] > layer) { + topShapeLowestLayerByQuad[quad] = layer; + } + } + } + + /** + * We want to find the number `layerToMergeAt` such that when the top shape is placed at that + * layer, the smallest gap between shapes is only 1. Instead of doing a guess-and-check method to + * find the appropriate layer, we just calculate all the gaps assuming a merge at layer 0, even + * though they go negative, and calculating the number to add to it so the minimum gap is 1 (ends + * up being 1 - minimum). + */ + const gapsBetweenShapes = []; + for (let quad = 0; quad < 4; ++quad) { + gapsBetweenShapes.push(topShapeLowestLayerByQuad[quad] - bottomShapeHighestLayerByQuad[quad]); + } + const smallestGapBetweenShapes = Math.min(...gapsBetweenShapes); + // Can't merge at a layer lower than 0 + const layerToMergeAt = Math.max(1 - smallestGapBetweenShapes, 0); + + const mergedLayers = this.internalCloneLayers(); + for (let layer = mergedLayers.length; layer < layerToMergeAt + topShapeLayers.length; ++layer) { + mergedLayers.push([null, null, null, null]); + } + + for (let layer = 0; layer < topShapeLayers.length; ++layer) { + const layerMergingAt = layerToMergeAt + layer; + const bottomShapeLayer = mergedLayers[layerMergingAt]; + const topShapeLayer = topShapeLayers[layer]; + for (let quad = 0; quad < 4; quad++) { + assert(!(bottomShapeLayer[quad] && topShapeLayer[quad]), "Shape merge: Sub shape got lost"); + bottomShapeLayer[quad] = bottomShapeLayer[quad] || topShapeLayer[quad]; + } + } + + // Limit to 4 layers at max + mergedLayers.splice(4); + + return new ShapeDefinition({ layers: mergedLayers }); + } + + /** + * Clones the shape and colors everything in the given color + * @param {enumColors} color + */ + cloneAndPaintWith(color) { + const newLayers = this.internalCloneLayers(); + + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { + const item = quadrants[quadrantIndex]; + if (item) { + item.color = color; + } + } + } + return new ShapeDefinition({ layers: newLayers }); + } + + /** + * Clones the shape and colors everything in the given colors + * @param {[enumColors, enumColors, enumColors, enumColors]} colors + */ + cloneAndPaintWith4Colors(colors) { + const newLayers = this.internalCloneLayers(); + + for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { + const quadrants = newLayers[layerIndex]; + for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { + const item = quadrants[quadrantIndex]; + if (item) { + item.color = colors[quadrantIndex] || item.color; + } + } + } + return new ShapeDefinition({ layers: newLayers }); + } +} diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 9943594c..13d98dc8 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -1,10 +1,10 @@ import { globalConfig } from "../../core/config"; import { BaseItem } from "../base_item"; -import { enumColorMixingResults } from "../colors"; -import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; +import { enumColors, enumColorMixingResults } from "../colors"; +import { enumItemProcessorTypes, ItemProcessorComponent, enumItemProcessorRequirements } from "../components/item_processor"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; -import { BOOL_TRUE_SINGLETON } from "../items/boolean_item"; +import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ShapeItem } from "../items/shape_item"; @@ -68,15 +68,81 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } + + // Check if we have an empty queue and can start a new charge if (processorComp.itemsToEject.length === 0) { - if (processorComp.inputSlots.length >= processorComp.inputsPerCharge) { + if (entity.components.ItemProcessor.processingRequirement) { + if (this.canProcess(entity)) { + this.startNewCharge(entity); + } + } else if (processorComp.inputSlots.length >= processorComp.inputsPerCharge) { this.startNewCharge(entity); } } } } + /** + * Checks whether it's possible to process something + * @param {Entity} entity + */ + canProcess(entity) { + switch (entity.components.ItemProcessor.processingRequirement) { + case enumItemProcessorRequirements.painterQuad: { + // For quad-painter, pins match slots + // boolean true means "disable input" + // a color means "disable if not matched" + + const processorComp = entity.components.ItemProcessor; + const pinsComp = entity.components.WiredPins; + + /** @type {Object.} */ + const itemsBySlot = {}; + for (let i = 0; i < processorComp.inputSlots.length; ++i) { + itemsBySlot[processorComp.inputSlots[i].sourceSlot] = processorComp.inputSlots[i]; + } + + // first slot is the shape + if (!itemsBySlot[0]) return false; + const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); + + // Here we check just basic things` + // Stop processing if anything except TRUE is + // set and there is no item. + for (let i = 0; i < 4; ++i) { + const netValue = pinsComp.slots[i].linkedNetwork ? + pinsComp.slots[i].linkedNetwork.currentValue : + null; + + const currentItem = itemsBySlot[i + 1]; + + if ((netValue == null || !netValue.equals(BOOL_TRUE_SINGLETON)) && currentItem == null) { + let quadCount = 0; + + for (let j = 0; j < 4; ++j) { + const layer = shapeItem.definition.layers[j]; + if (layer && layer[i]) { + quadCount++; + } + } + + if (quadCount > 0) { + return false; + } + } + } + + return true; + } + default: + assertAlways( + false, + "Unknown requirement for " + entity.components.ItemProcessor.processingRequirement + ); + } + } + /** * Starts a new charge for the entity * @param {Entity} entity @@ -307,20 +373,56 @@ export class ItemProcessorSystem extends GameSystemWithFilter { case enumItemProcessorTypes.painterQuad: { const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); - const colorItem1 = /** @type {ColorItem} */ (itemsBySlot[1].item); - const colorItem2 = /** @type {ColorItem} */ (itemsBySlot[2].item); - const colorItem3 = /** @type {ColorItem} */ (itemsBySlot[3].item); - const colorItem4 = /** @type {ColorItem} */ (itemsBySlot[4].item); - assert(shapeItem instanceof ShapeItem, "Input for painter is not a shape"); - assert(colorItem1 instanceof ColorItem, "Input for painter is not a color"); - assert(colorItem2 instanceof ColorItem, "Input for painter is not a color"); - assert(colorItem3 instanceof ColorItem, "Input for painter is not a color"); - assert(colorItem4 instanceof ColorItem, "Input for painter is not a color"); + + /** @type {Array} */ + const colorItems = [].fill(null, 0, 4); + + for (let i = 0; i < 4; ++i) { + if (itemsBySlot[i + 1]) { + colorItems[i] = /** @type {ColorItem} */ (itemsBySlot[i + 1].item); + assert(colorItems[i] instanceof ColorItem, "Input for painter is not a color"); + } + } + + const pinValues = entity.components.WiredPins.slots + .map(slot => slot.linkedNetwork ? slot.linkedNetwork.currentValue : BOOL_FALSE_SINGLETON); + + // @todo cleanup + const colorTL = colorItems[0]; + const colorTR = colorItems[1]; + const colorBR = colorItems[2]; + const colorBL = colorItems[3]; + + /** @type {Array} */ + let skipped = []; + for (let i = 0; i < 4; ++i) { + skipped[i] = pinValues[i] ? pinValues[i].equals(BOOL_TRUE_SINGLETON) : false; + } + + for (let i = 0; i < 4; ++i) { + if (colorItems[i] == null) { + skipped[i] = false; // make sure we never insert null item back + } else if (pinValues[i] instanceof ColorItem) { + // if pin value is a color, skip anything except that color + // but still require any color, because it would not work on + // slow factories. + if (!colorItems[i].equals(pinValues[i])) { + skipped[i] = true; + } + } + } + + const toColor = [ + (!skipped[0] && colorTL) ? colorTL.color : null, + (!skipped[1] && colorTR) ? colorTR.color : null, + (!skipped[2] && colorBR) ? colorBR.color : null, + (!skipped[3] && colorBL) ? colorBL.color : null, + ]; const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith4Colors( shapeItem.definition, - [colorItem2.color, colorItem3.color, colorItem4.color, colorItem1.color] + /** @type {[enumColors, enumColors, enumColors, enumColors]} */(toColor) ); outItems.push({