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:
commit
f3a65812a9
52
mod_examples/README.md
Normal file
52
mod_examples/README.md
Normal 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 |
|
||||
@ -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
27
mod_examples/custom_keybinding.js
Normal file
27
mod_examples/custom_keybinding.js
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,8 @@ class Mod extends shapez.Mod {
|
||||
0
|
||||
);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.stroke();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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
41
mod_examples/modify_ui.js
Normal 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;
|
||||
}
|
||||
|
||||
`);
|
||||
}
|
||||
}
|
||||
312
mod_examples/notification_blocks.js
Normal file
312
mod_examples/notification_blocks.js
Normal file
File diff suppressed because one or more lines are too long
BIN
res/ui/icons/notification_error.png
Normal file
BIN
res/ui/icons/notification_error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
res/ui/icons/notification_info.png
Normal file
BIN
res/ui/icons/notification_info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
res/ui/icons/notification_warning.png
Normal file
BIN
res/ui/icons/notification_warning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@ -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 */
|
||||
|
||||
@ -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",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 */
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = {};
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>>}
|
||||
|
||||
@ -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)"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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)"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()),
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user