1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-13 02:01:51 +00:00

Merge branch 'modloader' into processing-refactor

This commit is contained in:
Sense101 2022-01-20 16:59:06 +00:00
commit f3a65812a9
39 changed files with 1071 additions and 517 deletions

52
mod_examples/README.md Normal file
View File

@ -0,0 +1,52 @@
# shapez.io Modding
## General Instructions
Currently there are two options to develop mods for shapez.io:
1. Writing single file mods, which doesn't require any additional tools and can be loaded directly in the game
2. Using the `create-shapezio-mod` package. This package is still in development but allows you to pack multiple files and images into a single mod file, so you don't have to base64 encode your images etc.
Since the `create-shapezio-mod` package is still in development, the current recommended way is to write single file mods, which I'll explain now.
## Mod Developer Discord
A great place to get help with mod development is the official [shapez.io modloader discord]https://discord.gg/xq5v8uyMue).
## Setting up your development environment
The simplest way of developing mods is by just creating a `mymod.js` file and putting it in the `mods/` folder of the standalone (You can find the `mods/` folder by clicking "Open Mods Folder" in the shapez.io Standalone, be sure to select the 1.5.0-modloader branch on Steam).
You can then add `--dev` to the launch options on Steam. This adds an application menu where you can click "Restart" to reload your mod, and will also show the developer console where you can see any potential errors.
## Getting started
To get into shapez.io modding, I highly recommend checking out all of the examples in this folder. Here's a list of examples and what features of the modloader they show:
| Example | Description | Demonstrates |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| [base.js](base.js) | The most basic mod | Base structure of a mod |
| [class_extensions.js](class_extensions.js) | Shows how to extend multiple methods of one class at once, useful for overriding a lot of methods | Overriding and extending builtin methods |
| [custom_css.js](custom_css.js) | Modifies the Main Menu State look | Modifying the UI styles with CSS |
| [replace_builtin_sprites.js](replace_builtin_sprites.js) | Replaces all color sprites with icons | Replacing builtin sprites |
| [translations.js](translations.js) | Shows how to replace and add new translations in multiple languages | Adding and replacing translations |
| [add_building_basic.js](add_building_basic.js) | Shows how to add a new building | Registering a new building |
| [add_building_flipper.js](add_building_flipper.js) | Adds a "flipper" building which mirrors shapes from top to bottom | Registering a new building, Adding a custom shape and item processing operation (flip) |
| [custom_drawing.js](custom_drawing.js) | Displays a a small indicator on every item processing building whether it is currently working | Adding a new GameSystem and drawing overlays |
| [custom_keybinding.js](custom_keybinding.js) | Adds a new customizable ingame keybinding (Shift+F) | Adding a new keybinding |
| [custom_sub_shapes.js](custom_sub_shapes.js) | Adds a new type of sub-shape (Line) | Adding a new sub shape and drawing it, making it spawn on the map, modifying the builtin levels |
| [modify_theme.js](modify_theme.js) | Modifies the default game themes | Modifying the builtin themes |
| [custom_theme.js](custom_theme.js) | Adds a new UI and map theme | Adding a new game theme |
| [mod_settings.js](mod_settings.js) | Shows a dialog counting how often the mod has been launched | Reading and storing mod settings |
| [modify_existing_building.js](modify_existing_building.js) | Makes the rotator building always unlocked and adds a new statistic to the building panel | Modifying a builtin building, replacing builtin methods |
| [modify_ui.js](modify_ui.js) | Shows how to add custom IU elements to builtin game states (the Main Menu in this case) | Extending builtin UI states, Adding CSS |
| [pasting.js](pasting.js) | Shows a dialog when pasting text in the game | Listening to paste events |
### Advanced Examples
| Example | Description | Demonstrates |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [notification_blocks.js](notification_blocks.js) | Adds a notification block building, which shows a user defined notification when receiving a truthy signal | Adding a new Component, Adding a new GameSystem, Working with wire networks, Adding a new building, Adding a new HUD part, Using Input Dialogs, Adding Translations |
| [usage_statistics.js](usage_statistics.js) | Displays a percentage on every building showing its utilization | Adding a new component, Adding a new GameSystem, Drawing within a GameSystem, Modifying builtin buildings, Adding custom game logic |
| [new_item_type.js](new_item_type.js) | Adds a new type of items to the map (fluids) | Adding a new item type, modifying map generation |
| [buildings_have_cost.js](buildings_have_cost.js) | Adds a new currency, and belts cost 1 of that currency | Extending and replacing builtin methods, Adding CSS and custom sprites |

View File

@ -65,10 +65,10 @@ class Mod extends shapez.Mod {
// Only allow placing an entity when there is enough currency
this.modInterface.replaceMethod(shapez.GameLogic, "checkCanPlaceEntity", function (
$original,
[entity, offset]
[entity, options]
) {
const storedCurrency = this.root.hubGoals.storedShapes[CURRENCY] || 0;
return storedCurrency > 0 && $original(entity, offset);
return storedCurrency > 0 && $original(entity, options);
});
// Take shapes when placing a building

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,27 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Custom Keybindings",
version: "1",
id: "base",
description: "Shows how to add a new keybinding",
};
class Mod extends shapez.Mod {
init() {
// Register keybinding
this.modInterface.registerIngameKeybinding({
id: "demo_mod_binding",
keyCode: shapez.keyToKeyCode("F"),
translation: "Do something (always with SHIFT)",
modifiers: {
shift: true,
},
handler: root => {
this.dialogs.showInfo("Mod Message", "It worked!");
return shapez.STOP_PROPAGATION;
},
});
}
}

