1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-09 02:54:01 +00:00
tobspr_shapez.io/src/js/game/shape_definition.js
tobspr c41aaa1fc5
Mod Support - 1.5.0 Update (#1361)
* initial modloader draft

* modloader features

* Refactor mods to use signals

* Add support for modifying and registering new transltions

* Minor adjustments

* Support for string building ids for mods

* Initial support for adding new buildings

* Refactor how mods are loaded to resolve circular dependencies and prepare for future mod loading

* Lazy Load mods to make sure all dependencies are loaded

* Expose all exported members automatically to mods

* Fix duplicate exports

* Allow loading mods from standalone

* update changelog

* Fix mods folder incorrect path

* Fix modloading in standalone

* Fix sprites not getting replaced, update demo mod

* Load dev mod via raw loader

* Improve mod developing so mods are directly ready to be deployed, load mods from local file server

* Proper mods ui

* Allow mods to register game systems and draw stuff

* Change mods path

* Fix sprites not loading

* Minor adjustments, closes #1333

* Add support for loading atlases via mods

* Add support for loading mods from external sources in DEV

* Add confirmation when loading mods

* Fix circular dependency

* Minor Keybindings refactor, add support for keybindings to mods, add support for dialogs to mods

* Add some mod signals

* refactor game loading states

* Make shapez exports global

* Start to make mods safer

* Refactor file system electron event handling

* Properly isolate electron renderer process

* Update to latest electron

* Show errors when loading mods

* Update confirm dialgo

* Minor restructure, start to add mod examples

* Allow adding custom themesw

* Add more examples and allow defining custom item processor operations

* Add interface to register new buildings

* Fixed typescript type errors (#1335)

* Refactor building registry, make it easier for mods to add new buildings

* Allow overriding existing methods

* Add more examples and more features

* More mod examples

* Make mod loading simpler

* Add example how to add custom drawings

* Remove unused code

* Minor modloader adjustments

* Support for rotation variants in mods (was broken previously)

* Allow mods to replace builtin sub shapes

* Add helper methods to extend classes

* Fix menu bar on mac os

* Remember window state

* Add support for paste signals

* Add example how to add custom components and systems

* Support for mod settings

* Add example for adding a new item type

* Update class extensions

* Minor adjustments

* Fix typo

* Add notification blocks mod example

* Add small tutorial

* Update readme

* Add better instructions

* Update JSDoc for Replacing Methods (#1336)

* upgraded types for overriding methods

* updated comments

Co-authored-by: Edward Badel <you@example.com>

* Direction lock now indicates when there is a building inbetween

* Fix mod examples

* Fix linter error

* Game state register (#1341)

* Added a gamestate register helper

Added a gamestate register helper

* Update mod_interface.js

* export build options

* Fix runBeforeMethod and runAfterMethod

* Minor game system code cleanup

* Belt path drawing optimization

* Fix belt path optimization

* Belt drawing improvements, again

* Do not render belts in statics disabled view

* Allow external URL to load more than one mod (#1337)

* Allow external URL to load more than one mod

Instead of loading the text returned from the remote server, load a JSON object with a `mods` field, containing strings of all the mods. This lets us work on more than one mod at a time or without separate repos. This will break tooling such as `create-shapezio-mod` though.

* Update modloader.js

* Prettier fixes

* Added link to create-shapezio-mod npm page (#1339)

Added link to create-shapezio-mod npm page: https://www.npmjs.com/package/create-shapezio-mod

* allow command line switch to load more than one mod (#1342)

* Fixed class handle type (#1345)

* Fixed class handle type

* Fixed import game state

* Minor adjustments

* Refactor item acceptor to allow only single direction slots

* Allow specifying minimumGameVersion

* Add sandbox example

* Replaced concatenated strings with template literals (#1347)

* Mod improvements

* Make wired pins component optional on the storage

* Fix mod examples

* Bind `this` for method overriding JSDoc (#1352)

* fix entity debugger reaching HTML elements (#1353)

* Store mods in savegame and show warning when it differs

* Closes #1357

* Fix All Shapez Exports Being Const (#1358)

* Allowed setting of variables inside webpack modules

* remove console log

* Fix stringification of things inside of eval

Co-authored-by: Edward Badel <you@example.com>

* Fix building placer intersection warning

* Add example for storing data in the savegame

* Fix double painter bug (#1349)

* Add example on how to extend builtin buildings

* update readme

* Disable steam achievements when playing with mods

* Update translations

Co-authored-by: Thomas (DJ1TJOO) <44841260+DJ1TJOO@users.noreply.github.com>
Co-authored-by: Bagel03 <70449196+Bagel03@users.noreply.github.com>
Co-authored-by: Edward Badel <you@example.com>
Co-authored-by: Emerald Block <69981203+EmeraldBlock@users.noreply.github.com>
Co-authored-by: saile515 <63782477+saile515@users.noreply.github.com>
Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
2022-02-01 16:35:49 +01:00

643 lines
21 KiB
JavaScript

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 {{
* context: CanvasRenderingContext2D,
* quadrantSize: number,
* layerScale: number,
* }} SubShapeDrawOptions
*/
/**
* @type {Object<string, (options: SubShapeDrawOptions) => void>}
*/
export const MODS_ADDITIONAL_SUB_SHAPE_DRAWERS = {};
/**
* @typedef {{
* subShape: enumSubShape,
* color: enumColors,
* }} ShapeLayerItem
*/
export const TOP_RIGHT = 0;
export const BOTTOM_RIGHT = 1;
export const BOTTOM_LEFT = 2;
export const TOP_LEFT = 3;
/**
* 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<ShapeLayer>}
*/
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<string, boolean>}
*/
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<ShapeLayer>} */ (definition.layers);
}
serialize() {
return this.getHash();
}
/**
*
* @param {object} param0
* @param {Array<ShapeLayer>=} param0.layers
*/
constructor({ layers = [] }) {
super();
/**
* The layers from bottom to top
* @type {Array<ShapeLayer>}
*/
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<ShapeLayer>}
*/
getClonedLayers() {
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);
}
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
this.internalGenerateShapeBuffer(null, context, size, size, 1);
}
/**
* 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;
if (MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]) {
MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]({
context,
layerScale,
quadrantSize,
});
} else {
switch (subShape) {
case enumSubShape.rect: {
context.beginPath();
const dims = quadrantSize * layerScale;
context.rect(-quadrantHalfSize, quadrantHalfSize - dims, dims, dims);
context.fill();
context.stroke();
break;
}
case enumSubShape.star: {
context.beginPath();
const dims = quadrantSize * layerScale;
let originX = -quadrantHalfSize;
let originY = 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();
context.fill();
context.stroke();
break;
}
case enumSubShape.windmill: {
context.beginPath();
const dims = quadrantSize * layerScale;
let originX = -quadrantHalfSize;
let originY = 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();
context.fill();
context.stroke();
break;
}
case enumSubShape.circle: {
context.beginPath();
context.moveTo(-quadrantHalfSize, quadrantHalfSize);
context.arc(
-quadrantHalfSize,
quadrantHalfSize,
quadrantSize * layerScale,
-Math.PI * 0.5,
0
);
context.closePath();
context.fill();
context.stroke();
break;
}
default: {
throw new Error("Unkown sub shape: " + subShape);
}
}
}
context.rotate(-rotation);
context.translate(-centerQuadrantX, -centerQuadrantY);
}
}
}
/**
* Returns a definition with only the given quadrants
* @param {Array<number>} includeQuadrants
* @returns {ShapeDefinition}
*/
cloneFilteredByQuadrants(includeQuadrants) {
const newLayers = this.getClonedLayers();
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.getClonedLayers();
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.getClonedLayers();
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
* @returns {ShapeDefinition}
*/
cloneRotate180() {
const newLayers = this.getClonedLayers();
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.getClonedLayers();
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.getClonedLayers();
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.getClonedLayers();
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 });
}
}