1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-15 19:21:49 +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 // Only allow placing an entity when there is enough currency
this.modInterface.replaceMethod(shapez.GameLogic, "checkCanPlaceEntity", function ( this.modInterface.replaceMethod(shapez.GameLogic, "checkCanPlaceEntity", function (
$original, $original,
[entity, offset] [entity, options]
) { ) {
const storedCurrency = this.root.hubGoals.storedShapes[CURRENCY] || 0; 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 // 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 0
); );
context.closePath(); context.closePath();
context.fill();
context.stroke();
}, },
}); });

View File

@ -40,6 +40,10 @@ const RESOURCES = {
color: "rgb(74, 237, 134)", color: "rgb(74, 237, 134)",
background: "rgba(74, 237, 134, 0.2)", 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)", 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; 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 { @each $icon in $icons {
[data-icon="icons/#{$icon}.png"] { [data-icon="icons/#{$icon}.png"] {
/* @load-async */ /* @load-async */

View File

@ -4,6 +4,7 @@ export const CHANGELOG = [
date: "unreleased", date: "unreleased",
entries: [ entries: [
"This version adds an official modloader! You can now load mods by placing it in the mods/ folder of the game.", "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 // Which dpi the assets have
assetsDpi: 192 / 32, assetsDpi: 192 / 32,
assetsSharpness: 1.5, assetsSharpness: 1.5,
shapesSharpness: 1.4, shapesSharpness: 1.3,
// Achievements // Achievements
achievementSliceDuration: 10, // Seconds achievementSliceDuration: 10, // Seconds
@ -58,9 +58,11 @@ export const globalConfig = {
// Map // Map
mapChunkSize: 16, mapChunkSize: 16,
chunkAggregateSize: 4, chunkAggregateSize: 4,
mapChunkOverviewMinZoom: 0.9, mapChunkOverviewMinZoom: 0,
mapChunkWorldSize: null, // COMPUTED mapChunkWorldSize: null, // COMPUTED
maxBeltShapeBundleSize: 20,
// Belt speeds // Belt speeds
// NOTICE: Update webpack.production.config too! // NOTICE: Update webpack.production.config too!
beltSpeedItemsPerSecond: 2, beltSpeedItemsPerSecond: 2,

View File

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

View File

@ -15,3 +15,20 @@ export function setGlobalApp(app) {
assert(!GLOBAL_APP, "Create application twice!"); assert(!GLOBAL_APP, "Create application twice!");
GLOBAL_APP = app; 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 { globalConfig } from "../core/config";
import { smoothenDpi } from "../core/dpi_manager";
import { DrawParameters } from "../core/draw_parameters"; import { DrawParameters } from "../core/draw_parameters";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { Rectangle } from "../core/rectangle"; import { Rectangle } from "../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../core/sprites";
import { clamp, epsilonCompare, round4Digits } from "../core/utils"; import { clamp, epsilonCompare, round4Digits } from "../core/utils";
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector"; import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
@ -1349,6 +1351,12 @@ export class BeltPath extends BasicSerializableObject {
let trackPos = 0.0; let trackPos = 0.0;
/**
* @type {Array<[Vector, BaseItem]>}
*/
let drawStack = [];
let drawStackProp = "";
// Iterate whole track and check items // Iterate whole track and check items
for (let i = 0; i < this.entityPath.length; ++i) { for (let i = 0; i < this.entityPath.length; ++i) {
const entity = this.entityPath[i]; const entity = this.entityPath[i];
@ -1368,25 +1376,185 @@ export class BeltPath extends BasicSerializableObject {
const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile(); const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile();
const distanceAndItem = this.items[currentItemIndex]; const distanceAndItem = this.items[currentItemIndex];
const item = distanceAndItem[1 /* item */];
const nextItemDistance = distanceAndItem[0 /* nextDistance */];
distanceAndItem[1 /* item */].drawItemCenteredClipped( if (
!parameters.visibleRect.containsCircle(
worldPos.x, worldPos.x,
worldPos.y, worldPos.y,
parameters,
globalConfig.defaultItemDiameter 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 // Check for the next item
currentItemPos += distanceAndItem[0 /* nextDistance */]; currentItemPos += nextItemDistance;
++currentItemIndex; ++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) { if (currentItemIndex >= this.items.length) {
// We rendered all items // We rendered all items
this.drawDrawStack(drawStack, parameters, drawStackProp);
return; return;
} }
} }
trackPos += beltLength; 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(); const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y); 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; parameters.context.globalAlpha = 0.3;
} else { } else {
parameters.context.globalAlpha = 1; parameters.context.globalAlpha = 1;
@ -131,7 +131,7 @@ export class Blueprint {
for (let i = 0; i < this.entities.length; ++i) { for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i]; const entity = this.entities[i];
if (root.logic.checkCanPlaceEntity(entity, tile)) { if (root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
anyPlaceable = true; anyPlaceable = true;
} }
} }
@ -160,7 +160,7 @@ export class Blueprint {
let count = 0; let count = 0;
for (let i = 0; i < this.entities.length; ++i) { for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i]; const entity = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, tile)) { if (!root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
continue; 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, // 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 // processors etc. In phase 2 we propagate it through the wires network
add("logicGate", LogicGateSystem); add("logicGate", LogicGateSystem);
add("beltReader", BeltReaderSystem); add("beltReader", BeltReaderSystem);
add("display", DisplaySystem); add("display", DisplaySystem);

View File

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

View File

@ -61,7 +61,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
this.currentInterpolatedCornerTile = new Vector(); this.currentInterpolatedCornerTile = new Vector();
this.lockIndicatorSprites = {}; this.lockIndicatorSprites = {};
layers.forEach(layer => { [...layers, "error"].forEach(layer => {
this.lockIndicatorSprites[layer] = this.makeLockIndicatorSprite(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 * Makes the lock indicator sprite for the given layer
* @param {Layer} layer * @param {string} layer
*/ */
makeLockIndicatorSprite(layer) { makeLockIndicatorSprite(layer) {
const dims = 48; const dims = 48;
@ -358,7 +358,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
rotationVariant rotationVariant
); );
const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity); const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity, {});
// Fade in / out // Fade in / out
parameters.context.lineWidth = 1; 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 * @param {DrawParameters} parameters
*/ */
@ -407,19 +443,38 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
return; return;
} }
const mouseWorld = this.root.camera.screenToWorld(mousePosition); const applyStyles = look => {
const mouseTile = mouseWorld.toTileSpace(); parameters.context.fillStyle = THEME.map.directionLock[look].color;
parameters.context.fillStyle = THEME.map.directionLock[this.root.currentLayer].color; parameters.context.strokeStyle = THEME.map.directionLock[look].background;
parameters.context.strokeStyle = THEME.map.directionLock[this.root.currentLayer].background;
parameters.context.lineWidth = 10; 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.beginCircle(mouseWorld.x, mouseWorld.y, 4);
parameters.context.fill(); parameters.context.fill();
return;
}
if (this.lastDragTile) { const mouseWorld = this.root.camera.screenToWorld(mousePosition);
const mouseTile = mouseWorld.toTileSpace();
const startLine = this.lastDragTile.toWorldSpaceCenterOfTile(); const startLine = this.lastDragTile.toWorldSpaceCenterOfTile();
const endLine = mouseTile.toWorldSpaceCenterOfTile(); const endLine = mouseTile.toWorldSpaceCenterOfTile();
const midLine = this.currentDirectionLockCorner.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();
parameters.context.beginCircle(startLine.x, startLine.y, 8); parameters.context.beginCircle(startLine.x, startLine.y, 8);
parameters.context.fill(); parameters.context.fill();
@ -434,7 +489,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
parameters.context.fill(); parameters.context.fill();
// Draw arrow // Draw arrow
const arrowSprite = this.lockIndicatorSprites[this.root.currentLayer]; const arrowSprite = this.lockIndicatorSprites[anyObstacle ? "error" : this.root.currentLayer];
const path = this.computeDirectionLockPath(); const path = this.computeDirectionLockPath();
for (let i = 0; i < path.length - 1; i += 1) { for (let i = 0; i < path.length - 1; i += 1) {
const { rotation, tile } = path[i]; const { rotation, tile } = path[i];
@ -457,7 +512,6 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
parameters.context.translate(-worldPos.x, -worldPos.y); parameters.context.translate(-worldPos.x, -worldPos.y);
} }
} }
}
/** /**
* @param {DrawParameters} parameters * @param {DrawParameters} parameters

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 { STOP_PROPAGATION } from "../../../core/signal";
import { fillInLinkIntoTranslation } from "../../../core/utils";
import { Vector } from "../../../core/vector"; import { Vector } from "../../../core/vector";
import { T } from "../../../translations";
import { BaseItem } from "../../base_item";
import { enumMouseButton } from "../../camera"; 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 { BaseHUDPart } from "../base_hud_part";
import trim from "trim";
import { enumColors } from "../../colors";
import { ShapeDefinition } from "../../shape_definition";
export class HUDConstantSignalEdit extends BaseHUDPart { export class HUDConstantSignalEdit extends BaseHUDPart {
initialize() { initialize() {
@ -23,7 +35,7 @@ export class HUDConstantSignalEdit extends BaseHUDPart {
const constantComp = contents.components.ConstantSignal; const constantComp = contents.components.ConstantSignal;
if (constantComp) { if (constantComp) {
if (button === enumMouseButton.left) { if (button === enumMouseButton.left) {
this.root.systemMgr.systems.constantSignal.editConstantSignal(contents, { this.editConstantSignal(contents, {
deleteOnCancel: false, deleteOnCancel: false,
}); });
return STOP_PROPAGATION; 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", saved: "saved",
upgrade: "upgrade", upgrade: "upgrade",
success: "success", success: "success",
info: "info",
warning: "warning",
error: "error",
}; };
const notificationDuration = 3; const notificationDuration = 3;
@ -17,14 +20,14 @@ export class HUDNotifications extends BaseHUDPart {
} }
initialize() { 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}>} */ /** @type {Array<{ element: HTMLElement, expireAt: number}>} */
this.notificationElements = []; this.notificationElements = [];
// Automatic notifications // Automatic notifications
this.root.signals.gameSaved.add(() => 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 {string} message
* @param {enumNotificationType} type * @param {enumNotificationType} type
*/ */
onNotification(message, type) { internalShowNotification(message, type) {
const element = makeDiv(this.element, null, ["notification", "type-" + type], message); const element = makeDiv(this.element, null, ["notification", "type-" + type], message);
element.setAttribute("data-icon", "icons/notification_" + type + ".png"); 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 * Checks if the given entity can be placed
* @param {Entity} entity * @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 * @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 // Compute area of the building
const rect = entity.components.StaticMapEntity.getTileSpaceBounds(); const rect = entity.components.StaticMapEntity.getTileSpaceBounds();
if (offset) { if (offset) {
@ -71,7 +73,7 @@ export class GameLogic {
const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer); const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer);
if (otherEntity) { if (otherEntity) {
const metaClass = otherEntity.components.StaticMapEntity.getMetaBuilding(); const metaClass = otherEntity.components.StaticMapEntity.getMetaBuilding();
if (!metaClass.getIsReplaceable()) { if (!allowReplaceBuildings || !metaClass.getIsReplaceable()) {
// This one is a direct blocker // This one is a direct blocker
return false; return false;
} }
@ -116,7 +118,7 @@ export class GameLogic {
rotationVariant, rotationVariant,
variant, variant,
}); });
if (this.checkCanPlaceEntity(entity)) { if (this.checkCanPlaceEntity(entity, {})) {
this.freeEntityAreaBeforeBuild(entity); this.freeEntityAreaBeforeBuild(entity);
this.root.map.placeStaticEntity(entity); this.root.map.placeStaticEntity(entity);
this.root.entityMgr.registerEntity(entity); this.root.entityMgr.registerEntity(entity);

View File

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

View File

@ -5,10 +5,8 @@ import { ConstantSignalComponent } from "../components/constant_signal";
import { ItemProducerComponent } from "../components/item_producer"; import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunk } from "../map_chunk"; import { MapChunk } from "../map_chunk";
import { GameRoot } from "../root";
export class ConstantProducerSystem extends GameSystemWithFilter { export class ConstantProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) { constructor(root) {
super(root, [ConstantSignalComponent, ItemProducerComponent]); 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 { ConstantSignalComponent } from "../components/constant_signal";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; 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 { export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
super(root, [ConstantSignalComponent]); super(root, [ConstantSignalComponent]);
this.root.signals.entityManuallyPlaced.add(entity => this.root.signals.entityManuallyPlaced.add(entity => {
this.editConstantSignal(entity, { deleteOnCancel: true }) const editorHud = this.root.hud.parts.constantSignalEdit;
); if (editorHud) {
editorHud.editConstantSignal(entity, { deleteOnCancel: true });
}
});
} }
update() { 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 { Loader } from "../../core/loader";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { enumColors } from "../colors"; import { enumColors } from "../colors";
import { DisplayComponent } from "../components/display"; import { GameSystem } from "../game_system";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { isTrueItem } from "../items/boolean_item"; import { isTrueItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { MapChunkView } from "../map_chunk_view"; import { MapChunkView } from "../map_chunk_view";
export class DisplaySystem extends GameSystemWithFilter { export class DisplaySystem extends GameSystem {
constructor(root) { constructor(root) {
super(root, [DisplayComponent]); super(root);
/** @type {Object<string, import("../../core/draw_utils").AtlasSprite>} */ /** @type {Object<string, import("../../core/draw_utils").AtlasSprite>} */
this.displaySprites = {}; this.displaySprites = {};

View File

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

View File

@ -1,12 +1,7 @@
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { ItemProducerComponent } from "../components/item_producer"; import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
export class ItemProducerSystem extends GameSystemWithFilter { export class ItemProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) { constructor(root) {
super(root, [ItemProducerComponent]); super(root, [ItemProducerComponent]);
this.item = null; 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 { 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 { export class LeverSystem extends GameSystemWithFilter {
constructor(root) { 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 { DrawParameters } from "../../core/draw_parameters";
import { formatBigNumber, lerp } from "../../core/utils";
import { Loader } from "../../core/loader"; 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"; import { MapChunkView } from "../map_chunk_view";
export class StorageSystem extends GameSystemWithFilter { 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 { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { WireTunnelComponent } from "../components/wire_tunnel"; import { WireTunnelComponent } from "../components/wire_tunnel";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { GameSystem } from "../game_system";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { isTruthyItem } from "../items/boolean_item"; import { isTruthyItem } from "../items/boolean_item";
import { MapChunkView } from "../map_chunk_view"; 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) { constructor(root) {
super(root, [WireComponent]); super(root);
/** /**
* @type {Object<enumWireVariant, Object<enumWireType, AtlasSprite>>} * @type {Object<enumWireVariant, Object<enumWireType, AtlasSprite>>}

View File

@ -18,6 +18,10 @@
"wires": { "wires": {
"color": "rgb(74, 237, 134)", "color": "rgb(74, 237, 134)",
"background": "rgba(74, 237, 134, 0.2)" "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": { "wires": {
"color": "rgb(74, 237, 134)", "color": "rgb(74, 237, 134)",
"background": "rgba(74, 237, 134, 0.2)" "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 { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { THEMES } from "../game/theme"; import { THEMES } from "../game/theme";
import { ModMetaBuilding } from "./mod_meta_building"; 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 { export class ModInterface {
/** /**
@ -37,7 +59,7 @@ export class ModInterface {
registerCss(cssString) { registerCss(cssString) {
// Preprocess css // Preprocess css
cssString = cssString.replace(/\$scaled\(([^\)]*)\)/gim, (substr, expression) => { cssString = cssString.replace(/\$scaled\(([^)]*)\)/gim, (substr, expression) => {
return "calc((" + expression + ") * var(--ui-scale))"; return "calc((" + expression + ") * var(--ui-scale))";
}); });
const element = document.createElement("style"); 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 {object} param0
* @param {"regular"|"wires"} param0.toolbar * @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) { replaceMethod(classHandle, methodName, override) {
const oldMethod = classHandle.prototype[methodName]; const oldMethod = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () { 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); 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) { runBeforeMethod(classHandle, methodName, executeBefore) {
const oldHandle = classHandle.prototype[methodName]; const oldHandle = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () { classHandle.prototype[methodName] = function () {
//@ts-ignore Same as above
executeBefore.apply(this, arguments); executeBefore.apply(this, arguments);
return oldHandle.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) { runAfterMethod(classHandle, methodName, executeAfter) {
const oldHandle = classHandle.prototype[methodName]; const oldHandle = classHandle.prototype[methodName];
classHandle.prototype[methodName] = function () { classHandle.prototype[methodName] = function () {
const returnValue = oldHandle.apply(this, arguments); const returnValue = oldHandle.apply(this, arguments);
//@ts-ignore
executeAfter.apply(this, arguments); executeAfter.apply(this, arguments);
return returnValue; return returnValue;
}; };
@ -416,4 +476,15 @@ export class ModInterface {
extendClass(classHandle, extender) { extendClass(classHandle, extender) {
this.extendObject(classHandle.prototype, 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()), hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
hudInitializer: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()),
gameInitialized: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), gameInitialized: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()),
gameLoadingStageEntered: /** @type {TypedSignal<[InGameState, string]>} */ (new Signal()), gameLoadingStageEntered: /** @type {TypedSignal<[InGameState, string]>} */ (new Signal()),

View File

@ -103,22 +103,26 @@ export class ModLoader {
mods = await ipcRenderer.invoke("get-mods"); mods = await ipcRenderer.invoke("get-mods");
} }
if (G_IS_DEV && globalConfig.debug.externalModUrl) { if (G_IS_DEV && globalConfig.debug.externalModUrl) {
const response = await fetch(globalConfig.debug.externalModUrl, { 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", method: "GET",
}); });
if (response.status !== 200) { if (response.status !== 200) {
throw new Error( throw new Error(
"Failed to load " + "Failed to load " +
globalConfig.debug.externalModUrl + modURLs[i] +
": " + ": " +
response.status + response.status +
" " + " " +
response.statusText response.statusText
); );
} }
mods.push(await response.text()); mods.push(await response.text());
} }
}
window.$shapez_registerMod = (modClass, meta) => { window.$shapez_registerMod = (modClass, meta) => {
if (this.initialized) { if (this.initialized) {