View File

@ -32,6 +32,8 @@ class Mod extends shapez.Mod {
0
);
context.closePath();
context.fill();
context.stroke();
},
});

View File

@ -40,6 +40,10 @@ const RESOURCES = {
color: "rgb(74, 237, 134)",
background: "rgba(74, 237, 134, 0.2)",
},
error: {
color: "rgb(255, 137, 137)",
background: "rgba(255, 137, 137, 0.2)",
},
},
colorBlindPickerTile: "rgba(50, 50, 50, 0.4)",

41
mod_examples/modify_ui.js Normal file
View File

@ -0,0 +1,41 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Modify UI",
version: "1",
id: "modify-ui",
description: "Shows how to modify a builtin game state, in this case the main menu",
};
class Mod extends shapez.Mod {
init() {
// Add fancy sign to main menu
this.signals.stateEntered.add(state => {
if (state.key === "MainMenuState") {
const element = document.createElement("div");
element.id = "demo_mod_hello_world_element";
document.body.appendChild(element);
const button = document.createElement("button");
button.classList.add("styledButton");
button.innerText = "Hello!";
button.addEventListener("click", () => {
this.dialogs.showInfo("Mod Message", "Button clicked!");
});
element.appendChild(button);
}
});
this.modInterface.registerCss(`
#demo_mod_hello_world_element {
position: absolute;
top: calc(10px * var(--ui-scale));
left: calc(10px * var(--ui-scale));
color: red;
z-index: 0;
}
`);
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -61,7 +61,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2,
background-image: uiResource("res/ui/building_tutorials/virtual_processor-cutter.png") !important;
}
$icons: notification_saved, notification_success, notification_upgrade;
$icons: notification_saved, notification_success, notification_upgrade, notification_info,
notification_warning, notification_error;
@each $icon in $icons {
[data-icon="icons/#{$icon}.png"] {
/* @load-async */

View File

@ -4,6 +4,7 @@ export const CHANGELOG = [
date: "unreleased",
entries: [
"This version adds an official modloader! You can now load mods by placing it in the mods/ folder of the game.",
"When holding shift while placing a belt, the indicator now becomes red when crossing buildings",
],
},
{

View File

@ -42,7 +42,7 @@ export const globalConfig = {
// Which dpi the assets have
assetsDpi: 192 / 32,
assetsSharpness: 1.5,
shapesSharpness: 1.4,
shapesSharpness: 1.3,
// Achievements
achievementSliceDuration: 10, // Seconds
@ -58,9 +58,11 @@ export const globalConfig = {
// Map
mapChunkSize: 16,
chunkAggregateSize: 4,
mapChunkOverviewMinZoom: 0.9,
mapChunkOverviewMinZoom: 0,
mapChunkWorldSize: null, // COMPUTED
maxBeltShapeBundleSize: 20,
// Belt speeds
// NOTICE: Update webpack.production.config too!
beltSpeedItemsPerSecond: 2,

View File

@ -119,5 +119,8 @@ export default {
// Allows to load a mod from an external source for developing it
// externalModUrl: "http://localhost:3005/combined.js",
// -----------------------------------------------------------------------------------
// Visualizes the shape grouping on belts
// showShapeGrouping: true
// -----------------------------------------------------------------------------------
/* dev:end */
};

View File

@ -15,3 +15,20 @@ export function setGlobalApp(app) {
assert(!GLOBAL_APP, "Create application twice!");
GLOBAL_APP = app;
}
export const BUILD_OPTIONS = {
HAVE_ASSERT: G_HAVE_ASSERT,
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
TRACKING_ENDPOINT: G_TRACKING_ENDPOINT,
CHINA_VERSION: G_CHINA_VERSION,
WEGAME_VERSION: G_WEGAME_VERSION,
IS_DEV: G_IS_DEV,
IS_RELEASE: G_IS_RELEASE,
IS_MOBILE_APP: G_IS_MOBILE_APP,
IS_BROWSER: G_IS_BROWSER,
IS_STANDALONE: G_IS_STANDALONE,
BUILD_TIME: G_BUILD_TIME,
BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH,
BUILD_VERSION: G_BUILD_VERSION,
ALL_UI_IMAGES: G_ALL_UI_IMAGES,
};

View File

@ -1,7 +1,9 @@
import { globalConfig } from "../core/config";
import { smoothenDpi } from "../core/dpi_manager";
import { DrawParameters } from "../core/draw_parameters";
import { createLogger } from "../core/logging";
import { Rectangle } from "../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../core/sprites";
import { clamp, epsilonCompare, round4Digits } from "../core/utils";
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization";
@ -1349,6 +1351,12 @@ export class BeltPath extends BasicSerializableObject {
let trackPos = 0.0;
/**
* @type {Array<[Vector, BaseItem]>}
*/
let drawStack = [];
let drawStackProp = "";
// Iterate whole track and check items
for (let i = 0; i < this.entityPath.length; ++i) {
const entity = this.entityPath[i];
@ -1368,25 +1376,185 @@ export class BeltPath extends BasicSerializableObject {
const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile();
const distanceAndItem = this.items[currentItemIndex];
const item = distanceAndItem[1 /* item */];
const nextItemDistance = distanceAndItem[0 /* nextDistance */];
distanceAndItem[1 /* item */].drawItemCenteredClipped(
worldPos.x,
worldPos.y,
parameters,
globalConfig.defaultItemDiameter
);
if (
!parameters.visibleRect.containsCircle(
worldPos.x,
worldPos.y,
globalConfig.defaultItemDiameter
)
) {
// this one isn't visible, do not append it
// Start a new stack
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
} else {
if (drawStack.length > 1) {
// Check if we can append to the stack, since its already a stack of two same items
const referenceItem = drawStack[0];
if (Math.abs(referenceItem[0][drawStackProp] - worldPos[drawStackProp]) < 0.001) {
// Will continue stack
} else {
// Start a new stack, since item doesn't follow in row
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else if (drawStack.length === 1) {
const firstItem = drawStack[0];
// Check if we can make it a stack
if (firstItem[1 /* item */].equals(item)) {
// Same item, check if it is either horizontal or vertical
const startPos = firstItem[0 /* pos */];
if (Math.abs(startPos.x - worldPos.x) < 0.001) {
drawStackProp = "x";
} else if (Math.abs(startPos.y - worldPos.y) < 0.001) {
drawStackProp = "y";
} else {
// Start a new stack
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else {
// Start a new stack, since item doesn't equal
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else {
// First item of stack, do nothing
}
drawStack.push([worldPos, item]);
}
// Check for the next item
currentItemPos += distanceAndItem[0 /* nextDistance */];
currentItemPos += nextItemDistance;
++currentItemIndex;
if (
nextItemDistance > globalConfig.itemSpacingOnBelts + 0.001 ||
drawStack.length > globalConfig.maxBeltShapeBundleSize
) {
// If next item is not directly following, abort drawing
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
if (currentItemIndex >= this.items.length) {
// We rendered all items
this.drawDrawStack(drawStack, parameters, drawStackProp);
return;
}
}
trackPos += beltLength;
}
this.drawDrawStack(drawStack, parameters, drawStackProp);
}
/**
*
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} context
* @param {number} w
* @param {number} h
* @param {number} dpi
* @param {object} param0
* @param {string} param0.direction
* @param {Array<[Vector, BaseItem]>} param0.stack
* @param {GameRoot} param0.root
* @param {number} param0.zoomLevel
*/
drawShapesInARow(canvas, context, w, h, dpi, { direction, stack, root, zoomLevel }) {
context.scale(dpi, dpi);
if (G_IS_DEV && globalConfig.debug.showShapeGrouping) {
context.fillStyle = "rgba(0, 0, 255, 0.5)";
context.fillRect(0, 0, w, h);
}
const parameters = new DrawParameters({
context,
desiredAtlasScale: ORIGINAL_SPRITE_SCALE,
root,
visibleRect: new Rectangle(-1000, -1000, 2000, 2000),
zoomLevel,
});
const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
const item = stack[0];
const pos = new Vector(itemSize / 2, itemSize / 2);
for (let i = 0; i < stack.length; i++) {
item[1].drawItemCenteredClipped(pos.x, pos.y, parameters, globalConfig.defaultItemDiameter);
pos[direction] += globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
}
}
/**
* @param {Array<[Vector, BaseItem]>} stack
* @param {DrawParameters} parameters
*/
drawDrawStack(stack, parameters, directionProp) {
if (stack.length === 0) {
return;
}
const firstItem = stack[0];
const firstItemPos = firstItem[0];
if (stack.length === 1) {
firstItem[1].drawItemCenteredClipped(
firstItemPos.x,
firstItemPos.y,
parameters,
globalConfig.defaultItemDiameter
);
return;
}
const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
const inverseDirection = directionProp === "x" ? "y" : "x";
const dimensions = new Vector(itemSize, itemSize);
dimensions[inverseDirection] *= stack.length;
const directionVector = firstItemPos.copy().sub(stack[1][0]);
const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel);
const sprite = this.root.buffers.getForKey({
key: "beltpaths",
subKey: "stack-" + directionProp + "-" + dpi + "-" + stack.length + firstItem[1].serialize(),
dpi,
w: dimensions.x,
h: dimensions.y,
redrawMethod: this.drawShapesInARow.bind(this),
additionalParams: {
direction: inverseDirection,
stack,
root: this.root,
zoomLevel: parameters.zoomLevel,
},
});
const anchor = directionVector[inverseDirection] < 0 ? firstItem : stack[stack.length - 1];
parameters.context.drawImage(
sprite,
anchor[0].x - itemSize / 2,
anchor[0].y - itemSize / 2,
dimensions.x,
dimensions.y
);
}
}

View File

@ -82,7 +82,7 @@ export class Blueprint {
const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y);
if (!parameters.root.logic.checkCanPlaceEntity(entity, tile)) {
if (!parameters.root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
parameters.context.globalAlpha = 0.3;
} else {
parameters.context.globalAlpha = 1;
@ -131,7 +131,7 @@ export class Blueprint {
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (root.logic.checkCanPlaceEntity(entity, tile)) {
if (root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
anyPlaceable = true;
}
}
@ -160,7 +160,7 @@ export class Blueprint {
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
if (!root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
continue;
}

View File

@ -187,6 +187,7 @@ export class GameSystemManager {
// IMPORTANT: We have 2 phases: In phase 1 we compute the output values of all gates,
// processors etc. In phase 2 we propagate it through the wires network
add("logicGate", LogicGateSystem);
add("beltReader", BeltReaderSystem);
add("display", DisplaySystem);

View File

@ -90,6 +90,8 @@ export class GameHUD {
this.parts[partId] = new part(this.root);
}
MOD_SIGNALS.hudInitializer.dispatch(this.root);
const frag = document.createDocumentFragment();
for (const key in this.parts) {
MOD_SIGNALS.hudElementInitialized.dispatch(this.parts[key]);

View File

@ -61,7 +61,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
this.currentInterpolatedCornerTile = new Vector();
this.lockIndicatorSprites = {};
layers.forEach(layer => {
[...layers, "error"].forEach(layer => {
this.lockIndicatorSprites[layer] = this.makeLockIndicatorSprite(layer);
});
@ -76,7 +76,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
/**
* Makes the lock indicator sprite for the given layer
* @param {Layer} layer
* @param {string} layer
*/
makeLockIndicatorSprite(layer) {
const dims = 48;
@ -358,7 +358,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
rotationVariant
);
const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity);
const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity, {});
// Fade in / out
parameters.context.lineWidth = 1;
@ -397,6 +397,42 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
}
}
/**
* Checks if there are any entities in the way, returns true if there are
* @param {Vector} from
* @param {Vector} to
* @returns
*/
checkForObstales(from, to) {
assert(from.x === to.x || from.y === to.y, "Must be a straight line");
const prop = from.x === to.x ? "y" : "x";
const current = from.copy();
const metaBuilding = this.currentMetaBuilding.get();
this.fakeEntity.layer = metaBuilding.getLayer();
const staticComp = this.fakeEntity.components.StaticMapEntity;
staticComp.origin = current;
staticComp.rotation = 0;
metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get());
staticComp.code = getCodeFromBuildingData(
this.currentMetaBuilding.get(),
this.currentVariant.get(),
0
);
const start = Math.min(from[prop], to[prop]);
const end = Math.max(from[prop], to[prop]);
for (let i = start; i <= end; i++) {
current[prop] = i;
if (!this.root.logic.checkCanPlaceEntity(this.fakeEntity, { allowReplaceBuildings: false })) {
return true;
}
}
return false;
}
/**
* @param {DrawParameters} parameters
*/
@ -407,55 +443,73 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
return;
}
const applyStyles = look => {
parameters.context.fillStyle = THEME.map.directionLock[look].color;
parameters.context.strokeStyle = THEME.map.directionLock[look].background;
parameters.context.lineWidth = 10;
};
if (!this.lastDragTile) {
// Not dragging yet
applyStyles(this.root.currentLayer);
const mouseWorld = this.root.camera.screenToWorld(mousePosition);
parameters.context.beginCircle(mouseWorld.x, mouseWorld.y, 4);
parameters.context.fill();
return;
}
const mouseWorld = this.root.camera.screenToWorld(mousePosition);
const mouseTile = mouseWorld.toTileSpace();
parameters.context.fillStyle = THEME.map.directionLock[this.root.currentLayer].color;
parameters.context.strokeStyle = THEME.map.directionLock[this.root.currentLayer].background;
parameters.context.lineWidth = 10;
const startLine = this.lastDragTile.toWorldSpaceCenterOfTile();
const endLine = mouseTile.toWorldSpaceCenterOfTile();
const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile();
const anyObstacle =
this.checkForObstales(this.lastDragTile, this.currentDirectionLockCorner) ||
this.checkForObstales(this.currentDirectionLockCorner, mouseTile);
if (anyObstacle) {
applyStyles("error");
} else {
applyStyles(this.root.currentLayer);
}
parameters.context.beginCircle(mouseWorld.x, mouseWorld.y, 4);
parameters.context.fill();
if (this.lastDragTile) {
const startLine = this.lastDragTile.toWorldSpaceCenterOfTile();
const endLine = mouseTile.toWorldSpaceCenterOfTile();
const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile();
parameters.context.beginCircle(startLine.x, startLine.y, 8);
parameters.context.fill();
parameters.context.beginCircle(startLine.x, startLine.y, 8);
parameters.context.fill();
parameters.context.beginPath();
parameters.context.moveTo(startLine.x, startLine.y);
parameters.context.lineTo(midLine.x, midLine.y);
parameters.context.lineTo(endLine.x, endLine.y);
parameters.context.stroke();
parameters.context.beginPath();
parameters.context.moveTo(startLine.x, startLine.y);
parameters.context.lineTo(midLine.x, midLine.y);
parameters.context.lineTo(endLine.x, endLine.y);
parameters.context.stroke();
parameters.context.beginCircle(endLine.x, endLine.y, 5);
parameters.context.fill();
parameters.context.beginCircle(endLine.x, endLine.y, 5);
parameters.context.fill();
// Draw arrow
const arrowSprite = this.lockIndicatorSprites[anyObstacle ? "error" : this.root.currentLayer];
const path = this.computeDirectionLockPath();
for (let i = 0; i < path.length - 1; i += 1) {
const { rotation, tile } = path[i];
const worldPos = tile.toWorldSpaceCenterOfTile();
const angle = Math.radians(rotation);
// Draw arrow
const arrowSprite = this.lockIndicatorSprites[this.root.currentLayer];
const path = this.computeDirectionLockPath();
for (let i = 0; i < path.length - 1; i += 1) {
const { rotation, tile } = path[i];
const worldPos = tile.toWorldSpaceCenterOfTile();
const angle = Math.radians(rotation);
parameters.context.translate(worldPos.x, worldPos.y);
parameters.context.rotate(angle);
parameters.context.drawImage(
arrowSprite,
-6,
-globalConfig.halfTileSize -
clamp((this.root.time.realtimeNow() * 1.5) % 1.0, 0, 1) * 1 * globalConfig.tileSize +
globalConfig.halfTileSize -
6,
12,
12
);
parameters.context.rotate(-angle);
parameters.context.translate(-worldPos.x, -worldPos.y);
}
parameters.context.translate(worldPos.x, worldPos.y);
parameters.context.rotate(angle);
parameters.context.drawImage(
arrowSprite,
-6,
-globalConfig.halfTileSize -
clamp((this.root.time.realtimeNow() * 1.5) % 1.0, 0, 1) * 1 * globalConfig.tileSize +
globalConfig.halfTileSize -
6,
12,
12
);
parameters.context.rotate(-angle);
parameters.context.translate(-worldPos.x, -worldPos.y);
}
}

View File

@ -1,7 +1,19 @@
import { THIRDPARTY_URLS } from "../../../core/config";
import { DialogWithForm } from "../../../core/modal_dialog_elements";
import { FormElementInput, FormElementItemChooser } from "../../../core/modal_dialog_forms";
import { STOP_PROPAGATION } from "../../../core/signal";
import { fillInLinkIntoTranslation } from "../../../core/utils";
import { Vector } from "../../../core/vector";
import { T } from "../../../translations";
import { BaseItem } from "../../base_item";
import { enumMouseButton } from "../../camera";
import { Entity } from "../../entity";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../../items/color_item";
import { BaseHUDPart } from "../base_hud_part";
import trim from "trim";
import { enumColors } from "../../colors";
import { ShapeDefinition } from "../../shape_definition";
export class HUDConstantSignalEdit extends BaseHUDPart {
initialize() {
@ -23,7 +35,7 @@ export class HUDConstantSignalEdit extends BaseHUDPart {
const constantComp = contents.components.ConstantSignal;
if (constantComp) {
if (button === enumMouseButton.left) {
this.root.systemMgr.systems.constantSignal.editConstantSignal(contents, {
this.editConstantSignal(contents, {
deleteOnCancel: false,
});
return STOP_PROPAGATION;
@ -31,4 +43,171 @@ export class HUDConstantSignalEdit extends BaseHUDPart {
}
}
}
/**
* Asks the entity to enter a valid signal code
* @param {Entity} entity
* @param {object} param0
* @param {boolean=} param0.deleteOnCancel
*/
editConstantSignal(entity, { deleteOnCancel = true }) {
if (!entity.components.ConstantSignal) {
return;
}
// Ok, query, but also save the uid because it could get stale
const uid = entity.uid;
const signal = entity.components.ConstantSignal.signal;
const signalValueInput = new FormElementInput({
id: "signalValue",
label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer),
placeholder: "",
defaultValue: signal ? signal.getAsCopyableKey() : "",
validator: val => this.parseSignalCode(entity, val),
});
const items = [...Object.values(COLOR_ITEM_SINGLETONS)];
if (entity.components.WiredPins) {
items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON);
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
)
);
} else {
// producer which can produce virtually anything
const shapes = ["CuCuCuCu", "RuRuRuRu", "WuWuWuWu", "SuSuSuSu"];
items.unshift(
...shapes.reverse().map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key))
);
}
if (this.root.gameMode.hasHub()) {
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(
this.root.hubGoals.currentGoal.definition
)
);
}
if (this.root.hud.parts.pinnedShapes) {
items.push(
...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key =>
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)
)
);
}
const itemInput = new FormElementItemChooser({
id: "signalItem",
label: null,
items,
});
const dialog = new DialogWithForm({
app: this.root.app,
title: T.dialogs.editConstantProducer.title,
desc: T.dialogs.editSignal.descItems,
formElements: [itemInput, signalValueInput],
buttons: ["cancel:bad:escape", "ok:good:enter"],
closeButton: false,
});
this.root.hud.parts.dialogs.internalShowDialog(dialog);
// When confirmed, set the signal
const closeHandler = () => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
if (itemInput.chosenItem) {
constantComp.signal = itemInput.chosenItem;
} else {
constantComp.signal = this.parseSignalCode(entity, signalValueInput.getValue());
}
};
dialog.buttonSignals.ok.add(() => {
closeHandler();
});
dialog.valueChosen.add(() => {
dialog.closeRequested.dispatch();
closeHandler();
});
// When cancelled, destroy the entity again
if (deleteOnCancel) {
dialog.buttonSignals.cancel.add(() => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
this.root.logic.tryDeleteBuilding(entityRef);
});
}
}
/**
* Tries to parse a signal code
* @param {Entity} entity
* @param {string} code
* @returns {BaseItem}
*/
parseSignalCode(entity, code) {
if (!this.root || !this.root.shapeDefinitionMgr) {
// Stale reference
return null;
}
code = trim(code);
const codeLower = code.toLowerCase();
if (enumColors[codeLower]) {
return COLOR_ITEM_SINGLETONS[codeLower];
}
if (entity.components.WiredPins) {
if (code === "1" || codeLower === "true") {
return BOOL_TRUE_SINGLETON;
}
if (code === "0" || codeLower === "false") {
return BOOL_FALSE_SINGLETON;
}
}
if (ShapeDefinition.isValidShortKey(code)) {
return this.root.shapeDefinitionMgr.getShapeItemFromShortKey(code);
}
return null;
}
}

View File

@ -7,6 +7,9 @@ export const enumNotificationType = {
saved: "saved",
upgrade: "upgrade",
success: "success",
info: "info",
warning: "warning",
error: "error",
};
const notificationDuration = 3;
@ -17,14 +20,14 @@ export class HUDNotifications extends BaseHUDPart {
}
initialize() {
this.root.hud.signals.notification.add(this.onNotification, this);
this.root.hud.signals.notification.add(this.internalShowNotification, this);
/** @type {Array<{ element: HTMLElement, expireAt: number}>} */
this.notificationElements = [];
// Automatic notifications
this.root.signals.gameSaved.add(() =>
this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved)
this.internalShowNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved)
);
}
@ -32,7 +35,7 @@ export class HUDNotifications extends BaseHUDPart {
* @param {string} message
* @param {enumNotificationType} type
*/
onNotification(message, type) {
internalShowNotification(message, type) {
const element = makeDiv(this.element, null, ["notification", "type-" + type], message);
element.setAttribute("data-icon", "icons/notification_" + type + ".png");

View File

@ -53,10 +53,12 @@ export class GameLogic {
/**
* Checks if the given entity can be placed
* @param {Entity} entity
* @param {Vector=} offset Optional, move the entity by the given offset first
* @param {Object} param0
* @param {boolean=} param0.allowReplaceBuildings
* @param {Vector=} param0.offset Optional, move the entity by the given offset first
* @returns {boolean} true if the entity could be placed there
*/
checkCanPlaceEntity(entity, offset = null) {
checkCanPlaceEntity(entity, { allowReplaceBuildings = true, offset = null }) {
// Compute area of the building
const rect = entity.components.StaticMapEntity.getTileSpaceBounds();
if (offset) {
@ -71,7 +73,7 @@ export class GameLogic {
const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer);
if (otherEntity) {
const metaClass = otherEntity.components.StaticMapEntity.getMetaBuilding();
if (!metaClass.getIsReplaceable()) {
if (!allowReplaceBuildings || !metaClass.getIsReplaceable()) {
// This one is a direct blocker
return false;
}
@ -116,7 +118,7 @@ export class GameLogic {
rotationVariant,
variant,
});
if (this.checkCanPlaceEntity(entity)) {
if (this.checkCanPlaceEntity(entity, {})) {
this.freeEntityAreaBeforeBuild(entity);
this.root.map.placeStaticEntity(entity);
this.root.entityMgr.registerEntity(entity);

View File

@ -11,6 +11,7 @@ import { arrayBeltVariantToRotation, MetaBeltBuilding } from "../buildings/belt"
import { getCodeFromBuildingData } from "../building_codes";
import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { GameSystem } from "../game_system";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
import { defaultBuildingVariant } from "../meta_building";
@ -22,9 +23,9 @@ const logger = createLogger("belt");
/**
* Manages all belts
*/
export class BeltSystem extends GameSystemWithFilter {
export class BeltSystem extends GameSystem {
constructor(root) {
super(root, [BeltComponent]);
super(root);
/**
* @type {Object.<enumDirection, Array<AtlasSprite>>}
*/
@ -425,8 +426,10 @@ export class BeltSystem extends GameSystemWithFilter {
const result = [];
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const beltEntities = this.root.entityMgr.getAllWithComponent(BeltComponent);
for (let i = 0; i < beltEntities.length; ++i) {
const entity = beltEntities[i];
if (visitedUids.has(entity.uid)) {
continue;
}
@ -494,6 +497,10 @@ export class BeltSystem extends GameSystemWithFilter {
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) {
return;
}
// Limit speed to avoid belts going backwards
const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10);

View File

@ -16,7 +16,7 @@ import { BeltUnderlaysComponent, enumClippedBeltUnderlayType } from "../componen
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { GameSystem } from "../game_system";
import { MapChunkView } from "../map_chunk_view";
import { BELT_ANIM_COUNT } from "./belt";
@ -31,9 +31,9 @@ const enumUnderlayTypeToClipRect = {
[enumClippedBeltUnderlayType.bottomOnly]: new Rectangle(0, 0.5, 1, 0.5),
};
export class BeltUnderlaysSystem extends GameSystemWithFilter {
export class BeltUnderlaysSystem extends GameSystem {
constructor(root) {
super(root, [BeltUnderlaysComponent]);
super(root);
this.underlayBeltSprites = [];

View File

@ -5,10 +5,8 @@ import { ConstantSignalComponent } from "../components/constant_signal";
import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunk } from "../map_chunk";
import { GameRoot } from "../root";
export class ConstantProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) {
super(root, [ConstantSignalComponent, ItemProducerComponent]);
}

View File

@ -1,25 +1,16 @@
import trim from "trim";
import { THIRDPARTY_URLS } from "../../core/config";
import { DialogWithForm } from "../../core/modal_dialog_elements";
import { FormElementInput, FormElementItemChooser } from "../../core/modal_dialog_forms";
import { fillInLinkIntoTranslation } from "../../core/utils";
import { T } from "../../translations";
import { BaseItem } from "../base_item";
import { enumColors } from "../colors";
import { ConstantSignalComponent } from "../components/constant_signal";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeDefinition } from "../shape_definition";
export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ConstantSignalComponent]);
this.root.signals.entityManuallyPlaced.add(entity =>
this.editConstantSignal(entity, { deleteOnCancel: true })
);
this.root.signals.entityManuallyPlaced.add(entity => {
const editorHud = this.root.hud.parts.constantSignalEdit;
if (editorHud) {
editorHud.editConstantSignal(entity, { deleteOnCancel: true });
}
});
}
update() {
@ -34,171 +25,4 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
}
}
}
/**
* Asks the entity to enter a valid signal code
* @param {Entity} entity
* @param {object} param0
* @param {boolean=} param0.deleteOnCancel
*/
editConstantSignal(entity, { deleteOnCancel = true }) {
if (!entity.components.ConstantSignal) {
return;
}
// Ok, query, but also save the uid because it could get stale
const uid = entity.uid;
const signal = entity.components.ConstantSignal.signal;
const signalValueInput = new FormElementInput({
id: "signalValue",
label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer),
placeholder: "",
defaultValue: signal ? signal.getAsCopyableKey() : "",
validator: val => this.parseSignalCode(entity, val),
});
const items = [...Object.values(COLOR_ITEM_SINGLETONS)];
if (entity.components.WiredPins) {
items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON);
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
)
);
} else {
// producer which can produce virtually anything
const shapes = ["CuCuCuCu", "RuRuRuRu", "WuWuWuWu", "SuSuSuSu"];
items.unshift(
...shapes.reverse().map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key))
);
}
if (this.root.gameMode.hasHub()) {
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(
this.root.hubGoals.currentGoal.definition
)
);
}
if (this.root.hud.parts.pinnedShapes) {
items.push(
...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key =>
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)
)
);
}
const itemInput = new FormElementItemChooser({
id: "signalItem",
label: null,
items,
});
const dialog = new DialogWithForm({
app: this.root.app,
title: T.dialogs.editConstantProducer.title,
desc: T.dialogs.editSignal.descItems,
formElements: [itemInput, signalValueInput],
buttons: ["cancel:bad:escape", "ok:good:enter"],
closeButton: false,
});
this.root.hud.parts.dialogs.internalShowDialog(dialog);
// When confirmed, set the signal
const closeHandler = () => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
if (itemInput.chosenItem) {
constantComp.signal = itemInput.chosenItem;
} else {
constantComp.signal = this.parseSignalCode(entity, signalValueInput.getValue());
}
};
dialog.buttonSignals.ok.add(() => {
closeHandler();
});
dialog.valueChosen.add(() => {
dialog.closeRequested.dispatch();
closeHandler();
});
// When cancelled, destroy the entity again
if (deleteOnCancel) {
dialog.buttonSignals.cancel.add(() => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
}
const entityRef = this.root.entityMgr.findByUid(uid, false);
if (!entityRef) {
// outdated
return;
}
const constantComp = entityRef.components.ConstantSignal;
if (!constantComp) {
// no longer interesting
return;
}
this.root.logic.tryDeleteBuilding(entityRef);
});
}
}
/**
* Tries to parse a signal code
* @param {Entity} entity
* @param {string} code
* @returns {BaseItem}
*/
parseSignalCode(entity, code) {
if (!this.root || !this.root.shapeDefinitionMgr) {
// Stale reference
return null;
}
code = trim(code);
const codeLower = code.toLowerCase();
if (enumColors[codeLower]) {
return COLOR_ITEM_SINGLETONS[codeLower];
}
if (entity.components.WiredPins) {
if (code === "1" || codeLower === "true") {
return BOOL_TRUE_SINGLETON;
}
if (code === "0" || codeLower === "false") {
return BOOL_FALSE_SINGLETON;
}
}
if (ShapeDefinition.isValidShortKey(code)) {
return this.root.shapeDefinitionMgr.getShapeItemFromShortKey(code);
}
return null;
}
}

