1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-13 10:11:50 +00:00

Merge branch 'modloader' into processing-refactor

This commit is contained in:
Sense101 2022-01-23 16:49:05 +00:00
commit fb0a66f5ca
41 changed files with 253 additions and 156 deletions

View File

@ -361,7 +361,8 @@ ipcMain.handle("get-mods", async () => {
if (externalMod) { if (externalMod) {
console.log("Adding external mod source:", externalMod); console.log("Adding external mod source:", externalMod);
modFiles.push(externalMod); const externalModPaths = externalMod.split(",");
modFiles = modFiles.concat(externalModPaths);
} }
if (modFiles.length > 0 && !isDev) { if (modFiles.length > 0 && !isDev) {

View File

@ -5,7 +5,7 @@
Currently there are two options to develop mods for shapez.io: 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 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. 2. Using the [create-shapezio-mod](https://www.npmjs.com/package/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. 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.
@ -41,6 +41,7 @@ To get into shapez.io modding, I highly recommend checking out all of the exampl
| [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_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 | | [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 | | [pasting.js](pasting.js) | Shows a dialog when pasting text in the game | Listening to paste events |
| [sandbox.js](sandbox.js) | Makes blueprints free and always unlocked | Overriding builtin methods |
### Advanced Examples ### Advanced Examples
@ -50,3 +51,7 @@ To get into shapez.io modding, I highly recommend checking out all of the exampl
| [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 | | [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 | | [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 | | [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 |
### Creating new sprites
If you want to add new buildings and create sprites for them, you can download the original Photoshop PSD files here: https://static.shapez.io/building-psds.zip

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "add-building-basic", id: "add-building-basic",
description: "Shows how to add a new basic building", description: "Shows how to add a new basic building",
minimumGameVersion: ">=1.5.0",
}; };
class MetaDemoModBuilding extends shapez.ModMetaBuilding { class MetaDemoModBuilding extends shapez.ModMetaBuilding {

View File

@ -7,6 +7,7 @@ const METADATA = {
id: "add-building-extended", id: "add-building-extended",
description: description:
"Shows how to add a new building with logic, in this case it flips/mirrors shapez from top to down", "Shows how to add a new building with logic, in this case it flips/mirrors shapez from top to down",
minimumGameVersion: ">=1.5.0",
}; };
// Declare a new type of item processor // Declare a new type of item processor
@ -80,7 +81,7 @@ class MetaModFlipperBuilding extends shapez.ModMetaBuilding {
slots: [ slots: [
{ {
pos: new shapez.Vector(0, 0), pos: new shapez.Vector(0, 0),
directions: [shapez.enumDirection.bottom], direction: shapez.enumDirection.bottom,
filter: "shape", filter: "shape",
}, },
], ],

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "base", id: "base",
description: "The most basic mod", description: "The most basic mod",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "patch-methods", id: "patch-methods",
description: "Shows how to patch existing methods to change the game by making the belts cost shapes", description: "Shows how to patch existing methods to change the game by making the belts cost shapes",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "class-extensions", id: "class-extensions",
description: "Shows how to extend builtin classes", description: "Shows how to extend builtin classes",
minimumGameVersion: ">=1.5.0",
}; };
const BeltExtension = ({ $super, $old }) => ({ const BeltExtension = ({ $super, $old }) => ({

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "custom-css", id: "custom-css",
description: "Shows how to add custom css", description: "Shows how to add custom css",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "base", id: "base",
description: "Displays an indicator on every item processing building when its working", description: "Displays an indicator on every item processing building when its working",
minimumGameVersion: ">=1.5.0",
}; };
class ItemProcessorStatusGameSystem extends shapez.GameSystem { class ItemProcessorStatusGameSystem extends shapez.GameSystem {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "base", id: "base",
description: "Shows how to add a new keybinding", description: "Shows how to add a new keybinding",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "custom-sub-shapes", id: "custom-sub-shapes",
description: "Shows how to add custom sub shapes", description: "Shows how to add custom sub shapes",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "custom-theme", id: "custom-theme",
description: "Shows how to add a custom game theme", description: "Shows how to add a custom game theme",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "mod-settings", id: "mod-settings",
description: "Shows how to add settings to your mod", description: "Shows how to add settings to your mod",
minimumGameVersion: ">=1.5.0",
settings: { settings: {
timesLaunched: 0, timesLaunched: 0,
@ -23,7 +24,7 @@ class Mod extends shapez.Mod {
if (state instanceof shapez.MainMenuState) { if (state instanceof shapez.MainMenuState) {
this.dialogs.showInfo( this.dialogs.showInfo(
"Welcome back", "Welcome back",
"You have launched this mod " + this.settings.timesLaunched + " times" `You have launched this mod ${this.settings.timesLaunched} times`
); );
} }
}); });

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "modify-existing-building", id: "modify-existing-building",
description: "Shows how to modify an existing building", description: "Shows how to modify an existing building",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "modify-theme", id: "modify-theme",
description: "Shows how to modify builtin themes", description: "Shows how to modify builtin themes",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "modify-ui", id: "modify-ui",
description: "Shows how to modify a builtin game state, in this case the main menu", description: "Shows how to modify a builtin game state, in this case the main menu",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "new-item-type", id: "new-item-type",
description: "Shows how to add a new item type (fluid)", description: "Shows how to add a new item type (fluid)",
minimumGameVersion: ">=1.5.0",
}; };
// Define which fluid types there are // Define which fluid types there are
@ -75,7 +76,7 @@ class FluidItem extends shapez.BaseItem {
*/ */
drawFullSizeOnCanvas(context, size) { drawFullSizeOnCanvas(context, size) {
if (!this.cachedSprite) { if (!this.cachedSprite) {
this.cachedSprite = shapez.Loader.getSprite("sprites/fluids/" + this.fluidType + ".png"); this.cachedSprite = shapez.Loader.getSprite(`sprites/fluids/${this.fluidType}.png`);
} }
this.cachedSprite.drawCentered(context, size / 2, size / 2, size); this.cachedSprite.drawCentered(context, size / 2, size / 2, size);
} }
@ -89,7 +90,7 @@ class FluidItem extends shapez.BaseItem {
drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
const realDiameter = diameter * 0.6; const realDiameter = diameter * 0.6;
if (!this.cachedSprite) { if (!this.cachedSprite) {
this.cachedSprite = shapez.Loader.getSprite("sprites/fluids/" + this.fluidType + ".png"); this.cachedSprite = shapez.Loader.getSprite(`sprites/fluids/${this.fluidType}.png`);
} }
this.cachedSprite.drawCachedCentered(parameters, x, y, realDiameter); this.cachedSprite.drawCachedCentered(parameters, x, y, realDiameter);
} }

View File

@ -7,6 +7,8 @@ const METADATA = {
id: "notification-blocks", id: "notification-blocks",
description: description:
"Adds a new building to the wires layer, 'Notification Blocks' which show a custom notification when they get a truthy signal.", "Adds a new building to the wires layer, 'Notification Blocks' which show a custom notification when they get a truthy signal.",
minimumGameVersion: ">=1.5.0",
}; };
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "pasting", id: "pasting",
description: "Shows how to properly receive paste events ingame", description: "Shows how to properly receive paste events ingame",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {
@ -15,7 +16,7 @@ class Mod extends shapez.Mod {
event.preventDefault(); event.preventDefault();
const data = event.clipboardData.getData("text"); const data = event.clipboardData.getData("text");
this.dialogs.showInfo("Pasted", "You pasted: '" + data + "'"); this.dialogs.showInfo("Pasted", `You pasted: '${data}'`);
}); });
}); });
} }

View File

@ -6,13 +6,14 @@ const METADATA = {
version: "1", version: "1",
id: "replace-builtin-sprites", id: "replace-builtin-sprites",
description: "Shows how to replace builtin sprites", description: "Shows how to replace builtin sprites",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {
init() { init() {
// Replace a builtin sprite // Replace a builtin sprite
["red", "green", "blue", "yellow", "purple", "cyan", "white"].forEach(color => { ["red", "green", "blue", "yellow", "purple", "cyan", "white"].forEach(color => {
this.modInterface.registerSprite("sprites/colors/" + color + ".png", RESOURCES[color + ".png"]); this.modInterface.registerSprite(`sprites/colors/${color}.png`, RESOURCES[color + ".png"]);
}); });
} }
} }

21
mod_examples/sandbox.js Normal file
View File

@ -0,0 +1,21 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Sandbox",
version: "1",
id: "sandbox",
description: "Blueprints are always unlocked and cost no money, also all buildings are unlocked",
minimumGameVersion: ">=1.5.0",
};
class Mod extends shapez.Mod {
init() {
this.modInterface.replaceMethod(shapez.Blueprint, "getCost", function () {
return 0;
});
this.modInterface.replaceMethod(shapez.HubGoals, "isRewardUnlocked", function () {
return true;
});
}
}

