From c41aaa1fc5f5a143b2661bb4ca1800c018ad7409 Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 1 Feb 2022 16:35:49 +0100 Subject: [PATCH] Mod Support - 1.5.0 Update (#1361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial modloader draft * modloader features * Refactor mods to use signals * Add support for modifying and registering new transltions * Minor adjustments * Support for string building ids for mods * Initial support for adding new buildings * Refactor how mods are loaded to resolve circular dependencies and prepare for future mod loading * Lazy Load mods to make sure all dependencies are loaded * Expose all exported members automatically to mods * Fix duplicate exports * Allow loading mods from standalone * update changelog * Fix mods folder incorrect path * Fix modloading in standalone * Fix sprites not getting replaced, update demo mod * Load dev mod via raw loader * Improve mod developing so mods are directly ready to be deployed, load mods from local file server * Proper mods ui * Allow mods to register game systems and draw stuff * Change mods path * Fix sprites not loading * Minor adjustments, closes #1333 * Add support for loading atlases via mods * Add support for loading mods from external sources in DEV * Add confirmation when loading mods * Fix circular dependency * Minor Keybindings refactor, add support for keybindings to mods, add support for dialogs to mods * Add some mod signals * refactor game loading states * Make shapez exports global * Start to make mods safer * Refactor file system electron event handling * Properly isolate electron renderer process * Update to latest electron * Show errors when loading mods * Update confirm dialgo * Minor restructure, start to add mod examples * Allow adding custom themesw * Add more examples and allow defining custom item processor operations * Add interface to register new buildings * Fixed typescript type errors (#1335) * Refactor building registry, make it easier for mods to add new buildings * Allow overriding existing methods * Add more examples and more features * More mod examples * Make mod loading simpler * Add example how to add custom drawings * Remove unused code * Minor modloader adjustments * Support for rotation variants in mods (was broken previously) * Allow mods to replace builtin sub shapes * Add helper methods to extend classes * Fix menu bar on mac os * Remember window state * Add support for paste signals * Add example how to add custom components and systems * Support for mod settings * Add example for adding a new item type * Update class extensions * Minor adjustments * Fix typo * Add notification blocks mod example * Add small tutorial * Update readme * Add better instructions * Update JSDoc for Replacing Methods (#1336) * upgraded types for overriding methods * updated comments Co-authored-by: Edward Badel * Direction lock now indicates when there is a building inbetween * Fix mod examples * Fix linter error * Game state register (#1341) * Added a gamestate register helper Added a gamestate register helper * Update mod_interface.js * export build options * Fix runBeforeMethod and runAfterMethod * Minor game system code cleanup * Belt path drawing optimization * Fix belt path optimization * Belt drawing improvements, again * Do not render belts in statics disabled view * Allow external URL to load more than one mod (#1337) * Allow external URL to load more than one mod Instead of loading the text returned from the remote server, load a JSON object with a `mods` field, containing strings of all the mods. This lets us work on more than one mod at a time or without separate repos. This will break tooling such as `create-shapezio-mod` though. * Update modloader.js * Prettier fixes * Added link to create-shapezio-mod npm page (#1339) Added link to create-shapezio-mod npm page: https://www.npmjs.com/package/create-shapezio-mod * allow command line switch to load more than one mod (#1342) * Fixed class handle type (#1345) * Fixed class handle type * Fixed import game state * Minor adjustments * Refactor item acceptor to allow only single direction slots * Allow specifying minimumGameVersion * Add sandbox example * Replaced concatenated strings with template literals (#1347) * Mod improvements * Make wired pins component optional on the storage * Fix mod examples * Bind `this` for method overriding JSDoc (#1352) * fix entity debugger reaching HTML elements (#1353) * Store mods in savegame and show warning when it differs * Closes #1357 * Fix All Shapez Exports Being Const (#1358) * Allowed setting of variables inside webpack modules * remove console log * Fix stringification of things inside of eval Co-authored-by: Edward Badel * Fix building placer intersection warning * Add example for storing data in the savegame * Fix double painter bug (#1349) * Add example on how to extend builtin buildings * update readme * Disable steam achievements when playing with mods * Update translations Co-authored-by: Thomas (DJ1TJOO) <44841260+DJ1TJOO@users.noreply.github.com> Co-authored-by: Bagel03 <70449196+Bagel03@users.noreply.github.com> Co-authored-by: Edward Badel Co-authored-by: Emerald Block <69981203+EmeraldBlock@users.noreply.github.com> Co-authored-by: saile515 <63782477+saile515@users.noreply.github.com> Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com> --- electron/.gitignore | 1 + electron/index.js | 233 +++++-- electron/mods/README.txt | 6 + electron/package.json | 10 +- electron/preload.js | 7 + electron/steam.js | 1 - electron/yarn.lock | 58 +- electron_wegame/index.js | 86 +-- gulp/gulpfile.js | 2 +- gulp/mod.js | 3 + gulp/package.json | 1 + gulp/standalone.js | 2 +- gulp/webpack.config.js | 4 + gulp/webpack.production.config.js | 3 +- gulp/yarn.lock | 46 ++ mod_examples/README.md | 59 ++ mod_examples/add_building_basic.js | 67 ++ mod_examples/add_building_flipper.js | 130 ++++ mod_examples/base.js | 20 + mod_examples/buildings_have_cost.js | 89 +++ mod_examples/class_extensions.js | 32 + mod_examples/custom_css.js | 44 ++ mod_examples/custom_drawing.js | 63 ++ mod_examples/custom_keybinding.js | 32 + mod_examples/custom_sub_shapes.js | 46 ++ mod_examples/custom_theme.js | 99 +++ mod_examples/mirrored_cutter.js | 81 +++ mod_examples/mod_settings.js | 32 + mod_examples/modify_existing_building.js | 27 + mod_examples/modify_theme.js | 24 + mod_examples/modify_ui.js | 46 ++ mod_examples/new_item_type.js | 147 ++++ mod_examples/notification_blocks.js | 314 +++++++++ mod_examples/pasting.js | 23 + mod_examples/replace_builtin_sprites.js | 48 ++ mod_examples/sandbox.js | 21 + mod_examples/storing_data_in_savegame.js | 78 +++ mod_examples/translations.js | 66 ++ mod_examples/usage_statistics.js | 148 ++++ package.json | 6 +- res/ui/icons/mods.png | Bin 0 -> 4450 bytes res/ui/icons/mods_white.png | Bin 0 -> 4455 bytes res/ui/icons/notification_error.png | Bin 0 -> 5509 bytes res/ui/icons/notification_info.png | Bin 0 -> 1899 bytes res/ui/icons/notification_warning.png | Bin 0 -> 4168 bytes src/css/ingame_hud/dialogs.scss | 31 + src/css/main.scss | 1 + src/css/resources.scss | 3 +- src/css/states/main_menu.scss | 150 +++- src/css/states/mods.scss | 141 ++++ src/css/states/settings.scss | 48 +- src/css/variables.scss | 1 + src/html/index.html | 4 +- src/js/application.js | 74 +- src/js/changelog.js | 8 + src/js/core/background_resources_loader.js | 1 + src/js/core/config.js | 6 +- src/js/core/config.local.template.js | 6 + src/js/core/globals.js | 17 + src/js/core/input_distributor.js | 10 + src/js/core/input_receiver.js | 3 + src/js/core/loader.js | 4 + src/js/core/modal_dialog_elements.js | 3 +- src/js/core/modal_dialog_forms.js | 13 +- src/js/core/sprites.js | 2 + src/js/core/state_manager.js | 3 + src/js/core/utils.js | 17 +- src/js/game/base_item.js | 3 - src/js/game/belt_path.js | 182 ++++- src/js/game/blueprint.js | 6 +- src/js/game/building_codes.js | 30 +- src/js/game/buildings/analyzer.js | 11 +- src/js/game/buildings/balancer.js | 36 +- src/js/game/buildings/belt.js | 22 +- src/js/game/buildings/block.js | 11 +- src/js/game/buildings/comparator.js | 11 +- src/js/game/buildings/constant_producer.js | 11 +- src/js/game/buildings/constant_signal.js | 11 +- src/js/game/buildings/cutter.js | 15 +- src/js/game/buildings/display.js | 11 +- src/js/game/buildings/filter.js | 13 +- src/js/game/buildings/goal_acceptor.js | 13 +- src/js/game/buildings/hub.js | 97 +-- src/js/game/buildings/item_producer.js | 11 +- src/js/game/buildings/lever.js | 11 +- src/js/game/buildings/logic_gate.js | 23 +- src/js/game/buildings/miner.js | 13 + src/js/game/buildings/mixer.js | 15 +- src/js/game/buildings/painter.js | 46 +- src/js/game/buildings/reader.js | 13 +- src/js/game/buildings/rotater.js | 19 +- src/js/game/buildings/stacker.js | 15 +- src/js/game/buildings/storage.js | 15 +- src/js/game/buildings/transistor.js | 13 + src/js/game/buildings/trash.js | 30 +- src/js/game/buildings/underground_belt.js | 27 +- src/js/game/buildings/virtual_processor.js | 27 +- src/js/game/buildings/wire.js | 45 ++ src/js/game/buildings/wire_tunnel.js | 11 +- src/js/game/component_registry.js | 47 +- src/js/game/components/belt.js | 2 +- src/js/game/components/item_acceptor.js | 27 +- src/js/game/components/item_processor.js | 5 + src/js/game/components/static_map_entity.js | 4 +- src/js/game/core.js | 2 + src/js/game/entity.js | 26 +- src/js/game/game_system_manager.js | 23 + src/js/game/hub_goals.js | 7 +- src/js/game/hud/hud.js | 7 +- src/js/game/hud/parts/base_toolbar.js | 9 +- src/js/game/hud/parts/building_placer.js | 280 +++++--- src/js/game/hud/parts/constant_signal_edit.js | 181 ++++- src/js/game/hud/parts/entity_debugger.js | 11 +- src/js/game/hud/parts/modal_dialogs.js | 2 +- ...{HUDPuzzleNextPuzzle.js => next_puzzle.js} | 0 src/js/game/hud/parts/notifications.js | 9 +- src/js/game/key_action_mapper.js | 263 ++++--- src/js/game/logic.js | 40 +- src/js/game/map_chunk.js | 22 +- src/js/game/map_chunk_aggregate.js | 3 +- src/js/game/map_chunk_view.js | 33 +- src/js/game/meta_building.js | 10 + src/js/game/meta_building_registry.js | 240 ++----- src/js/game/modes/puzzle_play.js | 2 +- src/js/game/modes/regular.js | 35 +- src/js/game/shape_definition.js | 171 +++-- src/js/game/systems/belt.js | 15 +- src/js/game/systems/belt_underlays.js | 16 +- src/js/game/systems/constant_producer.js | 2 - src/js/game/systems/constant_signal.js | 188 +---- src/js/game/systems/display.js | 7 +- src/js/game/systems/goal_acceptor.js | 2 - src/js/game/systems/item_ejector.js | 7 +- src/js/game/systems/item_processor.js | 85 +-- src/js/game/systems/item_producer.js | 5 - src/js/game/systems/lever.js | 7 +- src/js/game/systems/storage.js | 17 +- src/js/game/systems/wire.js | 5 +- src/js/game/themes/dark.json | 5 +- src/js/game/themes/light.json | 5 +- src/js/globals.d.ts | 8 +- src/js/main.js | 35 +- src/js/mods/mod.js | 36 + src/js/mods/mod_interface.js | 658 ++++++++++++++++++ src/js/mods/mod_meta_building.js | 18 + src/js/mods/mod_signals.js | 33 + src/js/mods/modloader.js | 270 +++++++ src/js/platform/api.js | 5 +- .../electron/steam_achievement_provider.js | 7 +- src/js/platform/electron/storage.js | 68 +- src/js/platform/electron/wrapper.js | 9 +- src/js/profile/application_settings.js | 339 ++++----- src/js/savegame/savegame.js | 11 +- .../savegame/savegame_interface_registry.js | 2 + src/js/savegame/savegame_serializer.js | 12 +- src/js/savegame/savegame_typedefs.js | 12 +- src/js/savegame/schemas/1010.js | 28 + src/js/savegame/schemas/1010.json | 5 + src/js/savegame/serialization.js | 4 +- src/js/savegame/serialization_data_types.js | 47 ++ src/js/states/ingame.js | 68 +- src/js/states/keybindings.js | 20 +- src/js/states/main_menu.js | 154 +++- src/js/states/mods.js | 149 ++++ src/js/states/settings.js | 31 +- src/js/translations.js | 24 +- translations/base-en.yaml | 33 + translations/base-zh-CN.yaml | 26 + version | 2 +- yarn.lock | 19 + 170 files changed, 5956 insertions(+), 1572 deletions(-) create mode 100644 electron/.gitignore create mode 100644 electron/mods/README.txt create mode 100644 electron/preload.js create mode 100644 gulp/mod.js create mode 100644 mod_examples/README.md create mode 100644 mod_examples/add_building_basic.js create mode 100644 mod_examples/add_building_flipper.js create mode 100644 mod_examples/base.js create mode 100644 mod_examples/buildings_have_cost.js create mode 100644 mod_examples/class_extensions.js create mode 100644 mod_examples/custom_css.js create mode 100644 mod_examples/custom_drawing.js create mode 100644 mod_examples/custom_keybinding.js create mode 100644 mod_examples/custom_sub_shapes.js create mode 100644 mod_examples/custom_theme.js create mode 100644 mod_examples/mirrored_cutter.js create mode 100644 mod_examples/mod_settings.js create mode 100644 mod_examples/modify_existing_building.js create mode 100644 mod_examples/modify_theme.js create mode 100644 mod_examples/modify_ui.js create mode 100644 mod_examples/new_item_type.js create mode 100644 mod_examples/notification_blocks.js create mode 100644 mod_examples/pasting.js create mode 100644 mod_examples/replace_builtin_sprites.js create mode 100644 mod_examples/sandbox.js create mode 100644 mod_examples/storing_data_in_savegame.js create mode 100644 mod_examples/translations.js create mode 100644 mod_examples/usage_statistics.js create mode 100644 res/ui/icons/mods.png create mode 100644 res/ui/icons/mods_white.png create mode 100644 res/ui/icons/notification_error.png create mode 100644 res/ui/icons/notification_info.png create mode 100644 res/ui/icons/notification_warning.png create mode 100644 src/css/states/mods.scss rename src/js/game/hud/parts/{HUDPuzzleNextPuzzle.js => next_puzzle.js} (100%) create mode 100644 src/js/mods/mod.js create mode 100644 src/js/mods/mod_interface.js create mode 100644 src/js/mods/mod_meta_building.js create mode 100644 src/js/mods/mod_signals.js create mode 100644 src/js/mods/modloader.js create mode 100644 src/js/savegame/schemas/1010.js create mode 100644 src/js/savegame/schemas/1010.json create mode 100644 src/js/states/mods.js diff --git a/electron/.gitignore b/electron/.gitignore new file mode 100644 index 00000000..0cdb30f4 --- /dev/null +++ b/electron/.gitignore @@ -0,0 +1 @@ +mods/*.js \ No newline at end of file diff --git a/electron/index.js b/electron/index.js index e7994050..03fb4278 100644 --- a/electron/index.js +++ b/electron/index.js @@ -1,27 +1,40 @@ /* eslint-disable quotes,no-undef */ -const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell } = require("electron"); +const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell, dialog, session } = require("electron"); const path = require("path"); const url = require("url"); const fs = require("fs"); const steam = require("./steam"); const asyncLock = require("async-lock"); +const windowStateKeeper = require("electron-window-state"); -const isDev = process.argv.indexOf("--dev") >= 0; -const isLocal = process.argv.indexOf("--local") >= 0; +// Disable hardware key handling, i.e. being able to pause/resume the game music +// with hardware keys +app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling"); + +const isDev = app.commandLine.hasSwitch("dev"); +const isLocal = app.commandLine.hasSwitch("local"); +const safeMode = app.commandLine.hasSwitch("safe-mode"); +const externalMod = app.commandLine.getSwitchValue("load-mod"); const roamingFolder = process.env.APPDATA || (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"); + let storePath = path.join(roamingFolder, "shapez.io", "saves"); +let modsPath = path.join(roamingFolder, "shapez.io", "mods"); if (!fs.existsSync(storePath)) { // No try-catch by design fs.mkdirSync(storePath, { recursive: true }); } +if (!fs.existsSync(modsPath)) { + fs.mkdirSync(modsPath, { recursive: true }); +} + /** @type {BrowserWindow} */ let win = null; let menu = null; @@ -32,26 +45,44 @@ function createWindow() { faviconExtension = ".ico"; } + const mainWindowState = windowStateKeeper({ + defaultWidth: 1000, + defaultHeight: 800, + }); + win = new BrowserWindow({ - width: 1280, - height: 800, + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, show: false, backgroundColor: "#222428", - useContentSize: true, + useContentSize: false, minWidth: 800, minHeight: 600, title: "shapez.io Standalone", transparent: false, icon: path.join(__dirname, "favicon" + faviconExtension), // fullscreen: true, - autoHideMenuBar: true, + autoHideMenuBar: !isDev, webPreferences: { - nodeIntegration: true, - webSecurity: false, + nodeIntegration: false, + nodeIntegrationInWorker: false, + nodeIntegrationInSubFrames: false, + contextIsolation: true, + enableRemoteModule: false, + disableBlinkFeatures: "Auxclick", + + webSecurity: true, + sandbox: true, + preload: path.join(__dirname, "preload.js"), + experimentalFeatures: false, }, allowRunningInsecureContent: false, }); + mainWindowState.manage(win); + if (isLocal) { win.loadURL("http://localhost:3005"); } else { @@ -66,9 +97,67 @@ function createWindow() { win.webContents.session.clearCache(); win.webContents.session.clearStorageData(); + ////// SECURITY + + // Disable permission requests + win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { + callback(false); + }); + session.fromPartition("default").setPermissionRequestHandler((webContents, permission, callback) => { + callback(false); + }); + + app.on("web-contents-created", (event, contents) => { + // Disable vewbiew + contents.on("will-attach-webview", (event, webPreferences, params) => { + event.preventDefault(); + }); + // Disable navigation + contents.on("will-navigate", (event, navigationUrl) => { + event.preventDefault(); + }); + }); + + win.webContents.on("will-redirect", (contentsEvent, navigationUrl) => { + // Log and prevent the app from redirecting to a new page + console.error( + `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.` + ); + contentsEvent.preventDefault(); + }); + + // Filter loading any module via remote; + // you shouldn't be using remote at all, though + // https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module + app.on("remote-require", (event, webContents, moduleName) => { + event.preventDefault(); + }); + + // built-ins are modules such as "app" + app.on("remote-get-builtin", (event, webContents, moduleName) => { + event.preventDefault(); + }); + + app.on("remote-get-global", (event, webContents, globalName) => { + event.preventDefault(); + }); + + app.on("remote-get-current-window", (event, webContents) => { + event.preventDefault(); + }); + + app.on("remote-get-current-web-contents", (event, webContents) => { + event.preventDefault(); + }); + + //// END SECURITY + win.webContents.on("new-window", (event, pth) => { event.preventDefault(); - shell.openExternal(pth); + + if (pth.startsWith("https://")) { + shell.openExternal(pth); + } }); win.on("closed", () => { @@ -79,15 +168,17 @@ function createWindow() { if (isDev) { menu = new Menu(); + win.webContents.toggleDevTools(); + const mainItem = new MenuItem({ label: "Toggle Dev Tools", - click: () => win.toggleDevTools(), + click: () => win.webContents.toggleDevTools(), accelerator: "F12", }); menu.append(mainItem); const reloadItem = new MenuItem({ - label: "Restart", + label: "Reload", click: () => win.reload(), accelerator: "F5", }); @@ -100,7 +191,15 @@ function createWindow() { }); menu.append(fullscreenItem); - Menu.setApplicationMenu(menu); + const mainMenu = new Menu(); + mainMenu.append( + new MenuItem({ + label: "shapez.io", + submenu: menu, + }) + ); + + Menu.setApplicationMenu(mainMenu); } else { Menu.setApplicationMenu(null); } @@ -114,7 +213,7 @@ function createWindow() { if (!app.requestSingleInstanceLock()) { app.exit(0); } else { - app.on("second-instance", (event, commandLine, workingDirectory) => { + app.on("second-instance", () => { // Someone tried to run a second instance, we should focus if (win) { if (win.isMinimized()) { @@ -136,7 +235,7 @@ ipcMain.on("set-fullscreen", (event, flag) => { win.setFullScreen(flag); }); -ipcMain.on("exit-app", (event, flag) => { +ipcMain.on("exit-app", () => { win.close(); app.quit(); }); @@ -167,14 +266,14 @@ async function writeFileSafe(filename, contents) { if (!fs.existsSync(filename)) { // this one is easy console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename)); - await fs.promises.writeFile(filename, contents, { encoding: "utf8" }); + await fs.promises.writeFile(filename, contents, "utf8"); return; } // first, write a temporary file (.tmp-XXX) const tempName = filename + ".tmp-" + transactionId; console.log(prefix, "Writing temporary file", niceFileName(tempName)); - await fs.promises.writeFile(tempName, contents, { encoding: "utf8" }); + await fs.promises.writeFile(tempName, contents, "utf8"); // now, rename the original file to (.backup-XXX) const oldTemporaryName = filename + ".backup-" + transactionId; @@ -216,68 +315,74 @@ async function writeFileSafe(filename, contents) { }); } -async function performFsJob(job) { - const fname = path.join(storePath, job.filename); - +ipcMain.handle("fs-job", async (event, job) => { + const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/i, "_"); + const fname = path.join(storePath, filenameSafe); switch (job.type) { case "read": { if (!fs.existsSync(fname)) { - return { - // Special FILE_NOT_FOUND error code - error: "file_not_found", - }; - } - - try { - const data = await fs.promises.readFile(fname, { encoding: "utf8" }); - return { - success: true, - data, - }; - } catch (ex) { - return { - error: ex, - }; + // Special FILE_NOT_FOUND error code + return { error: "file_not_found" }; } + return await fs.promises.readFile(fname, "utf8"); } case "write": { - try { - await writeFileSafe(fname, job.contents); - return { - success: true, - data: job.contents, - }; - } catch (ex) { - return { - error: ex, - }; - } + await writeFileSafe(fname, job.contents); + return job.contents; } case "delete": { - try { - await fs.promises.unlink(fname); - } catch (ex) { - return { - error: ex, - }; - } - - return { - success: true, - data: null, - }; + await fs.promises.unlink(fname); + return; } default: - throw new Error("Unkown fs job: " + job.type); + throw new Error("Unknown fs job: " + job.type); + } +}); + +ipcMain.handle("open-mods-folder", async () => { + shell.openPath(modsPath); +}); + +console.log("Loading mods ..."); + +function loadMods() { + if (safeMode) { + console.log("Safe Mode enabled for mods, skipping mod search"); + } + console.log("Loading mods from", modsPath); + let modFiles = safeMode + ? [] + : fs + .readdirSync(modsPath) + .filter(filename => filename.endsWith(".js")) + .map(filename => path.join(modsPath, filename)); + + if (externalMod) { + console.log("Adding external mod source:", externalMod); + const externalModPaths = externalMod.split(","); + modFiles = modFiles.concat(externalModPaths); } + + return modFiles.map(filename => fs.readFileSync(filename, "utf8")); } -ipcMain.on("fs-job", async (event, arg) => { - const result = await performFsJob(arg); - event.reply("fs-response", { id: arg.id, result }); +let mods = []; +try { + mods = loadMods(); + console.log("Loaded", mods.length, "mods"); +} catch (ex) { + console.error("Failed ot load mods"); + dialog.showErrorBox("Failed to load mods:", ex); +} + +ipcMain.handle("get-mods", async () => { + return mods; }); steam.init(isDev); -steam.listen(); + +if (mods) { + steam.listen(); +} diff --git a/electron/mods/README.txt b/electron/mods/README.txt new file mode 100644 index 00000000..666cc18f --- /dev/null +++ b/electron/mods/README.txt @@ -0,0 +1,6 @@ +Here you can place mods. Every mod should be a single file ending with ".js". + +--- WARNING --- +Mods can potentially access to your filesystem. +Please only install mods from trusted sources and developers. +--- WARNING --- diff --git a/electron/package.json b/electron/package.json index d21aff71..a0b97f6f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -9,13 +9,13 @@ "startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local", "start": "electron --disable-direct-composition --in-process-gpu ." }, - "devDependencies": { - "electron": "10.4.3" - }, + "devDependencies": {}, "optionalDependencies": { - "shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v82" + "shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v99" }, "dependencies": { - "async-lock": "^1.2.8" + "async-lock": "^1.2.8", + "electron": "16.0.7", + "electron-window-state": "^5.0.3" } } diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 00000000..c6336230 --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,7 @@ +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("ipcRenderer", { + invoke: ipcRenderer.invoke.bind(ipcRenderer), + on: ipcRenderer.on.bind(ipcRenderer), + send: ipcRenderer.send.bind(ipcRenderer), +}); diff --git a/electron/steam.js b/electron/steam.js index 464b7924..7faceb04 100644 --- a/electron/steam.js +++ b/electron/steam.js @@ -13,7 +13,6 @@ try { // greenworks is not installed console.warn("Failed to load steam api:", err); } - function init(isDev) { if (!greenworks) { return; diff --git a/electron/yarn.lock b/electron/yarn.lock index db2b6278..22eb0a46 100644 --- a/electron/yarn.lock +++ b/electron/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@electron/get@^1.0.1": - version "1.12.4" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.4.tgz#a5971113fc1bf8fa12a8789dc20152a7359f06ab" - integrity sha512-6nr9DbJPUR9Xujw6zD3y+rS95TyItEVM0NVjt1EehY2vUWfIgPiIPVHxCvaTS0xr2B+DRxovYVKbuOWqC35kjg== +"@electron/get@^1.13.0": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.13.1.tgz#42a0aa62fd1189638bd966e23effaebb16108368" + integrity sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA== dependencies: debug "^4.1.1" env-paths "^2.2.0" @@ -15,7 +15,7 @@ semver "^6.2.0" sumchecker "^3.0.1" optionalDependencies: - global-agent "^2.0.2" + global-agent "^3.0.0" global-tunnel-ng "^2.7.1" "@sindresorhus/is@^0.14.0": @@ -30,10 +30,10 @@ dependencies: defer-to-connect "^1.0.1" -"@types/node@^12.0.12": - version "12.20.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.5.tgz#4ca82a766f05c359fd6c77505007e5a272f4bb9b" - integrity sha512-5Oy7tYZnu3a4pnJ//d4yVvOImExl4Vtwf0D40iKUlU+XlUsyV9iyFWyCFlwy489b72FMAik/EFwRkNLjjOdSPg== +"@types/node@^14.6.2": + version "14.18.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.5.tgz#0dd636fe7b2c6055cbed0d4ca3b7fb540f130a96" + integrity sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A== async-lock@^1.2.8: version "1.2.8" @@ -93,11 +93,6 @@ config-chain@^1.1.11: ini "^1.3.4" proto-list "~1.2.1" -core-js@^3.6.5: - version "3.9.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae" - integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg== - core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -146,13 +141,21 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -electron@10.4.3: - version "10.4.3" - resolved "https://registry.yarnpkg.com/electron/-/electron-10.4.3.tgz#8d1c0f5e562d1b78dcec8074c0d59e58137fd508" - integrity sha512-qL8XZBII9KQHr1+YmVMj1AqyTR2I8/lxozvKEWoKKSkF8Hl6GzzxrLXRfISP7aDAvsJEyyhc6b2/42ME8hG5JA== +electron-window-state@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/electron-window-state/-/electron-window-state-5.0.3.tgz#4f36d09e3f953d87aff103bf010f460056050aa8" + integrity sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg== dependencies: - "@electron/get" "^1.0.1" - "@types/node" "^12.0.12" + jsonfile "^4.0.0" + mkdirp "^0.5.1" + +electron@16.0.7: + version "16.0.7" + resolved "https://registry.yarnpkg.com/electron/-/electron-16.0.7.tgz#87eaccd05ab61563d3c17dfbad2949bba7ead162" + integrity sha512-/IMwpBf2svhA1X/7Q58RV+Nn0fvUJsHniG4TizaO7q4iKFYSQ6hBvsLz+cylcZ8hRMKmVy5G1XaMNJID2ah23w== + dependencies: + "@electron/get" "^1.13.0" + "@types/node" "^14.6.2" extract-zip "^1.0.3" encodeurl@^1.0.2: @@ -222,13 +225,12 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -global-agent@^2.0.2: - version "2.1.12" - resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-2.1.12.tgz#e4ae3812b731a9e81cbf825f9377ef450a8e4195" - integrity sha512-caAljRMS/qcDo69X9BfkgrihGUgGx44Fb4QQToNQjsiWh+YlQ66uqYVAdA8Olqit+5Ng0nkz09je3ZzANMZcjg== +global-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" + integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== dependencies: boolean "^3.0.1" - core-js "^3.6.5" es6-error "^4.1.1" matcher "^3.0.0" roarr "^2.15.3" @@ -357,7 +359,7 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp@^0.5.4: +mkdirp@^0.5.1, mkdirp@^0.5.4: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -503,9 +505,9 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" -"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v82": +"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v99": version "0.1.0" - resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#8aa3bfd3b569eb5695fc8a585a3f2ee3ed2db290" + resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#b638501e81bba324923fc1b9d9aadc925da9b2c6" sprintf-js@^1.1.2: version "1.1.2" diff --git a/electron_wegame/index.js b/electron_wegame/index.js index 23c277c4..2c183f15 100644 --- a/electron_wegame/index.js +++ b/electron_wegame/index.js @@ -49,9 +49,12 @@ function createWindow() { // fullscreen: true, autoHideMenuBar: true, webPreferences: { - nodeIntegration: true, - webSecurity: false, - contextIsolation: false, + nodeIntegration: false, + webSecurity: true, + sandbox: true, + + contextIsolation: true, + preload: path.join(__dirname, "preload.js"), }, allowRunningInsecureContent: false, }); @@ -165,20 +168,20 @@ async function writeFileSafe(filename, contents) { console.warn(prefix, "Concurrent write process on", filename); } - await fileLock.acquire(filename, async () => { + fileLock.acquire(filename, async () => { console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId); if (!fs.existsSync(filename)) { // this one is easy console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename)); - fs.writeFileSync(filename, contents, { encoding: "utf8" }); + await fs.promises.writeFile(filename, contents, "utf8"); return; } // first, write a temporary file (.tmp-XXX) const tempName = filename + ".tmp-" + transactionId; console.log(prefix, "Writing temporary file", niceFileName(tempName)); - fs.writeFileSync(tempName, contents, { encoding: "utf8" }); + await fs.promises.writeFile(tempName, contents, "utf8"); // now, rename the original file to (.backup-XXX) const oldTemporaryName = filename + ".backup-" + transactionId; @@ -189,7 +192,7 @@ async function writeFileSafe(filename, contents) { "to", niceFileName(oldTemporaryName) ); - fs.renameSync(filename, oldTemporaryName); + await fs.promises.rename(filename, oldTemporaryName); // now, rename the temporary file (.tmp-XXX) to the target console.log( @@ -199,7 +202,7 @@ async function writeFileSafe(filename, contents) { "to the original", niceFileName(filename) ); - fs.renameSync(tempName, filename); + await fs.promises.rename(tempName, filename); // we are done now, try to create a backup, but don't fail if the backup fails try { @@ -208,82 +211,43 @@ async function writeFileSafe(filename, contents) { if (fs.existsSync(backupFileName)) { console.log(prefix, "Deleting old backup file", niceFileName(backupFileName)); // delete the old backup - fs.unlinkSync(backupFileName); + await fs.promises.unlink(backupFileName); } // rename the old file to the new backup file console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location"); - fs.renameSync(oldTemporaryName, backupFileName); + await fs.promises.rename(oldTemporaryName, backupFileName); } catch (ex) { console.error(prefix, "Failed to switch backup files:", ex); } }); } -async function performFsJob(job) { - const fname = path.join(storePath, job.filename); - +ipcMain.handle("fs-job", async (event, job) => { + const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/i, ""); + const fname = path.join(storePath, filenameSafe); switch (job.type) { case "read": { if (!fs.existsSync(fname)) { - return { - // Special FILE_NOT_FOUND error code - error: "file_not_found", - }; - } - - try { - const data = fs.readFileSync(fname, { encoding: "utf8" }); - return { - success: true, - data, - }; - } catch (ex) { - console.error(ex); - return { - error: ex, - }; + // Special FILE_NOT_FOUND error code + return { error: "file_not_found" }; } + return await fs.promises.readFile(fname, "utf8"); } case "write": { - try { - writeFileSafe(fname, job.contents); - return { - success: true, - data: job.contents, - }; - } catch (ex) { - console.error(ex); - return { - error: ex, - }; - } + await writeFileSafe(fname, job.contents); + return job.contents; } case "delete": { - try { - fs.unlinkSync(fname); - } catch (ex) { - console.error(ex); - return { - error: ex, - }; - } - - return { - success: true, - data: null, - }; + await fs.promises.unlink(fname); + return; } default: - throw new Error("Unkown fs job: " + job.type); + throw new Error("Unknown fs job: " + job.type); } -} - -ipcMain.on("fs-job", async (event, arg) => { - const result = await performFsJob(arg); - event.sender.send("fs-response", { id: arg.id, result }); }); + wegame.init(isDev); wegame.listen(); diff --git a/gulp/gulpfile.js b/gulp/gulpfile.js index 0f4f4185..32a751fd 100644 --- a/gulp/gulpfile.js +++ b/gulp/gulpfile.js @@ -146,7 +146,7 @@ gulp.task("main.webserver", () => { */ function serve({ version = "web" }) { browserSync.init({ - server: buildFolder, + server: [buildFolder, path.join(baseDir, "mod_examples")], port: 3005, ghostMode: { clicks: false, diff --git a/gulp/mod.js b/gulp/mod.js new file mode 100644 index 00000000..87912bb3 --- /dev/null +++ b/gulp/mod.js @@ -0,0 +1,3 @@ +module.exports = function (source, map) { + return source + `\nexport let $s=(n,v)=>eval(n+"=v")`; +}; diff --git a/gulp/package.json b/gulp/package.json index 2a17b4fd..adc4389f 100644 --- a/gulp/package.json +++ b/gulp/package.json @@ -46,6 +46,7 @@ "postcss": ">=5.0.0", "promise-polyfill": "^8.1.0", "query-string": "^6.8.1", + "raw-loader": "^4.0.2", "rusha": "^0.8.13", "serialize-error": "^3.0.0", "stream-browserify": "^3.0.0", diff --git a/gulp/standalone.js b/gulp/standalone.js index 81b41929..75f0b7a8 100644 --- a/gulp/standalone.js +++ b/gulp/standalone.js @@ -143,7 +143,7 @@ function gulptasksStandalone($, gulp) { packager({ dir: tempDestBuildDir, - appCopyright: "Tobias Springer", + appCopyright: "tobspr Games", appVersion: getVersion(), buildVersion: "1.0.0", arch, diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index 14987cfa..3c07d7cc 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -71,6 +71,7 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w type: "javascript/auto", }, { test: /\.(png|jpe?g|svg)$/, loader: "ignore-loader" }, + { test: /\.nobuild/, loader: "ignore-loader" }, { test: /\.md$/, use: [ @@ -92,6 +93,9 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w end: "typehints:end", }, }, + { + loader: path.resolve(__dirname, "mod.js"), + }, ], }, { diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index fd7551e0..fdd477b9 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -145,7 +145,7 @@ module.exports = ({ braces: false, ecma: es6 ? 6 : 5, preamble: - "/* shapez.io Codebase - Copyright 2020 Tobias Springer - " + + "/* shapez.io Codebase - Copyright 2022 tobspr Games - " + getVersion() + " @ " + getRevision() + @@ -177,6 +177,7 @@ module.exports = ({ type: "javascript/auto", }, { test: /\.(png|jpe?g|svg)$/, loader: "ignore-loader" }, + { test: /\.nobuild/, loader: "ignore-loader" }, { test: /\.js$/, enforce: "pre", diff --git a/gulp/yarn.lock b/gulp/yarn.lock index f4f3ba7f..93ef55b1 100644 --- a/gulp/yarn.lock +++ b/gulp/yarn.lock @@ -989,6 +989,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/json-schema@^7.0.8": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz" @@ -1237,6 +1242,11 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz" integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv@^4.7.0: version "4.11.8" resolved "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz" @@ -1255,6 +1265,16 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.5.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz" @@ -7407,6 +7427,15 @@ loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4 emojis-list "^3.0.0" json5 "^1.0.1" +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + localtunnel@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.0.tgz" @@ -10227,6 +10256,14 @@ raw-body@^2.3.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rcedit@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/rcedit/-/rcedit-2.0.0.tgz" @@ -10869,6 +10906,15 @@ schema-utils@^2.6.5: ajv "^6.12.0" ajv-keywords "^3.4.1" +schema-utils@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + seek-bzip@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz" diff --git a/mod_examples/README.md b/mod_examples/README.md new file mode 100644 index 00000000..04702576 --- /dev/null +++ b/mod_examples/README.md @@ -0,0 +1,59 @@ +# 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](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. + +## 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 | +| [storing_data_in_savegame.js](storing_data_in_savegame.js) | Shows how to store custom (structured) data in the savegame | Storing custom data in savegame | +| [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 + +| 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 | +| [mirrored_cutter.js](mirrored_cutter.js) | Adds a mirorred variant of the cutter | Adding a new variant to existing buildings | + +### 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 new file mode 100644 index 00000000..6b92e769 --- /dev/null +++ b/mod_examples/add_building_basic.js @@ -0,0 +1,67 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Add new basic building", + version: "1", + id: "add-building-basic", + description: "Shows how to add a new basic building", + minimumGameVersion: ">=1.5.0", +}; + +class MetaDemoModBuilding extends shapez.ModMetaBuilding { + constructor() { + super("demoModBuilding"); + } + + static getAllVariantCombinations() { + return [ + { + variant: shapez.defaultBuildingVariant, + name: "A test name", + description: "A test building", + + regularImageBase64: RESOURCES["demoBuilding.png"], + blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"], + tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"], + }, + ]; + } + + getSilhouetteColor() { + return "red"; + } + + setupEntityComponents(entity) { + // Here you can add components, for example an ItemProcessorComponent. + // To get an idea what you can do with the builtin components, have a look + // at the builtin buildings in + } +} + +class Mod extends shapez.Mod { + init() { + // Register the new building + this.modInterface.registerNewBuilding({ + metaClass: MetaDemoModBuilding, + buildingIconBase64: RESOURCES["demoBuilding.png"], + }); + + // Add it to the regular toolbar + this.modInterface.addNewBuildingToToolbar({ + toolbar: "regular", + location: "primary", + metaClass: MetaDemoModBuilding, + }); + } +} + +//////////////////////////////////////////////////////////////////////// + +const RESOURCES = { + "demoBuilding.png": + "data:image/png;base64,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", + + "demoBuildingBlueprint.png": + "data:image/png;base64,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", +}; diff --git a/mod_examples/add_building_flipper.js b/mod_examples/add_building_flipper.js new file mode 100644 index 00000000..03442499 --- /dev/null +++ b/mod_examples/add_building_flipper.js @@ -0,0 +1,130 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Add a flipper building", + version: "1", + 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 +shapez.enumItemProcessorTypes.flipper = "flipper"; + +// For now, flipper always has the same speed +shapez.MOD_ITEM_PROCESSOR_SPEEDS.flipper = () => 10; + +// Declare a handler for the processor so we define the "flip" operation +shapez.MOD_ITEM_PROCESSOR_HANDLERS.flipper = function (payload) { + const shapeDefinition = payload.items.get(0).definition; + + // Flip bottom with top on a new, cloned item (NEVER modify the incoming item!) + const newLayers = shapeDefinition.getClonedLayers(); + newLayers.forEach(layer => { + const tr = layer[shapez.TOP_RIGHT]; + const br = layer[shapez.BOTTOM_RIGHT]; + const bl = layer[shapez.BOTTOM_LEFT]; + const tl = layer[shapez.TOP_LEFT]; + + layer[shapez.BOTTOM_LEFT] = tl; + layer[shapez.BOTTOM_RIGHT] = tr; + + layer[shapez.TOP_LEFT] = bl; + layer[shapez.TOP_RIGHT] = br; + }); + + const newDefinition = new shapez.ShapeDefinition({ layers: newLayers }); + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(newDefinition), + }); +}; + +// Create the building +class MetaModFlipperBuilding extends shapez.ModMetaBuilding { + constructor() { + super("modFlipperBuilding"); + } + + static getAllVariantCombinations() { + return [ + { + name: "Flipper", + description: "Flipps/Mirrors shapez from top to bottom", + variant: shapez.defaultBuildingVariant, + + regularImageBase64: RESOURCES["flipper.png"], + blueprintImageBase64: RESOURCES["flipper.png"], + tutorialImageBase64: RESOURCES["flipper.png"], + }, + ]; + } + + getSilhouetteColor() { + return "red"; + } + + getAdditionalStatistics(root) { + const speed = root.hubGoals.getProcessorBaseSpeed(shapez.enumItemProcessorTypes.flipper); + return [[shapez.T.ingame.buildingPlacement.infoTexts.speed, shapez.formatItemsPerSecond(speed)]]; + } + + getIsUnlocked(root) { + return true; + } + + setupEntityComponents(entity) { + // Accept shapes from the bottom + entity.addComponent( + new shapez.ItemAcceptorComponent({ + slots: [ + { + pos: new shapez.Vector(0, 0), + direction: shapez.enumDirection.bottom, + filter: "shape", + }, + ], + }) + ); + + // Process those shapes with tye processor type "flipper" (which we added above) + entity.addComponent( + new shapez.ItemProcessorComponent({ + inputsPerCharge: 1, + processorType: shapez.enumItemProcessorTypes.flipper, + }) + ); + + // Eject the result to the top + entity.addComponent( + new shapez.ItemEjectorComponent({ + slots: [{ pos: new shapez.Vector(0, 0), direction: shapez.enumDirection.top }], + }) + ); + } +} + +class Mod extends shapez.Mod { + init() { + // Register the new building + this.modInterface.registerNewBuilding({ + metaClass: MetaModFlipperBuilding, + buildingIconBase64: RESOURCES["flipper.png"], + }); + + // Add it to the regular toolbar + this.modInterface.addNewBuildingToToolbar({ + toolbar: "regular", + location: "primary", + metaClass: MetaModFlipperBuilding, + }); + } +} + +//////////////////////////////////////////////////////////////////////// + +const RESOURCES = { + "flipper.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LmRhYmFjYmIsIDIwMjEvMDQvMTQtMDA6Mzk6NDQgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCAyMy4wIChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkZDMkFGQkY5NkUyQTExRUM5QUY0OTQyNUQyODU2NURGIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkZDMkFGQkZBNkUyQTExRUM5QUY0OTQyNUQyODU2NURGIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RkMyQUZCRjc2RTJBMTFFQzlBRjQ5NDI1RDI4NTY1REYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RkMyQUZCRjg2RTJBMTFFQzlBRjQ5NDI1RDI4NTY1REYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz43izVKAAATMklEQVR42uzda3QU533H8b/u97tW2tUFjA0YA8YYkMHESgoB14a6iVv7nNTN6eUcjBO3dU7rtGnavmybXnL8MiRv01M7rp3WdmzjxNjIlgQCIRsJkHTACAmJO8LIGDBKDJ3/aEUwF+mZnZnd2d3v55zniMvsXPX8Zp555pnN2LDxGQGQnjLZBQABACANZbML0lKtVZqt8oBV2qzSapUT7BYCAKlrmVWejlb6Odf8+3eiPw9Ew+BHVtnF7qIJgNTx91bptMqfX1f5rzUn+v+d0elBACDJ1VvlNav8wOHnfhD9XD27kCYAktcmqzwS42cnP/f77EauAJCcl/2PuJzHIzQHCAAkn/IYLvunag6Us0sJACTX2d9L32eXEgBIDnOt8j2P5/l3VrmTXZt6uAmY5O5etPgLf9/Ts/u7Pi3qWWtZG6//R2t5HAQCwDMZHBJnrquA663ypOlnI3X1cuzoEdPJn7SW9Qvr5+vsdVeuEAAiOTLxRNrk46jzrRKySi6/H/FRXl4h1dUhuXjxgpz9+GPTj73GnnPtvFVGrDIkv30MW3/+JtUD4G6rPGGVVVZZzu9B4mRmZko4Umf/ORyuk0/GxuTy5cvsmPgokon7KVoevObfO6zSYpUXrRK3dlU8bgLqZekbVumRibvTVP4EC4VqJCcnZ+JSzPqpf0fCrYjWjw+j9WV9KgTApmibcR3HNxhycnKl+roKX20HAq2vAFkXrTc/Fp/vi/kVAF+KnvG/xbEMlnAkYjcBbmwSRNg5wfNUtB49kEwBoJW+LdrmR4AUFhbZN/9uRv+9sKiInRQ8C2XiRuGzyRAA/x697EcA1dVPPbivro7BfwH2Q6v8Z5AD4K9l4okxBFBFRaUUFBROOY3+v06HwPputJ4FLgC0jfIcxyeYtI1fGzZr44fDN94jQKA85+U9AS+OdAaX/cEWqqm92u03nWztFqypZacF2ybxqHcg26OVWej0Q6WlZbJg4QK5beZtUl1dbd+AysrK4tAaeGfLFtmxo8No2tzcXAmFQs4Cw5r+4zOjMj4+bjT98uUr5Ktr1nBgDOg+PXfunIyNnZWhoSHp6+2Vs2fPOp3Nwmi9c93LlrVkqavnctY5vfTXiv/Qw+tk/fr1MmvW7Vabs0Ly8vK47DR08uRJ+cVrrxpPX9/QKPn5Bc4u6TIy7CsG/SU1ceTIiNw5b54U0YswfYWzTnKFhRP3WmbNmiXLmpqkqqpajh09JpcuXXIyK33Ja5dV9ieyCfC0k4nnzJ0rG596ShYsWGD/ksG5zp07jafVCllWFtu7PPRzRUXFvqwXvhi2Wh8m64Wf9c/rAFgkDh5XbLrvPnnsscftS1LEpr+/X7q7zR8Tj9Q1uFpexEG3oK5Xf18fBylGWi++9vVH5b7ljq7IH47Ww4QEwDdMJ5w/f76sXfsgZ32XPugyf11/RWWVFBQUuFqefr7Smo+prq4uDpJLa9asteuLH/XQ6wBYZdrmX7f+9ziyLm1rb5fBwUGzg2q1M8PhsCfLrbXmk2l4c3ZoaNBeT7ij9UXrjZf10OsA0N6DFUZrt3oVl/0uXbx4UVpathpPX1NTK9nZOZ4sW+dT46BbUNdT1xfumgOrv7radPIV4qI3L9YAMHoQoayszLqcWcARdam9rdXRL4++6MNLOr/c3Dxf1hc3d9dd8+3642V99DIAms3a/tztd+vE8eOy08Ed9kik3vN9rvOL1NUZT6/rq+sNd/vcwcmzOd4BYJQ4M2+byZF0qXNXp/G0RcXFUmp+1nBE26Q6f+P17uzk4LnkoP7E/QrA6Mk/3jTjTl9fr/R0dxufMfwezafzN7266Onplt7eXg6iCw7qz8J4B0C1yUT6xBNi17XLvFtNnyxz+sSfUzr/ikrz0YJOui3hqv6E4hkAegowuq3Ps/2xa29vk8OHh8R0P5uO9nOrtjZifFwPHz5sbwdi46D+5EiMg4N4AD+AtBvtvZYW4+knuv3i84JnXY6TbkHdDroFg4sACKC21veNp9WBVFUed/tNR5eny/Vje0AApLWjR486uoOu7/ePd1erLi8cMb/hqNuj2wUCANPocnDjrLi4xMkjo54qLS2V4pISX7YLBEBa2rt3r+zp6TE+C0cS/BJPJw8d6Xbp9oEAwC046TbTUXr5+fkJXV9dvpPRgnQLEgC4hStXrsjIyIjRtBPdfuFArLeuh2l3lW6fbicIAFxneHjYvNLVaqULxje763ro+vixnSAA0igADhtNl5dnXXZXVQdq3XV9dL283E4QAFwB3ISOygvaCEsnowW5AgiWbHZBMDQ2NsrAwYPTTjd4aCDptxNcAeCGijGD7QQBkM5XAGwnCIA0pe3ohoaGlN5G3T7eEEUA4BaWLF3G9oEASFcLFy60KsnSFK38S+3tAwGAKTz00MNSU5ta386r26PbBQIABjZseDJlrgR0O3R7EEw8BxDgK4GGhkZ7AI3pGIEg0Rt+2ubnsp8AgIt7Alr0a6P1CTp9jHb48LAVCMMBrPCN0jij0e7n164+J28MAgGAKWhlmj17tl0A7gEAIAAAEAAACAAABAAAAgAAAQCAAABAAAAgAAAQAAABwC4ACAAABAAAAgAAAQCAAABAAAAgAAAQAAAIAAAEAAACAAABAIAAAEAAACAAABAAAAgAAAQAAAIAAAEAgAAAQAAAIAAAEAAACAAABAAAAgAAAQCAAABAAAAgAAAQAAAIAAAEAIBrZSfLivb398lbmzfLhQsXAruOVVVV8tjjj1s/q285zfj4uLz6yv/JgQMHYpr/E3/8TSkpKUnodiZiG1rff09aW1vjdhwrKyvtP8+cOVNCNTVSXR1K+H5P6wDY09MT6MqvRkdHpXffPmn+8lduOc3p06diqjiT8+/r3Sf3LV+R0O1MxDbEq/JPrqMWde12NjQ0yD2LF8vtt9+RMmGQLfDU8ePH2QkpamRkxC6qqalJVty/MumDgAAAYtDZ2WmX5uZmWb7ifsnNzU3K7eAmIOCCNk1+9sLzVpPhNAEApGvT4L9++lM5dGiAAADSkd6gfuH55+3eKgIASFP/+/OfJ1VzgAAAPKbNAX1WggAA0rQ58Mu3NhMAQLras2ePHD16hAAA0lV7WxsBkG7y8/PZCbDpY8RBvwogADw2Y+ZMdgKu2rd3b6DXL20eBV6zZo3vy9BRY7Nm3c5vfcDo6L57773XaNqxsTEZGBi4OhjIdQDs2ydrH/xdAiDREj2CDomjQ3udHn+vhp/r57UZUFdXTxMASBbz5t0lT//FX9pDgN06deoU9wCAZKMj/B79gz+UwsJCV/M5eeIEAQAkIx3vv3LlSlfzOHv2LAEAJKs7Zs9O2W0jAIBpTPWORwIAAAEApKpkfdsPAQB4YPJFoAQAkGZ0XH/H9u2u5lFeXk4AAMnovZatrh8LLisrIwCAZPP2r35pv/rbrYbGxsBuY9qMBdi5o8OzeWm/cCp3DaX7Jf/AwEHrzN/i2YAg/VoxAiDBtmzZ4um8/uqZ76Tkd8WloiNHjshL//PitNOdOXPGs0o/ac6cOYH+0hC+GShG5859QgAkCR2RF+t3Gbp196JFgd433AMAfKKDiPSLRAkAIA3pIKKgf2cgAQD4dPZffO+SwK8nAQD4YNXq1UnxjcEEAOAxvfN/zz2Lk2JdCQDAQ/oKsa99/dGkWV8CAPCIvn1YXyGWDJf+k3gOAPDozP+NP3oiqSo/AQB4oLm5WZavuD/pKj8BALg8669Zuzaw7/wnAHyid3mDPMAD/h9/fcRXvzsg2aVNAPzDP/4Tv7lwVenn3nmn/WhvKo0B4QoAKc/JdwNOysvPl1AoJHl5eSk99JsAQMqL5bsB0wXPAQAEAIB0RBMgyej31+vXTfspmbu1QACkNH1JpRcvqpyK9m//yZ/+GTubJgDSkX4Rht9XGSAAABAAAAgAAAQAAAIAAAEAgAAAQAAAIAAAEAAACAAABACAFA4AN9+zrq9tDgp9maiOtgsyff/dVEOC3WyDvp7rjtmzHX+uqakppuXpl3S6+d1JdRkbNj7j+DNWuWwyIS/iBNz513/5Zycn8ys0AQAQAAAIAAAEAAACAAABAIAAAAgAdgFAAAAgAAAQAAAIAAAEAAACAAABAIAAAEAAACAAABAAAAgAAAQAAAIAAAEAgAAAQAAAIAAAEAAACAAABAAAAgAAAQDAN9nsgvRz/vx5GT58WIZHhqWxoVEaZ8yQoqIidgwBgFR17Ngx6eraJSPDw3LmzJmr/965c6f9s7KyUhoaG2Xp0mUSiUTYYQQAUsX2bdtk69Z3p5xGQ0FLT3e3rFq1Wu5fuZIdRwAgmZ07d07e2vymHDhwwNHnNCxGrObBQw+vk5KSEnZkCuMmYAqLpfJP0s/p50EAIEkv+2Ot/NeGgM4HBACSyGeffTZtm99Jc0DnBwIASXT299K2be3sVAIAyUDv5G/f7m0AdGzfLqOjo+xcAgBB19Gx3Zf57tjRwc4lABBkH330kez+8EPj6SN19cbT6nw/cnlTEQQAfKRP+pkqL6+Q6uqQlFdU+DJ/EACII73xd9C6AjA66JmZEo7U2X8Oh+vsv5s4ePAgNwQJAASN026/UKhGcnJy7D/rT/27qZatW+kWJAAQJNvazc/KOTm5Un1dha+2AyHXeB7t7W3sdAIAQXD69GlHd/7DkcgNl/wTTQLzEYA7Ojrs5YIAQII56Z4rLCyyb/7djP57oYN3AtAtSAAgwfbv3y/du3cbT19XP3W3X52DbkFdri4fBAASxEm3XEVFpRQUFE45jf6/TufH8kEAwEPaHXdoYMDsIFtt/NqwWRs/HI4Ydwvq8ukWJAAQZ9oNp91xpkI1tVe7/aaTrd2C1vSm6BYkABBn7W3m3XC5ubkSCoUczV+n18/5sT4gAODCyZMnHd2B1yf+MjKcHWadfvJJQRO6PrpeSI8AuGKVcZMJP//8c/awxybf4mtCX/VdVlYe03L0c0VFxb6sF8w4qD+/jtbLuF0BGD0FcuHCBY6ih/r7+6W727zbL1LX4Gp5TkYL6nr19/VxkDzkoP6cincTYJ9RSpw6xVH00AdOuv0qq6SgoMDV8vTzldZ8THV1dXGQPHTqlHGzam+8A8Dors/g4CBH0SP6vL/p/szMypJwOOzJcmut+ej8TAwNDToal4Bp9ufgkKf10csAaDWZqLe3V65cucKRdOnixYvS0mLe7VdTUyvZ2TmeLFvnU+OkW9BaT11fuKP1RuuPl/Ux7lcAY2Nnpa+vl6PpUnub+fHV7jt90YeXdH65uXm+rC9uTuuN1p+gXgHoXccdJhO++867Mj4+zhGN0Ynjx2WngzvskUi9ZGRkeLoOOr9InXm3oK6vrjdio/VF640h7RP+TbwDwK7bJhN98smYvPnGGxzVGHXu6jSetqi4WErLynxZj9LSMnv+xuvd2cnBi5HWF603hra6WZabAHjRdMLe3n3y9tu/4n5ADJeB+mWdpmdpJ6P5YqHzN7266OnpdtKGRdQ7W7bY9cWPeuh1AOhv5mbjM4J1Wfjyyy/RHHCga5d5t5qO4svPL/B1fXT+FZXmowU/YLSgo8v+V195xel7FjZH62HMspYsXe7m83qd8oTpxGdGR2Xv3r1SXFJs31jyuq2aSvS1W6Zn/6ysLJl52yzjUXxu6EtFPj4zanQ1NzY2JplZmTJjxgwO6C3oftQrvZdfetn+RmaH/sYq+xMZALpwHWe6zPQDly5dsp9o6+nukfMXzusekOzsbHu0GoEwQbvRfvbCC8bT6xDe4jh9jbeGjB6nTz89ZzT90OCgLGtqMh6NmOouX75s7btP5eiRI7J794ey+c3N9k+tFw79xCrPuV2fjA0bn3E9D23yWWUhhzf+8vLyZM7ceXENTz1rHdjfH8svLbyhT/4tkhif//fqHsDV3werfJtjkhgTo/3ie+WkywtH6tn5ifNtLyq/VwGg9EGEZzku8VVcXGJ3zyVCaWlp3Jod+IJnxcWDP34FgETbI//B8YnfWThSl9izsB8PHWHaOvaclzP0+rbx92gOxIeO0svPz0/oOujynYwWhCt/68dVth/9Rj+2ygNW2cMx84d2+9V6NNrPLV2PLMPRgoiJ3vBrtsoP/Zi5Xx3HOiZ0UTQM4HWlq9VKlx2QMMq21we++Em0Hvn20kW/nxzR5sAj4uCJQUwtL8+67K6qDlZzxFofXS94ZnO03nxLPLrbn6gAUK9bZV00yf5NDEcR4uZ0VF7Qbrw5HS2Im9oRrR9LovXl9XgsNJ7XkXpP4PvRP+dE7xM0R38usEpNnNcnKQ0eGmAnJDcduqvv+toXvbRvjf78dSJWJlEVTjd2q9w4lDGd+5S+bJWWNNjO37HK+2l8nAM1JDabnRMYrWm0nYwLDwi+GCQ4LltlW4pv47bodoIAwE38iO0DAZC+/tsqm1J02zZFtw8EAKbwtLh8y0sAdUe3CwQADCxOoSuBTdHtAQEAh1cC35TkvTG4Lbr+nPkDjAdvgn9PQIsO+n9Afvvw1JcCuK46/mPyoRYtYxw+AgDe0Mr0RrSoID4wRd8+AQAqG7gHAIAAABBs/y/AAPho4dBfgj+jAAAAAElFTkSuQmCC", +}; diff --git a/mod_examples/base.js b/mod_examples/base.js new file mode 100644 index 00000000..f76ded9b --- /dev/null +++ b/mod_examples/base.js @@ -0,0 +1,20 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Base", + version: "1", + id: "base", + description: "The most basic mod", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +class Mod extends shapez.Mod { + init() { + // Start the modding here + } +} diff --git a/mod_examples/buildings_have_cost.js b/mod_examples/buildings_have_cost.js new file mode 100644 index 00000000..3dae84ae --- /dev/null +++ b/mod_examples/buildings_have_cost.js @@ -0,0 +1,89 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Patch Methods", + 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 { + init() { + // Define our currency + const CURRENCY = "CyCyCyCy:--------:CuCuCuCu"; + + // Make sure the currency is always pinned + this.modInterface.runAfterMethod(shapez.HUDPinnedShapes, "rerenderFull", function () { + this.internalPinShape({ + key: CURRENCY, + canUnpin: false, + className: "currency", + }); + }); + + // Style it + this.modInterface.registerCss(` + #ingame_HUD_PinnedShapes .shape.currency::after { + content: " "; + position: absolute; + display: inline-block; + width: $scaled(8px); + height: $scaled(8px); + top: $scaled(4px); + left: $scaled(-7px); + background: url('${RESOURCES["currency.png"]}') center center / contain no-repeat; + } + + .currencyIcon { + display: inline-block; + vertical-align: middle; + background: url('${RESOURCES["currency.png"]}') center center / contain no-repeat; + } + + .currencyIcon.small { + width: $scaled(11px); + height: $scaled(11px); + } + `); + + // Make the player start with some currency + this.modInterface.runAfterMethod(shapez.GameCore, "initNewGame", function () { + this.root.hubGoals.storedShapes[CURRENCY] = 100; + }); + + // Make belts have a cost + this.modInterface.replaceMethod(shapez.MetaBeltBuilding, "getAdditionalStatistics", function ( + $original, + [root, variant] + ) { + const oldStats = $original(root, variant); + oldStats.push(["Cost", "1 x "]); + return oldStats; + }); + + // Only allow placing an entity when there is enough currency + this.modInterface.replaceMethod(shapez.GameLogic, "checkCanPlaceEntity", function ( + $original, + [entity, options] + ) { + const storedCurrency = this.root.hubGoals.storedShapes[CURRENCY] || 0; + return storedCurrency > 0 && $original(entity, options); + }); + + // Take shapes when placing a building + this.modInterface.replaceMethod(shapez.GameLogic, "tryPlaceBuilding", function ($original, args) { + const result = $original(...args); + if (result && result.components.Belt) { + this.root.hubGoals.storedShapes[CURRENCY]--; + } + return result; + }); + } +} + +const RESOURCES = { + "currency.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAIAAAACAAF+ftPjAAAFwWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZGFiYWNiYiwgMjAyMS8wNC8xNC0wMDozOTo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjAgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAxLTE2VDE2OjAzOjE1KzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMS0xNlQxNjowNDowMyswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMS0xNlQxNjowNDowMyswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6M2IxMTM4ZjEtNzdmMi00MzcyLTg4ZDktZTgzN2I4NzlkNGUwIiB4bXBNTTpEb2N1bWVudElE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", +}; diff --git a/mod_examples/class_extensions.js b/mod_examples/class_extensions.js new file mode 100644 index 00000000..8647fd45 --- /dev/null +++ b/mod_examples/class_extensions.js @@ -0,0 +1,32 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Class Extensions", + version: "1", + id: "class-extensions", + description: "Shows how to extend builtin classes", + minimumGameVersion: ">=1.5.0", +}; + +const BeltExtension = ({ $super, $old }) => ({ + getShowWiresLayerPreview() { + // Access the old method + return !$old.getShowWiresLayerPreview(); + }, + + getIsReplaceable() { + // Instead of super, use $super + return $super.getIsReplaceable.call(this); + }, + + getIsRemoveable() { + return false; + }, +}); + +class Mod extends shapez.Mod { + init() { + this.modInterface.extendClass(shapez.MetaBeltBuilding, BeltExtension); + } +} diff --git a/mod_examples/custom_css.js b/mod_examples/custom_css.js new file mode 100644 index 00000000..0d28fda7 --- /dev/null +++ b/mod_examples/custom_css.js @@ -0,0 +1,44 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Add custom CSS", + version: "1", + id: "custom-css", + description: "Shows how to add custom css", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +class Mod extends shapez.Mod { + init() { + // Notice that, since the UI is scaled dynamically, every pixel value + // should be wrapped in '$scaled()' (see below) + + this.modInterface.registerCss(` + * { + font-family: "Comic Sans", "Comic Sans MS", "ComicSans", Tahoma !important; + } + + #state_MainMenuState { + background: #9dc499 url('${RESOURCES["cat.png"]}') top left repeat !important; + } + + #state_MainMenuState .fullscreenBackgroundVideo { + display: none !important; + } + + #state_MainMenuState .mainContainer, #state_MainMenuState .modsOverview { + border: $scaled(5px) solid #000 !important; + } + `); + } +} + +const RESOURCES = { + "cat.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAGxAAABsQFhmCgOAAAE8mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZGFiYWNiYiwgMjAyMS8wNC8xNC0wMDozOTo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjAgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAxLTE1VDEzOjI3OjU1KzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMS0xNVQxMzozMDowMyswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMS0xNVQxMzozMDowMyswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTI2OGI2OWUtYTQ2Ni00YmNkLWJjNGYtM2VlNmUwOGI2NzA2IiB4bXBNTTpEb2N1bWVudElE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", +}; diff --git a/mod_examples/custom_drawing.js b/mod_examples/custom_drawing.js new file mode 100644 index 00000000..2dccab2d --- /dev/null +++ b/mod_examples/custom_drawing.js @@ -0,0 +1,63 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: custom drawing", + version: "1", + id: "base", + description: "Displays an indicator on every item processing building when its working", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +class ItemProcessorStatusGameSystem extends shapez.GameSystem { + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const processorComp = entity.components.ItemProcessor; + if (!processorComp) { + continue; + } + + const staticComp = entity.components.StaticMapEntity; + + const context = parameters.context; + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + + // Culling for better performance + if (parameters.visibleRect.containsCircle(center.x, center.y, 40)) { + // Circle + context.fillStyle = processorComp.ongoingCharges.length === 0 ? "#aaa" : "#53cf47"; + context.strokeStyle = "#000"; + context.lineWidth = 1; + + context.beginCircle(center.x + 5, center.y + 5, 4); + context.fill(); + context.stroke(); + } + } + } +} + +class Mod extends shapez.Mod { + init() { + // Register our game system + this.modInterface.registerGameSystem({ + id: "item_processor_status", + systemClass: ItemProcessorStatusGameSystem, + + // Specify at which point the update method will be called, + // in this case directly before the belt system. You can use + // before: "end" to make it the last system + before: "belt", + + // Specify where our drawChunk method should be called, check out + // map_chunk_view + drawHooks: ["staticAfter"], + }); + } +} diff --git a/mod_examples/custom_keybinding.js b/mod_examples/custom_keybinding.js new file mode 100644 index 00000000..0109833c --- /dev/null +++ b/mod_examples/custom_keybinding.js @@ -0,0 +1,32 @@ +// @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", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +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; + }, + }); + } +} diff --git a/mod_examples/custom_sub_shapes.js b/mod_examples/custom_sub_shapes.js new file mode 100644 index 00000000..3aea03cf --- /dev/null +++ b/mod_examples/custom_sub_shapes.js @@ -0,0 +1,46 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Custom Sub Shapes", + version: "1", + id: "custom-sub-shapes", + description: "Shows how to add custom sub shapes", + minimumGameVersion: ">=1.5.0", +}; + +class Mod extends shapez.Mod { + init() { + // Add a new type of sub shape ("Line", short code "L") + this.modInterface.registerSubShapeType({ + id: "line", + shortCode: "L", + + // Make it spawn on the map + weightComputation: distanceToOriginInChunks => + Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)), + + // This defines how to draw it + draw: ({ context, quadrantSize, layerScale }) => { + const quadrantHalfSize = quadrantSize / 2; + context.beginPath(); + context.moveTo(-quadrantHalfSize, quadrantHalfSize); + context.arc( + -quadrantHalfSize, + quadrantHalfSize, + quadrantSize * layerScale, + -Math.PI * 0.25, + 0 + ); + context.closePath(); + context.fill(); + context.stroke(); + }, + }); + + // Modify the goal of the first level to add our goal + this.signals.modifyLevelDefinitions.add(definitions => { + definitions[0].shape = "LuLuLuLu"; + }); + } +} diff --git a/mod_examples/custom_theme.js b/mod_examples/custom_theme.js new file mode 100644 index 00000000..a70d949f --- /dev/null +++ b/mod_examples/custom_theme.js @@ -0,0 +1,99 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Custom Game Theme", + version: "1", + id: "custom-theme", + description: "Shows how to add a custom game theme", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +class Mod extends shapez.Mod { + init() { + this.modInterface.registerGameTheme({ + id: "my-theme", + name: "My fancy theme", + theme: RESOURCES["my-theme.json"], + }); + } +} + +const RESOURCES = { + "my-theme.json": { + map: { + background: "#abc", + grid: "#ccc", + gridLineWidth: 1, + + selectionOverlay: "rgba(74, 163, 223, 0.7)", + selectionOutline: "rgba(74, 163, 223, 0.5)", + selectionBackground: "rgba(74, 163, 223, 0.2)", + + chunkBorders: "rgba(0, 30, 50, 0.03)", + + directionLock: { + regular: { + color: "rgb(74, 237, 134)", + background: "rgba(74, 237, 134, 0.2)", + }, + 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)", + }, + }, + + colorBlindPickerTile: "rgba(50, 50, 50, 0.4)", + + resources: { + shape: "#eaebec", + red: "#ffbfc1", + green: "#cbffc4", + blue: "#bfdaff", + }, + + chunkOverview: { + empty: "#a6afbb", + filled: "#c5ccd6", + beltColor: "#777", + }, + + wires: { + overlayColor: "rgba(97, 161, 152, 0.75)", + previewColor: "rgb(97, 161, 152, 0.4)", + highlightColor: "rgba(72, 137, 255, 1)", + }, + + connectedMiners: { + overlay: "rgba(40, 50, 60, 0.5)", + textColor: "#fff", + textColorCapped: "#ef5072", + background: "rgba(40, 50, 60, 0.8)", + }, + + zone: { + borderSolid: "rgba(23, 192, 255, 1)", + outerColor: "rgba(240, 240, 255, 0.5)", + }, + }, + + items: { + outline: "#55575a", + outlineWidth: 0.75, + circleBackground: "rgba(40, 50, 65, 0.1)", + }, + + shapeTooltip: { + background: "#dee1ea", + outline: "#54565e", + }, + }, +}; diff --git a/mod_examples/mirrored_cutter.js b/mod_examples/mirrored_cutter.js new file mode 100644 index 00000000..ae457a8c --- /dev/null +++ b/mod_examples/mirrored_cutter.js @@ -0,0 +1,81 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Mirrored Cutter Variant", + version: "1", + id: "mirrored-cutter", + description: "Shows how to add new variants to existing buildings", + minimumGameVersion: ">=1.5.0", +}; + +class Mod extends shapez.Mod { + init() { + shapez.enumCutterVariants.mirrored = "mirrored"; + + this.modInterface.addVariantToExistingBuilding( + shapez.MetaCutterBuilding, + shapez.enumCutterVariants.mirrored, + { + name: "Cutter (Mirrored)", + description: "A mirrored cutter", + + tutorialImageBase64: RESOURCES["cutter-mirrored.png"], + regularSpriteBase64: RESOURCES["cutter-mirrored.png"], + blueprintSpriteBase64: RESOURCES["cutter-mirrored.png"], + + dimensions: new shapez.Vector(2, 1), + + additionalStatistics(root) { + const speed = root.hubGoals.getProcessorBaseSpeed(shapez.enumItemProcessorTypes.cutter); + return [ + [ + shapez.T.ingame.buildingPlacement.infoTexts.speed, + shapez.formatItemsPerSecond(speed), + ], + ]; + }, + + isUnlocked(root) { + return true; + }, + } + ); + + // Extend instance methods + this.modInterface.extendClass(shapez.MetaCutterBuilding, ({ $old }) => ({ + updateVariants(entity, rotationVariant, variant) { + if (variant === shapez.enumCutterVariants.mirrored) { + entity.components.ItemEjector.setSlots([ + { pos: new shapez.Vector(0, 0), direction: shapez.enumDirection.top }, + { pos: new shapez.Vector(1, 0), direction: shapez.enumDirection.top }, + ]); + entity.components.ItemProcessor.type = shapez.enumItemProcessorTypes.cutter; + entity.components.ItemAcceptor.setSlots([ + { + pos: new shapez.Vector(1, 0), + direction: shapez.enumDirection.bottom, + filter: "shape", + }, + ]); + } else { + // Since we are changing the ItemAcceptor slots, we should reset + // it to the regular slots when we are not using our mirrored variant + entity.components.ItemAcceptor.setSlots([ + { + pos: new shapez.Vector(0, 0), + direction: shapez.enumDirection.bottom, + filter: "shape", + }, + ]); + $old.updateVariants.bind(this)(entity, rotationVariant, variant); + } + }, + })); + } +} + +const RESOURCES = { + "cutter-mirrored.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAADACAYAAAAN6LRnAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4VpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LmRhYmFjYmIsIDIwMjEvMDQvMTQtMDA6Mzk6NDQgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NGQwZTY5MmYtOTRlNy00MDQyLWFjY2ItNmU3OGEzMGU1N2ZjIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIwOTY4MzA0N0I4QTExRUNCQzRERjNBOEI5ODYyRkJBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIwOTY4MzAzN0I4QTExRUNCQzRERjNBOEI5ODYyRkJBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOmRmNmU1ODFjLTQ3ZDItYzE0OS05MmQzLWRhZDMyMTg5YTA3MiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk3MDYxZWYyLTY2ZGMtZjI0Zi1iZTMy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", +}; diff --git a/mod_examples/mod_settings.js b/mod_examples/mod_settings.js new file mode 100644 index 00000000..b87c138b --- /dev/null +++ b/mod_examples/mod_settings.js @@ -0,0 +1,32 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Mod Settings", + version: "1", + id: "mod-settings", + description: "Shows how to add settings to your mod", + minimumGameVersion: ">=1.5.0", + + settings: { + timesLaunched: 0, + }, +}; + +class Mod extends shapez.Mod { + init() { + // Increment the setting every time we launch the mod + this.settings.timesLaunched++; + this.saveSettings(); + + // Show a dialog in the main menu with the settings + this.signals.stateEntered.add(state => { + if (state instanceof shapez.MainMenuState) { + this.dialogs.showInfo( + "Welcome back", + `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 new file mode 100644 index 00000000..b09f5a20 --- /dev/null +++ b/mod_examples/modify_existing_building.js @@ -0,0 +1,27 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Modify existing building", + version: "1", + id: "modify-existing-building", + description: "Shows how to modify an existing building", + minimumGameVersion: ">=1.5.0", +}; + +class Mod extends shapez.Mod { + init() { + // Make Rotator always unlocked + this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getIsUnlocked", function () { + return true; + }); + + // Add some custom stats to the info panel when selecting the building + this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getAdditionalStatistics", function ( + root, + variant + ) { + return [["Awesomeness", 5]]; + }); + } +} diff --git a/mod_examples/modify_theme.js b/mod_examples/modify_theme.js new file mode 100644 index 00000000..de7f0ad2 --- /dev/null +++ b/mod_examples/modify_theme.js @@ -0,0 +1,24 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Modify Builtin Themes", + version: "1", + id: "modify-theme", + description: "Shows how to modify builtin themes", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +class Mod extends shapez.Mod { + init() { + shapez.THEMES.light.map.background = "#eee"; + shapez.THEMES.light.items.outline = "#000"; + + shapez.THEMES.dark.map.background = "#245"; + shapez.THEMES.dark.items.outline = "#fff"; + } +} diff --git a/mod_examples/modify_ui.js b/mod_examples/modify_ui.js new file mode 100644 index 00000000..4beb403d --- /dev/null +++ b/mod_examples/modify_ui.js @@ -0,0 +1,46 @@ +// @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", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +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; + } + + `); + } +} diff --git a/mod_examples/new_item_type.js b/mod_examples/new_item_type.js new file mode 100644 index 00000000..3cd52cef --- /dev/null +++ b/mod_examples/new_item_type.js @@ -0,0 +1,147 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: New Item Type (Fluids)", + 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 +const enumFluidType = { + water: "water", + oil: "oil", +}; + +// Define which color they should have on the map +const fluidColors = { + [enumFluidType.water]: "#477be7", + [enumFluidType.oil]: "#bc483a", +}; + +// The fluid item class (also see ColorItem and ShapeItem) +class FluidItem extends shapez.BaseItem { + static getId() { + return "fluid"; + } + + static getSchema() { + return shapez.types.enum(enumFluidType); + } + + serialize() { + return this.fluidType; + } + + deserialize(data) { + this.fluidType = data; + } + + getItemType() { + return "fluid"; + } + + /** + * @returns {string} + */ + getAsCopyableKey() { + return this.fluidType; + } + + /** + * @param {BaseItem} other + */ + equalsImpl(other) { + return this.fluidType === /** @type {FluidItem} */ (other).fluidType; + } + + /** + * @param {enumFluidType} fluidType + */ + constructor(fluidType) { + super(); + this.fluidType = fluidType; + } + + getBackgroundColorAsResource() { + return fluidColors[this.fluidType]; + } + + /** + * Draws the item to a canvas + * @param {CanvasRenderingContext2D} context + * @param {number} size + */ + drawFullSizeOnCanvas(context, size) { + if (!this.cachedSprite) { + this.cachedSprite = shapez.Loader.getSprite(`sprites/fluids/${this.fluidType}.png`); + } + this.cachedSprite.drawCentered(context, size / 2, size / 2, size); + } + + /** + * @param {number} x + * @param {number} y + * @param {number} diameter + * @param {DrawParameters} parameters + */ + 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.drawCachedCentered(parameters, x, y, realDiameter); + } +} + +/** + * Singleton instances. + * + * NOTICE: The game tries to instantiate as few instances as possible. + * Which means that if you have two types of fluids in this case, there should + * ONLY be 2 instances of FluidItem at *any* time. + * + * This works by having a map from fluid type to the FluidItem singleton. + * Additionally, all items are and should be immutable. + * @type {Object} + */ +const FLUID_ITEM_SINGLETONS = {}; + +for (const fluidType in enumFluidType) { + FLUID_ITEM_SINGLETONS[fluidType] = new FluidItem(fluidType); +} + +class Mod extends shapez.Mod { + init() { + // Register the sprites + this.modInterface.registerSprite("sprites/fluids/oil.png", RESOURCES["oil.png"]); + this.modInterface.registerSprite("sprites/fluids/water.png", RESOURCES["water.png"]); + + // Make the item spawn on the map + this.modInterface.runAfterMethod(shapez.MapChunk, "generatePatches", function ({ + rng, + chunkCenter, + distanceToOriginInChunks, + }) { + // Generate a simple patch + // ALWAYS use rng and NEVER use Math.random() otherwise the map will look different + // every time you resume the game + if (rng.next() > 0.8) { + const fluidType = rng.choice(Array.from(Object.keys(enumFluidType))); + this.internalGeneratePatch(rng, 4, FLUID_ITEM_SINGLETONS[fluidType]); + } + }); + } +} + +/////////////////////////////////////// + +const RESOURCES = { + "oil.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAOxAAADsQH1g+1JAAAE8mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZGFiYWNiYiwgMjAyMS8wNC8xNC0wMDozOTo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjAgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAxLTE3VDEyOjIyOjUxKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMS0xN1QxMjoyMzozMCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMS0xN1QxMjoyMzozMCswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Y2EzMGQxMDEtZWU5Yy00Mzc2LTgyOGEtZDM5ZmFkN2ViZTYyIiB4bXBNTTpEb2N1bWVudElE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", + + "water.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAATEAAAExAE8zNSDAAAE8mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZGFiYWNiYiwgMjAyMS8wNC8xNC0wMDozOTo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjAgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAxLTE3VDEyOjI0OjA3KzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMS0xN1QxMjoyNTowNiswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMS0xN1QxMjoyNTowNiswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NmU3YjM1ZDctOTAyNi00ZjNlLTkxNGItZTc0NjJhMzM3MGE4IiB4bXBNTTpEb2N1bWVudElE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", +}; diff --git a/mod_examples/notification_blocks.js b/mod_examples/notification_blocks.js new file mode 100644 index 00000000..23f95943 --- /dev/null +++ b/mod_examples/notification_blocks.js @@ -0,0 +1,314 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Notification Blocks", + version: "1", + 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", +}; + +//////////////////////////////////////////////////////////////////////// +// This is the component storing which text the block should show as +// a notification. +class NotificationBlockComponent extends shapez.Component { + static getId() { + return "NotificationBlock"; + } + + static getSchema() { + // Here you define which properties should be saved to the savegame + // and get automatically restored + return { + notificationText: shapez.types.string, + lastStoredInput: shapez.types.bool, + }; + } + + constructor() { + super(); + this.notificationText = "Test"; + this.lastStoredInput = false; + } +} + +//////////////////////////////////////////////////////////////////////// +// The game system to trigger notifications when the signal changes +class NotificationBlocksSystem extends shapez.GameSystemWithFilter { + constructor(root) { + // By specifying the list of components, `this.allEntities` will only + // contain entities which have *all* of the specified components + super(root, [NotificationBlockComponent]); + + // Ask for a notification text once an entity is placed + this.root.signals.entityManuallyPlaced.add(entity => { + const editorHud = this.root.hud.parts.notificationBlockEdit; + if (editorHud) { + editorHud.editNotificationText(entity, { deleteOnCancel: true }); + } + }); + } + + update() { + if (!this.root.gameInitialized) { + // Do not start updating before the wires network was + // computed to avoid dispatching all notifications + return; + } + + // Go over all notification blocks and check if the signal changed + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + + // Compute if the bottom pin currently has a truthy input + const pinsComp = entity.components.WiredPins; + const network = pinsComp.slots[0].linkedNetwork; + + let currentInput = false; + + if (network && network.hasValue()) { + const value = network.currentValue; + if (value && shapez.isTruthyItem(value)) { + currentInput = true; + } + } + + // If the value changed, show the notification if its truthy + const notificationComp = entity.components.NotificationBlock; + if (currentInput !== notificationComp.lastStoredInput) { + notificationComp.lastStoredInput = currentInput; + if (currentInput) { + this.root.hud.signals.notification.dispatch( + notificationComp.notificationText, + shapez.enumNotificationType.info + ); + } + } + } + } +} + +//////////////////////////////////////////////////////////////////////// +// The actual notification block building +class MetaNotificationBlockBuilding extends shapez.ModMetaBuilding { + constructor() { + super("notification_block"); + } + + static getAllVariantCombinations() { + return [ + { + variant: shapez.defaultBuildingVariant, + name: "Notification Block", + description: "Shows a predefined notification on screen when receiving a truthy signal", + + regularImageBase64: RESOURCES["notification_block.png"], + blueprintImageBase64: RESOURCES["notification_block.png"], + tutorialImageBase64: RESOURCES["notification_block.png"], + }, + ]; + } + + getSilhouetteColor() { + return "#daff89"; + } + + getIsUnlocked(root) { + return root.hubGoals.isRewardUnlocked(shapez.enumHubGoalRewards.reward_wires_painter_and_levers); + } + + getLayer() { + return "wires"; + } + + getDimensions() { + return new shapez.Vector(1, 1); + } + + getRenderPins() { + // Do not show pin overlays since it would hide our building icon + return false; + } + + setupEntityComponents(entity) { + // Accept logical input from the bottom + entity.addComponent( + new shapez.WiredPinsComponent({ + slots: [ + { + pos: new shapez.Vector(0, 0), + direction: shapez.enumDirection.bottom, + type: shapez.enumPinSlotType.logicalAcceptor, + }, + ], + }) + ); + + // Add your notification component to identify the building as a notification block + entity.addComponent(new NotificationBlockComponent()); + } +} + +//////////////////////////////////////////////////////////////////////// +// HUD Component to be able to edit notification blocks by clicking them +class HUDNotificationBlockEdit extends shapez.BaseHUDPart { + initialize() { + this.root.camera.downPreHandler.add(this.downPreHandler, this); + } + + /** + * @param {Vector} pos + * @param {enumMouseButton} button + */ + downPreHandler(pos, button) { + if (this.root.currentLayer !== "wires") { + return; + } + + const tile = this.root.camera.screenToWorld(pos).toTileSpace(); + const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "wires"); + if (contents) { + const notificationComp = contents.components.NotificationBlock; + if (notificationComp) { + if (button === shapez.enumMouseButton.left) { + this.editNotificationText(contents, { + deleteOnCancel: false, + }); + return shapez.STOP_PROPAGATION; + } + } + } + } + + /** + * Asks the player to enter a notification text + * @param {Entity} entity + * @param {object} param0 + * @param {boolean=} param0.deleteOnCancel + */ + editNotificationText(entity, { deleteOnCancel = true }) { + const notificationComp = entity.components.NotificationBlock; + if (!notificationComp) { + return; + } + + // save the uid because it could get stale + const uid = entity.uid; + + // create an input field to query the text + const textInput = new shapez.FormElementInput({ + id: "notificationText", + placeholder: "", + defaultValue: notificationComp.notificationText, + validator: val => val.length > 0, + }); + + // create the dialog & show it + const dialog = new shapez.DialogWithForm({ + app: this.root.app, + title: shapez.T.mods.notificationBlocks.dialogTitle, + desc: shapez.T.mods.notificationBlocks.enterNotificationText, + formElements: [textInput], + buttons: ["cancel:bad:escape", "ok:good:enter"], + closeButton: false, + }); + this.root.hud.parts.dialogs.internalShowDialog(dialog); + + // When confirmed, set the text + dialog.buttonSignals.ok.add(() => { + if (!this.root || !this.root.entityMgr) { + // Game got stopped + return; + } + + const entityRef = this.root.entityMgr.findByUid(uid, false); + if (!entityRef) { + // outdated + return; + } + + const notificationComp = entityRef.components.NotificationBlock; + if (!notificationComp) { + // no longer interesting + return; + } + + // set the text + notificationComp.notificationText = textInput.getValue(); + }); + + // 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 notificationComp = entityRef.components.NotificationBlock; + if (!notificationComp) { + // no longer interesting + return; + } + + this.root.logic.tryDeleteBuilding(entityRef); + }); + } + } +} + +//////////////////////////////////////////////////////////////////////// +// The actual mod logic +class Mod extends shapez.Mod { + init() { + // Register the component + this.modInterface.registerComponent(NotificationBlockComponent); + + // Register the new building + this.modInterface.registerNewBuilding({ + metaClass: MetaNotificationBlockBuilding, + buildingIconBase64: RESOURCES["notification_block.png"], + }); + + // Add it to the regular toolbar + this.modInterface.addNewBuildingToToolbar({ + toolbar: "wires", + location: "secondary", + metaClass: MetaNotificationBlockBuilding, + }); + + // Register our game system so we can dispatch the notifications + this.modInterface.registerGameSystem({ + id: "notificationBlocks", + systemClass: NotificationBlocksSystem, + before: "constantSignal", + }); + + // Register our hud element to be able to edit the notification texts + this.modInterface.registerHudElement("notificationBlockEdit", HUDNotificationBlockEdit); + + // This mod also supports translations + this.modInterface.registerTranslations("en", { + mods: { + notificationBlocks: { + enterNotificationText: + "Enter the notification text to show once the signal switches from 0 to 1:", + }, + }, + }); + } +} + +const RESOURCES = { + "notification_block.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4VpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LmRhYmFjYmIsIDIwMjEvMDQvMTQtMDA6Mzk6NDQgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NGQwZTY5MmYtOTRlNy00MDQyLWFjY2ItNmU3OGEzMGU1N2ZjIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjAyQTMyQTVGNzA1RTExRUNBQ0EzQTZCNEYxQjM5MkFBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjAyQTMyQTVFNzA1RTExRUNBQ0EzQTZCNEYxQjM5MkFBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY3NDVhMzZlLTkyMWUtNDViYS04NDFjLWVjOWZjZTc0Y2MyNSIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk3MDYxZWYyLTY2ZGMtZjI0Zi1iZTMy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", +}; diff --git a/mod_examples/pasting.js b/mod_examples/pasting.js new file mode 100644 index 00000000..698edeff --- /dev/null +++ b/mod_examples/pasting.js @@ -0,0 +1,23 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Pasting", + version: "1", + id: "pasting", + description: "Shows how to properly receive paste events ingame", + minimumGameVersion: ">=1.5.0", +}; + +class Mod extends shapez.Mod { + init() { + this.signals.gameInitialized.add(root => { + root.gameState.inputReciever.paste.add(event => { + event.preventDefault(); + + const data = event.clipboardData.getData("text"); + this.dialogs.showInfo("Pasted", `You pasted: '${data}'`); + }); + }); + } +} diff --git a/mod_examples/replace_builtin_sprites.js b/mod_examples/replace_builtin_sprites.js new file mode 100644 index 00000000..6e88b7fb --- /dev/null +++ b/mod_examples/replace_builtin_sprites.js @@ -0,0 +1,48 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Replace builtin sprites", + version: "1", + id: "replace-builtin-sprites", + description: "Shows how to replace builtin sprites", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +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"]); + }); + } +} + +//////////////////////////////////////////////////////////////////////// + +const RESOURCES = { + "red.png": + "data:image/png;base64,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", + + "green.png": + "data:image/png;base64,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", + + "purple.png": + "data:image/png;base64,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", + + "blue.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAADdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABK7SURBVHja7V1neFRVGrasbYsr6logO3PvZFJIQghJSEgCSQgJCQkBQgopECAJCUhRqkqRJkUUpESKFJXepUsRCL0E1EVWBUGQYoC1ru6uq65nzxkmydxzzkxunzvj8XneHz4PhJv7vufc833n+97vDgDAHUaHr5krhLgM8QlEO094Zk+BJ5AfCPEzBLDjW4hHGHm/HQGsdSC/FtMYeb8BAUCiwyB+pQjg3xBPMgK9XwDbKOTXopIR6MUCgATHuCAf4b8QZkai9wpgryPhVosvTQRLGIleKABIbBJOdmLf4SC6axEugF8g/BmR3ieAY45E+/kFgN57PgJF6w4BX47HRbCaEelFAoCEdsRXf9LAUaDvqVs2xOaX4gJAUUIoI9MLBACJvBPifUeC/QOCQOm+83UC6LHpJLByFlwEmxmZ3iGAPHz1JwwcV0d+LVoXP0U7EEYzQj1YAJDAuyE+diQ1ICgUFO44Rwig5/b3YVRgxQWwhxHq2QLoSZz8h04BxQevEQJASCgbTNsFEhmpHigASNw9EJ85khnYrAXI33Ee9Dr0BVUAvXadBX5Wf1wARxipnimAfsTJ/7kZoPDdK6DkCF0ACG37P0/bBdIZsR4kAEjY/RDXHUls2iIK5O+8aBNA6bEapwIo2XsO+Ps3xQXwHoomGLmeI4Ch+CpOfmGujXyEPsdvOhUAQvLgcbRdIIeR6wECgET9EeIfjuQFt4wDBbsv1Qmg4qRrAZRWXQQBTUNwAXwEcRcj2PgCGI2v3vYvLq4jvwiib/UtlwJAaP/sFNouUMwINrAAIEGN7OVddaSFxLYFhXsu1wmg+76rDZKPUHboMowawnABXETRBSPZuAKYjK/aDtOW15GP0KPqmigBIHQYM4O2C1Qwkg0oAEjMYxA/OJIVmpAmIB+h54HrogVQfvQqCAqLxAVwDUUZjGjjCWAmvlozZq4nBNDrkHgBIGRMfI22CwxmRBtIAJAQH4gfHUkKS+lEkG9LAh2tkSSA8uPXQXBkDC6AWyjaYGQbRwAL8FWaOXcrVQBlx6UJAKHTtMW0XWAUI9sAAoBE+GJNHiA8PZdKvi0JdOKGZAFUnLwBmsXE4wL4BuIhRrj7BbAMX52dF+52KoCK6luSBYDQZeZy2i4wiRHuRgFAAoIg/udISmSXHk7JtyWBTskTQN/qm6B5fDIuABR1PMZId58ANggI4Sygy5tVTgXQff9V+QKAyJ67jrYLvMpId4MA4IuPwFu8ovLKnJKPUFx1XZEAEFq0S8cF8B8UhTDi9RfADkGTB+8Luq446lIAPQ8qF0Du4q20XWA+I15HAcAXHoeT0Kp7f5fkI/Q+/IViASBEdsjCBfAThIWRr58AqgSr39cP5KytblAApUdqVBFAt2W7abvAUka+DgKALzoFf/lxJUMbJN+WBDp2QxUBIER1zscFgKKRICYA7QVwQtjiFQhyN3wgSgB9GigEkYLCNQdou8B6JgANBQBfcGf8pbfuO1IU+YVKcgBOEJPbk9ZSFs4EoF2L1xlBi1dgMMjbfFYU+UV7r6ougO4bj9FaynYwAWgjgHyixWvQeNGrX2kSyBniulfQPgWRTADqt3idE7R4BYeCbls/Fi2AYgmVQFJQvPU0zWiiFxOAugIowVdZ26FTRZN/uxDkC00EgJDYbwRuMBHPBKAe+ffaDR3rW7xCw20tXlIEoFYSyNlFUerzL9k+B216D5xtJDIsPhY/K8clBAcH3+upAhiAr/52I2dKIt+WBDpao50AhPgE4m5DkM8J2uOONm7c+PceJQD40A9A1AhbvKJBgb3FSwrKjt/QSwAIbj8D+Pv4NLFfVNVnTM3mkZ4mgOH46k8ZN08y+QjlKiaBGiweeXXpLRge7keJIYiH3VIka+LmUaKTbzhO+yomtch/EOJLQYtXVGtQsPuyLAHoRX7PHR/YDKgcnvsDvVvKAsxm3n5BRQlRzRM9RQBj8YdPnbREFvnd9+knAOQ35O6WMl+Oe8u5Gab5e//G/o8aWgBo24T4zvHBm8UlgcI9n8sTwP5rugmg/Ng10LR5uNtayuAWH2gPRV04ovKvGF0ALxEtXi+vlEX+7W6ga3oeAEH6+Dm0F99Xp9UvdELneBAS3ZowxjabzU8aUgDw4Z6A+JegxSsxTTb5crqB1NgFgiOidW8p8+NIJ/S4wnKQu2gLIUiLma80qgBmEy1eszYoEkDvwzW6CgAhc+pC2i4wRNurcn4bXiZXvPWUvYqpC2GMzfO82VACgA9lsjt2O7R4dVZEfkOWMFqh4kQNaBbdRreWMhjjE07osT3611cxLSWrmKxmbrHRBLCIaPGat02xAPromwSqQ+cZS2m7wGiNbksxJ3QrKNz8XgNVTOafUarYEAKAD+SHt3hFZHRTTL7eSSDBLlB9AzRvnURrKWukLvlmwgk9tvcQog+ycPUB2i6wwigCWEm0eC3eo4oAKtxAfi26Vq7RvKUM5flxJ/ScDe9TvZDwKiaLmfvB7QJAE7uIFq+sYlXI16ISSHIzSVIaLoAbatnOWczmDKJMruI5p8mv7huPw8OhoIrpx4iIiHvcLYAc/JfIWnpAFQH0qHK/AHIWvE3bBRTb0cP/7rR7GAqc0PPe/tBlAUzWrBXAH+4SfgFNQfKQ8WuNsAPMFxg7xSSqQr5a3UBqRAQBgUG4AIYqPvmbeGLhxA8YKyr3UXGirj7ih35nvm3kbgF8KkhelA5TTQBaVgJJAWU0zU6Fq/8uu3ehgxN6s7oyOVd2uMRnYcPR2TzPB7hFAGhKF5H2nb5KNQFIeRFaosPYmbgAULbzPvnXvXwx4YQ+ZHJ9/YNII4yOL86tPxCauMnuEEAJnr2SUuypZzeQEhStP0I7B7SV84LRoc1+wVRfJhcSBvK3n3MwwhAX+vr7BwrDQo5L1VsAix0foHlShmrkuzMJRKsbpBhPjpF54VNBFMk+O12yGSZCUIuWeHKoWm8BbBYmMAarKgCxK0EPtMzMwwUwS8Z17/32iyWHMrmWgjI5KSXwyc+8QKsZyNJTAAcd//E2T41RjfyivVcMQ76T6WSSO4otHPcM4YQ+phK7/BIf+ZRWfQojlGD8uc6iQ6ZeAjijpN7fdSXQVUMJIL70afxFb5HyrkIff/wP8O/cdPwZQZGxoGDXJUUV0O1HTKZcF5uL9BLAFYGaR89RTQA9qq4ZSgBJg0bjL/qgxBu/kYQT+oRF5N2HRCu8PoeRMXZz/NkuJN6R+Ds9BPC9GnV/9Eqg64YSAGoewV7yhxK+/Q/BP/+1oEi2VQIo2IMVye6Vd/fRYcyr5FnAxPfRVACoWlbLHICm3UAykDGhEn/JVyTc+E3E31Xa1GWqNcH2QcbYZERw1Wq13qf1DvCNoO5/7Dz1soAHjbUDpAydQMwiErX1W61/QdW8giLZNinUItliBfWPGZPmkWcBeOjUWgCnBVFAv9GqCcDZfEB3Ia6IaCVfJ7LUaxJOTPqMNarveuh+ILhlLHFzCSOCu7UUwDqB119Ob/UOgfuNFQWEJ2fgL3eqyHf0N2GyrKNmPZCdXnmD2AX8OGkjdBWVgIfGp6qYBzCWAAJDiJN2g4csX1/fx/BK35Sxc51XP51QlvmsOFkDAoND8fTw81oKoK/gPrtpiHoCEDkgSg+UHfyMdheQJCLtSzijdFqwg/47Q8GrUf0Uk9cLPwzu0VIA7fFfsOvKY6qJoNwgqeC8JdtoAjDLif3zNv9dUxuc9HGz8ee8pKUAHsWLQRMHT1LvMuiEMS6D4ksG4S+1RkzTKFx9L+O3floferNmr8Kf9WutC0J2CvsA23lVLgCNnQkMboa/1Nniav74RWL7JIpVSnzlLNyEP+svWguAGPOeteyQ10QC2fPW07b/WDkCaNE+S3MjrKzK1YQApFwOyfUCEAx9ih84Xr0r4ZM33Rz/lxMZQLFVwegELjgk+wUSlz/1YldHAG37P0fcC+jRF7CRnPd7WZ2ysKPu+wyUVl2gTSN/WULhZy4RBbz+jvOwV4WoJywhBbsTkFa/KFcAuWqYQdG/je7LCGIWcpKNJK0mUzBR/TNsmmZ5ACRYrG8AwFB0lh4CICpdAoJCRVvBuoSbEkLILYRiInlKxru5IJyLVOSiCLZG9fOKxWRJ1qs5tIjoceulTomYjjZxrizlRR/+BAdBk3AmMpqPkO0kV6KkBgJlASPSOhMdzZreBVAMoY8Kq4RdD4ISnRXcd1XXrKCTbqDlct4Lz/Oh+M+K7FTgohBW3qE3ZfgkWvPoXL0NIlriue+wdh1leQO6KydQduAizRsAjZhrotqENHQnMH6BapY4+cv32Ery8d4Fi8VicodH0Jv4L2ubCCbTJMrxlFx+Unt7GMpMIQRFJo126zeh8SM8X3Scs0lxBhTdU4REtqKZSY3RpTWMIoAnIf6JP1CbfqOM3SpWfRPEFpTRyL+gpAvIIS08Af/ZqBk0bcpSakGsmJL4nu+cAeHkKDx08j+PStDd6ROYSbM7U1w0atsFtEkMJQ0YSSMfCTlMlZYreBjDfYBqEQMPy3lbPhL/KYDiQDn/wODmtO/+V0p6BdU0PiA6YNChMHXym8ryAhpUC1MKPoH9kitVrfdRXxpurqaJAO0GqK/CNj/R/rlEZXGO85JRnJ81ayWZ7HEwj/Lj+XjdGkNEiGAS7UHj+owABbs+c3u9YNmhSyCmW4mzl1miiR8QKhLhhA01hBgCg0GL1K42s4ikoVNATEEpCImKA67+Dlr58OemGXFgxFLaA6M+wpy1pxR0DivLDRStO0w77ddivJaWcKhm38pxU/GISTY47qRatnFaCAB1w75De3A0OiZ95jr53cMym0czp7wO/Kz+zl7oQt1mAphM7awm4Sg9ibhlMXHPqjlQQquhUWhu0GRnio/IzHd6SdJQaCglZMp5/W0QntzR2ctEDt2D3GEPb+V5VFm1D79VdQEYmfDD0JnC0wZHIlOkr5z9YuEdciR7C9pCpgYig+z5G0CLpA6uXujnENHyVrGl4+3Sb3Os0veDvAOQZazVbC61mPnXLGbuOArpINaglY7y+iaTqZGWHOkxO9iETxDFgQon0HCJnHWnRVvK414CxdtOg4yJlSAsMbWh1bRd7mAI1H4l3EHMSe7YQTxxfDwaJjVNzJaHrOZbVzwP0qevtt0r0BxI8jadBZ2X7AOZM1eCxIphNIdtGr6zTzWRZflmtVoftOK7GcedQe5fTADihdAYGS3gadKGgGLmkJi2IDg63jZ/WOLBCbWzjYNQNH4FnuLH036+heMKmADk2czPQF74qoRFdKDVOhqVsCl93sAmTR6hpbtr07BSr2B/8wJwEMLj9tVZrVKMjLJ5VRCD1XT6ht/6aa7+XYvZXMIEoFwMqK2qO8QKuz2rWNLRiX4hRFc1VjsONK1DxE51Wc9hj14pAIogkMUKmqmTjOb7IpcuCFR12wMiEcKq9VQPe6l3JU54dFYR7Tp2ABOAlwGlW/GhGOHtMmx1BEHhUUT3kI+PzwNMAF4EK+aLiJC7ZPtt187JC2i7wHAmAC+BxWLxx2scwlOzBKbSlPzDlwEBAX9iAvACQDJX4Ss8e8lO4ZiZ6W9RyrK5sUwAnr76fSzN8LA0omM+4WVccfIGCI1rS2Qc4VngYSYAT179Jm4TPtCx8+J3qXV7XeesJos1OHF2MkwAxjz5R+GEtuzay7mFDRRFWEJ7okSb47gnmAA88eTPcbuFq9kCspYeBMUuunmz52+kmTjPZgLwPPITiKRPQbmoEvXwFKLw5Ec5jRpMAG49+ZsP4c0c2auOi5pmkrdkO20XWMgE4Cnkc1waUb9fPMihHrHhotTI9Gxi2qevj6+VCcAz4v59AsNFqz/IWXta0jAL1LOnRsMmE4DOsHv7/yToZSgbIWuYRXSXQlwEF5kAPHD77/LGXlndSV1mLSd2ARgSckwARs78cUIHVJT46bbjvEOTqvjOpKL1h2mHwSQmAGNn/kYIRru0iJbdldTnyBWbgLCKoWwmAGMLYJBAABGtZFvWlB/+nDBvsJr4TCYAQwvAFEHc/K2pljXRNHfRFvwT8CsqKmUCMPJJ2NbLL5zykTJufr1djQSjinZPj8Grhs+wKMATDoImbpdgFkJCap3Lp9gooHT/pyAoLBL7/vOVTACekQYeRdjc9B1VN8ugolqOd7/t+5/DBOAZJWB/RuXd+E1graePSwvb6psgbeQ0Wo3gYaM3jTDyhZ2/rWleR8jtM2ddNXUXQHE/5SbQVhlk5AQQEwAQP/Ov1vk7qkshaD9ikq0WMPmZsSAqM5fm16d4nCsTgBuB7FwggfuVtagZ/xqYCaAB4wZU0SOD+J8hhrKqYO/5HCTB8PCACPJRBfEGnuebs8YQbzwc/tUSaUsXc9xbkOizt5tF+VPw/xf4mvhyJUaN7sb/AWO1JrNEJCohAAAAAElFTkSuQmCC", + + "yellow.png": + "data:image/png;base64,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", + + "cyan.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAOxAAADsQH1g+1JAAAE8mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZGFiYWNiYiwgMjAyMS8wNC8xNC0wMDozOTo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjAgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAxLTE0VDEwOjMwOjMyKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMS0xNFQxMDozMDo1MCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMS0xNFQxMDozMDo1MCswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZTFiZTU2MjEtMTU1ZC00YmFmLWFiNzgtYjM2Y2QzMmIzNTNkIiB4bXBNTTpEb2N1bWVudElE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", + + "white.png": + "data:image/png;base64,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", +}; 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/storing_data_in_savegame.js b/mod_examples/storing_data_in_savegame.js new file mode 100644 index 00000000..92f7733b --- /dev/null +++ b/mod_examples/storing_data_in_savegame.js @@ -0,0 +1,78 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Storing Data in Savegame", + version: "1", + id: "storing-savegame-data", + description: "Shows how to add custom data to a savegame", + minimumGameVersion: ">=1.5.0", +}; + +class Mod extends shapez.Mod { + init() { + //////////////////////////////////////////////////////////////////// + // Option 1: For simple data + this.signals.gameSerialized.add((root, data) => { + data.modExtraData["storing-savegame-data"] = Math.random(); + }); + + this.signals.gameDeserialized.add((root, data) => { + alert("The value stored in the savegame was: " + data.modExtraData["storing-savegame-data"]); + }); + + //////////////////////////////////////////////////////////////////// + // Option 2: If you need a structured way of storing data + + class SomeSerializableObject extends shapez.BasicSerializableObject { + static getId() { + return "SomeSerializableObject"; + } + + static getSchema() { + return { + someInt: shapez.types.int, + someString: shapez.types.string, + someVector: shapez.types.vector, + + // this value is allowed to be null + nullableInt: shapez.types.nullable(shapez.types.int), + + // There is a lot more .. be sure to checkout src/js/savegame/serialization.js + // You can have maps, classes, arrays etc.. + // And if you need something specific you can always ask in the modding discord. + }; + } + + constructor() { + super(); + this.someInt = 42; + this.someString = "Hello World"; + this.someVector = new shapez.Vector(1, 2); + + this.nullableInt = null; + } + } + + // Store our object in the global game root + this.signals.gameInitialized.add(root => { + root.myObject = new SomeSerializableObject(); + }); + + // Save it within the savegame + this.signals.gameSerialized.add((root, data) => { + data.modExtraData["storing-savegame-data-2"] = root.myObject.serialize(); + }); + + // Restore it when the savegame is loaded + this.signals.gameDeserialized.add((root, data) => { + const errorText = root.myObject.deserialize(data.modExtraData["storing-savegame-data-2"]); + if (errorText) { + alert("Mod failed to deserialize from savegame: " + errorText); + } + alert("The other value stored in the savegame (option 2) was " + root.myObject.someInt); + }); + + //////////////////////////////////////////////////////////////////// + } +} diff --git a/mod_examples/translations.js b/mod_examples/translations.js new file mode 100644 index 00000000..6b9c708e --- /dev/null +++ b/mod_examples/translations.js @@ -0,0 +1,66 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Translations", + version: "1", + id: "translations", + description: "Shows how to add and modify translations", + minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, +}; + +class Mod extends shapez.Mod { + init() { + // Replace an existing translation in the english language + this.modInterface.registerTranslations("en", { + ingame: { + interactiveTutorial: { + title: "Hello", + hints: { + "1_1_extractor": "World!", + }, + }, + }, + }); + + // Replace an existing translation in german + this.modInterface.registerTranslations("de", { + ingame: { + interactiveTutorial: { + title: "Hallo", + hints: { + "1_1_extractor": "Welt!", + }, + }, + }, + }); + + // Add an entirely new translation which is localized in german and english + this.modInterface.registerTranslations("en", { + mods: { + mymod: { + test: "Test Translation", + }, + }, + }); + this.modInterface.registerTranslations("de", { + mods: { + mymod: { + test: "Test Übersetzung", + }, + }, + }); + + // Show a dialog in the main menu + this.signals.stateEntered.add(state => { + if (state instanceof shapez.MainMenuState) { + // Will show differently based on the selected language + this.dialogs.showInfo("My translation", shapez.T.mods.mymod.test); + } + }); + } +} diff --git a/mod_examples/usage_statistics.js b/mod_examples/usage_statistics.js new file mode 100644 index 00000000..80828d95 --- /dev/null +++ b/mod_examples/usage_statistics.js @@ -0,0 +1,148 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Usage Statistics", + version: "1", + 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", +}; + +/** + * Quick info on how this mod works: + * + * It tracks how many ticks a building was idle within X seconds to compute + * the usage percentage. + * + * Every tick the logic checks if the building is idle, if so, it increases aggregatedIdleTime. + * Once X seconds are over, the aggregatedIdleTime is copied to computedUsage which + * is displayed on screen via the UsageStatisticsSystem + */ + +const MEASURE_INTERVAL_SECONDS = 5; + +class UsageStatisticsComponent extends shapez.Component { + static getId() { + return "UsageStatistics"; + } + + static getSchema() { + // Here you define which properties should be saved to the savegame + // and get automatically restored + return { + lastTimestamp: shapez.types.float, + computedUsage: shapez.types.float, + aggregatedIdleTime: shapez.types.float, + }; + } + + constructor() { + super(); + this.lastTimestamp = 0; + this.computedUsage = 0; + this.aggregatedIdleTime = 0; + } +} + +class UsageStatisticsSystem extends shapez.GameSystemWithFilter { + constructor(root) { + // By specifying the list of components, `this.allEntities` will only + // contain entities which have *all* of the specified components + super(root, [UsageStatisticsComponent, shapez.ItemProcessorComponent]); + } + + update() { + const now = this.root.time.now(); + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + + const processorComp = entity.components.ItemProcessor; + const usageComp = entity.components.UsageStatistics; + + if (now - usageComp.lastTimestamp > MEASURE_INTERVAL_SECONDS) { + usageComp.computedUsage = shapez.clamp( + 1 - usageComp.aggregatedIdleTime / MEASURE_INTERVAL_SECONDS + ); + usageComp.aggregatedIdleTime = 0; + usageComp.lastTimestamp = now; + } + + if (processorComp.ongoingCharges.length === 0) { + usageComp.aggregatedIdleTime += this.root.dynamicTickrate.deltaSeconds; + } + } + } + + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const usageComp = entity.components.UsageStatistics; + if (!usageComp) { + continue; + } + + const staticComp = entity.components.StaticMapEntity; + const context = parameters.context; + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + + // Culling for better performance + if (parameters.visibleRect.containsCircle(center.x, center.y, 40)) { + // Background badge + context.fillStyle = "rgba(250, 250, 250, 0.8)"; + context.beginRoundedRect(center.x - 10, center.y + 3, 20, 8, 2); + + context.fill(); + + // Text + const usage = usageComp.computedUsage * 100.0; + if (usage > 99.99) { + context.fillStyle = "green"; + } else if (usage > 70) { + context.fillStyle = "orange"; + } else { + context.fillStyle = "red"; + } + + context.textAlign = "center"; + context.font = "7px GameFont"; + context.fillText(Math.round(usage) + "%", center.x, center.y + 10); + } + } + } +} + +class Mod extends shapez.Mod { + init() { + // Register the component + this.modInterface.registerComponent(UsageStatisticsComponent); + + // Add our new component to all item processor buildings so we can see how many items it processed. + // You can also inspect the entity with F8 + const buildings = [ + shapez.MetaBalancerBuilding, + shapez.MetaCutterBuilding, + shapez.MetaRotaterBuilding, + shapez.MetaStackerBuilding, + shapez.MetaMixerBuilding, + shapez.MetaPainterBuilding, + ]; + + buildings.forEach(metaClass => { + this.modInterface.runAfterMethod(metaClass, "setupEntityComponents", function (entity) { + entity.addComponent(new UsageStatisticsComponent()); + }); + }); + + // Register our game system so we can update and draw stuff + this.modInterface.registerGameSystem({ + id: "demo_mod", + systemClass: UsageStatisticsSystem, + before: "belt", + drawHooks: ["staticAfter"], + }); + } +} diff --git a/package.json b/package.json index f3ab2f80..ef752aac 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "index.js", "repository": "https://github.com/tobspr/shapez.io", - "author": "Tobias Springer ", + "author": "tobspr Games ", "license": "MIT", "private": true, "scripts": { @@ -19,7 +19,8 @@ "publishStandalone": "yarn publishOnItch && yarn publishOnSteam", "publishWeb": "cd gulp && yarn main.deploy.prod", "publish": "yarn publishStandalone && yarn publishWeb", - "syncTranslations": "node sync-translations.js" + "syncTranslations": "node sync-translations.js", + "buildTypes": "tsc src/js/application.js --declaration --allowJs --emitDeclarationOnly --skipLibCheck --out types.js" }, "dependencies": { "@babel/core": "^7.5.4", @@ -55,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/res/ui/icons/mods.png b/res/ui/icons/mods.png new file mode 100644 index 0000000000000000000000000000000000000000..1b3cb477aff032e4770ee2c8010e2d919c9fc5c7 GIT binary patch literal 4450 zcmaJ@2{@Ep`+r83B9stsmO-Ipj3qMmFr^H#WDsVU!NeHN3^ORCMGDDSDtne>NR6$m zAqo|uWoRNTiZRyg%YXD$?{{7Q_x+yhdY*I6ecku(+~;0?=ZUqmIV>#rmmmND!d6Et z?78FPjdwFY_iO(?K!7_)lP!^C2Z9fof+k`CGYr8C3%0_eeX;gfG$!1YYhN~Ce$Fbw?CE)_QLw&0*w?WY8w^7IE;~^^8o}D5oC__ z!yO4DVjaS49KFN*z4bAQCdObxDvS$&$CA-tDn1~P1fv=${=|iG=Nn=TMet7*vcHky zf1E-h?7-#(A{ML%(Ny<_LZM(ieFz5ah4%LH+7H%(YH4afbu=_})S*zAwmwWp2mI?% zcNp#b z2xJ1uj}Qdb(uP36stB|oZF;QE%j z7TS86nwIAJ2cd`Hnr4TfW(VNV0|&K#+gcF3gYnov@^4$r-?s37wcRKOd=PhM3oH>A zg2ljz1U&fXsbRQ(_r?5Q@qXE2{@oXgf3?-%I-{|X+yBb-ZxOeEHl}|{m%I2={MbNl z#S^)u%|_nW=C<1_D+@D6YCk*Ey{N!R`e}B3*VE@2i{zE89+^yB-}6ISyEkcDD&`hl z1w%EPrjWh_4(Vy_ji8G zpVgJStiK%EI2Stqp;6v2OR0Pg=5I2#*fZeqc%aW&nk8l?UtA z4td$bdC?xSr&h}@#El`_6gJ>NsbIaOhIaOF{G)>>=BCF{tfho-T0CC|r=y2+dLrGQ zR1r?BKVQZIK>JnFg;O(aLX2HhkP!;Z@0gjUnvv)q_o%F%kglH?H&HH?=$-*_^vgzn zjjt?9PY9)sP{>7)2Az?z>mjP~jdFMmf(qI~^jcMdm-B40x0V9HHUQuOOaWjs0Du56 z0Pq4)zl2g^ARS!270W+q(&|yw)&ihnI7kzDLa3sKF0PbOxp34fCH5Y_(<74pz*$I) z?Ms(4tz);-x_ak|Oe9$`+kNT@(-q6JVFH8Hi$B=bx+xBhM8=y!f@ta>irK%Gu72z? zuJm}>8b13iOC%zOp5$IVIE>si_zmR{)MiG&;9J**2!C!)6%q2-lDb+h|6+=g%nH8fd9&XL z=C(z4nTFsU{gA}==oEv&j7GC2b=g8y%So8|J+<1%i0Vk?X5+GDi0I`vf1S&lAm>HC z^{{?8!I`gd4(qFDc1~=qayz@m!lEdw=lpXh|OodpnEVmP%A6YYO@W~`{Fkc~2CeM&rO6LH<-Q$njLG;?OY?zb~ zzuk_p4~}3D*R`7`oMauaK;_mPkA3mw`}n#a@PDQ@cb8{;LO6Kx0ni}uKd1IrzE|Ws zy1(C(7AT@gT&pA@#G=GpD;m$axA=XV*;6airRpZb-h)*>22~UJ+pPw z7uM90@1_m&M^8RUX^gPv>5e@IOT<@Q0Q!nvYzCHgM_qGEt*u8M0*-b>y7AcY%)#jz zSkatO1v{sy8n;n}t3vRUg41+peF=Q_46*15NpN=Oo>EWzkHADbzRW4m>7wZM>zFbz zw@v5f1eUI^2k4wv1n{}@qDB?l+%`2;96$}UY(B5GV}C9Oe2@QV+S$B9ARFo1r%1hUk1yM@|18ppF9h251R>3%Z* zQoy{*n%Xwdtir-I<#2W8UbTiL(_;z=R)8(CDtszh!tKRfeq7fg0a--S^Uli0GqF+-StZwf z>-LOB9Sa~O;t|^Wj=;L}3gwc|mzeh6wPiJ@xZ3Te^?qKq4pX(oHHc<`i1%pogE-e< z`!h7TL?p~zmh`4%r))gp3PI4)Mm?r!%C|tYNpxWxe{U$Q&&e!i_k{c_1qmI(yL-r9 z=Ch|l9#3jj5H@tigiC5YsqIP^@3HuPm*{+I(XQ5ah_yI9LaT3daQ=kC78HSjmo&L4 zgR0MVt46Xbmb1LP{4JzH4`@0F=5=UM0I5ZT4OWqI0wo5Bw{?dg^Oq_zmY;3|)|2t1M!>7XFJPWnhb~ZL?_yE0dyjbG^F*?^neBtQx&`t(x zf>W3>D7rQ0LJgWl`+qwDpuH3rb&v-xd%4(|XM0b%0py`dJb)ja2tDp%!r+e}__`dqGwYFowi8TC}`rOKmE*6P+~ zim>|i}R>+%MnPPg=lQ}a@wOk4<1R*&WPL} zC=FmEs@13>;01KO>aK9S$NA^7D6iw7XcP36Xj9FS zd<8pyela@5Z_CB1yS*>F`-!(gK}9$KzOu?DTdUw+HSD$<}$Y z)*oKS5X#w4;%|E|NHp$=*W4OyEF4)O)Na!`eg3M?rM(`5X+YJ&I`qsrGchxXodfE{ znC+3xml^MLZ9*>eU@vMK#H}C&xtC*HF+g43l-D7}EpVKU=%f#Wu5J)q`)rp2A_rF% zEL&yNdSCDqoaX6T3B@y~GZmGj5Lvh%%?ewF?xe~FYC78irxL5++?IAbxfKY`4W&o! zS$1c9;?sC2_E}EUDhT)jUYzxm)=($h9rubql)- ztJVB>Q49SLU z9Z1^+2W42#C9OBGp2pch;6AK7D%(wGLQR;SwABImXYEdqr#CzoZIL1Slz99W%qLx{ z&p+XGzj_!mq-+Fk@-s|xOp2fzJZ7b0FMii`CMz%O(|aRfW^xHrOFL`KG&pfa8QJow zSM2fmc1Sf*rF9HVYP>Ek`Xf)OH-Buk-Nxk+ZTOlMKtVprUr3+_MH>LK-$ioVts!$a z@M%RO=kuwhn{GWRgQ66RhP`U@@a6{}fwp1LiAlp&ALM2Dl?(a5ye{w|`Cg`b|C6 z=S>^_*$XAj`P3vND*u0Xz(bTfCe4f$=OkV2B#3H7iU|L3z-2g9s$F0-lb?@%cZ$EN zLu3xzSD?<@WnK-;JiuELIR;6W6^-Z$WU<^CZ;NZgtFLlm`lrJ3mdlD?wh!^0Vh+^H z%(mxO*QVrg3ZiV=-gkwLdQ2`U8MEKqohj*Z-V*=z7Dx^ z_PcJ1(G6Fl3*mXK3FS_MGQpnm9$$KeUe_~MPiJBW#t-+aXJ>2`%o&g$Sta?V?*?RV z&sj4@h$8gQef8~fBK%2fFNL`M9Uc%b@;xLqq~VQje;RK_3D_y?yMqxdyw!3|y3F7e zXTJVLq{z}2n@)RcB_3+a*jl>4VU29VM0S4^AnA^@RWaB$87p$Ev5WDdQ;~;i#O$hD z4Hdh4B3pb=3iM4fv~I}v02wB+-(bSIChfd)l)DE-WkHra)EpU&KOWQd)o(ssf3%^0 zcyHvLI*xPnm%#_`sxHcwEyts3gR38^`PK#xFJDK83OOd1N}7Dm8F#+-ttvd76ldk+ z<9**FCB1{mG2DB`L4V1oxpM>;-Y3r*4^#}8X=v}|1P>ZN*uvu{p*mRnfKxnRm#@Bn zx$yU&_Wviw$BA2VI6sufb2LUzs`QFy_l;kJoz+U_>4vT1n;5SC4SQo|YScvZcsurK z`g$k4Os`lt)g~+Hso#&M%-T)jB|WE3ajwcX5#7`yK06I8f4z-bEPWFtGp#I>Txz`^ zD`P1plPp~LIjI1}@~!76&7lkXFLs~v*KP_kppPb0Ea!abM8U?gV=u$;2fZI;2+T|r z{a8(#Q`H*@K0HurtpD*WrYWSWj54`u8wCQgP`TgMEmUOz?qg+XV{z{gD*FEbq&m@y literal 0 HcmV?d00001 diff --git a/res/ui/icons/mods_white.png b/res/ui/icons/mods_white.png new file mode 100644 index 0000000000000000000000000000000000000000..1cb34f27dc90aaec243ce98e1b387f56cd7a2c46 GIT binary patch literal 4455 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 UsxqbV`^TuR)@{vl^@m~q1HkPG`Tzg` literal 0 HcmV?d00001 diff --git a/res/ui/icons/notification_info.png b/res/ui/icons/notification_info.png new file mode 100644 index 0000000000000000000000000000000000000000..04afd526036b87a90b99131a1b70c1d79fab3312 GIT binary patch literal 1899 zcmaJ?c~BEq98MwuB2WZk5h=0+M6i(EBq$^$NFX3V!x4hwK_S_!WTD9}$qE6{0xh*r z(R!3qDp;xLRHalI5yZq3FNzM;7F1BFXlq*zuht6PplJUn-I?9@-fzC|`<-vLI4)+f z-SpYhDHMvGBuXqJ*HH6oZApIjqnWqJ#f6ASA`-AQM7BbQP(&(Bi2xFfA`Out3RO<# zc_f%ZndXhklZYhgQUQ!<*a|a-ZP4H(n?ea*WWW`020{QzBn{OHS%0*iU;(H~$XdQY z3Q6&BM2$w}>X3xo7&)Aq0rOR?MIk`2K|l&<5JCYMG#_a70)vn>p(`NwW;4hFCLlzH zko7XCBxxKFj_D8}kj?RjAqWBj`D~R!seqMAKY$ByIUp1OasvDzNWkL@0s??n4~ulC zQ>_um#F4L@ktZQbO%S*M1hccV+1Wfcrb`1kd_Et9xFDD7Pa^#FIa)$t@Ym`+Cl$nq z9@e2afnr*~tf)|8YY8EX4E1FW8vM1aR{yF_WW_*(0tY#4$ehz8P%8a@s7CV|ttVv2 zn|S|ISTE1P5m1Kcv9&sw?A#hpvnyN>u0s?ArjujX2a{!tQ)2|CS7SK9<*^~aN2-8P zt=Zx`L6J%Y60M$4XkkPm7P3eYHj1hQ5Ld+GiWYJMp#>2fPDHpUT*MPC3=4?lgz@-X zk!X@D#^ALYL`zI^Rd2YwDY@ox(BPzJF``4W5LKiO(*P4$3(%=!fu_`Z#Z^rm3uj6$ zNCpF%d;70mPnyU9GH+jxE_rx8{D_vEcpW*~qjAnQ>23U{ihhVP|J2pLciIe$nSOne7*M@N8?P5t!e-Dg6gQR-7;{B8T2R;4lCweAm{$F4fz z#$tBY;3*Ptqg+Gl3H@m3(Ci?~RmuHcNBzq-RvBxSI)+XwlYHAI*D|-SpsAV7$xY#p zmgQVNbZ8|#S#uil{%8bZwd9o+)>vje&8MGl_Iwb1J8Vw*VCxn4oX{V9uRNiBAAYIw zIGw%Ztug1o^}KkO_rNO~Le8n5*(TX-$WL^1tF6nBRO#D?leg7J%xNB9-9xLIYZbfF z>VoW@K0m&}#98QiBtLUQV#l>`{KtVG6B=U+?44>^E%fgB)4wFFZ_FGnz8d^{d8-kQ zFwIM+m8=*Tu|LOjo?Ch;t)1bz+Gmr`J(JkE7QH{eu3UXgC}#K!jve}He#4(XwCs41 zP`j<}L|<$FVmINmbf&8tQf(tSQK~NxKh8Z8mXh8;|Cl+i|Jk%nt@{h74gJMz&|CrHYu=nkT8r_N#gvj1Dk^}b{26^=u)yB4-Gn)mA2CCB_r za)RTnA7@nLkBL3Rzg&T+!t)HnnPic(6u&f{H|s%{57q5FT=4P1;%8}|m4E%##s5jY z6@CFyA07B6v1!=RW{|n$N!=H4>6YWk&w|?}r}v)SBxi&~9j~TLaUlX%*hCx2f;Xq zg0l{?u)zj-VKv=AI@&;Z0E|xHgQH@A0Y2VDKUjbk=r3LveSc(D1OfkYp?Yb7{*#ow zxg`)mqTqn)U?l}C1Ofr7Yl7V{t{AMVt2_`2fhs9NR1}p|6d(|ovL;ML1^D*`qKBin zxx=gs4F3*BKWTwHs8ljcQISTYfoaNM5(TfQq^YT?2!Sd>p$c>l1;0Qd6%(L9^pp6_ zV1V<(QV3)!fkXrzF=AXv*Qi<`daD1a!H4`0Ez$4qKG8d-7=R%wDuE$KHT`xpH~;@# zeSH4$_M=+i{+sXrN$h76NX98z;rvL~C|LUB+$D}ekzoi54nrkTY)B;U-)*$?AW=zv z9waglstkqzWy~>H0`bTq`xnIA9EKwLQ87d;4rQPPqLY9L1UHxp#86*VUr${fq6Sq` zLL&4Kddhlg`YMJ>`pTM6J-y#p0}}R{4~|IvjdlAkR^wl>M{@8X(<2+;C^hM^v@Sfv{Un0#8JE8B+4R^sf|+#InDF}( zNv*+hEHr0!ua`Gt#?%V@`%TNPu*b!HwvO!!;xnV@hP3W-&kAbQuw;uIcR5_@!2Gzk zl|LGsJX(JVNFA~m>R1Iax%&eJPktyr$PLkZ$1s|QsM*yHaye78Q~Grwbd@72r8KcQ zG(A`OK-Q8r^T3|^?$S#KyKg)i`QT0l!|$=xV!{)F;=9f^y~-ONRV+oXxSq4deDFZ7 z=~Tyv<$Ja6A}SIMu3vYtMc)_A--~#UuE(s*YC`Z6z4(y0IVFR3cB97up~B6lZcjZe z7iRBOxyzJu3Audyg)1`nYNKd3X;^PTruIE5m>hkpPg*MI&ks?P(`|O4Rn1QMJdjC; zHt7Y`1c1)&E1tqD*VfN`ngQ$y+$>wSr{X0-l7Gd0N0W%1nre&{{Yy$6_dkBv?px!T zM8~>3YR-(_X9Docd&To`1FqbCQ+TasC;pUAdzlntsbM*$Ee;P<(7p24= zz#_a}8;+>EVBC_hukK6Ohpsj1N8}aQ-bTC=Yrm+FRoa+uCE>u8h%4)hlO0uV#>=8U zkWNP8Z)KrawjCy#l@ngObB-;mj;D{B&MgOgdM+%*)ob0F%5&+!K{Aadb~ECQ;&s8A zGglt7)zm$4L*wEuKDm<73L@f`HqT4E+|uf4TEwpJSq1ftjPKFK{~73%G2#~RZ1%L} zUP&O7Lr$Q!STo~T$S3yryd0bL6%D?~LrwE4(F%@3<3!GweAMVYf75+YAeHUe6H4mK zcBxUF?8J6-SdznZSZZ9oYqPGVF25WO+-?@^`aS4=$JKTrkG>asP6}lO%Jzf?`Odq| zB0udC<$6>-U_0X3=DAkh$l5}RZUv)$pJeG;*a0ucDoij!v#IyNvV>NhC{K>D2J+2R zRH00UNW|ltOCvwWzuU;Xj|yO(^B#Z41$mI=u>Qj~b*Cfbfp-X5C|)|PtN56FUXCVb z@&iXE;-an0V0#uxcZ)_u=AWyAIeh7SVi9Rw!ri&=${SkkoKVo z1+1=ZQ2EFC*tOr3^);JcDQ9AwImeHfTK#CYJPzkLi|)k%X~D!Q~Fw$nH7 z77p|2DVYmK$U8^!YQyv#xH zVLgj7cq-2Fk@-SJpUOi0>xR;djmVZ4r4yGktk*Tzh38eWF`S*wik|`Kc(YIHn_*f( zc`bzt8F_Ls`eK7utba|^2S!HS@wO!Iyz=E>kU{{h!4L}j)}LgHVcy`B_u(DPV;#QS zLxxoYI6EfB)}SO*zbW(Z3uXwuq~fDkpJL@F*`@G2f8`wJ?8wIzmO_)@3`3pVm@0)1 zg@HUt#@3Y+Yzxm=eEKh^#}vu_I9@R`2>4C<FbxK5%fXJ#XO`e zaRrgO!rp#$E6MEYK#dYdtI@EtwwQ*pwzi7?#}#>D3|9QT(9CliZEg&icvDV)$CVF3 z%%J;frqX@#w>xA#F1SDsc?uuQPwKdMU$x9Eo*eAjs^A|)M)xtwbFQ*8h}8mUP`D|` zs0Asre`&)S9xi+TGoWEi_}0AzQwdg?=kOM*#poGI`1L@BnwBaO$@+Uj( z#R|6yxe9jqT#X*Cy<&xY%}|yUTDOWK<1EA`Vl8WH`=Rh!nE#osXZOI$Ii~fV_j#9JB!~Leh}U)q zoOSlob!R9z));I5&9c7`)vX&W$_+n*R2kUlK4Jccyd<7>c~!=RD@S?_E?@ttZhcN! z>1*-V?1etuYYqWv8`t=^{7kk47zf@*rqZw_l3{? zaBfC=D#|E&f{hcpRtZrr_)^MgV0Uu=^}zy6HW|sPsg@z4xyR@_Z&jS!d1l^q{X1X! z&`n+r|I2zC**xanf?9W=5gL->+{RILT*my?f?M#jg0-R49kXw3t@^hxt`UK~4DqP% z7^09?OEsMSjj61C{=AVkvjMX6y#3MPaGfdd-N{HK$mQqprR!1R<(mx78fOF%PZ^nA z>yw+=GKu*&ItX+^5*dm@+ zF-Pn~XuN1~k1AFy&@O&EYweq|Etc|J{f%q?!;J3WfstcGzFI?q#T^ZzHTTs3V~Uup zivk04wq$JV^zLy`@M#zKQ^Rl@yt7^052{C$gj&zZZx425yH5GCN4K8@U+l5jKi6gd zz)YwlS}foT3o2=Q!zCDNfUev~d4?WretUj{%*|sKo)%%GFyc$CO9^FFu!?Y!>l}v3 ztEhWugvF=jh@AgEL-<0OA46kn>zvDbY!C*b+7td-z1!nL@WIP3ymI-3lq#B`Q4)7m zTX*7-5-Ga6-IeG!`C(O=(+opJMelQg12|uBv)7>GL!z}+qqNzO4#3N4CnUFiJd2G4 z)qd%YS@yb=5Pr?9Dn2#B;t{-e=i5HYR)G!?_e}X|FP@JbtG7{KW>I|B zbWO08e`G&W3D4|ifLGQ_d)~9Jo .buttons { diff --git a/src/css/main.scss b/src/css/main.scss index 1ac4f537..4850e3df 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -31,6 +31,7 @@ @import "states/mobile_warning"; @import "states/changelog"; @import "states/puzzle_menu"; +@import "states/mods"; @import "ingame_hud/buildings_toolbar"; @import "ingame_hud/building_placer"; diff --git a/src/css/resources.scss b/src/css/resources.scss index 3a581c30..83d5f1cb 100644 --- a/src/css/resources.scss +++ b/src/css/resources.scss @@ -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 */ diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index 15cdbe1c..9027d8a8 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -185,21 +185,20 @@ .updateLabel { position: absolute; transform: translateX(50%) rotate(-5deg); - color: #ff590b; - @include Heading; + color: #fff; + @include PlainText; font-weight: bold; @include S(right, 40px); @include S(bottom, 20px); + background: $modsColor; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 0, 5px, 1px, 5px); @include InlineAnimation(1.3s ease-in-out infinite) { 50% { transform: translateX(50%) rotate(-7deg) scale(1.1); } } - - @include DarkThemeOverride { - color: $colorBlueBright; - } } } @@ -290,6 +289,99 @@ } } + .modsOverview { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background: #fff; + grid-row: 1 / 2; + grid-column: 2 / 3; + position: relative; + text-align: left; + align-items: flex-start; + @include S(width, 250px); + @include S(padding, 15px); + @include S(padding-bottom, 10px); + @include S(border-radius, $globalBorderRadius); + + .header { + display: flex; + width: 100%; + align-items: center; + @include S(margin-bottom, 10px); + + .editMods { + margin-left: auto; + @include S(width, 20px); + @include S(height, 20px); + padding: 0; + opacity: 0.5; + background: transparent center center/ 80% no-repeat; + & { + /* @load-async */ + background-image: uiResource("icons/edit_key.png") !important; + } + @include DarkThemeInvert; + } + } + + h3 { + @include Heading; + color: $modsColor; + margin: 0; + } + + .dlcHint { + @include SuperSmallText; + @include S(margin-top, 10px); + width: 100%; + + display: grid; + grid-template-columns: 1fr auto; + grid-gap: 20px; + align-items: center; + } + + .mod { + background: #eee; + width: 100%; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 5px); + box-sizing: border-box; + @include PlainText; + @include S(margin-bottom, 5px); + display: flex; + flex-direction: column; + + .author, + .version { + @include SuperSmallText; + align-self: end; + opacity: 0.4; + } + .name { + overflow: hidden; + } + } + + .modsList { + box-sizing: border-box; + @include S(height, 100px); + @include S(padding, 5px); + border: D(1px) solid #eee; + overflow-y: scroll; + width: 100%; + display: flex; + flex-direction: column; + pointer-events: all; + + :last-child { + margin-bottom: auto; + } + } + } + .mainContainer { display: flex; align-items: center; @@ -308,6 +400,7 @@ display: flex; flex-direction: column; align-items: center; + width: 100%; } .modeButtons { @@ -352,27 +445,33 @@ } } - .importButton { + .outer { @include S(margin-top, 15px); - @include IncreasedClickArea(0px); } - .newGameButton { + .importButton { @include IncreasedClickArea(0px); - @include S(margin-top, 15px); - @include S(margin-left, 15px); } - .playModeButton { + .newGameButton { @include IncreasedClickArea(0px); - @include S(margin-top, 15px); - @include S(margin-left, 15px); + @include S(margin-left, 10px); } - .editModeButton { + .modsButton { @include IncreasedClickArea(0px); - @include S(margin-top, 15px); - @include S(margin-left, 15px); + @include S(margin-left, 10px); + + // @include S(width, 20px); + + // & { + // /* @load-async */ + // background-image: uiResource("res/ui/icons/mods_white.png") !important; + // } + background-position: center center; + background-size: D(15px); + background-color: $modsColor !important; + background-repeat: no-repeat; } .savegames { @@ -736,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 new file mode 100644 index 00000000..acec41fb --- /dev/null +++ b/src/css/states/mods.scss @@ -0,0 +1,141 @@ +#state_ModsState { + .mainContent { + display: flex; + flex-direction: column; + } + + > .headerBar { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + + > h1 { + justify-self: start; + } + + .openModsFolder { + background-color: $modsColor; + } + } + + .noModSupport { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + flex-direction: column; + + .steamLink { + @include S(height, 50px); + @include S(width, 220px); + background: #171a23 center center / contain no-repeat; + overflow: hidden; + display: block; + text-indent: -999em; + cursor: pointer; + @include S(margin-top, 30px); + pointer-events: all; + transition: all 0.12s ease-in; + transition-property: opacity, transform; + + @include S(border-radius, $globalBorderRadius); + + &:hover { + opacity: 0.9; + } + } + } + + .modsStats { + @include PlainText; + color: $accentColorDark; + + &.noMods { + @include S(width, 400px); + align-self: center; + justify-self: center; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + @include Text; + @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: ""; + @include S(width, 50px); + @include S(height, 50px); + background-position: center center; + background-size: contain; + opacity: 0.2; + } + &::before { + /* @load-async */ + background-image: uiResource("res/ui/icons/mods.png") !important; + } + } + } + + .modsList { + @include S(margin-top, 10px); + overflow-y: scroll; + pointer-events: all; + @include S(padding-right, 5px); + flex-grow: 1; + + .mod { + @include S(border-radius, $globalBorderRadius); + background: $accentColorBright; + @include S(margin-bottom, 4px); + @include S(padding, 7px, 10px); + @include S(grid-gap, 15px); + 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; + } + + .mainInfo { + display: flex; + flex-direction: column; + + .description { + @include SuperSmallText; + @include S(margin-top, 5px); + color: $accentColorDark; + } + .website { + text-transform: uppercase; + align-self: start; + @include SuperSmallText; + @include S(margin-top, 5px); + } + } + + .version, + .author { + display: flex; + flex-direction: column; + align-self: center; + strong { + text-transform: uppercase; + color: $accentColorDark; + @include SuperSmallText; + } + } + } + } +} diff --git a/src/css/states/settings.scss b/src/css/states/settings.scss index 5b36c677..15187cda 100644 --- a/src/css/states/settings.scss +++ b/src/css/states/settings.scss @@ -15,20 +15,21 @@ } .sidebar { - display: grid; + display: flex; @include S(min-width, 210px); @include S(max-width, 320px); - @include S(grid-gap, 3px); - grid-template-rows: auto auto auto auto auto 1fr; + flex-direction: column; @include StyleBelowWidth($layoutBreak) { - grid-template-rows: 1fr 1fr; - grid-template-columns: auto auto; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + @include S(grid-gap, 5px); max-width: unset !important; } button { text-align: left; + @include S(margin-bottom, 3px); &::after { content: unset; } @@ -37,15 +38,26 @@ @include StyleBelowWidth($layoutBreak) { text-align: center; + height: D(30px) !important; + padding: D(5px) !important; } } .other { - @include S(margin-top, 10px); align-self: end; + margin-top: auto; @include StyleBelowWidth($layoutBreak) { margin-top: 0; + display: grid; + grid-template-columns: 1fr 1fr; + @include S(grid-gap, 5px); + max-width: unset !important; + grid-column: 1 / 3; + + button { + margin: 0 !important; + } } } @@ -69,8 +81,28 @@ } } - button.privacy { - @include S(margin-top, 4px); + button.manageMods { + background-color: lighten($modsColor, 38); + color: $modsColor; + display: flex; + @include S(padding-right, 5px); + .newBadge { + color: #fff; + @include S(border-radius, $globalBorderRadius); + background: $modsColor; + margin-left: auto; + @include S(padding, 0, 3px, 0, 3px); + + @include InlineAnimation(1.3s ease-in-out infinite) { + 50% { + transform: rotate(0deg) scale(1.1); + } + } + } + + &.active { + background-color: $colorGreenBright; + } } .versionbar { diff --git a/src/css/variables.scss b/src/css/variables.scss index c7b7c17c..fe1fa864 100644 --- a/src/css/variables.scss +++ b/src/css/variables.scss @@ -38,6 +38,7 @@ $colorRedBright: #ef5072; $colorOrangeBright: #ef9d50; $themeColor: #393747; $ingameHudBg: rgba(#333438, 0.9); +$modsColor: rgb(214, 60, 228); $text3dColor: #f4ffff; diff --git a/src/html/index.html b/src/html/index.html index b1d89377..6ffa1876 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -15,8 +15,8 @@ - - + + 0.95 ? "steam_1_pr" : "steam_2_npr"; @@ -42,7 +44,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 @@ -61,6 +63,8 @@ export const globalConfig = { mapChunkOverviewMinZoom: 0.9, mapChunkWorldSize: null, // COMPUTED + maxBeltShapeBundleSize: 20, + // Belt speeds // NOTICE: Update webpack.production.config too! beltSpeedItemsPerSecond: 2, diff --git a/src/js/core/config.local.template.js b/src/js/core/config.local.template.js index 41677997..9a432b56 100644 --- a/src/js/core/config.local.template.js +++ b/src/js/core/config.local.template.js @@ -116,5 +116,11 @@ export default { // Disables slow asserts, useful for debugging performance // disableSlowAsserts: true, // ----------------------------------------------------------------------------------- + // 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 */ }; diff --git a/src/js/core/globals.js b/src/js/core/globals.js index 15197880..c47abfed 100644 --- a/src/js/core/globals.js +++ b/src/js/core/globals.js @@ -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, +}; diff --git a/src/js/core/input_distributor.js b/src/js/core/input_distributor.js index 03ad8e0c..be5440a9 100644 --- a/src/js/core/input_distributor.js +++ b/src/js/core/input_distributor.js @@ -149,6 +149,8 @@ export class InputDistributor { window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this)); window.addEventListener("blur", this.handleBlur.bind(this)); + + document.addEventListener("paste", this.handlePaste.bind(this)); } forwardToReceiver(eventId, payload = null) { @@ -186,6 +188,13 @@ export class InputDistributor { this.keysDown.clear(); } + /** + * + */ + handlePaste(ev) { + this.forwardToReceiver("paste", ev); + } + /** * @param {KeyboardEvent | MouseEvent} event */ @@ -211,6 +220,7 @@ export class InputDistributor { keyCode: keyCode, shift: event.shiftKey, alt: event.altKey, + ctrl: event.ctrlKey, initial: isInitial, event, }) === STOP_PROPAGATION diff --git a/src/js/core/input_receiver.js b/src/js/core/input_receiver.js index ae54f24d..164ab84b 100644 --- a/src/js/core/input_receiver.js +++ b/src/js/core/input_receiver.js @@ -12,12 +12,15 @@ export class InputReceiver { // Dispatched on destroy this.destroyed = new Signal(); + + this.paste = new Signal(); } cleanup() { this.backButton.removeAll(); this.keydown.removeAll(); this.keyup.removeAll(); + this.paste.removeAll(); this.destroyed.dispatch(); } diff --git a/src/js/core/loader.js b/src/js/core/loader.js index cadbc048..14866ad6 100644 --- a/src/js/core/loader.js +++ b/src/js/core/loader.js @@ -169,6 +169,9 @@ class LoaderImpl { sprite = new AtlasSprite(spriteName); this.sprites.set(spriteName, sprite); } + if (sprite.frozen) { + continue; + } const link = new SpriteAtlasLink({ packedX: frame.x, @@ -181,6 +184,7 @@ class LoaderImpl { w: sourceSize.w, h: sourceSize.h, }); + sprite.linksByResolution[scale] = link; } } diff --git a/src/js/core/modal_dialog_elements.js b/src/js/core/modal_dialog_elements.js index ee552aa9..5af7d9d1 100644 --- a/src/js/core/modal_dialog_elements.js +++ b/src/js/core/modal_dialog_elements.js @@ -90,8 +90,9 @@ export class Dialog { * @param {number} param0.keyCode * @param {boolean} param0.shift * @param {boolean} param0.alt + * @param {boolean} param0.ctrl */ - handleKeydown({ keyCode, shift, alt }) { + handleKeydown({ keyCode, shift, alt, ctrl }) { if (keyCode === kbEnter && this.enterHandler) { this.internalButtonHandler(this.enterHandler); return STOP_PROPAGATION; diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index ccf9bfb2..a4e617f8 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -1,7 +1,6 @@ import { BaseItem } from "../game/base_item"; import { ClickDetector } from "./click_detector"; import { Signal } from "./signal"; -import { getIPCRenderer } from "./utils"; /* * *************************************************** @@ -113,13 +112,11 @@ export class FormElementInput extends FormElement { if (G_WEGAME_VERSION) { const value = String(this.element.value); - getIPCRenderer() - .invoke("profanity-check", value) - .then(newValue => { - if (value !== newValue && this.element) { - this.element.value = newValue; - } - }); + ipcRenderer.invoke("profanity-check", value).then(newValue => { + if (value !== newValue && this.element) { + this.element.value = newValue; + } + }); } } diff --git a/src/js/core/sprites.js b/src/js/core/sprites.js index 1019d8f7..51032e4e 100644 --- a/src/js/core/sprites.js +++ b/src/js/core/sprites.js @@ -71,6 +71,8 @@ export class AtlasSprite extends BaseSprite { /** @type {Object.} */ this.linksByResolution = {}; this.spriteName = spriteName; + + this.frozen = false; } getRawTexture() { diff --git a/src/js/core/state_manager.js b/src/js/core/state_manager.js index 2e55f5d4..e8a8ba94 100644 --- a/src/js/core/state_manager.js +++ b/src/js/core/state_manager.js @@ -6,6 +6,7 @@ import { GameState } from "./game_state"; import { createLogger } from "./logging"; import { APPLICATION_ERROR_OCCURED } from "./error_handler"; import { waitNextFrame, removeAllChildren } from "./utils"; +import { MOD_SIGNALS } from "../mods/mod_signals"; const logger = createLogger("state_manager"); @@ -109,6 +110,8 @@ export class StateManager { key ); + MOD_SIGNALS.stateEntered.dispatch(this.currentState); + waitNextFrame().then(() => { document.body.classList.add("arrived"); }); diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 1d1b0b02..35e70310 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -42,21 +42,6 @@ export function getPlatformName() { return "unknown"; } -/** - * Returns the IPC renderer, or null if not within the standalone - * @returns {object|null} - */ -let ipcRenderer = null; -export function getIPCRenderer() { - if (!G_IS_STANDALONE) { - return null; - } - if (!ipcRenderer) { - ipcRenderer = eval("require")("electron").ipcRenderer; - } - return ipcRenderer; -} - /** * Makes a new 2D array with undefined contents * @param {number} w @@ -395,7 +380,7 @@ export function clamp(v, minimum = 0, maximum = 1) { * @param {Array=} classes * @param {string=} innerHTML */ -function makeDivElement(id = null, classes = [], innerHTML = "") { +export function makeDivElement(id = null, classes = [], innerHTML = "") { const div = document.createElement("div"); if (id) { div.id = id; diff --git a/src/js/game/base_item.js b/src/js/game/base_item.js index d74ff834..ae569ddf 100644 --- a/src/js/game/base_item.js +++ b/src/js/game/base_item.js @@ -2,9 +2,6 @@ import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; import { BasicSerializableObject } from "../savegame/serialization"; -/** @type {ItemType[]} **/ -export const itemTypes = ["shape", "color", "boolean"]; - /** * Class for items on belts etc. Not an entity for performance reasons */ diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 8ad4f7e3..7e2bfaf4 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -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"; @@ -1430,6 +1432,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]; @@ -1449,25 +1457,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 + ); } } diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index 795b27c3..14848485 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -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; } diff --git a/src/js/game/building_codes.js b/src/js/game/building_codes.js index 78240c31..1d178daf 100644 --- a/src/js/game/building_codes.js +++ b/src/js/game/building_codes.js @@ -4,6 +4,8 @@ import { AtlasSprite } from "../core/sprites"; import { Vector } from "../core/vector"; /* typehints:end */ +import { gMetaBuildingRegistry } from "../core/global_registries"; + /** * @typedef {{ * metaClass: typeof MetaBuilding, @@ -19,7 +21,7 @@ import { Vector } from "../core/vector"; /** * Stores a lookup table for all building variants (for better performance) - * @type {Object} + * @type {Object} */ export const gBuildingVariants = { // Set later @@ -27,13 +29,13 @@ export const gBuildingVariants = { /** * Mapping from 'metaBuildingId/variant/rotationVariant' to building code - * @type {Map} + * @type {Map} */ const variantsCache = new Map(); /** * Registers a new variant - * @param {number} code + * @param {number|string} code * @param {typeof MetaBuilding} meta * @param {string} variant * @param {number} rotationVariant @@ -47,6 +49,7 @@ export function registerBuildingVariant( assert(!gBuildingVariants[code], "Duplicate id: " + code); gBuildingVariants[code] = { metaClass: meta, + metaInstance: gMetaBuildingRegistry.findByClass(meta), variant, rotationVariant, // @ts-ignore @@ -54,9 +57,20 @@ export function registerBuildingVariant( }; } +/** + * Hashes the combination of buildng, variant and rotation variant + * @param {string} buildingId + * @param {string} variant + * @param {number} rotationVariant + * @returns + */ +function generateBuildingHash(buildingId, variant, rotationVariant) { + return buildingId + "/" + variant + "/" + rotationVariant; +} + /** * - * @param {number} code + * @param {string|number} code * @returns {BuildingVariantIdentifier} */ export function getBuildingDataFromCode(code) { @@ -70,8 +84,8 @@ export function getBuildingDataFromCode(code) { export function buildBuildingCodeCache() { for (const code in gBuildingVariants) { const data = gBuildingVariants[code]; - const hash = data.metaInstance.getId() + "/" + data.variant + "/" + data.rotationVariant; - variantsCache.set(hash, +code); + const hash = generateBuildingHash(data.metaInstance.getId(), data.variant, data.rotationVariant); + variantsCache.set(hash, isNaN(+code) ? code : +code); } } @@ -80,10 +94,10 @@ export function buildBuildingCodeCache() { * @param {MetaBuilding} metaBuilding * @param {string} variant * @param {number} rotationVariant - * @returns {number} + * @returns {number|string} */ export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) { - const hash = metaBuilding.getId() + "/" + variant + "/" + rotationVariant; + const hash = generateBuildingHash(metaBuilding.getId(), variant, rotationVariant); const result = variantsCache.get(hash); if (G_IS_DEV) { if (!result) { diff --git a/src/js/game/buildings/analyzer.js b/src/js/game/buildings/analyzer.js index 8335f730..13597d7b 100644 --- a/src/js/game/buildings/analyzer.js +++ b/src/js/game/buildings/analyzer.js @@ -3,7 +3,7 @@ import { enumDirection, Vector } from "../../core/vector"; import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -14,6 +14,15 @@ export class MetaAnalyzerBuilding extends MetaBuilding { super("analyzer"); } + static getAllVariantCombinations() { + return [ + { + internalId: 43, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#3a52bc"; } diff --git a/src/js/game/buildings/balancer.js b/src/js/game/buildings/balancer.js index 38a568e1..ce685a9a 100644 --- a/src/js/game/buildings/balancer.js +++ b/src/js/game/buildings/balancer.js @@ -31,6 +31,31 @@ export class MetaBalancerBuilding extends MetaBuilding { super("balancer"); } + static getAllVariantCombinations() { + return [ + { + internalId: 4, + variant: defaultBuildingVariant, + }, + { + internalId: 5, + variant: enumBalancerVariants.merger, + }, + { + internalId: 6, + variant: enumBalancerVariants.mergerInverse, + }, + { + internalId: 47, + variant: enumBalancerVariants.splitter, + }, + { + internalId: 48, + variant: enumBalancerVariants.splitterInverse, + }, + ]; + } + getDimensions(variant) { switch (variant) { case defaultBuildingVariant: @@ -154,11 +179,11 @@ export class MetaBalancerBuilding extends MetaBuilding { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, { pos: new Vector(1, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, ]); @@ -179,15 +204,14 @@ export class MetaBalancerBuilding extends MetaBuilding { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, { pos: new Vector(0, 0), - directions: [ + direction: variant === enumBalancerVariants.mergerInverse ? enumDirection.left : enumDirection.right, - ], }, ]); @@ -206,7 +230,7 @@ export class MetaBalancerBuilding extends MetaBuilding { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, ]); diff --git a/src/js/game/buildings/belt.js b/src/js/game/buildings/belt.js index f4e31ba9..11a53cdf 100644 --- a/src/js/game/buildings/belt.js +++ b/src/js/game/buildings/belt.js @@ -5,7 +5,7 @@ import { SOUNDS } from "../../platform/sound"; import { T } from "../../translations"; import { BeltComponent } from "../components/belt"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { THEME } from "../theme"; @@ -22,6 +22,26 @@ export class MetaBeltBuilding extends MetaBuilding { super("belt"); } + static getAllVariantCombinations() { + return [ + { + internalId: 1, + variant: defaultBuildingVariant, + rotationVariant: 0, + }, + { + internalId: 2, + variant: defaultBuildingVariant, + rotationVariant: 1, + }, + { + internalId: 3, + variant: defaultBuildingVariant, + rotationVariant: 2, + }, + ]; + } + getSilhouetteColor() { return THEME.map.chunkOverview.beltColor; } diff --git a/src/js/game/buildings/block.js b/src/js/game/buildings/block.js index d6499648..024c6322 100644 --- a/src/js/game/buildings/block.js +++ b/src/js/game/buildings/block.js @@ -2,13 +2,22 @@ import { Entity } from "../entity"; /* typehints:end */ -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; export class MetaBlockBuilding extends MetaBuilding { constructor() { super("block"); } + static getAllVariantCombinations() { + return [ + { + internalId: 64, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#333"; } diff --git a/src/js/game/buildings/comparator.js b/src/js/game/buildings/comparator.js index 6738d514..1f45dbbb 100644 --- a/src/js/game/buildings/comparator.js +++ b/src/js/game/buildings/comparator.js @@ -2,7 +2,7 @@ import { enumDirection, Vector } from "../../core/vector"; import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -11,6 +11,15 @@ export class MetaComparatorBuilding extends MetaBuilding { super("comparator"); } + static getAllVariantCombinations() { + return [ + { + internalId: 46, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#823cab"; } diff --git a/src/js/game/buildings/constant_producer.js b/src/js/game/buildings/constant_producer.js index c1c502d0..5c19a6ed 100644 --- a/src/js/game/buildings/constant_producer.js +++ b/src/js/game/buildings/constant_producer.js @@ -6,13 +6,22 @@ import { enumDirection, Vector } from "../../core/vector"; import { ConstantSignalComponent } from "../components/constant_signal"; import { ItemEjectorComponent } from "../components/item_ejector"; import { ItemProducerComponent } from "../components/item_producer"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; export class MetaConstantProducerBuilding extends MetaBuilding { constructor() { super("constant_producer"); } + static getAllVariantCombinations() { + return [ + { + internalId: 62, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#bfd630"; } diff --git a/src/js/game/buildings/constant_signal.js b/src/js/game/buildings/constant_signal.js index 983594cb..2bb72def 100644 --- a/src/js/game/buildings/constant_signal.js +++ b/src/js/game/buildings/constant_signal.js @@ -1,7 +1,7 @@ import { enumDirection, Vector } from "../../core/vector"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { ConstantSignalComponent } from "../components/constant_signal"; import { generateMatrixRotations } from "../../core/utils"; @@ -14,6 +14,15 @@ export class MetaConstantSignalBuilding extends MetaBuilding { super("constant_signal"); } + static getAllVariantCombinations() { + return [ + { + internalId: 31, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#2b84fd"; } diff --git a/src/js/game/buildings/cutter.js b/src/js/game/buildings/cutter.js index 37264c9d..21acdaa0 100644 --- a/src/js/game/buildings/cutter.js +++ b/src/js/game/buildings/cutter.js @@ -17,6 +17,19 @@ export class MetaCutterBuilding extends MetaBuilding { super("cutter"); } + static getAllVariantCombinations() { + return [ + { + internalId: 9, + variant: defaultBuildingVariant, + }, + { + internalId: 10, + variant: enumCutterVariants.quad, + }, + ]; + } + getSilhouetteColor() { return "#7dcda2"; } @@ -83,7 +96,7 @@ export class MetaCutterBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "shape", }, ], diff --git a/src/js/game/buildings/display.js b/src/js/game/buildings/display.js index 422bea81..7c1277c2 100644 --- a/src/js/game/buildings/display.js +++ b/src/js/game/buildings/display.js @@ -1,7 +1,7 @@ import { enumDirection, Vector } from "../../core/vector"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { DisplayComponent } from "../components/display"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -11,6 +11,15 @@ export class MetaDisplayBuilding extends MetaBuilding { super("display"); } + static getAllVariantCombinations() { + return [ + { + internalId: 40, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#aaaaaa"; } diff --git a/src/js/game/buildings/filter.js b/src/js/game/buildings/filter.js index 08296853..f8c29b1f 100644 --- a/src/js/game/buildings/filter.js +++ b/src/js/game/buildings/filter.js @@ -6,7 +6,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -15,6 +15,15 @@ export class MetaFilterBuilding extends MetaBuilding { super("filter"); } + static getAllVariantCombinations() { + return [ + { + internalId: 37, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#c45c2e"; } @@ -69,7 +78,7 @@ export class MetaFilterBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, ], }) diff --git a/src/js/game/buildings/goal_acceptor.js b/src/js/game/buildings/goal_acceptor.js index dde720e3..463798ad 100644 --- a/src/js/game/buildings/goal_acceptor.js +++ b/src/js/game/buildings/goal_acceptor.js @@ -6,13 +6,22 @@ import { enumDirection, Vector } from "../../core/vector"; import { GoalAcceptorComponent } from "../components/goal_acceptor"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; export class MetaGoalAcceptorBuilding extends MetaBuilding { constructor() { super("goal_acceptor"); } + static getAllVariantCombinations() { + return [ + { + internalId: 63, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#ce418a"; } @@ -36,7 +45,7 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "shape", }, ], diff --git a/src/js/game/buildings/hub.js b/src/js/game/buildings/hub.js index b9929b31..a0a9227e 100644 --- a/src/js/game/buildings/hub.js +++ b/src/js/game/buildings/hub.js @@ -3,7 +3,7 @@ import { HubComponent } from "../components/hub"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins"; export class MetaHubBuilding extends MetaBuilding { @@ -11,6 +11,15 @@ export class MetaHubBuilding extends MetaBuilding { super("hub"); } + static getAllVariantCombinations() { + return [ + { + internalId: 26, + variant: defaultBuildingVariant, + }, + ]; + } + getDimensions() { return new Vector(4, 4); } @@ -61,80 +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), - directions: [enumDirection.top, enumDirection.left], - filter: "shape", - }, - { - pos: new Vector(1, 0), - directions: [enumDirection.top], - filter: "shape", - }, - { - pos: new Vector(2, 0), - directions: [enumDirection.top], - filter: "shape", - }, - { - pos: new Vector(3, 0), - directions: [enumDirection.top, enumDirection.right], - filter: "shape", - }, - { - pos: new Vector(0, 3), - directions: [enumDirection.bottom, enumDirection.left], - filter: "shape", - }, - { - pos: new Vector(1, 3), - directions: [enumDirection.bottom], - filter: "shape", - }, - { - pos: new Vector(2, 3), - directions: [enumDirection.bottom], - filter: "shape", - }, - { - pos: new Vector(3, 3), - directions: [enumDirection.bottom, enumDirection.right], - filter: "shape", - }, - { - pos: new Vector(0, 1), - directions: [enumDirection.left], - filter: "shape", - }, - { - pos: new Vector(0, 2), - directions: [enumDirection.left], - filter: "shape", - }, - { - pos: new Vector(0, 3), - directions: [enumDirection.left], - filter: "shape", - }, - { - pos: new Vector(3, 1), - directions: [enumDirection.right], - filter: "shape", - }, - { - pos: new Vector(3, 2), - directions: [enumDirection.right], - filter: "shape", - }, - { - pos: new Vector(3, 3), - directions: [enumDirection.right], - filter: "shape", - }, - ], + slots, }) ); } diff --git a/src/js/game/buildings/item_producer.js b/src/js/game/buildings/item_producer.js index 1140c8f1..a367beae 100644 --- a/src/js/game/buildings/item_producer.js +++ b/src/js/game/buildings/item_producer.js @@ -3,13 +3,22 @@ import { ItemEjectorComponent } from "../components/item_ejector"; import { ItemProducerComponent } from "../components/item_producer"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; export class MetaItemProducerBuilding extends MetaBuilding { constructor() { super("item_producer"); } + static getAllVariantCombinations() { + return [ + { + internalId: 61, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#b37dcd"; } diff --git a/src/js/game/buildings/lever.js b/src/js/game/buildings/lever.js index 7ddaf65f..970a4bc6 100644 --- a/src/js/game/buildings/lever.js +++ b/src/js/game/buildings/lever.js @@ -1,7 +1,7 @@ import { enumDirection, Vector } from "../../core/vector"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { LeverComponent } from "../components/lever"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -11,6 +11,15 @@ export class MetaLeverBuilding extends MetaBuilding { super("lever"); } + static getAllVariantCombinations() { + return [ + { + internalId: 33, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { // @todo: Render differently based on if its activated or not return "#1a678b"; diff --git a/src/js/game/buildings/logic_gate.js b/src/js/game/buildings/logic_gate.js index b61d4373..bd5e1940 100644 --- a/src/js/game/buildings/logic_gate.js +++ b/src/js/game/buildings/logic_gate.js @@ -15,7 +15,7 @@ export const enumLogicGateVariants = { }; /** @enum {string} */ -export const enumVariantToGate = { +const enumVariantToGate = { [defaultBuildingVariant]: enumLogicGateType.and, [enumLogicGateVariants.not]: enumLogicGateType.not, [enumLogicGateVariants.xor]: enumLogicGateType.xor, @@ -41,6 +41,27 @@ export class MetaLogicGateBuilding extends MetaBuilding { super("logic_gate"); } + static getAllVariantCombinations() { + return [ + { + internalId: 32, + variant: defaultBuildingVariant, + }, + { + internalId: 34, + variant: enumLogicGateVariants.not, + }, + { + internalId: 35, + variant: enumLogicGateVariants.xor, + }, + { + internalId: 36, + variant: enumLogicGateVariants.or, + }, + ]; + } + getSilhouetteColor(variant) { return colors[variant]; } diff --git a/src/js/game/buildings/miner.js b/src/js/game/buildings/miner.js index 473aa262..b086a562 100644 --- a/src/js/game/buildings/miner.js +++ b/src/js/game/buildings/miner.js @@ -21,6 +21,19 @@ export class MetaMinerBuilding extends MetaBuilding { super("miner"); } + static getAllVariantCombinations() { + return [ + { + internalId: 7, + variant: defaultBuildingVariant, + }, + { + internalId: 8, + variant: enumMinerVariants.chainable, + }, + ]; + } + getSilhouetteColor() { return "#b37dcd"; } diff --git a/src/js/game/buildings/mixer.js b/src/js/game/buildings/mixer.js index e572bbba..de1665c4 100644 --- a/src/js/game/buildings/mixer.js +++ b/src/js/game/buildings/mixer.js @@ -5,7 +5,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -14,6 +14,15 @@ export class MetaMixerBuilding extends MetaBuilding { super("mixer"); } + static getAllVariantCombinations() { + return [ + { + internalId: 15, + variant: defaultBuildingVariant, + }, + ]; + } + getDimensions() { return new Vector(2, 1); } @@ -64,12 +73,12 @@ export class MetaMixerBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "color", }, { pos: new Vector(1, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "color", }, ], diff --git a/src/js/game/buildings/painter.js b/src/js/game/buildings/painter.js index e7a0b72d..432973d0 100644 --- a/src/js/game/buildings/painter.js +++ b/src/js/game/buildings/painter.js @@ -22,6 +22,27 @@ export class MetaPainterBuilding extends MetaBuilding { super("painter"); } + static getAllVariantCombinations() { + return [ + { + internalId: 16, + variant: defaultBuildingVariant, + }, + { + internalId: 17, + variant: enumPainterVariants.mirrored, + }, + { + internalId: 18, + variant: enumPainterVariants.double, + }, + { + internalId: 19, + variant: enumPainterVariants.quad, + }, + ]; + } + getDimensions(variant) { switch (variant) { case defaultBuildingVariant: @@ -107,12 +128,12 @@ export class MetaPainterBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.left], + direction: enumDirection.left, filter: "shape", }, { pos: new Vector(1, 0), - directions: [enumDirection.top], + direction: enumDirection.top, filter: "color", }, ], @@ -139,14 +160,13 @@ export class MetaPainterBuilding extends MetaBuilding { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), - directions: [enumDirection.left], + direction: enumDirection.left, filter: "shape", }, { pos: new Vector(1, 0), - directions: [ + direction: variant === defaultBuildingVariant ? enumDirection.top : enumDirection.bottom, - ], filter: "color", }, ]); @@ -172,17 +192,17 @@ export class MetaPainterBuilding extends MetaBuilding { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), - directions: [enumDirection.left], + direction: enumDirection.left, filter: "shape", }, { pos: new Vector(0, 1), - directions: [enumDirection.left], + direction: enumDirection.left, filter: "shape", }, { pos: new Vector(1, 0), - directions: [enumDirection.top], + direction: enumDirection.top, filter: "color", }, ]); @@ -230,27 +250,27 @@ export class MetaPainterBuilding extends MetaBuilding { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), - directions: [enumDirection.left], + direction: enumDirection.left, filter: "shape", }, { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "color", }, { pos: new Vector(1, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "color", }, { pos: new Vector(2, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "color", }, { pos: new Vector(3, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "color", }, ]); diff --git a/src/js/game/buildings/reader.js b/src/js/game/buildings/reader.js index 006d6582..5c9307c3 100644 --- a/src/js/game/buildings/reader.js +++ b/src/js/game/buildings/reader.js @@ -4,7 +4,7 @@ import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { BeltUnderlaysComponent } from "../components/belt_underlays"; import { BeltReaderComponent } from "../components/belt_reader"; @@ -18,6 +18,15 @@ export class MetaReaderBuilding extends MetaBuilding { super("reader"); } + static getAllVariantCombinations() { + return [ + { + internalId: 49, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#25fff2"; } @@ -75,7 +84,7 @@ export class MetaReaderBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, ], }) diff --git a/src/js/game/buildings/rotater.js b/src/js/game/buildings/rotater.js index f24fee14..e1080767 100644 --- a/src/js/game/buildings/rotater.js +++ b/src/js/game/buildings/rotater.js @@ -23,6 +23,23 @@ export class MetaRotaterBuilding extends MetaBuilding { super("rotater"); } + static getAllVariantCombinations() { + return [ + { + internalId: 11, + variant: defaultBuildingVariant, + }, + { + internalId: 12, + variant: enumRotaterVariants.ccw, + }, + { + internalId: 13, + variant: enumRotaterVariants.rotate180, + }, + ]; + } + getSilhouetteColor() { return "#7dc6cd"; } @@ -111,7 +128,7 @@ export class MetaRotaterBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "shape", }, ], diff --git a/src/js/game/buildings/stacker.js b/src/js/game/buildings/stacker.js index 6b70365d..f36ef248 100644 --- a/src/js/game/buildings/stacker.js +++ b/src/js/game/buildings/stacker.js @@ -5,7 +5,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -14,6 +14,15 @@ export class MetaStackerBuilding extends MetaBuilding { super("stacker"); } + static getAllVariantCombinations() { + return [ + { + internalId: 14, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#9fcd7d"; } @@ -64,12 +73,12 @@ export class MetaStackerBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "shape", }, { pos: new Vector(1, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, filter: "shape", }, ], diff --git a/src/js/game/buildings/storage.js b/src/js/game/buildings/storage.js index 5574e137..78f398be 100644 --- a/src/js/game/buildings/storage.js +++ b/src/js/game/buildings/storage.js @@ -6,7 +6,7 @@ import { ItemEjectorComponent } from "../components/item_ejector"; import { StorageComponent } from "../components/storage"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -17,6 +17,15 @@ export class MetaStorageBuilding extends MetaBuilding { super("storage"); } + static getAllVariantCombinations() { + return [ + { + internalId: 21, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#bbdf6d"; } @@ -65,11 +74,11 @@ export class MetaStorageBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 1), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, { pos: new Vector(1, 1), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, ], }) diff --git a/src/js/game/buildings/transistor.js b/src/js/game/buildings/transistor.js index ebcfeac3..1505903a 100644 --- a/src/js/game/buildings/transistor.js +++ b/src/js/game/buildings/transistor.js @@ -22,6 +22,19 @@ export class MetaTransistorBuilding extends MetaBuilding { super("transistor"); } + static getAllVariantCombinations() { + return [ + { + internalId: 38, + variant: defaultBuildingVariant, + }, + { + internalId: 60, + variant: enumTransistorVariants.mirrored, + }, + ]; + } + getSilhouetteColor() { return "#bc3a61"; } diff --git a/src/js/game/buildings/trash.js b/src/js/game/buildings/trash.js index 0ad5bfd7..fcf7f11f 100644 --- a/src/js/game/buildings/trash.js +++ b/src/js/game/buildings/trash.js @@ -4,7 +4,7 @@ import { ACHIEVEMENTS } from "../../platform/achievement_provider"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -15,6 +15,15 @@ export class MetaTrashBuilding extends MetaBuilding { super("trash"); } + static getAllVariantCombinations() { + return [ + { + internalId: 20, + variant: defaultBuildingVariant, + }, + ]; + } + getIsRotateable() { return false; } @@ -67,12 +76,19 @@ export class MetaTrashBuilding extends MetaBuilding { slots: [ { pos: new Vector(0, 0), - directions: [ - enumDirection.top, - enumDirection.right, - enumDirection.bottom, - enumDirection.left, - ], + 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/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 12e887c9..7009ebd7 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -40,6 +40,31 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { super("underground_belt"); } + static getAllVariantCombinations() { + return [ + { + internalId: 22, + variant: defaultBuildingVariant, + rotationVariant: 0, + }, + { + internalId: 23, + variant: defaultBuildingVariant, + rotationVariant: 1, + }, + { + internalId: 24, + variant: enumUndergroundBeltVariants.tier2, + rotationVariant: 0, + }, + { + internalId: 25, + variant: enumUndergroundBeltVariants.tier2, + rotationVariant: 1, + }, + ]; + } + getSilhouetteColor(variant, rotationVariant) { return colorsByRotationVariant[rotationVariant]; } @@ -252,7 +277,7 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }, ]); return; diff --git a/src/js/game/buildings/virtual_processor.js b/src/js/game/buildings/virtual_processor.js index b4f91762..b42ceb5d 100644 --- a/src/js/game/buildings/virtual_processor.js +++ b/src/js/game/buildings/virtual_processor.js @@ -19,7 +19,7 @@ export const enumVirtualProcessorVariants = { }; /** @enum {string} */ -export const enumVariantToGate = { +const enumVariantToGate = { [defaultBuildingVariant]: enumLogicGateType.cutter, [enumVirtualProcessorVariants.rotater]: enumLogicGateType.rotater, [enumVirtualProcessorVariants.unstacker]: enumLogicGateType.unstacker, @@ -40,6 +40,31 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding { super("virtual_processor"); } + static getAllVariantCombinations() { + return [ + { + internalId: 42, + variant: defaultBuildingVariant, + }, + { + internalId: 44, + variant: enumVirtualProcessorVariants.rotater, + }, + { + internalId: 45, + variant: enumVirtualProcessorVariants.unstacker, + }, + { + internalId: 50, + variant: enumVirtualProcessorVariants.stacker, + }, + { + internalId: 51, + variant: enumVirtualProcessorVariants.painter, + }, + ]; + } + getSilhouetteColor(variant) { return colors[variant]; } diff --git a/src/js/game/buildings/wire.js b/src/js/game/buildings/wire.js index 61b75073..c69dbfa1 100644 --- a/src/js/game/buildings/wire.js +++ b/src/js/game/buildings/wire.js @@ -37,6 +37,51 @@ export class MetaWireBuilding extends MetaBuilding { super("wire"); } + static getAllVariantCombinations() { + return [ + { + internalId: 27, + variant: defaultBuildingVariant, + rotationVariant: 0, + }, + { + internalId: 28, + variant: defaultBuildingVariant, + rotationVariant: 1, + }, + { + internalId: 29, + variant: defaultBuildingVariant, + rotationVariant: 2, + }, + { + internalId: 30, + variant: defaultBuildingVariant, + rotationVariant: 3, + }, + { + internalId: 52, + variant: enumWireVariant.second, + rotationVariant: 0, + }, + { + internalId: 53, + variant: enumWireVariant.second, + rotationVariant: 1, + }, + { + internalId: 54, + variant: enumWireVariant.second, + rotationVariant: 2, + }, + { + internalId: 55, + variant: enumWireVariant.second, + rotationVariant: 3, + }, + ]; + } + getHasDirectionLockAvailable() { return true; } diff --git a/src/js/game/buildings/wire_tunnel.js b/src/js/game/buildings/wire_tunnel.js index 2626dd12..80faa9df 100644 --- a/src/js/game/buildings/wire_tunnel.js +++ b/src/js/game/buildings/wire_tunnel.js @@ -2,7 +2,7 @@ import { generateMatrixRotations } from "../../core/utils"; import { Vector } from "../../core/vector"; import { WireTunnelComponent } from "../components/wire_tunnel"; import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; import { enumHubGoalRewards } from "../tutorial_goals"; @@ -13,6 +13,15 @@ export class MetaWireTunnelBuilding extends MetaBuilding { super("wire_tunnel"); } + static getAllVariantCombinations() { + return [ + { + internalId: 39, + variant: defaultBuildingVariant, + }, + ]; + } + getSilhouetteColor() { return "#777a86"; } diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index 9c9247e6..1add879e 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -22,31 +22,34 @@ import { ItemProducerComponent } from "./components/item_producer"; import { GoalAcceptorComponent } from "./components/goal_acceptor"; export function initComponentRegistry() { - gComponentRegistry.register(StaticMapEntityComponent); - gComponentRegistry.register(BeltComponent); - gComponentRegistry.register(ItemEjectorComponent); - gComponentRegistry.register(ItemAcceptorComponent); - gComponentRegistry.register(MinerComponent); - gComponentRegistry.register(ItemProcessorComponent); - gComponentRegistry.register(UndergroundBeltComponent); - gComponentRegistry.register(HubComponent); - gComponentRegistry.register(StorageComponent); - gComponentRegistry.register(WiredPinsComponent); - gComponentRegistry.register(BeltUnderlaysComponent); - gComponentRegistry.register(WireComponent); - gComponentRegistry.register(ConstantSignalComponent); - gComponentRegistry.register(LogicGateComponent); - gComponentRegistry.register(LeverComponent); - gComponentRegistry.register(WireTunnelComponent); - gComponentRegistry.register(DisplayComponent); - gComponentRegistry.register(BeltReaderComponent); - gComponentRegistry.register(FilterComponent); - gComponentRegistry.register(ItemProducerComponent); - gComponentRegistry.register(GoalAcceptorComponent); + const components = [ + StaticMapEntityComponent, + BeltComponent, + ItemEjectorComponent, + ItemAcceptorComponent, + MinerComponent, + ItemProcessorComponent, + UndergroundBeltComponent, + HubComponent, + StorageComponent, + WiredPinsComponent, + BeltUnderlaysComponent, + WireComponent, + ConstantSignalComponent, + LogicGateComponent, + LeverComponent, + WireTunnelComponent, + DisplayComponent, + BeltReaderComponent, + FilterComponent, + ItemProducerComponent, + GoalAcceptorComponent, + ]; + components.forEach(component => gComponentRegistry.register(component)); // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS - // Sanity check - If this is thrown, you (=me, lol) forgot to add a new component here + // Sanity check - If this is thrown, you forgot to add a new component here assert( // @ts-ignore diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index 3144ad96..81d2c65c 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -8,7 +8,7 @@ export const curvedBeltLength = /* Math.PI / 4 */ 0.78; /** @type {import("./item_acceptor").ItemAcceptorSlot} */ export const FAKE_BELT_ACCEPTOR_SLOT = { pos: new Vector(0, 0), - directions: [enumDirection.bottom], + direction: enumDirection.bottom, }; /** @type {Object} */ diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index 354f9024..d3df3763 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -3,9 +3,10 @@ import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { Component } from "../component"; -/** @typedef {{ +/** + * @typedef {{ * pos: Vector, - * directions: enumDirection[], + * direction: enumDirection, * filter?: ItemType * }} ItemAcceptorSlot */ @@ -14,12 +15,12 @@ import { Component } from "../component"; * @typedef {{ * slot: ItemAcceptorSlot, * index: number, - * acceptedDirection: enumDirection * }} ItemAcceptorLocatedSlot */ -/** @typedef {{ +/** + * @typedef {{ * pos: Vector, - * directions: enumDirection[], + * direction: enumDirection, * filter?: ItemType * }} ItemAcceptorSlotConfig */ @@ -64,7 +65,7 @@ export class ItemAcceptorComponent extends Component { const slot = slots[i]; this.slots.push({ pos: slot.pos, - directions: slot.directions, + direction: slot.direction, // Which type of item to accept (shape | color | all) @see ItemType filter: slot.filter, @@ -122,15 +123,11 @@ export class ItemAcceptorComponent extends Component { } // Check if the acceptor slot accepts items from our direction - for (let i = 0; i < slot.directions.length; ++i) { - // const localDirection = targetStaticComp.localDirectionToWorld(slot.directions[l]); - if (desiredDirection === slot.directions[i]) { - return { - slot, - index: slotIndex, - acceptedDirection: desiredDirection, - }; - } + if (desiredDirection === slot.direction) { + return { + slot, + index: slotIndex, + }; } } diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index f7dddec1..be7d1ce4 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -109,6 +109,11 @@ export class ItemProcessorComponent extends Component { * @type {number} */ this.bonusTime = 0; + + /** + * @type {Array} + */ + this.queuedEjects = []; } /** diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index c76a298e..a3d6a8ca 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -19,7 +19,7 @@ export class StaticMapEntityComponent extends Component { originalRotation: types.float, // See building_codes.js - code: types.uint, + code: types.uintOrString, }; } @@ -99,7 +99,7 @@ export class StaticMapEntityComponent extends Component { * @param {Vector=} param0.tileSize Size of the entity in tiles * @param {number=} param0.rotation Rotation in degrees. Must be multiple of 90 * @param {number=} param0.originalRotation Original Rotation in degrees. Must be multiple of 90 - * @param {number=} param0.code Building code + * @param {number|string=} param0.code Building code */ constructor({ origin = new Vector(), diff --git a/src/js/game/core.js b/src/js/game/core.js index a0ee3713..aa411c3d 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -38,6 +38,7 @@ import { ShapeDefinitionManager } from "./shape_definition_manager"; import { AchievementProxy } from "./achievement_proxy"; import { SoundProxy } from "./sound_proxy"; import { GameTime } from "./time/game_time"; +import { MOD_SIGNALS } from "../mods/mod_signals"; const logger = createLogger("ingame/core"); @@ -161,6 +162,7 @@ export class GameCore { } logger.log("root initialized"); + MOD_SIGNALS.gameInitialized.dispatch(root); } /** diff --git a/src/js/game/entity.js b/src/js/game/entity.js index d7dd715e..3010f067 100644 --- a/src/js/game/entity.js +++ b/src/js/game/entity.js @@ -197,20 +197,18 @@ export class Entity extends BasicSerializableObject { for (let i = 0; i < acceptorComp.slots.length; ++i) { const slot = acceptorComp.slots[i]; const slotTile = staticComp.localTileToWorld(slot.pos); - for (let k = 0; k < slot.directions.length; ++k) { - const direction = staticComp.localDirectionToWorld(slot.directions[k]); - const directionVector = enumDirectionToVector[direction]; - const angle = Math.radians(enumDirectionToAngle[direction] + 180); - context.globalAlpha = 0.4; - drawRotatedSprite({ - parameters, - sprite: acceptorSprite, - x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, - y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, - angle, - size: globalConfig.tileSize * 0.25, - }); - } + const direction = staticComp.localDirectionToWorld(slot.direction); + const directionVector = enumDirectionToVector[direction]; + const angle = Math.radians(enumDirectionToAngle[direction] + 180); + context.globalAlpha = 0.4; + drawRotatedSprite({ + parameters, + sprite: acceptorSprite, + x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, + y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, + angle, + size: globalConfig.tileSize * 0.25, + }); } } diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index 08609f89..a799b42a 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -1,4 +1,5 @@ /* typehints:start */ +import { GameSystem } from "./game_system"; import { GameRoot } from "./root"; /* typehints:end */ @@ -30,6 +31,11 @@ import { ZoneSystem } from "./systems/zone"; const logger = createLogger("game_system_manager"); +/** + * @type {Object GameSystem}>>} + */ +export const MODS_ADDITIONAL_SYSTEMS = {}; + export class GameSystemManager { /** * @@ -123,7 +129,15 @@ export class GameSystemManager { * Initializes all systems */ internalInitSystems() { + const addBefore = id => { + const systems = MODS_ADDITIONAL_SYSTEMS[id]; + if (systems) { + systems.forEach(({ id, systemClass }) => add(id, systemClass)); + } + }; + const add = (id, systemClass) => { + addBefore(id); this.systems[id] = new systemClass(this.root); this.systemUpdateOrder.push(id); }; @@ -173,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); @@ -187,6 +202,14 @@ export class GameSystemManager { add("zone", ZoneSystem); } + addBefore("end"); + + for (const key in MODS_ADDITIONAL_SYSTEMS) { + if (!this.systems[key] && key !== "end") { + logger.error("Mod system not attached due to invalid 'before': ", key); + } + } + logger.log("📦 There are", this.systemUpdateOrder.length, "game systems"); } diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 8351775e..9f9c63be 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -9,6 +9,8 @@ import { GameRoot } from "./root"; import { enumSubShape, ShapeDefinition } from "./shape_definition"; import { enumHubGoalRewards } from "./tutorial_goals"; +export const MOD_ITEM_PROCESSOR_SPEEDS = {}; + export class HubGoals extends BasicSerializableObject { static getId() { return "HubGoals"; @@ -433,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; @@ -556,6 +558,9 @@ export class HubGoals extends BasicSerializableObject { ); } default: + if (MOD_ITEM_PROCESSOR_SPEEDS[processorType]) { + return MOD_ITEM_PROCESSOR_SPEEDS[processorType](this.root); + } assertAlways(false, "invalid processor type: " + processorType); } diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 3d22787c..2f3b5629 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -1,6 +1,7 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; import { Signal } from "../../core/signal"; +import { MOD_SIGNALS } from "../../mods/mod_signals"; import { KEYMAPPINGS } from "../key_action_mapper"; import { MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; @@ -64,7 +65,7 @@ export class GameHUD { /* typehints:end */ }; - if (G_IS_DEV && globalConfig.debug.enableEntityInspector) { + if (G_IS_DEV) { this.parts.entityDebugger = new HUDEntityDebugger(this.root); } @@ -89,8 +90,11 @@ 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]); this.parts[key].createElements(frag); } @@ -98,6 +102,7 @@ export class GameHUD { for (const key in this.parts) { this.parts[key].initialize(); + MOD_SIGNALS.hudElementFinalized.dispatch(this.parts[key]); } this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); diff --git a/src/js/game/hud/parts/base_toolbar.js b/src/js/game/hud/parts/base_toolbar.js index 15faad66..bee88574 100644 --- a/src/js/game/hud/parts/base_toolbar.js +++ b/src/js/game/hud/parts/base_toolbar.js @@ -1,4 +1,5 @@ import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { globalWarn } from "../../../core/logging"; import { STOP_PROPAGATION } from "../../../core/signal"; import { makeDiv, safeModulo } from "../../../core/utils"; import { MetaBlockBuilding } from "../../buildings/block"; @@ -101,7 +102,12 @@ export class HUDBaseToolbar extends BaseHUDPart { rawBinding = KEYMAPPINGS.buildings[metaBuilding.getId()]; } - const binding = actionMapper.getBinding(rawBinding); + if (rawBinding) { + const binding = actionMapper.getBinding(rawBinding); + binding.add(() => this.selectBuildingForPlacement(metaBuilding)); + } else { + globalWarn("Building has no keybinding:", metaBuilding.getId()); + } const itemContainer = makeDiv( this.primaryBuildings.includes(allBuildings[i]) ? rowPrimary : rowSecondary, @@ -110,7 +116,6 @@ export class HUDBaseToolbar extends BaseHUDPart { ); itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png"); itemContainer.setAttribute("data-id", metaBuilding.getId()); - binding.add(() => this.selectBuildingForPlacement(metaBuilding)); const icon = makeDiv(itemContainer, null, ["icon"]); diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index 33e6ebc2..d2904720 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -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; @@ -126,12 +126,15 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { rawBinding = KEYMAPPINGS.buildings[metaBuilding.getId()]; } - const binding = this.root.keyMapper.getBinding(rawBinding); - - this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace( - "", - "" + binding.getKeyCodeString() + "" - ); + if (rawBinding) { + const binding = this.root.keyMapper.getBinding(rawBinding); + this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace( + "", + "" + binding.getKeyCodeString() + "" + ); + } else { + this.buildingInfoElements.hotkey.innerHTML = ""; + } this.buildingInfoElements.tutorialImage.setAttribute( "data-icon", @@ -355,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; @@ -394,6 +397,46 @@ 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 + * @param {Vector[]=} ignorePositions + * @returns + */ + checkForObstales(from, to, ignorePositions = []) { + 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 (ignorePositions.some(p => p.distanceSquare(current) < 0.1)) { + continue; + } + if (!this.root.logic.checkCanPlaceEntity(this.fakeEntity, { allowReplaceBuildings: false })) { + return true; + } + } + return false; + } + /** * @param {DrawParameters} parameters */ @@ -404,55 +447,76 @@ 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.lastDragTile, + mouseTile, + ]) || + this.checkForObstales(this.currentDirectionLockCorner, mouseTile, [this.lastDragTile, 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[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); - } + // 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); + + 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); } } @@ -473,7 +537,13 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const offsetShift = 10; + /** + * @type {Array} + */ let acceptorSlots = []; + /** + * @type {Array} + */ let ejectorSlots = []; if (ejectorComp) { @@ -491,71 +561,65 @@ 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(); - // Go over all slots - for ( - let acceptorDirectionIndex = 0; - acceptorDirectionIndex < slot.directions.length; - ++acceptorDirectionIndex - ) { - const direction = slot.directions[acceptorDirectionIndex]; - const worldDirection = staticComp.localDirectionToWorld(direction); - - // Figure out which tile ejects to this slot - const sourceTile = acceptorSlotWsTile.add(enumDirectionToVector[worldDirection]); - - let isBlocked = false; - let isConnected = false; - - // Find all entities which are on that tile - 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]; - const sourceEjector = sourceEntity.components.ItemEjector; - const sourceBeltComp = sourceEntity.components.Belt; - const sourceStaticComp = sourceEntity.components.StaticMapEntity; - const ejectorAcceptLocalTile = sourceStaticComp.worldToLocalTile(acceptorSlotWsTile); - - // If this entity is on the same layer as the slot - if so, it can either be - // connected, or it can not be connected and thus block the input - if (sourceEjector && sourceEjector.anySlotEjectsToLocalTile(ejectorAcceptLocalTile)) { - // This one is connected, all good - isConnected = true; - } else if ( - sourceBeltComp && - sourceStaticComp.localDirectionToWorld(sourceBeltComp.direction) === - enumInvertedDirections[worldDirection] - ) { - // Belt connected - isConnected = true; - } else { - // This one is blocked - isBlocked = true; - } - } + const direction = slot.direction; + const worldDirection = staticComp.localDirectionToWorld(direction); - const alpha = isConnected || isBlocked ? 1.0 : 0.3; - const sprite = isBlocked ? badArrowSprite : goodArrowSprite; - - parameters.context.globalAlpha = alpha; - drawRotatedSprite({ - parameters, - sprite, - x: acceptorSlotWsPos.x, - y: acceptorSlotWsPos.y, - angle: Math.radians(enumDirectionToAngle[enumInvertedDirections[worldDirection]]), - size: 13, - offsetY: offsetShift + 13, - }); - parameters.context.globalAlpha = 1; + // Figure out which tile ejects to this slot + const sourceTile = acceptorSlotWsTile.add(enumDirectionToVector[worldDirection]); + + let isBlocked = false; + let isConnected = false; + + // Find all entities which are on that tile + const sourceEntities = this.root.map.getLayersContentsMultipleXY(sourceTile.x, sourceTile.y); + + // Check for every entity: + 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; + const ejectorAcceptLocalTile = sourceStaticComp.worldToLocalTile(acceptorSlotWsTile); + + // If this entity is on the same layer as the slot - if so, it can either be + // connected, or it can not be connected and thus block the input + if (sourceEjector && sourceEjector.anySlotEjectsToLocalTile(ejectorAcceptLocalTile)) { + // This one is connected, all good + isConnected = true; + } else if ( + sourceBeltComp && + sourceStaticComp.localDirectionToWorld(sourceBeltComp.direction) === + enumInvertedDirections[worldDirection] + ) { + // Belt connected + isConnected = true; + } else { + // This one is blocked + isBlocked = true; + } } + + const alpha = isConnected || isBlocked ? 1.0 : 0.3; + const sprite = isBlocked ? badArrowSprite : goodArrowSprite; + + parameters.context.globalAlpha = alpha; + drawRotatedSprite({ + parameters, + sprite, + x: acceptorSlotWsPos.x, + y: acceptorSlotWsPos.y, + angle: Math.radians(enumDirectionToAngle[enumInvertedDirections[worldDirection]]), + size: 13, + offsetY: offsetShift + 13, + }); + parameters.context.globalAlpha = 1; } // Go over all slots diff --git a/src/js/game/hud/parts/constant_signal_edit.js b/src/js/game/hud/parts/constant_signal_edit.js index 283c7619..a6e7501d 100644 --- a/src/js/game/hud/parts/constant_signal_edit.js +++ b/src/js/game/hud/parts/constant_signal_edit.js @@ -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; + } } 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/hud/parts/modal_dialogs.js b/src/js/game/hud/parts/modal_dialogs.js index 33211cf6..f86160bc 100644 --- a/src/js/game/hud/parts/modal_dialogs.js +++ b/src/js/game/hud/parts/modal_dialogs.js @@ -150,7 +150,7 @@ export class HUDModalDialogs extends BaseHUDPart { internalShowDialog(dialog) { const elem = dialog.createElement(); - dialog.setIndex(this.dialogStack.length); + dialog.setIndex(1000 + this.dialogStack.length); // Hide last dialog in queue if (this.dialogStack.length > 0) { diff --git a/src/js/game/hud/parts/HUDPuzzleNextPuzzle.js b/src/js/game/hud/parts/next_puzzle.js similarity index 100% rename from src/js/game/hud/parts/HUDPuzzleNextPuzzle.js rename to src/js/game/hud/parts/next_puzzle.js diff --git a/src/js/game/hud/parts/notifications.js b/src/js/game/hud/parts/notifications.js index bef8dd0f..abeab205 100644 --- a/src/js/game/hud/parts/notifications.js +++ b/src/js/game/hud/parts/notifications.js @@ -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"); diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 090b8b83..3378368a 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -7,118 +7,152 @@ import { Application } from "../application"; import { Signal, STOP_PROPAGATION } from "../core/signal"; import { IS_MOBILE } from "../core/config"; import { T } from "../translations"; -function key(str) { + +export function keyToKeyCode(str) { return str.toUpperCase().charCodeAt(0); } -const KEYCODE_UP_ARROW = 38; -const KEYCODE_DOWN_ARROW = 40; -const KEYCODE_LEFT_ARROW = 37; -const KEYCODE_RIGHT_ARROW = 39; +export const KEYCODES = { + Tab: 9, + Enter: 13, + + Shift: 16, + Ctrl: 17, + Alt: 18, + + Escape: 27, + + Space: 32, + + ArrowLeft: 37, + ArrowUp: 38, + ArrowRight: 39, + ArrowDown: 40, + + Delete: 46, + + F1: 112, + F2: 113, + F3: 114, + F4: 115, + F5: 116, + F6: 117, + F7: 118, + F8: 119, + F9: 120, + F10: 121, + F11: 122, + F12: 123, + + Plus: 187, + Minus: 189, +}; export const KEYMAPPINGS = { + // Make sure mods come first so they can override everything + mods: {}, + general: { - confirm: { keyCode: 13 }, // enter - back: { keyCode: 27, builtin: true }, // escape + confirm: { keyCode: KEYCODES.Enter }, + back: { keyCode: KEYCODES.Escape, builtin: true }, }, ingame: { - menuOpenShop: { keyCode: key("F") }, - menuOpenStats: { keyCode: key("G") }, - menuClose: { keyCode: key("Q") }, + menuOpenShop: { keyCode: keyToKeyCode("F") }, + menuOpenStats: { keyCode: keyToKeyCode("G") }, + menuClose: { keyCode: keyToKeyCode("Q") }, - toggleHud: { keyCode: 113 }, // F2 - exportScreenshot: { keyCode: 114 }, // F3PS - toggleFPSInfo: { keyCode: 115 }, // F4 + toggleHud: { keyCode: KEYCODES.F2 }, + exportScreenshot: { keyCode: KEYCODES.F3 }, + toggleFPSInfo: { keyCode: KEYCODES.F4 }, - switchLayers: { keyCode: key("E") }, + switchLayers: { keyCode: keyToKeyCode("E") }, - showShapeTooltip: { keyCode: 18 }, // ALT + showShapeTooltip: { keyCode: KEYCODES.Alt }, }, navigation: { - mapMoveUp: { keyCode: key("W") }, - mapMoveRight: { keyCode: key("D") }, - mapMoveDown: { keyCode: key("S") }, - mapMoveLeft: { keyCode: key("A") }, - mapMoveFaster: { keyCode: 16 }, //shift - - centerMap: { keyCode: 32 }, // SPACE - mapZoomIn: { keyCode: 187, repeated: true }, // "+" - mapZoomOut: { keyCode: 189, repeated: true }, // "-" - - createMarker: { keyCode: key("M") }, + mapMoveUp: { keyCode: keyToKeyCode("W") }, + mapMoveRight: { keyCode: keyToKeyCode("D") }, + mapMoveDown: { keyCode: keyToKeyCode("S") }, + mapMoveLeft: { keyCode: keyToKeyCode("A") }, + mapMoveFaster: { keyCode: KEYCODES.Shift }, + + centerMap: { keyCode: KEYCODES.Space }, + mapZoomIn: { keyCode: KEYCODES.Plus, repeated: true }, + mapZoomOut: { keyCode: KEYCODES.Minus, repeated: true }, + createMarker: { keyCode: keyToKeyCode("M") }, }, buildings: { // Puzzle buildings - constant_producer: { keyCode: key("H") }, - goal_acceptor: { keyCode: key("N") }, - block: { keyCode: key("4") }, + constant_producer: { keyCode: keyToKeyCode("H") }, + goal_acceptor: { keyCode: keyToKeyCode("N") }, + block: { keyCode: keyToKeyCode("4") }, // Primary Toolbar - belt: { keyCode: key("1") }, - balancer: { keyCode: key("2") }, - underground_belt: { keyCode: key("3") }, - miner: { keyCode: key("4") }, - cutter: { keyCode: key("5") }, - rotater: { keyCode: key("6") }, - stacker: { keyCode: key("7") }, - mixer: { keyCode: key("8") }, - painter: { keyCode: key("9") }, - trash: { keyCode: key("0") }, + belt: { keyCode: keyToKeyCode("1") }, + balancer: { keyCode: keyToKeyCode("2") }, + underground_belt: { keyCode: keyToKeyCode("3") }, + miner: { keyCode: keyToKeyCode("4") }, + cutter: { keyCode: keyToKeyCode("5") }, + rotater: { keyCode: keyToKeyCode("6") }, + stacker: { keyCode: keyToKeyCode("7") }, + mixer: { keyCode: keyToKeyCode("8") }, + painter: { keyCode: keyToKeyCode("9") }, + trash: { keyCode: keyToKeyCode("0") }, // Sandbox - item_producer: { keyCode: key("L") }, + item_producer: { keyCode: keyToKeyCode("L") }, // Secondary toolbar - storage: { keyCode: key("Y") }, - reader: { keyCode: key("U") }, - lever: { keyCode: key("I") }, - filter: { keyCode: key("O") }, - display: { keyCode: key("P") }, + storage: { keyCode: keyToKeyCode("Y") }, + reader: { keyCode: keyToKeyCode("U") }, + lever: { keyCode: keyToKeyCode("I") }, + filter: { keyCode: keyToKeyCode("O") }, + display: { keyCode: keyToKeyCode("P") }, // Wires toolbar - wire: { keyCode: key("1") }, - wire_tunnel: { keyCode: key("2") }, - constant_signal: { keyCode: key("3") }, - logic_gate: { keyCode: key("4") }, - virtual_processor: { keyCode: key("5") }, - analyzer: { keyCode: key("6") }, - comparator: { keyCode: key("7") }, - transistor: { keyCode: key("8") }, + wire: { keyCode: keyToKeyCode("1") }, + wire_tunnel: { keyCode: keyToKeyCode("2") }, + constant_signal: { keyCode: keyToKeyCode("3") }, + logic_gate: { keyCode: keyToKeyCode("4") }, + virtual_processor: { keyCode: keyToKeyCode("5") }, + analyzer: { keyCode: keyToKeyCode("6") }, + comparator: { keyCode: keyToKeyCode("7") }, + transistor: { keyCode: keyToKeyCode("8") }, }, placement: { - pipette: { keyCode: key("Q") }, - rotateWhilePlacing: { keyCode: key("R") }, - rotateInverseModifier: { keyCode: 16 }, // SHIFT - rotateToUp: { keyCode: KEYCODE_UP_ARROW }, - rotateToDown: { keyCode: KEYCODE_DOWN_ARROW }, - rotateToRight: { keyCode: KEYCODE_RIGHT_ARROW }, - rotateToLeft: { keyCode: KEYCODE_LEFT_ARROW }, - cycleBuildingVariants: { keyCode: key("T") }, - cycleBuildings: { keyCode: 9 }, // TAB - switchDirectionLockSide: { keyCode: key("R") }, - - copyWireValue: { keyCode: key("Z") }, + pipette: { keyCode: keyToKeyCode("Q") }, + rotateWhilePlacing: { keyCode: keyToKeyCode("R") }, + rotateInverseModifier: { keyCode: KEYCODES.Shift }, + rotateToUp: { keyCode: KEYCODES.ArrowUp }, + rotateToDown: { keyCode: KEYCODES.ArrowDown }, + rotateToRight: { keyCode: KEYCODES.ArrowRight }, + rotateToLeft: { keyCode: KEYCODES.ArrowLeft }, + cycleBuildingVariants: { keyCode: keyToKeyCode("T") }, + cycleBuildings: { keyCode: KEYCODES.Tab }, + switchDirectionLockSide: { keyCode: keyToKeyCode("R") }, + + copyWireValue: { keyCode: keyToKeyCode("Z") }, }, massSelect: { - massSelectStart: { keyCode: 17 }, // CTRL - massSelectSelectMultiple: { keyCode: 16 }, // SHIFT - massSelectCopy: { keyCode: key("C") }, - massSelectCut: { keyCode: key("X") }, - massSelectClear: { keyCode: key("B") }, - confirmMassDelete: { keyCode: 46 }, // DEL - pasteLastBlueprint: { keyCode: key("V") }, + massSelectStart: { keyCode: KEYCODES.Ctrl }, + massSelectSelectMultiple: { keyCode: KEYCODES.Shift }, + massSelectCopy: { keyCode: keyToKeyCode("C") }, + massSelectCut: { keyCode: keyToKeyCode("X") }, + massSelectClear: { keyCode: keyToKeyCode("B") }, + confirmMassDelete: { keyCode: KEYCODES.Delete }, + pasteLastBlueprint: { keyCode: keyToKeyCode("V") }, }, placementModifiers: { - lockBeltDirection: { keyCode: 16 }, // SHIFT - placementDisableAutoOrientation: { keyCode: 17 }, // CTRL - placeMultiple: { keyCode: 16 }, // SHIFT - placeInverse: { keyCode: 18 }, // ALT + lockBeltDirection: { keyCode: KEYCODES.Shift }, + placementDisableAutoOrientation: { keyCode: KEYCODES.Ctrl }, + placeMultiple: { keyCode: KEYCODES.Shift }, + placeInverse: { keyCode: KEYCODES.Alt }, }, }; @@ -153,23 +187,23 @@ export function getStringForKeyCode(code) { return "MB5"; case 8: return "⌫"; - case 9: + case KEYCODES.Tab: return T.global.keys.tab; - case 13: + case KEYCODES.Enter: return "⏎"; - case 16: + case KEYCODES.Shift: return "⇪"; - case 17: + case KEYCODES.Ctrl: return T.global.keys.control; - case 18: + case KEYCODES.Alt: return T.global.keys.alt; case 19: return "PAUSE"; case 20: return "CAPS"; - case 27: + case KEYCODES.Escape: return T.global.keys.escape; - case 32: + case KEYCODES.Space: return T.global.keys.space; case 33: return "PGUP"; @@ -179,13 +213,13 @@ export function getStringForKeyCode(code) { return "END"; case 36: return "HOME"; - case KEYCODE_LEFT_ARROW: + case KEYCODES.ArrowLeft: return "⬅"; - case KEYCODE_UP_ARROW: + case KEYCODES.ArrowUp: return "⬆"; - case KEYCODE_RIGHT_ARROW: + case KEYCODES.ArrowRight: return "➡"; - case KEYCODE_DOWN_ARROW: + case KEYCODES.ArrowDown: return "⬇"; case 44: return "PRNT"; @@ -225,29 +259,29 @@ export function getStringForKeyCode(code) { return "."; case 111: return "/"; - case 112: + case KEYCODES.F1: return "F1"; - case 113: + case KEYCODES.F2: return "F2"; - case 114: + case KEYCODES.F3: return "F3"; - case 115: + case KEYCODES.F4: return "F4"; - case 116: + case KEYCODES.F5: return "F5"; - case 117: + case KEYCODES.F6: return "F6"; - case 118: + case KEYCODES.F7: return "F7"; - case 119: + case KEYCODES.F8: return "F8"; - case 120: + case KEYCODES.F9: return "F9"; - case 121: + case KEYCODES.F10: return "F10"; - case 122: + case KEYCODES.F11: return "F11"; - case 123: + case KEYCODES.F12: return "F12"; case 144: @@ -296,8 +330,9 @@ export class Keybinding { * @param {number} param0.keyCode * @param {boolean=} param0.builtin * @param {boolean=} param0.repeated + * @param {{ shift?: boolean; alt?: boolean; ctrl?: boolean; }=} param0.modifiers */ - constructor(keyMapper, app, { keyCode, builtin = false, repeated = false }) { + constructor(keyMapper, app, { keyCode, builtin = false, repeated = false, modifiers = {} }) { assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode); this.keyMapper = keyMapper; this.app = app; @@ -305,6 +340,8 @@ export class Keybinding { this.builtin = builtin; this.repeated = repeated; + this.modifiers = modifiers; + this.signal = new Signal(); this.toggled = new Signal(); } @@ -395,7 +432,6 @@ export class KeyActionMapper { if (overrides[key]) { payload.keyCode = overrides[key]; } - this.keybindings[key] = new Keybinding(this, this.root.app, payload); if (G_IS_DEV) { @@ -459,9 +495,10 @@ export class KeyActionMapper { * @param {number} param0.keyCode * @param {boolean} param0.shift * @param {boolean} param0.alt + * @param {boolean} param0.ctrl * @param {boolean=} param0.initial */ - handleKeydown({ keyCode, shift, alt, initial }) { + handleKeydown({ keyCode, shift, alt, ctrl, initial }) { let stop = false; // Find mapping @@ -469,6 +506,18 @@ export class KeyActionMapper { /** @type {Keybinding} */ const binding = this.keybindings[key]; if (binding.keyCode === keyCode && (initial || binding.repeated)) { + if (binding.modifiers.shift && !shift) { + continue; + } + + if (binding.modifiers.ctrl && !ctrl) { + continue; + } + + if (binding.modifiers.alt && !alt) { + continue; + } + /** @type {Signal} */ const signal = this.keybindings[key].signal; if (signal.dispatch() === STOP_PROPAGATION) { @@ -505,4 +554,14 @@ export class KeyActionMapper { assert(this.keybindings[id], "Keybinding " + id + " not known!"); return this.keybindings[id]; } + + /** + * Returns a given keybinding + * @param {string} id + * @returns {Keybinding} + */ + getBindingById(id) { + assert(this.keybindings[id], "Keybinding " + id + " not known!"); + return this.keybindings[id]; + } } diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 79104958..0e915fea 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -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); @@ -393,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; @@ -434,19 +443,16 @@ export class GameLogic { for (let acceptorSlot = 0; acceptorSlot < acceptorSlots.length; ++acceptorSlot) { const slot = acceptorSlots[acceptorSlot]; const wsTile = staticComp.localTileToWorld(slot.pos); - for (let k = 0; k < slot.directions.length; ++k) { - const direction = slot.directions[k]; - const wsDirection = staticComp.localDirectionToWorld(direction); - - const sourceTile = wsTile.add(enumDirectionToVector[wsDirection]); - if (sourceTile.equals(tile)) { - acceptors.push({ - entity, - slot, - toTile: wsTile, - fromDirection: wsDirection, - }); - } + const direction = slot.direction; + const wsDirection = staticComp.localDirectionToWorld(direction); + const sourceTile = wsTile.add(enumDirectionToVector[wsDirection]); + if (sourceTile.equals(tile)) { + acceptors.push({ + entity, + slot, + toTile: wsTile, + fromDirection: wsDirection, + }); } } } diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 54af1125..b966a322 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -13,6 +13,11 @@ import { Rectangle } from "../core/rectangle"; const logger = createLogger("map_chunk"); +/** + * @type {Object number>} + */ +export const MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS = {}; + export class MapChunk { /** * @@ -192,6 +197,10 @@ export class MapChunk { [enumSubShape.windmill]: Math.round(6 + clamp(distanceToOriginInChunks / 2, 0, 20)), }; + for (const key in MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS) { + weights[key] = MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[key](distanceToOriginInChunks); + } + if (distanceToOriginInChunks < 7) { // Initial chunks can not spawn the good stuff weights[enumSubShape.star] = 0; @@ -274,6 +283,17 @@ export class MapChunk { const chunkCenter = new Vector(this.x, this.y).addScalar(0.5); const distanceToOriginInChunks = Math.round(chunkCenter.length()); + this.generatePatches({ rng, chunkCenter, distanceToOriginInChunks }); + } + + /** + * + * @param {object} param0 + * @param {RandomNumberGenerator} param0.rng + * @param {Vector} param0.chunkCenter + * @param {number} param0.distanceToOriginInChunks + */ + generatePatches({ rng, chunkCenter, distanceToOriginInChunks }) { // Determine how likely it is that there is a color patch const colorPatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5; @@ -424,7 +444,7 @@ export class MapChunk { * Sets the chunks contents * @param {number} tileX * @param {number} tileY - * @param {Entity=} contents + * @param {Entity} contents * @param {Layer} layer */ setLayerContentFromWorldCords(tileX, tileY, contents, layer) { diff --git a/src/js/game/map_chunk_aggregate.js b/src/js/game/map_chunk_aggregate.js index de15362d..f47ed676 100644 --- a/src/js/game/map_chunk_aggregate.js +++ b/src/js/game/map_chunk_aggregate.js @@ -2,10 +2,9 @@ import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; import { drawSpriteClipped } from "../core/draw_utils"; import { safeModulo } from "../core/utils"; +import { CHUNK_OVERLAY_RES } from "./map_chunk_view"; import { GameRoot } from "./root"; -export const CHUNK_OVERLAY_RES = 3; - export class MapChunkAggregate { /** * diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 947b7a9f..86c14fb8 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -5,10 +5,20 @@ import { Entity } from "./entity"; import { MapChunk } from "./map_chunk"; import { GameRoot } from "./root"; import { THEME } from "./theme"; -import { drawSpriteClipped } from "../core/draw_utils"; export const CHUNK_OVERLAY_RES = 3; +export const MOD_CHUNK_DRAW_HOOKS = { + backgroundLayerBefore: [], + backgroundLayerAfter: [], + + foregroundDynamicBefore: [], + foregroundDynamicAfter: [], + + staticBefore: [], + staticAfter: [], +}; + export class MapChunkView extends MapChunk { /** * @@ -42,6 +52,11 @@ export class MapChunkView extends MapChunk { */ drawBackgroundLayer(parameters) { const systems = this.root.systemMgr.systems; + + MOD_CHUNK_DRAW_HOOKS.backgroundLayerBefore.forEach(systemId => + systems[systemId].drawChunk(parameters, this) + ); + if (systems.zone) { systems.zone.drawChunk(parameters, this); } @@ -52,6 +67,10 @@ export class MapChunkView extends MapChunk { systems.beltUnderlays.drawChunk(parameters, this); systems.belt.drawChunk(parameters, this); + + MOD_CHUNK_DRAW_HOOKS.backgroundLayerAfter.forEach(systemId => + systems[systemId].drawChunk(parameters, this) + ); } /** @@ -61,9 +80,17 @@ export class MapChunkView extends MapChunk { drawForegroundDynamicLayer(parameters) { const systems = this.root.systemMgr.systems; + MOD_CHUNK_DRAW_HOOKS.foregroundDynamicBefore.forEach(systemId => + systems[systemId].drawChunk(parameters, this) + ); + systems.itemEjector.drawChunk(parameters, this); systems.itemAcceptor.drawChunk(parameters, this); systems.miner.drawChunk(parameters, this); + + MOD_CHUNK_DRAW_HOOKS.foregroundDynamicAfter.forEach(systemId => + systems[systemId].drawChunk(parameters, this) + ); } /** @@ -73,6 +100,8 @@ export class MapChunkView extends MapChunk { drawForegroundStaticLayer(parameters) { const systems = this.root.systemMgr.systems; + MOD_CHUNK_DRAW_HOOKS.staticBefore.forEach(systemId => systems[systemId].drawChunk(parameters, this)); + systems.staticMapEntities.drawChunk(parameters, this); systems.lever.drawChunk(parameters, this); systems.display.drawChunk(parameters, this); @@ -80,6 +109,8 @@ export class MapChunkView extends MapChunk { systems.constantProducer.drawChunk(parameters, this); systems.goalAcceptor.drawChunk(parameters, this); systems.itemProcessorOverlays.drawChunk(parameters, this); + + MOD_CHUNK_DRAW_HOOKS.staticAfter.forEach(systemId => systems[systemId].drawChunk(parameters, this)); } /** diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js index 7bfbce25..0e92d3d9 100644 --- a/src/js/game/meta_building.js +++ b/src/js/game/meta_building.js @@ -18,6 +18,16 @@ export class MetaBuilding { this.id = id; } + /** + * Should return all possible variants of this building, no matter + * if they are already available or will be unlocked later on + * + * @returns {Array<{ variant: string, rotationVariant?: number, internalId?: number|string }>} + */ + static getAllVariantCombinations() { + throw new Error("implement getAllVariantCombinations"); + } + /** * Returns the id of this building */ diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 0c93153d..55bc46d4 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -2,202 +2,89 @@ import { gMetaBuildingRegistry } from "../core/global_registries"; import { createLogger } from "../core/logging"; import { T } from "../translations"; import { MetaAnalyzerBuilding } from "./buildings/analyzer"; -import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer"; +import { MetaBalancerBuilding } from "./buildings/balancer"; import { MetaBeltBuilding } from "./buildings/belt"; import { MetaBlockBuilding } from "./buildings/block"; import { MetaComparatorBuilding } from "./buildings/comparator"; import { MetaConstantProducerBuilding } from "./buildings/constant_producer"; import { MetaConstantSignalBuilding } from "./buildings/constant_signal"; -import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter"; +import { MetaCutterBuilding } from "./buildings/cutter"; import { MetaDisplayBuilding } from "./buildings/display"; import { MetaFilterBuilding } from "./buildings/filter"; import { MetaGoalAcceptorBuilding } from "./buildings/goal_acceptor"; import { MetaHubBuilding } from "./buildings/hub"; import { MetaItemProducerBuilding } from "./buildings/item_producer"; import { MetaLeverBuilding } from "./buildings/lever"; -import { enumLogicGateVariants, MetaLogicGateBuilding } from "./buildings/logic_gate"; -import { enumMinerVariants, MetaMinerBuilding } from "./buildings/miner"; +import { MetaLogicGateBuilding } from "./buildings/logic_gate"; +import { MetaMinerBuilding } from "./buildings/miner"; import { MetaMixerBuilding } from "./buildings/mixer"; -import { enumPainterVariants, MetaPainterBuilding } from "./buildings/painter"; +import { MetaPainterBuilding } from "./buildings/painter"; import { MetaReaderBuilding } from "./buildings/reader"; -import { enumRotaterVariants, MetaRotaterBuilding } from "./buildings/rotater"; +import { MetaRotaterBuilding } from "./buildings/rotater"; import { MetaStackerBuilding } from "./buildings/stacker"; import { MetaStorageBuilding } from "./buildings/storage"; -import { enumTransistorVariants, MetaTransistorBuilding } from "./buildings/transistor"; +import { MetaTransistorBuilding } from "./buildings/transistor"; import { MetaTrashBuilding } from "./buildings/trash"; -import { enumUndergroundBeltVariants, MetaUndergroundBeltBuilding } from "./buildings/underground_belt"; -import { enumVirtualProcessorVariants, MetaVirtualProcessorBuilding } from "./buildings/virtual_processor"; +import { MetaUndergroundBeltBuilding } from "./buildings/underground_belt"; +import { MetaVirtualProcessorBuilding } from "./buildings/virtual_processor"; import { MetaWireBuilding } from "./buildings/wire"; import { MetaWireTunnelBuilding } from "./buildings/wire_tunnel"; import { buildBuildingCodeCache, gBuildingVariants, registerBuildingVariant } from "./building_codes"; -import { enumWireVariant } from "./components/wire"; import { KEYMAPPINGS } from "./key_action_mapper"; -import { defaultBuildingVariant } from "./meta_building"; +import { defaultBuildingVariant, MetaBuilding } from "./meta_building"; const logger = createLogger("building_registry"); -export function initMetaBuildingRegistry() { - gMetaBuildingRegistry.register(MetaBalancerBuilding); - gMetaBuildingRegistry.register(MetaMinerBuilding); - gMetaBuildingRegistry.register(MetaCutterBuilding); - gMetaBuildingRegistry.register(MetaRotaterBuilding); - gMetaBuildingRegistry.register(MetaStackerBuilding); - gMetaBuildingRegistry.register(MetaMixerBuilding); - gMetaBuildingRegistry.register(MetaPainterBuilding); - gMetaBuildingRegistry.register(MetaTrashBuilding); - gMetaBuildingRegistry.register(MetaStorageBuilding); - gMetaBuildingRegistry.register(MetaBeltBuilding); - gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding); - gMetaBuildingRegistry.register(MetaGoalAcceptorBuilding); - gMetaBuildingRegistry.register(MetaHubBuilding); - gMetaBuildingRegistry.register(MetaWireBuilding); - gMetaBuildingRegistry.register(MetaConstantSignalBuilding); - gMetaBuildingRegistry.register(MetaLogicGateBuilding); - gMetaBuildingRegistry.register(MetaLeverBuilding); - gMetaBuildingRegistry.register(MetaFilterBuilding); - gMetaBuildingRegistry.register(MetaWireTunnelBuilding); - gMetaBuildingRegistry.register(MetaDisplayBuilding); - gMetaBuildingRegistry.register(MetaVirtualProcessorBuilding); - gMetaBuildingRegistry.register(MetaReaderBuilding); - gMetaBuildingRegistry.register(MetaTransistorBuilding); - gMetaBuildingRegistry.register(MetaAnalyzerBuilding); - gMetaBuildingRegistry.register(MetaComparatorBuilding); - gMetaBuildingRegistry.register(MetaItemProducerBuilding); - gMetaBuildingRegistry.register(MetaConstantProducerBuilding); - gMetaBuildingRegistry.register(MetaBlockBuilding); - - // Belt - registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0); - registerBuildingVariant(2, MetaBeltBuilding, defaultBuildingVariant, 1); - registerBuildingVariant(3, MetaBeltBuilding, defaultBuildingVariant, 2); - - // Balancer - registerBuildingVariant(4, MetaBalancerBuilding); - registerBuildingVariant(5, MetaBalancerBuilding, enumBalancerVariants.merger); - registerBuildingVariant(6, MetaBalancerBuilding, enumBalancerVariants.mergerInverse); - registerBuildingVariant(47, MetaBalancerBuilding, enumBalancerVariants.splitter); - registerBuildingVariant(48, MetaBalancerBuilding, enumBalancerVariants.splitterInverse); - - // Miner - registerBuildingVariant(7, MetaMinerBuilding); - registerBuildingVariant(8, MetaMinerBuilding, enumMinerVariants.chainable); - - // Cutter - registerBuildingVariant(9, MetaCutterBuilding); - registerBuildingVariant(10, MetaCutterBuilding, enumCutterVariants.quad); - - // Rotater - registerBuildingVariant(11, MetaRotaterBuilding); - registerBuildingVariant(12, MetaRotaterBuilding, enumRotaterVariants.ccw); - registerBuildingVariant(13, MetaRotaterBuilding, enumRotaterVariants.rotate180); - - // Stacker - registerBuildingVariant(14, MetaStackerBuilding); - - // Mixer - registerBuildingVariant(15, MetaMixerBuilding); - - // Painter - registerBuildingVariant(16, MetaPainterBuilding); - registerBuildingVariant(17, MetaPainterBuilding, enumPainterVariants.mirrored); - registerBuildingVariant(18, MetaPainterBuilding, enumPainterVariants.double); - registerBuildingVariant(19, MetaPainterBuilding, enumPainterVariants.quad); - - // Trash - registerBuildingVariant(20, MetaTrashBuilding); - - // Storage - registerBuildingVariant(21, MetaStorageBuilding); - - // Underground belt - registerBuildingVariant(22, MetaUndergroundBeltBuilding, defaultBuildingVariant, 0); - registerBuildingVariant(23, MetaUndergroundBeltBuilding, defaultBuildingVariant, 1); - registerBuildingVariant(24, MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2, 0); - registerBuildingVariant(25, MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2, 1); - - // Hub - registerBuildingVariant(26, MetaHubBuilding); - - // Wire - registerBuildingVariant(27, MetaWireBuilding, defaultBuildingVariant, 0); - registerBuildingVariant(28, MetaWireBuilding, defaultBuildingVariant, 1); - registerBuildingVariant(29, MetaWireBuilding, defaultBuildingVariant, 2); - registerBuildingVariant(30, MetaWireBuilding, defaultBuildingVariant, 3); - - registerBuildingVariant(52, MetaWireBuilding, enumWireVariant.second, 0); - registerBuildingVariant(53, MetaWireBuilding, enumWireVariant.second, 1); - registerBuildingVariant(54, MetaWireBuilding, enumWireVariant.second, 2); - registerBuildingVariant(55, MetaWireBuilding, enumWireVariant.second, 3); - - // Constant signal - registerBuildingVariant(31, MetaConstantSignalBuilding); - - // Logic gate - registerBuildingVariant(32, MetaLogicGateBuilding); - registerBuildingVariant(34, MetaLogicGateBuilding, enumLogicGateVariants.not); - registerBuildingVariant(35, MetaLogicGateBuilding, enumLogicGateVariants.xor); - registerBuildingVariant(36, MetaLogicGateBuilding, enumLogicGateVariants.or); - - // Transistor - registerBuildingVariant(38, MetaTransistorBuilding, defaultBuildingVariant); - registerBuildingVariant(60, MetaTransistorBuilding, enumTransistorVariants.mirrored); - - // Lever - registerBuildingVariant(33, MetaLeverBuilding); - - // Filter - registerBuildingVariant(37, MetaFilterBuilding); - - // Wire tunnel - registerBuildingVariant(39, MetaWireTunnelBuilding); - - // Display - registerBuildingVariant(40, MetaDisplayBuilding); - - // Virtual Processor - registerBuildingVariant(42, MetaVirtualProcessorBuilding); - registerBuildingVariant(44, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.rotater); - registerBuildingVariant(45, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.unstacker); - registerBuildingVariant(50, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.stacker); - registerBuildingVariant(51, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.painter); - - // Analyzer - registerBuildingVariant(46, MetaComparatorBuilding); - registerBuildingVariant(43, MetaAnalyzerBuilding); - - // Reader - registerBuildingVariant(49, MetaReaderBuilding); - - // Item producer - registerBuildingVariant(61, MetaItemProducerBuilding); - - // Constant producer - registerBuildingVariant(62, MetaConstantProducerBuilding); - - // Goal acceptor - registerBuildingVariant(63, MetaGoalAcceptorBuilding); - - // Block - registerBuildingVariant(64, MetaBlockBuilding); - - // Propagate instances - for (const key in gBuildingVariants) { - gBuildingVariants[key].metaInstance = gMetaBuildingRegistry.findByClass( - gBuildingVariants[key].metaClass +/** + * + * @param {typeof MetaBuilding} metaBuilding + */ +export function registerBuildingVariants(metaBuilding) { + gMetaBuildingRegistry.register(metaBuilding); + const combinations = metaBuilding.getAllVariantCombinations(); + combinations.forEach(combination => { + registerBuildingVariant( + combination.internalId, + metaBuilding, + combination.variant || defaultBuildingVariant, + combination.rotationVariant || 0 ); - } - - for (const key in gBuildingVariants) { - const variant = gBuildingVariants[key]; - assert(variant.metaClass, "Variant has no meta: " + key); + }); +} - if (typeof variant.rotationVariant === "undefined") { - variant.rotationVariant = 0; - } - if (typeof variant.variant === "undefined") { - variant.variant = defaultBuildingVariant; - } - } +export function initMetaBuildingRegistry() { + const buildings = [ + MetaBalancerBuilding, + MetaMinerBuilding, + MetaCutterBuilding, + MetaRotaterBuilding, + MetaStackerBuilding, + MetaMixerBuilding, + MetaPainterBuilding, + MetaTrashBuilding, + MetaStorageBuilding, + MetaBeltBuilding, + MetaUndergroundBeltBuilding, + MetaGoalAcceptorBuilding, + MetaHubBuilding, + MetaWireBuilding, + MetaConstantSignalBuilding, + MetaLogicGateBuilding, + MetaLeverBuilding, + MetaFilterBuilding, + MetaWireTunnelBuilding, + MetaDisplayBuilding, + MetaVirtualProcessorBuilding, + MetaReaderBuilding, + MetaTransistorBuilding, + MetaAnalyzerBuilding, + MetaComparatorBuilding, + MetaItemProducerBuilding, + MetaConstantProducerBuilding, + MetaBlockBuilding, + ]; + + buildings.forEach(registerBuildingVariants); // Check for valid keycodes if (G_IS_DEV) { @@ -205,18 +92,15 @@ export function initMetaBuildingRegistry() { const id = metaBuilding.getId(); if (!["hub"].includes(id)) { if (!KEYMAPPINGS.buildings[id]) { - assertAlways( - false, + console.error( "Building " + id + " has no keybinding assigned! Add it to key_action_mapper.js" ); } if (!T.buildings[id]) { - assertAlways(false, "Translation for building " + id + " missing!"); - } - - if (!T.buildings[id].default) { - assertAlways(false, "Translation for building " + id + " missing (default variant)!"); + console.error("Translation for building " + id + " missing!"); + } else if (!T.buildings[id].default) { + console.error("Translation for building " + id + " missing (default variant)!"); } } }); diff --git a/src/js/game/modes/puzzle_play.js b/src/js/game/modes/puzzle_play.js index fc9a8f11..19957eee 100644 --- a/src/js/game/modes/puzzle_play.js +++ b/src/js/game/modes/puzzle_play.js @@ -30,7 +30,7 @@ import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings"; import { MetaBlockBuilding } from "../buildings/block"; import { MetaBuilding } from "../meta_building"; import { gMetaBuildingRegistry } from "../../core/global_registries"; -import { HUDPuzzleNextPuzzle } from "../hud/parts/HUDPuzzleNextPuzzle"; +import { HUDPuzzleNextPuzzle } from "../hud/parts/next_puzzle"; const logger = createLogger("puzzle-play"); const copy = require("clipboard-copy"); diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 429c1515..cd14833e 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -38,6 +38,7 @@ import { HUDSandboxController } from "../hud/parts/sandbox_controller"; import { queryParamOptions } from "../../core/query_parameters"; import { MetaBlockBuilding } from "../buildings/block"; import { MetaItemProducerBuilding } from "../buildings/item_producer"; +import { MOD_SIGNALS } from "../../mods/mod_signals"; /** @typedef {{ * shape: string, @@ -68,10 +69,16 @@ const tierGrowth = 2.5; const chinaShapes = G_WEGAME_VERSION || G_CHINA_VERSION; +const upgradesCache = {}; + /** * Generates all upgrades * @returns {Object} */ function generateUpgrades(limitedVersion = false) { + if (upgradesCache[limitedVersion]) { + return upgradesCache[limitedVersion]; + } + const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1; @@ -264,6 +271,8 @@ function generateUpgrades(limitedVersion = false) { } } + MOD_SIGNALS.modifyUpgrades.dispatch(upgrades); + // VALIDATE if (G_IS_DEV) { for (const upgradeId in upgrades) { @@ -279,14 +288,20 @@ function generateUpgrades(limitedVersion = false) { } } + upgradesCache[limitedVersion] = upgrades; return upgrades; } +const levelDefinitionsCache = {}; + /** * Generates the level definitions * @param {boolean} limitedVersion */ export function generateLevelDefinitions(limitedVersion = false) { + if (levelDefinitionsCache[limitedVersion]) { + return levelDefinitionsCache[limitedVersion]; + } const levelDefinitions = [ // 1 // Circle @@ -511,6 +526,8 @@ export function generateLevelDefinitions(limitedVersion = false) { ]), ]; + MOD_SIGNALS.modifyLevelDefinitions.dispatch(levelDefinitions); + if (G_IS_DEV) { levelDefinitions.forEach(({ shape }) => { try { @@ -521,15 +538,11 @@ export function generateLevelDefinitions(limitedVersion = false) { }); } + levelDefinitionsCache[limitedVersion] = levelDefinitions; + return levelDefinitions; } -const fullVersionUpgrades = generateUpgrades(false); -const demoVersionUpgrades = generateUpgrades(true); - -const fullVersionLevels = generateLevelDefinitions(false); -const demoVersionLevels = generateLevelDefinitions(true); - export class RegularGameMode extends GameMode { static getId() { return enumGameModeIds.regular; @@ -589,7 +602,7 @@ export class RegularGameMode extends GameMode { /** @type {(typeof MetaBuilding)[]} */ this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding]; - // @ts-expect-error + // @ts-ignore if (!(G_IS_DEV || window.sandboxMode || queryParamOptions.sandboxMode)) { this.hiddenBuildings.push(MetaItemProducerBuilding); } @@ -600,9 +613,7 @@ export class RegularGameMode extends GameMode { * @returns {Object} */ getUpgrades() { - return this.root.app.restrictionMgr.getHasExtendedUpgrades() - ? fullVersionUpgrades - : demoVersionUpgrades; + return generateUpgrades(!this.root.app.restrictionMgr.getHasExtendedUpgrades()); } /** @@ -610,9 +621,7 @@ export class RegularGameMode extends GameMode { * @returns {Array} */ getLevelDefinitions() { - return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay() - ? fullVersionLevels - : demoVersionLevels; + return generateLevelDefinitions(!this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay()); } /** diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index b09d73c5..12d9c741 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -7,6 +7,19 @@ import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; import { THEME } from "./theme"; +/** + * @typedef {{ + * context: CanvasRenderingContext2D, + * quadrantSize: number, + * layerScale: number, + * }} SubShapeDrawOptions + */ + +/** + * @type {Object void>} + */ +export const MODS_ADDITIONAL_SUB_SHAPE_DRAWERS = {}; + /** * @typedef {{ * subShape: enumSubShape, @@ -14,6 +27,11 @@ import { THEME } from "./theme"; * }} ShapeLayerItem */ +export const TOP_RIGHT = 0; +export const BOTTOM_RIGHT = 1; +export const BOTTOM_LEFT = 2; +export const TOP_LEFT = 3; + /** * Order is Q1 (tr), Q2(br), Q3(bl), Q4(tl) * @typedef {[ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?, ShapeLayerItem?]} ShapeLayer @@ -51,7 +69,7 @@ for (const key in enumSubShapeToShortcode) { /** * Converts the given parameters to a valid shape definition * @param {*} layers - * @returns {Array} + * @returns {Array} */ export function createSimpleShape(layers) { layers.forEach(layer => { @@ -229,7 +247,7 @@ export class ShapeDefinition extends BasicSerializableObject { * Internal method to clone the shape definition * @returns {Array} */ - internalCloneLayers() { + getClonedLayers() { return JSON.parse(JSON.stringify(this.layers)); } @@ -366,74 +384,79 @@ export class ShapeDefinition extends BasicSerializableObject { context.strokeStyle = THEME.items.outline; context.lineWidth = THEME.items.outlineWidth; - const insetPadding = 0.0; - - switch (subShape) { - case enumSubShape.rect: { - context.beginPath(); - const dims = quadrantSize * layerScale; - context.rect( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize - dims, - dims, - dims - ); - - break; - } - case enumSubShape.star: { - context.beginPath(); - const dims = quadrantSize * layerScale; - - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; - - const moveInwards = dims * 0.4; - context.moveTo(originX, originY + moveInwards); - context.lineTo(originX + dims, originY); - context.lineTo(originX + dims - moveInwards, originY + dims); - context.lineTo(originX, originY + dims); - context.closePath(); - break; - } - - case enumSubShape.windmill: { - context.beginPath(); - const dims = quadrantSize * layerScale; - - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; - const moveInwards = dims * 0.4; - context.moveTo(originX, originY + moveInwards); - context.lineTo(originX + dims, originY); - context.lineTo(originX + dims, originY + dims); - context.lineTo(originX, originY + dims); - context.closePath(); - break; - } - - case enumSubShape.circle: { - context.beginPath(); - context.moveTo(insetPadding + -quadrantHalfSize, -insetPadding + quadrantHalfSize); - context.arc( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize, - quadrantSize * layerScale, - -Math.PI * 0.5, - 0 - ); - context.closePath(); - break; - } - - default: { - assertAlways(false, "Unkown sub shape: " + subShape); + if (MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]) { + MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]({ + context, + layerScale, + quadrantSize, + }); + } else { + switch (subShape) { + case enumSubShape.rect: { + context.beginPath(); + const dims = quadrantSize * layerScale; + context.rect(-quadrantHalfSize, quadrantHalfSize - dims, dims, dims); + context.fill(); + context.stroke(); + break; + } + case enumSubShape.star: { + context.beginPath(); + const dims = quadrantSize * layerScale; + + let originX = -quadrantHalfSize; + let originY = quadrantHalfSize - dims; + + const moveInwards = dims * 0.4; + context.moveTo(originX, originY + moveInwards); + context.lineTo(originX + dims, originY); + context.lineTo(originX + dims - moveInwards, originY + dims); + context.lineTo(originX, originY + dims); + context.closePath(); + context.fill(); + context.stroke(); + break; + } + + case enumSubShape.windmill: { + context.beginPath(); + const dims = quadrantSize * layerScale; + + let originX = -quadrantHalfSize; + let originY = quadrantHalfSize - dims; + const moveInwards = dims * 0.4; + context.moveTo(originX, originY + moveInwards); + context.lineTo(originX + dims, originY); + context.lineTo(originX + dims, originY + dims); + context.lineTo(originX, originY + dims); + context.closePath(); + context.fill(); + context.stroke(); + break; + } + + case enumSubShape.circle: { + context.beginPath(); + context.moveTo(-quadrantHalfSize, quadrantHalfSize); + context.arc( + -quadrantHalfSize, + quadrantHalfSize, + quadrantSize * layerScale, + -Math.PI * 0.5, + 0 + ); + context.closePath(); + context.fill(); + context.stroke(); + break; + } + + default: { + throw new Error("Unkown sub shape: " + subShape); + } } } - context.fill(); - context.stroke(); - context.rotate(-rotation); context.translate(-centerQuadrantX, -centerQuadrantY); } @@ -446,7 +469,7 @@ export class ShapeDefinition extends BasicSerializableObject { * @returns {ShapeDefinition} */ cloneFilteredByQuadrants(includeQuadrants) { - const newLayers = this.internalCloneLayers(); + const newLayers = this.getClonedLayers(); for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { const quadrants = newLayers[layerIndex]; let anyContents = false; @@ -472,7 +495,7 @@ export class ShapeDefinition extends BasicSerializableObject { * @returns {ShapeDefinition} */ cloneRotateCW() { - const newLayers = this.internalCloneLayers(); + const newLayers = this.getClonedLayers(); for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { const quadrants = newLayers[layerIndex]; quadrants.unshift(quadrants[3]); @@ -486,7 +509,7 @@ export class ShapeDefinition extends BasicSerializableObject { * @returns {ShapeDefinition} */ cloneRotateCCW() { - const newLayers = this.internalCloneLayers(); + const newLayers = this.getClonedLayers(); for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { const quadrants = newLayers[layerIndex]; quadrants.push(quadrants[0]); @@ -500,7 +523,7 @@ export class ShapeDefinition extends BasicSerializableObject { * @returns {ShapeDefinition} */ cloneRotate180() { - const newLayers = this.internalCloneLayers(); + const newLayers = this.getClonedLayers(); for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { const quadrants = newLayers[layerIndex]; quadrants.push(quadrants.shift(), quadrants.shift()); @@ -558,7 +581,7 @@ export class ShapeDefinition extends BasicSerializableObject { // Can't merge at a layer lower than 0 const layerToMergeAt = Math.max(1 - smallestGapBetweenShapes, 0); - const mergedLayers = this.internalCloneLayers(); + const mergedLayers = this.getClonedLayers(); for (let layer = mergedLayers.length; layer < layerToMergeAt + topShapeLayers.length; ++layer) { mergedLayers.push([null, null, null, null]); } @@ -584,7 +607,7 @@ export class ShapeDefinition extends BasicSerializableObject { * @param {enumColors} color */ cloneAndPaintWith(color) { - const newLayers = this.internalCloneLayers(); + const newLayers = this.getClonedLayers(); for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { const quadrants = newLayers[layerIndex]; @@ -603,7 +626,7 @@ export class ShapeDefinition extends BasicSerializableObject { * @param {[enumColors, enumColors, enumColors, enumColors]} colors */ cloneAndPaintWith4Colors(colors) { - const newLayers = this.internalCloneLayers(); + const newLayers = this.getClonedLayers(); for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { const quadrants = newLayers[layerIndex]; diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 00491eff..38ac9b0b 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -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.>} */ @@ -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); diff --git a/src/js/game/systems/belt_underlays.js b/src/js/game/systems/belt_underlays.js index c5c69d26..ddbe051a 100644 --- a/src/js/game/systems/belt_underlays.js +++ b/src/js/game/systems/belt_underlays.js @@ -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 = []; @@ -113,12 +113,10 @@ export class BeltUnderlaysSystem extends GameSystemWithFilter { continue; } - // Step 2: Check if any of the directions matches - for (let j = 0; j < slot.directions.length; ++j) { - const slotDirection = staticComp.localDirectionToWorld(slot.directions[j]); - if (slotDirection === fromDirection) { - return true; - } + // Step 2: Check if the direction matches + const slotDirection = staticComp.localDirectionToWorld(slot.direction); + if (slotDirection === fromDirection) { + return true; } } } diff --git a/src/js/game/systems/constant_producer.js b/src/js/game/systems/constant_producer.js index 5c10b409..a95efdb0 100644 --- a/src/js/game/systems/constant_producer.js +++ b/src/js/game/systems/constant_producer.js @@ -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]); } diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js index 29079825..75a4dbdd 100644 --- a/src/js/game/systems/constant_signal.js +++ b/src/js/game/systems/constant_signal.js @@ -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; - } } diff --git a/src/js/game/systems/display.js b/src/js/game/systems/display.js index f11091b9..65cb3a5c 100644 --- a/src/js/game/systems/display.js +++ b/src/js/game/systems/display.js @@ -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} */ this.displaySprites = {}; diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js index 60d4a984..2ffc3b52 100644 --- a/src/js/game/systems/goal_acceptor.js +++ b/src/js/game/systems/goal_acceptor.js @@ -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]); diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index db37455a..8c7468ad 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -4,7 +4,6 @@ import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; import { StaleAreaDetector } from "../../core/stale_area_detector"; import { enumDirection, enumDirectionToVector } from "../../core/vector"; -import { ACHIEVEMENTS } from "../../platform/achievement_provider"; import { BaseItem } from "../base_item"; import { BeltComponent } from "../components/belt"; import { ItemAcceptorComponent } from "../components/item_acceptor"; @@ -204,11 +203,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { if (this.tryPassOverItem(item, destEntity, destSlot.index)) { // Handover successful, clear slot if (!this.root.app.settings.getAllSettings().simplifiedBelts) { - targetAcceptorComp.onItemAccepted( - destSlot.index, - destSlot.acceptedDirection, - item - ); + targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.slot.direction, item); } sourceSlot.item = null; continue; diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 525c242c..6e1032c9 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -8,7 +8,7 @@ import { } from "../components/item_processor"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; -import { BOOL_TRUE_SINGLETON, isTruthyItem } from "../items/boolean_item"; +import { isTruthyItem } from "../items/boolean_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ShapeItem } from "../items/shape_item"; @@ -38,6 +38,11 @@ const MAX_QUEUED_CHARGES = 2; * }} ProcessorImplementationPayload */ +/** + * @type {Object void>} + */ +export const MOD_ITEM_PROCESSOR_HANDLERS = {}; + export class ItemProcessorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemProcessorComponent]); @@ -61,6 +66,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { [enumItemProcessorTypes.hub]: this.process_HUB, [enumItemProcessorTypes.reader]: this.process_READER, [enumItemProcessorTypes.goal]: this.process_GOAL, + ...MOD_ITEM_PROCESSOR_HANDLERS, }; // Bind all handlers @@ -88,49 +94,16 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } - // Check if it finished - if (currentCharge.remainingTime <= 0.0) { + // Check if it finished and we don't already have queued ejects + if (currentCharge.remainingTime <= 0.0 && !processorComp.queuedEjects.length) { const itemsToEject = currentCharge.items; - // Go over all items and try to eject them + // Go over all items and add them to the queue for (let j = 0; j < itemsToEject.length; ++j) { - const { item, requiredSlot, preferredSlot } = itemsToEject[j]; - - assert(ejectorComp, "To eject items, the building needs to have an ejector"); - - let slot = null; - if (requiredSlot !== null && requiredSlot !== undefined) { - // We have a slot override, check if that is free - if (ejectorComp.canEjectOnSlot(requiredSlot)) { - slot = requiredSlot; - } - } else if (preferredSlot !== null && preferredSlot !== undefined) { - // We have a slot preference, try using it but otherwise use a free slot - if (ejectorComp.canEjectOnSlot(preferredSlot)) { - slot = preferredSlot; - } else { - slot = ejectorComp.getFirstFreeSlot(); - } - } else { - // We can eject on any slot - slot = ejectorComp.getFirstFreeSlot(); - } - - if (slot !== null) { - // Alright, we can actually eject - if (!ejectorComp.tryEject(slot, item)) { - assert(false, "Failed to eject"); - } else { - itemsToEject.splice(j, 1); - j -= 1; - } - } + processorComp.queuedEjects.push(itemsToEject[j]); } - // If the charge was entirely emptied to the outputs, start the next charge - if (itemsToEject.length === 0) { - processorComp.ongoingCharges.shift(); - } + processorComp.ongoingCharges.shift(); } } @@ -140,6 +113,40 @@ export class ItemProcessorSystem extends GameSystemWithFilter { this.startNewCharge(entity); } } + + for (let j = 0; j < processorComp.queuedEjects.length; ++j) { + const { item, requiredSlot, preferredSlot } = processorComp.queuedEjects[j]; + + assert(ejectorComp, "To eject items, the building needs to have an ejector"); + + let slot = null; + if (requiredSlot !== null && requiredSlot !== undefined) { + // We have a slot override, check if that is free + if (ejectorComp.canEjectOnSlot(requiredSlot)) { + slot = requiredSlot; + } + } else if (preferredSlot !== null && preferredSlot !== undefined) { + // We have a slot preference, try using it but otherwise use a free slot + if (ejectorComp.canEjectOnSlot(preferredSlot)) { + slot = preferredSlot; + } else { + slot = ejectorComp.getFirstFreeSlot(); + } + } else { + // We can eject on any slot + slot = ejectorComp.getFirstFreeSlot(); + } + + if (slot !== null) { + // Alright, we can actually eject + if (!ejectorComp.tryEject(slot, item)) { + assert(false, "Failed to eject"); + } else { + processorComp.queuedEjects.splice(j, 1); + j -= 1; + } + } + } } } diff --git a/src/js/game/systems/item_producer.js b/src/js/game/systems/item_producer.js index 0a385907..8ca29ae1 100644 --- a/src/js/game/systems/item_producer.js +++ b/src/js/game/systems/item_producer.js @@ -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; diff --git a/src/js/game/systems/lever.js b/src/js/game/systems/lever.js index 75b6cf28..343894ae 100644 --- a/src/js/game/systems/lever.js +++ b/src/js/game/systems/lever.js @@ -1,9 +1,8 @@ -import { GameSystemWithFilter } from "../game_system_with_filter"; +import { Loader } from "../../core/loader"; import { LeverComponent } from "../components/lever"; -import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; import { MapChunkView } from "../map_chunk_view"; -import { globalConfig } from "../../core/config"; -import { Loader } from "../../core/loader"; export class LeverSystem extends GameSystemWithFilter { constructor(root) { diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index 80affac9..20204a89 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -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 { @@ -50,8 +50,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/game/systems/wire.js b/src/js/game/systems/wire.js index 0491def6..4a255866 100644 --- a/src/js/game/systems/wire.js +++ b/src/js/game/systems/wire.js @@ -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>} diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index fec42e28..786cfda2 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -1,5 +1,4 @@ { - "uiStyle": "dark", "map": { "background": "#3e3f47", "grid": "rgba(255, 255, 255, 0.02)", @@ -19,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)" } }, diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index d44a15ab..1236d43d 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -1,5 +1,4 @@ { - "uiStyle": "light", "map": { "background": "#fff", "grid": "#fafafa", @@ -19,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)" } }, diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index bf870fab..f18d1d61 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -22,6 +22,10 @@ declare const G_IS_RELEASE: boolean; declare const G_CHINA_VERSION: boolean; declare const G_WEGAME_VERSION: boolean; +declare const shapez: any; + +declare const ipcRenderer: any; + // Polyfills declare interface String { replaceAll(search: string, replacement: string): string; @@ -92,9 +96,11 @@ declare interface Window { cpmstarAPI: any; // Mods - registerMod: any; + $shapez_registerMod: any; anyModLoaded: any; + shapez: any; + webkitRequestAnimationFrame(); assert(condition: boolean, failureMessage: string); diff --git a/src/js/main.js b/src/js/main.js index 94f3d37a..0f80f527 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -2,6 +2,8 @@ import "./core/polyfills"; import "./core/assert"; import "./core/error_handler"; +import "./mods/modloader"; + import { createLogger, logSection } from "./core/logging"; import { Application } from "./application"; import { IS_DEBUG } from "./core/config"; @@ -19,39 +21,8 @@ if (window.coreThreadLoadedCb) { window.coreThreadLoadedCb(); } -// Logrocket -// if (!G_IS_DEV && !G_IS_STANDALONE) { -// const monthlyUsers = 300; // thousand -// const logrocketLimit = 10; // thousand -// const percentageOfUsers = logrocketLimit / monthlyUsers; - -// if (Math.random() <= percentageOfUsers) { -// logger.log("Analyzing this session with logrocket"); -// const logrocket = require("logrocket"); -// logrocket.init("p1x9zh/shapezio"); - -// try { -// logrocket.getSessionURL(function (sessionURL) { -// logger.log("Connected lockrocket to GA"); -// // @ts-ignore -// try { -// window.ga("send", { -// hitType: "event", -// eventCategory: "LogRocket", -// eventAction: sessionURL, -// }); -// } catch (ex) { -// logger.warn("Logrocket connection to analytics failed:", ex); -// } -// }); -// } catch (ex) { -// logger.warn("Logrocket connection to analytics failed:", ex); -// } -// } -// } - console.log( - `%cshapez.io ️%c\n© 2020 Tobias Springer IT Solutions\nCommit %c${G_BUILD_COMMIT_HASH}%c on %c${new Date( + `%cshapez.io ️%c\n© 2022 tobspr Games\nCommit %c${G_BUILD_COMMIT_HASH}%c on %c${new Date( G_BUILD_TIME ).toLocaleString()}\n`, "font-size: 35px; font-family: Arial;font-weight: bold; padding: 10px 0;", diff --git a/src/js/mods/mod.js b/src/js/mods/mod.js new file mode 100644 index 00000000..cea152d8 --- /dev/null +++ b/src/js/mods/mod.js @@ -0,0 +1,36 @@ +/* typehints:start */ +import { Application } from "../application"; +import { ModLoader } from "./modloader"; +/* typehints:end */ + +import { MOD_SIGNALS } from "./mod_signals"; + +export class Mod { + /** + * @param {object} param0 + * @param {Application} param0.app + * @param {ModLoader} param0.modLoader + * @param {import("./modloader").ModMetadata} param0.meta + * @param {Object} param0.settings + * @param {() => Promise} param0.saveSettings + */ + constructor({ app, modLoader, meta, settings, saveSettings }) { + this.app = app; + this.modLoader = modLoader; + this.metadata = meta; + + this.signals = MOD_SIGNALS; + this.modInterface = modLoader.modInterface; + + this.settings = settings; + this.saveSettings = saveSettings; + } + + init() { + // to be overridden + } + + get dialogs() { + return this.modInterface.dialogs; + } +} diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js new file mode 100644 index 00000000..ff96d592 --- /dev/null +++ b/src/js/mods/mod_interface.js @@ -0,0 +1,658 @@ +/* typehints:start */ +import { ModLoader } from "./modloader"; +import { GameSystem } from "../game/game_system"; +import { Component } from "../game/component"; +import { MetaBuilding } from "../game/meta_building"; +/* typehints:end */ + +import { defaultBuildingVariant } from "../game/meta_building"; +import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; +import { + enumShortcodeToSubShape, + enumSubShape, + enumSubShapeToShortcode, + MODS_ADDITIONAL_SUB_SHAPE_DRAWERS, +} from "../game/shape_definition"; +import { Loader } from "../core/loader"; +import { LANGUAGES } from "../languages"; +import { matchDataRecursive, T } from "../translations"; +import { gBuildingVariants, registerBuildingVariant } from "../game/building_codes"; +import { gComponentRegistry, gMetaBuildingRegistry } from "../core/global_registries"; +import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../game/map_chunk"; +import { MODS_ADDITIONAL_SYSTEMS } from "../game/game_system_manager"; +import { MOD_CHUNK_DRAW_HOOKS } from "../game/map_chunk_view"; +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"; +import { Vector } from "../core/vector"; +import { GameRoot } from "../game/root"; + +/** + * @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 + * @typedef {(...args: [P, Parameters]) => ReturnType} beforePrams IMPORTANT: this puts the original parameters into an array + */ + +/** + * @template {(...args: any[]) => any} F + * @template P + * @typedef {(...args: [...Parameters, P]) => ReturnType} afterPrams + */ + +/** + * @template {(...args: any[]) => any} F + * @typedef {(...args: [...Parameters, ...any]) => ReturnType} extendsPrams + */ + +export class ModInterface { + /** + * + * @param {ModLoader} modLoader + */ + constructor(modLoader) { + this.modLoader = modLoader; + } + + registerCss(cssString) { + // Preprocess css + cssString = cssString.replace(/\$scaled\(([^)]*)\)/gim, (substr, expression) => { + return "calc((" + expression + ") * var(--ui-scale))"; + }); + const element = document.createElement("style"); + element.textContent = cssString; + document.head.appendChild(element); + } + + registerSprite(spriteId, base64string) { + assert(base64string.startsWith("data:image")); + const img = new Image(); + + const sprite = new AtlasSprite(spriteId); + sprite.frozen = true; + + img.addEventListener("load", () => { + for (const resolution in sprite.linksByResolution) { + const link = sprite.linksByResolution[resolution]; + link.w = img.width; + link.h = img.height; + link.packedW = img.width; + link.packedH = img.height; + } + }); + + img.src = base64string; + + const link = new SpriteAtlasLink({ + w: 1, + h: 1, + atlas: img, + packOffsetX: 0, + packOffsetY: 0, + packedW: 1, + packedH: 1, + packedX: 0, + packedY: 0, + }); + + sprite.linksByResolution["0.25"] = link; + sprite.linksByResolution["0.5"] = link; + sprite.linksByResolution["0.75"] = link; + + Loader.sprites.set(spriteId, sprite); + } + + /** + * + * @param {string} imageBase64 + * @param {string} jsonTextData + */ + registerAtlas(imageBase64, jsonTextData) { + const atlasData = JSON.parse(jsonTextData); + const img = new Image(); + img.src = imageBase64; + + const sourceData = atlasData.frames; + for (const spriteName in sourceData) { + const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName]; + + const sprite = new AtlasSprite(spriteName); + Loader.sprites.set(spriteName, sprite); + sprite.frozen = true; + + const link = new SpriteAtlasLink({ + packedX: frame.x, + packedY: frame.y, + packedW: frame.w, + packedH: frame.h, + packOffsetX: spriteSourceSize.x, + packOffsetY: spriteSourceSize.y, + atlas: img, + w: sourceSize.w, + h: sourceSize.h, + }); + sprite.linksByResolution["0.25"] = link; + sprite.linksByResolution["0.5"] = link; + sprite.linksByResolution["0.75"] = link; + } + } + + /** + * + * @param {object} param0 + * @param {string} param0.id + * @param {string} param0.shortCode + * @param {(distanceToOriginInChunks: number) => number} param0.weightComputation + * @param {(options: import("../game/shape_definition").SubShapeDrawOptions) => void} param0.draw + */ + registerSubShapeType({ id, shortCode, weightComputation, draw }) { + if (shortCode.length !== 1) { + throw new Error("Bad short code: " + shortCode); + } + enumSubShape[id] = id; + enumSubShapeToShortcode[id] = shortCode; + enumShortcodeToSubShape[shortCode] = id; + + MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[id] = weightComputation; + MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[id] = draw; + } + + registerTranslations(language, translations) { + const data = LANGUAGES[language]; + if (!data) { + throw new Error("Unknown language: " + language); + } + + matchDataRecursive(data.data, translations, true); + if (language === "en") { + matchDataRecursive(T, translations, true); + } + } + + /** + * + * @param {typeof Component} component + */ + registerComponent(component) { + gComponentRegistry.register(component); + } + + /** + * + * @param {Object} param0 + * @param {string} param0.id + * @param {new (any) => GameSystem} param0.systemClass + * @param {string=} param0.before + * @param {string[]=} param0.drawHooks + */ + registerGameSystem({ id, systemClass, before, drawHooks }) { + const key = before || "key"; + const payload = { id, systemClass }; + + if (MODS_ADDITIONAL_SYSTEMS[key]) { + MODS_ADDITIONAL_SYSTEMS[key].push(payload); + } else { + MODS_ADDITIONAL_SYSTEMS[key] = [payload]; + } + if (drawHooks) { + drawHooks.forEach(hookId => this.registerGameSystemDrawHook(hookId, id)); + } + } + + /** + * + * @param {string} hookId + * @param {string} systemId + */ + registerGameSystemDrawHook(hookId, systemId) { + if (!MOD_CHUNK_DRAW_HOOKS[hookId]) { + throw new Error("bad game system draw hook: " + hookId); + } + MOD_CHUNK_DRAW_HOOKS[hookId].push(systemId); + } + + /** + * + * @param {object} param0 + * @param {typeof ModMetaBuilding} param0.metaClass + * @param {string=} param0.buildingIconBase64 + */ + registerNewBuilding({ metaClass, buildingIconBase64 }) { + const id = new /** @type {new (...args) => ModMetaBuilding} */ (metaClass)().getId(); + if (gMetaBuildingRegistry.hasId(id)) { + throw new Error("Tried to register building twice: " + id); + } + gMetaBuildingRegistry.register(metaClass); + const metaInstance = gMetaBuildingRegistry.findByClass(metaClass); + T.buildings[id] = {}; + + metaClass.getAllVariantCombinations().forEach(combination => { + const variant = combination.variant || defaultBuildingVariant; + const rotationVariant = combination.rotationVariant || 0; + + const buildingIdentifier = id + (variant === defaultBuildingVariant ? "" : "-" + variant); + + const uniqueTypeId = buildingIdentifier + (rotationVariant === 0 ? "" : "-" + rotationVariant); + registerBuildingVariant(uniqueTypeId, metaClass, variant, rotationVariant); + + gBuildingVariants[id].metaInstance = metaInstance; + + this.registerTranslations("en", { + buildings: { + [id]: { + [variant]: { + name: combination.name || "Name", + description: combination.description || "Description", + }, + }, + }, + }); + + if (combination.regularImageBase64) { + this.registerSprite( + "sprites/buildings/" + buildingIdentifier + ".png", + combination.regularImageBase64 + ); + } + + if (combination.blueprintImageBase64) { + this.registerSprite( + "sprites/blueprints/" + buildingIdentifier + ".png", + combination.blueprintImageBase64 + ); + } + if (combination.tutorialImageBase64) { + this.setBuildingTutorialImage(id, variant, combination.tutorialImageBase64); + } + }); + + if (buildingIconBase64) { + this.setBuildingToolbarIcon(id, buildingIconBase64); + } + } + + /** + * + * @param {Object} param0 + * @param {string} param0.id + * @param {number} param0.keyCode + * @param {string} param0.translation + * @param {boolean=} param0.repeated + * @param {((GameRoot) => void)=} param0.handler + * @param {{shift?: boolean; alt?: boolean; ctrl?: boolean}=} param0.modifiers + * @param {boolean=} param0.builtin + */ + registerIngameKeybinding({ + id, + keyCode, + translation, + modifiers = {}, + repeated = false, + builtin = false, + handler = null, + }) { + if (!KEYMAPPINGS.mods) { + KEYMAPPINGS.mods = {}; + } + const binding = (KEYMAPPINGS.mods[id] = { + keyCode, + id, + repeated, + modifiers, + builtin, + }); + this.registerTranslations("en", { + keybindings: { + mappings: { + [id]: translation, + }, + }, + }); + + if (handler) { + this.modLoader.signals.gameStarted.add(root => { + root.keyMapper.getBindingById(id).addToTop(handler.bind(null, root)); + }); + } + + return binding; + } + + /** + * @returns {HUDModalDialogs} + */ + get dialogs() { + const state = this.modLoader.app.stateMgr.currentState; + // @ts-ignore + if (state.dialogs) { + // @ts-ignore + return state.dialogs; + } + throw new Error("Tried to access dialogs but current state doesn't support it"); + } + + setBuildingToolbarIcon(buildingId, iconBase64) { + this.registerCss(` + [data-icon="building_icons/${buildingId}.png"] .icon { + background-image: url('${iconBase64}') !important; + } + `); + } + + /** + * + * @param {string | (new () => MetaBuilding)} buildingIdOrClass + * @param {*} variant + * @param {*} imageBase64 + */ + setBuildingTutorialImage(buildingIdOrClass, variant, imageBase64) { + if (typeof buildingIdOrClass === "function") { + buildingIdOrClass = new buildingIdOrClass().id; + } + const buildingIdentifier = + buildingIdOrClass + (variant === defaultBuildingVariant ? "" : "-" + variant); + + this.registerCss(` + [data-icon="building_tutorials/${buildingIdentifier}.png"] { + background-image: url('${imageBase64}') !important; + } + `); + } + + /** + * @param {Object} param0 + * @param {string} param0.id + * @param {string} param0.name + * @param {Object} param0.theme + */ + registerGameTheme({ id, name, theme }) { + THEMES[id] = theme; + this.registerTranslations("en", { + settings: { + labels: { + theme: { + themes: { + [id]: name, + }, + }, + }, + }, + }); + } + + /** + * Registers a new state class, should be a GameState derived class + * @param {typeof import("../core/game_state").GameState} stateClass + */ + registerGameState(stateClass) { + this.modLoader.app.stateMgr.register(stateClass); + } + + /** + * @param {object} param0 + * @param {"regular"|"wires"} param0.toolbar + * @param {"primary"|"secondary"} param0.location + * @param {typeof MetaBuilding} param0.metaClass + */ + addNewBuildingToToolbar({ toolbar, location, metaClass }) { + const hudElementName = toolbar === "wires" ? "HUDWiresToolbar" : "HUDBuildingsToolbar"; + const property = location === "secondary" ? "secondaryBuildings" : "primaryBuildings"; + + this.modLoader.signals.hudElementInitialized.add(element => { + if (element.constructor.name === hudElementName) { + element[property].push(metaClass); + } + }); + } + + /** + * 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} O the method that will override the old one + * @param {C} classHandle + * @param {M} methodName + * @param {bindThis, InstanceType>} 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 + 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} O the method that will run before the old one + * @param {C} classHandle + * @param {M} methodName + * @param {bindThis>} 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} O the method that will run before the old one + * @param {C} classHandle + * @param {M} methodName + * @param {bindThis>} 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; + }; + } + + /** + * + * @param {Object} prototype + * @param {({ $super, $old }) => any} extender + */ + extendObject(prototype, extender) { + const $super = Object.getPrototypeOf(prototype); + const $old = {}; + const extensionMethods = extender({ $super, $old }); + const properties = Array.from(Object.getOwnPropertyNames(extensionMethods)); + properties.forEach(propertyName => { + if (["constructor", "prototype"].includes(propertyName)) { + return; + } + $old[propertyName] = prototype[propertyName]; + prototype[propertyName] = extensionMethods[propertyName]; + }); + } + + /** + * + * @param {Class} classHandle + * @param {({ $super, $old }) => any} extender + */ + 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); + }); + } + + /** + * + * @param {string | (new () => MetaBuilding)} buildingIdOrClass + * @param {string} variant + * @param {object} param0 + * @param {string} param0.name + * @param {string} param0.description + * @param {string=} param0.language + */ + registerBuildingTranslation(buildingIdOrClass, variant, { name, description, language = "en" }) { + if (typeof buildingIdOrClass === "function") { + buildingIdOrClass = new buildingIdOrClass().id; + } + this.registerTranslations(language, { + buildings: { + [buildingIdOrClass]: { + [variant]: { + name, + description, + }, + }, + }, + }); + } + + /** + * + * @param {string | (new () => MetaBuilding)} buildingIdOrClass + * @param {string} variant + * @param {object} param2 + * @param {string=} param2.regularBase64 + * @param {string=} param2.blueprintBase64 + */ + registerBuildingSprites(buildingIdOrClass, variant, { regularBase64, blueprintBase64 }) { + if (typeof buildingIdOrClass === "function") { + buildingIdOrClass = new buildingIdOrClass().id; + } + + const spriteId = + buildingIdOrClass + (variant === defaultBuildingVariant ? "" : "-" + variant) + ".png"; + + if (regularBase64) { + this.registerSprite("sprites/buildings/" + spriteId, regularBase64); + } + + if (blueprintBase64) { + this.registerSprite("sprites/blueprints/" + spriteId, blueprintBase64); + } + } + + /** + * @param {new () => MetaBuilding} metaClass + * @param {string} variant + * @param {object} payload + * @param {number[]=} payload.rotationVariants + * @param {string=} payload.tutorialImageBase64 + * @param {string=} payload.regularSpriteBase64 + * @param {string=} payload.blueprintSpriteBase64 + * @param {string=} payload.name + * @param {string=} payload.description + * @param {Vector=} payload.dimensions + * @param {(root: GameRoot) => [string, string][]} payload.additionalStatistics + * @param {(root: GameRoot) => boolean[]} payload.isUnlocked + */ + addVariantToExistingBuilding(metaClass, variant, payload) { + if (!payload.rotationVariants) { + payload.rotationVariants = [0]; + } + + if (payload.tutorialImageBase64) { + this.setBuildingTutorialImage(metaClass, variant, payload.tutorialImageBase64); + } + if (payload.regularSpriteBase64) { + this.registerBuildingSprites(metaClass, variant, { regularBase64: payload.regularSpriteBase64 }); + } + if (payload.blueprintSpriteBase64) { + this.registerBuildingSprites(metaClass, variant, { + blueprintBase64: payload.blueprintSpriteBase64, + }); + } + if (payload.name && payload.description) { + this.registerBuildingTranslation(metaClass, variant, { + name: payload.name, + description: payload.description, + }); + } + + const internalId = new metaClass().getId() + "-" + variant; + + // Extend static methods + this.extendObject(metaClass, ({ $old }) => ({ + getAllVariantCombinations() { + return [ + ...$old.bind(this).getAllVariantCombinations(), + ...payload.rotationVariants.map(rotationVariant => ({ + internalId, + variant, + rotationVariant, + })), + ]; + }, + })); + + // Dimensions + const $variant = variant; + if (payload.dimensions) { + this.extendClass(metaClass, ({ $old }) => ({ + getDimensions(variant) { + if (variant === $variant) { + return payload.dimensions; + } + return $old.getDimensions.bind(this)(...arguments); + }, + })); + } + + if (payload.additionalStatistics) { + this.extendClass(metaClass, ({ $old }) => ({ + getAdditionalStatistics(root, variant) { + if (variant === $variant) { + return payload.additionalStatistics(root); + } + return $old.getAdditionalStatistics.bind(this)(root, variant); + }, + })); + } + + if (payload.isUnlocked) { + this.extendClass(metaClass, ({ $old }) => ({ + getAvailableVariants(root) { + if (payload.isUnlocked(root)) { + return [...$old.getAvailableVariants.bind(this)(root), $variant]; + } + return $old.getAvailableVariants.bind(this)(root); + }, + })); + } + + // Register our variant finally + payload.rotationVariants.forEach(rotationVariant => + shapez.registerBuildingVariant(internalId, metaClass, variant, rotationVariant) + ); + } +} diff --git a/src/js/mods/mod_meta_building.js b/src/js/mods/mod_meta_building.js new file mode 100644 index 00000000..0d8f215a --- /dev/null +++ b/src/js/mods/mod_meta_building.js @@ -0,0 +1,18 @@ +import { MetaBuilding } from "../game/meta_building"; + +export class ModMetaBuilding extends MetaBuilding { + /** + * @returns {({ + * variant: string; + * rotationVariant?: number; + * name: string; + * description: string; + * blueprintImageBase64?: string; + * regularImageBase64?: string; + * tutorialImageBase64?: string; + * }[])} + */ + static getAllVariantCombinations() { + throw new Error("Implement getAllVariantCombinations"); + } +} diff --git a/src/js/mods/mod_signals.js b/src/js/mods/mod_signals.js new file mode 100644 index 00000000..a534dd89 --- /dev/null +++ b/src/js/mods/mod_signals.js @@ -0,0 +1,33 @@ +/* typehints:start */ +import { BaseHUDPart } from "../game/hud/base_hud_part"; +import { GameRoot } from "../game/root"; +import { GameState } from "../core/game_state"; +import { InGameState } from "../states/ingame"; +/* typehints:end */ + +import { Signal } from "../core/signal"; + +// Single file to avoid circular deps + +export const MOD_SIGNALS = { + // Called when the application has booted and instances like the app settings etc are available + appBooted: new Signal(), + + modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()), + modifyUpgrades: /** @type {TypedSignal<[Object]>} */ (new Signal()), + + 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()), + + gameStarted: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), + + stateEntered: /** @type {TypedSignal<[GameState]>} */ (new Signal()), + + gameSerialized: /** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (new Signal()), + gameDeserialized: /** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (new Signal()), +}; diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js new file mode 100644 index 00000000..356b7d6b --- /dev/null +++ b/src/js/mods/modloader.js @@ -0,0 +1,270 @@ +/* typehints:start */ +import { Application } from "../application"; +/* typehints:end */ +import { globalConfig } from "../core/config"; +import { createLogger } from "../core/logging"; +import { StorageImplBrowserIndexedDB } from "../platform/browser/storage_indexed_db"; +import { StorageImplElectron } from "../platform/electron/storage"; +import { FILE_NOT_FOUND } from "../platform/storage"; +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"); + +/** + * @typedef {{ + * name: string; + * version: string; + * author: string; + * website: string; + * description: string; + * id: string; + * minimumGameVersion?: string; + * settings: []; + * doesNotAffectSavegame?: boolean + * }} ModMetadata + */ + +export class ModLoader { + constructor() { + LOG.log("modloader created"); + + /** + * @type {Application} + */ + this.app = undefined; + + /** @type {Mod[]} */ + this.mods = []; + + this.modInterface = new ModInterface(this); + + /** @type {({ meta: ModMetadata, modClass: typeof Mod})[]} */ + this.modLoadQueue = []; + + this.initialized = false; + + this.signals = MOD_SIGNALS; + } + + linkApp(app) { + this.app = app; + } + + anyModsActive() { + return this.mods.length > 0; + } + + /** + * + * @returns {import("../savegame/savegame_typedefs").SavegameStoredMods} + */ + getModsListForSavegame() { + return this.mods + .filter(mod => !mod.metadata.doesNotAffectSavegame) + .map(mod => ({ + id: mod.metadata.id, + version: mod.metadata.version, + website: mod.metadata.website, + name: mod.metadata.name, + author: mod.metadata.author, + })); + } + + /** + * + * @param {import("../savegame/savegame_typedefs").SavegameStoredMods} originalMods + */ + computeModDifference(originalMods) { + /** + * @type {import("../savegame/savegame_typedefs").SavegameStoredMods} + */ + let missing = []; + + const current = this.getModsListForSavegame(); + + originalMods.forEach(mod => { + for (let i = 0; i < current.length; ++i) { + const currentMod = current[i]; + if (currentMod.id === mod.id && currentMod.version === mod.version) { + current.splice(i, 1); + return; + } + } + missing.push(mod); + }); + + return { + missing, + extra: current, + }; + } + + exposeExports() { + if (G_IS_DEV || G_IS_STANDALONE) { + let exports = {}; + const modules = require.context("../", true, /\.js$/); + Array.from(modules.keys()).forEach(key => { + // @ts-ignore + const module = modules(key); + for (const member in module) { + if (member === "default" || member === "$s") { + // Setter + continue; + } + if (exports[member]) { + throw new Error("Duplicate export of " + member); + } + + Object.defineProperty(exports, member, { + get() { + return module[member]; + }, + set(v) { + module["$s"](member, v); + }, + }); + } + }); + + window.shapez = exports; + } + } + + async initMods() { + if (!G_IS_STANDALONE && !G_IS_DEV) { + this.initialized = true; + return; + } + + // Create a storage for reading mod settings + const storage = G_IS_STANDALONE + ? new StorageImplElectron(this.app) + : new StorageImplBrowserIndexedDB(this.app); + await storage.initialize(); + + LOG.log("hook:init", this.app, this.app.storage); + this.exposeExports(); + + let mods = []; + if (G_IS_STANDALONE) { + mods = await ipcRenderer.invoke("get-mods"); + } + if (G_IS_DEV && globalConfig.debug.externalModUrl) { + 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 + ); + } + mods.push(await response.text()); + } + } + + window.$shapez_registerMod = (modClass, meta) => { + if (this.initialized) { + throw new Error("Can't register mod after modloader is initialized"); + } + if (this.modLoadQueue.some(entry => entry.meta.id === meta.id)) { + console.warn("Not registering mod", meta, "since a mod with the same id is already loaded"); + return; + } + this.modLoadQueue.push({ + modClass, + meta, + }); + }; + + mods.forEach(modCode => { + modCode += ` + if (typeof Mod !== 'undefined') { + if (typeof METADATA !== 'object') { + throw new Error("No METADATA variable found"); + } + window.$shapez_registerMod(Mod, METADATA); + } + `; + try { + const func = new Function(modCode); + func(); + } catch (ex) { + console.error(ex); + alert("Failed to parse mod (launch with --dev for more info): \n\n" + ex); + } + }); + + delete window.$shapez_registerMod; + + for (let i = 0; i < this.modLoadQueue.length; i++) { + const { modClass, meta } = this.modLoadQueue[i]; + const modDataFile = "modsettings_" + meta.id + "__" + meta.version + ".json"; + + if (meta.minimumGameVersion) { + 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) { + try { + const storedSettings = await storage.readFileAsync(modDataFile); + settings = JSON.parse(storedSettings); + } catch (ex) { + if (ex === FILE_NOT_FOUND) { + // Write default data + await storage.writeFileAsync(modDataFile, JSON.stringify(meta.settings)); + } else { + alert("Failed to load settings for " + meta.id + ", will use defaults:\n\n" + ex); + } + } + } + + try { + const mod = new modClass({ + app: this.app, + modLoader: this, + meta, + settings, + saveSettings: () => storage.writeFileAsync(modDataFile, JSON.stringify(mod.settings)), + }); + mod.init(); + this.mods.push(mod); + } catch (ex) { + console.error(ex); + alert("Failed to initialize mods (launch with --dev for more info): \n\n" + ex); + } + } + + this.modLoadQueue = []; + this.initialized = true; + } +} + +export const MODS = new ModLoader(); diff --git a/src/js/platform/api.js b/src/js/platform/api.js index d518c98a..4e7a82f9 100644 --- a/src/js/platform/api.js +++ b/src/js/platform/api.js @@ -3,7 +3,6 @@ import { Application } from "../application"; /* typehints:end */ import { createLogger } from "../core/logging"; import { compressX64 } from "../core/lzstring"; -import { getIPCRenderer } from "../core/utils"; import { T } from "../translations"; const logger = createLogger("puzzle-api"); @@ -113,9 +112,7 @@ export class ClientAPI { return Promise.resolve({ token }); } - const renderer = getIPCRenderer(); - - return renderer.invoke("steam:get-ticket").then( + return ipcRenderer.invoke("steam:get-ticket").then( ticket => { logger.log("Got auth ticket:", ticket); return this._request("/v1/public/login", { diff --git a/src/js/platform/electron/steam_achievement_provider.js b/src/js/platform/electron/steam_achievement_provider.js index c0ef552c..638cdbc5 100644 --- a/src/js/platform/electron/steam_achievement_provider.js +++ b/src/js/platform/electron/steam_achievement_provider.js @@ -4,7 +4,6 @@ import { GameRoot } from "../../game/root"; /* typehints:end */ import { createLogger } from "../../core/logging"; -import { getIPCRenderer } from "../../core/utils"; import { ACHIEVEMENTS, AchievementCollection, AchievementProviderInterface } from "../achievement_provider"; const logger = createLogger("achievements/steam"); @@ -109,9 +108,7 @@ export class SteamAchievementProvider extends AchievementProviderInterface { return Promise.resolve(); } - this.ipc = getIPCRenderer(); - - return this.ipc.invoke("steam:is-initialized").then(initialized => { + return ipcRenderer.invoke("steam:is-initialized").then(initialized => { this.initialized = initialized; if (!this.initialized) { @@ -136,7 +133,7 @@ export class SteamAchievementProvider extends AchievementProviderInterface { if (!this.initialized) { promise = Promise.resolve(); } else { - promise = this.ipc.invoke("steam:activate-achievement", ACHIEVEMENT_IDS[key]); + promise = ipcRenderer.invoke("steam:activate-achievement", ACHIEVEMENT_IDS[key]); } return promise diff --git a/src/js/platform/electron/storage.js b/src/js/platform/electron/storage.js index 41ed1746..65f0e507 100644 --- a/src/js/platform/electron/storage.js +++ b/src/js/platform/electron/storage.js @@ -1,30 +1,8 @@ -import { StorageInterface } from "../storage"; -import { getIPCRenderer } from "../../core/utils"; -import { createLogger } from "../../core/logging"; - -const logger = createLogger("electron-storage"); +import { FILE_NOT_FOUND, StorageInterface } from "../storage"; export class StorageImplElectron extends StorageInterface { constructor(app) { super(app); - - /** @type {Object.} */ - this.jobs = {}; - this.jobId = 0; - - getIPCRenderer().on("fs-response", (event, arg) => { - const id = arg.id; - if (!this.jobs[id]) { - logger.warn("Got unhandled FS response, job not known:", id); - return; - } - const { resolve, reject } = this.jobs[id]; - if (arg.result.success) { - resolve(arg.result.data); - } else { - reject(arg.result.error); - } - }); } initialize() { @@ -32,44 +10,32 @@ export class StorageImplElectron extends StorageInterface { } writeFileAsync(filename, contents) { - return new Promise((resolve, reject) => { - // ipcMain - const jobId = ++this.jobId; - this.jobs[jobId] = { resolve, reject }; - - getIPCRenderer().send("fs-job", { - type: "write", - filename, - contents, - id: jobId, - }); + return ipcRenderer.invoke("fs-job", { + type: "write", + filename, + contents, }); } readFileAsync(filename) { - return new Promise((resolve, reject) => { - // ipcMain - const jobId = ++this.jobId; - this.jobs[jobId] = { resolve, reject }; - - getIPCRenderer().send("fs-job", { + return ipcRenderer + .invoke("fs-job", { type: "read", filename, - id: jobId, + }) + .then(res => { + if (res && res.error === FILE_NOT_FOUND) { + throw FILE_NOT_FOUND; + } + + return res; }); - }); } deleteFileAsync(filename) { - return new Promise((resolve, reject) => { - // ipcMain - const jobId = ++this.jobId; - this.jobs[jobId] = { resolve, reject }; - getIPCRenderer().send("fs-job", { - type: "delete", - filename, - id: jobId, - }); + return ipcRenderer.invoke("fs-job", { + type: "delete", + filename, }); } } diff --git a/src/js/platform/electron/wrapper.js b/src/js/platform/electron/wrapper.js index c1764f68..65451395 100644 --- a/src/js/platform/electron/wrapper.js +++ b/src/js/platform/electron/wrapper.js @@ -1,6 +1,5 @@ import { NoAchievementProvider } from "../browser/no_achievement_provider"; import { PlatformWrapperImplBrowser } from "../browser/wrapper"; -import { getIPCRenderer } from "../../core/utils"; import { createLogger } from "../../core/logging"; import { StorageImplElectron } from "./storage"; import { SteamAchievementProvider } from "./steam_achievement_provider"; @@ -71,15 +70,13 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { } initializeDlcStatus() { - const renderer = getIPCRenderer(); - if (G_WEGAME_VERSION) { return Promise.resolve(); } logger.log("Checking DLC ownership ..."); // @todo: Don't hardcode the app id - return renderer.invoke("steam:check-app-ownership", 1625400).then( + return ipcRenderer.invoke("steam:check-app-ownership", 1625400).then( res => { logger.log("Got DLC ownership:", res); this.dlcs.puzzle = Boolean(res); @@ -106,7 +103,7 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { } setFullscreen(flag) { - getIPCRenderer().send("set-fullscreen", flag); + ipcRenderer.send("set-fullscreen", flag); } getSupportsAppExit() { @@ -115,6 +112,6 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { exitApp() { logger.log(this, "Sending app exit signal"); - getIPCRenderer().send("exit-app"); + ipcRenderer.send("exit-app"); } } diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 22074eae..03f4fdcf 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -122,7 +122,7 @@ export const autosaveIntervals = [ }, ]; -const refreshRateOptions = ["30", "60", "120", "180", "240"]; +export const refreshRateOptions = ["30", "60", "120", "180", "240"]; if (G_IS_DEV) { refreshRateOptions.unshift("10"); @@ -133,163 +133,161 @@ if (G_IS_DEV) { refreshRateOptions.push("10000"); } -/** @type {Array} */ -export const allApplicationSettings = [ - new EnumSetting("language", { - options: Object.keys(LANGUAGES), - valueGetter: key => key, - textGetter: key => LANGUAGES[key].name, - category: enumCategories.general, - restartRequired: true, - changeCb: (app, id) => null, - magicValue: "auto-detect", - }), - - new EnumSetting("uiScale", { - options: uiScales.sort((a, b) => a.size - b.size), - valueGetter: scale => scale.id, - textGetter: scale => T.settings.labels.uiScale.scales[scale.id], - category: enumCategories.userInterface, - restartRequired: false, - changeCb: +/** @returns {Array} */ +function initializeSettings() { + return [ + new EnumSetting("language", { + options: Object.keys(LANGUAGES), + valueGetter: key => key, + textGetter: key => LANGUAGES[key].name, + category: enumCategories.general, + restartRequired: true, + changeCb: (app, id) => null, + magicValue: "auto-detect", + }), + + new EnumSetting("uiScale", { + options: uiScales.sort((a, b) => a.size - b.size), + valueGetter: scale => scale.id, + textGetter: scale => T.settings.labels.uiScale.scales[scale.id], + category: enumCategories.userInterface, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => app.updateAfterUiScaleChanged(), + }), + + new RangeSetting( + "soundVolume", + enumCategories.general, /** * @param {Application} app */ - (app, id) => app.updateAfterUiScaleChanged(), - }), - - new RangeSetting( - "soundVolume", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setSoundVolume(value) - ), - new RangeSetting( - "musicVolume", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setMusicVolume(value) - ), - - new BoolSetting( - "fullscreen", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => { - if (app.platformWrapper.getSupportsFullscreen()) { - app.platformWrapper.setFullscreen(value); - } - }, - /** - * @param {Application} app - */ app => app.restrictionMgr.getHasExtendedSettings() - ), + (app, value) => app.sound.setSoundVolume(value) + ), + new RangeSetting( + "musicVolume", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => app.sound.setMusicVolume(value) + ), - new BoolSetting( - "enableColorBlindHelper", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => null - ), - - new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}), - - new EnumSetting("theme", { - options: Object.keys(THEMES), - valueGetter: theme => theme, - textGetter: theme => T.settings.labels.theme.themes[theme], - category: enumCategories.userInterface, - restartRequired: false, - changeCb: + new BoolSetting( + "fullscreen", + enumCategories.general, /** * @param {Application} app */ - (app, id) => { - applyGameTheme(id); - document.documentElement.setAttribute("data-theme", id); + (app, value) => { + if (app.platformWrapper.getSupportsFullscreen()) { + app.platformWrapper.setFullscreen(value); + } }, - enabledCb: /** - * @param {Application} app - */ app => app.restrictionMgr.getHasExtendedSettings(), - }), - - new EnumSetting("autosaveInterval", { - options: autosaveIntervals, - valueGetter: interval => interval.id, - textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: /** * @param {Application} app - */ - (app, id) => null, - }), - - new EnumSetting("scrollWheelSensitivity", { - options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale), - valueGetter: scale => scale.id, - textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: + */ app => app.restrictionMgr.getHasExtendedSettings() + ), + + new BoolSetting( + "enableColorBlindHelper", + enumCategories.general, /** * @param {Application} app */ - (app, id) => app.updateAfterUiScaleChanged(), - }), - - new EnumSetting("movementSpeed", { - options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier), - valueGetter: multiplier => multiplier.id, - textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: (app, id) => {}, - }), - - new BoolSetting("enableMousePan", enumCategories.advanced, (app, value) => {}), - new BoolSetting("shapeTooltipAlwaysOn", enumCategories.advanced, (app, value) => {}), - new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}), - new BoolSetting("zoomToCursor", enumCategories.advanced, (app, value) => {}), - new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}), - new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}), - new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}), - new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}), - new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}), - new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}), - new BoolSetting("displayChunkBorders", enumCategories.advanced, (app, value) => {}), - new BoolSetting("pickMinerOnPatch", enumCategories.advanced, (app, value) => {}), - new RangeSetting("mapResourcesScale", enumCategories.advanced, () => null), - - new EnumSetting("refreshRate", { - options: refreshRateOptions, - valueGetter: rate => rate, - textGetter: rate => T.settings.tickrateHz.replace("", rate), - category: enumCategories.performance, - restartRequired: false, - changeCb: (app, id) => {}, - enabledCb: /** - * @param {Application} app - */ app => app.restrictionMgr.getHasExtendedSettings(), - }), - - new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), - new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}), - new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), - new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}), -]; - -export function getApplicationSettingById(id) { - return allApplicationSettings.find(setting => setting.id === id); + (app, value) => null + ), + + new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}), + + new EnumSetting("theme", { + options: Object.keys(THEMES), + valueGetter: theme => theme, + textGetter: theme => T.settings.labels.theme.themes[theme], + category: enumCategories.userInterface, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => { + applyGameTheme(id); + document.documentElement.setAttribute("data-theme", id); + }, + enabledCb: /** + * @param {Application} app + */ app => app.restrictionMgr.getHasExtendedSettings(), + }), + + new EnumSetting("autosaveInterval", { + options: autosaveIntervals, + valueGetter: interval => interval.id, + textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => null, + }), + + new EnumSetting("scrollWheelSensitivity", { + options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale), + valueGetter: scale => scale.id, + textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => app.updateAfterUiScaleChanged(), + }), + + new EnumSetting("movementSpeed", { + options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier), + valueGetter: multiplier => multiplier.id, + textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: (app, id) => {}, + }), + + new BoolSetting("enableMousePan", enumCategories.advanced, (app, value) => {}), + new BoolSetting("shapeTooltipAlwaysOn", enumCategories.advanced, (app, value) => {}), + new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}), + new BoolSetting("zoomToCursor", enumCategories.advanced, (app, value) => {}), + new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}), + new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}), + new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}), + new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}), + new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}), + new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}), + new BoolSetting("displayChunkBorders", enumCategories.advanced, (app, value) => {}), + new BoolSetting("pickMinerOnPatch", enumCategories.advanced, (app, value) => {}), + new RangeSetting("mapResourcesScale", enumCategories.advanced, () => null), + + new EnumSetting("refreshRate", { + options: refreshRateOptions, + valueGetter: rate => rate, + textGetter: rate => T.settings.tickrateHz.replace("", rate), + category: enumCategories.performance, + restartRequired: false, + changeCb: (app, id) => {}, + enabledCb: /** + * @param {Application} app + */ app => app.restrictionMgr.getHasExtendedSettings(), + }), + + new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), + new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}), + new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), + new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}), + ]; } class SettingsStorage { @@ -339,6 +337,8 @@ class SettingsStorage { export class ApplicationSettings extends ReadWriteProxy { constructor(app) { super(app, "app_settings.bin"); + + this.settingHandles = initializeSettings(); } initialize() { @@ -347,8 +347,8 @@ export class ApplicationSettings extends ReadWriteProxy { .then(() => { // Apply default setting callbacks const settings = this.getAllSettings(); - for (let i = 0; i < allApplicationSettings.length; ++i) { - const handle = allApplicationSettings[i]; + for (let i = 0; i < this.settingHandles.length; ++i) { + const handle = this.settingHandles[i]; handle.apply(this.app, settings[handle.id]); } }) @@ -360,6 +360,10 @@ export class ApplicationSettings extends ReadWriteProxy { return this.writeAsync(); } + getSettingHandleById(id) { + return this.settingHandles.find(setting => setting.id === id); + } + // Getters /** @@ -457,20 +461,18 @@ export class ApplicationSettings extends ReadWriteProxy { * @param {string|boolean|number} value */ updateSetting(key, value) { - for (let i = 0; i < allApplicationSettings.length; ++i) { - const setting = allApplicationSettings[i]; - if (setting.id === key) { - if (!setting.validate(value)) { - assertAlways(false, "Bad setting value: " + key); - } - this.getAllSettings()[key] = value; - if (setting.changeCb) { - setting.changeCb(this.app, value); - } - return this.writeAsync(); - } + const setting = this.getSettingHandleById(key); + if (!setting) { + assertAlways(false, "Unknown setting: " + key); + } + if (!setting.validate(value)) { + assertAlways(false, "Bad setting value: " + key); + } + this.getAllSettings()[key] = value; + if (setting.changeCb) { + setting.changeCb(this.app, value); } - assertAlways(false, "Unknown setting: " + key); + return this.writeAsync(); } /** @@ -510,8 +512,15 @@ export class ApplicationSettings extends ReadWriteProxy { } const settings = data.settings; - for (let i = 0; i < allApplicationSettings.length; ++i) { - const setting = allApplicationSettings[i]; + + // MODS + if (!THEMES[settings.theme]) { + console.warn("Resetting theme because its no longer available: " + settings.theme); + settings.theme = "light"; + } + + for (let i = 0; i < this.settingHandles.length; ++i) { + const setting = this.settingHandles[i]; const storedValue = settings[setting.id]; if (!setting.validate(storedValue)) { return ExplainedResult.bad( @@ -690,6 +699,12 @@ export class ApplicationSettings extends ReadWriteProxy { data.version = 31; } + // MODS + if (!THEMES[data.settings.theme]) { + console.warn("Resetting theme because its no longer available: " + data.settings.theme); + data.settings.theme = "light"; + } + return ExplainedResult.good(); } } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 36ed884f..b4472b2b 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -14,6 +14,8 @@ import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; +import { MODS } from "../mods/modloader"; +import { SavegameInterface_V1010 } from "./schemas/1010"; const logger = createLogger("savegame"); @@ -54,7 +56,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1009; + return 1010; } /** @@ -103,6 +105,7 @@ export class Savegame extends ReadWriteProxy { usedInverseRotater: false, }, lastUpdate: Date.now(), + mods: MODS.getModsListForSavegame(), }; } @@ -160,6 +163,11 @@ export class Savegame extends ReadWriteProxy { data.version = 1009; } + if (data.version === 1009) { + SavegameInterface_V1010.migrate1009to1010(data); + data.version = 1010; + } + return ExplainedResult.good(); } @@ -269,6 +277,7 @@ export class Savegame extends ReadWriteProxy { shadowData.dump = dump; shadowData.lastUpdate = new Date().getTime(); shadowData.version = this.getCurrentVersion(); + shadowData.mods = MODS.getModsListForSavegame(); const reader = this.getDumpReaderForExternalData(shadowData); diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index b4dc4233..089b15fc 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -10,6 +10,7 @@ import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; +import { SavegameInterface_V1010 } from "./schemas/1010"; /** @type {Object.} */ export const savegameInterfaces = { @@ -23,6 +24,7 @@ export const savegameInterfaces = { 1007: SavegameInterface_V1007, 1008: SavegameInterface_V1008, 1009: SavegameInterface_V1009, + 1010: SavegameInterface_V1010, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index 3230cdd5..f95c9896 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -1,9 +1,8 @@ import { ExplainedResult } from "../core/explained_result"; -import { createLogger } from "../core/logging"; import { gComponentRegistry } from "../core/global_registries"; +import { createLogger } from "../core/logging"; +import { MOD_SIGNALS } from "../mods/mod_signals"; import { SerializerInternal } from "./serializer_internal"; -import { HUDPinnedShapes } from "../game/hud/parts/pinned_shapes"; -import { HUDWaypoints } from "../game/hud/parts/waypoints"; /** * @typedef {import("../game/component").Component} Component @@ -42,8 +41,12 @@ export class SavegameSerializer { beltPaths: root.systemMgr.systems.belt.serializePaths(), pinnedShapes: root.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null, waypoints: root.hud.parts.waypoints ? root.hud.parts.waypoints.serialize() : null, + + modExtraData: {}, }; + MOD_SIGNALS.gameSerialized.dispatch(root, data); + if (G_IS_DEV) { if (sanityChecks) { // Sanity check @@ -151,6 +154,9 @@ export class SavegameSerializer { return ExplainedResult.bad(errorReason); } + // Mods + MOD_SIGNALS.gameDeserialized.dispatch(root, savegame); + return ExplainedResult.good(); } } diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index c5e0e5c5..b1980115 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -2,6 +2,14 @@ * @typedef {import("../game/entity").Entity} Entity * * @typedef {{ + * id: string; + * version: string; + * website: string; + * name: string; + * author: string; + * }[]} SavegameStoredMods + * + * @typedef {{ * failedMam: boolean, * trashedCount: number, * usedInverseRotater: boolean @@ -17,7 +25,8 @@ * pinnedShapes: any, * waypoints: any, * entities: Array, - * beltPaths: Array + * beltPaths: Array, + * modExtraData: Object * }} SerializedGame * * @typedef {{ @@ -25,6 +34,7 @@ * dump: SerializedGame, * stats: SavegameStats, * lastUpdate: number, + * mods: SavegameStoredMods * }} SavegameData * * @typedef {{ diff --git a/src/js/savegame/schemas/1010.js b/src/js/savegame/schemas/1010.js new file mode 100644 index 00000000..8f480800 --- /dev/null +++ b/src/js/savegame/schemas/1010.js @@ -0,0 +1,28 @@ +import { createLogger } from "../../core/logging.js"; +import { SavegameInterface_V1009 } from "./1009.js"; + +const schema = require("./1010.json"); +const logger = createLogger("savegame_interface/1010"); + +export class SavegameInterface_V1010 extends SavegameInterface_V1009 { + getVersion() { + return 1010; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1009to1010(data) { + logger.log("Migrating 1009 to 1010"); + + data.mods = []; + + if (data.dump) { + data.dump.modExtraData = {}; + } + } +} diff --git a/src/js/savegame/schemas/1010.json b/src/js/savegame/schemas/1010.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1010.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +} diff --git a/src/js/savegame/serialization.js b/src/js/savegame/serialization.js index 78642ceb..770f166f 100644 --- a/src/js/savegame/serialization.js +++ b/src/js/savegame/serialization.js @@ -22,6 +22,7 @@ import { TypeString, TypeStructuredObject, TypeVector, + TypePositiveIntegerOrString, } from "./serialization_data_types"; const logger = createLogger("serialization"); @@ -38,6 +39,7 @@ export const types = { vector: new TypeVector(), tileVector: new TypeVector(), bool: new TypeBoolean(), + uintOrString: new TypePositiveIntegerOrString(), /** * @param {BaseDataType} wrapped @@ -136,7 +138,7 @@ export const types = { /** * A full schema declaration - * @typedef {Object.} Schema + * @typedef {Object. | object} Schema */ const globalSchemaCache = {}; diff --git a/src/js/savegame/serialization_data_types.js b/src/js/savegame/serialization_data_types.js index 9d3b689f..df352e78 100644 --- a/src/js/savegame/serialization_data_types.js +++ b/src/js/savegame/serialization_data_types.js @@ -213,6 +213,53 @@ export class TypePositiveInteger extends BaseDataType { } } +export class TypePositiveIntegerOrString extends BaseDataType { + serialize(value) { + if (Number.isInteger(value)) { + assert(value >= 0, "type integer got negative value: " + value); + } else if (typeof value === "string") { + // all good + } else { + assertAlways(false, "Type integer|string got non integer or string for serialize: " + value); + } + return value; + } + + /** + * @see BaseDataType.deserialize + * @param {any} value + * @param {GameRoot} root + * @param {object} targetObject + * @param {string|number} targetKey + * @returns {string|void} String error code or null on success + */ + deserialize(value, targetObject, targetKey, root) { + targetObject[targetKey] = value; + } + + getAsJsonSchemaUncached() { + return { + oneOf: [{ type: "integer", minimum: 0 }, { type: "string" }], + }; + } + + verifySerializedValue(value) { + if (Number.isInteger(value)) { + if (value < 0) { + return "Negative value for positive integer"; + } + } else if (typeof value === "string") { + // all good + } else { + return "Not a valid number or string: " + value; + } + } + + getCacheKey() { + return "uint_str"; + } +} + export class TypeBoolean extends BaseDataType { serialize(value) { assert(value === true || value === false, "Type bool got non bool for serialize: " + value); diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 0dd6c72a..108028a4 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -9,24 +9,25 @@ import { Savegame } from "../savegame/savegame"; import { GameCore } from "../game/core"; import { MUSIC } from "../platform/sound"; import { enumGameModeIds } from "../game/game_mode"; +import { MOD_SIGNALS } from "../mods/mod_signals"; const logger = createLogger("state/ingame"); // Different sub-states -const stages = { - s3_createCore: "🌈 3: Create core", - s4_A_initEmptyGame: "🌈 4/A: Init empty game", - s4_B_resumeGame: "🌈 4/B: Resume game", +export const GAME_LOADING_STATES = { + s3_createCore: "s3_createCore", + s4_A_initEmptyGame: "s4_A_initEmptyGame", + s4_B_resumeGame: "s4_B_resumeGame", - s5_firstUpdate: "🌈 5: First game update", - s6_postLoadHook: "🌈 6: Post load hook", - s7_warmup: "🌈 7: Warmup", + s5_firstUpdate: "s5_firstUpdate", + s6_postLoadHook: "s6_postLoadHook", + s7_warmup: "s7_warmup", - s10_gameRunning: "🌈 10: Game finally running", + s10_gameRunning: "s10_gameRunning", - leaving: "🌈 Saving, then leaving the game", - destroyed: "🌈 DESTROYED: Core is empty and waits for state leave", - initFailed: "🌈 ERROR: Initialization failed!", + leaving: "leaving", + destroyed: "destroyed", + initFailed: "initFailed", }; export const gameCreationAction = { @@ -82,6 +83,10 @@ export class InGameState extends GameState { this.currentSavePromise = null; } + get dialogs() { + return this.core.root.hud.parts.dialogs; + } + /** * Switches the game into another sub-state * @param {string} stage @@ -91,6 +96,7 @@ export class InGameState extends GameState { if (stage !== this.stage) { this.stage = stage; logger.log(this.stage); + MOD_SIGNALS.gameLoadingStageEntered.dispatch(this, stage); return true; } else { // log(this, "Re entering", stage); @@ -146,7 +152,7 @@ export class InGameState extends GameState { onResized(w, h) { super.onResized(w, h); - if (this.stage === stages.s10_gameRunning) { + if (this.stage === GAME_LOADING_STATES.s10_gameRunning) { this.core.resize(w, h); } } @@ -190,7 +196,7 @@ export class InGameState extends GameState { * @param {any=} payload */ saveThenGoToState(stateId, payload) { - if (this.stage === stages.leaving || this.stage === stages.destroyed) { + if (this.stage === GAME_LOADING_STATES.leaving || this.stage === GAME_LOADING_STATES.destroyed) { logger.warn( "Tried to leave game twice or during destroy:", this.stage, @@ -217,7 +223,7 @@ export class InGameState extends GameState { * @param {string} err */ onInitializationFailure(err) { - if (this.switchStage(stages.initFailed)) { + if (this.switchStage(GAME_LOADING_STATES.initFailed)) { logger.error("Init failure:", err); this.stageDestroyed(); this.moveToState("MainMenuState", { loadError: err }); @@ -230,7 +236,7 @@ export class InGameState extends GameState { * Creates the game core instance, and thus the root */ stage3CreateCore() { - if (this.switchStage(stages.s3_createCore)) { + if (this.switchStage(GAME_LOADING_STATES.s3_createCore)) { logger.log("Creating new game core"); this.core = new GameCore(this.app); @@ -249,7 +255,7 @@ export class InGameState extends GameState { * Initializes a new empty game */ stage4aInitEmptyGame() { - if (this.switchStage(stages.s4_A_initEmptyGame)) { + if (this.switchStage(GAME_LOADING_STATES.s4_A_initEmptyGame)) { this.core.initNewGame(); this.stage5FirstUpdate(); } @@ -259,7 +265,7 @@ export class InGameState extends GameState { * Resumes an existing game */ stage4bResumeGame() { - if (this.switchStage(stages.s4_B_resumeGame)) { + if (this.switchStage(GAME_LOADING_STATES.s4_B_resumeGame)) { if (!this.core.initExistingGame()) { this.onInitializationFailure("Savegame is corrupt and can not be restored."); return; @@ -273,7 +279,7 @@ export class InGameState extends GameState { * Performs the first game update on the game which initializes most caches */ stage5FirstUpdate() { - if (this.switchStage(stages.s5_firstUpdate)) { + if (this.switchStage(GAME_LOADING_STATES.s5_firstUpdate)) { this.core.root.logicInitialized = true; this.core.updateLogic(); this.stage6PostLoadHook(); @@ -285,7 +291,7 @@ export class InGameState extends GameState { * can operate and start to work now. */ stage6PostLoadHook() { - if (this.switchStage(stages.s6_postLoadHook)) { + if (this.switchStage(GAME_LOADING_STATES.s6_postLoadHook)) { logger.log("Post load hook"); this.core.postLoadHook(); this.stage7Warmup(); @@ -298,7 +304,7 @@ export class InGameState extends GameState { * are in the VRAM and we have a smooth experience once we start. */ stage7Warmup() { - if (this.switchStage(stages.s7_warmup)) { + if (this.switchStage(GAME_LOADING_STATES.s7_warmup)) { if (this.creationPayload.fastEnter) { this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast; } else { @@ -311,13 +317,15 @@ export class InGameState extends GameState { * The final stage where this game is running and updating regulary. */ stage10GameRunning() { - if (this.switchStage(stages.s10_gameRunning)) { + if (this.switchStage(GAME_LOADING_STATES.s10_gameRunning)) { this.core.root.signals.readyToRender.dispatch(); logSection("GAME STARTED", "#26a69a"); // Initial resize, might have changed during loading (this is possible) this.core.resize(this.app.screenWidth, this.app.screenHeight); + + MOD_SIGNALS.gameStarted.dispatch(this.core.root); } } @@ -325,7 +333,7 @@ export class InGameState extends GameState { * This stage destroys the whole game, used to cleanup */ stageDestroyed() { - if (this.switchStage(stages.destroyed)) { + if (this.switchStage(GAME_LOADING_STATES.destroyed)) { // Cleanup all api calls this.cancelAllAsyncOperations(); @@ -346,7 +354,7 @@ export class InGameState extends GameState { * When leaving the game */ stageLeavingGame() { - if (this.switchStage(stages.leaving)) { + if (this.switchStage(GAME_LOADING_STATES.leaving)) { // ... } } @@ -357,7 +365,7 @@ export class InGameState extends GameState { * Filters the input (keybindings) */ filterInput() { - return this.stage === stages.s10_gameRunning; + return this.stage === GAME_LOADING_STATES.s10_gameRunning; } /** @@ -395,7 +403,7 @@ export class InGameState extends GameState { return; } - if (this.stage === stages.s7_warmup) { + if (this.stage === GAME_LOADING_STATES.s7_warmup) { this.core.draw(); this.warmupTimeSeconds -= dt / 1000.0; if (this.warmupTimeSeconds < 0) { @@ -404,12 +412,12 @@ export class InGameState extends GameState { } } - if (this.stage === stages.s10_gameRunning) { + if (this.stage === GAME_LOADING_STATES.s10_gameRunning) { this.core.tick(dt); } // If the stage is still active (This might not be the case if tick() moved us to game over) - if (this.stage === stages.s10_gameRunning) { + if (this.stage === GAME_LOADING_STATES.s10_gameRunning) { // Only draw if page visible if (this.app.pageVisible) { this.core.draw(); @@ -442,9 +450,9 @@ export class InGameState extends GameState { } if ( - this.stage !== stages.s10_gameRunning && - this.stage !== stages.s7_warmup && - this.stage !== stages.leaving + this.stage !== GAME_LOADING_STATES.s10_gameRunning && + this.stage !== GAME_LOADING_STATES.s7_warmup && + this.stage !== GAME_LOADING_STATES.leaving ) { logger.warn("Skipping save because game is not ready"); return Promise.resolve(); diff --git a/src/js/states/keybindings.js b/src/js/states/keybindings.js index a01629f1..e6721bf8 100644 --- a/src/js/states/keybindings.js +++ b/src/js/states/keybindings.js @@ -19,7 +19,7 @@ export class KeybindingsState extends TextualGameState {
${T.keybindings.hint} - +
@@ -34,6 +34,10 @@ export class KeybindingsState extends TextualGameState { this.trackClicks(this.htmlElement.querySelector(".resetBindings"), this.resetBindings); for (const category in KEYMAPPINGS) { + if (Object.keys(KEYMAPPINGS[category]).length === 0) { + continue; + } + const categoryDiv = document.createElement("div"); categoryDiv.classList.add("category"); keybindingsElem.appendChild(categoryDiv); @@ -138,7 +142,19 @@ export class KeybindingsState extends TextualGameState { } const mappingDiv = container.querySelector(".mapping"); - mappingDiv.innerHTML = getStringForKeyCode(keyCode); + let modifiers = ""; + + if (mapped.modifiers && mapped.modifiers.shift) { + modifiers += "⇪ "; + } + if (mapped.modifiers && mapped.modifiers.alt) { + modifiers += T.global.keys.alt + " "; + } + if (mapped.modifiers && mapped.modifiers.ctrl) { + modifiers += T.global.keys.control + " "; + } + + mappingDiv.innerHTML = modifiers + getStringForKeyCode(keyCode); mappingDiv.classList.toggle("changed", !!overrides[keybindingId]); const resetBtn = container.querySelector("button.resetKeybinding"); diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 60495a9c..10e280b9 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -10,16 +10,17 @@ import { generateFileDownload, isSupportedBrowser, makeButton, - makeButtonElement, makeDiv, + makeDivElement, removeAllChildren, startFileChoose, waitNextFrame, } from "../core/utils"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { MODS } from "../mods/modloader"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { PlatformWrapperImplElectron } from "../platform/electron/wrapper"; -import { getApplicationSettingById } from "../profile/application_settings"; +import { Savegame } from "../savegame/savegame"; import { T } from "../translations"; const trim = require("trim"); @@ -41,6 +42,7 @@ export class MainMenuState extends GameState { const showBrowserWarning = !G_IS_STANDALONE && !isSupportedBrowser(); const showPuzzleDLC = !G_WEGAME_VERSION && (G_IS_STANDALONE || G_IS_DEV); const showWegameFooter = G_WEGAME_VERSION; + const hasMods = MODS.anyModsActive(); let showExternalLinks = true; @@ -94,7 +96,7 @@ export class MainMenuState extends GameState {
@@ -112,7 +114,7 @@ export class MainMenuState extends GameState {
${ - showPuzzleDLC && ownsPuzzleDLC + showPuzzleDLC && ownsPuzzleDLC && !hasMods ? `
@@ -147,6 +149,38 @@ export class MainMenuState extends GameState {
` : "" } + ${ + hasMods + ? ` + +
+
+

${T.mods.title}

+ +
+
+ ${MODS.mods + .map(mod => { + return ` +
+
${mod.metadata.name}
+
by ${mod.metadata.author}
+
+ `; + }) + .join("")} +
+ +
+ ${T.mainMenu.mods.warningPuzzleDLC} +
+ + +
+ ` + : "" + } +
${ @@ -195,7 +229,7 @@ export class MainMenuState extends GameState {
${T.mainMenu.madeBy.replace( "", - 'Tobias Springer' + 'tobspr Games' )}
@@ -335,6 +369,7 @@ export class MainMenuState extends GameState { ".puzzleDlcPlayButton": this.onPuzzleModeButtonClicked, ".puzzleDlcGetButton": this.onPuzzleWishlistButtonClicked, ".wegameDisclaimer > .rating": this.onWegameRatingClicked, + ".editMods": this.onModsClicked, }; for (const key in clickHandling) { @@ -353,36 +388,41 @@ export class MainMenuState extends GameState { const buttonContainer = this.htmlElement.querySelector(".mainContainer .buttons"); removeAllChildren(buttonContainer); + const outerDiv = makeDivElement(null, ["outer"], null); + // Import button - const importButtonElement = makeButtonElement( - ["importButton", "styledButton"], - T.mainMenu.importSavegame + this.trackClicks( + makeButton(outerDiv, ["importButton", "styledButton"], T.mainMenu.importSavegame), + this.requestImportSavegame ); - this.trackClicks(importButtonElement, this.requestImportSavegame); if (this.savedGames.length > 0) { // Continue game - const continueButton = makeButton( - buttonContainer, - ["continueButton", "styledButton"], - T.mainMenu.continue + this.trackClicks( + makeButton(buttonContainer, ["continueButton", "styledButton"], T.mainMenu.continue), + this.onContinueButtonClicked ); - this.trackClicks(continueButton, this.onContinueButtonClicked); - - const outerDiv = makeDiv(buttonContainer, null, ["outer"], null); - outerDiv.appendChild(importButtonElement); - const newGameButton = makeButton( - this.htmlElement.querySelector(".mainContainer .outer"), - ["newGameButton", "styledButton"], - T.mainMenu.newGame + + // New game + this.trackClicks( + makeButton(outerDiv, ["newGameButton", "styledButton"], T.mainMenu.newGame), + this.onPlayButtonClicked ); - this.trackClicks(newGameButton, this.onPlayButtonClicked); } else { // New game - const playBtn = makeButton(buttonContainer, ["playButton", "styledButton"], T.mainMenu.play); - this.trackClicks(playBtn, this.onPlayButtonClicked); - buttonContainer.appendChild(importButtonElement); + this.trackClicks( + makeButton(buttonContainer, ["playButton", "styledButton"], T.mainMenu.play), + this.onPlayButtonClicked + ); } + + // Mods + this.trackClicks( + makeButton(outerDiv, ["modsButton", "styledButton"], T.mods.title), + this.onModsClicked + ); + + buttonContainer.appendChild(outerDiv); } onPuzzleModeButtonClicked(force = false) { @@ -434,7 +474,7 @@ export class MainMenuState extends GameState { onLanguageChooseClicked() { this.app.analytics.trackUiClick("choose_language"); - const setting = /** @type {EnumSetting} */ (getApplicationSettingById("language")); + const setting = /** @type {EnumSetting} */ (this.app.settings.getSettingHandleById("language")); const { optionSelected } = this.dialogs.showOptionChooser(T.settings.labels.language.title, { active: this.app.settings.getLanguage(), @@ -576,11 +616,13 @@ export class MainMenuState extends GameState { const savegame = this.app.savegameMgr.getSavegameById(game.internalId); savegame .readAsync() + .then(() => this.checkForModDifferences(savegame)) .then(() => { this.moveToState("InGameState", { savegame, }); }) + .catch(err => { this.dialogs.showWarning( T.dialogs.gameLoadFailure.title, @@ -590,6 +632,57 @@ export class MainMenuState extends GameState { }); } + /** + * @param {Savegame} savegame + */ + checkForModDifferences(savegame) { + const difference = MODS.computeModDifference(savegame.currentData.mods); + + if (difference.missing.length === 0 && difference.extra.length === 0) { + return Promise.resolve(); + } + + let dialogHtml = T.dialogs.modsDifference.desc; + + /** + * + * @param {import("../savegame/savegame_typedefs").SavegameStoredMods[0]} mod + */ + function formatMod(mod) { + return ` +
+
${mod.name}
+
${T.mods.version} ${mod.version}
+ + +
+ `; + } + + if (difference.missing.length > 0) { + dialogHtml += "

" + T.dialogs.modsDifference.missingMods + "

"; + dialogHtml += difference.missing.map(formatMod).join("
"); + } + + if (difference.extra.length > 0) { + dialogHtml += "

" + T.dialogs.modsDifference.newMods + "

"; + dialogHtml += difference.extra.map(formatMod).join("
"); + } + + const signals = this.dialogs.showWarning(T.dialogs.modsDifference.title, dialogHtml, [ + "cancel:good", + "continue:bad", + ]); + + return new Promise(resolve => { + signals.continue.add(resolve); + }); + } + /** * @param {SavegameMetadata} game */ @@ -695,6 +788,12 @@ export class MainMenuState extends GameState { ); } + onModsClicked() { + this.moveToState("ModsState", { + backToStateId: "MainMenuState", + }); + } + onContinueButtonClicked() { let latestLastUpdate = 0; let latestInternalId; @@ -709,6 +808,7 @@ export class MainMenuState extends GameState { savegame .readAsync() .then(() => this.app.adProvider.showVideoAd()) + .then(() => this.checkForModDifferences(savegame)) .then(() => { this.moveToState("InGameState", { savegame, diff --git a/src/js/states/mods.js b/src/js/states/mods.js new file mode 100644 index 00000000..1e0fe5f1 --- /dev/null +++ b/src/js/states/mods.js @@ -0,0 +1,149 @@ +import { THIRDPARTY_URLS } from "../core/config"; +import { TextualGameState } from "../core/textual_game_state"; +import { MODS } from "../mods/modloader"; +import { T } from "../translations"; + +export class ModsState extends TextualGameState { + constructor() { + super("ModsState"); + } + + getStateHeaderTitle() { + return T.mods.title; + } + + internalGetFullHtml() { + let headerHtml = ` +
+

${this.getStateHeaderTitle()}

+ +
+ ${ + (G_IS_STANDALONE || G_IS_DEV) && MODS.mods.length > 0 + ? `` + : "" + } + ${ + G_IS_STANDALONE || G_IS_DEV + ? `` + : "" + } +
+ +
`; + + return ` + ${headerHtml} +
+ ${this.getInnerHTML()} +
+ `; + } + + getMainContentHTML() { + if (!G_IS_STANDALONE && !G_IS_DEV) { + return ` +
+ +

${T.mods.noModSupport}

+ + Get the shapez.io standalone! + + +
+ `; + } + + if (MODS.mods.length === 0) { + return ` + +
+ ${T.mods.modsInfo} + + +
+ + `; + } + + let modsHtml = ``; + + MODS.mods.forEach(mod => { + modsHtml += ` +
+
+ ${mod.metadata.name} + ${mod.metadata.description} + ${T.mods.modWebsite} +
+ ${T.mods.version}${mod.metadata.version} + ${T.mods.author}${mod.metadata.author} +
+ +
+ +
+ `; + }); + return ` + +
+ ${T.mods.modsInfo} +
+ +
+ ${modsHtml} +
+ `; + } + + onEnter() { + const steamLink = this.htmlElement.querySelector(".steamLink"); + if (steamLink) { + this.trackClicks(steamLink, this.onSteamLinkClicked); + } + const openModsFolder = this.htmlElement.querySelector(".openModsFolder"); + 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 => { + this.trackClicks(checkbox, this.showModTogglingComingSoon); + }); + } + + showModTogglingComingSoon() { + this.dialogs.showWarning(T.mods.togglingComingSoon.title, T.mods.togglingComingSoon.description); + } + + openModsFolder() { + if (!G_IS_STANDALONE) { + this.dialogs.showWarning(T.global.error, T.mods.folderOnlyStandalone); + return; + } + 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( + THIRDPARTY_URLS.stanaloneCampaignLink + "/shapez_modsettings" + ); + + return false; + } + + getDefaultPreviousState() { + return "SettingsState"; + } +} diff --git a/src/js/states/settings.js b/src/js/states/settings.js index 352e0153..ad52f3df 100644 --- a/src/js/states/settings.js +++ b/src/js/states/settings.js @@ -1,7 +1,7 @@ import { THIRDPARTY_URLS } from "../core/config"; import { TextualGameState } from "../core/textual_game_state"; import { formatSecondsToTimeAgo } from "../core/utils"; -import { allApplicationSettings, enumCategories } from "../profile/application_settings"; +import { enumCategories } from "../profile/application_settings"; import { T } from "../translations"; export class SettingsState extends TextualGameState { @@ -19,6 +19,8 @@ export class SettingsState extends TextualGameState {