View File

@ -2,15 +2,14 @@ import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { BaseItem } from "../base_item";
import { enumColors } from "../colors";
import { DisplayComponent } from "../components/display";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { GameSystem } from "../game_system";
import { isTrueItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { MapChunkView } from "../map_chunk_view";
export class DisplaySystem extends GameSystemWithFilter {
export class DisplaySystem extends GameSystem {
constructor(root) {
super(root, [DisplayComponent]);
super(root);
/** @type {Object<string, import("../../core/draw_utils").AtlasSprite>} */
this.displaySprites = {};

View File

@ -5,10 +5,8 @@ import { Vector } from "../../core/vector";
import { GoalAcceptorComponent } from "../components/goal_acceptor";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunk } from "../map_chunk";
import { GameRoot } from "../root";
export class GoalAcceptorSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) {
super(root, [GoalAcceptorComponent]);

View File

@ -1,12 +1,7 @@
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter";
export class ItemProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) {
super(root, [ItemProducerComponent]);
this.item = null;

View File

@ -1,9 +1,8 @@
import { GameSystemWithFilter } from "../game_system_with_filter";
import { LeverComponent } from "../components/lever";
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { LeverComponent } from "../components/lever";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
export class LeverSystem extends GameSystemWithFilter {
constructor(root) {

View File

@ -1,9 +1,9 @@
import { GameSystemWithFilter } from "../game_system_with_filter";
import { StorageComponent } from "../components/storage";
import { DrawParameters } from "../../core/draw_parameters";
import { formatBigNumber, lerp } from "../../core/utils";
import { Loader } from "../../core/loader";
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
import { formatBigNumber, lerp } from "../../core/utils";
import { StorageComponent } from "../components/storage";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
export class StorageSystem extends GameSystemWithFilter {

View File

@ -21,6 +21,7 @@ import { enumWireType, enumWireVariant, WireComponent } from "../components/wire
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { WireTunnelComponent } from "../components/wire_tunnel";
import { Entity } from "../entity";
import { GameSystem } from "../game_system";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { isTruthyItem } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view";
@ -90,9 +91,9 @@ export class WireNetwork {
}
}
export class WireSystem extends GameSystemWithFilter {
export class WireSystem extends GameSystem {
constructor(root) {
super(root, [WireComponent]);
super(root);
/**
* @type {Object<enumWireVariant, Object<enumWireType, AtlasSprite>>}

View File

@ -18,6 +18,10 @@
"wires": {
"color": "rgb(74, 237, 134)",
"background": "rgba(74, 237, 134, 0.2)"
},
"error": {
"color": "rgb(255, 137, 137)",
"background": "rgba(255, 137, 137, 0.2)"
}
},

View File

@ -18,6 +18,10 @@
"wires": {
"color": "rgb(74, 237, 134)",
"background": "rgba(74, 237, 134, 0.2)"
},
"error": {
"color": "rgb(255, 137, 137)",
"background": "rgba(255, 137, 137, 0.2)"
}
},

View File

@ -25,6 +25,28 @@ import { KEYMAPPINGS } from "../game/key_action_mapper";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { THEMES } from "../game/theme";
import { ModMetaBuilding } from "./mod_meta_building";
import { BaseHUDPart } from "../game/hud/base_hud_part";
/**
* @typedef {{new(...args: any[]): any, prototype: any}} constructable
*/
/**
* @template {(...args: any[]) => any} F
* @template P
* @typedef {(...args: [P, Parameters<F>]) => ReturnType<F>} beforePrams IMPORTANT: this puts the original parameters into an array
*/
/**
* @template {(...args: any[]) => any} F
* @template P
* @typedef {(...args: [...Parameters<F>, P]) => ReturnType<F>} afterPrams
*/
/**
* @template {(...args: any[]) => any} F
* @typedef {(...args: [...Parameters<F>, ...any]) => ReturnType<F>} extendsPrams
*/
export class ModInterface {
/**
@ -37,7 +59,7 @@ export class ModInterface {
registerCss(cssString) {
// Preprocess css
cssString = cssString.replace(/\$scaled\(([^\)]*)\)/gim, (substr, expression) => {
cssString = cssString.replace(/\$scaled\(([^)]*)\)/gim, (substr, expression) => {
return "calc((" + expression + ") * var(--ui-scale))";
});
const element = document.createElement("style");
@ -345,6 +367,14 @@ export class ModInterface {
});
}
/**
* Registers a new state class, should be a GameState derived class
* @param {typeof GameState} stateClass
*/
registerGameState(stateClass) {
this.modLoader.app.stateMgr.register(stateClass);
}
/**
* @param {object} param0
* @param {"regular"|"wires"} param0.toolbar
@ -363,27 +393,57 @@ export class ModInterface {
}
/**
* Patches a method on a given object
* Patches a method on a given class
* @template {constructable} C the class
* @template {C["prototype"]} P the prototype of said class
* @template {keyof P} M the name of the method we are overriding
* @template {extendsPrams<P[M]>} O the method that will override the old one
* @param {C} classHandle
* @param {M} methodName
* @param {beforePrams<O, P[M]>} override
*/
replaceMethod(classHandle, methodName, override) {
const oldMethod = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () {
//@ts-ignore This is true I just cant tell it that arguments will be Arguments<O>
return override.call(this, oldMethod.bind(this), arguments);
};
}
/**
* Runs before a method on a given class
* @template {constructable} C the class
* @template {C["prototype"]} P the prototype of said class
* @template {keyof P} M the name of the method we are overriding
* @template {extendsPrams<P[M]>} O the method that will run before the old one
* @param {C} classHandle
* @param {M} methodName
* @param {O} executeBefore
*/
runBeforeMethod(classHandle, methodName, executeBefore) {
const oldHandle = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () {
//@ts-ignore Same as above
executeBefore.apply(this, arguments);
return oldHandle.apply(this, arguments);
};
}
/**
* Runs after a method on a given class
* @template {constructable} C the class
* @template {C["prototype"]} P the prototype of said class
* @template {keyof P} M the name of the method we are overriding
* @template {extendsPrams<P[M]>} O the method that will run before the old one
* @param {C} classHandle
* @param {M} methodName
* @param {O} executeAfter
*/
runAfterMethod(classHandle, methodName, executeAfter) {
const oldHandle = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () {
const returnValue = oldHandle.apply(this, arguments);
//@ts-ignore
executeAfter.apply(this, arguments);
return returnValue;
};
@ -416,4 +476,15 @@ export class ModInterface {
extendClass(classHandle, extender) {
this.extendObject(classHandle.prototype, extender);
}
/**
*
* @param {string} id
* @param {new (...args) => BaseHUDPart} element
*/
registerHudElement(id, element) {
this.modLoader.signals.hudInitializer.add(root => {
root.hud.parts[id] = new element(root);
});
}
}

View File

@ -19,6 +19,8 @@ export const MOD_SIGNALS = {
hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
hudInitializer: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()),
gameInitialized: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()),
gameLoadingStageEntered: /** @type {TypedSignal<[InGameState, string]>} */ (new Signal()),

View File

@ -103,21 +103,25 @@ export class ModLoader {
mods = await ipcRenderer.invoke("get-mods");
}
if (G_IS_DEV && globalConfig.debug.externalModUrl) {
const response = await fetch(globalConfig.debug.externalModUrl, {
method: "GET",
});
if (response.status !== 200) {
throw new Error(
"Failed to load " +
globalConfig.debug.externalModUrl +
": " +
response.status +
" " +
response.statusText
);
let modURLs = Array.isArray(globalConfig.debug.externalModUrl) ?
globalConfig.debug.externalModUrl : [globalConfig.debug.externalModUrl];
for(let i = 0; i < modURLs.length; i++) {
const response = await fetch(modURLs[i], {
method: "GET",
});
if (response.status !== 200) {
throw new Error(
"Failed to load " +
modURLs[i] +
": " +
response.status +
" " +
response.statusText
);
}
mods.push(await response.text());
}
mods.push(await response.text());
}
window.$shapez_registerMod = (modClass, meta) => {