View File

@ -6,6 +6,7 @@ const METADATA = {
version: "1", version: "1",
id: "translations", id: "translations",
description: "Shows how to add and modify translations", description: "Shows how to add and modify translations",
minimumGameVersion: ">=1.5.0",
}; };
class Mod extends shapez.Mod { class Mod extends shapez.Mod {

View File

@ -7,6 +7,8 @@ const METADATA = {
id: "usage-statistics", id: "usage-statistics",
description: description:
"Shows how to add a new component to the game, how to save additional data and how to add custom logic and drawings", "Shows how to add a new component to the game, how to save additional data and how to add custom logic and drawings",
minimumGameVersion: ">=1.5.0",
}; };
/** /**

View File

@ -56,6 +56,7 @@
"promise-polyfill": "^8.1.0", "promise-polyfill": "^8.1.0",
"query-string": "^6.8.1", "query-string": "^6.8.1",
"rusha": "^0.8.13", "rusha": "^0.8.13",
"semver": "^7.3.5",
"serialize-error": "^3.0.0", "serialize-error": "^3.0.0",
"strictdom": "^1.0.1", "strictdom": "^1.0.1",
"string-replace-webpack-plugin": "^0.1.3", "string-replace-webpack-plugin": "^0.1.3",

View File

@ -199,10 +199,6 @@
transform: translateX(50%) rotate(-7deg) scale(1.1); transform: translateX(50%) rotate(-7deg) scale(1.1);
} }
} }
@include DarkThemeOverride {
color: $colorBlueBright;
}
} }
} }
@ -326,6 +322,7 @@
/* @load-async */ /* @load-async */
background-image: uiResource("icons/edit_key.png") !important; background-image: uiResource("icons/edit_key.png") !important;
} }
@include DarkThemeInvert;
} }
} }
@ -354,13 +351,14 @@
box-sizing: border-box; box-sizing: border-box;
@include PlainText; @include PlainText;
@include S(margin-bottom, 5px); @include S(margin-bottom, 5px);
display: grid; display: flex;
grid-template-columns: 1fr auto auto; flex-direction: column;
@include S(grid-gap, 5px);
.author, .author,
.version { .version {
@include SuperSmallText; @include SuperSmallText;
align-self: end;
opacity: 0.4;
} }
.name { .name {
overflow: hidden; overflow: hidden;
@ -457,19 +455,19 @@
.newGameButton { .newGameButton {
@include IncreasedClickArea(0px); @include IncreasedClickArea(0px);
@include S(margin-left, 15px); @include S(margin-left, 10px);
} }
.modsButton { .modsButton {
@include IncreasedClickArea(0px); @include IncreasedClickArea(0px);
@include S(margin-left, 15px); @include S(margin-left, 10px);
@include S(width, 20px); // @include S(width, 20px);
& { // & {
/* @load-async */ // /* @load-async */
background-image: uiResource("res/ui/icons/mods_white.png") !important; // background-image: uiResource("res/ui/icons/mods_white.png") !important;
} // }
background-position: center center; background-position: center center;
background-size: D(15px); background-size: D(15px);
background-color: $modsColor !important; background-color: $modsColor !important;
@ -837,6 +835,23 @@
} }
} }
.modsOverview {
background: $darkModeControlsBackground;
.modsList {
border-color: darken($darkModeControlsBackground, 5);
.mod {
background: darken($darkModeControlsBackground, 5);
color: white;
}
}
.dlcHint {
color: $accentColorBright;
}
}
.footer { .footer {
> a, > a,
.sidelinks > a { .sidelinks > a {

View File

@ -62,6 +62,11 @@
@include S(margin-top, 100px); @include S(margin-top, 100px);
color: lighten($accentColorDark, 15); color: lighten($accentColorDark, 15);
button {
@include S(margin-top, 10px);
@include S(padding, 10px, 20px);
}
&::before { &::before {
@include S(margin-bottom, 15px); @include S(margin-bottom, 15px);
content: ""; content: "";
@ -94,6 +99,10 @@
display: grid; display: grid;
grid-template-columns: 1fr D(100px) D(80px) D(50px); grid-template-columns: 1fr D(100px) D(80px) D(50px);
@include DarkThemeOverride {
background: darken($darkModeControlsBackground, 5);
}
.checkbox { .checkbox {
align-self: center; align-self: center;
justify-self: center; justify-self: center;

View File

@ -28,6 +28,8 @@ export const THIRDPARTY_URLS = {
25: "https://www.youtube.com/watch?v=7OCV1g40Iew&", 25: "https://www.youtube.com/watch?v=7OCV1g40Iew&",
26: "https://www.youtube.com/watch?v=gfm6dS1dCoY", 26: "https://www.youtube.com/watch?v=gfm6dS1dCoY",
}, },
modBrowser: "https://shapez.mod.io/?preview=f55f6304ca4873d9a25f3b575571b948",
}; };
// export const A_B_TESTING_LINK_TYPE = Math.random() > 0.95 ? "steam_1_pr" : "steam_2_npr"; // export const A_B_TESTING_LINK_TYPE = Math.random() > 0.95 ? "steam_1_pr" : "steam_2_npr";
@ -58,7 +60,7 @@ export const globalConfig = {
// Map // Map
mapChunkSize: 16, mapChunkSize: 16,
chunkAggregateSize: 4, chunkAggregateSize: 4,
mapChunkOverviewMinZoom: 0, mapChunkOverviewMinZoom: 0.9,
mapChunkWorldSize: null, // COMPUTED mapChunkWorldSize: null, // COMPUTED
maxBeltShapeBundleSize: 20, maxBeltShapeBundleSize: 20,

View File

@ -70,100 +70,22 @@ export class MetaHubBuilding extends MetaBuilding {
}) })
); );
/**
* @type {Array<import("../components/item_acceptor").ItemAcceptorSlotConfig>}
*/
const slots = [];
for (let i = 0; i < 4; ++i) {
slots.push(
{ pos: new Vector(i, 0), direction: enumDirection.top, filter: "shape" },
{ pos: new Vector(i, 3), direction: enumDirection.bottom, filter: "shape" },
{ pos: new Vector(0, i), direction: enumDirection.left, filter: "shape" },
{ pos: new Vector(3, i), direction: enumDirection.right, filter: "shape" }
);
}
entity.addComponent( entity.addComponent(
new ItemAcceptorComponent({ new ItemAcceptorComponent({
slots: [ slots,
{
pos: new Vector(0, 0),
direction: enumDirection.top,
filter: "shape",
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
direction: enumDirection.top,
filter: "shape",
},
{
pos: new Vector(2, 0),
direction: enumDirection.top,
filter: "shape",
},
{
pos: new Vector(3, 0),
direction: enumDirection.top,
filter: "shape",
},
{
pos: new Vector(3, 0),
direction: enumDirection.right,
filter: "shape",
},
{
pos: new Vector(0, 3),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 3),
direction: enumDirection.bottom,
filter: "shape",
},
{
pos: new Vector(1, 3),
direction: enumDirection.bottom,
filter: "shape",
},
{
pos: new Vector(2, 3),
direction: enumDirection.bottom,
filter: "shape",
},
{
pos: new Vector(3, 3),
direction: enumDirection.bottom,
filter: "shape",
},
{
pos: new Vector(3, 3),
direction: enumDirection.right,
filter: "shape",
},
{
pos: new Vector(0, 1),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 2),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 3),
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(3, 1),
direction: enumDirection.right,
filter: "shape",
},
{
pos: new Vector(3, 2),
direction: enumDirection.right,
filter: "shape",
},
{
pos: new Vector(3, 3),
direction: enumDirection.right,
filter: "shape",
},
],
}) })
); );
} }

