diff --git a/electron/index.js b/electron/index.js index b877900c..f11ca666 100644 --- a/electron/index.js +++ b/electron/index.js @@ -74,20 +74,8 @@ function createWindow() { win.on("closed", () => { console.log("Window closed"); win = null; - app.quit(); }); - function handleWindowBeforeunload(event) { - const confirmed = dialog.showMessageBox(remote.getCurrentWindow(), options) === 1; - if (confirmed) { - remote.getCurrentWindow().close(); - } else { - event.returnValue = false; - } - } - - win.on("", handleWindowBeforeunload); - if (isDev) { menu = new Menu(); diff --git a/electron/package.json b/electron/package.json index 893e3609..d21aff71 100644 --- a/electron/package.json +++ b/electron/package.json @@ -10,10 +10,10 @@ "start": "electron --disable-direct-composition --in-process-gpu ." }, "devDependencies": { - "electron": "10.4.0" + "electron": "10.4.3" }, "optionalDependencies": { - "shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v85" + "shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v82" }, "dependencies": { "async-lock": "^1.2.8" diff --git a/electron/steam.js b/electron/steam.js index 08d0a046..7ebd457d 100644 --- a/electron/steam.js +++ b/electron/steam.js @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); const { ipcMain } = require("electron"); let greenworks = null; @@ -11,10 +11,10 @@ try { appId = parseInt(fs.readFileSync(path.join(__dirname, "steam_appid.txt"), "utf8")); } catch (err) { // greenworks is not installed - // throw err; + console.warn("Failed to load steam api:", err); } -function init (isDev) { +function init(isDev) { if (!greenworks) { return; } @@ -34,11 +34,16 @@ function init (isDev) { initialized = true; } -function listen () { +function listen() { ipcMain.handle("steam:is-initialized", isInitialized); - if (!greenworks || !initialized) { - console.log("Ignoring Steam IPC events"); + if (!initialized) { + console.warn("Steam not initialized, won't be able to listen"); + return; + } + + if (!greenworks) { + console.warn("Greenworks not loaded, won't be able to listen"); return; } @@ -53,7 +58,7 @@ function isInitialized(event) { function getAchievementNames(event) { return new Promise((resolve, reject) => { try { - const achievements = greenworks.getAchievementNames() + const achievements = greenworks.getAchievementNames(); resolve(achievements); } catch (err) { reject(err); @@ -63,11 +68,15 @@ function getAchievementNames(event) { function activateAchievement(event, id) { return new Promise((resolve, reject) => { - greenworks.activateAchievement(id, () => resolve(), err => reject(err)) + greenworks.activateAchievement( + id, + () => resolve(), + err => reject(err) + ); }); } module.exports = { init, - listen + listen, }; diff --git a/electron/yarn.lock b/electron/yarn.lock index 8c5b1dec..db2b6278 100644 --- a/electron/yarn.lock +++ b/electron/yarn.lock @@ -146,10 +146,10 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -electron@10.4.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-10.4.0.tgz#018385914474b56110a5a43087a53c114b67c08d" - integrity sha512-qK8OOCWuNvEFWThmjkukkqDwIpBqULlDuMXVC9MC/2P4UaWJEjIYvBmBuTyxtFcKoE3kWvcWyeRDUuvzVxxXjA== +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== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -503,9 +503,9 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" -"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v85": +"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v82": version "0.1.0" - resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#63adf7e0ea4b90c2a29053ce1f0ec9d573b3ac0a" + resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#8aa3bfd3b569eb5695fc8a585a3f2ee3ed2db290" sprintf-js@^1.1.2: version "1.1.2" diff --git a/gulp/bundle-loader.js b/gulp/bundle-loader.js index d8bb8c24..16db26fa 100644 --- a/gulp/bundle-loader.js +++ b/gulp/bundle-loader.js @@ -54,8 +54,11 @@ document.documentElement.appendChild(element); } - window.addEventListener("error", errorHandler); - window.addEventListener("unhandledrejection", errorHandler); + + if (window.location.host.indexOf("localhost") < 0) { + window.addEventListener("error", errorHandler); + window.addEventListener("unhandledrejection", errorHandler); + } function makeJsTag(src, integrity) { var script = document.createElement("script"); diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index 1779a76f..dc9559b4 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -40,7 +40,7 @@ module.exports = ({ G_ALL_UI_IMAGES: JSON.stringify(getAllResourceImages()), }; - const minifyNames = environment === "prod"; + const minifyNames = false; return { mode: "production", diff --git a/res/puzzle_dlc_logo.png b/res/puzzle_dlc_logo.png new file mode 100644 index 00000000..1c430c82 Binary files /dev/null and b/res/puzzle_dlc_logo.png differ diff --git a/res/ui/building_icons/block.png b/res/ui/building_icons/block.png new file mode 100644 index 00000000..a6d914f6 Binary files /dev/null and b/res/ui/building_icons/block.png differ diff --git a/res/ui/building_icons/constant_producer.png b/res/ui/building_icons/constant_producer.png new file mode 100644 index 00000000..f7ac8afa Binary files /dev/null and b/res/ui/building_icons/constant_producer.png differ diff --git a/res/ui/building_icons/goal_acceptor.png b/res/ui/building_icons/goal_acceptor.png new file mode 100644 index 00000000..9087c155 Binary files /dev/null and b/res/ui/building_icons/goal_acceptor.png differ diff --git a/res/ui/building_tutorials/block.png b/res/ui/building_tutorials/block.png new file mode 100644 index 00000000..73925265 Binary files /dev/null and b/res/ui/building_tutorials/block.png differ diff --git a/res/ui/building_tutorials/constant_producer.png b/res/ui/building_tutorials/constant_producer.png new file mode 100644 index 00000000..8af4da33 Binary files /dev/null and b/res/ui/building_tutorials/constant_producer.png differ diff --git a/res/ui/building_tutorials/goal_acceptor.png b/res/ui/building_tutorials/goal_acceptor.png new file mode 100644 index 00000000..054783b6 Binary files /dev/null and b/res/ui/building_tutorials/goal_acceptor.png differ diff --git a/res/ui/icons/puzzle_action_liked_no.png b/res/ui/icons/puzzle_action_liked_no.png new file mode 100644 index 00000000..7b30f81e Binary files /dev/null and b/res/ui/icons/puzzle_action_liked_no.png differ diff --git a/res/ui/icons/puzzle_action_liked_yes.png b/res/ui/icons/puzzle_action_liked_yes.png new file mode 100644 index 00000000..07b8bbcf Binary files /dev/null and b/res/ui/icons/puzzle_action_liked_yes.png differ diff --git a/res/ui/icons/puzzle_complete_indicator.png b/res/ui/icons/puzzle_complete_indicator.png new file mode 100644 index 00000000..e2c95b8b Binary files /dev/null and b/res/ui/icons/puzzle_complete_indicator.png differ diff --git a/res/ui/icons/puzzle_complete_indicator_inverse.png b/res/ui/icons/puzzle_complete_indicator_inverse.png new file mode 100644 index 00000000..f3946efc Binary files /dev/null and b/res/ui/icons/puzzle_complete_indicator_inverse.png differ diff --git a/res/ui/icons/puzzle_completion_rate.png b/res/ui/icons/puzzle_completion_rate.png new file mode 100644 index 00000000..2b07ce22 Binary files /dev/null and b/res/ui/icons/puzzle_completion_rate.png differ diff --git a/res/ui/icons/puzzle_plays.png b/res/ui/icons/puzzle_plays.png new file mode 100644 index 00000000..358b5362 Binary files /dev/null and b/res/ui/icons/puzzle_plays.png differ diff --git a/res/ui/icons/puzzle_upvotes.png b/res/ui/icons/puzzle_upvotes.png new file mode 100644 index 00000000..685d4bd7 Binary files /dev/null and b/res/ui/icons/puzzle_upvotes.png differ diff --git a/res/ui/icons/state_next_button.png b/res/ui/icons/state_next_button.png new file mode 100644 index 00000000..d6e09644 Binary files /dev/null and b/res/ui/icons/state_next_button.png differ diff --git a/res/ui/languages/he.svg b/res/ui/languages/he.svg new file mode 100644 index 00000000..aaa64e98 --- /dev/null +++ b/res/ui/languages/he.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ui/puzzle_dlc_logo.png b/res/ui/puzzle_dlc_logo.png new file mode 100644 index 00000000..1c430c82 Binary files /dev/null and b/res/ui/puzzle_dlc_logo.png differ diff --git a/res/ui/puzzle_dlc_logo_inverse.png b/res/ui/puzzle_dlc_logo_inverse.png new file mode 100644 index 00000000..4709f5c4 Binary files /dev/null and b/res/ui/puzzle_dlc_logo_inverse.png differ diff --git a/res_raw/sounds/music/puzzle-full.mp3 b/res_raw/sounds/music/puzzle-full.mp3 new file mode 100644 index 00000000..6ea6f544 Binary files /dev/null and b/res_raw/sounds/music/puzzle-full.mp3 differ diff --git a/res_raw/sprites/blueprints/block.png b/res_raw/sprites/blueprints/block.png new file mode 100644 index 00000000..ff6107cf Binary files /dev/null and b/res_raw/sprites/blueprints/block.png differ diff --git a/res_raw/sprites/blueprints/constant_producer.png b/res_raw/sprites/blueprints/constant_producer.png new file mode 100644 index 00000000..85b55ded Binary files /dev/null and b/res_raw/sprites/blueprints/constant_producer.png differ diff --git a/res_raw/sprites/blueprints/goal_acceptor.png b/res_raw/sprites/blueprints/goal_acceptor.png new file mode 100644 index 00000000..58097279 Binary files /dev/null and b/res_raw/sprites/blueprints/goal_acceptor.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_exit-tier2.png b/res_raw/sprites/blueprints/underground_belt_exit-tier2.png index be78107b..15dc6b86 100644 Binary files a/res_raw/sprites/blueprints/underground_belt_exit-tier2.png and b/res_raw/sprites/blueprints/underground_belt_exit-tier2.png differ diff --git a/res_raw/sprites/buildings/block.png b/res_raw/sprites/buildings/block.png new file mode 100644 index 00000000..3596f682 Binary files /dev/null and b/res_raw/sprites/buildings/block.png differ diff --git a/res_raw/sprites/buildings/constant_producer.png b/res_raw/sprites/buildings/constant_producer.png new file mode 100644 index 00000000..4bd8a60f Binary files /dev/null and b/res_raw/sprites/buildings/constant_producer.png differ diff --git a/res_raw/sprites/buildings/goal_acceptor.png b/res_raw/sprites/buildings/goal_acceptor.png new file mode 100644 index 00000000..17fa224f Binary files /dev/null and b/res_raw/sprites/buildings/goal_acceptor.png differ diff --git a/res_raw/sprites/create_blueprint_previews.py b/res_raw/sprites/create_blueprint_previews.py index 96688fe4..714804d3 100644 --- a/res_raw/sprites/create_blueprint_previews.py +++ b/res_raw/sprites/create_blueprint_previews.py @@ -41,7 +41,7 @@ def process_image(data, outfilename, src_image): if isWire: targetR = 255 targetG = 104 - targetB = 232 + targetB = 232 for x in range(img.width): for y in range(img.height): @@ -85,6 +85,8 @@ def generate_blueprint_sprite(infilename, outfilename): buildings = listdir("buildings") for buildingId in buildings: + if not ".png" in buildingId: + continue if "hub" in buildingId: continue if "wire-" in buildingId: diff --git a/src/css/ingame_hud/beta_overlay.scss b/src/css/ingame_hud/beta_overlay.scss index caadd127..08eba960 100644 --- a/src/css/ingame_hud/beta_overlay.scss +++ b/src/css/ingame_hud/beta_overlay.scss @@ -1,6 +1,6 @@ #ingame_HUD_BetaOverlay { position: fixed; - @include S(top, 10px); + @include S(top, 70px); left: 50%; transform: translateX(-50%); color: $colorRedBright; diff --git a/src/css/ingame_hud/buildings_toolbar.scss b/src/css/ingame_hud/buildings_toolbar.scss index 54205d64..af9001bb 100644 --- a/src/css/ingame_hud/buildings_toolbar.scss +++ b/src/css/ingame_hud/buildings_toolbar.scss @@ -37,7 +37,7 @@ .building { @include S(width, 30px); - @include S(height, 22px); + @include S(height, 30px); background-size: 45%; &:not(.unlocked) { @@ -49,63 +49,97 @@ } .building { - color: $accentColorDark; display: flex; - flex-direction: column; + @include S(width, 40px); position: relative; - align-items: center; - justify-content: center; - @include S(padding, 5px); - @include S(padding-bottom, 1px); - @include S(width, 35px); @include S(height, 40px); - - background: center center / 70% no-repeat; + .icon { + color: $accentColorDark; + display: flex; + flex-direction: column-reverse; + position: relative; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + @include S(border-radius, $globalBorderRadius); + + background: center center / 70% no-repeat; + } &:not(.unlocked) { - @include S(width, 20px); - opacity: 0.15; - background-image: none !important; + @include S(width, 25px); + .icon { + opacity: 0.15; + } + &.editor { + .icon { + pointer-events: all; + cursor: pointer; + &:hover { + background-color: rgba(22, 30, 68, 0.1); + } + } + } + &:not(.editor) { + .icon { + background-image: uiResource("locked_building.png") !important; + } + } + } - &::before { - content: " "; + &.unlocked { + .icon { + pointer-events: all; + transition: all 50ms ease-in-out; + transition-property: background-color, transform; + cursor: pointer; + + &:hover { + background-color: rgba(30, 40, 90, 0.1); + } - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 4; + &.pressed { + transform: scale(0.9) !important; + } + + &.selected { + // transform: scale(1.05); + background-color: rgba(lighten($colorBlueBright, 9), 0.4); + + .keybinding { + color: #111; + } + } + } + + .puzzle-lock { & { /* @load-async */ - background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)} + background: uiResource("locked_building.png") center center / #{D(14px)} #{D(14px)} no-repeat; } - } - } - - @include S(border-radius, $globalBorderRadius); - &.unlocked { - pointer-events: all; - transition: all 50ms ease-in-out; - transition-property: background-color, transform; + display: grid; + grid-auto-flow: column; - cursor: pointer; - &:hover { - background-color: rgba(30, 40, 90, 0.1); - } + position: absolute; + top: -30px; + left: 50%; + transform: translateX(-50%) !important; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; - &.pressed { - transform: scale(0.9) !important; - } + cursor: pointer; + pointer-events: all; - &.selected { - // transform: scale(1.05); - background-color: rgba(lighten($colorBlueBright, 9), 0.4); + @include S(width, 14px); + @include S(height, 14px); - .keybinding { - color: #111; + &:hover { + opacity: 0.5; } } } diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index ad3f76d0..cc742d42 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -67,6 +67,14 @@ * { color: #fff; } + + display: flex; + flex-direction: column; + + .text { + text-transform: uppercase; + @include S(margin-bottom, 10px); + } } > .dialogInner { @@ -168,6 +176,11 @@ &.errored { background-color: rgb(250, 206, 206); + + &::placeholder { + color: #fff; + opacity: 0.8; + } } } diff --git a/src/css/ingame_hud/puzzle_back_to_menu.scss b/src/css/ingame_hud/puzzle_back_to_menu.scss new file mode 100644 index 00000000..564b592e --- /dev/null +++ b/src/css/ingame_hud/puzzle_back_to_menu.scss @@ -0,0 +1,41 @@ +#ingame_HUD_PuzzleBackToMenu { + position: absolute; + @include S(top, 10px); + @include S(left, 0px); + + display: flex; + flex-direction: column; + align-items: flex-start; + backdrop-filter: blur(D(1px)); + padding: D(3px); + + > .button { + @include PlainText; + pointer-events: all; + cursor: pointer; + position: relative; + color: #333438; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; + text-transform: uppercase; + @include PlainText; + @include S(width, 30px); + @include S(height, 30px); + + @include DarkThemeInvert; + + opacity: 1; + &:hover { + opacity: 0.9 !important; + } + + &.pressed { + transform: scale(0.95) !important; + } + + & { + /* @load-async */ + background: uiResource("icons/state_back_button.png") center center / D(15px) no-repeat; + } + } +} diff --git a/src/css/ingame_hud/puzzle_complete_notification.scss b/src/css/ingame_hud/puzzle_complete_notification.scss new file mode 100644 index 00000000..5f36df82 --- /dev/null +++ b/src/css/ingame_hud/puzzle_complete_notification.scss @@ -0,0 +1,171 @@ +#ingame_HUD_PuzzleCompleteNotification { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: auto; + pointer-events: all; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + & { + /* @load-async */ + background: rgba(#333538, 0.95) uiResource("dialog_bg_pattern.png") top left / #{D(10px)} repeat; + } + + @include InlineAnimation(0.1s ease-in-out) { + 0% { + opacity: 0; + } + } + + > .dialog { + // background: rgba(#222428, 0.5); + @include S(border-radius, $globalBorderRadius); + @include S(padding, 30px); + + @include InlineAnimation(0.5s ease-in-out) { + 0% { + opacity: 0; + } + } + + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + color: #fff; + text-align: center; + + > .title { + @include SuperHeading; + text-transform: uppercase; + @include S(font-size, 30px); + @include S(margin-bottom, 40px); + color: $colorGreenBright !important; + + @include InlineAnimation(0.5s ease-in-out) { + 0% { + transform: translateY(-50vh); + } + 50% { + transform: translateY(5vh); + } + 75% { + transform: translateY(-2vh); + } + } + } + + > .contents { + @include InlineAnimation(0.5s ease-in-out) { + 0% { + transform: translateX(-100vw); + } + 50% { + transform: translateX(5vw); + } + + 75% { + transform: translateX(-2vw); + } + } + + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + + > .stepLike { + display: flex; + flex-direction: column; + @include S(margin-bottom, 10px); + @include SuperSmallText; + + > .buttons { + display: flex; + align-items: center; + justify-content: center; + @include S(margin, 10px, 0); + + > button { + @include S(width, 60px); + @include S(height, 60px); + @include S(margin, 0, 10px); + box-sizing: border-box; + border-radius: 50%; + transition: opacity 0.12s ease-in-out, background-color 0.12s ease-in-out; + @include IncreasedClickArea(0px); + + &.liked-yes { + /* @load-async */ + background: uiResource("icons/puzzle_action_liked_yes.png") center 55% / 60% + no-repeat; + } + + &:hover:not(.active) { + opacity: 0.5 !important; + } + + &.active { + background-color: $colorRedBright !important; + @include InlineAnimation(0.3s ease-in-out) { + 0% { + transform: scale(0); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } + } + } + &:not(.active) { + opacity: 0.4; + } + } + } + } + + > .buttonBar { + display: flex; + @include S(margin-top, 20px); + + button.continue { + background: #555; + @include S(margin-right, 10px); + } + + button.menu { + background-color: $colorGreenBright; + } + + > button { + @include S(min-width, 100px); + @include S(padding, 10px, 20px); + @include IncreasedClickArea(0px); + } + } + + > .actions { + position: absolute; + @include S(bottom, 40px); + + display: grid; + @include S(grid-gap, 15px); + grid-auto-flow: column; + + button { + @include SuperSmallText; + } + .report { + background-color: $accentColorDark; + } + } + } + } +} diff --git a/src/css/ingame_hud/puzzle_dlc_logo.scss b/src/css/ingame_hud/puzzle_dlc_logo.scss new file mode 100644 index 00000000..684cead4 --- /dev/null +++ b/src/css/ingame_hud/puzzle_dlc_logo.scss @@ -0,0 +1,19 @@ +#ingame_HUD_PuzzleDLCLogo { + position: absolute; + @include S(width, 120px); + @include S(height, 40px); + @include S(left, 40px); + @include S(top, 7px); + + & { + /* @load-async */ + background: uiResource("puzzle_dlc_logo.png") center center / contain no-repeat; + } + + @include DarkThemeOverride { + & { + /* @load-async */ + background: uiResource("puzzle_dlc_logo_inverse.png") center center / contain no-repeat; + } + } +} diff --git a/src/css/ingame_hud/puzzle_editor_controls.scss b/src/css/ingame_hud/puzzle_editor_controls.scss new file mode 100644 index 00000000..7ce76b41 --- /dev/null +++ b/src/css/ingame_hud/puzzle_editor_controls.scss @@ -0,0 +1,36 @@ +#ingame_HUD_PuzzleEditorControls { + position: absolute; + + @include S(top, 70px); + @include S(left, 10px); + + display: flex; + flex-direction: column; + @include SuperDuperSmallText; + @include S(width, 200px); + + > span { + @include S(margin-bottom, 10px); + + strong { + font-weight: bold; + } + } + + @include DarkThemeInvert; +} + +#ingame_HUD_PuzzleEditorTitle { + position: absolute; + + @include S(top, 18px); + left: 50%; + transform: translateX(-50%); + text-transform: uppercase; + @include Heading; + text-align: center; + + @include DarkThemeOverride { + color: #eee; + } +} diff --git a/src/css/ingame_hud/puzzle_editor_review.scss b/src/css/ingame_hud/puzzle_editor_review.scss new file mode 100644 index 00000000..523d8025 --- /dev/null +++ b/src/css/ingame_hud/puzzle_editor_review.scss @@ -0,0 +1,50 @@ +#ingame_HUD_PuzzleEditorReview { + position: absolute; + @include S(top, 17px); + @include S(right, 10px); + + display: flex; + flex-direction: column; + align-items: flex-end; + backdrop-filter: blur(D(1px)); + padding: D(3px); + + > .button { + @include ButtonText; + @include IncreasedClickArea(0px); + pointer-events: all; + cursor: pointer; + position: relative; + color: #333438; + transition: all 0.12s ease-in-out; + text-transform: uppercase; + transition-property: opacity, transform; + @include PlainText; + @include S(padding-right, 25px); + opacity: 1; + + @include DarkThemeInvert; + + &:hover { + opacity: 0.9 !important; + } + + &.pressed { + transform: scale(0.95) !important; + } + + & { + /* @load-async */ + background: uiResource("icons/state_next_button.png") right center / D(15px) no-repeat; + } + } + + > .content { + @include SuperDuperSmallText; + @include S(width, 180px); + @include S(padding-right, 25px); + text-align: right; + text-transform: uppercase; + color: $accentColorDark; + } +} diff --git a/src/css/ingame_hud/puzzle_editor_settings.scss b/src/css/ingame_hud/puzzle_editor_settings.scss new file mode 100644 index 00000000..70d16123 --- /dev/null +++ b/src/css/ingame_hud/puzzle_editor_settings.scss @@ -0,0 +1,62 @@ +#ingame_HUD_PuzzleEditorSettings { + position: absolute; + background: $ingameHudBg; + @include S(padding, 10px); + @include S(bottom, 60px); + @include S(left, 10px); + + @include SuperSmallText; + color: #eee; + display: flex; + flex-direction: column; + @include S(border-radius, $globalBorderRadius); + + > .section { + > label { + text-transform: uppercase; + } + + .plusMinus { + @include S(margin-top, 5px); + display: grid; + grid-template-columns: 1fr auto auto auto; + align-items: center; + @include S(grid-gap, 5px); + + label { + @include S(margin-right, 10px); + } + + button { + @include PlainText; + @include S(padding, 0); + display: flex; + align-items: center; + justify-content: center; + @include S(width, 15px); + @include S(height, 15px); + @include IncreasedClickArea(0px); + } + + .value { + text-align: center; + @include S(min-width, 15px); + } + } + + > .buttons { + > .buttonBar { + display: flex; + align-items: center; + @include S(margin-top, 10px); + > button { + @include S(margin-right, 4px); + @include SuperSmallText; + &:last-child { + margin-right: 0; + } + } + } + } + } +} diff --git a/src/css/ingame_hud/puzzle_play_metadata.scss b/src/css/ingame_hud/puzzle_play_metadata.scss new file mode 100644 index 00000000..d0675b13 --- /dev/null +++ b/src/css/ingame_hud/puzzle_play_metadata.scss @@ -0,0 +1,129 @@ +#ingame_HUD_PuzzlePlayMetadata { + position: absolute; + + @include S(top, 70px); + @include S(left, 10px); + + display: flex; + flex-direction: column; + @include S(width, 200px); + + > .info { + display: flex; + flex-direction: column; + @include SuperSmallText; + @include S(margin-bottom, 5px); + + > label { + text-transform: uppercase; + @include SuperDuperSmallText; + color: $accentColorDark; + } + > span { + display: flex; + color: darken($accentColorDark, 25); + @include SuperSmallText; + @include DarkThemeOverride { + color: lighten($accentColorDark, 15); + } + } + } + + > .plays { + display: flex; + align-items: center; + justify-self: end; + align-self: end; + flex-direction: row; + @include S(margin-bottom, 10px); + opacity: 0.8; + @include DarkThemeInvert; + @include DarkThemeOverride { + opacity: 0.8; + } + + > .downloads { + @include SuperSmallText; + color: #000; + align-self: start; + justify-self: start; + font-weight: bold; + @include S(margin-right, 10px); + @include S(padding-left, 14px); + opacity: 0.7; + display: inline-flex; + align-items: center; + justify-content: center; + + & { + /* @load-async */ + background: uiResource("icons/puzzle_plays.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat; + } + } + + > .likes { + @include SuperSmallText; + align-items: center; + justify-content: center; + color: #000; + align-self: start; + justify-self: start; + font-weight: bold; + @include S(padding-left, 14px); + opacity: 0.7; + & { + /* @load-async */ + background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat; + } + } + } + + > .key { + button { + @include S(margin-top, 2px); + } + } + + button { + @include SuperSmallText; + align-self: start; + @include S(min-width, 50px); + + &.report { + background-color: $accentColorDark; + @include SuperDuperSmallText; + } + } + + > .buttons { + display: flex; + flex-direction: column; + + > button { + @include S(margin-bottom, 4px); + } + } +} + +#ingame_HUD_PuzzlePlayTitle { + position: absolute; + + @include S(top, 18px); + left: 50%; + transform: translateX(-50%); + text-transform: uppercase; + @include Heading; + text-align: center; + + display: flex; + flex-direction: column; + + > .name { + @include PlainText; + opacity: 0.5; + } + + @include DarkThemeOverride { + color: #eee; + } +} diff --git a/src/css/ingame_hud/puzzle_play_settings.scss b/src/css/ingame_hud/puzzle_play_settings.scss new file mode 100644 index 00000000..13e25c61 --- /dev/null +++ b/src/css/ingame_hud/puzzle_play_settings.scss @@ -0,0 +1,23 @@ +#ingame_HUD_PuzzlePlaySettings { + position: absolute; + background: $ingameHudBg; + @include S(padding, 10px); + @include S(bottom, 60px); + @include S(left, 10px); + + @include SuperSmallText; + color: #eee; + display: flex; + flex-direction: column; + @include S(border-radius, $globalBorderRadius); + + > .section { + display: grid; + @include S(grid-gap, 10px); + grid-auto-flow: row; + + > button { + @include SuperSmallText; + } + } +} diff --git a/src/css/main.scss b/src/css/main.scss index 35d54e23..1bd82828 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -29,6 +29,7 @@ @import "states/about"; @import "states/mobile_warning"; @import "states/changelog"; +@import "states/puzzle_menu"; @import "ingame_hud/buildings_toolbar"; @import "ingame_hud/building_placer"; @@ -55,12 +56,21 @@ @import "ingame_hud/sandbox_controller"; @import "ingame_hud/standalone_advantages"; @import "ingame_hud/cat_memes"; +@import "ingame_hud/puzzle_back_to_menu"; +@import "ingame_hud/puzzle_editor_review"; +@import "ingame_hud/puzzle_dlc_logo"; +@import "ingame_hud/puzzle_editor_controls"; +@import "ingame_hud/puzzle_editor_settings"; +@import "ingame_hud/puzzle_play_settings"; +@import "ingame_hud/puzzle_play_metadata"; +@import "ingame_hud/puzzle_complete_notification"; // prettier-ignore $elements: // Base ingame_Canvas, ingame_VignetteOverlay, +ingame_HUD_PuzzleDLCLogo, // Ingame overlays ingame_HUD_Waypoints, @@ -71,6 +81,14 @@ ingame_HUD_PlacerVariants, ingame_HUD_PinnedShapes, ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, +ingame_HUD_PuzzleBackToMenu, +ingame_HUD_PuzzleEditorReview, +ingame_HUD_PuzzleEditorControls, +ingame_HUD_PuzzleEditorTitle, +ingame_HUD_PuzzleEditorSettings, +ingame_HUD_PuzzlePlaySettings, +ingame_HUD_PuzzlePlayMetadata, +ingame_HUD_PuzzlePlayTitle, ingame_HUD_Notifications, ingame_HUD_DebugInfo, ingame_HUD_EntityDebugger, @@ -94,6 +112,7 @@ ingame_HUD_Statistics, ingame_HUD_ShapeViewer, ingame_HUD_StandaloneAdvantages, ingame_HUD_UnlockNotification, +ingame_HUD_PuzzleCompleteNotification, ingame_HUD_SettingsMenu, ingame_HUD_ModalDialogs, ingame_HUD_CatMemes; @@ -113,6 +132,8 @@ body.uiHidden { #ingame_HUD_PlacementHints, #ingame_HUD_GameMenu, #ingame_HUD_PinnedShapes, + #ingame_HUD_PuzzleBackToMenu, + #ingame_HUD_PuzzleEditorReview, #ingame_HUD_Notifications, #ingame_HUD_TutorialHints, #ingame_HUD_Waypoints, diff --git a/src/css/resources.scss b/src/css/resources.scss index 5bb3ea99..3a581c30 100644 --- a/src/css/resources.scss +++ b/src/css/resources.scss @@ -1,11 +1,13 @@ $buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire, constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage, - transistor, analyzer, comparator, item_producer; + transistor, analyzer, comparator, item_producer, constant_producer, goal_acceptor, block; @each $building in $buildings { [data-icon="building_icons/#{$building}.png"] { /* @load-async */ - background-image: uiResource("res/ui/building_icons/#{$building}.png") !important; + .icon { + background-image: uiResource("res/ui/building_icons/#{$building}.png") !important; + } } } @@ -13,7 +15,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2, cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage, reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not, logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, item_producer, - virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator; + constant_producer, virtual_processor-stacker, virtual_processor-painter, wire-second, painter, + painter-mirrored, comparator, goal_acceptor, block; @each $building in $buildingsAndVariants { [data-icon="building_tutorials/#{$building}.png"] { /* @load-async */ @@ -67,7 +70,7 @@ $icons: notification_saved, notification_success, notification_upgrade; } $languages: en, de, cs, da, et, es-419, fr, it, pt-BR, sv, tr, el, ru, uk, zh-TW, zh-CN, nb, mt-MT, ar, nl, vi, - th, hu, pl, ja, kor, no, pt-PT, fi, ro; + th, hu, pl, ja, kor, no, pt-PT, fi, ro, he; @each $language in $languages { [data-languageicon="#{$language}"] { diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index b21d465f..005ee7af 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -88,11 +88,7 @@ @include S(grid-column-gap, 10px); display: grid; - grid-template-columns: 1fr; - - &.demo { - grid-template-columns: 1fr 1fr; - } + grid-template-columns: 1fr 1fr; .standaloneBanner { background: rgb(255, 75, 84); @@ -183,7 +179,7 @@ .updateLabel { position: absolute; transform: translateX(50%) rotate(-5deg); - color: #3291e9; + color: #ff590b; @include Heading; font-weight: bold; @include S(right, 40px); @@ -223,9 +219,33 @@ } } + .puzzleContainer { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background: #4cc98a; + grid-row: 1 / 2; + grid-column: 2 / 3; + @include S(padding, 20px); + @include S(border-radius, $globalBorderRadius); + > .dlcLogo { + @include S(width, 200px); + } + + > button { + @include S(margin-top, 20px); + @include Heading; + @include S(padding, 10px, 30px); + background-color: #333; + color: #fff; + } + } + .mainContainer { display: flex; align-items: center; + grid-row: 1 / 2; justify-content: center; flex-direction: column; background: #fafafa; @@ -242,6 +262,16 @@ align-items: center; } + .modeButtons { + display: grid; + grid-template-columns: repeat(2, 1fr); + @include S(grid-column-gap, 10px); + align-items: start; + height: 100%; + width: 100%; + box-sizing: border-box; + } + .browserWarning { @include S(margin-bottom, 10px); background-color: $colorRedBright; @@ -285,6 +315,18 @@ @include S(margin-left, 15px); } + .playModeButton { + @include IncreasedClickArea(0px); + @include S(margin-top, 15px); + @include S(margin-left, 15px); + } + + .editModeButton { + @include IncreasedClickArea(0px); + @include S(margin-top, 15px); + @include S(margin-left, 15px); + } + .savegames { @include S(max-height, 105px); overflow-y: auto; @@ -439,6 +481,27 @@ } } + .bottomContainer { + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + @include S(padding-top, 10px); + height: 100%; + width: 100%; + box-sizing: border-box; + + .buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + @include S(grid-column-gap, 10px); + align-items: start; + height: 100%; + width: 100%; + box-sizing: border-box; + } + } + .footer { display: grid; flex-grow: 1; diff --git a/src/css/states/preload.scss b/src/css/states/preload.scss index 514c60d2..2e14abd6 100644 --- a/src/css/states/preload.scss +++ b/src/css/states/preload.scss @@ -17,7 +17,7 @@ @include S(border-radius, 3px); @include DarkThemeOverride { - background: #424242; + background: #33343c; } .version { diff --git a/src/css/states/puzzle_menu.scss b/src/css/states/puzzle_menu.scss new file mode 100644 index 00000000..2c0d3773 --- /dev/null +++ b/src/css/states/puzzle_menu.scss @@ -0,0 +1,277 @@ +#state_PuzzleMenuState { + > .headerBar { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + + > h1 { + justify-self: start; + } + + .createPuzzle { + background-color: $colorGreenBright; + @include S(margin-left, 5px); + } + } + + > .container { + > .mainContent { + overflow: hidden; + + > .categoryChooser { + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + @include S(grid-gap, 2px); + @include S(padding-right, 10px); + + > .category { + background: $accentColorBright; + border-radius: 0; + color: $accentColorDark; + transition: all 0.12s ease-in-out; + transition-property: opacity, background-color, color; + + &:first-child { + @include S(border-top-left-radius, $globalBorderRadius); + @include S(border-bottom-left-radius, $globalBorderRadius); + } + &:last-child { + border-top-right-radius: $globalBorderRadius; + border-bottom-right-radius: $globalBorderRadius; + } + + &.active { + background: $colorBlueBright; + opacity: 1 !important; + color: #fff; + cursor: default; + } + + @include DarkThemeOverride { + background: $accentColorDark; + color: #bbbbc4; + + &.active { + background: $colorBlueBright; + color: #fff; + } + } + } + } + + > .puzzles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(D(180px), 1fr)); + @include S(grid-auto-rows, 65px); + @include S(grid-gap, 7px); + @include S(margin-top, 10px); + @include S(padding-right, 4px); + @include S(height, 360px); + overflow-y: scroll; + pointer-events: all; + position: relative; + + > .puzzle { + width: 100%; + @include S(height, 65px); + background: #f3f3f8; + @include S(border-radius, $globalBorderRadius); + + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: D(15px) D(15px) 1fr; + @include S(padding, 5px); + @include S(grid-column-gap, 5px); + box-sizing: border-box; + pointer-events: all; + cursor: pointer; + position: relative; + @include S(padding-left, 10px); + + @include DarkThemeOverride { + background: rgba(0, 0, 10, 0.2); + } + + @include InlineAnimation(0.12s ease-in-out) { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + &:hover { + background: #f0f0f8; + } + + > .title { + grid-column: 2 / 3; + grid-row: 1 / 2; + @include PlainText; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + align-self: center; + justify-self: start; + width: 100%; + box-sizing: border-box; + @include S(padding, 2px, 5px); + @include S(height, 17px); + } + + > .author { + grid-column: 2 / 2; + grid-row: 2 / 3; + @include SuperSmallText; + color: $accentColorDark; + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + @include S(padding, 2px, 5px); + } + + > .icon { + grid-column: 1 / 2; + grid-row: 1 / 4; + align-self: center; + justify-self: center; + @include S(width, 45px); + @include S(height, 45px); + + canvas { + width: 100%; + height: 100%; + } + } + + > .stats { + grid-column: 2 / 3; + grid-row: 3 / 4; + display: flex; + align-items: center; + justify-self: end; + justify-content: center; + align-self: end; + @include S(height, 14px); + + > .downloads { + @include SuperSmallText; + color: #000; + font-weight: bold; + @include S(margin-right, 5px); + @include S(padding-left, 12px); + opacity: 0.7; + display: inline-flex; + align-items: center; + justify-content: center; + @include DarkThemeInvert; + + & { + /* @load-async */ + background: uiResource("icons/puzzle_plays.png") #{D(2px)} #{D(2.5px)} / #{D( + 8px + )} #{D(8px)} no-repeat; + } + } + + > .likes { + @include SuperSmallText; + align-items: center; + justify-content: center; + color: #000; + font-weight: bold; + @include S(padding-left, 14px); + opacity: 0.7; + @include DarkThemeInvert; + + & { + /* @load-async */ + background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} #{D(2.4px)} / #{D( + 9px + )} #{D(9px)} no-repeat; + } + } + + > .difficulty { + @include SuperSmallText; + align-items: center; + justify-content: center; + color: #000; + font-weight: bold; + @include S(margin-right, 3px); + opacity: 0.7; + + &.stage--easy { + color: $colorGreenBright; + } + &.stage--normal { + color: #000; + @include DarkThemeInvert; + } + &.stage--medium { + color: $colorOrangeBright; + } + &.stage--hard { + color: $colorRedBright; + } + } + } + + &.completed { + > .icon, + > .stats, + > .author, + > .title { + opacity: 0.3; + } + + background: #fafafa; + + @include DarkThemeOverride { + background: rgba(0, 0, 0, 0.05); + } + + &::after { + content: ""; + position: absolute; + @include S(top, 10px); + @include S(right, 10px); + @include S(width, 30px); + @include S(height, 30px); + opacity: 0.1; + + & { + /* @load-async */ + background: uiResource("icons/puzzle_complete_indicator.png") center center / + contain no-repeat; + } + } + @include DarkThemeOverride { + &::after { + /* @load-async */ + background: uiResource("icons/puzzle_complete_indicator_inverse.png") center + center / contain no-repeat; + } + } + } + } + + > .loader, + > .empty { + display: flex; + align-items: center; + color: $accentColorDark; + justify-content: center; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + } + } + } +} diff --git a/src/css/variables.scss b/src/css/variables.scss index d2798f41..c7b7c17c 100644 --- a/src/css/variables.scss +++ b/src/css/variables.scss @@ -18,8 +18,10 @@ $textLineHeight: 21px; $plainTextFontSize: 13px; $plainTextLineHeight: 17px; -$supersmallTextFontSize: 10px; -$supersmallTextLineHeight: 13px; +$superDuperSmallTextFontSize: 8px; +$superDuperSmallTextLineHeight: 9px; +$superSmallTextFontSize: 10px; +$superSmallTextLineHeight: 13px; $buttonFontSize: 14px; $buttonLineHeight: 18px; @@ -33,6 +35,7 @@ $accentColorDark: #7d808a; $colorGreenBright: #66bb6a; $colorBlueBright: rgb(74, 151, 223); $colorRedBright: #ef5072; +$colorOrangeBright: #ef9d50; $themeColor: #393747; $ingameHudBg: rgba(#333438, 0.9); @@ -76,8 +79,16 @@ $mainFontScale: 1; // } } +@mixin SuperDuperSmallText { + @include ScaleFont($superDuperSmallTextFontSize, $superDuperSmallTextLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + letter-spacing: $mainFontSpacing; + @include DebugText(green); +} + @mixin SuperSmallText { - @include ScaleFont($supersmallTextFontSize, $supersmallTextLineHeight); + @include ScaleFont($superSmallTextFontSize, $superSmallTextLineHeight); font-weight: $mainFontWeight; font-family: $mainFont; letter-spacing: $mainFontSpacing; diff --git a/src/js/application.js b/src/js/application.js index 2c632ef9..4e74b014 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -31,6 +31,9 @@ import { PreloadState } from "./states/preload"; import { SettingsState } from "./states/settings"; import { ShapezGameAnalytics } from "./platform/browser/game_analytics"; import { RestrictionManager } from "./core/restriction_manager"; +import { PuzzleMenuState } from "./states/puzzle_menu"; +import { ClientAPI } from "./platform/api"; +import { LoginState } from "./states/login"; /** * @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface @@ -72,6 +75,7 @@ export class Application { this.savegameMgr = new SavegameManager(this); this.inputMgr = new InputDistributor(this); this.backgroundResourceLoader = new BackgroundResourcesLoader(this); + this.clientApi = new ClientAPI(this); // Restrictions (Like demo etc) this.restrictionMgr = new RestrictionManager(this); @@ -159,6 +163,8 @@ export class Application { KeybindingsState, AboutState, ChangelogState, + PuzzleMenuState, + LoginState, ]; for (let i = 0; i < states.length; ++i) { diff --git a/src/js/changelog.js b/src/js/changelog.js index 61c1c79a..3a28acde 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,12 +1,32 @@ export const CHANGELOG = [ { - version: "1.3.1", - date: "beta", + version: "1.4.0", + date: "UNRELEASED", entries: [ - "Fixed savegames getting corrupt in rare conditions", - "Fixed game crashing sometimes since the achievements update", + "Added puzzle mode", + "Belts in blueprints should now always paste correctly", + "You can now clear belts by selecting them, and then pressing 'B'", ], }, + { + version: "1.3.1", + date: "16.04.2021", + entries: G_CHINA_VERSION + ? [ + "第13关的交付目标更改为:中国古代指南针。(感谢玩家:凯风入心 创作并提供", + "第17关的交付目标更改为:永乐通宝。(感谢玩家:金天赐 创作并提供", + "第22关的交付目标更改为:凤凰。(感谢玩家:我没得眼镜 创作并提供", + "第23关的交付目标更改为:古代车轮。(感谢玩家:我没得眼镜 创作并提供", + "第24关的交付目标更改为:大熊猫。(感谢玩家:窝囸倪现任 创作并提供", + + "修复了一些特定情况下偶尔会发生的存档损坏问题", + "修复了成就更新后有时候游戏崩溃的问题", + ] + : [ + "Fixed savegames getting corrupt in rare conditions", + "Fixed game crashing sometimes since the achievements update", + ], + }, { version: "1.3.0", date: "12.03.2020", diff --git a/src/js/core/animation_frame.js b/src/js/core/animation_frame.js index eeefb4b0..6aa629a5 100644 --- a/src/js/core/animation_frame.js +++ b/src/js/core/animation_frame.js @@ -51,9 +51,12 @@ export class AnimationFrame { dt = resetDtMs; } - this.frameEmitted.dispatch(dt); + try { + this.frameEmitted.dispatch(dt); + } catch (ex) { + console.error(ex); + } this.lastTime = time; - window.requestAnimationFrame(this.boundMethod); } } diff --git a/src/js/core/config.js b/src/js/core/config.js index d5dc7089..7aa226c0 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -71,6 +71,13 @@ export const globalConfig = { readerAnalyzeIntervalSeconds: 10, + goalAcceptorMinimumDurationSeconds: 5, + goalAcceptorsPerProducer: 4.5, + puzzleModeSpeed: 3, + puzzleMinBoundsSize: 2, + puzzleMaxBoundsSize: 20, + puzzleValidationDurationSeconds: 30, + buildingSpeeds: { cutter: 1 / 4, cutterQuad: 1 / 4, @@ -93,7 +100,7 @@ export const globalConfig = { gameSpeed: 1, warmupTimeSecondsFast: 0.5, - warmupTimeSecondsRegular: 3, + warmupTimeSecondsRegular: 1.5, smoothing: { smoothMainCanvas: smoothCanvas && true, diff --git a/src/js/core/config.local.template.js b/src/js/core/config.local.template.js index 5e3cdad6..fc71c01e 100644 --- a/src/js/core/config.local.template.js +++ b/src/js/core/config.local.template.js @@ -62,6 +62,9 @@ export default { // Allows unlocked achievements to be logged to console in the local build // testAchievements: true, // ----------------------------------------------------------------------------------- + // Enables use of (some) existing flags within the puzzle mode context + // testPuzzleMode: true, + // ----------------------------------------------------------------------------------- // Disables the automatic switch to an overview when zooming out // disableMapOverview: true, // ----------------------------------------------------------------------------------- diff --git a/src/js/core/error_handler.js b/src/js/core/error_handler.js index 686e4e4e..c149ba76 100644 --- a/src/js/core/error_handler.js +++ b/src/js/core/error_handler.js @@ -123,4 +123,6 @@ function catchErrors(message, source, lineno, colno, error) { return true; } -window.onerror = catchErrors; +if (!G_IS_DEV) { + window.onerror = catchErrors; +} diff --git a/src/js/core/global_registries.js b/src/js/core/global_registries.js index ad45850c..723bf567 100644 --- a/src/js/core/global_registries.js +++ b/src/js/core/global_registries.js @@ -5,6 +5,7 @@ import { Factory } from "./factory"; * @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed * @typedef {import("../game/component").Component} Component * @typedef {import("../game/base_item").BaseItem} BaseItem + * @typedef {import("../game/game_mode").GameMode} GameMode * @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding @@ -19,6 +20,9 @@ export let gBuildingsByCategory = null; /** @type {FactoryTemplate} */ export let gComponentRegistry = new Factory("component"); +/** @type {FactoryTemplate} */ +export let gGameModeRegistry = new Factory("gameMode"); + /** @type {FactoryTemplate} */ export let gGameSpeedRegistry = new Factory("gamespeed"); diff --git a/src/js/core/modal_dialog_elements.js b/src/js/core/modal_dialog_elements.js index 5f0ed59f..ee552aa9 100644 --- a/src/js/core/modal_dialog_elements.js +++ b/src/js/core/modal_dialog_elements.js @@ -267,7 +267,7 @@ export class Dialog { * Dialog which simply shows a loading spinner */ export class DialogLoading extends Dialog { - constructor(app) { + constructor(app, text = "") { super({ app, title: "", @@ -279,6 +279,8 @@ export class DialogLoading extends Dialog { // Loading dialog can not get closed with back button this.inputReciever.backButton.removeAll(); this.inputReciever.context = "dialog-loading"; + + this.text = text; } createElement() { @@ -287,6 +289,13 @@ export class DialogLoading extends Dialog { elem.classList.add("loadingDialog"); this.element = elem; + if (this.text) { + const text = document.createElement("div"); + text.classList.add("text"); + text.innerText = this.text; + elem.appendChild(text); + } + const loader = document.createElement("div"); loader.classList.add("prefab_LoadingTextWithAnim"); loader.classList.add("loadingIndicator"); @@ -309,7 +318,7 @@ export class DialogOptionChooser extends Dialog {
- ${iconHtml} + ${iconHtml} ${text} ${descHtml}
@@ -444,7 +453,7 @@ export class DialogWithForm extends Dialog { for (let i = 0; i < this.formElements.length; ++i) { const elem = this.formElements[i]; elem.bindEvents(div, this.clickDetectors); - elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested); + // elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested); elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen); } diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index 1c5b1986..aac81d82 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -117,6 +117,11 @@ export class FormElementInput extends FormElement { return this.element.value; } + setValue(value) { + this.element.value = value; + this.updateErrorState(); + } + focus() { this.element.focus(); } diff --git a/src/js/core/rectangle.js b/src/js/core/rectangle.js index f17825ca..bd3421d9 100644 --- a/src/js/core/rectangle.js +++ b/src/js/core/rectangle.js @@ -44,6 +44,15 @@ export class Rectangle { return new Rectangle(left, top, right - left, bottom - top); } + /** + * + * @param {number} width + * @param {number} height + */ + static centered(width, height) { + return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height); + } + /** * Returns if a intersects b * @param {Rectangle} a @@ -72,7 +81,7 @@ export class Rectangle { /** * Returns if this rectangle is equal to the other while taking an epsilon into account * @param {Rectangle} other - * @param {number} epsilon + * @param {number} [epsilon] */ equalsEpsilon(other, epsilon) { return ( @@ -287,6 +296,15 @@ export class Rectangle { return Rectangle.fromTRBL(top, right, bottom, left); } + /** + * Returns whether the rectangle fully intersects the given rectangle + * @param {Rectangle} rect + */ + intersectsFully(rect) { + const intersection = this.getIntersection(rect); + return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001; + } + /** * Returns the union of this rectangle with another * @param {Rectangle} rect diff --git a/src/js/core/signal.js b/src/js/core/signal.js index 7daae4ea..2dbc9f93 100644 --- a/src/js/core/signal.js +++ b/src/js/core/signal.js @@ -17,6 +17,17 @@ export class Signal { ++this.modifyCount; } + /** + * Adds a new signal listener + * @param {function} receiver + * @param {object} scope + */ + addToTop(receiver, scope = null) { + assert(receiver, "receiver is null"); + this.receivers.unshift({ receiver, scope }); + ++this.modifyCount; + } + /** * Dispatches the signal * @param {...any} payload diff --git a/src/js/core/state_manager.js b/src/js/core/state_manager.js index 3c49ada9..e0c04bba 100644 --- a/src/js/core/state_manager.js +++ b/src/js/core/state_manager.js @@ -90,9 +90,9 @@ export class StateManager { dialogParent.classList.add("modalDialogParent"); document.body.appendChild(dialogParent); + this.currentState.internalEnterCallback(payload); this.app.sound.playThemeMusic(this.currentState.getThemeMusic()); - this.currentState.internalEnterCallback(payload); this.currentState.onResized(this.app.screenWidth, this.app.screenHeight); this.app.analytics.trackStateEnter(key); diff --git a/src/js/game/base_item.js b/src/js/game/base_item.js index 0075e6c1..d74ff834 100644 --- a/src/js/game/base_item.js +++ b/src/js/game/base_item.js @@ -11,6 +11,7 @@ export const itemTypes = ["shape", "color", "boolean"]; export class BaseItem extends BasicSerializableObject { constructor() { super(); + this._type = this.getItemType(); } static getId() { diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index dde81549..e2f803e1 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -13,8 +13,6 @@ import { GameRoot } from "./root"; const logger = createLogger("belt_path"); // Helpers for more semantic access into interleaved arrays -const _nextDistance = 0; -const _item = 1; const DEBUG = G_IS_DEV && false; @@ -110,6 +108,15 @@ export class BeltPath extends BasicSerializableObject { } } + /** + * Clears all items + */ + clearAllItems() { + this.items = []; + this.spacingToFirstItem = this.totalLength; + this.numCompressedItemsAfterFirstItem = 0; + } + /** * Returns whether this path can accept a new item * @returns {boolean} @@ -174,7 +181,7 @@ export class BeltPath extends BasicSerializableObject { * Recomputes cache variables once the path was changed */ onPathChanged() { - this.acceptorTarget = this.computeAcceptingEntityAndSlot(); + this.boundAcceptor = this.computeAcceptingEntityAndSlot(); /** * How many items past the first item are compressed @@ -192,7 +199,7 @@ export class BeltPath extends BasicSerializableObject { /** * Finds the entity which accepts our items * @param {boolean=} debug_Silent Whether debug output should be silent - * @return {{ entity: Entity, slot: number, direction?: enumDirection }} + * @return { (BaseItem, number) => boolean } */ computeAcceptingEntityAndSlot(debug_Silent = false) { DEBUG && !debug_Silent && logger.log("Recomputing acceptor target"); @@ -214,55 +221,142 @@ export class BeltPath extends BasicSerializableObject { "regular" ); - if (targetEntity) { - DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid); - const targetStaticComp = targetEntity.components.StaticMapEntity; - const targetBeltComp = targetEntity.components.Belt; + if (!targetEntity) { + return; + } + + const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts; - // Check for belts (special case) - if (targetBeltComp) { - const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top); - DEBUG && - !debug_Silent && - logger.log( - " Entity is accepting items from", - ejectSlotWsDirection, - "vs", - beltAcceptingDirection, - "Rotation:", - targetStaticComp.rotation + DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid); + const targetStaticComp = targetEntity.components.StaticMapEntity; + const targetBeltComp = targetEntity.components.Belt; + + // Check for belts (special case) + if (targetBeltComp) { + const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top); + DEBUG && + !debug_Silent && + logger.log( + " Entity is accepting items from", + ejectSlotWsDirection, + "vs", + beltAcceptingDirection, + "Rotation:", + targetStaticComp.rotation + ); + if (ejectSlotWsDirection === beltAcceptingDirection) { + return item => { + const path = targetBeltComp.assignedPath; + assert(path, "belt has no path"); + return path.tryAcceptItem(item); + }; + } + } + + // Check for item acceptors + const targetAcceptorComp = targetEntity.components.ItemAcceptor; + if (!targetAcceptorComp) { + // Entity doesn't accept items + return; + } + + const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection); + const matchingSlot = targetAcceptorComp.findMatchingSlot( + targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile), + ejectingDirection + ); + + if (!matchingSlot) { + // No matching slot found + return; + } + + const matchingSlotIndex = matchingSlot.index; + const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex); + if (!passOver) { + return; + } + + const matchingDirection = enumInvertedDirections[ejectingDirection]; + const filter = matchingSlot.slot.filter; + + return function (item, remainingProgress = 0.0) { + // Check if the acceptor has a filter + if (filter && item._type !== filter) { + return false; + } + + // Try to pass over + if (passOver(item, matchingSlotIndex)) { + // Trigger animation on the acceptor comp + if (noSimplifiedBelts) { + targetAcceptorComp.onItemAccepted( + matchingSlotIndex, + matchingDirection, + item, + remainingProgress ); - if (ejectSlotWsDirection === beltAcceptingDirection) { - return { - entity: targetEntity, - direction: null, - slot: 0, - }; } + return true; } + return false; + }; + } - // Check for item acceptors - const targetAcceptorComp = targetEntity.components.ItemAcceptor; - if (!targetAcceptorComp) { - // Entity doesn't accept items - return; - } + /** + * Computes a method to pass over the item to the entity + * @param {Entity} entity + * @param {number} matchingSlotIndex + * @returns {(item: BaseItem, slotIndex: number) => boolean | void} + */ + computePassOverFunctionWithoutBelts(entity, matchingSlotIndex) { + const systems = this.root.systemMgr.systems; + const hubGoals = this.root.hubGoals; + + // NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS + + const itemProcessorComp = entity.components.ItemProcessor; + if (itemProcessorComp) { + // Its an item processor .. + return function (item) { + // Check for potential filters + if (!systems.itemProcessor.checkRequirements(entity, item, matchingSlotIndex)) { + return; + } + return itemProcessorComp.tryTakeItem(item, matchingSlotIndex); + }; + } - const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection); - const matchingSlot = targetAcceptorComp.findMatchingSlot( - targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile), - ejectingDirection - ); + const undergroundBeltComp = entity.components.UndergroundBelt; + if (undergroundBeltComp) { + // Its an underground belt. yay. + return function (item) { + return undergroundBeltComp.tryAcceptExternalItem( + item, + hubGoals.getUndergroundBeltBaseSpeed() + ); + }; + } - if (!matchingSlot) { - // No matching slot found - return; - } + const storageComp = entity.components.Storage; + if (storageComp) { + // It's a storage + return function (item) { + if (storageComp.canAcceptItem(item)) { + storageComp.takeItem(item); + return true; + } + }; + } - return { - entity: targetEntity, - slot: matchingSlot.index, - direction: enumInvertedDirections[ejectingDirection], + const filterComp = entity.components.Filter; + if (filterComp) { + // It's a filter! Unfortunately the filter has to know a lot about it's + // surrounding state and components, so it can't be within the component itself. + return function (item) { + if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) { + return true; + } }; } } @@ -365,17 +459,17 @@ export class BeltPath extends BasicSerializableObject { for (let i = 0; i < this.items.length; ++i) { const item = this.items[i]; - if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) { + if (item[0 /* nextDistance */] < 0 || item[0 /* nextDistance */] > this.totalLength + 0.02) { return fail( "Item has invalid offset to next item: ", - item[_nextDistance], + item[0 /* nextDistance */], "(total length:", this.totalLength, ")" ); } - currentPos += item[_nextDistance]; + currentPos += item[0 /* nextDistance */]; } // Check the total sum matches @@ -387,7 +481,7 @@ export class BeltPath extends BasicSerializableObject { this.spacingToFirstItem, ") and items does not match total length (", this.totalLength, - ") -> items: " + this.items.map(i => i[_nextDistance]).join("|") + ") -> items: " + this.items.map(i => i[0 /* nextDistance */]).join("|") ); } @@ -399,43 +493,14 @@ export class BeltPath extends BasicSerializableObject { // Check acceptor const acceptor = this.computeAcceptingEntityAndSlot(true); - if (!!acceptor !== !!this.acceptorTarget) { - return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.acceptorTarget); - } - - if (acceptor) { - if (this.acceptorTarget.entity !== acceptor.entity) { - return fail( - "Mismatching entity on acceptor target:", - acceptor.entity.uid, - "vs", - this.acceptorTarget.entity.uid - ); - } - - if (this.acceptorTarget.slot !== acceptor.slot) { - return fail( - "Mismatching entity on acceptor target:", - acceptor.slot, - "vs stored", - this.acceptorTarget.slot - ); - } - - if (this.acceptorTarget.direction !== acceptor.direction) { - return fail( - "Mismatching direction on acceptor target:", - acceptor.direction, - "vs stored", - this.acceptorTarget.direction - ); - } + if (!!acceptor !== !!this.boundAcceptor) { + return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.boundAcceptor); } // Check first nonzero offset let firstNonzero = 0; for (let i = this.items.length - 2; i >= 0; --i) { - if (this.items[i][_nextDistance] < globalConfig.itemSpacingOnBelts + 1e-5) { + if (this.items[i][0 /* nextDistance */] < globalConfig.itemSpacingOnBelts + 1e-5) { ++firstNonzero; } else { break; @@ -483,11 +548,11 @@ export class BeltPath extends BasicSerializableObject { DEBUG && logger.log( " Extended spacing of last item from", - lastItem[_nextDistance], + lastItem[0 /* nextDistance */], "to", - lastItem[_nextDistance] + additionalLength + lastItem[0 /* nextDistance */] + additionalLength ); - lastItem[_nextDistance] += additionalLength; + lastItem[0 /* nextDistance */] += additionalLength; } // Assign reference @@ -618,7 +683,7 @@ export class BeltPath extends BasicSerializableObject { DEBUG && logger.log( "Old items are", - this.items.map(i => i[_nextDistance]) + this.items.map(i => i[0 /* nextDistance */]) ); // Create second path @@ -628,7 +693,7 @@ export class BeltPath extends BasicSerializableObject { let itemPos = this.spacingToFirstItem; for (let i = 0; i < this.items.length; ++i) { const item = this.items[i]; - const distanceToNext = item[_nextDistance]; + const distanceToNext = item[0 /* nextDistance */]; DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); @@ -643,7 +708,7 @@ export class BeltPath extends BasicSerializableObject { // Check if its on the second path (otherwise its on the removed belt and simply lost) if (itemPos >= secondPathStart) { // Put item on second path - secondPath.items.push([distanceToNext, item[_item]]); + secondPath.items.push([distanceToNext, item[1 /* item */]]); DEBUG && logger.log( " Put item to second path @", @@ -672,7 +737,7 @@ export class BeltPath extends BasicSerializableObject { "to", clampedDistanceToNext ); - item[_nextDistance] = clampedDistanceToNext; + item[0 /* nextDistance */] = clampedDistanceToNext; } } @@ -683,13 +748,13 @@ export class BeltPath extends BasicSerializableObject { DEBUG && logger.log( "New items are", - this.items.map(i => i[_nextDistance]) + this.items.map(i => i[0 /* nextDistance */]) ); DEBUG && logger.log( "And second path items are", - secondPath.items.map(i => i[_nextDistance]) + secondPath.items.map(i => i[0 /* nextDistance */]) ); // Adjust our total length @@ -776,9 +841,17 @@ export class BeltPath extends BasicSerializableObject { continue; } - DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]); + DEBUG && + logger.log( + "Item", + i, + "is at", + itemOffset, + "with next offset", + item[0 /* nextDistance */] + ); lastItemOffset = itemOffset; - itemOffset += item[_nextDistance]; + itemOffset += item[0 /* nextDistance */]; } // If we still have an item, make sure the last item matches @@ -805,7 +878,7 @@ export class BeltPath extends BasicSerializableObject { this.totalLength, ")" ); - this.items[this.items.length - 1][_nextDistance] = lastDistance; + this.items[this.items.length - 1][0 /* nextDistance */] = lastDistance; } else { DEBUG && logger.log(" Removed all items so we'll update spacing to total length"); @@ -893,7 +966,7 @@ export class BeltPath extends BasicSerializableObject { DEBUG && logger.log( " Items:", - this.items.map(i => i[_nextDistance]) + this.items.map(i => i[0 /* nextDistance */]) ); // Find offset to first item @@ -912,7 +985,7 @@ export class BeltPath extends BasicSerializableObject { // This item must be dropped this.items.splice(i, 1); i -= 1; - itemOffset += item[_nextDistance]; + itemOffset += item[0 /* nextDistance */]; continue; } else { // This item can be kept, thus its the first we know @@ -990,9 +1063,13 @@ export class BeltPath extends BasicSerializableObject { // Now, update the distance of our last item if (this.items.length !== 0) { const lastItem = this.items[this.items.length - 1]; - lastItem[_nextDistance] += otherPath.spacingToFirstItem; + lastItem[0 /* nextDistance */] += otherPath.spacingToFirstItem; DEBUG && - logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now"); + logger.log( + " Add distance to last item, effectively being", + lastItem[0 /* nextDistance */], + "now" + ); } else { // Seems we have no items, update our first item distance this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem; @@ -1012,7 +1089,7 @@ export class BeltPath extends BasicSerializableObject { // Aaand push the other paths items for (let i = 0; i < otherPath.items.length; ++i) { const item = otherPath.items[i]; - this.items.push([item[_nextDistance], item[_item]]); + this.items.push([item[0 /* nextDistance */], item[1 /* item */]]); } // Update bounds @@ -1046,6 +1123,11 @@ export class BeltPath extends BasicSerializableObject { this.debug_checkIntegrity("pre-update"); } + // Skip empty belts + if (this.items.length === 0) { + return; + } + // Divide by item spacing on belts since we use throughput and not speed let beltSpeed = this.root.hubGoals.getBeltBaseSpeed() * @@ -1074,30 +1156,40 @@ export class BeltPath extends BasicSerializableObject { lastItemProcessed === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts; // Compute how much we can advance - const clampedProgress = Math.max( - 0, - Math.min(remainingVelocity, nextDistanceAndItem[_nextDistance] - minimumSpacing) - ); + let clampedProgress = nextDistanceAndItem[0 /* nextDistance */] - minimumSpacing; + + // Make sure we don't advance more than the remaining velocity has stored + if (remainingVelocity < clampedProgress) { + clampedProgress = remainingVelocity; + } + + // Make sure we don't advance back + if (clampedProgress < 0) { + clampedProgress = 0; + } // Reduce our velocity by the amount we consumed remainingVelocity -= clampedProgress; // Reduce the spacing - nextDistanceAndItem[_nextDistance] -= clampedProgress; + nextDistanceAndItem[0 /* nextDistance */] -= clampedProgress; // Advance all items behind by the progress we made this.spacingToFirstItem += clampedProgress; // If the last item can be ejected, eject it and reduce the spacing, because otherwise // we lose velocity - if (isFirstItemProcessed && nextDistanceAndItem[_nextDistance] < 1e-7) { + if (isFirstItemProcessed && nextDistanceAndItem[0 /* nextDistance */] < 1e-7) { // Store how much velocity we "lost" because we bumped the item to the end of the // belt but couldn't move it any farther. We need this to tell the item acceptor // animation to start a tad later, so everything matches up. Yes I'm a perfectionist. const excessVelocity = beltSpeed - clampedProgress; // Try to directly get rid of the item - if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) { + if ( + this.boundAcceptor && + this.boundAcceptor(nextDistanceAndItem[1 /* item */], excessVelocity) + ) { this.items.pop(); const itemBehind = this.items[lastItemProcessed - 1]; @@ -1108,11 +1200,11 @@ export class BeltPath extends BasicSerializableObject { // Also see #999 const fixupProgress = Math.max( 0, - Math.min(remainingVelocity, itemBehind[_nextDistance]) + Math.min(remainingVelocity, itemBehind[0 /* nextDistance */]) ); // See above - itemBehind[_nextDistance] -= fixupProgress; + itemBehind[0 /* nextDistance */] -= fixupProgress; remainingVelocity -= fixupProgress; this.spacingToFirstItem += fixupProgress; } @@ -1145,8 +1237,8 @@ export class BeltPath extends BasicSerializableObject { // Check if we have an item which is ready to be emitted const lastItem = this.items[this.items.length - 1]; - if (lastItem && lastItem[_nextDistance] === 0 && this.acceptorTarget) { - if (this.tryHandOverItem(lastItem[_item])) { + if (lastItem && lastItem[0 /* nextDistance */] === 0) { + if (this.boundAcceptor && this.boundAcceptor(lastItem[1 /* item */])) { this.items.pop(); this.numCompressedItemsAfterFirstItem = Math.max( 0, @@ -1160,50 +1252,6 @@ export class BeltPath extends BasicSerializableObject { } } - /** - * Tries to hand over the item to the end entity - * @param {BaseItem} item - */ - tryHandOverItem(item, remainingProgress = 0.0) { - if (!this.acceptorTarget) { - return; - } - - const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor; - - // Check if the acceptor has a filter for example - if (targetAcceptorComp && !targetAcceptorComp.canAcceptItem(this.acceptorTarget.slot, item)) { - // Well, this item is not accepted - return false; - } - - // Try to pass over - if ( - this.root.systemMgr.systems.itemEjector.tryPassOverItem( - item, - this.acceptorTarget.entity, - this.acceptorTarget.slot - ) - ) { - // Trigger animation on the acceptor comp - const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor; - if (targetAcceptorComp) { - if (!this.root.app.settings.getAllSettings().simplifiedBelts) { - targetAcceptorComp.onItemAccepted( - this.acceptorTarget.slot, - this.acceptorTarget.direction, - item, - remainingProgress - ); - } - } - - return true; - } - - return false; - } - /** * Computes a world space position from the given progress * @param {number} progress @@ -1270,11 +1318,11 @@ export class BeltPath extends BasicSerializableObject { parameters.context.font = "6px GameFont"; parameters.context.fillStyle = "#111"; parameters.context.fillText( - "" + round4Digits(nextDistanceAndItem[_nextDistance]), + "" + round4Digits(nextDistanceAndItem[0 /* nextDistance */]), worldPos.x + 5, worldPos.y + 2 ); - progress += nextDistanceAndItem[_nextDistance]; + progress += nextDistanceAndItem[0 /* nextDistance */]; if (this.items.length - 1 - this.numCompressedItemsAfterFirstItem === i) { parameters.context.fillStyle = "red"; @@ -1370,7 +1418,7 @@ export class BeltPath extends BasicSerializableObject { const centerPos = staticComp.localTileToWorld(centerPosLocal).toWorldSpaceCenterOfTile(); parameters.context.globalAlpha = 0.5; - firstItem[_item].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters); + firstItem[1 /* item */].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters); parameters.context.globalAlpha = 1; } @@ -1402,7 +1450,7 @@ export class BeltPath extends BasicSerializableObject { const distanceAndItem = this.items[currentItemIndex]; - distanceAndItem[_item].drawItemCenteredClipped( + distanceAndItem[1 /* item */].drawItemCenteredClipped( worldPos.x, worldPos.y, parameters, @@ -1410,7 +1458,7 @@ export class BeltPath extends BasicSerializableObject { ); // Check for the next item - currentItemPos += distanceAndItem[_nextDistance]; + currentItemPos += distanceAndItem[0 /* nextDistance */]; ++currentItemIndex; if (currentItemIndex >= this.items.length) { diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index 3aaef831..3e7cdaa6 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -149,29 +149,31 @@ export class Blueprint { */ tryPlace(root, tile) { return root.logic.performBulkOperation(() => { - let count = 0; - for (let i = 0; i < this.entities.length; ++i) { - const entity = this.entities[i]; - if (!root.logic.checkCanPlaceEntity(entity, tile)) { - continue; + return root.logic.performImmutableOperation(() => { + let count = 0; + for (let i = 0; i < this.entities.length; ++i) { + const entity = this.entities[i]; + if (!root.logic.checkCanPlaceEntity(entity, tile)) { + continue; + } + + const clone = entity.clone(); + clone.components.StaticMapEntity.origin.addInplace(tile); + root.logic.freeEntityAreaBeforeBuild(clone); + root.map.placeStaticEntity(clone); + root.entityMgr.registerEntity(clone); + count++; } - const clone = entity.clone(); - clone.components.StaticMapEntity.origin.addInplace(tile); - root.logic.freeEntityAreaBeforeBuild(clone); - root.map.placeStaticEntity(clone); - root.entityMgr.registerEntity(clone); - count++; - } - - root.signals.bulkAchievementCheck.dispatch( - ACHIEVEMENTS.placeBlueprint, - count, - ACHIEVEMENTS.placeBp1000, - count - ); + root.signals.bulkAchievementCheck.dispatch( + ACHIEVEMENTS.placeBlueprint, + count, + ACHIEVEMENTS.placeBp1000, + count + ); - return count !== 0; + return count !== 0; + }); }); } } diff --git a/src/js/game/buildings/balancer.js b/src/js/game/buildings/balancer.js index 2f14e36c..38a568e1 100644 --- a/src/js/game/buildings/balancer.js +++ b/src/js/game/buildings/balancer.js @@ -66,6 +66,10 @@ export class MetaBalancerBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } + let speedMultiplier = 2; switch (variant) { case enumBalancerVariants.merger: @@ -88,9 +92,11 @@ export class MetaBalancerBuilding extends MetaBuilding { * @param {GameRoot} root */ getAvailableVariants(root) { - let available = [defaultBuildingVariant]; + const deterministic = root.gameMode.getIsDeterministic(); + + let available = deterministic ? [] : [defaultBuildingVariant]; - if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) { + if (!deterministic && root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) { available.push(enumBalancerVariants.merger, enumBalancerVariants.mergerInverse); } diff --git a/src/js/game/buildings/belt.js b/src/js/game/buildings/belt.js index 84646b19..f4e31ba9 100644 --- a/src/js/game/buildings/belt.js +++ b/src/js/game/buildings/belt.js @@ -55,6 +55,9 @@ export class MetaBeltBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } const beltSpeed = root.hubGoals.getBeltBaseSpeed(); return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]]; } diff --git a/src/js/game/buildings/block.js b/src/js/game/buildings/block.js new file mode 100644 index 00000000..d6499648 --- /dev/null +++ b/src/js/game/buildings/block.js @@ -0,0 +1,30 @@ +/* typehints:start */ +import { Entity } from "../entity"; +/* typehints:end */ + +import { MetaBuilding } from "../meta_building"; + +export class MetaBlockBuilding extends MetaBuilding { + constructor() { + super("block"); + } + + getSilhouetteColor() { + return "#333"; + } + + /** + * + * @param {import("../../savegame/savegame_serializer").GameRoot} root + * @returns + */ + getIsRemovable(root) { + return root.gameMode.getIsEditor(); + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) {} +} diff --git a/src/js/game/buildings/constant_producer.js b/src/js/game/buildings/constant_producer.js new file mode 100644 index 00000000..1b08eeb7 --- /dev/null +++ b/src/js/game/buildings/constant_producer.js @@ -0,0 +1,50 @@ +/* typehints:start */ +import { Entity } from "../entity"; +/* typehints:end */ + +import { enumDirection, Vector } from "../../core/vector"; +import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { enumItemProducerType, ItemProducerComponent } from "../components/item_producer"; +import { MetaBuilding } from "../meta_building"; + +export class MetaConstantProducerBuilding extends MetaBuilding { + constructor() { + super("constant_producer"); + } + + getSilhouetteColor() { + return "#bfd630"; + } + + /** + * + * @param {import("../../savegame/savegame_serializer").GameRoot} root + * @returns + */ + getIsRemovable(root) { + return root.gameMode.getIsEditor(); + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new ItemEjectorComponent({ + slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }], + }) + ); + entity.addComponent( + new ItemProducerComponent({ + type: enumItemProducerType.wireless, + }) + ); + entity.addComponent( + new ConstantSignalComponent({ + type: enumConstantSignalType.wireless, + }) + ); + } +} diff --git a/src/js/game/buildings/cutter.js b/src/js/game/buildings/cutter.js index 7dee4449..37264c9d 100644 --- a/src/js/game/buildings/cutter.js +++ b/src/js/game/buildings/cutter.js @@ -38,6 +38,9 @@ export class MetaCutterBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } const speed = root.hubGoals.getProcessorBaseSpeed( variant === enumCutterVariants.quad ? enumItemProcessorTypes.cutterQuad diff --git a/src/js/game/buildings/filter.js b/src/js/game/buildings/filter.js index 2d81ce83..08296853 100644 --- a/src/js/game/buildings/filter.js +++ b/src/js/game/buildings/filter.js @@ -40,6 +40,9 @@ export class MetaFilterBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } const beltSpeed = root.hubGoals.getBeltBaseSpeed(); return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]]; } diff --git a/src/js/game/buildings/goal_acceptor.js b/src/js/game/buildings/goal_acceptor.js new file mode 100644 index 00000000..d3b79c97 --- /dev/null +++ b/src/js/game/buildings/goal_acceptor.js @@ -0,0 +1,56 @@ +/* typehints:start */ +import { Entity } from "../entity"; +/* typehints:end */ + +import { enumDirection, Vector } from "../../core/vector"; +import { enumBeltReaderType, BeltReaderComponent } from "../components/belt_reader"; +import { GoalAcceptorComponent } from "../components/goal_acceptor"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; +import { MetaBuilding } from "../meta_building"; + +export class MetaGoalAcceptorBuilding extends MetaBuilding { + constructor() { + super("goal_acceptor"); + } + + getSilhouetteColor() { + return "#ce418a"; + } + + /** + * + * @param {import("../../savegame/savegame_serializer").GameRoot} root + * @returns + */ + getIsRemovable(root) { + return root.gameMode.getIsEditor(); + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new ItemAcceptorComponent({ + slots: [ + { + pos: new Vector(0, 0), + directions: [enumDirection.bottom], + filter: "shape", + }, + ], + }) + ); + + entity.addComponent( + new ItemProcessorComponent({ + processorType: enumItemProcessorTypes.goal, + }) + ); + + entity.addComponent(new GoalAcceptorComponent({})); + } +} diff --git a/src/js/game/buildings/item_producer.js b/src/js/game/buildings/item_producer.js index 477ed603..1140c8f1 100644 --- a/src/js/game/buildings/item_producer.js +++ b/src/js/game/buildings/item_producer.js @@ -39,6 +39,6 @@ export class MetaItemProducerBuilding extends MetaBuilding { ], }) ); - entity.addComponent(new ItemProducerComponent()); + entity.addComponent(new ItemProducerComponent({})); } } diff --git a/src/js/game/buildings/miner.js b/src/js/game/buildings/miner.js index f0b837a1..473aa262 100644 --- a/src/js/game/buildings/miner.js +++ b/src/js/game/buildings/miner.js @@ -31,6 +31,9 @@ export class MetaMinerBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } const speed = root.hubGoals.getMinerBaseSpeed(); return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } diff --git a/src/js/game/buildings/mixer.js b/src/js/game/buildings/mixer.js index cbde309e..e572bbba 100644 --- a/src/js/game/buildings/mixer.js +++ b/src/js/game/buildings/mixer.js @@ -35,6 +35,9 @@ export class MetaMixerBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.mixer); return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } diff --git a/src/js/game/buildings/painter.js b/src/js/game/buildings/painter.js index 6e941403..e7a0b72d 100644 --- a/src/js/game/buildings/painter.js +++ b/src/js/game/buildings/painter.js @@ -46,6 +46,9 @@ export class MetaPainterBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } switch (variant) { case defaultBuildingVariant: case enumPainterVariants.mirrored: { @@ -71,7 +74,10 @@ export class MetaPainterBuilding extends MetaBuilding { if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) { variants.push(enumPainterVariants.double); } - if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers)) { + if ( + root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) && + root.gameMode.getSupportsWires() + ) { variants.push(enumPainterVariants.quad); } return variants; diff --git a/src/js/game/buildings/reader.js b/src/js/game/buildings/reader.js index 006d6582..62207564 100644 --- a/src/js/game/buildings/reader.js +++ b/src/js/game/buildings/reader.js @@ -110,6 +110,6 @@ export class MetaReaderBuilding extends MetaBuilding { }) ); - entity.addComponent(new BeltReaderComponent()); + entity.addComponent(new BeltReaderComponent({})); } } diff --git a/src/js/game/buildings/rotater.js b/src/js/game/buildings/rotater.js index 7df94d16..f24fee14 100644 --- a/src/js/game/buildings/rotater.js +++ b/src/js/game/buildings/rotater.js @@ -48,6 +48,9 @@ export class MetaRotaterBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } switch (variant) { case defaultBuildingVariant: { const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater); diff --git a/src/js/game/buildings/stacker.js b/src/js/game/buildings/stacker.js index 40a9c5ae..6b70365d 100644 --- a/src/js/game/buildings/stacker.js +++ b/src/js/game/buildings/stacker.js @@ -28,6 +28,9 @@ export class MetaStackerBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { + if (root.gameMode.throughputDoesNotMatter()) { + return []; + } const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.stacker); return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } diff --git a/src/js/game/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 2761443d..12e887c9 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -72,13 +72,21 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { globalConfig.undergroundBeltMaxTilesByTier[enumUndergroundBeltVariantToTier[variant]]; const beltSpeed = root.hubGoals.getUndergroundBeltBaseSpeed(); - return [ + + /** @type {Array<[string, string]>} */ + const stats = [ [ T.ingame.buildingPlacement.infoTexts.range, T.ingame.buildingPlacement.infoTexts.tiles.replace("", "" + rangeTiles), ], - [T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)], ]; + + if (root.gameMode.throughputDoesNotMatter()) { + return stats; + } + stats.push([T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]); + + return stats; } /** diff --git a/src/js/game/camera.js b/src/js/game/camera.js index 107d1fb4..fc90f4de 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -392,13 +392,20 @@ export class Camera extends BasicSerializableObject { return rect.containsPoint(point.x, point.y); } + getMaximumZoom() { + return this.root.gameMode.getMaximumZoom(); + } + + getMinimumZoom() { + return this.root.gameMode.getMinimumZoom(); + } + /** * Returns if we can further zoom in * @returns {boolean} */ canZoomIn() { - const maxLevel = this.root.app.platformWrapper.getMaximumZoom(); - return this.zoomLevel <= maxLevel - 0.01; + return this.zoomLevel <= this.getMaximumZoom() - 0.01; } /** @@ -406,8 +413,7 @@ export class Camera extends BasicSerializableObject { * @returns {boolean} */ canZoomOut() { - const minLevel = this.root.app.platformWrapper.getMinimumZoom(); - return this.zoomLevel >= minLevel + 0.01; + return this.zoomLevel >= this.getMinimumZoom() + 0.01; } // EVENTS @@ -468,6 +474,7 @@ export class Camera extends BasicSerializableObject { // Clamp everything afterwards this.clampZoomLevel(); + this.clampToBounds(); return false; } @@ -743,15 +750,27 @@ export class Camera extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { return; } - const wrapper = this.root.app.platformWrapper; - assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); - this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + this.zoomLevel = clamp(this.zoomLevel, this.getMinimumZoom(), this.getMaximumZoom()); assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel); if (this.desiredZoom) { - this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + this.desiredZoom = clamp(this.desiredZoom, this.getMinimumZoom(), this.getMaximumZoom()); + } + } + + /** + * Clamps the center within set boundaries + */ + clampToBounds() { + const bounds = this.root.gameMode.getCameraBounds(); + if (!bounds) { + return; } + + const tileScaleBounds = this.root.gameMode.getCameraBounds().allScaled(globalConfig.tileSize); + this.center.x = clamp(this.center.x, tileScaleBounds.x, tileScaleBounds.x + tileScaleBounds.w); + this.center.y = clamp(this.center.y, tileScaleBounds.y, tileScaleBounds.y + tileScaleBounds.h); } /** @@ -857,6 +876,7 @@ export class Camera extends BasicSerializableObject { // Panning this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); + this.clampToBounds(); } } @@ -921,6 +941,8 @@ export class Camera extends BasicSerializableObject { ((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed() ) ); + + this.clampToBounds(); } /** @@ -1006,6 +1028,8 @@ export class Camera extends BasicSerializableObject { this.center.x += moveAmount * forceX * movementSpeed; this.center.y += moveAmount * forceY * movementSpeed; + + this.clampToBounds(); } } } diff --git a/src/js/game/component.js b/src/js/game/component.js index 46b1b545..cff14d62 100644 --- a/src/js/game/component.js +++ b/src/js/game/component.js @@ -23,6 +23,11 @@ export class Component extends BasicSerializableObject { */ copyAdditionalStateTo(otherComponent) {} + /** + * Clears all items and state + */ + clear() {} + /* dev:start */ /** diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index f094e60d..9c9247e6 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display"; import { BeltReaderComponent } from "./components/belt_reader"; import { FilterComponent } from "./components/filter"; import { ItemProducerComponent } from "./components/item_producer"; +import { GoalAcceptorComponent } from "./components/goal_acceptor"; export function initComponentRegistry() { gComponentRegistry.register(StaticMapEntityComponent); @@ -41,6 +42,7 @@ export function initComponentRegistry() { gComponentRegistry.register(BeltReaderComponent); gComponentRegistry.register(FilterComponent); gComponentRegistry.register(ItemProducerComponent); + gComponentRegistry.register(GoalAcceptorComponent); // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index 138c4775..3144ad96 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -57,6 +57,12 @@ export class BeltComponent extends Component { this.assignedPath = null; } + clear() { + if (this.assignedPath) { + this.assignedPath.clearAllItems(); + } + } + /** * Returns the effective length of this belt in tile space * @returns {number} diff --git a/src/js/game/components/belt_reader.js b/src/js/game/components/belt_reader.js index d451bab5..5a33db29 100644 --- a/src/js/game/components/belt_reader.js +++ b/src/js/game/components/belt_reader.js @@ -3,6 +3,12 @@ import { BaseItem } from "../base_item"; import { typeItemSingleton } from "../item_resolver"; import { types } from "../../savegame/serialization"; +/** @enum {string} */ +export const enumBeltReaderType = { + wired: "wired", + wireless: "wireless", +}; + export class BeltReaderComponent extends Component { static getId() { return "BeltReader"; @@ -10,13 +16,24 @@ export class BeltReaderComponent extends Component { static getSchema() { return { + type: types.string, lastItem: types.nullable(typeItemSingleton), }; } - constructor() { + /** + * @param {object} param0 + * @param {string=} param0.type + */ + constructor({ type = enumBeltReaderType.wired }) { super(); + this.type = type; + + this.clear(); + } + + clear() { /** * Which items went through the reader, we only store the time * @type {Array} @@ -41,4 +58,8 @@ export class BeltReaderComponent extends Component { */ this.lastThroughputComputation = 0; } + + isWireless() { + return this.type === enumBeltReaderType.wireless; + } } diff --git a/src/js/game/components/constant_signal.js b/src/js/game/components/constant_signal.js index 286108be..d2186bda 100644 --- a/src/js/game/components/constant_signal.js +++ b/src/js/game/components/constant_signal.js @@ -4,6 +4,12 @@ import { Component } from "../component"; import { BaseItem } from "../base_item"; import { typeItemSingleton } from "../item_resolver"; +/** @enum {string} */ +export const enumConstantSignalType = { + wired: "wired", + wireless: "wireless", +}; + export class ConstantSignalComponent extends Component { static getId() { return "ConstantSignal"; @@ -11,6 +17,7 @@ export class ConstantSignalComponent extends Component { static getSchema() { return { + type: types.string, signal: types.nullable(typeItemSingleton), }; } @@ -21,15 +28,22 @@ export class ConstantSignalComponent extends Component { */ copyAdditionalStateTo(otherComponent) { otherComponent.signal = this.signal; + otherComponent.type = this.type; } /** * * @param {object} param0 + * @param {string=} param0.type * @param {BaseItem=} param0.signal The signal to store */ - constructor({ signal = null }) { + constructor({ signal = null, type = enumConstantSignalType.wired }) { super(); this.signal = signal; + this.type = type; + } + + isWireless() { + return this.type === enumConstantSignalType.wireless; } } diff --git a/src/js/game/components/filter.js b/src/js/game/components/filter.js index cffee969..8a22a076 100644 --- a/src/js/game/components/filter.js +++ b/src/js/game/components/filter.js @@ -40,6 +40,10 @@ export class FilterComponent extends Component { constructor() { super(); + this.clear(); + } + + clear() { /** * Items in queue to leave through * @type {Array} diff --git a/src/js/game/components/goal_acceptor.js b/src/js/game/components/goal_acceptor.js new file mode 100644 index 00000000..87c55501 --- /dev/null +++ b/src/js/game/components/goal_acceptor.js @@ -0,0 +1,49 @@ +import { globalConfig } from "../../core/config"; +import { BaseItem } from "../base_item"; +import { Component } from "../component"; +import { typeItemSingleton } from "../item_resolver"; + +export class GoalAcceptorComponent extends Component { + static getId() { + return "GoalAcceptor"; + } + + static getSchema() { + return { + item: typeItemSingleton, + }; + } + + /** + * @param {object} param0 + * @param {BaseItem=} param0.item + * @param {number=} param0.rate + */ + constructor({ item = null, rate = null }) { + super(); + + // ths item to produce + /** @type {BaseItem | undefined} */ + this.item = item; + + this.clear(); + } + + clear() { + // the last items we delivered + /** @type {{ item: BaseItem; time: number; }[]} */ + this.deliveryHistory = []; + + // Used for animations + this.displayPercentage = 0; + } + + getRequiredDeliveryHistorySize() { + return ( + (globalConfig.puzzleModeSpeed * + globalConfig.goalAcceptorMinimumDurationSeconds * + globalConfig.beltSpeedItemsPerSecond) / + globalConfig.goalAcceptorsPerProducer + ); + } +} diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index 7dbd9677..354f9024 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -36,6 +36,11 @@ export class ItemAcceptorComponent extends Component { constructor({ slots = [] }) { super(); + this.setSlots(slots); + this.clear(); + } + + clear() { /** * Fixes belt animations * @type {Array<{ @@ -46,8 +51,6 @@ export class ItemAcceptorComponent extends Component { * }>} */ this.itemConsumptionAnimations = []; - - this.setSlots(slots); } /** @@ -71,6 +74,8 @@ export class ItemAcceptorComponent extends Component { /** * Returns if this acceptor can accept a new item at slot N + * + * NOTICE: The belt path ignores this for performance reasons and does his own check * @param {number} slotIndex * @param {BaseItem=} item */ diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index 47253b4b..719925af 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -48,6 +48,13 @@ export class ItemEjectorComponent extends Component { this.renderFloatingItems = renderFloatingItems; } + clear() { + for (const slot of this.slots) { + slot.item = null; + slot.progress = 0; + } + } + /** * @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on */ diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index fd466662..4c0e1835 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -19,6 +19,7 @@ export const enumItemProcessorTypes = { hub: "hub", filter: "filter", reader: "reader", + goal: "goal", }; /** @enum {string} */ @@ -63,10 +64,8 @@ export class ItemProcessorComponent extends Component { }) { super(); - // Which slot to emit next, this is only a preference and if it can't emit - // it will take the other one. Some machines ignore this (e.g. the balancer) to make - // sure the outputs always match - this.nextOutputSlot = 0; + // How many inputs we need for one charge + this.inputsPerCharge = inputsPerCharge; // Type of the processor this.type = processorType; @@ -74,8 +73,14 @@ export class ItemProcessorComponent extends Component { // Type of processing requirement this.processingRequirement = processingRequirement; - // How many inputs we need for one charge - this.inputsPerCharge = inputsPerCharge; + this.clear(); + } + + clear() { + // Which slot to emit next, this is only a preference and if it can't emit + // it will take the other one. Some machines ignore this (e.g. the balancer) to make + // sure the outputs always match + this.nextOutputSlot = 0; /** * Our current inputs @@ -104,7 +109,11 @@ export class ItemProcessorComponent extends Component { * @param {number} sourceSlot */ tryTakeItem(item, sourceSlot) { - if (this.type === enumItemProcessorTypes.hub || this.type === enumItemProcessorTypes.trash) { + if ( + this.type === enumItemProcessorTypes.hub || + this.type === enumItemProcessorTypes.trash || + this.type === enumItemProcessorTypes.goal + ) { // Hub has special logic .. not really nice but efficient. this.inputSlots.push({ item, sourceSlot }); return true; diff --git a/src/js/game/components/item_producer.js b/src/js/game/components/item_producer.js index ef3571e2..4cb36132 100644 --- a/src/js/game/components/item_producer.js +++ b/src/js/game/components/item_producer.js @@ -1,7 +1,33 @@ +import { types } from "../../savegame/serialization"; import { Component } from "../component"; +/** @enum {string} */ +export const enumItemProducerType = { + wired: "wired", + wireless: "wireless", +}; + export class ItemProducerComponent extends Component { static getId() { return "ItemProducer"; } + + static getSchema() { + return { + type: types.string, + }; + } + + /** + * @param {object} param0 + * @param {string=} param0.type + */ + constructor({ type = enumItemProducerType.wired }) { + super(); + this.type = type; + } + + isWireless() { + return this.type === enumItemProducerType.wireless; + } } diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index ab87760f..5321ae11 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -24,13 +24,6 @@ export class MinerComponent extends Component { this.lastMiningTime = 0; this.chainable = chainable; - /** - * Stores items from other miners which were chained to this - * miner. - * @type {Array} - */ - this.itemChainBuffer = []; - /** * @type {BaseItem} */ @@ -42,6 +35,17 @@ export class MinerComponent extends Component { * @type {Entity|null|false} */ this.cachedChainedMiner = null; + + this.clear(); + } + + clear() { + /** + * Stores items from other miners which were chained to this + * miner. + * @type {Array} + */ + this.itemChainBuffer = []; } /** diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index 7e2f5314..c76a298e 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -71,6 +71,14 @@ export class StaticMapEntityComponent extends Component { return getBuildingDataFromCode(this.code).variant; } + /** + * Returns the buildings rotation variant + * @returns {number} + */ + getRotationVariant() { + return getBuildingDataFromCode(this.code).rotationVariant; + } + /** * Copy the current state to another component * @param {Component} otherComponent diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index a3e883ec..2b744edd 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -41,6 +41,17 @@ export class UndergroundBeltComponent extends Component { this.mode = mode; this.tier = tier; + /** + * The linked entity, used to speed up performance. This contains either + * the entrance or exit depending on the tunnel type + * @type {LinkedUndergroundBelt} + */ + this.cachedLinkedEntity = null; + + this.clear(); + } + + clear() { /** @type {Array<{ item: BaseItem, progress: number }>} */ this.consumptionAnimations = []; @@ -51,13 +62,6 @@ export class UndergroundBeltComponent extends Component { * @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item] */ this.pendingItems = []; - - /** - * The linked entity, used to speed up performance. This contains either - * the entrance or exit depending on the tunnel type - * @type {LinkedUndergroundBelt} - */ - this.cachedLinkedEntity = null; } /** diff --git a/src/js/game/core.js b/src/js/game/core.js index f4b3e9ee..a0ee3713 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -31,7 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper"; import { GameLogic } from "./logic"; import { MapView } from "./map_view"; import { defaultBuildingVariant } from "./meta_building"; -import { RegularGameMode } from "./modes/regular"; +import { GameMode } from "./game_mode"; import { ProductionAnalytics } from "./production_analytics"; import { GameRoot } from "./root"; import { ShapeDefinitionManager } from "./shape_definition_manager"; @@ -82,7 +82,9 @@ export class GameCore { * @param {import("../states/ingame").InGameState} parentState * @param {Savegame} savegame */ - initializeRoot(parentState, savegame) { + initializeRoot(parentState, savegame, gameModeId) { + logger.log("initializing root"); + // Construct the root element, this is the data representation of the game this.root = new GameRoot(this.app); this.root.gameState = parentState; @@ -100,12 +102,12 @@ export class GameCore { // This isn't nice, but we need it right here root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); + // Init game mode + root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters); + // Needs to come first root.dynamicTickrate = new DynamicTickrate(root); - // Init game mode - root.gameMode = new RegularGameMode(root); - // Init classes root.camera = new Camera(root); root.map = new MapView(root); @@ -157,6 +159,8 @@ export class GameCore { } }); } + + logger.log("root initialized"); } /** @@ -168,6 +172,10 @@ export class GameCore { this.root.gameIsFresh = true; this.root.map.seed = randomInt(0, 100000); + if (!this.root.gameMode.hasHub()) { + return; + } + // Place the hub const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({ root: this.root, @@ -447,7 +455,9 @@ export class GameCore { systems.hub.draw(params); // Green wires overlay - root.hud.parts.wiresOverlay.draw(params); + if (root.hud.parts.wiresOverlay) { + root.hud.parts.wiresOverlay.draw(params); + } if (this.root.currentLayer === "wires") { // Static map entities diff --git a/src/js/game/dynamic_tickrate.js b/src/js/game/dynamic_tickrate.js index 3e29aba3..c76fa2e1 100644 --- a/src/js/game/dynamic_tickrate.js +++ b/src/js/game/dynamic_tickrate.js @@ -23,10 +23,16 @@ export class DynamicTickrate { this.averageFps = 60; - this.setTickRate(this.root.app.settings.getDesiredFps()); - - if (G_IS_DEV && globalConfig.debug.renderForTrailer) { - this.setTickRate(300); + const fixedRate = this.root.gameMode.getFixedTickrate(); + if (fixedRate) { + logger.log("Setting fixed tickrate of", fixedRate); + this.setTickRate(fixedRate); + } else { + this.setTickRate(this.root.app.settings.getDesiredFps()); + + if (G_IS_DEV && globalConfig.debug.renderForTrailer) { + this.setTickRate(300); + } } } @@ -99,9 +105,7 @@ export class DynamicTickrate { this.averageTickDuration = average; - const desiredFps = this.root.app.settings.getDesiredFps(); - - // Disabled for now: Dynamicall adjusting tick rate + // Disabled for now: Dynamically adjusting tick rate // if (this.averageFps > desiredFps * 0.9) { // // if (average < maxTickDuration) { // this.increaseTickRate(); diff --git a/src/js/game/entity_components.js b/src/js/game/entity_components.js index 7dee590a..163be9f9 100644 --- a/src/js/game/entity_components.js +++ b/src/js/game/entity_components.js @@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display"; import { BeltReaderComponent } from "./components/belt_reader"; import { FilterComponent } from "./components/filter"; import { ItemProducerComponent } from "./components/item_producer"; +import { GoalAcceptorComponent } from "./components/goal_acceptor"; /* typehints:end */ /** @@ -89,6 +90,9 @@ export class EntityComponentStorage { /** @type {ItemProducerComponent} */ this.ItemProducer; + /** @type {GoalAcceptorComponent} */ + this.GoalAcceptor; + /* typehints:end */ } } diff --git a/src/js/game/game_mode.js b/src/js/game/game_mode.js index 15403eb5..8abeafba 100644 --- a/src/js/game/game_mode.js +++ b/src/js/game/game_mode.js @@ -1,71 +1,192 @@ /* typehints:start */ -import { enumHubGoalRewards } from "./tutorial_goals"; +import { GameRoot } from "./root"; /* typehints:end */ -import { GameRoot } from "./root"; +import { Rectangle } from "../core/rectangle"; +import { gGameModeRegistry } from "../core/global_registries"; +import { types, BasicSerializableObject } from "../savegame/serialization"; +import { MetaBuilding } from "./meta_building"; +import { MetaItemProducerBuilding } from "./buildings/item_producer"; +import { BaseHUDPart } from "./hud/base_hud_part"; -/** @typedef {{ - * shape: string, - * amount: number - * }} UpgradeRequirement */ +/** @enum {string} */ +export const enumGameModeIds = { + puzzleEdit: "puzzleEditMode", + puzzlePlay: "puzzlePlayMode", + regular: "regularMode", +}; -/** @typedef {{ - * required: Array - * improvement?: number, - * excludePrevious?: boolean - * }} TierRequirement */ +/** @enum {string} */ +export const enumGameModeTypes = { + default: "defaultModeType", + puzzle: "puzzleModeType", +}; -/** @typedef {Array} UpgradeTiers */ +export class GameMode extends BasicSerializableObject { + /** @returns {string} */ + static getId() { + abstract; + return "unknownMode"; + } -/** @typedef {{ - * shape: string, - * required: number, - * reward: enumHubGoalRewards, - * throughputOnly?: boolean - * }} LevelDefinition */ + /** @returns {string} */ + static getType() { + abstract; + return "unknownType"; + } + /** + * @param {GameRoot} root + * @param {string} [id=Regular] + * @param {object|undefined} payload + */ + static create(root, id = enumGameModeIds.regular, payload = undefined) { + return new (gGameModeRegistry.findById(id))(root, payload); + } -export class GameMode { /** - * * @param {GameRoot} root */ constructor(root) { + super(); this.root = root; + + /** + * @type {Record} + */ + this.additionalHudParts = {}; + + /** @type {typeof MetaBuilding[]} */ + this.hiddenBuildings = [MetaItemProducerBuilding]; + } + + /** @returns {object} */ + serialize() { + return { + $: this.getId(), + data: super.serialize(), + }; + } + + /** @param {object} savedata */ + deserialize({ data }) { + super.deserialize(data, this.root); + } + + /** @returns {string} */ + getId() { + // @ts-ignore + return this.constructor.getId(); + } + + /** @returns {string} */ + getType() { + // @ts-ignore + return this.constructor.getType(); } /** - * Should return all available upgrades - * @returns {Object} + * @param {typeof MetaBuilding} building - Class name of building + * @returns {boolean} */ + isBuildingExcluded(building) { + return this.hiddenBuildings.indexOf(building) >= 0; + } + + /** @returns {undefined|Rectangle[]} */ + getBuildableZones() { + return; + } + + /** @returns {Rectangle|undefined} */ + getCameraBounds() { + return; + } + + /** @returns {boolean} */ + hasHub() { + return true; + } + + /** @returns {boolean} */ + hasResources() { + return true; + } + + /** @returns {number} */ + getMinimumZoom() { + return 0.1; + } + + /** @returns {number} */ + getMaximumZoom() { + return 3.5; + } + + /** @returns {Object} */ getUpgrades() { - abstract; - return null; + return { + belt: [], + miner: [], + processors: [], + painting: [], + }; + } + + throughputDoesNotMatter() { + return false; } /** - * Returns the blueprint shape key - * @returns {string} + * @param {number} w + * @param {number} h */ - getBlueprintShapeKey() { + adjustZone(w = 0, h = 0) { abstract; - return null; + return; } - /** - * Returns the goals for all levels including their reward - * @returns {Array} - */ + /** @returns {array} */ getLevelDefinitions() { - abstract; - return null; + return []; } - /** - * Should return whether free play is available or if the game stops - * after the predefined levels - * @returns {boolean} - */ + /** @returns {boolean} */ getIsFreeplayAvailable() { + return false; + } + + /** @returns {boolean} */ + getIsSaveable() { + return true; + } + + /** @returns {boolean} */ + getSupportsCopyPaste() { return true; } + + /** @returns {boolean} */ + getSupportsWires() { + return true; + } + + /** @returns {boolean} */ + getIsEditor() { + return false; + } + + /** @returns {boolean} */ + getIsDeterministic() { + return false; + } + + /** @returns {number | undefined} */ + getFixedTickrate() { + return; + } + + /** @returns {string} */ + getBlueprintShapeKey() { + return "CbCbCbRb:CwCwCwCw"; + } } diff --git a/src/js/game/game_mode_registry.js b/src/js/game/game_mode_registry.js new file mode 100644 index 00000000..03daceb0 --- /dev/null +++ b/src/js/game/game_mode_registry.js @@ -0,0 +1,10 @@ +import { gGameModeRegistry } from "../core/global_registries"; +import { PuzzleEditGameMode } from "./modes/puzzle_edit"; +import { PuzzlePlayGameMode } from "./modes/puzzle_play"; +import { RegularGameMode } from "./modes/regular"; + +export function initGameModeRegistry() { + gGameModeRegistry.register(PuzzleEditGameMode); + gGameModeRegistry.register(PuzzlePlayGameMode); + gGameModeRegistry.register(RegularGameMode); +} diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index 74ba798f..08609f89 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -24,6 +24,9 @@ import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays"; import { BeltReaderSystem } from "./systems/belt_reader"; import { FilterSystem } from "./systems/filter"; import { ItemProducerSystem } from "./systems/item_producer"; +import { ConstantProducerSystem } from "./systems/constant_producer"; +import { GoalAcceptorSystem } from "./systems/goal_acceptor"; +import { ZoneSystem } from "./systems/zone"; const logger = createLogger("game_system_manager"); @@ -100,6 +103,15 @@ export class GameSystemManager { /** @type {ItemProducerSystem} */ itemProducer: null, + /** @type {ConstantProducerSystem} */ + ConstantProducer: null, + + /** @type {GoalAcceptorSystem} */ + GoalAcceptor: null, + + /** @type {ZoneSystem} */ + zone: null, + /* typehints:end */ }; this.systemUpdateOrder = []; @@ -138,7 +150,9 @@ export class GameSystemManager { add("itemEjector", ItemEjectorSystem); - add("mapResources", MapResourcesSystem); + if (this.root.gameMode.hasResources()) { + add("mapResources", MapResourcesSystem); + } add("hub", HubSystem); @@ -165,6 +179,14 @@ export class GameSystemManager { add("itemProcessorOverlays", ItemProcessorOverlaysSystem); + add("constantProducer", ConstantProducerSystem); + + add("goalAcceptor", GoalAcceptorSystem); + + if (this.root.gameMode.getBuildableZones()) { + add("zone", ZoneSystem); + } + 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 327b6da7..8351775e 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -110,7 +110,7 @@ export class HubGoals extends BasicSerializableObject { // Allow quickly switching goals in dev mode if (G_IS_DEV) { window.addEventListener("keydown", ev => { - if (ev.key === "b") { + if (ev.key === "p") { // root is not guaranteed to exist within ~0.5s after loading in if (this.root && this.root.app && this.root.app.gameAnalytics) { if (!this.isEndOfDemoReached()) { @@ -195,6 +195,10 @@ export class HubGoals extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.allBuildingsUnlocked) { return true; } + if (this.root.gameMode.getLevelDefinitions().length < 1) { + // no story, so always unlocked + return true; + } return !!this.gainedRewards[reward]; } @@ -472,6 +476,9 @@ export class HubGoals extends BasicSerializableObject { * @returns {number} items / sec */ getBeltBaseSpeed() { + if (this.root.gameMode.throughputDoesNotMatter()) { + return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed; + } return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; } @@ -480,6 +487,9 @@ export class HubGoals extends BasicSerializableObject { * @returns {number} items / sec */ getUndergroundBeltBaseSpeed() { + if (this.root.gameMode.throughputDoesNotMatter()) { + return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed; + } return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; } @@ -488,6 +498,9 @@ export class HubGoals extends BasicSerializableObject { * @returns {number} items / sec */ getMinerBaseSpeed() { + if (this.root.gameMode.throughputDoesNotMatter()) { + return globalConfig.minerSpeedItemsPerSecond * globalConfig.puzzleModeSpeed; + } return globalConfig.minerSpeedItemsPerSecond * this.upgradeImprovements.miner; } @@ -497,9 +510,14 @@ export class HubGoals extends BasicSerializableObject { * @returns {number} items / sec */ getProcessorBaseSpeed(processorType) { + if (this.root.gameMode.throughputDoesNotMatter()) { + return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed * 10; + } + switch (processorType) { case enumItemProcessorTypes.trash: case enumItemProcessorTypes.hub: + case enumItemProcessorTypes.goal: return 1e30; case enumItemProcessorTypes.balancer: return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index d64f96a8..10daa561 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -1,54 +1,22 @@ -/* typehints:start */ -import { GameRoot } from "../root"; -/* typehints:end */ - -/* dev:start */ -import { TrailerMaker } from "./trailer_maker"; -/* dev:end */ - -import { Signal } from "../../core/signal"; +import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; -import { HUDBuildingsToolbar } from "./parts/buildings_toolbar"; -import { HUDBuildingPlacer } from "./parts/building_placer"; -import { HUDBlueprintPlacer } from "./parts/blueprint_placer"; -import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; -import { HUDUnlockNotification } from "./parts/unlock_notification"; -import { HUDGameMenu } from "./parts/game_menu"; -import { HUDShop } from "./parts/shop"; -import { IS_MOBILE, globalConfig } from "../../core/config"; -import { HUDMassSelector } from "./parts/mass_selector"; -import { HUDVignetteOverlay } from "./parts/vignette_overlay"; -import { HUDStatistics } from "./parts/statistics"; +import { Signal } from "../../core/signal"; +import { KEYMAPPINGS } from "../key_action_mapper"; import { MetaBuilding } from "../meta_building"; -import { HUDPinnedShapes } from "./parts/pinned_shapes"; +import { GameRoot } from "../root"; import { ShapeDefinition } from "../shape_definition"; -import { HUDNotifications, enumNotificationType } from "./parts/notifications"; -import { HUDSettingsMenu } from "./parts/settings_menu"; +import { HUDBetaOverlay } from "./parts/beta_overlay"; +import { HUDBuildingsToolbar } from "./parts/buildings_toolbar"; +import { HUDBuildingPlacer } from "./parts/building_placer"; +import { HUDColorBlindHelper } from "./parts/color_blind_helper"; +import { HUDChangesDebugger } from "./parts/debug_changes"; import { HUDDebugInfo } from "./parts/debug_info"; import { HUDEntityDebugger } from "./parts/entity_debugger"; -import { KEYMAPPINGS } from "../key_action_mapper"; -import { HUDWatermark } from "./parts/watermark"; import { HUDModalDialogs } from "./parts/modal_dialogs"; -import { HUDPartTutorialHints } from "./parts/tutorial_hints"; -import { HUDWaypoints } from "./parts/waypoints"; -import { HUDInteractiveTutorial } from "./parts/interactive_tutorial"; -import { HUDScreenshotExporter } from "./parts/screenshot_exporter"; -import { HUDColorBlindHelper } from "./parts/color_blind_helper"; -import { HUDShapeViewer } from "./parts/shape_viewer"; -import { HUDWiresOverlay } from "./parts/wires_overlay"; -import { HUDChangesDebugger } from "./parts/debug_changes"; -import { queryParamOptions } from "../../core/query_parameters"; -import { HUDSandboxController } from "./parts/sandbox_controller"; -import { HUDWiresToolbar } from "./parts/wires_toolbar"; -import { HUDWireInfo } from "./parts/wire_info"; -import { HUDLeverToggle } from "./parts/lever_toggle"; -import { HUDLayerPreview } from "./parts/layer_preview"; -import { HUDMinerHighlight } from "./parts/miner_highlight"; -import { HUDBetaOverlay } from "./parts/beta_overlay"; -import { HUDStandaloneAdvantages } from "./parts/standalone_advantages"; -import { HUDCatMemes } from "./parts/cat_memes"; -import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer"; -import { HUDConstantSignalEdit } from "./parts/constant_signal_edit"; +import { enumNotificationType } from "./parts/notifications"; +import { HUDSettingsMenu } from "./parts/settings_menu"; +import { HUDVignetteOverlay } from "./parts/vignette_overlay"; +import { TrailerMaker } from "./trailer_maker"; export class GameHUD { /** @@ -76,33 +44,12 @@ export class GameHUD { this.parts = { buildingsToolbar: new HUDBuildingsToolbar(this.root), - wiresToolbar: new HUDWiresToolbar(this.root), - blueprintPlacer: new HUDBlueprintPlacer(this.root), buildingPlacer: new HUDBuildingPlacer(this.root), - unlockNotification: new HUDUnlockNotification(this.root), - gameMenu: new HUDGameMenu(this.root), - massSelector: new HUDMassSelector(this.root), - shop: new HUDShop(this.root), - statistics: new HUDStatistics(this.root), - waypoints: new HUDWaypoints(this.root), - wireInfo: new HUDWireInfo(this.root), - leverToggle: new HUDLeverToggle(this.root), - constantSignalEdit: new HUDConstantSignalEdit(this.root), // Must always exist - pinnedShapes: new HUDPinnedShapes(this.root), - notifications: new HUDNotifications(this.root), settingsMenu: new HUDSettingsMenu(this.root), debugInfo: new HUDDebugInfo(this.root), dialogs: new HUDModalDialogs(this.root), - screenshotExporter: new HUDScreenshotExporter(this.root), - shapeViewer: new HUDShapeViewer(this.root), - - wiresOverlay: new HUDWiresOverlay(this.root), - layerPreview: new HUDLayerPreview(this.root), - - minerHighlight: new HUDMinerHighlight(this.root), - tutorialVideoOffer: new HUDTutorialVideoOffer(this.root), // Typing hints /* typehints:start */ @@ -111,29 +58,14 @@ export class GameHUD { /* typehints:end */ }; - if (!IS_MOBILE) { - this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root); - } - if (G_IS_DEV && globalConfig.debug.enableEntityInspector) { this.parts.entityDebugger = new HUDEntityDebugger(this.root); } - if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) { - this.parts.watermark = new HUDWatermark(this.root); - this.parts.standaloneAdvantages = new HUDStandaloneAdvantages(this.root); - this.parts.catMemes = new HUDCatMemes(this.root); - } - if (G_IS_DEV && globalConfig.debug.renderChanges) { this.parts.changesDebugger = new HUDChangesDebugger(this.root); } - if (this.root.app.settings.getAllSettings().offerHints) { - this.parts.tutorialHints = new HUDPartTutorialHints(this.root); - this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root); - } - if (this.root.app.settings.getAllSettings().vignette) { this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root); } @@ -142,14 +74,15 @@ export class GameHUD { this.parts.colorBlindHelper = new HUDColorBlindHelper(this.root); } - if (queryParamOptions.sandboxMode || G_IS_DEV) { - this.parts.sandboxController = new HUDSandboxController(this.root); - } - if (!G_IS_RELEASE && !G_IS_DEV) { this.parts.betaOverlay = new HUDBetaOverlay(this.root); } + const additionalParts = this.root.gameMode.additionalHudParts; + for (const [partId, part] of Object.entries(additionalParts)) { + this.parts[partId] = new part(this.root); + } + const frag = document.createDocumentFragment(); for (const key in this.parts) { this.parts[key].createElements(frag); diff --git a/src/js/game/hud/parts/base_toolbar.js b/src/js/game/hud/parts/base_toolbar.js index b3f5abfc..15faad66 100644 --- a/src/js/game/hud/parts/base_toolbar.js +++ b/src/js/game/hud/parts/base_toolbar.js @@ -1,6 +1,10 @@ import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { STOP_PROPAGATION } from "../../../core/signal"; import { makeDiv, safeModulo } from "../../../core/utils"; +import { MetaBlockBuilding } from "../../buildings/block"; +import { MetaConstantProducerBuilding } from "../../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; import { KEYMAPPINGS } from "../../key_action_mapper"; import { MetaBuilding } from "../../meta_building"; import { GameRoot } from "../../root"; @@ -23,8 +27,8 @@ export class HUDBaseToolbar extends BaseHUDPart { ) { super(root); - this.primaryBuildings = primaryBuildings; - this.secondaryBuildings = secondaryBuildings; + this.primaryBuildings = this.filterBuildings(primaryBuildings); + this.secondaryBuildings = this.filterBuildings(secondaryBuildings); this.visibilityCondition = visibilityCondition; this.htmlElementId = htmlElementId; this.layer = layer; @@ -35,6 +39,7 @@ export class HUDBaseToolbar extends BaseHUDPart { * selected: boolean, * element: HTMLElement, * index: number + * puzzleLocked: boolean; * }>} */ this.buildingHandles = {}; } @@ -47,6 +52,24 @@ export class HUDBaseToolbar extends BaseHUDPart { this.element = makeDiv(parent, this.htmlElementId, ["ingame_buildingsToolbar"], ""); } + /** + * @param {Array} buildings + * @returns {Array} + */ + filterBuildings(buildings) { + const filtered = []; + + for (let i = 0; i < buildings.length; i++) { + if (this.root.gameMode.isBuildingExcluded(buildings[i])) { + continue; + } + + filtered.push(buildings[i]); + } + + return filtered; + } + /** * Returns all buildings * @returns {Array} @@ -87,19 +110,31 @@ 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)); - this.trackClicks(itemContainer, () => this.selectBuildingForPlacement(metaBuilding), { + const icon = makeDiv(itemContainer, null, ["icon"]); + + this.trackClicks(icon, () => this.selectBuildingForPlacement(metaBuilding), { clickSound: null, }); + //lock icon for puzzle editor + if (this.root.gameMode.getIsEditor() && !this.inRequiredBuildings(metaBuilding)) { + const puzzleLock = makeDiv(itemContainer, null, ["puzzle-lock"]); + + itemContainer.classList.toggle("editor", true); + this.trackClicks(puzzleLock, () => this.toggleBuildingLock(metaBuilding), { + clickSound: null, + }); + } + this.buildingHandles[metaBuilding.id] = { - metaBuilding, + metaBuilding: metaBuilding, element: itemContainer, unlocked: false, selected: false, index: i, + puzzleLocked: false, }; } @@ -127,7 +162,7 @@ export class HUDBaseToolbar extends BaseHUDPart { let recomputeSecondaryToolbarVisibility = false; for (const buildingId in this.buildingHandles) { const handle = this.buildingHandles[buildingId]; - const newStatus = handle.metaBuilding.getIsUnlocked(this.root); + const newStatus = !handle.puzzleLocked && handle.metaBuilding.getIsUnlocked(this.root); if (handle.unlocked !== newStatus) { handle.unlocked = newStatus; handle.element.classList.toggle("unlocked", newStatus); @@ -216,6 +251,14 @@ export class HUDBaseToolbar extends BaseHUDPart { return STOP_PROPAGATION; } + const handle = this.buildingHandles[metaBuilding.getId()]; + if (handle.puzzleLocked) { + handle.puzzleLocked = false; + handle.element.classList.toggle("unlocked", false); + this.root.soundProxy.playUiClick(); + return; + } + // Allow clicking an item again to deselect it for (const buildingId in this.buildingHandles) { const handle = this.buildingHandles[buildingId]; @@ -229,4 +272,51 @@ export class HUDBaseToolbar extends BaseHUDPart { this.root.hud.signals.buildingSelectedForPlacement.dispatch(metaBuilding); this.onSelectedPlacementBuildingChanged(metaBuilding); } + + /** + * @param {MetaBuilding} metaBuilding + */ + toggleBuildingLock(metaBuilding) { + if (!this.visibilityCondition()) { + // Not active + return; + } + + if (this.inRequiredBuildings(metaBuilding) || !metaBuilding.getIsUnlocked(this.root)) { + this.root.soundProxy.playUiError(); + return STOP_PROPAGATION; + } + + const handle = this.buildingHandles[metaBuilding.getId()]; + handle.puzzleLocked = !handle.puzzleLocked; + handle.element.classList.toggle("unlocked", !handle.puzzleLocked); + this.root.soundProxy.playUiClick(); + + const entityManager = this.root.entityMgr; + for (const entity of entityManager.getAllWithComponent(StaticMapEntityComponent)) { + const staticComp = entity.components.StaticMapEntity; + if (staticComp.getMetaBuilding().id === metaBuilding.id) { + this.root.map.removeStaticEntity(entity); + entityManager.destroyEntity(entity); + } + } + entityManager.processDestroyList(); + + const currentMetaBuilding = this.root.hud.parts.buildingPlacer.currentMetaBuilding; + if (currentMetaBuilding.get() == metaBuilding) { + currentMetaBuilding.set(null); + } + } + + /** + * @param {MetaBuilding} metaBuilding + */ + inRequiredBuildings(metaBuilding) { + const requiredBuildings = [ + gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding), + gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding), + gMetaBuildingRegistry.findByClass(MetaBlockBuilding), + ]; + return requiredBuildings.includes(metaBuilding); + } } diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index 7d618b6b..33e6ebc2 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -234,7 +234,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { * @param {DrawParameters} parameters */ draw(parameters) { - if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { + if (this.root.camera.getIsMapOverlayActive()) { // Dont allow placing in overview mode this.domAttach.update(false); this.variantsAttach.update(false); @@ -275,11 +275,13 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const worldPosition = this.root.camera.screenToWorld(mousePosition); // Draw peeker - this.root.hud.parts.layerPreview.renderPreview( - parameters, - worldPosition, - 1 / this.root.camera.zoomLevel - ); + if (this.root.hud.parts.layerPreview) { + this.root.hud.parts.layerPreview.renderPreview( + parameters, + worldPosition, + 1 / this.root.camera.zoomLevel + ); + } } /** diff --git a/src/js/game/hud/parts/building_placer_logic.js b/src/js/game/hud/parts/building_placer_logic.js index 1e88abc7..9e91f372 100644 --- a/src/js/game/hud/parts/building_placer_logic.js +++ b/src/js/game/hud/parts/building_placer_logic.js @@ -366,7 +366,8 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { if ( tileBelow && this.root.app.settings.getAllSettings().pickMinerOnPatch && - this.root.currentLayer === "regular" + this.root.currentLayer === "regular" && + this.root.gameMode.hasResources() ) { this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding)); @@ -390,6 +391,12 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { return; } + // Disallow picking excluded buildings + if (this.root.gameMode.isBuildingExcluded(extracted.metaClass)) { + this.currentMetaBuilding.set(null); + return; + } + // If the building we are picking is the same as the one we have, clear the cursor. if ( this.currentMetaBuilding.get() && @@ -430,7 +437,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { * @param {Vector} tile */ tryPlaceCurrentBuildingAt(tile) { - if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { + if (this.root.camera.getIsMapOverlayActive()) { // Dont allow placing in overview mode return; } diff --git a/src/js/game/hud/parts/buildings_toolbar.js b/src/js/game/hud/parts/buildings_toolbar.js index 05ffc795..994a70ed 100644 --- a/src/js/game/hud/parts/buildings_toolbar.js +++ b/src/js/game/hud/parts/buildings_toolbar.js @@ -15,23 +15,28 @@ import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; import { HUDBaseToolbar } from "./base_toolbar"; import { MetaStorageBuilding } from "../../buildings/storage"; import { MetaItemProducerBuilding } from "../../buildings/item_producer"; -import { queryParamOptions } from "../../../core/query_parameters"; +import { MetaConstantProducerBuilding } from "../../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor"; +import { MetaBlockBuilding } from "../../buildings/block"; export class HUDBuildingsToolbar extends HUDBaseToolbar { constructor(root) { super(root, { primaryBuildings: [ + MetaConstantProducerBuilding, + MetaGoalAcceptorBuilding, MetaBeltBuilding, MetaBalancerBuilding, MetaUndergroundBeltBuilding, MetaMinerBuilding, + MetaBlockBuilding, MetaCutterBuilding, MetaRotaterBuilding, MetaStackerBuilding, MetaMixerBuilding, MetaPainterBuilding, MetaTrashBuilding, - ...(queryParamOptions.sandboxMode || G_IS_DEV ? [MetaItemProducerBuilding] : []), + MetaItemProducerBuilding, ], secondaryBuildings: [ MetaStorageBuilding, diff --git a/src/js/game/hud/parts/keybinding_overlay.js b/src/js/game/hud/parts/keybinding_overlay.js index 65919d3c..2384ab84 100644 --- a/src/js/game/hud/parts/keybinding_overlay.js +++ b/src/js/game/hud/parts/keybinding_overlay.js @@ -254,6 +254,13 @@ export class HUDKeybindingOverlay extends BaseHUDPart { condition: () => this.anythingSelectedOnMap, }, + { + // [SELECTION] Clear + label: T.ingame.keybindingsOverlay.clearBelts, + keys: [k.massSelect.massSelectClear], + condition: () => this.anythingSelectedOnMap, + }, + { // Switch layers label: T.ingame.keybindingsOverlay.switchLayers, diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index d73e3be3..ab933da3 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -1,20 +1,19 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { Vector } from "../../../core/vector"; -import { STOP_PROPAGATION } from "../../../core/signal"; -import { DrawParameters } from "../../../core/draw_parameters"; -import { Entity } from "../../entity"; -import { Loader } from "../../../core/loader"; import { globalConfig } from "../../../core/config"; -import { makeDiv, formatBigNumber, formatBigNumberFull } from "../../../core/utils"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { DrawParameters } from "../../../core/draw_parameters"; import { createLogger } from "../../../core/logging"; +import { STOP_PROPAGATION } from "../../../core/signal"; +import { formatBigNumberFull } from "../../../core/utils"; +import { Vector } from "../../../core/vector"; import { ACHIEVEMENTS } from "../../../platform/achievement_provider"; -import { enumMouseButton } from "../../camera"; import { T } from "../../../translations"; +import { Blueprint } from "../../blueprint"; +import { enumMouseButton } from "../../camera"; +import { Component } from "../../component"; +import { Entity } from "../../entity"; import { KEYMAPPINGS } from "../../key_action_mapper"; import { THEME } from "../../theme"; import { enumHubGoalRewards } from "../../tutorial_goals"; -import { Blueprint } from "../../blueprint"; +import { BaseHUDPart } from "../base_hud_part"; const logger = createLogger("hud/mass_selector"); @@ -33,12 +32,13 @@ export class HUDMassSelector extends BaseHUDPart { this.root.camera.movePreHandler.add(this.onMouseMove, this); this.root.camera.upPostHandler.add(this.onMouseUp, this); - this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.onBack, this); + this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).addToTop(this.onBack, this); this.root.keyMapper .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) .add(this.confirmDelete, this); this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCut).add(this.confirmCut, this); this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCopy).add(this.startCopy, this); + this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectClear).add(this.clearBelts, this); this.root.hud.signals.selectedPlacementBuildingChanged.add(this.clearSelection, this); this.root.signals.editModeChanged.add(this.clearSelection, this); @@ -142,6 +142,16 @@ export class HUDMassSelector extends BaseHUDPart { } } + clearBelts() { + for (const uid of this.selectedUids) { + const entity = this.root.entityMgr.findByUid(uid); + for (const component of Object.values(entity.components)) { + /** @type {Component} */ (component).clear(); + } + } + this.selectedUids = new Set(); + } + confirmCut() { if (!this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { this.root.hud.parts.dialogs.showInfo( diff --git a/src/js/game/hud/parts/modal_dialogs.js b/src/js/game/hud/parts/modal_dialogs.js index 263b23dd..a43260e3 100644 --- a/src/js/game/hud/parts/modal_dialogs.js +++ b/src/js/game/hud/parts/modal_dialogs.js @@ -29,11 +29,14 @@ export class HUDModalDialogs extends BaseHUDPart { } shouldPauseRendering() { - return this.dialogStack.length > 0; + // return this.dialogStack.length > 0; + // @todo: Check if change this affects anything + return false; } shouldPauseGame() { - return this.shouldPauseRendering(); + // @todo: Check if this change affects anything + return false; } createElements(parent) { @@ -139,8 +142,8 @@ export class HUDModalDialogs extends BaseHUDPart { } // Returns method to be called when laoding finishd - showLoadingDialog() { - const dialog = new DialogLoading(this.app); + showLoadingDialog(text = "") { + const dialog = new DialogLoading(this.app, text); this.internalShowDialog(dialog); return this.closeDialog.bind(this, dialog); } diff --git a/src/js/game/hud/parts/pinned_shapes.js b/src/js/game/hud/parts/pinned_shapes.js index 4a9fce0d..a53ecbe5 100644 --- a/src/js/game/hud/parts/pinned_shapes.js +++ b/src/js/game/hud/parts/pinned_shapes.js @@ -55,7 +55,7 @@ export class HUDPinnedShapes extends BaseHUDPart { */ deserialize(data) { if (!data || !data.shapes || !Array.isArray(data.shapes)) { - return "Invalid pinned shapes data"; + return "Invalid pinned shapes data: " + JSON.stringify(data); } this.pinnedShapes = data.shapes; } diff --git a/src/js/game/hud/parts/puzzle_back_to_menu.js b/src/js/game/hud/parts/puzzle_back_to_menu.js new file mode 100644 index 00000000..bde5b66d --- /dev/null +++ b/src/js/game/hud/parts/puzzle_back_to_menu.js @@ -0,0 +1,21 @@ +import { makeDiv } from "../../../core/utils"; +import { BaseHUDPart } from "../base_hud_part"; + +export class HUDPuzzleBackToMenu extends BaseHUDPart { + createElements(parent) { + const key = this.root.gameMode.getId(); + + this.element = makeDiv(parent, "ingame_HUD_PuzzleBackToMenu"); + this.button = document.createElement("button"); + this.button.classList.add("button"); + this.element.appendChild(this.button); + + this.trackClicks(this.button, this.back); + } + + initialize() {} + + back() { + this.root.gameState.goBackToMenu(); + } +} diff --git a/src/js/game/hud/parts/puzzle_complete_notification.js b/src/js/game/hud/parts/puzzle_complete_notification.js new file mode 100644 index 00000000..f223c1d6 --- /dev/null +++ b/src/js/game/hud/parts/puzzle_complete_notification.js @@ -0,0 +1,112 @@ +/* typehints:start */ +import { PuzzlePlayGameMode } from "../../modes/puzzle_play"; +/* typehints:end */ + +import { InputReceiver } from "../../../core/input_receiver"; +import { makeDiv } from "../../../core/utils"; +import { SOUNDS } from "../../../platform/sound"; +import { T } from "../../../translations"; +import { enumColors } from "../../colors"; +import { ColorItem } from "../../items/color_item"; +import { finalGameShape, rocketShape } from "../../modes/regular"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { ShapeItem } from "../../items/shape_item"; +import { ShapeDefinition } from "../../shape_definition"; + +export class HUDPuzzleCompleteNotification extends BaseHUDPart { + initialize() { + this.visible = false; + + this.domAttach = new DynamicDomAttach(this.root, this.element, { + timeToKeepSeconds: 0, + }); + + this.root.signals.puzzleComplete.add(this.show, this); + + this.userDidLikePuzzle = false; + this.timeOfCompletion = 0; + } + + createElements(parent) { + this.inputReciever = new InputReceiver("puzzle-complete"); + + this.element = makeDiv(parent, "ingame_HUD_PuzzleCompleteNotification", ["noBlur"]); + + const dialog = makeDiv(this.element, null, ["dialog"]); + + this.elemTitle = makeDiv(dialog, null, ["title"], T.ingame.puzzleCompletion.title); + this.elemContents = makeDiv(dialog, null, ["contents"]); + this.elemActions = makeDiv(dialog, null, ["actions"]); + + const stepLike = makeDiv(this.elemContents, null, ["step", "stepLike"]); + makeDiv(stepLike, null, ["title"], T.ingame.puzzleCompletion.titleLike); + + const likeButtons = makeDiv(stepLike, null, ["buttons"]); + + this.buttonLikeYes = document.createElement("button"); + this.buttonLikeYes.classList.add("liked-yes"); + likeButtons.appendChild(this.buttonLikeYes); + this.trackClicks(this.buttonLikeYes, () => { + this.userDidLikePuzzle = !this.userDidLikePuzzle; + this.updateState(); + }); + + const buttonBar = document.createElement("div"); + buttonBar.classList.add("buttonBar"); + this.elemContents.appendChild(buttonBar); + + this.continueBtn = document.createElement("button"); + this.continueBtn.classList.add("continue", "styledButton"); + this.continueBtn.innerText = T.ingame.puzzleCompletion.continueBtn; + buttonBar.appendChild(this.continueBtn); + this.trackClicks(this.continueBtn, () => { + this.close(false); + }); + + this.menuBtn = document.createElement("button"); + this.menuBtn.classList.add("menu", "styledButton"); + this.menuBtn.innerText = T.ingame.puzzleCompletion.menuBtn; + buttonBar.appendChild(this.menuBtn); + + this.trackClicks(this.menuBtn, () => { + this.close(true); + }); + } + + updateState() { + this.buttonLikeYes.classList.toggle("active", this.userDidLikePuzzle === true); + } + + show() { + this.root.soundProxy.playUi(SOUNDS.levelComplete); + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.visible = true; + this.timeOfCompletion = this.root.time.now(); + } + + cleanup() { + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + } + + isBlockingOverlay() { + return this.visible; + } + + close(toMenu) { + /** @type {PuzzlePlayGameMode} */ (this.root.gameMode) + .trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion)) + .then(() => { + if (toMenu) { + this.root.gameState.moveToState("PuzzleMenuState"); + } else { + this.visible = false; + this.cleanup(); + } + }); + } + + update() { + this.domAttach.update(this.visible); + } +} diff --git a/src/js/game/hud/parts/puzzle_dlc_logo.js b/src/js/game/hud/parts/puzzle_dlc_logo.js new file mode 100644 index 00000000..ec50808a --- /dev/null +++ b/src/js/game/hud/parts/puzzle_dlc_logo.js @@ -0,0 +1,13 @@ +import { makeDiv } from "../../../core/utils"; +import { BaseHUDPart } from "../base_hud_part"; + +export class HUDPuzzleDLCLogo extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PuzzleDLCLogo"); + parent.appendChild(this.element); + } + + initialize() {} + + next() {} +} diff --git a/src/js/game/hud/parts/puzzle_editor_controls.js b/src/js/game/hud/parts/puzzle_editor_controls.js new file mode 100644 index 00000000..d8055f11 --- /dev/null +++ b/src/js/game/hud/parts/puzzle_editor_controls.js @@ -0,0 +1,18 @@ +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { BaseHUDPart } from "../base_hud_part"; + +export class HUDPuzzleEditorControls extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorControls"); + + this.element.innerHTML = T.ingame.puzzleEditorControls.instructions + .map(text => `${text}`) + .join(""); + + this.titleElement = makeDiv(parent, "ingame_HUD_PuzzleEditorTitle"); + this.titleElement.innerText = T.ingame.puzzleEditorControls.title; + } + + initialize() {} +} diff --git a/src/js/game/hud/parts/puzzle_editor_review.js b/src/js/game/hud/parts/puzzle_editor_review.js new file mode 100644 index 00000000..68f5360c --- /dev/null +++ b/src/js/game/hud/parts/puzzle_editor_review.js @@ -0,0 +1,233 @@ +import { globalConfig, THIRDPARTY_URLS } from "../../../core/config"; +import { createLogger } from "../../../core/logging"; +import { DialogWithForm } from "../../../core/modal_dialog_elements"; +import { FormElementInput, FormElementItemChooser } from "../../../core/modal_dialog_forms"; +import { STOP_PROPAGATION } from "../../../core/signal"; +import { fillInLinkIntoTranslation, makeDiv } from "../../../core/utils"; +import { PuzzleSerializer } from "../../../savegame/puzzle_serializer"; +import { T } from "../../../translations"; +import { ConstantSignalComponent } from "../../components/constant_signal"; +import { GoalAcceptorComponent } from "../../components/goal_acceptor"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { ShapeItem } from "../../items/shape_item"; +import { ShapeDefinition } from "../../shape_definition"; +import { BaseHUDPart } from "../base_hud_part"; + +const trim = require("trim"); +const logger = createLogger("puzzle-review"); + +export class HUDPuzzleEditorReview extends BaseHUDPart { + constructor(root) { + super(root); + } + + createElements(parent) { + const key = this.root.gameMode.getId(); + + this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorReview"); + this.button = document.createElement("button"); + this.button.classList.add("button"); + this.button.textContent = T.puzzleMenu.reviewPuzzle; + this.element.appendChild(this.button); + + this.trackClicks(this.button, this.startReview); + } + + initialize() {} + + startReview() { + const validationError = this.validatePuzzle(); + if (validationError) { + this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError); + return; + } + + const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.validatingPuzzle); + + // Wait a bit, so the user sees the puzzle actually got validated + setTimeout(() => { + // Manually simulate ticks + this.root.logic.clearAllBeltsAndItems(); + + const maxTicks = + this.root.gameMode.getFixedTickrate() * globalConfig.puzzleValidationDurationSeconds; + const deltaMs = this.root.dynamicTickrate.deltaMs; + logger.log("Simulating up to", maxTicks, "ticks, start=", this.root.time.now().toFixed(1)); + const now = performance.now(); + + let simulatedTicks = 0; + for (let i = 0; i < maxTicks; ++i) { + // Perform logic tick + this.root.time.performTicks(deltaMs, this.root.gameState.core.boundInternalTick); + simulatedTicks++; + + if (simulatedTicks % 100 == 0 && !this.validatePuzzle()) { + break; + } + } + + const duration = performance.now() - now; + logger.log( + "Simulated", + simulatedTicks, + "ticks, end=", + this.root.time.now().toFixed(1), + "duration=", + duration.toFixed(2), + "ms" + ); + + console.log("duration: " + duration); + closeLoading(); + + //if it took so little ticks that it must have autocompeted + if (simulatedTicks <= 300) { + this.root.hud.parts.dialogs.showWarning( + T.puzzleMenu.validation.title, + T.puzzleMenu.validation.autoComplete + ); + return; + } + + //if we reached maximum ticks and the puzzle still isn't completed + const validationError = this.validatePuzzle(); + if (simulatedTicks == maxTicks && validationError) { + this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError); + return; + } + this.startSubmit(); + }, 750); + } + + startSubmit(title = "", shortKey = "") { + const regex = /^[a-zA-Z0-9_\- ]{4,20}$/; + const nameInput = new FormElementInput({ + id: "nameInput", + label: T.dialogs.submitPuzzle.descName, + placeholder: T.dialogs.submitPuzzle.placeholderName, + defaultValue: title, + validator: val => trim(val).match(regex) && trim(val).length > 0, + }); + + let items = new Set(); + const acceptors = this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent); + for (const acceptor of acceptors) { + const item = acceptor.components.GoalAcceptor.item; + if (item.getItemType() === "shape") { + items.add(item); + } + } + + while (items.size < 8) { + // add some randoms + const item = this.root.hubGoals.computeFreeplayShape(Math.round(10 + Math.random() * 10000)); + items.add(new ShapeItem(item)); + } + + const itemInput = new FormElementItemChooser({ + id: "signalItem", + label: fillInLinkIntoTranslation(T.dialogs.submitPuzzle.descIcon, THIRDPARTY_URLS.shapeViewer), + items: Array.from(items), + }); + + const shapeKeyInput = new FormElementInput({ + id: "shapeKeyInput", + label: null, + placeholder: "CuCuCuCu", + defaultValue: shortKey, + validator: val => ShapeDefinition.isValidShortKey(trim(val)), + }); + + const dialog = new DialogWithForm({ + app: this.root.app, + title: T.dialogs.submitPuzzle.title, + desc: "", + formElements: [nameInput, itemInput, shapeKeyInput], + buttons: ["ok:good:enter"], + }); + + itemInput.valueChosen.add(value => { + shapeKeyInput.setValue(value.definition.getHash()); + }); + + this.root.hud.parts.dialogs.internalShowDialog(dialog); + + dialog.buttonSignals.ok.add(() => { + const title = trim(nameInput.getValue()); + const shortKey = trim(shapeKeyInput.getValue()); + this.doSubmitPuzzle(title, shortKey); + }); + } + + doSubmitPuzzle(title, shortKey) { + const serialized = new PuzzleSerializer().generateDumpFromGameRoot(this.root); + + logger.log("Submitting puzzle, title=", title, "shortKey=", shortKey); + if (G_IS_DEV) { + logger.log("Serialized data:", serialized); + } + + const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.submittingPuzzle); + + this.root.app.clientApi + .apiSubmitPuzzle({ + title, + shortKey, + data: serialized, + }) + .then( + () => { + closeLoading(); + const { ok } = this.root.hud.parts.dialogs.showInfo( + T.dialogs.puzzleSubmitOk.title, + T.dialogs.puzzleSubmitOk.desc + ); + ok.add(() => this.root.gameState.moveToState("PuzzleMenuState")); + }, + err => { + closeLoading(); + logger.warn("Failed to submit puzzle:", err); + const signals = this.root.hud.parts.dialogs.showWarning( + T.dialogs.puzzleSubmitError.title, + T.dialogs.puzzleSubmitError.desc + " " + err, + ["cancel", "retry:good"] + ); + signals.retry.add(() => this.startSubmit(title, shortKey)); + } + ); + } + + validatePuzzle() { + // Check there is at least one constant producer and goal acceptor + const producers = this.root.entityMgr.getAllWithComponent(ConstantSignalComponent); + const acceptors = this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent); + + if (producers.length === 0) { + return T.puzzleMenu.validation.noProducers; + } + + if (acceptors.length === 0) { + return T.puzzleMenu.validation.noGoalAcceptors; + } + + // Check if all acceptors satisfy the constraints + for (const acceptor of acceptors) { + const goalComp = acceptor.components.GoalAcceptor; + if (!goalComp.item) { + return T.puzzleMenu.validation.goalAcceptorNoItem; + } + const required = goalComp.getRequiredDeliveryHistorySize(); + if (goalComp.deliveryHistory.length < required) { + return T.puzzleMenu.validation.goalAcceptorRateNotMet; + } + } + + // Check if all buildings are within the area + const entities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent); + for (const entity of entities) { + if (this.root.systemMgr.systems.zone.prePlacementCheck(entity) === STOP_PROPAGATION) { + return T.puzzleMenu.validation.buildingOutOfBounds; + } + } + } +} diff --git a/src/js/game/hud/parts/puzzle_editor_settings.js b/src/js/game/hud/parts/puzzle_editor_settings.js new file mode 100644 index 00000000..cf283a9b --- /dev/null +++ b/src/js/game/hud/parts/puzzle_editor_settings.js @@ -0,0 +1,200 @@ +/* typehints:start */ +import { PuzzleGameMode } from "../../modes/puzzle"; +/* typehints:end */ + +import { globalConfig } from "../../../core/config"; +import { createLogger } from "../../../core/logging"; +import { Rectangle } from "../../../core/rectangle"; +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { BaseHUDPart } from "../base_hud_part"; + +const logger = createLogger("puzzle-editor"); + +export class HUDPuzzleEditorSettings extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorSettings"); + + if (this.root.gameMode.getBuildableZones()) { + const bind = (selector, handler) => + this.trackClicks(this.element.querySelector(selector), handler); + this.zone = makeDiv( + this.element, + null, + ["section", "zone"], + ` + + +
+
+ + + + +
+ +
+ + + + +
+ +
+ + +
+
` + ); + + bind(".zoneWidth .minus", () => this.modifyZone(-1, 0)); + bind(".zoneWidth .plus", () => this.modifyZone(1, 0)); + bind(".zoneHeight .minus", () => this.modifyZone(0, -1)); + bind(".zoneHeight .plus", () => this.modifyZone(0, 1)); + bind("button.trim", this.trim); + bind("button.clear", this.clear); + } + } + + clear() { + this.root.logic.clearAllBeltsAndItems(); + } + + trim() { + // Now, find the center + const buildings = this.root.entityMgr.entities.slice(); + + if (buildings.length === 0) { + // nothing to do + return; + } + + let minRect = null; + + for (const building of buildings) { + const staticComp = building.components.StaticMapEntity; + const bounds = staticComp.getTileSpaceBounds(); + + if (!minRect) { + minRect = bounds; + } else { + minRect = minRect.getUnion(bounds); + } + } + + const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode); + const moveByInverse = minRect.getCenter().round(); + + // move buildings + if (moveByInverse.length() > 0) { + // increase area size + mode.zoneWidth = globalConfig.puzzleMaxBoundsSize; + mode.zoneHeight = globalConfig.puzzleMaxBoundsSize; + + // First, remove any items etc + this.root.logic.clearAllBeltsAndItems(); + + this.root.logic.performImmutableOperation(() => { + // 1. remove all buildings + for (const building of buildings) { + if (!this.root.logic.tryDeleteBuilding(building)) { + assertAlways(false, "Failed to remove building in trim"); + } + } + + // 2. place them again, but centered + for (const building of buildings) { + const staticComp = building.components.StaticMapEntity; + const result = this.root.logic.tryPlaceBuilding({ + origin: staticComp.origin.sub(moveByInverse), + building: staticComp.getMetaBuilding(), + originalRotation: staticComp.originalRotation, + rotation: staticComp.rotation, + rotationVariant: staticComp.getRotationVariant(), + variant: staticComp.getVariant(), + }); + if (!result) { + this.root.bulkOperationRunning = false; + assertAlways(false, "Failed to re-place building in trim"); + } + + if (building.components.ConstantSignal) { + result.components.ConstantSignal.signal = building.components.ConstantSignal.signal; + } + } + }); + } + + // 3. Actually trim + let w = mode.zoneWidth; + let h = mode.zoneHeight; + + while (!this.anyBuildingOutsideZone(w - 1, h)) { + --w; + } + + while (!this.anyBuildingOutsideZone(w, h - 1)) { + --h; + } + + mode.zoneWidth = w; + mode.zoneHeight = h; + this.updateZoneValues(); + } + + initialize() { + this.visible = true; + this.updateZoneValues(); + } + + anyBuildingOutsideZone(width, height) { + if (Math.min(width, height) < globalConfig.puzzleMinBoundsSize) { + return true; + } + const newZone = Rectangle.centered(width, height); + const entities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent); + + for (const entity of entities) { + const staticComp = entity.components.StaticMapEntity; + const bounds = staticComp.getTileSpaceBounds(); + if (!newZone.intersectsFully(bounds)) { + return true; + } + } + } + + modifyZone(deltaW, deltaH) { + const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode); + + const newWidth = mode.zoneWidth + deltaW; + const newHeight = mode.zoneHeight + deltaH; + + if (Math.min(newWidth, newHeight) < globalConfig.puzzleMinBoundsSize) { + return; + } + + if (Math.max(newWidth, newHeight) > globalConfig.puzzleMaxBoundsSize) { + return; + } + + if (this.anyBuildingOutsideZone(newWidth, newHeight)) { + this.root.hud.parts.dialogs.showWarning( + T.dialogs.puzzleResizeBadBuildings.title, + T.dialogs.puzzleResizeBadBuildings.desc + ); + return; + } + + mode.zoneWidth = newWidth; + mode.zoneHeight = newHeight; + this.updateZoneValues(); + } + + updateZoneValues() { + const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode); + + this.element.querySelector(".zoneWidth > .value").textContent = String(mode.zoneWidth); + this.element.querySelector(".zoneHeight > .value").textContent = String(mode.zoneHeight); + } +} diff --git a/src/js/game/hud/parts/puzzle_play_metadata.js b/src/js/game/hud/parts/puzzle_play_metadata.js new file mode 100644 index 00000000..3550a1e6 --- /dev/null +++ b/src/js/game/hud/parts/puzzle_play_metadata.js @@ -0,0 +1,72 @@ +/* typehints:start */ +import { PuzzlePlayGameMode } from "../../modes/puzzle_play"; +/* typehints:end */ + +import { formatBigNumberFull, formatSeconds, makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { BaseHUDPart } from "../base_hud_part"; + +const copy = require("clipboard-copy"); + +export class HUDPuzzlePlayMetadata extends BaseHUDPart { + createElements(parent) { + this.titleElement = makeDiv(parent, "ingame_HUD_PuzzlePlayTitle"); + this.titleElement.innerText = "PUZZLE"; + + const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode); + const puzzle = mode.puzzle; + + this.puzzleNameElement = makeDiv(this.titleElement, null, ["name"]); + this.puzzleNameElement.innerText = puzzle.meta.title; + + this.element = makeDiv(parent, "ingame_HUD_PuzzlePlayMetadata"); + this.element.innerHTML = ` + +
+ ${formatBigNumberFull(puzzle.meta.downloads)} + +
+ + +
+
+ ${puzzle.meta.shortKey} +
+
+ + ${puzzle.meta.averageTime ? formatSeconds(puzzle.meta.averageTime) : "-"} +
+
+ + ${ + puzzle.meta.downloads > 0 + ? ((puzzle.meta.completions / puzzle.meta.downloads) * 100.0).toFixed(1) + "%" + : "-" + } +
+ +
+ + +
+ `; + + this.trackClicks(this.element.querySelector("button.share"), this.share); + this.trackClicks(this.element.querySelector("button.report"), this.report); + + /** @type {HTMLElement} */ (this.element.querySelector(".author span")).innerText = + puzzle.meta.author; + } + + initialize() {} + + share() { + const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode); + mode.sharePuzzle(); + } + + report() { + const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode); + mode.reportPuzzle(); + } +} diff --git a/src/js/game/hud/parts/puzzle_play_settings.js b/src/js/game/hud/parts/puzzle_play_settings.js new file mode 100644 index 00000000..168c3de2 --- /dev/null +++ b/src/js/game/hud/parts/puzzle_play_settings.js @@ -0,0 +1,36 @@ +import { createLogger } from "../../../core/logging"; +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { BaseHUDPart } from "../base_hud_part"; + +const logger = createLogger("puzzle-play"); + +export class HUDPuzzlePlaySettings extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PuzzlePlaySettings"); + + if (this.root.gameMode.getBuildableZones()) { + const bind = (selector, handler) => + this.trackClicks(this.element.querySelector(selector), handler); + makeDiv( + this.element, + null, + ["section"], + ` + + + ` + ); + + bind("button.clear", this.clear); + } + } + + clear() { + this.root.logic.clearAllBeltsAndItems(); + } + + initialize() { + this.visible = true; + } +} diff --git a/src/js/game/hud/parts/sandbox_controller.js b/src/js/game/hud/parts/sandbox_controller.js index 592487ee..3689fa36 100644 --- a/src/js/game/hud/parts/sandbox_controller.js +++ b/src/js/game/hud/parts/sandbox_controller.js @@ -1,3 +1,4 @@ +import { queryParamOptions } from "../../../core/query_parameters"; import { makeDiv } from "../../../core/utils"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; @@ -19,25 +20,25 @@ export class HUDSandboxController extends BaseHUDPart { - +
- +
- +
- +
@@ -117,7 +118,9 @@ export class HUDSandboxController extends BaseHUDPart { // Clear all shapes of this level hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0; - this.root.hud.parts.pinnedShapes.rerenderFull(); + if (this.root.hud.parts.pinnedShapes) { + this.root.hud.parts.pinnedShapes.rerenderFull(); + } // Compute gained rewards hubGoals.gainedRewards = {}; @@ -144,7 +147,7 @@ export class HUDSandboxController extends BaseHUDPart { } }); - this.visible = !G_IS_DEV; + this.visible = false; this.domAttach = new DynamicDomAttach(this.root, this.element); } diff --git a/src/js/game/hud/parts/settings_menu.js b/src/js/game/hud/parts/settings_menu.js index eb902934..16da0440 100644 --- a/src/js/game/hud/parts/settings_menu.js +++ b/src/js/game/hud/parts/settings_menu.js @@ -13,17 +13,19 @@ export class HUDSettingsMenu extends BaseHUDPart { this.menuElement = makeDiv(this.background, null, ["menuElement"]); - this.statsElement = makeDiv( - this.background, - null, - ["statsElement"], - ` + if (this.root.gameMode.hasHub()) { + this.statsElement = makeDiv( + this.background, + null, + ["statsElement"], + ` ${T.ingame.settingsMenu.beltsPlaced} ${T.ingame.settingsMenu.buildingsPlaced} ${T.ingame.settingsMenu.playtime} ` - ); + ); + } this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]); @@ -94,23 +96,25 @@ export class HUDSettingsMenu extends BaseHUDPart { const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60); - /** @type {HTMLElement} */ - const playtimeElement = this.statsElement.querySelector(".playtime"); - /** @type {HTMLElement} */ - const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced"); - /** @type {HTMLElement} */ - const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced"); + if (this.root.gameMode.hasHub()) { + /** @type {HTMLElement} */ + const playtimeElement = this.statsElement.querySelector(".playtime"); + /** @type {HTMLElement} */ + const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced"); + /** @type {HTMLElement} */ + const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced"); - playtimeElement.innerText = T.global.time.xMinutes.replace("", `${totalMinutesPlayed}`); + playtimeElement.innerText = T.global.time.xMinutes.replace("", `${totalMinutesPlayed}`); - buildingsPlacedElement.innerText = formatBigNumberFull( - this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - - this.root.entityMgr.getAllWithComponent(BeltComponent).length - ); + buildingsPlacedElement.innerText = formatBigNumberFull( + this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - + this.root.entityMgr.getAllWithComponent(BeltComponent).length + ); - beltsPlacedElement.innerText = formatBigNumberFull( - this.root.entityMgr.getAllWithComponent(BeltComponent).length - ); + beltsPlacedElement.innerText = formatBigNumberFull( + this.root.entityMgr.getAllWithComponent(BeltComponent).length + ); + } } close() { diff --git a/src/js/game/hud/parts/waypoints.js b/src/js/game/hud/parts/waypoints.js index 1a0e3739..2e0bc159 100644 --- a/src/js/game/hud/parts/waypoints.js +++ b/src/js/game/hud/parts/waypoints.js @@ -100,16 +100,14 @@ export class HUDWaypoints extends BaseHUDPart { this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png"); - /** @type {Array} - */ - this.waypoints = [ - { - label: null, - center: { x: 0, y: 0 }, - zoomLevel: 3, - layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(), - }, - ]; + /** @type {Array} */ + this.waypoints = []; + this.waypoints.push({ + label: null, + center: { x: 0, y: 0 }, + zoomLevel: 3, + layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(), + }); // Create a buffer we can use to measure text this.dummyBuffer = makeOffscreenBuffer(1, 1, { diff --git a/src/js/game/hud/parts/wires_overlay.js b/src/js/game/hud/parts/wires_overlay.js index 2fd3092c..328d6689 100644 --- a/src/js/game/hud/parts/wires_overlay.js +++ b/src/js/game/hud/parts/wires_overlay.js @@ -28,6 +28,9 @@ export class HUDWiresOverlay extends BaseHUDPart { * Switches between layers */ switchLayers() { + if (!this.root.gameMode.getSupportsWires()) { + return; + } if (this.root.currentLayer === "regular") { if ( this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) || diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 13f33d66..41208d13 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -49,6 +49,11 @@ export const KEYMAPPINGS = { }, buildings: { + // Puzzle buildings + constant_producer: { keyCode: key("H") }, + goal_acceptor: { keyCode: key("N") }, + block: { keyCode: key("4") }, + // Primary Toolbar belt: { keyCode: key("1") }, balancer: { keyCode: key("2") }, @@ -102,6 +107,7 @@ export const KEYMAPPINGS = { massSelectSelectMultiple: { keyCode: 16 }, // SHIFT massSelectCopy: { keyCode: key("C") }, massSelectCut: { keyCode: key("X") }, + massSelectClear: { keyCode: key("B") }, confirmMassDelete: { keyCode: 46 }, // DEL pasteLastBlueprint: { keyCode: key("V") }, }, @@ -262,6 +268,8 @@ export function getStringForKeyCode(code) { return "."; case 191: return "/"; + case 192: + return "`"; case 219: return "["; case 220: @@ -322,6 +330,15 @@ export class Keybinding { this.signal.add(receiver, scope); } + /** + * Adds an event listener + * @param {function() : void} receiver + * @param {object=} scope + */ + addToTop(receiver, scope = null) { + this.signal.addToTop(receiver, scope); + } + /** * @param {Element} elem * @returns {HTMLElement} the created element, or null if the keybindings are not shown diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 7ec7b8ab..20caca31 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -4,6 +4,7 @@ import { STOP_PROPAGATION } from "../core/signal"; import { round2Digits } from "../core/utils"; import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector"; import { getBuildingDataFromCode } from "./building_codes"; +import { Component } from "./component"; import { enumWireVariant } from "./components/wire"; import { Entity } from "./entity"; import { CHUNK_OVERLAY_RES } from "./map_chunk_view"; @@ -161,13 +162,34 @@ export class GameLogic { return returnValue; } + /** + * Performs a immutable operation, causing no recalculations + * @param {function} operation + */ + performImmutableOperation(operation) { + logger.warn("Running immutable operation ..."); + assert(!this.root.immutableOperationRunning, "Can not run two immutalbe operations twice"); + this.root.immutableOperationRunning = true; + const now = performance.now(); + const returnValue = operation(); + const duration = performance.now() - now; + logger.log("Done in", round2Digits(duration), "ms"); + assert( + this.root.immutableOperationRunning, + "Immutable operation = false while immutable operation was running" + ); + this.root.immutableOperationRunning = false; + this.root.signals.immutableOperationFinished.dispatch(); + return returnValue; + } + /** * Returns whether the given building can get removed * @param {Entity} building */ canDeleteBuilding(building) { const staticComp = building.components.StaticMapEntity; - return staticComp.getMetaBuilding().getIsRemovable(); + return staticComp.getMetaBuilding().getIsRemovable(this.root); } /** @@ -342,8 +364,6 @@ export class GameLogic { return !!overlayMatrix[localPosition.x + localPosition.y * 3]; } - g(tile, edge) {} - /** * Returns the acceptors and ejectors which affect the current tile * @param {Vector} tile @@ -425,4 +445,15 @@ export class GameLogic { } return { ejectors, acceptors }; } + + /** + * Clears all belts and items + */ + clearAllBeltsAndItems() { + for (const entity of this.root.entityMgr.entities) { + for (const component of Object.values(entity.components)) { + /** @type {Component} */ (component).clear(); + } + } + } } diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 848afbab..131ce37b 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -41,7 +41,14 @@ export class MapChunkView extends MapChunk { */ drawBackgroundLayer(parameters) { const systems = this.root.systemMgr.systems; - systems.mapResources.drawChunk(parameters, this); + if (systems.zone) { + systems.zone.drawChunk(parameters, this); + } + + if (this.root.gameMode.hasResources()) { + systems.mapResources.drawChunk(parameters, this); + } + systems.beltUnderlays.drawChunk(parameters, this); systems.belt.drawChunk(parameters, this); } @@ -69,6 +76,8 @@ export class MapChunkView extends MapChunk { systems.lever.drawChunk(parameters, this); systems.display.drawChunk(parameters, this); systems.storage.drawChunk(parameters, this); + systems.constantProducer.drawChunk(parameters, this); + systems.goalAcceptor.drawChunk(parameters, this); systems.itemProcessorOverlays.drawChunk(parameters, this); } diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js index 9deee272..f3df0b62 100644 --- a/src/js/game/meta_building.js +++ b/src/js/game/meta_building.js @@ -108,9 +108,10 @@ export class MetaBuilding { /** * Returns whether this building is removable + * @param {GameRoot} root * @returns {boolean} */ - getIsRemovable() { + getIsRemovable(root) { return true; } diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 0613103e..0c93153d 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -4,11 +4,14 @@ import { T } from "../translations"; import { MetaAnalyzerBuilding } from "./buildings/analyzer"; import { enumBalancerVariants, 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 { 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"; @@ -45,6 +48,7 @@ export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaStorageBuilding); gMetaBuildingRegistry.register(MetaBeltBuilding); gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding); + gMetaBuildingRegistry.register(MetaGoalAcceptorBuilding); gMetaBuildingRegistry.register(MetaHubBuilding); gMetaBuildingRegistry.register(MetaWireBuilding); gMetaBuildingRegistry.register(MetaConstantSignalBuilding); @@ -59,6 +63,8 @@ export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaAnalyzerBuilding); gMetaBuildingRegistry.register(MetaComparatorBuilding); gMetaBuildingRegistry.register(MetaItemProducerBuilding); + gMetaBuildingRegistry.register(MetaConstantProducerBuilding); + gMetaBuildingRegistry.register(MetaBlockBuilding); // Belt registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0); @@ -165,6 +171,15 @@ export function initMetaBuildingRegistry() { // 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( diff --git a/src/js/game/modes/puzzle.js b/src/js/game/modes/puzzle.js new file mode 100644 index 00000000..4bf3b1e6 --- /dev/null +++ b/src/js/game/modes/puzzle.js @@ -0,0 +1,106 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { Rectangle } from "../../core/rectangle"; +import { types } from "../../savegame/serialization"; +import { enumGameModeTypes, GameMode } from "../game_mode"; +import { HUDPuzzleBackToMenu } from "../hud/parts/puzzle_back_to_menu"; +import { HUDPuzzleDLCLogo } from "../hud/parts/puzzle_dlc_logo"; + +export class PuzzleGameMode extends GameMode { + static getType() { + return enumGameModeTypes.puzzle; + } + + /** @returns {object} */ + static getSchema() { + return { + zoneHeight: types.uint, + zoneWidth: types.uint, + }; + } + + /** @param {GameRoot} root */ + constructor(root) { + super(root); + + const data = this.getSaveData(); + + this.additionalHudParts = { + puzzleBackToMenu: HUDPuzzleBackToMenu, + puzzleDlcLogo: HUDPuzzleDLCLogo, + }; + + this.zoneWidth = data.zoneWidth || 8; + this.zoneHeight = data.zoneHeight || 6; + } + + /** + * @param {typeof import("../meta_building").MetaBuilding} building + */ + isBuildingExcluded(building) { + return this.hiddenBuildings.indexOf(building) >= 0; + } + + getSaveData() { + const save = this.root.savegame.getCurrentDump(); + if (!save) { + return {}; + } + return save.gameMode.data; + } + + getCameraBounds() { + return Rectangle.centered(this.zoneWidth + 20, this.zoneHeight + 20); + } + + getBuildableZones() { + return [Rectangle.centered(this.zoneWidth, this.zoneHeight)]; + } + + hasHub() { + return false; + } + + hasResources() { + return false; + } + + getMinimumZoom() { + return 1; + } + + getMaximumZoom() { + return 4; + } + + getIsSaveable() { + return false; + } + + getSupportsCopyPaste() { + return false; + } + + throughputDoesNotMatter() { + return true; + } + + getSupportsWires() { + return false; + } + + getFixedTickrate() { + return 300; + } + + getIsDeterministic() { + return true; + } + + /** @returns {boolean} */ + getIsFreeplayAvailable() { + return true; + } +} diff --git a/src/js/game/modes/puzzle_edit.js b/src/js/game/modes/puzzle_edit.js new file mode 100644 index 00000000..e3d2e40d --- /dev/null +++ b/src/js/game/modes/puzzle_edit.js @@ -0,0 +1,66 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { enumGameModeIds } from "../game_mode"; +import { PuzzleGameMode } from "./puzzle"; +import { MetaStorageBuilding } from "../buildings/storage"; +import { MetaReaderBuilding } from "../buildings/reader"; +import { MetaFilterBuilding } from "../buildings/filter"; +import { MetaDisplayBuilding } from "../buildings/display"; +import { MetaLeverBuilding } from "../buildings/lever"; +import { MetaItemProducerBuilding } from "../buildings/item_producer"; +import { MetaMinerBuilding } from "../buildings/miner"; +import { MetaWireBuilding } from "../buildings/wire"; +import { MetaWireTunnelBuilding } from "../buildings/wire_tunnel"; +import { MetaConstantSignalBuilding } from "../buildings/constant_signal"; +import { MetaLogicGateBuilding } from "../buildings/logic_gate"; +import { MetaVirtualProcessorBuilding } from "../buildings/virtual_processor"; +import { MetaAnalyzerBuilding } from "../buildings/analyzer"; +import { MetaComparatorBuilding } from "../buildings/comparator"; +import { MetaTransistorBuilding } from "../buildings/transistor"; +import { HUDPuzzleEditorControls } from "../hud/parts/puzzle_editor_controls"; +import { HUDPuzzleEditorReview } from "../hud/parts/puzzle_editor_review"; +import { HUDPuzzleEditorSettings } from "../hud/parts/puzzle_editor_settings"; + +export class PuzzleEditGameMode extends PuzzleGameMode { + static getId() { + return enumGameModeIds.puzzleEdit; + } + + static getSchema() { + return {}; + } + + /** @param {GameRoot} root */ + constructor(root) { + super(root); + + this.hiddenBuildings = [ + MetaStorageBuilding, + MetaReaderBuilding, + MetaFilterBuilding, + MetaDisplayBuilding, + MetaLeverBuilding, + MetaItemProducerBuilding, + MetaMinerBuilding, + + MetaWireBuilding, + MetaWireTunnelBuilding, + MetaConstantSignalBuilding, + MetaLogicGateBuilding, + MetaVirtualProcessorBuilding, + MetaAnalyzerBuilding, + MetaComparatorBuilding, + MetaTransistorBuilding, + ]; + + this.additionalHudParts.puzzleEditorControls = HUDPuzzleEditorControls; + this.additionalHudParts.puzzleEditorReview = HUDPuzzleEditorReview; + this.additionalHudParts.puzzleEditorSettings = HUDPuzzleEditorSettings; + } + + getIsEditor() { + return true; + } +} diff --git a/src/js/game/modes/puzzle_play.js b/src/js/game/modes/puzzle_play.js new file mode 100644 index 00000000..46480c51 --- /dev/null +++ b/src/js/game/modes/puzzle_play.js @@ -0,0 +1,193 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { enumGameModeIds } from "../game_mode"; +import { PuzzleGameMode } from "./puzzle"; +import { MetaStorageBuilding } from "../buildings/storage"; +import { MetaReaderBuilding } from "../buildings/reader"; +import { MetaFilterBuilding } from "../buildings/filter"; +import { MetaDisplayBuilding } from "../buildings/display"; +import { MetaLeverBuilding } from "../buildings/lever"; +import { MetaItemProducerBuilding } from "../buildings/item_producer"; +import { MetaMinerBuilding } from "../buildings/miner"; +import { MetaWireBuilding } from "../buildings/wire"; +import { MetaWireTunnelBuilding } from "../buildings/wire_tunnel"; +import { MetaConstantSignalBuilding } from "../buildings/constant_signal"; +import { MetaLogicGateBuilding } from "../buildings/logic_gate"; +import { MetaVirtualProcessorBuilding } from "../buildings/virtual_processor"; +import { MetaAnalyzerBuilding } from "../buildings/analyzer"; +import { MetaComparatorBuilding } from "../buildings/comparator"; +import { MetaTransistorBuilding } from "../buildings/transistor"; +import { MetaConstantProducerBuilding } from "../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor"; +import { PuzzleSerializer } from "../../savegame/puzzle_serializer"; +import { T } from "../../translations"; +import { HUDPuzzlePlayMetadata } from "../hud/parts/puzzle_play_metadata"; +import { createLogger } from "../../core/logging"; +import { HUDPuzzleCompleteNotification } from "../hud/parts/puzzle_complete_notification"; +import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings"; +import { MetaBlockBuilding } from "../buildings/block"; +import { MetaBuilding } from "../meta_building"; +import { gMetaBuildingRegistry } from "../../core/global_registries"; + +const logger = createLogger("puzzle-play"); +const copy = require("clipboard-copy"); + +export class PuzzlePlayGameMode extends PuzzleGameMode { + static getId() { + return enumGameModeIds.puzzlePlay; + } + + /** + * @param {GameRoot} root + * @param {object} payload + * @param {import("../../savegame/savegame_typedefs").PuzzleFullData} payload.puzzle + */ + constructor(root, { puzzle }) { + super(root); + + /** @type {Array} */ + let excludedBuildings = [ + MetaConstantProducerBuilding, + MetaGoalAcceptorBuilding, + MetaBlockBuilding, + + MetaStorageBuilding, + MetaReaderBuilding, + MetaFilterBuilding, + MetaDisplayBuilding, + MetaLeverBuilding, + MetaItemProducerBuilding, + MetaMinerBuilding, + + MetaWireBuilding, + MetaWireTunnelBuilding, + MetaConstantSignalBuilding, + MetaLogicGateBuilding, + MetaVirtualProcessorBuilding, + MetaAnalyzerBuilding, + MetaComparatorBuilding, + MetaTransistorBuilding, + ]; + + if (puzzle.game.excludedBuildings) { + /** + * @type {any} + */ + const puzzleHidden = puzzle.game.excludedBuildings + .map(id => { + if (!gMetaBuildingRegistry.hasId(id)) { + return; + } + return gMetaBuildingRegistry.findById(id).constructor; + }) + .filter(x => !!x); + excludedBuildings = excludedBuildings.concat(puzzleHidden); + } + + this.hiddenBuildings = excludedBuildings; + + this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata; + this.additionalHudParts.puzzlePlaySettings = HUDPuzzlePlaySettings; + this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification; + + root.signals.postLoadHook.add(this.loadPuzzle, this); + + this.puzzle = puzzle; + } + + loadPuzzle() { + let errorText; + logger.log("Loading puzzle", this.puzzle); + + try { + this.zoneWidth = this.puzzle.game.bounds.w; + this.zoneHeight = this.puzzle.game.bounds.h; + errorText = new PuzzleSerializer().deserializePuzzle(this.root, this.puzzle.game); + } catch (ex) { + errorText = ex.message || ex; + } + + if (errorText) { + this.root.gameState.moveToState("PuzzleMenuState", { + error: { + title: T.dialogs.puzzleLoadError.title, + desc: T.dialogs.puzzleLoadError.desc + " " + errorText, + }, + }); + // const signals = this.root.hud.parts.dialogs.showWarning( + // T.dialogs.puzzleLoadError.title, + // T.dialogs.puzzleLoadError.desc + " " + errorText + // ); + // signals.ok.add(() => this.root.gameState.moveToState("PuzzleMenuState")); + } + } + + /** + * + * @param {boolean} liked + * @param {number} time + */ + trackCompleted(liked, time) { + const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(); + + return this.root.app.clientApi + .apiCompletePuzzle(this.puzzle.meta.id, { + time, + liked, + }) + .catch(err => { + logger.warn("Failed to complete puzzle:", err); + }) + .then(() => { + closeLoading(); + }); + } + + sharePuzzle() { + copy(this.puzzle.meta.shortKey); + + this.root.hud.parts.dialogs.showInfo( + T.dialogs.puzzleShare.title, + T.dialogs.puzzleShare.desc.replace("", this.puzzle.meta.shortKey) + ); + } + + reportPuzzle() { + const { optionSelected } = this.root.hud.parts.dialogs.showOptionChooser( + T.dialogs.puzzleReport.title, + { + options: [ + { value: "profane", text: T.dialogs.puzzleReport.options.profane }, + { value: "unsolvable", text: T.dialogs.puzzleReport.options.unsolvable }, + { value: "trolling", text: T.dialogs.puzzleReport.options.trolling }, + ], + } + ); + + return new Promise(resolve => { + optionSelected.add(option => { + const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(); + + this.root.app.clientApi.apiReportPuzzle(this.puzzle.meta.id, option).then( + () => { + closeLoading(); + const { ok } = this.root.hud.parts.dialogs.showInfo( + T.dialogs.puzzleReportComplete.title, + T.dialogs.puzzleReportComplete.desc + ); + ok.add(resolve); + }, + err => { + closeLoading(); + const { ok } = this.root.hud.parts.dialogs.showInfo( + T.dialogs.puzzleReportError.title, + T.dialogs.puzzleReportError.desc + " " + err + ); + } + ); + }); + }); + } +} diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index e99f4a7c..0b71ff39 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -1,19 +1,74 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +import { MetaBuilding } from "../meta_building"; +/* typehints:end */ + import { findNiceIntegerValue } from "../../core/utils"; -import { GameMode } from "../game_mode"; +import { MetaConstantProducerBuilding } from "../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor"; +import { enumGameModeIds, enumGameModeTypes, GameMode } from "../game_mode"; import { ShapeDefinition } from "../shape_definition"; import { enumHubGoalRewards } from "../tutorial_goals"; - -const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; -const finalGameShape = "RuCw--Cw:----Ru--"; +import { HUDWiresToolbar } from "../hud/parts/wires_toolbar"; +import { HUDBlueprintPlacer } from "../hud/parts/blueprint_placer"; +import { HUDUnlockNotification } from "../hud/parts/unlock_notification"; +import { HUDMassSelector } from "../hud/parts/mass_selector"; +import { HUDShop } from "../hud/parts/shop"; +import { HUDWaypoints } from "../hud/parts/waypoints"; +import { HUDStatistics } from "../hud/parts/statistics"; +import { HUDWireInfo } from "../hud/parts/wire_info"; +import { HUDLeverToggle } from "../hud/parts/lever_toggle"; +import { HUDPinnedShapes } from "../hud/parts/pinned_shapes"; +import { HUDNotifications } from "../hud/parts/notifications"; +import { HUDScreenshotExporter } from "../hud/parts/screenshot_exporter"; +import { HUDWiresOverlay } from "../hud/parts/wires_overlay"; +import { HUDShapeViewer } from "../hud/parts/shape_viewer"; +import { HUDLayerPreview } from "../hud/parts/layer_preview"; +import { HUDTutorialVideoOffer } from "../hud/parts/tutorial_video_offer"; +import { HUDMinerHighlight } from "../hud/parts/miner_highlight"; +import { HUDGameMenu } from "../hud/parts/game_menu"; +import { HUDConstantSignalEdit } from "../hud/parts/constant_signal_edit"; +import { IS_MOBILE } from "../../core/config"; +import { HUDKeybindingOverlay } from "../hud/parts/keybinding_overlay"; +import { HUDWatermark } from "../hud/parts/watermark"; +import { HUDStandaloneAdvantages } from "../hud/parts/standalone_advantages"; +import { HUDCatMemes } from "../hud/parts/cat_memes"; +import { HUDPartTutorialHints } from "../hud/parts/tutorial_hints"; +import { HUDInteractiveTutorial } from "../hud/parts/interactive_tutorial"; +import { HUDSandboxController } from "../hud/parts/sandbox_controller"; +import { queryParamOptions } from "../../core/query_parameters"; +import { MetaBlockBuilding } from "../buildings/block"; + +/** @typedef {{ + * shape: string, + * amount: number + * }} UpgradeRequirement */ + +/** @typedef {{ + * required: Array + * improvement?: number, + * excludePrevious?: boolean + * }} TierRequirement */ + +/** @typedef {Array} UpgradeTiers */ + +/** @typedef {{ + * shape: string, + * required: number, + * reward: enumHubGoalRewards, + * throughputOnly?: boolean + * }} LevelDefinition */ + +export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; +export const finalGameShape = "RuCw--Cw:----Ru--"; const preparementShape = "CpRpCp--:SwSwSwSw"; -const blueprintShape = "CbCbCbRb:CwCwCwCw"; // Tiers need % of the previous tier as requirement too const tierGrowth = 2.5; /** * Generates all upgrades - * @returns {Object} */ + * @returns {Object} */ function generateUpgrades(limitedVersion = false) { const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1; @@ -87,7 +142,14 @@ function generateUpgrades(limitedVersion = false) { required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }], }, { - required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }], + required: [ + { + shape: G_CHINA_VERSION + ? "CyCyCyCy:CyCyCyCy:RyRyRyRy:RuRuRuRu" + : "CbRbRbCb:CwCwCwCw:WbWbWbWb", + amount: 50000, + }, + ], }, { required: [{ shape: preparementShape, amount: 25000 }], @@ -141,7 +203,12 @@ function generateUpgrades(limitedVersion = false) { required: [{ shape: "WrWrWrWr", amount: 3800 }], }, { - required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }], + required: [ + { + shape: G_CHINA_VERSION ? "CuCuCuCu:CwCwCwCw:Sb--Sr--" : "RpRpRpRp:CwCwCwCw", + amount: 6500, + }, + ], }, { required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }], @@ -315,7 +382,7 @@ export function generateLevelDefinitions(limitedVersion = false) { // 13 // Tunnel Tier 2 { - shape: "RpRpRpRp:CwCwCwCw", // painting t3 + shape: G_CHINA_VERSION ? "CuCuCuCu:CwCwCwCw:Sb--Sr--" : "RpRpRpRp:CwCwCwCw", // painting t3 required: 3800, reward: enumHubGoalRewards.reward_underground_belt_tier_2, }, @@ -324,7 +391,7 @@ export function generateLevelDefinitions(limitedVersion = false) { ...(limitedVersion ? [ { - shape: "RpRpRpRp:CwCwCwCw", + shape: G_CHINA_VERSION ? "CuCuCuCu:CwCwCwCw:Sb--Sr--" : "RpRpRpRp:CwCwCwCw", required: 0, reward: enumHubGoalRewards.reward_demo_end, }, @@ -358,7 +425,9 @@ export function generateLevelDefinitions(limitedVersion = false) { // 17 // Double painter { - shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants) + shape: G_CHINA_VERSION + ? "CyCyCyCy:CyCyCyCy:RyRyRyRy:RuRuRuRu" + : "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants) required: 20000, reward: enumHubGoalRewards.reward_painter_double, }, @@ -398,7 +467,9 @@ export function generateLevelDefinitions(limitedVersion = false) { // 22 // Constant signal { - shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy", + shape: G_CHINA_VERSION + ? "RrSySrSy:RyCrCwCr:CyCyRyCy" + : "Cg----Cr:Cw----Cw:Sy------:Cy----Cy", required: 25000, reward: enumHubGoalRewards.reward_constant_signal, }, @@ -406,14 +477,18 @@ export function generateLevelDefinitions(limitedVersion = false) { // 23 // Display { - shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy", + shape: G_CHINA_VERSION + ? "CrCrCrCr:CwCwCwCw:WwWwWwWw:CrCrCrCr" + : "CcSyCcSy:SyCcSyCc:CcSyCcSy", required: 25000, reward: enumHubGoalRewards.reward_display, }, // 24 Logic gates { - shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy", + shape: G_CHINA_VERSION + ? "Su----Su:RwRwRwRw:Cu----Cu:CwCwCwCw" + : "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy", required: 25000, reward: enumHubGoalRewards.reward_logic_gates, }, @@ -454,27 +529,90 @@ const fullVersionLevels = generateLevelDefinitions(false); const demoVersionLevels = generateLevelDefinitions(true); export class RegularGameMode extends GameMode { + static getId() { + return enumGameModeIds.regular; + } + + static getType() { + return enumGameModeTypes.default; + } + + /** @param {GameRoot} root */ constructor(root) { super(root); + + this.additionalHudParts = { + wiresToolbar: HUDWiresToolbar, + blueprintPlacer: HUDBlueprintPlacer, + unlockNotification: HUDUnlockNotification, + massSelector: HUDMassSelector, + shop: HUDShop, + statistics: HUDStatistics, + waypoints: HUDWaypoints, + wireInfo: HUDWireInfo, + leverToggle: HUDLeverToggle, + pinnedShapes: HUDPinnedShapes, + notifications: HUDNotifications, + screenshotExporter: HUDScreenshotExporter, + wiresOverlay: HUDWiresOverlay, + shapeViewer: HUDShapeViewer, + layerPreview: HUDLayerPreview, + minerHighlight: HUDMinerHighlight, + tutorialVideoOffer: HUDTutorialVideoOffer, + gameMenu: HUDGameMenu, + constantSignalEdit: HUDConstantSignalEdit, + }; + + if (!IS_MOBILE) { + this.additionalHudParts.keybindingOverlay = HUDKeybindingOverlay; + } + + if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) { + this.additionalHudParts.watermark = HUDWatermark; + this.additionalHudParts.standaloneAdvantages = HUDStandaloneAdvantages; + this.additionalHudParts.catMemes = HUDCatMemes; + } + + if (this.root.app.settings.getAllSettings().offerHints) { + this.additionalHudParts.tutorialHints = HUDPartTutorialHints; + this.additionalHudParts.interactiveTutorial = HUDInteractiveTutorial; + } + + // @ts-ignore + if (queryParamOptions.sandboxMode || window.sandboxMode || G_IS_DEV) { + this.additionalHudParts.sandboxController = HUDSandboxController; + } + + /** @type {(typeof MetaBuilding)[]} */ + this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding]; } + /** + * Should return all available upgrades + * @returns {Object} + */ getUpgrades() { return this.root.app.restrictionMgr.getHasExtendedUpgrades() ? fullVersionUpgrades : demoVersionUpgrades; } - getIsFreeplayAvailable() { - return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay(); - } - - getBlueprintShapeKey() { - return blueprintShape; - } - + /** + * Returns the goals for all levels including their reward + * @returns {Array} + */ getLevelDefinitions() { return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay() ? fullVersionLevels : demoVersionLevels; } + + /** + * Should return whether free play is available or if the game stops + * after the predefined levels + * @returns {boolean} + */ + getIsFreeplayAvailable() { + return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay(); + } } diff --git a/src/js/game/root.js b/src/js/game/root.js index 82d1e49f..64004e9d 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -79,6 +79,11 @@ export class GameRoot { */ this.bulkOperationRunning = false; + /** + * Whether a immutable operation is running + */ + this.immutableOperationRunning = false; + //////// Other properties /////// /** @type {Camera} */ @@ -169,6 +174,7 @@ export class GameRoot { itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()), bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), + immutableOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()), @@ -183,6 +189,9 @@ export class GameRoot { // Called with an achievement key and necessary args to validate it can be unlocked. achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()), bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()), + + // Puzzle mode + puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()), }; // RNG's diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 10543e6c..00491eff 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -164,7 +164,10 @@ export class BeltSystem extends GameSystemWithFilter { // Compute delta to see if anything changed const newDirection = arrayBeltVariantToRotation[rotationVariant]; - if (targetStaticComp.rotation !== rotation || newDirection !== targetBeltComp.direction) { + if ( + !this.root.immutableOperationRunning && + (targetStaticComp.rotation !== rotation || newDirection !== targetBeltComp.direction) + ) { const originalPath = targetBeltComp.assignedPath; // Ok, first remove it from its current path diff --git a/src/js/game/systems/belt_reader.js b/src/js/game/systems/belt_reader.js index fbd00b6c..f6080aa9 100644 --- a/src/js/game/systems/belt_reader.js +++ b/src/js/game/systems/belt_reader.js @@ -14,7 +14,6 @@ export class BeltReaderSystem extends GameSystemWithFilter { const minimumTimeForThroughput = now - 1; for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; - const readerComp = entity.components.BeltReader; const pinsComp = entity.components.WiredPins; @@ -23,12 +22,14 @@ export class BeltReaderSystem extends GameSystemWithFilter { readerComp.lastItemTimes.shift(); } - pinsComp.slots[1].value = readerComp.lastItem; - pinsComp.slots[0].value = - (readerComp.lastItemTimes[readerComp.lastItemTimes.length - 1] || 0) > - minimumTimeForThroughput - ? BOOL_TRUE_SINGLETON - : BOOL_FALSE_SINGLETON; + if (!entity.components.BeltReader.isWireless()) { + pinsComp.slots[1].value = readerComp.lastItem; + pinsComp.slots[0].value = + (readerComp.lastItemTimes[readerComp.lastItemTimes.length - 1] || 0) > + minimumTimeForThroughput + ? BOOL_TRUE_SINGLETON + : BOOL_FALSE_SINGLETON; + } if (now - readerComp.lastThroughputComputation > 0.5) { // Compute throughput diff --git a/src/js/game/systems/constant_producer.js b/src/js/game/systems/constant_producer.js new file mode 100644 index 00000000..fa9f9e52 --- /dev/null +++ b/src/js/game/systems/constant_producer.js @@ -0,0 +1,66 @@ +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { Vector } from "../../core/vector"; +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]); + } + + update() { + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const producerComp = entity.components.ItemProducer; + const signalComp = entity.components.ConstantSignal; + + if (!producerComp.isWireless() || !signalComp.isWireless()) { + continue; + } + + const ejectorComp = entity.components.ItemEjector; + + ejectorComp.tryEject(0, signalComp.signal); + } + } + + /** + * + * @param {DrawParameters} parameters + * @param {MapChunk} chunk + * @returns + */ + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const producerComp = contents[i].components.ItemProducer; + const signalComp = contents[i].components.ConstantSignal; + + if (!producerComp || !producerComp.isWireless() || !signalComp || !signalComp.isWireless()) { + continue; + } + + const staticComp = contents[i].components.StaticMapEntity; + const item = signalComp.signal; + + if (!item) { + continue; + } + + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + + const localOffset = new Vector(0, 1).rotateFastMultipleOf90(staticComp.rotation); + item.drawItemCenteredClipped( + center.x + localOffset.x, + center.y + localOffset.y, + parameters, + globalConfig.tileSize * 0.65 + ); + } + } +} diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js index d698c1d5..bcaa0583 100644 --- a/src/js/game/systems/constant_signal.js +++ b/src/js/game/systems/constant_signal.js @@ -6,7 +6,7 @@ import { fillInLinkIntoTranslation } from "../../core/utils"; import { T } from "../../translations"; import { BaseItem } from "../base_item"; import { enumColors } from "../colors"; -import { ConstantSignalComponent } from "../components/constant_signal"; +import { ConstantSignalComponent, enumConstantSignalType } 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"; @@ -26,8 +26,13 @@ export class ConstantSignalSystem extends GameSystemWithFilter { // Set signals for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; - const pinsComp = entity.components.WiredPins; const signalComp = entity.components.ConstantSignal; + + if (signalComp.isWireless()) { + continue; + } + + const pinsComp = entity.components.WiredPins; pinsComp.slots[0].value = signalComp.signal; } } @@ -51,31 +56,50 @@ export class ConstantSignalSystem extends GameSystemWithFilter { label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer), placeholder: "", defaultValue: "", - validator: val => this.parseSignalCode(val), + validator: val => this.parseSignalCode(entity.components.ConstantSignal.type, val), }); - const itemInput = new FormElementItemChooser({ - id: "signalItem", - label: null, - items: [ - BOOL_FALSE_SINGLETON, - BOOL_TRUE_SINGLETON, - ...Object.values(COLOR_ITEM_SINGLETONS), - this.root.shapeDefinitionMgr.getShapeItemFromDefinition( - this.root.hubGoals.currentGoal.definition - ), + const items = [...Object.values(COLOR_ITEM_SINGLETONS)]; + + if (entity.components.ConstantSignal.type === enumConstantSignalType.wired) { + items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON); + items.push( this.root.shapeDefinitionMgr.getShapeItemFromShortKey( this.root.gameMode.getBlueprintShapeKey() - ), + ) + ); + } else if (entity.components.ConstantSignal.type === enumConstantSignalType.wireless) { + 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.editSignal.title, + title: T.dialogs.editConstantProducer.title, desc: T.dialogs.editSignal.descItems, formElements: [itemInput, signalValueInput], buttons: ["cancel:bad:escape", "ok:good:enter"], @@ -103,15 +127,22 @@ export class ConstantSignalSystem extends GameSystemWithFilter { } if (itemInput.chosenItem) { - console.log(itemInput.chosenItem); constantComp.signal = itemInput.chosenItem; } else { - constantComp.signal = this.parseSignalCode(signalValueInput.getValue()); + constantComp.signal = this.parseSignalCode( + entity.components.ConstantSignal.type, + signalValueInput.getValue() + ); } }; - dialog.buttonSignals.ok.add(closeHandler); - dialog.valueChosen.add(closeHandler); + dialog.buttonSignals.ok.add(() => { + closeHandler(); + }); + dialog.valueChosen.add(() => { + dialog.closeRequested.dispatch(); + closeHandler(); + }); // When cancelled, destroy the entity again if (deleteOnCancel) { @@ -140,10 +171,11 @@ export class ConstantSignalSystem extends GameSystemWithFilter { /** * Tries to parse a signal code + * @param {string} type * @param {string} code * @returns {BaseItem} */ - parseSignalCode(code) { + parseSignalCode(type, code) { if (!this.root || !this.root.shapeDefinitionMgr) { // Stale reference return null; @@ -155,12 +187,15 @@ export class ConstantSignalSystem extends GameSystemWithFilter { if (enumColors[codeLower]) { return COLOR_ITEM_SINGLETONS[codeLower]; } - if (code === "1" || codeLower === "true") { - return BOOL_TRUE_SINGLETON; - } - if (code === "0" || codeLower === "false") { - return BOOL_FALSE_SINGLETON; + if (type === enumConstantSignalType.wired) { + if (code === "1" || codeLower === "true") { + return BOOL_TRUE_SINGLETON; + } + + if (code === "0" || codeLower === "false") { + return BOOL_FALSE_SINGLETON; + } } if (ShapeDefinition.isValidShortKey(code)) { diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js new file mode 100644 index 00000000..75b286d3 --- /dev/null +++ b/src/js/game/systems/goal_acceptor.js @@ -0,0 +1,132 @@ +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { clamp, lerp } from "../../core/utils"; +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]); + + this.puzzleCompleted = false; + } + + update() { + const now = this.root.time.now(); + + let allAccepted = true; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const goalComp = entity.components.GoalAcceptor; + + // filter the ones which are no longer active, or which are not the same + goalComp.deliveryHistory = goalComp.deliveryHistory.filter( + d => + now - d.time < globalConfig.goalAcceptorMinimumDurationSeconds && d.item === goalComp.item + ); + + if (goalComp.deliveryHistory.length < goalComp.getRequiredDeliveryHistorySize()) { + allAccepted = false; + } + } + + if ( + !this.puzzleCompleted && + this.root.gameInitialized && + allAccepted && + !this.root.gameMode.getIsEditor() + ) { + this.root.signals.puzzleComplete.dispatch(); + this.puzzleCompleted = true; + } + } + + /** + * + * @param {DrawParameters} parameters + * @param {MapChunk} chunk + * @returns + */ + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const goalComp = contents[i].components.GoalAcceptor; + + if (!goalComp) { + continue; + } + + const staticComp = contents[i].components.StaticMapEntity; + const item = goalComp.item; + + const requiredItemsForSuccess = goalComp.getRequiredDeliveryHistorySize(); + const percentage = clamp(goalComp.deliveryHistory.length / requiredItemsForSuccess, 0, 1); + + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + if (item) { + const localOffset = new Vector(0, -1.8).rotateFastMultipleOf90(staticComp.rotation); + item.drawItemCenteredClipped( + center.x + localOffset.x, + center.y + localOffset.y, + parameters, + globalConfig.tileSize * 0.65 + ); + } + + const isValid = item && goalComp.deliveryHistory.length >= requiredItemsForSuccess; + + parameters.context.translate(center.x, center.y); + parameters.context.rotate((staticComp.rotation / 180) * Math.PI); + + parameters.context.lineWidth = 1; + parameters.context.fillStyle = "#8de255"; + parameters.context.strokeStyle = "#64666e"; + parameters.context.lineCap = "round"; + + // progress arc + + goalComp.displayPercentage = lerp(goalComp.displayPercentage, percentage, 0.3); + + const startAngle = Math.PI * 0.595; + const maxAngle = Math.PI * 1.82; + parameters.context.beginPath(); + parameters.context.arc( + 0.25, + -1.5, + 11.6, + startAngle, + startAngle + goalComp.displayPercentage * maxAngle, + false + ); + parameters.context.arc( + 0.25, + -1.5, + 15.5, + startAngle + goalComp.displayPercentage * maxAngle, + startAngle, + true + ); + parameters.context.closePath(); + parameters.context.fill(); + parameters.context.stroke(); + parameters.context.lineCap = "butt"; + + // LED indicator + + parameters.context.lineWidth = 1; + parameters.context.strokeStyle = "#64666e"; + parameters.context.fillStyle = isValid ? "#8de255" : "#ff666a"; + parameters.context.beginCircle(10, 11.8, 3); + parameters.context.fill(); + parameters.context.stroke(); + + parameters.context.rotate((-staticComp.rotation / 180) * Math.PI); + parameters.context.translate(-center.x, -center.y); + } + } +} diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 1daaad6b..db37455a 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -239,6 +239,14 @@ export class ItemEjectorSystem extends GameSystemWithFilter { return false; } + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + // + // NOTICE ! THIS CODE IS DUPLICATED IN THE BELT PATH FOR PERFORMANCE REASONS + // + //////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + const itemProcessorComp = receiver.components.ItemProcessor; if (itemProcessorComp) { // Check for potential filters diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 9775afde..e06d4a21 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -59,6 +59,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { [enumItemProcessorTypes.painterQuad]: this.process_PAINTER_QUAD, [enumItemProcessorTypes.hub]: this.process_HUB, [enumItemProcessorTypes.reader]: this.process_READER, + [enumItemProcessorTypes.goal]: this.process_GOAL, }; // Bind all handlers @@ -562,4 +563,32 @@ export class ItemProcessorSystem extends GameSystemWithFilter { this.root.hubGoals.handleDefinitionDelivered(item.definition); } } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_GOAL(payload) { + const goalComp = payload.entity.components.GoalAcceptor; + const item = payload.items[0].item; + const now = this.root.time.now(); + + if (this.root.gameMode.getIsEditor()) { + // while playing in editor, assign the item + goalComp.item = payload.items[0].item; + goalComp.deliveryHistory.push({ + item, + time: now, + }); + } else { + // otherwise, make sure it is the same, otherwise reset + if (item.equals(goalComp.item)) { + goalComp.deliveryHistory.push({ + item, + time: now, + }); + } else { + goalComp.deliveryHistory = []; + } + } + } } diff --git a/src/js/game/systems/item_producer.js b/src/js/game/systems/item_producer.js index 52edf5d1..be78e4e8 100644 --- a/src/js/game/systems/item_producer.js +++ b/src/js/game/systems/item_producer.js @@ -1,14 +1,27 @@ +/* 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; } update() { for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; + const producerComp = entity.components.ItemProducer; + const ejectorComp = entity.components.ItemEjector; + + if (producerComp.isWireless()) { + continue; + } + const pinsComp = entity.components.WiredPins; const pin = pinsComp.slots[0]; const network = pin.linkedNetwork; @@ -17,8 +30,8 @@ export class ItemProducerSystem extends GameSystemWithFilter { continue; } - const ejectorComp = entity.components.ItemEjector; - ejectorComp.tryEject(0, network.currentValue); + this.item = network.currentValue; + ejectorComp.tryEject(0, this.item); } } } diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index 7a7609f8..9b31eec1 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -224,13 +224,16 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { update() { this.staleAreaWatcher.update(); + const sender = enumUndergroundBeltMode.sender; + const now = this.root.time.now(); + for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; const undergroundComp = entity.components.UndergroundBelt; - if (undergroundComp.mode === enumUndergroundBeltMode.sender) { + if (undergroundComp.mode === sender) { this.handleSender(entity); } else { - this.handleReceiver(entity); + this.handleReceiver(entity, now); } } } @@ -327,14 +330,15 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { /** * * @param {Entity} entity + * @param {number} now */ - handleReceiver(entity) { + handleReceiver(entity, now) { const undergroundComp = entity.components.UndergroundBelt; // Try to eject items, we only check the first one because it is sorted by remaining time const nextItemAndDuration = undergroundComp.pendingItems[0]; if (nextItemAndDuration) { - if (this.root.time.now() > nextItemAndDuration[1]) { + if (now > nextItemAndDuration[1]) { const ejectorComp = entity.components.ItemEjector; const nextSlotIndex = ejectorComp.getFirstFreeSlot(); diff --git a/src/js/game/systems/zone.js b/src/js/game/systems/zone.js new file mode 100644 index 00000000..109f5166 --- /dev/null +++ b/src/js/game/systems/zone.js @@ -0,0 +1,105 @@ +/* typehints:start */ +import { DrawParameters } from "../../core/draw_parameters"; +import { MapChunkView } from "../map_chunk_view"; +import { GameRoot } from "../root"; +/* typehints:end */ + +import { globalConfig } from "../../core/config"; +import { STOP_PROPAGATION } from "../../core/signal"; +import { GameSystem } from "../game_system"; +import { THEME } from "../theme"; +import { Entity } from "../entity"; +import { Vector } from "../../core/vector"; + +export class ZoneSystem extends GameSystem { + /** @param {GameRoot} root */ + constructor(root) { + super(root); + this.drawn = false; + this.root.signals.prePlacementCheck.add(this.prePlacementCheck, this); + + this.root.signals.gameFrameStarted.add(() => { + this.drawn = false; + }); + } + + /** + * + * @param {Entity} entity + * @param {Vector | undefined} tile + * @returns + */ + prePlacementCheck(entity, tile = null) { + const staticComp = entity.components.StaticMapEntity; + + if (!staticComp) { + return; + } + + const mode = this.root.gameMode; + + const zones = mode.getBuildableZones(); + if (!zones) { + return; + } + + const transformed = staticComp.getTileSpaceBounds(); + if (tile) { + transformed.x += tile.x; + transformed.y += tile.y; + } + + if (!zones.some(zone => zone.intersectsFully(transformed))) { + return STOP_PROPAGATION; + } + } + + /** + * Draws the zone + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawChunk(parameters, chunk) { + if (this.drawn) { + // oof + return; + } + this.drawn = true; + + const mode = this.root.gameMode; + + const zones = mode.getBuildableZones(); + if (!zones) { + return; + } + + const zone = zones[0].allScaled(globalConfig.tileSize); + const context = parameters.context; + + context.lineWidth = 2; + context.strokeStyle = THEME.map.zone.borderSolid; + context.beginPath(); + context.rect(zone.x - 1, zone.y - 1, zone.w + 2, zone.h + 2); + context.stroke(); + + const outer = zone; + const padding = 40 * globalConfig.tileSize; + context.fillStyle = THEME.map.zone.outerColor; + context.fillRect(outer.x + outer.w, outer.y, padding, outer.h); + context.fillRect(outer.x - padding, outer.y, padding, outer.h); + context.fillRect( + outer.x - padding - globalConfig.tileSize, + outer.y - padding, + 2 * padding + zone.w + 2 * globalConfig.tileSize, + padding + ); + context.fillRect( + outer.x - padding - globalConfig.tileSize, + outer.y + outer.h, + 2 * padding + zone.w + 2 * globalConfig.tileSize, + padding + ); + + context.globalAlpha = 1; + } +} diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index 733b7682..cb111430 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -47,6 +47,11 @@ "textColor": "#fff", "textColorCapped": "#ef5072", "background": "rgba(40, 50, 60, 0.8)" + }, + + "zone": { + "borderSolid": "rgba(23, 192, 255, 1)", + "outerColor": "rgba(20 , 20, 25, 0.5)" } }, diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index 0c793c26..0962eb93 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -48,6 +48,11 @@ "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)" } }, diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index d1fb5305..5bb3bbba 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -185,6 +185,7 @@ declare const STOP_PROPAGATION = "stop_propagation"; declare interface TypedSignal> { add(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void, scope?: object); + addToTop(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void, scope?: object); remove(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void); dispatch(...args: T): /* STOP_PROPAGATION */ string | void; diff --git a/src/js/languages.js b/src/js/languages.js index 6899ef09..4dfb15d4 100644 --- a/src/js/languages.js +++ b/src/js/languages.js @@ -184,4 +184,12 @@ export const LANGUAGES = { code: "uk", region: "", }, + + "he": { + // hebrew + name: "עברית", + data: require("./built-temp/base-he.json"), + code: "he", + region: "", + }, }; diff --git a/src/js/main.js b/src/js/main.js index 5b9df699..94f3d37a 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -9,6 +9,7 @@ import { initComponentRegistry } from "./game/component_registry"; import { initDrawUtils } from "./core/draw_utils"; import { initItemRegistry } from "./game/item_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry"; +import { initGameModeRegistry } from "./game/game_mode_registry"; import { initGameSpeedRegistry } from "./game/game_speed_registry"; const logger = createLogger("main"); @@ -81,6 +82,7 @@ initDrawUtils(); initComponentRegistry(); initItemRegistry(); initMetaBuildingRegistry(); +initGameModeRegistry(); initGameSpeedRegistry(); let app = null; diff --git a/src/js/platform/api.js b/src/js/platform/api.js new file mode 100644 index 00000000..cbecfb15 --- /dev/null +++ b/src/js/platform/api.js @@ -0,0 +1,203 @@ +/* typehints:start */ +import { Application } from "../application"; +/* typehints:end */ +import { createLogger } from "../core/logging"; +import { compressX64 } from "../core/lzstring"; +import { T } from "../translations"; + +const logger = createLogger("puzzle-api"); +const rusha = require("rusha"); + +export class ClientAPI { + /** + * + * @param {Application} app + */ + constructor(app) { + this.app = app; + + /** + * The current users session token + * @type {string|null} + */ + this.token = null; + + this.syncToken = window.localStorage.getItem("tmp.syncToken"); + if (!this.syncToken) { + this.syncToken = rusha + .createHash() + .update(new Date().getTime() + "=" + Math.random()) + .digest("hex"); + window.localStorage.setItem("tmp.syncToken", this.syncToken); + } + } + + getEndpoint() { + if (G_IS_DEV) { + return "http://localhost:15001"; + } + if (window.location.host === "beta.shapez.io") { + return "https://api-staging.shapez.io"; + } + return "https://api.shapez.io"; + } + + isLoggedIn() { + return Boolean(this.token); + } + + /** + * + * @param {string} endpoint + * @param {object} options + * @param {"GET"|"POST"=} options.method + * @param {any=} options.body + */ + _request(endpoint, options) { + const headers = { + "x-api-key": "d5c54aaa491f200709afff082c153ef2", + "Content-Type": "application/json", + }; + + if (this.token) { + headers["x-token"] = this.token; + } + + return Promise.race([ + fetch(this.getEndpoint() + endpoint, { + cache: "no-cache", + mode: "cors", + headers, + method: options.method || "GET", + body: options.body ? JSON.stringify(options.body) : undefined, + }) + .then(res => { + if (res.status !== 200) { + throw "bad-status: " + res.status + " / " + res.statusText; + } + return res; + }) + .then(res => res.json()), + new Promise((resolve, reject) => setTimeout(() => reject("timeout"), 15000)), + ]) + .then(data => { + if (data && data.error) { + logger.warn("Got error from api:", data); + throw T.backendErrors[data.error] || data.error; + } + return data; + }) + .catch(err => { + logger.warn("Failure:", endpoint, ":", err); + throw err; + }); + } + + tryLogin() { + return this.apiTryLogin() + .then(({ token }) => { + this.token = token; + return true; + }) + .catch(err => { + logger.warn("Failed to login:", err); + return false; + }); + } + + /** + * @returns {Promise<{token: string}>} + */ + apiTryLogin() { + return this._request("/v1/public/login", { + method: "POST", + body: { + token: this.syncToken, + }, + }); + } + + /** + * @param {"new"|"top-rated"|"mine"} category + * @returns {Promise} + */ + apiListPuzzles(category) { + if (!this.isLoggedIn()) { + return Promise.reject("not-logged-in"); + } + return this._request("/v1/puzzles/list/" + category, {}); + } + + /** + * @param {number} puzzleId + * @returns {Promise} + */ + apiDownloadPuzzle(puzzleId) { + if (!this.isLoggedIn()) { + return Promise.reject("not-logged-in"); + } + return this._request("/v1/puzzles/download/" + puzzleId, {}); + } + + /** + * @param {number} shortKey + * @returns {Promise} + */ + apiDownloadPuzzleByKey(shortKey) { + if (!this.isLoggedIn()) { + return Promise.reject("not-logged-in"); + } + return this._request("/v1/puzzles/download/" + shortKey, {}); + } + + /** + * @param {number} puzzleId + * @returns {Promise} + */ + apiReportPuzzle(puzzleId, reason) { + if (!this.isLoggedIn()) { + return Promise.reject("not-logged-in"); + } + return this._request("/v1/puzzles/report/" + puzzleId, { + method: "POST", + body: { reason }, + }); + } + + /** + * @param {number} puzzleId + * @param {object} payload + * @param {number} payload.time + * @param {boolean} payload.liked + * @returns {Promise<{ success: true }>} + */ + apiCompletePuzzle(puzzleId, payload) { + if (!this.isLoggedIn()) { + return Promise.reject("not-logged-in"); + } + return this._request("/v1/puzzles/complete/" + puzzleId, { + method: "POST", + body: payload, + }); + } + + /** + * @param {object} payload + * @param {string} payload.title + * @param {string} payload.shortKey + * @param {import("../savegame/savegame_typedefs").PuzzleGameData} payload.data + * @returns {Promise<{ success: true }>} + */ + apiSubmitPuzzle(payload) { + if (!this.isLoggedIn()) { + return Promise.reject("not-logged-in"); + } + return this._request("/v1/puzzles/submit", { + method: "POST", + body: { + ...payload, + data: compressX64(JSON.stringify(payload.data)), + }, + }); + } +} diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index a3947be6..65fc5080 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -3,6 +3,7 @@ import { createLogger } from "../../core/logging"; import { queryParamOptions } from "../../core/query_parameters"; import { BeltComponent } from "../../game/components/belt"; import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; +import { RegularGameMode } from "../../game/modes/regular"; import { GameRoot } from "../../game/root"; import { InGameState } from "../../states/ingame"; import { GameAnalyticsInterface } from "../game_analytics"; @@ -163,6 +164,10 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { return; } + if (!(root.gameMode instanceof RegularGameMode)) { + return; + } + logger.log("Sending event", category, value); this.sendToApi("/v1/game-event", { diff --git a/src/js/platform/sound.js b/src/js/platform/sound.js index 9d5a8461..d43c76c2 100644 --- a/src/js/platform/sound.js +++ b/src/js/platform/sound.js @@ -35,6 +35,10 @@ export const MUSIC = { menu: "menu", }; +if (G_IS_STANDALONE || G_IS_DEV) { + MUSIC.puzzle = "puzzle-full"; +} + export class SoundInstanceInterface { constructor(key, url) { this.key = key; diff --git a/src/js/savegame/puzzle_serializer.js b/src/js/savegame/puzzle_serializer.js new file mode 100644 index 00000000..c7bfa652 --- /dev/null +++ b/src/js/savegame/puzzle_serializer.js @@ -0,0 +1,211 @@ +/* typehints:start */ +import { GameRoot } from "../game/root"; +import { PuzzleGameMode } from "../game/modes/puzzle"; +/* typehints:end */ +import { enumConstantSignalType } from "../game/components/constant_signal"; +import { StaticMapEntityComponent } from "../game/components/static_map_entity"; +import { ShapeItem } from "../game/items/shape_item"; +import { Vector } from "../core/vector"; +import { MetaConstantProducerBuilding } from "../game/buildings/constant_producer"; +import { defaultBuildingVariant, MetaBuilding } from "../game/meta_building"; +import { gMetaBuildingRegistry } from "../core/global_registries"; +import { MetaGoalAcceptorBuilding } from "../game/buildings/goal_acceptor"; +import { createLogger } from "../core/logging"; +import { BaseItem } from "../game/base_item"; +import trim from "trim"; +import { enumColors } from "../game/colors"; +import { COLOR_ITEM_SINGLETONS } from "../game/items/color_item"; +import { ShapeDefinition } from "../game/shape_definition"; +import { MetaBlockBuilding } from "../game/buildings/block"; + +const logger = createLogger("puzzle-serializer"); + +export class PuzzleSerializer { + /** + * Serializes the game root into a dump + * @param {GameRoot} root + * @returns {import("./savegame_typedefs").PuzzleGameData} + */ + generateDumpFromGameRoot(root) { + console.log("serializing", root); + + /** + * @type {import("./savegame_typedefs").PuzzleGameData["buildings"]} + */ + let buildings = []; + for (const entity of root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { + const staticComp = entity.components.StaticMapEntity; + const signalComp = entity.components.ConstantSignal; + + if (signalComp) { + assert(signalComp.type === enumConstantSignalType.wireless, "not a wireless signal"); + assert(["shape", "color"].includes(signalComp.signal.getItemType()), "not a shape signal"); + buildings.push({ + type: "emitter", + item: signalComp.signal.getAsCopyableKey(), + pos: { + x: staticComp.origin.x, + y: staticComp.origin.y, + r: staticComp.rotation, + }, + }); + continue; + } + + const goalComp = entity.components.GoalAcceptor; + if (goalComp) { + assert(goalComp.item, "goals is missing item"); + assert(goalComp.item.getItemType() === "shape", "goal is not an item"); + buildings.push({ + type: "goal", + item: goalComp.item.getAsCopyableKey(), + pos: { + x: staticComp.origin.x, + y: staticComp.origin.y, + r: staticComp.rotation, + }, + }); + continue; + } + + if (staticComp.getMetaBuilding().id === gMetaBuildingRegistry.findByClass(MetaBlockBuilding).id) { + buildings.push({ + type: "block", + pos: { + x: staticComp.origin.x, + y: staticComp.origin.y, + r: staticComp.rotation, + }, + }); + } + } + + const mode = /** @type {PuzzleGameMode} */ (root.gameMode); + + const handles = root.hud.parts.buildingsToolbar.buildingHandles; + const ids = gMetaBuildingRegistry.getAllIds(); + + /** @type {Array} */ + let excludedBuildings = []; + for (let i = 0; i < ids.length; ++i) { + const handle = handles[ids[i]]; + if (handle && handle.puzzleLocked) { + // @ts-ignore + excludedBuildings.push(handle.metaBuilding.getId()); + } + } + + return { + version: 1, + buildings, + bounds: { + w: mode.zoneWidth, + h: mode.zoneHeight, + }, + //read from the toolbar when making a puzzle + excludedBuildings, + }; + } + + /** + * Tries to parse a signal code + * @param {GameRoot} root + * @param {string} code + * @returns {BaseItem} + */ + parseItemCode(root, code) { + if (!root || !root.shapeDefinitionMgr) { + // Stale reference + return null; + } + + code = trim(code); + const codeLower = code.toLowerCase(); + + if (enumColors[codeLower]) { + return COLOR_ITEM_SINGLETONS[codeLower]; + } + + if (ShapeDefinition.isValidShortKey(code)) { + return root.shapeDefinitionMgr.getShapeItemFromShortKey(code); + } + + return null; + } + /** + * @param {GameRoot} root + * @param {import("./savegame_typedefs").PuzzleGameData} puzzle + */ + deserializePuzzle(root, puzzle) { + if (puzzle.version !== 1) { + return "invalid-version"; + } + + for (const building of puzzle.buildings) { + switch (building.type) { + case "emitter": { + const item = this.parseItemCode(root, building.item); + if (!item) { + return "bad-item:" + building.item; + } + + const entity = root.logic.tryPlaceBuilding({ + origin: new Vector(building.pos.x, building.pos.y), + building: gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding), + originalRotation: building.pos.r, + rotation: building.pos.r, + rotationVariant: 0, + variant: defaultBuildingVariant, + }); + if (!entity) { + logger.warn("Failed to place emitter:", building); + return "failed-to-place-emitter"; + } + + entity.components.ConstantSignal.signal = item; + break; + } + case "goal": { + const item = this.parseItemCode(root, building.item); + if (!item) { + return "bad-item:" + building.item; + } + const entity = root.logic.tryPlaceBuilding({ + origin: new Vector(building.pos.x, building.pos.y), + building: gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding), + originalRotation: building.pos.r, + rotation: building.pos.r, + rotationVariant: 0, + variant: defaultBuildingVariant, + }); + if (!entity) { + logger.warn("Failed to place goal:", building); + return "failed-to-place-goal"; + } + + entity.components.GoalAcceptor.item = item; + break; + } + case "block": { + const entity = root.logic.tryPlaceBuilding({ + origin: new Vector(building.pos.x, building.pos.y), + building: gMetaBuildingRegistry.findByClass(MetaBlockBuilding), + originalRotation: building.pos.r, + rotation: building.pos.r, + rotationVariant: 0, + variant: defaultBuildingVariant, + }); + if (!entity) { + logger.warn("Failed to place block:", building); + return "failed-to-place-block"; + } + break; + } + default: { + // @ts-ignore + return "invalid-building-type: " + building.type; + } + } + } + } +} diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index e56ae1dc..999b90ec 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -13,6 +13,7 @@ import { SavegameInterface_V1005 } from "./schemas/1005"; import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; +import { SavegameInterface_V1009 } from "./schemas/1009"; const logger = createLogger("savegame"); @@ -53,7 +54,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1008; + return 1009; } /** @@ -136,6 +137,11 @@ export class Savegame extends ReadWriteProxy { data.version = 1008; } + if (data.version === 1008) { + SavegameInterface_V1009.migrate1008to1009(data); + data.version = 1009; + } + return ExplainedResult.good(); } diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index 395040b3..b4dc4233 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -9,6 +9,7 @@ import { SavegameInterface_V1005 } from "./schemas/1005"; import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; +import { SavegameInterface_V1009 } from "./schemas/1009"; /** @type {Object.} */ export const savegameInterfaces = { @@ -21,6 +22,7 @@ export const savegameInterfaces = { 1006: SavegameInterface_V1006, 1007: SavegameInterface_V1007, 1008: SavegameInterface_V1008, + 1009: SavegameInterface_V1009, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index c1247225..3230cdd5 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -2,6 +2,8 @@ import { ExplainedResult } from "../core/explained_result"; import { createLogger } from "../core/logging"; import { gComponentRegistry } from "../core/global_registries"; 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 @@ -33,12 +35,13 @@ export class SavegameSerializer { camera: root.camera.serialize(), time: root.time.serialize(), map: root.map.serialize(), + gameMode: root.gameMode.serialize(), entityMgr: root.entityMgr.serialize(), hubGoals: root.hubGoals.serialize(), - pinnedShapes: root.hud.parts.pinnedShapes.serialize(), - waypoints: root.hud.parts.waypoints.serialize(), entities: this.internal.serializeEntityArray(root.entityMgr.entities), 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, }; if (G_IS_DEV) { @@ -130,12 +133,19 @@ export class SavegameSerializer { errorReason = errorReason || root.time.deserialize(savegame.time); errorReason = errorReason || root.camera.deserialize(savegame.camera); errorReason = errorReason || root.map.deserialize(savegame.map); + errorReason = errorReason || root.gameMode.deserialize(savegame.gameMode); errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals, root); - errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); - errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths); + if (root.hud.parts.pinnedShapes) { + errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); + } + + if (root.hud.parts.waypoints) { + errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); + } + // Check for errors if (errorReason) { return ExplainedResult.bad(errorReason); diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index fb872113..c5e0e5c5 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -12,6 +12,7 @@ * time: any, * entityMgr: any, * map: any, + * gameMode: object, * hubGoals: any, * pinnedShapes: any, * waypoints: any, @@ -40,4 +41,61 @@ * }} SavegamesData */ +import { MetaBuilding } from "../game/meta_building"; + +// Notice: Update backend too +/** + * @typedef {{ + * id: number; + * shortKey: string; + * likes: number; + * downloads: number; + * completions: number; + * difficulty: number | null; + * averageTime: number | null; + * title: string; + * author: string; + * completed: boolean; + * }} PuzzleMetadata + */ + +/** + * @typedef {{ + * type: "emitter"; + * item: string; + * pos: { x: number; y: number; r: number } + * }} PuzzleGameBuildingConstantProducer + */ + +/** + * @typedef {{ + * type: "goal"; + * item: string; + * pos: { x: number; y: number; r: number } + * }} PuzzleGameBuildingGoal + */ + +/** + * @typedef {{ + * type: "block"; + * pos: { x: number; y: number; r: number } + * }} PuzzleGameBuildingBlock + */ + +/** + * @typedef {{ + * version: number; + * bounds: { w: number; h: number; }, + * buildings: (PuzzleGameBuildingGoal | PuzzleGameBuildingConstantProducer | PuzzleGameBuildingBlock)[], + * excludedBuildings: Array, + * }} PuzzleGameData + */ + +/** + * @typedef {{ + * meta: PuzzleMetadata, + * game: PuzzleGameData + * }} PuzzleFullData + */ + export default {}; diff --git a/src/js/savegame/schemas/1009.js b/src/js/savegame/schemas/1009.js new file mode 100644 index 00000000..e6e1abc6 --- /dev/null +++ b/src/js/savegame/schemas/1009.js @@ -0,0 +1,34 @@ +import { createLogger } from "../../core/logging.js"; +import { RegularGameMode } from "../../game/modes/regular.js"; +import { SavegameInterface_V1008 } from "./1008.js"; + +const schema = require("./1009.json"); +const logger = createLogger("savegame_interface/1009"); + +export class SavegameInterface_V1009 extends SavegameInterface_V1008 { + getVersion() { + return 1009; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1008to1009(data) { + logger.log("Migrating 1008 to 1009"); + const dump = data.dump; + if (!dump) { + return true; + } + + dump.gameMode = { + mode: { + id: RegularGameMode.getId(), + data: {}, + }, + }; + } +} diff --git a/src/js/savegame/schemas/1009.json b/src/js/savegame/schemas/1009.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1009.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +} diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 316c536c..0dd6c72a 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -8,6 +8,7 @@ import { KeyActionMapper } from "../game/key_action_mapper"; import { Savegame } from "../savegame/savegame"; import { GameCore } from "../game/core"; import { MUSIC } from "../platform/sound"; +import { enumGameModeIds } from "../game/game_mode"; const logger = createLogger("state/ingame"); @@ -39,8 +40,14 @@ export class GameCreationPayload { /** @type {boolean|undefined} */ this.fastEnter; + /** @type {string} */ + this.gameModeId; + /** @type {Savegame} */ this.savegame; + + /** @type {object|undefined} */ + this.gameModeParameters; } } @@ -97,6 +104,9 @@ export class InGameState extends GameState { } getThemeMusic() { + if (this.creationPayload.gameModeId && this.creationPayload.gameModeId.includes("puzzle")) { + return MUSIC.puzzle; + } return MUSIC.theme; } @@ -147,7 +157,11 @@ export class InGameState extends GameState { * Goes back to the menu state */ goBackToMenu() { - this.saveThenGoToState("MainMenuState"); + if ([enumGameModeIds.puzzleEdit, enumGameModeIds.puzzlePlay].includes(this.gameModeId)) { + this.saveThenGoToState("PuzzleMenuState"); + } else { + this.saveThenGoToState("MainMenuState"); + } } /** @@ -220,7 +234,7 @@ export class InGameState extends GameState { logger.log("Creating new game core"); this.core = new GameCore(this.app); - this.core.initializeRoot(this, this.savegame); + this.core.initializeRoot(this, this.savegame, this.gameModeId); if (this.savegame.hasGameDump()) { this.stage4bResumeGame(); @@ -354,6 +368,7 @@ export class InGameState extends GameState { this.creationPayload = payload; this.savegame = payload.savegame; + this.gameModeId = payload.gameModeId; this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); this.loadingOverlay.showBasic(); @@ -361,7 +376,13 @@ export class InGameState extends GameState { // Remove unneded default element document.body.querySelector(".modalDialogParent").remove(); - this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore()); + this.asyncChannel + .watch(waitNextFrame()) + .then(() => this.stage3CreateCore()) + .catch(ex => { + logger.error(ex); + throw ex; + }); } /** @@ -433,6 +454,11 @@ export class InGameState extends GameState { logger.warn("Skipping double save and returning same promise"); return this.currentSavePromise; } + + if (!this.core.root.gameMode.getIsSaveable()) { + return Promise.resolve(); + } + logger.log("Starting to save game ..."); this.savegame.updateData(this.core.root); diff --git a/src/js/states/login.js b/src/js/states/login.js new file mode 100644 index 00000000..64f599e4 --- /dev/null +++ b/src/js/states/login.js @@ -0,0 +1,102 @@ +import { GameState } from "../core/game_state"; +import { getRandomHint } from "../game/hints"; +import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { T } from "../translations"; + +export class LoginState extends GameState { + constructor() { + super("LoginState"); + } + + getInnerHTML() { + return ` +
+
+ ${T.global.loggingIn} +
+
+ + `; + } + + /** + * + * @param {object} payload + * @param {string} payload.nextStateId + */ + onEnter(payload) { + this.payload = payload; + if (!this.payload.nextStateId) { + throw new Error("No next state id"); + } + + if (this.app.clientApi.isLoggedIn()) { + this.finishLoading(); + return; + } + + this.dialogs = new HUDModalDialogs(null, this.app); + const dialogsElement = document.body.querySelector(".modalDialogParent"); + this.dialogs.initializeToElement(dialogsElement); + + this.htmlElement.classList.add("prefab_LoadingState"); + + /** @type {HTMLElement} */ + this.hintsText = this.htmlElement.querySelector(".prefab_GameHint"); + this.lastHintShown = -1000; + this.nextHintDuration = 0; + + this.tryLogin(); + } + + tryLogin() { + this.app.clientApi.tryLogin().then(success => { + console.log("Logged in:", success); + + if (!success) { + const signals = this.dialogs.showWarning( + T.dialogs.offlineMode.title, + T.dialogs.offlineMode.desc, + ["retry", "playOffline:bad"] + ); + signals.retry.add(() => setTimeout(() => this.tryLogin(), 2000), this); + signals.playOffline.add(this.finishLoading, this); + } else { + this.finishLoading(); + } + }); + } + + finishLoading() { + this.moveToState(this.payload.nextStateId); + } + + getDefaultPreviousState() { + return "MainMenuState"; + } + + update() { + const now = performance.now(); + if (now - this.lastHintShown > this.nextHintDuration) { + this.lastHintShown = now; + const hintText = getRandomHint(); + + this.hintsText.innerHTML = hintText; + + /** + * Compute how long the user will need to read the hint. + * We calculate with 130 words per minute, with an average of 5 chars + * that is 650 characters / minute + */ + this.nextHintDuration = Math.max(2500, (hintText.length / 650) * 60 * 1000); + } + } + + onRender() { + this.update(); + } + + onBackgroundTick() { + this.update(); + } +} diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 3afad9bf..25d93d7a 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -66,7 +66,7 @@ export class MainMenuState extends GameState { shapez.io Logo - v${G_BUILD_VERSION} - Achievements! + v${G_BUILD_VERSION} - Puzzle DLC!
@@ -82,6 +82,19 @@ export class MainMenuState extends GameState { }
+ + ${ + // @TODO: Only display if DLC is owned, otherwise show ad for store page + showDemoBadges + ? "" + : ` +
+ + +
` + }