diff --git a/electron/index.js b/electron/index.js index 18943589..f1de906f 100644 --- a/electron/index.js +++ b/electron/index.js @@ -361,7 +361,8 @@ ipcMain.handle("get-mods", async () => { if (externalMod) { console.log("Adding external mod source:", externalMod); - modFiles.push(externalMod); + const externalModPaths = externalMod.split(","); + modFiles = modFiles.concat(externalModPaths); } if (modFiles.length > 0 && !isDev) { diff --git a/mod_examples/README.md b/mod_examples/README.md index 1545a46b..e38d5834 100644 --- a/mod_examples/README.md +++ b/mod_examples/README.md @@ -5,7 +5,7 @@ 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. +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. @@ -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_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 | +| [sandbox.js](sandbox.js) | Makes blueprints free and always unlocked | Overriding builtin methods | ### 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 | | [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 | + +### 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 diff --git a/mod_examples/add_building_basic.js b/mod_examples/add_building_basic.js index 97e0a358..6b92e769 100644 --- a/mod_examples/add_building_basic.js +++ b/mod_examples/add_building_basic.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "add-building-basic", description: "Shows how to add a new basic building", + minimumGameVersion: ">=1.5.0", }; class MetaDemoModBuilding extends shapez.ModMetaBuilding { diff --git a/mod_examples/add_building_flipper.js b/mod_examples/add_building_flipper.js index b3b746f3..03442499 100644 --- a/mod_examples/add_building_flipper.js +++ b/mod_examples/add_building_flipper.js @@ -7,6 +7,7 @@ const METADATA = { id: "add-building-extended", description: "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 @@ -80,7 +81,7 @@ class MetaModFlipperBuilding extends shapez.ModMetaBuilding { slots: [ { pos: new shapez.Vector(0, 0), - directions: [shapez.enumDirection.bottom], + direction: shapez.enumDirection.bottom, filter: "shape", }, ], diff --git a/mod_examples/base.js b/mod_examples/base.js index 6dc8df8f..09e12f61 100644 --- a/mod_examples/base.js +++ b/mod_examples/base.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "base", description: "The most basic mod", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/buildings_have_cost.js b/mod_examples/buildings_have_cost.js index 79061d35..3dae84ae 100644 --- a/mod_examples/buildings_have_cost.js +++ b/mod_examples/buildings_have_cost.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "patch-methods", 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 { diff --git a/mod_examples/class_extensions.js b/mod_examples/class_extensions.js index c5b5b95e..8647fd45 100644 --- a/mod_examples/class_extensions.js +++ b/mod_examples/class_extensions.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "class-extensions", description: "Shows how to extend builtin classes", + minimumGameVersion: ">=1.5.0", }; const BeltExtension = ({ $super, $old }) => ({ diff --git a/mod_examples/custom_css.js b/mod_examples/custom_css.js index 74d19057..dce316c5 100644 --- a/mod_examples/custom_css.js +++ b/mod_examples/custom_css.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "custom-css", description: "Shows how to add custom css", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_drawing.js b/mod_examples/custom_drawing.js index 6ba49454..e1c25b30 100644 --- a/mod_examples/custom_drawing.js +++ b/mod_examples/custom_drawing.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "base", description: "Displays an indicator on every item processing building when its working", + minimumGameVersion: ">=1.5.0", }; class ItemProcessorStatusGameSystem extends shapez.GameSystem { diff --git a/mod_examples/custom_keybinding.js b/mod_examples/custom_keybinding.js index 650065f0..0a6b11fc 100644 --- a/mod_examples/custom_keybinding.js +++ b/mod_examples/custom_keybinding.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "base", description: "Shows how to add a new keybinding", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_sub_shapes.js b/mod_examples/custom_sub_shapes.js index afb901c0..3aea03cf 100644 --- a/mod_examples/custom_sub_shapes.js +++ b/mod_examples/custom_sub_shapes.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "custom-sub-shapes", description: "Shows how to add custom sub shapes", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_theme.js b/mod_examples/custom_theme.js index a596799c..cc4c9de8 100644 --- a/mod_examples/custom_theme.js +++ b/mod_examples/custom_theme.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "custom-theme", description: "Shows how to add a custom game theme", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/mod_settings.js b/mod_examples/mod_settings.js index bbcfe3fd..b87c138b 100644 --- a/mod_examples/mod_settings.js +++ b/mod_examples/mod_settings.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "mod-settings", description: "Shows how to add settings to your mod", + minimumGameVersion: ">=1.5.0", settings: { timesLaunched: 0, @@ -23,7 +24,7 @@ class Mod extends shapez.Mod { if (state instanceof shapez.MainMenuState) { this.dialogs.showInfo( "Welcome back", - "You have launched this mod " + this.settings.timesLaunched + " times" + `You have launched this mod ${this.settings.timesLaunched} times` ); } }); diff --git a/mod_examples/modify_existing_building.js b/mod_examples/modify_existing_building.js index 1264b2c3..b09f5a20 100644 --- a/mod_examples/modify_existing_building.js +++ b/mod_examples/modify_existing_building.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "modify-existing-building", description: "Shows how to modify an existing building", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/modify_theme.js b/mod_examples/modify_theme.js index dfdfb6e9..4bc9be34 100644 --- a/mod_examples/modify_theme.js +++ b/mod_examples/modify_theme.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "modify-theme", description: "Shows how to modify builtin themes", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/modify_ui.js b/mod_examples/modify_ui.js index 749e191e..0b2d1341 100644 --- a/mod_examples/modify_ui.js +++ b/mod_examples/modify_ui.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "modify-ui", description: "Shows how to modify a builtin game state, in this case the main menu", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/new_item_type.js b/mod_examples/new_item_type.js index ee06538d..3cd52cef 100644 --- a/mod_examples/new_item_type.js +++ b/mod_examples/new_item_type.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "new-item-type", description: "Shows how to add a new item type (fluid)", + minimumGameVersion: ">=1.5.0", }; // Define which fluid types there are @@ -75,7 +76,7 @@ class FluidItem extends shapez.BaseItem { */ drawFullSizeOnCanvas(context, size) { 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); } @@ -89,7 +90,7 @@ class FluidItem extends shapez.BaseItem { drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { const realDiameter = diameter * 0.6; 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); } diff --git a/mod_examples/notification_blocks.js b/mod_examples/notification_blocks.js index a8116849..23f95943 100644 --- a/mod_examples/notification_blocks.js +++ b/mod_examples/notification_blocks.js @@ -7,6 +7,8 @@ const METADATA = { id: "notification-blocks", description: "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", }; //////////////////////////////////////////////////////////////////////// diff --git a/mod_examples/pasting.js b/mod_examples/pasting.js index edd4952c..698edeff 100644 --- a/mod_examples/pasting.js +++ b/mod_examples/pasting.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "pasting", description: "Shows how to properly receive paste events ingame", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { @@ -15,7 +16,7 @@ class Mod extends shapez.Mod { event.preventDefault(); const data = event.clipboardData.getData("text"); - this.dialogs.showInfo("Pasted", "You pasted: '" + data + "'"); + this.dialogs.showInfo("Pasted", `You pasted: '${data}'`); }); }); } diff --git a/mod_examples/replace_builtin_sprites.js b/mod_examples/replace_builtin_sprites.js index 9865121e..885846e7 100644 --- a/mod_examples/replace_builtin_sprites.js +++ b/mod_examples/replace_builtin_sprites.js @@ -6,13 +6,14 @@ const METADATA = { version: "1", id: "replace-builtin-sprites", description: "Shows how to replace builtin sprites", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { init() { // Replace a builtin sprite ["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"]); }); } } diff --git a/mod_examples/sandbox.js b/mod_examples/sandbox.js new file mode 100644 index 00000000..f405ab59 --- /dev/null +++ b/mod_examples/sandbox.js @@ -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; + }); + } +} diff --git a/mod_examples/translations.js b/mod_examples/translations.js index 8a3e07da..2f3a4015 100644 --- a/mod_examples/translations.js +++ b/mod_examples/translations.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "translations", description: "Shows how to add and modify translations", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/usage_statistics.js b/mod_examples/usage_statistics.js index 36bb81f5..64da102b 100644 --- a/mod_examples/usage_statistics.js +++ b/mod_examples/usage_statistics.js @@ -7,6 +7,8 @@ const METADATA = { id: "usage-statistics", description: "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", }; /** diff --git a/package.json b/package.json index 4ee134b7..ef752aac 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "promise-polyfill": "^8.1.0", "query-string": "^6.8.1", "rusha": "^0.8.13", + "semver": "^7.3.5", "serialize-error": "^3.0.0", "strictdom": "^1.0.1", "string-replace-webpack-plugin": "^0.1.3", diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index 2458350e..9027d8a8 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -199,10 +199,6 @@ transform: translateX(50%) rotate(-7deg) scale(1.1); } } - - @include DarkThemeOverride { - color: $colorBlueBright; - } } } @@ -326,6 +322,7 @@ /* @load-async */ background-image: uiResource("icons/edit_key.png") !important; } + @include DarkThemeInvert; } } @@ -354,13 +351,14 @@ box-sizing: border-box; @include PlainText; @include S(margin-bottom, 5px); - display: grid; - grid-template-columns: 1fr auto auto; - @include S(grid-gap, 5px); + display: flex; + flex-direction: column; .author, .version { @include SuperSmallText; + align-self: end; + opacity: 0.4; } .name { overflow: hidden; @@ -457,19 +455,19 @@ .newGameButton { @include IncreasedClickArea(0px); - @include S(margin-left, 15px); + @include S(margin-left, 10px); } .modsButton { @include IncreasedClickArea(0px); - @include S(margin-left, 15px); + @include S(margin-left, 10px); - @include S(width, 20px); + // @include S(width, 20px); - & { - /* @load-async */ - background-image: uiResource("res/ui/icons/mods_white.png") !important; - } + // & { + // /* @load-async */ + // background-image: uiResource("res/ui/icons/mods_white.png") !important; + // } background-position: center center; background-size: D(15px); 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 { > a, .sidelinks > a { diff --git a/src/css/states/mods.scss b/src/css/states/mods.scss index 54d8422b..acec41fb 100644 --- a/src/css/states/mods.scss +++ b/src/css/states/mods.scss @@ -62,6 +62,11 @@ @include S(margin-top, 100px); color: lighten($accentColorDark, 15); + button { + @include S(margin-top, 10px); + @include S(padding, 10px, 20px); + } + &::before { @include S(margin-bottom, 15px); content: ""; @@ -94,6 +99,10 @@ display: grid; grid-template-columns: 1fr D(100px) D(80px) D(50px); + @include DarkThemeOverride { + background: darken($darkModeControlsBackground, 5); + } + .checkbox { align-self: center; justify-self: center; diff --git a/src/js/core/config.js b/src/js/core/config.js index cf9c05a0..754bd7fc 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -28,6 +28,8 @@ export const THIRDPARTY_URLS = { 25: "https://www.youtube.com/watch?v=7OCV1g40Iew&", 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"; @@ -58,7 +60,7 @@ export const globalConfig = { // Map mapChunkSize: 16, chunkAggregateSize: 4, - mapChunkOverviewMinZoom: 0, + mapChunkOverviewMinZoom: 0.9, mapChunkWorldSize: null, // COMPUTED maxBeltShapeBundleSize: 20, diff --git a/src/js/game/buildings/hub.js b/src/js/game/buildings/hub.js index caac9733..a0a9227e 100644 --- a/src/js/game/buildings/hub.js +++ b/src/js/game/buildings/hub.js @@ -70,100 +70,22 @@ export class MetaHubBuilding extends MetaBuilding { }) ); + /** + * @type {Array} + */ + 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( new ItemAcceptorComponent({ - 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", - }, - ], + slots, }) ); } diff --git a/src/js/game/buildings/trash.js b/src/js/game/buildings/trash.js index b461ef3a..bda36001 100644 --- a/src/js/game/buildings/trash.js +++ b/src/js/game/buildings/trash.js @@ -55,10 +55,22 @@ export class MetaTrashBuilding extends MetaBuilding { entity.addComponent( new ItemAcceptorComponent({ slots: [ - { pos: new Vector(0, 0), direction: enumDirection.top }, - { pos: new Vector(0, 0), direction: enumDirection.left }, - { pos: new Vector(0, 0), direction: enumDirection.right }, - { pos: new Vector(0, 0), direction: enumDirection.bottom }, + { + pos: new Vector(0, 0), + direction: enumDirection.top, + }, + { + pos: new Vector(0, 0), + direction: enumDirection.right, + }, + { + pos: new Vector(0, 0), + direction: enumDirection.bottom, + }, + { + pos: new Vector(0, 0), + direction: enumDirection.left, + }, ], }) ); diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index de0e77c3..4abc0117 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -13,11 +13,11 @@ import { GameRoot } from "../root"; * * Contains information about a slot plus its location * @typedef {{ - * slot: ItemAcceptorSlot, - * index: number, - * acceptedDirection: enumDirection - * }} ItemAcceptorLocatedSlot - * + * slot: ItemAcceptorSlot, + * index: number, + * }} ItemAcceptorLocatedSlot */ + +/** * @typedef {{ * pos: Vector, * direction: enumDirection, @@ -128,11 +128,11 @@ export class ItemAcceptorComponent extends Component { continue; } + // Check if the acceptor slot accepts items from our direction if (desiredDirection === slot.direction) { return { slot, index: slotIndex, - acceptedDirection: desiredDirection, }; } } diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index c1b2a4e4..e0e4608c 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -435,7 +435,7 @@ export class HubGoals extends BasicSerializableObject { } const randomColor = () => rng.choice(colors); - const randomShape = () => rng.choice(Object.values(enumSubShape)); + const randomShape = () => rng.choice(availableShapes); let anyIsMissingTwo = false; diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index c5f95682..ca9c128b 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -530,7 +530,13 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const offsetShift = 10; + /** + * @type {Array} + */ let acceptorSlots = []; + /** + * @type {Array} + */ let ejectorSlots = []; if (ejectorComp) { @@ -548,8 +554,9 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { acceptorSlots.push(fakeAcceptorSlot); } - for (let acceptorSlotIndex = 0; acceptorSlotIndex < acceptorSlots.length; ++acceptorSlotIndex) { - const slot = acceptorSlots[acceptorSlotIndex]; + // Go over all slots + for (let i = 0; i < acceptorSlots.length; ++i) { + const slot = acceptorSlots[i]; const acceptorSlotWsTile = staticComp.localTileToWorld(slot.pos); const acceptorSlotWsPos = acceptorSlotWsTile.toWorldSpaceCenterOfTile(); @@ -567,8 +574,8 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const sourceEntities = this.root.map.getLayersContentsMultipleXY(sourceTile.x, sourceTile.y); // Check for every entity: - for (let i = 0; i < sourceEntities.length; ++i) { - const sourceEntity = sourceEntities[i]; + for (let j = 0; j < sourceEntities.length; ++j) { + const sourceEntity = sourceEntities[j]; const sourceEjector = sourceEntity.components.ItemEjector; const sourceBeltComp = sourceEntity.components.Belt; const sourceStaticComp = sourceEntity.components.StaticMapEntity; diff --git a/src/js/game/hud/parts/entity_debugger.js b/src/js/game/hud/parts/entity_debugger.js index 640ad4d6..debd456d 100644 --- a/src/js/game/hud/parts/entity_debugger.js +++ b/src/js/game/hud/parts/entity_debugger.js @@ -94,11 +94,12 @@ export class HUDEntityDebugger extends BaseHUDPart {
`; for (const property in val) { - const isRoot = val[property] == this.root; - const isRecursive = recursion.includes(val[property]); - - let hiddenValue = isRoot ? "" : null; - if (isRecursive) { + let hiddenValue = null; + if (val[property] == this.root) { + hiddenValue = ""; + } else if (val[property] instanceof Node) { + hiddenValue = `<${val[property].constructor.name}>`; + } else if (recursion.includes(val[property])) { // Avoid recursion by not "expanding" object more than once hiddenValue = ""; } diff --git a/src/js/game/logic.js b/src/js/game/logic.js index d13a0b46..cf32c5d6 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -395,7 +395,14 @@ export class GameLogic { const entity = this.root.map.getLayerContentXY(tile.x + dx, tile.y + dy, "regular"); if (entity) { + /** + * @type {Array} + */ let ejectorSlots = []; + + /** + * @type {Array} + */ let acceptorSlots = []; const staticComp = entity.components.StaticMapEntity; @@ -438,7 +445,6 @@ export class GameLogic { const wsTile = staticComp.localTileToWorld(slot.pos); const direction = slot.direction; const wsDirection = staticComp.localDirectionToWorld(direction); - const sourceTile = wsTile.add(enumDirectionToVector[wsDirection]); if (sourceTile.equals(tile)) { acceptors.push({ diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index b134c25c..8db8df06 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -65,8 +65,13 @@ export class StorageSystem extends GameSystemWithFilter { let targetAlpha = storageComp.storedCount > 0 ? 1 : 0; storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05); - pinsComp.slots[0].value = storageComp.storedItem; - pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON; + // a wired pins component is not guaranteed, but if its there, set the value + if (pinsComp) { + pinsComp.slots[0].value = storageComp.storedItem; + pinsComp.slots[1].value = storageComp.getIsFull() + ? BOOL_TRUE_SINGLETON + : BOOL_FALSE_SINGLETON; + } } } diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 949e4301..195c005b 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -31,6 +31,12 @@ import { BaseHUDPart } from "../game/hud/base_hud_part"; * @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) => ReturnType} bindThis + */ + /** * @template {(...args: any[]) => any} F * @template P @@ -369,7 +375,7 @@ export class ModInterface { /** * 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) { this.modLoader.app.stateMgr.register(stateClass); @@ -400,7 +406,7 @@ export class ModInterface { * @template {extendsPrams} O the method that will override the old one * @param {C} classHandle * @param {M} methodName - * @param {beforePrams} override + * @param {bindThis, InstanceType>} override */ replaceMethod(classHandle, methodName, override) { const oldMethod = classHandle.prototype[methodName]; @@ -418,7 +424,7 @@ export class ModInterface { * @template {extendsPrams} O the method that will run before the old one * @param {C} classHandle * @param {M} methodName - * @param {O} executeBefore + * @param {bindThis>} executeBefore */ runBeforeMethod(classHandle, methodName, executeBefore) { const oldHandle = classHandle.prototype[methodName]; @@ -437,7 +443,7 @@ export class ModInterface { * @template {extendsPrams} O the method that will run before the old one * @param {C} classHandle * @param {M} methodName - * @param {O} executeAfter + * @param {bindThis>} executeAfter */ runAfterMethod(classHandle, methodName, executeAfter) { const oldHandle = classHandle.prototype[methodName]; @@ -470,7 +476,7 @@ export class ModInterface { /** * - * @param {typeof Object} classHandle + * @param {Class} classHandle * @param {({ $super, $old }) => any} extender */ extendClass(classHandle, extender) { diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index dc64eb6c..48911abd 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -10,6 +10,9 @@ import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; import { MOD_SIGNALS } from "./mod_signals"; +import semverValidRange from "semver/ranges/valid"; +import semverSatisifies from "semver/functions/satisfies"; + const LOG = createLogger("mods"); /** @@ -20,6 +23,7 @@ const LOG = createLogger("mods"); * website: string; * description: string; * id: string; + * minimumGameVersion?: string; * settings: [] * }} ModMetadata */ @@ -103,21 +107,17 @@ export class ModLoader { mods = await ipcRenderer.invoke("get-mods"); } if (G_IS_DEV && globalConfig.debug.externalModUrl) { - let modURLs = Array.isArray(globalConfig.debug.externalModUrl) ? - globalConfig.debug.externalModUrl : [globalConfig.debug.externalModUrl]; - - for(let i = 0; i < modURLs.length; i++) { + const 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 + "Failed to load " + modURLs[i] + ": " + response.status + " " + response.statusText ); } mods.push(await response.text()); @@ -162,6 +162,27 @@ export class ModLoader { const { modClass, meta } = this.modLoadQueue[i]; 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; if (meta.settings) { diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index b23a15f3..4d62f2ce 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -161,7 +161,10 @@ export class MainMenuState extends GameState { ${MODS.mods .map(mod => { return ` -
${mod.metadata.name} @ v${mod.metadata.version}
+
+
${mod.metadata.name}
+
by ${mod.metadata.author}
+
`; }) .join("")} @@ -413,7 +416,10 @@ export class MainMenuState extends GameState { } // Mods - this.trackClicks(makeButton(outerDiv, ["modsButton", "styledButton"], " "), this.onModsClicked); + this.trackClicks( + makeButton(outerDiv, ["modsButton", "styledButton"], T.mods.title), + this.onModsClicked + ); buttonContainer.appendChild(outerDiv); } diff --git a/src/js/states/mods.js b/src/js/states/mods.js index c3cbce52..42add8bd 100644 --- a/src/js/states/mods.js +++ b/src/js/states/mods.js @@ -18,6 +18,11 @@ export class ModsState extends TextualGameState {

${this.getStateHeaderTitle()}

+ ${ + (G_IS_STANDALONE || G_IS_DEV) && MODS.mods.length > 0 + ? `` + : "" + } ${ G_IS_STANDALONE || G_IS_DEV ? `` @@ -53,8 +58,9 @@ export class ModsState extends TextualGameState { return `
- ${T.mods.modsInfo} + +
`; @@ -100,6 +106,10 @@ export class ModsState extends TextualGameState { if (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"); Array.from(checkboxes).forEach(checkbox => { @@ -119,6 +129,11 @@ export class ModsState extends TextualGameState { ipcRenderer.invoke("open-mods-folder"); } + openBrowseMods() { + this.app.analytics.trackUiClick("mods_sbrowse_link"); + this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.modBrowser); + } + onSteamLinkClicked() { this.app.analytics.trackUiClick("mods_steam_link"); this.app.platformWrapper.openExternalLink( diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 2f76c00f..7070c318 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -1098,6 +1098,7 @@ mods: version: Version openFolder: Open Mods Folder folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. + browseMods: Browse Mods 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. diff --git a/yarn.lock b/yarn.lock index cdcdd19e..27552f93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5245,6 +5245,13 @@ lru-cache@^5.1.1: dependencies: 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: version "1.4.4" 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" 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: version "0.17.1" 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" 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: version "0.1.5" resolved "https://registry.npmjs.org/yaml-js/-/yaml-js-0.1.5.tgz"