View File

@ -55,10 +55,22 @@ export class MetaTrashBuilding extends MetaBuilding {
entity.addComponent( entity.addComponent(
new ItemAcceptorComponent({ new ItemAcceptorComponent({
slots: [ slots: [
{ pos: new Vector(0, 0), direction: enumDirection.top }, {
{ pos: new Vector(0, 0), direction: enumDirection.left }, pos: new Vector(0, 0),
{ pos: new Vector(0, 0), direction: enumDirection.right }, direction: enumDirection.top,
{ pos: new Vector(0, 0), direction: enumDirection.bottom }, },
{
pos: new Vector(0, 0),
direction: enumDirection.right,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
},
], ],
}) })
); );

View File

@ -13,11 +13,11 @@ import { GameRoot } from "../root";
* *
* Contains information about a slot plus its location * Contains information about a slot plus its location
* @typedef {{ * @typedef {{
* slot: ItemAcceptorSlot, * slot: ItemAcceptorSlot,
* index: number, * index: number,
* acceptedDirection: enumDirection * }} ItemAcceptorLocatedSlot */
* }} ItemAcceptorLocatedSlot
* /**
* @typedef {{ * @typedef {{
* pos: Vector, * pos: Vector,
* direction: enumDirection, * direction: enumDirection,
@ -128,11 +128,11 @@ export class ItemAcceptorComponent extends Component {
continue; continue;
} }
// Check if the acceptor slot accepts items from our direction
if (desiredDirection === slot.direction) { if (desiredDirection === slot.direction) {
return { return {
slot, slot,
index: slotIndex, index: slotIndex,
acceptedDirection: desiredDirection,
}; };
} }
} }

View File

@ -435,7 +435,7 @@ export class HubGoals extends BasicSerializableObject {
} }
const randomColor = () => rng.choice(colors); const randomColor = () => rng.choice(colors);
const randomShape = () => rng.choice(Object.values(enumSubShape)); const randomShape = () => rng.choice(availableShapes);
let anyIsMissingTwo = false; let anyIsMissingTwo = false;

View File

@ -530,7 +530,13 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
const offsetShift = 10; const offsetShift = 10;
/**
* @type {Array<import("../../components/item_acceptor").ItemAcceptorSlot>}
*/
let acceptorSlots = []; let acceptorSlots = [];
/**
* @type {Array<import("../../components/item_ejector").ItemEjectorSlot>}
*/
let ejectorSlots = []; let ejectorSlots = [];
if (ejectorComp) { if (ejectorComp) {
@ -548,8 +554,9 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
acceptorSlots.push(fakeAcceptorSlot); acceptorSlots.push(fakeAcceptorSlot);
} }
for (let acceptorSlotIndex = 0; acceptorSlotIndex < acceptorSlots.length; ++acceptorSlotIndex) { // Go over all slots
const slot = acceptorSlots[acceptorSlotIndex]; for (let i = 0; i < acceptorSlots.length; ++i) {
const slot = acceptorSlots[i];
const acceptorSlotWsTile = staticComp.localTileToWorld(slot.pos); const acceptorSlotWsTile = staticComp.localTileToWorld(slot.pos);
const acceptorSlotWsPos = acceptorSlotWsTile.toWorldSpaceCenterOfTile(); const acceptorSlotWsPos = acceptorSlotWsTile.toWorldSpaceCenterOfTile();
@ -567,8 +574,8 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
const sourceEntities = this.root.map.getLayersContentsMultipleXY(sourceTile.x, sourceTile.y); const sourceEntities = this.root.map.getLayersContentsMultipleXY(sourceTile.x, sourceTile.y);
// Check for every entity: // Check for every entity:
for (let i = 0; i < sourceEntities.length; ++i) { for (let j = 0; j < sourceEntities.length; ++j) {
const sourceEntity = sourceEntities[i]; const sourceEntity = sourceEntities[j];
const sourceEjector = sourceEntity.components.ItemEjector; const sourceEjector = sourceEntity.components.ItemEjector;
const sourceBeltComp = sourceEntity.components.Belt; const sourceBeltComp = sourceEntity.components.Belt;
const sourceStaticComp = sourceEntity.components.StaticMapEntity; const sourceStaticComp = sourceEntity.components.StaticMapEntity;

View File

@ -94,11 +94,12 @@ export class HUDEntityDebugger extends BaseHUDPart {
<div>`; <div>`;
for (const property in val) { for (const property in val) {
const isRoot = val[property] == this.root; let hiddenValue = null;
const isRecursive = recursion.includes(val[property]); if (val[property] == this.root) {
hiddenValue = "<root>";
let hiddenValue = isRoot ? "<root>" : null; } else if (val[property] instanceof Node) {
if (isRecursive) { hiddenValue = `<${val[property].constructor.name}>`;
} else if (recursion.includes(val[property])) {
// Avoid recursion by not "expanding" object more than once // Avoid recursion by not "expanding" object more than once
hiddenValue = "<recursion>"; hiddenValue = "<recursion>";
} }

View File

@ -395,7 +395,14 @@ export class GameLogic {
const entity = this.root.map.getLayerContentXY(tile.x + dx, tile.y + dy, "regular"); const entity = this.root.map.getLayerContentXY(tile.x + dx, tile.y + dy, "regular");
if (entity) { if (entity) {
/**
* @type {Array<import("./components/item_ejector").ItemEjectorSlot>}
*/
let ejectorSlots = []; let ejectorSlots = [];
/**
* @type {Array<import("./components/item_acceptor").ItemAcceptorSlot>}
*/
let acceptorSlots = []; let acceptorSlots = [];
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
@ -438,7 +445,6 @@ export class GameLogic {
const wsTile = staticComp.localTileToWorld(slot.pos); const wsTile = staticComp.localTileToWorld(slot.pos);
const direction = slot.direction; const direction = slot.direction;
const wsDirection = staticComp.localDirectionToWorld(direction); const wsDirection = staticComp.localDirectionToWorld(direction);
const sourceTile = wsTile.add(enumDirectionToVector[wsDirection]); const sourceTile = wsTile.add(enumDirectionToVector[wsDirection]);
if (sourceTile.equals(tile)) { if (sourceTile.equals(tile)) {
acceptors.push({ acceptors.push({

View File

@ -65,8 +65,13 @@ export class StorageSystem extends GameSystemWithFilter {
let targetAlpha = storageComp.storedCount > 0 ? 1 : 0; let targetAlpha = storageComp.storedCount > 0 ? 1 : 0;
storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05); storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05);
pinsComp.slots[0].value = storageComp.storedItem; // a wired pins component is not guaranteed, but if its there, set the value
pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON; if (pinsComp) {
pinsComp.slots[0].value = storageComp.storedItem;
pinsComp.slots[1].value = storageComp.getIsFull()
? BOOL_TRUE_SINGLETON
: BOOL_FALSE_SINGLETON;
}
} }
} }

View File

@ -31,6 +31,12 @@ import { BaseHUDPart } from "../game/hud/base_hud_part";
* @typedef {{new(...args: any[]): any, prototype: any}} constructable * @typedef {{new(...args: any[]): any, prototype: any}} constructable
*/ */
/**
* @template {(...args: any) => any} F The function
* @template {object} T The value of this
* @typedef {(this: T, ...args: Parameters<F>) => ReturnType<F>} bindThis
*/
/** /**
* @template {(...args: any[]) => any} F * @template {(...args: any[]) => any} F
* @template P * @template P
@ -369,7 +375,7 @@ export class ModInterface {
/** /**
* Registers a new state class, should be a GameState derived class * Registers a new state class, should be a GameState derived class
* @param {typeof GameState} stateClass * @param {typeof import("../core/game_state").GameState} stateClass
*/ */
registerGameState(stateClass) { registerGameState(stateClass) {
this.modLoader.app.stateMgr.register(stateClass); this.modLoader.app.stateMgr.register(stateClass);
@ -400,7 +406,7 @@ export class ModInterface {
* @template {extendsPrams<P[M]>} O the method that will override the old one * @template {extendsPrams<P[M]>} O the method that will override the old one
* @param {C} classHandle * @param {C} classHandle
* @param {M} methodName * @param {M} methodName
* @param {beforePrams<O, P[M]>} override * @param {bindThis<beforePrams<O, P[M]>, InstanceType<C>>} override
*/ */
replaceMethod(classHandle, methodName, override) { replaceMethod(classHandle, methodName, override) {
const oldMethod = classHandle.prototype[methodName]; const oldMethod = classHandle.prototype[methodName];
@ -418,7 +424,7 @@ export class ModInterface {
* @template {extendsPrams<P[M]>} O the method that will run before the old one * @template {extendsPrams<P[M]>} O the method that will run before the old one
* @param {C} classHandle * @param {C} classHandle
* @param {M} methodName * @param {M} methodName
* @param {O} executeBefore * @param {bindThis<O, InstanceType<C>>} executeBefore
*/ */
runBeforeMethod(classHandle, methodName, executeBefore) { runBeforeMethod(classHandle, methodName, executeBefore) {
const oldHandle = classHandle.prototype[methodName]; const oldHandle = classHandle.prototype[methodName];
@ -437,7 +443,7 @@ export class ModInterface {
* @template {extendsPrams<P[M]>} O the method that will run before the old one * @template {extendsPrams<P[M]>} O the method that will run before the old one
* @param {C} classHandle * @param {C} classHandle
* @param {M} methodName * @param {M} methodName
* @param {O} executeAfter * @param {bindThis<O, InstanceType<C>>} executeAfter
*/ */
runAfterMethod(classHandle, methodName, executeAfter) { runAfterMethod(classHandle, methodName, executeAfter) {
const oldHandle = classHandle.prototype[methodName]; const oldHandle = classHandle.prototype[methodName];
@ -470,7 +476,7 @@ export class ModInterface {
/** /**
* *
* @param {typeof Object} classHandle * @param {Class} classHandle
* @param {({ $super, $old }) => any} extender * @param {({ $super, $old }) => any} extender
*/ */
extendClass(classHandle, extender) { extendClass(classHandle, extender) {

View File

@ -10,6 +10,9 @@ import { Mod } from "./mod";
import { ModInterface } from "./mod_interface"; import { ModInterface } from "./mod_interface";
import { MOD_SIGNALS } from "./mod_signals"; import { MOD_SIGNALS } from "./mod_signals";
import semverValidRange from "semver/ranges/valid";
import semverSatisifies from "semver/functions/satisfies";
const LOG = createLogger("mods"); const LOG = createLogger("mods");
/** /**
@ -20,6 +23,7 @@ const LOG = createLogger("mods");
* website: string; * website: string;
* description: string; * description: string;
* id: string; * id: string;
* minimumGameVersion?: string;
* settings: [] * settings: []
* }} ModMetadata * }} ModMetadata
*/ */
@ -103,21 +107,17 @@ 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) {
let modURLs = Array.isArray(globalConfig.debug.externalModUrl) ? const modURLs = Array.isArray(globalConfig.debug.externalModUrl)
globalConfig.debug.externalModUrl : [globalConfig.debug.externalModUrl]; ? globalConfig.debug.externalModUrl
: [globalConfig.debug.externalModUrl];
for(let i = 0; i < modURLs.length; i++) {
for (let i = 0; i < modURLs.length; i++) {
const response = await fetch(modURLs[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 " + modURLs[i] + ": " + response.status + " " + response.statusText
modURLs[i] +
": " +
response.status +
" " +
response.statusText
); );
} }
mods.push(await response.text()); mods.push(await response.text());
@ -162,6 +162,27 @@ export class ModLoader {
const { modClass, meta } = this.modLoadQueue[i]; const { modClass, meta } = this.modLoadQueue[i];
const modDataFile = "modsettings_" + meta.id + "__" + meta.version + ".json"; const modDataFile = "modsettings_" + meta.id + "__" + meta.version + ".json";
if (meta.minimumGameVersion) {
console.warn(meta.minimumGameVersion, G_BUILD_VERSION);
const minimumGameVersion = meta.minimumGameVersion;
if (!semverValidRange(minimumGameVersion)) {
alert("Mod " + meta.id + " has invalid minimumGameVersion: " + minimumGameVersion);
continue;
}
if (!semverSatisifies(G_BUILD_VERSION, minimumGameVersion)) {
alert(
"Mod '" +
meta.id +
"' is incompatible with this version of the game: \n\n" +
"Mod requires version " +
minimumGameVersion +
" but this game has version " +
G_BUILD_VERSION
);
continue;
}
}
let settings = meta.settings; let settings = meta.settings;
if (meta.settings) { if (meta.settings) {

View File

@ -161,7 +161,10 @@ export class MainMenuState extends GameState {
${MODS.mods ${MODS.mods
.map(mod => { .map(mod => {
return ` return `
<div class="mod">${mod.metadata.name} @ v${mod.metadata.version}</div> <div class="mod">
<div class="name">${mod.metadata.name}</div>
<div class="author">by ${mod.metadata.author}</div>
</div>
`; `;
}) })
.join("")} .join("")}
@ -413,7 +416,10 @@ export class MainMenuState extends GameState {
} }
// Mods // Mods
this.trackClicks(makeButton(outerDiv, ["modsButton", "styledButton"], "&nbsp;"), this.onModsClicked); this.trackClicks(
makeButton(outerDiv, ["modsButton", "styledButton"], T.mods.title),
this.onModsClicked
);
buttonContainer.appendChild(outerDiv); buttonContainer.appendChild(outerDiv);
} }

View File

@ -18,6 +18,11 @@ export class ModsState extends TextualGameState {
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1> <h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
<div class="actions"> <div class="actions">
${
(G_IS_STANDALONE || G_IS_DEV) && MODS.mods.length > 0
? `<button class="styledButton browseMods">${T.mods.browseMods}</button>`
: ""
}
${ ${
G_IS_STANDALONE || G_IS_DEV G_IS_STANDALONE || G_IS_DEV
? `<button class="styledButton openModsFolder">${T.mods.openFolder}</button>` ? `<button class="styledButton openModsFolder">${T.mods.openFolder}</button>`
@ -53,8 +58,9 @@ export class ModsState extends TextualGameState {
return ` return `
<div class="modsStats noMods"> <div class="modsStats noMods">
${T.mods.modsInfo} ${T.mods.modsInfo}
<button class="styledButton browseMods">${T.mods.browseMods}</button>
</div> </div>
`; `;
@ -100,6 +106,10 @@ export class ModsState extends TextualGameState {
if (openModsFolder) { if (openModsFolder) {
this.trackClicks(openModsFolder, this.openModsFolder); this.trackClicks(openModsFolder, this.openModsFolder);
} }
const browseMods = this.htmlElement.querySelector(".browseMods");
if (browseMods) {
this.trackClicks(browseMods, this.openBrowseMods);
}
const checkboxes = this.htmlElement.querySelectorAll(".checkbox"); const checkboxes = this.htmlElement.querySelectorAll(".checkbox");
Array.from(checkboxes).forEach(checkbox => { Array.from(checkboxes).forEach(checkbox => {
@ -119,6 +129,11 @@ export class ModsState extends TextualGameState {
ipcRenderer.invoke("open-mods-folder"); ipcRenderer.invoke("open-mods-folder");
} }
openBrowseMods() {
this.app.analytics.trackUiClick("mods_sbrowse_link");
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.modBrowser);
}
onSteamLinkClicked() { onSteamLinkClicked() {
this.app.analytics.trackUiClick("mods_steam_link"); this.app.analytics.trackUiClick("mods_steam_link");
this.app.platformWrapper.openExternalLink( this.app.platformWrapper.openExternalLink(

View File

@ -1098,6 +1098,7 @@ mods:
version: Version version: Version
openFolder: Open Mods Folder openFolder: Open Mods Folder
folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. folderOnlyStandalone: Opening the mod folder is only possible when running the standalone.
browseMods: Browse Mods
modsInfo: >- modsInfo: >-
To install and manage mods, copy them to the mods folder within the game directory. You can also use the 'Open Mods Folder' button on the top right. To install and manage mods, copy them to the mods folder within the game directory. You can also use the 'Open Mods Folder' button on the top right.

View File

@ -5245,6 +5245,13 @@ lru-cache@^5.1.1:
dependencies: dependencies:
yallist "^3.0.2" yallist "^3.0.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
lz-string@^1.4.4: lz-string@^1.4.4:
version "1.4.4" version "1.4.4"
resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz"
@ -7496,6 +7503,13 @@ semver@^7.2.1, semver@^7.3.2:
resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz" resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
send@0.17.1: send@0.17.1:
version "0.17.1" version "0.17.1"
resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz" resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz"
@ -8800,6 +8814,11 @@ yallist@^3.0.2:
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml-js@^0.1.3: yaml-js@^0.1.3:
version "0.1.5" version "0.1.5"
resolved "https://registry.npmjs.org/yaml-js/-/yaml-js-0.1.5.tgz" resolved "https://registry.npmjs.org/yaml-js/-/yaml-js-0.1.5.tgz"