diff --git a/artwork/itch.io/background.png b/artwork/itch.io/background.png deleted file mode 100644 index 674b7fab..00000000 Binary files a/artwork/itch.io/background.png and /dev/null differ diff --git a/artwork/itch.io/banner.png b/artwork/itch.io/banner.png index 05f4cfe3..17488ee2 100644 Binary files a/artwork/itch.io/banner.png and b/artwork/itch.io/banner.png differ diff --git a/artwork/itch.io/banner.psd b/artwork/itch.io/banner.psd index 6fb9ef3c..6db16375 100644 Binary files a/artwork/itch.io/banner.psd and b/artwork/itch.io/banner.psd differ diff --git a/artwork/logo.png b/artwork/logo.png index 14141d58..75dd3a35 100644 Binary files a/artwork/logo.png and b/artwork/logo.png differ diff --git a/artwork/logo.psd b/artwork/logo.psd index 4638aa05..6e30b914 100644 Binary files a/artwork/logo.psd and b/artwork/logo.psd differ diff --git a/res/logo.png b/res/logo.png index 14141d58..75dd3a35 100644 Binary files a/res/logo.png and b/res/logo.png differ diff --git a/res/videos/level_1.webm b/res/videos/level_1.webm new file mode 100644 index 00000000..f5962a97 Binary files /dev/null and b/res/videos/level_1.webm differ diff --git a/res/videos/level_2.webm b/res/videos/level_2.webm new file mode 100644 index 00000000..5cf00271 Binary files /dev/null and b/res/videos/level_2.webm differ diff --git a/src/css/ingame_hud/tutorial_hints.scss b/src/css/ingame_hud/tutorial_hints.scss new file mode 100644 index 00000000..a57ac197 --- /dev/null +++ b/src/css/ingame_hud/tutorial_hints.scss @@ -0,0 +1,68 @@ +#ingame_HUD_TutorialHints { + position: absolute; + @include S(left, 10px); + @include S(bottom, 50px); + display: flex; + flex-direction: column; + background: rgba(50, 60, 70, 0); + + transition: all 0.2s ease-in-out; + pointer-events: all; + + transition-property: background-color, transform, bottom, left; + + @include S(padding, 5px); + video { + transition: all 0.2s ease-in-out; + transition-property: opacity, width; + @include S(width, 0px); + opacity: 0; + } + + .header { + @include PlainText; + color: #333438; + display: grid; + align-items: center; + @include S(grid-gap, 2px); + grid-template-columns: 1fr; + @include S(margin-bottom, 3px); + } + button.toggleHint { + .hide { + display: none; + } + } + + &.enlarged { + background: rgba(50, 60, 70, 0.9); + left: 50%; + bottom: 50%; + transform: translate(-50%, 50%); + + .header { + grid-template-columns: 1fr auto; + color: #fff; + } + + video { + @include InlineAnimation(0.2s ease-in-out) { + 0% { + opacity: 0; + @include S(width, 0px); + } + } + + opacity: 1; + @include S(width, 500px); + } + button.toggleHint { + .hide { + display: block; + } + .show { + display: none; + } + } + } +} diff --git a/src/css/ingame_hud/unlock_notification.scss b/src/css/ingame_hud/unlock_notification.scss index 597b45f9..5f72909d 100644 --- a/src/css/ingame_hud/unlock_notification.scss +++ b/src/css/ingame_hud/unlock_notification.scss @@ -9,6 +9,7 @@ justify-content: center; align-items: center; pointer-events: all; + @include InlineAnimation(0.1s ease-in-out) { 0% { opacity: 0; @@ -16,7 +17,7 @@ } .dialog { - background: rgba(#222428, 0.5); + // background: rgba(#222428, 0.5); @include S(border-radius, $globalBorderRadius); @include S(padding, 30px); @@ -32,7 +33,7 @@ .subTitle { @include SuperHeading; text-transform: uppercase; - @include S(font-size, 50px); + @include S(font-size, 40px); @include InlineAnimation(0.5s ease-in-out) { 0% { @@ -48,11 +49,10 @@ } .subTitle { - @include Heading; - background: $colorGreenBright; + @include PlainText; display: inline-block; - @include S(padding, 1px, 6px); - @include S(margin, 20px, 0, 20px); + @include S(margin, 0px, 0, 20px); + color: $colorGreenBright; @include S(border-radius, $globalBorderRadius); @include InlineAnimation(0.5s ease-in-out) { @@ -82,14 +82,15 @@ transform: translateX(-2vw); } } - display: grid; - grid-template-columns: auto auto; + display: flex; + flex-direction: column; align-items: center; justify-content: center; @include S(grid-gap, 10px); - .reward { + .rewardName { grid-column: 1 / 3; + display: none; @include InlineAnimation(0.5s ease-in-out) { 0% { transform: translateX(200vw); @@ -104,29 +105,63 @@ } } - .buildingExplanation { - @include S(width, 200px); - @include S(height, 200px); - display: inline-block; - background-position: center center; - background-size: cover; - background-repeat: no-repeat; - @include S(border-radius, $globalBorderRadius); - box-shadow: #{D(2px)} #{D(3px)} 0 0 rgba(0, 0, 0, 0.15); + .rewardDesc { + grid-column: 1 / 3; + @include PlainText; + @include S(margin-bottom, 15px); + color: #aaacaf; + @include S(width, 400px); + text-align: left; + strong { + color: #fff; + } + } + + .images { + display: flex; + .buildingExplanation { + @include S(width, 200px); + @include S(height, 200px); + display: inline-block; + background-position: center center; + background-size: cover; + background-repeat: no-repeat; + @include S(border-radius, $globalBorderRadius); + box-shadow: #{D(2px)} #{D(3px)} 0 0 rgba(0, 0, 0, 0.15); + } } } button.close { border: 0; - @include InlineAnimation(2s ease-in-out) { - 0% { - opacity: 0; - } - 95% { - opacity: 0; + position: relative; + @include S(margin-top, 30px); + + &:not(.unlocked) { + pointer-events: none; + opacity: 0.8; + cursor: default; + } + + &::after { + content: " "; + display: inline-block; + position: absolute; + top: 0; + left: 100%; + right: 0; + bottom: 0; + background: rgba(0, 10, 20, 0.8); + + @include InlineAnimation(10s linear) { + 0% { + left: 0; + } + 100% { + left: 100%; + } } } - @include S(margin-top, 30px); } } } diff --git a/src/css/main.scss b/src/css/main.scss index a46723cd..37f84867 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -26,6 +26,7 @@ @import "states/keybindings"; @import "states/settings"; @import "states/about"; +@import "states/mobile_warning"; @import "ingame_hud/buildings_toolbar"; @import "ingame_hud/building_placer"; @@ -44,6 +45,7 @@ @import "ingame_hud/settings_menu"; @import "ingame_hud/debug_info"; @import "ingame_hud/entity_debugger"; +@import "ingame_hud/tutorial_hints"; // prettier-ignore $elements: @@ -57,13 +59,14 @@ ingame_HUD_PlacerVariants, // Regular hud ingame_HUD_PinnedShapes, -ingame_HUD_buildings_toolbar, ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_Notifications, ingame_HUD_MassSelector, ingame_HUD_DebugInfo, ingame_HUD_EntityDebugger, +ingame_HUD_TutorialHints, +ingame_HUD_buildings_toolbar, // Overlays ingame_HUD_BetaOverlay, @@ -92,7 +95,8 @@ body.uiHidden { #ingame_HUD_GameMenu, #ingame_HUD_MassSelector, #ingame_HUD_PinnedShapes, - #ingame_HUD_Notifications { + #ingame_HUD_Notifications, + #ingame_HUD_TutorialHints { display: none !important; } } @@ -100,7 +104,7 @@ body.uiHidden { body.modalDialogActive, body.externalAdOpen, body.ingameDialogOpen { - > *:not(.ingameDialog):not(.modalDialogParent):not(.loadingDialog):not(.gameLoadingOverlay):not(#ingame_HUD_ModalDialogs) { + > *:not(.ingameDialog):not(.modalDialogParent):not(.loadingDialog):not(.gameLoadingOverlay):not(#ingame_HUD_ModalDialogs):not(.noBlur) { filter: blur(5px) !important; } } diff --git a/src/css/states/mobile_warning.scss b/src/css/states/mobile_warning.scss new file mode 100644 index 00000000..2e68b56a --- /dev/null +++ b/src/css/states/mobile_warning.scss @@ -0,0 +1,48 @@ +#state_MobileWarningState { + display: flex; + align-items: center; + background: #333438 !important; + @include S(padding, 20px); + box-sizing: border-box; + justify-content: center; + flex-direction: column; + + .logo { + width: 80%; + max-width: 200px; + margin-bottom: 10px; + } + + p { + color: #aaacaf; + display: block; + margin-bottom: 13px; + font-size: 16px; + line-height: 20px; + max-width: 300px; + text-align: left; + a { + color: $colorBlueBright; + } + } + + .standaloneLink { + width: 200px; + height: 80px; + min-height: 40px; + background: uiResource("get_on_itch_io.svg") center center / contain no-repeat; + overflow: hidden; + display: block; + text-indent: -999em; + cursor: pointer; + margin-top: 10px; + pointer-events: all; + transition: all 0.12s ease-in; + transition-property: opacity, transform; + transform: skewX(-0.5deg); + &:hover { + transform: skewX(-1deg) scale(1.02); + opacity: 0.9; + } + } +} diff --git a/src/js/application.js b/src/js/application.js index c61beb26..bb87b768 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -1,5 +1,5 @@ import { AnimationFrame } from "./core/animation_frame"; -import { performanceNow } from "./core/builtins"; +import { performanceNow, Math_min } from "./core/builtins"; import { GameState } from "./core/game_state"; import { GLOBAL_APP, setGlobalApp } from "./core/globals"; import { InputDistributor } from "./core/input_distributor"; @@ -36,6 +36,7 @@ import { KeybindingsState } from "./states/keybindings"; import { AboutState } from "./states/about"; import { PlatformWrapperImplElectron } from "./platform/electron/wrapper"; import { StorageImplElectron } from "./platform/electron/storage"; +import { MobileWarningState } from "./states/mobile_warning"; const logger = createLogger("application"); @@ -158,6 +159,7 @@ export class Application { /** @type {Array} */ const states = [ PreloadState, + MobileWarningState, MainMenuState, InGameState, SettingsState, @@ -315,7 +317,12 @@ export class Application { Loader.linkAppAfterBoot(this); - this.stateMgr.moveToState("PreloadState"); + // Check for mobile + if (IS_MOBILE) { + this.stateMgr.moveToState("MobileWarningState"); + } else { + this.stateMgr.moveToState("PreloadState"); + } // Starting rendering this.ticker.frameEmitted.add(this.onFrameEmitted, this); diff --git a/src/js/core/background_resources_loader.js b/src/js/core/background_resources_loader.js index d48f5ef4..28d414f2 100644 --- a/src/js/core/background_resources_loader.js +++ b/src/js/core/background_resources_loader.js @@ -25,13 +25,8 @@ const essentialBareGameSprites = G_ALL_UI_IMAGES; const essentialBareGameSounds = [MUSIC.theme]; const additionalGameSprites = []; -const additionalGameSounds = []; -for (const key in SOUNDS) { - additionalGameSounds.push(SOUNDS[key]); -} -for (const key in MUSIC) { - additionalGameSounds.push(MUSIC[key]); -} +// @ts-ignore +const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)]; export class BackgroundResourcesLoader { /** diff --git a/src/js/core/config.js b/src/js/core/config.js index 67277dba..7a43ffed 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -83,8 +83,8 @@ export const globalConfig = { debug: { /* dev:start */ - // fastGameEnter: true, - // noArtificialDelays: true, + fastGameEnter: true, + noArtificialDelays: true, // disableSavegameWrite: true, // showEntityBounds: true, // showAcceptorEjectors: true, diff --git a/src/js/game/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 50cb7bc1..0cfc0421 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -143,6 +143,7 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { const tier = enumUndergroundBeltVariantToTier[variant]; const targetRotation = (rotation + 180) % 360; + const targetSenderRotation = rotation; for ( let searchOffset = 1; @@ -161,12 +162,20 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { // If we encounter an underground receiver on our way which is also faced in our direction, we don't accept that break; } - return { rotation: targetRotation, rotationVariant: 1, connectedEntities: [contents], }; + } else if (staticComp.rotation === targetSenderRotation) { + // Draw connections to receivers + if (undergroundComp.mode === enumUndergroundBeltMode.receiver) { + return { + rotation: rotation, + rotationVariant: 0, + connectedEntities: [contents], + }; + } } } } diff --git a/src/js/game/camera.js b/src/js/game/camera.js index 7c902a95..e8064ba2 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -352,6 +352,9 @@ export class Camera extends BasicSerializableObject { mapper.getBinding(KEYMAPPINGS.ingame.mapMoveRight).add(() => (this.keyboardForce.x = 1)); mapper.getBinding(KEYMAPPINGS.ingame.mapMoveLeft).add(() => (this.keyboardForce.x = -1)); + mapper.getBinding(KEYMAPPINGS.ingame.mapZoomIn).add(() => (this.desiredZoom = this.zoomLevel * 1.2)); + mapper.getBinding(KEYMAPPINGS.ingame.mapZoomOut).add(() => (this.desiredZoom = this.zoomLevel * 0.8)); + mapper.getBinding(KEYMAPPINGS.ingame.centerMap).add(() => this.centerOnMap()); } @@ -832,7 +835,7 @@ export class Camera extends BasicSerializableObject { internalUpdateZooming(now, dt) { if (!this.currentlyPinching && this.desiredZoom !== null) { const diff = this.zoomLevel - this.desiredZoom; - if (Math_abs(diff) > 0.05) { + if (Math_abs(diff) > 0.0001) { let fade = 0.94; if (diff > 0) { // Zoom out faster than in diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 531d2fda..6c155165 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -188,13 +188,11 @@ export class HubGoals extends BasicSerializableObject { return; } - const reward = enumHubGoalRewards.no_reward; - this.currentGoal = { /** @type {ShapeDefinition} */ definition: this.createRandomShape(), required: 1000 + findNiceIntegerValue(this.level * 47.5), - reward, + reward: enumHubGoalRewards.no_reward_freeplay, }; } @@ -212,6 +210,13 @@ export class HubGoals extends BasicSerializableObject { this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward); } + /** + * Returns whether we are playing in free-play + */ + isFreePlay() { + return this.level >= tutorialGoals.length; + } + /** * Returns whether a given upgrade can be unlocked * @param {string} upgradeId diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index da3e1086..c89dff39 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -27,7 +27,7 @@ import { HUDEntityDebugger } from "./parts/entity_debugger"; import { KEYMAPPINGS } from "../key_action_mapper"; import { HUDWatermark } from "./parts/watermark"; import { HUDModalDialogs } from "./parts/modal_dialogs"; -import { Vector } from "../../core/vector"; +import { HUDPartTutorialHints } from "./parts/tutorial_hints"; export class GameHUD { /** @@ -64,6 +64,8 @@ export class GameHUD { notifications: new HUDNotifications(this.root), settingsMenu: new HUDSettingsMenu(this.root), + tutorialHints: new HUDPartTutorialHints(this.root), + // betaOverlay: new HUDBetaOverlay(this.root), debugInfo: new HUDDebugInfo(this.root), diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index f83970a3..fa477dbd 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -468,7 +468,11 @@ export class HUDBuildingPlacer extends BaseHUDPart { this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; } - if (!metaBuilding.getStayInPlacementMode() && !this.root.app.inputMgr.shiftIsDown) { + if ( + !metaBuilding.getStayInPlacementMode() && + !this.root.app.inputMgr.shiftIsDown && + !this.root.app.settings.getAllSettings().alwaysMultiplace + ) { // Stop placement this.currentMetaBuilding.set(null); } diff --git a/src/js/game/hud/parts/tutorial_hints.js b/src/js/game/hud/parts/tutorial_hints.js new file mode 100644 index 00000000..2b04315f --- /dev/null +++ b/src/js/game/hud/parts/tutorial_hints.js @@ -0,0 +1,101 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; +import { cachebust } from "../../../core/cachebust"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { InputReceiver } from "../../../core/input_receiver"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { tutorialGoals } from "../../tutorial_goals"; +import { TrackedState } from "../../../core/tracked_state"; + +const maxTutorialVideo = 2; + +export class HUDPartTutorialHints extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv( + parent, + "ingame_HUD_TutorialHints", + [], + ` +
+ No idea what to do? + +
+ + + ` + ); + + this.videoElement = this.element.querySelector("video"); + } + + shouldPauseGame() { + return this.enlarged; + } + + initialize() { + this.trackClicks(this.element.querySelector(".toggleHint"), this.toggleHintEnlarged); + + this.videoAttach = new DynamicDomAttach(this.root, this.videoElement, { + timeToKeepSeconds: 0.3, + }); + + this.videoAttach.update(false); + this.enlarged = false; + + this.inputReciever = new InputReceiver("tutorial_hints"); + this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + + this.domAttach = new DynamicDomAttach(this.root, this.element); + + this.currentShownLevel = new TrackedState(this.updateVideoUrl, this); + } + + updateVideoUrl(level) { + console.log("update video url.", level); + this.videoElement + .querySelector("source") + .setAttribute("src", cachebust("res/videos/level_" + level + ".webm")); + } + + close() { + this.enlarged = false; + document.body.classList.remove("ingameDialogOpen"); + this.element.classList.remove("enlarged", "noBlur"); + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + this.update(); + } + + show() { + document.body.classList.add("ingameDialogOpen"); + this.element.classList.add("enlarged", "noBlur"); + this.enlarged = true; + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.update(); + + this.videoElement.currentTime = 0; + this.videoElement.play(); + } + + update() { + this.videoAttach.update(this.enlarged); + + this.currentShownLevel.set(this.root.hubGoals.level); + + const tutorialVisible = this.root.hubGoals.level <= maxTutorialVideo; + this.domAttach.update(tutorialVisible); + } + + toggleHintEnlarged() { + if (this.enlarged) { + this.close(); + } else { + this.show(); + } + } +} diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index 44fa7deb..612d900d 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -2,18 +2,12 @@ import { globalConfig } from "../../../core/config"; import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { makeDiv } from "../../../core/utils"; import { SOUNDS } from "../../../platform/sound"; -import { MetaCutterBuilding } from "../../buildings/cutter"; -import { MetaMixerBuilding } from "../../buildings/mixer"; -import { MetaPainterBuilding } from "../../buildings/painter"; -import { MetaRotaterBuilding } from "../../buildings/rotater"; -import { MetaSplitterBuilding } from "../../buildings/splitter"; -import { MetaStackerBuilding } from "../../buildings/stacker"; -import { MetaTrashBuilding } from "../../buildings/trash"; -import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; +import { T } from "../../../translations"; +import { defaultBuildingVariant } from "../../meta_building"; import { enumHubGoalRewards } from "../../tutorial_goals"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { T } from "../../../translations"; +import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; export class HUDUnlockNotification extends BaseHUDPart { initialize() { @@ -26,10 +20,8 @@ export class HUDUnlockNotification extends BaseHUDPart { if (!(G_IS_DEV && globalConfig.debug.disableUnlockDialog)) { this.root.signals.storyGoalCompleted.add(this.showForLevel, this); } - } - shouldPauseGame() { - return this.visible; + this.buttonShowTimeout = null; } createElements(parent) { @@ -60,63 +52,50 @@ export class HUDUnlockNotification extends BaseHUDPart { ("" + level).padStart(2, "0") ); - const rewardText = T.storyRewards[reward]; + const rewardName = T.storyRewards[reward].title; - let html = - "" + - T.ingame.levelCompleteNotification.unlockText.replace("", rewardText) + - ""; + let html = ` +
+ ${T.ingame.levelCompleteNotification.unlockText.replace("", rewardName)} +
+ +
+ ${T.storyRewards[reward].desc} +
- const addBuildingExplanation = metaBuildingClass => { - const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); - html += `
`; - }; + `; - switch (reward) { - case enumHubGoalRewards.reward_cutter_and_trash: { - addBuildingExplanation(MetaCutterBuilding); - addBuildingExplanation(MetaTrashBuilding); - break; - } - case enumHubGoalRewards.reward_mixer: { - addBuildingExplanation(MetaMixerBuilding); - break; - } - - case enumHubGoalRewards.reward_painter: { - addBuildingExplanation(MetaPainterBuilding); - break; - } - - case enumHubGoalRewards.reward_rotater: { - addBuildingExplanation(MetaRotaterBuilding); - break; - } - - case enumHubGoalRewards.reward_splitter: { - addBuildingExplanation(MetaSplitterBuilding); - break; - } - - case enumHubGoalRewards.reward_stacker: { - addBuildingExplanation(MetaStackerBuilding); - break; - } - - case enumHubGoalRewards.reward_tunnel: { - addBuildingExplanation(MetaUndergroundBeltBuilding); - break; - } + html += "
"; + const gained = enumHubGoalRewardsToContentUnlocked[reward]; + if (gained) { + gained.forEach(([metaBuildingClass, variant]) => { + const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); + html += `
`; + }); } - - // addBuildingExplanation(MetaSplitterBuilding); - // addBuildingExplanation(MetaCutterBuilding); + html += "
"; this.elemContents.innerHTML = html; - this.visible = true; - this.root.soundProxy.playUi(SOUNDS.levelComplete); + + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + } + + this.buttonShowTimeout = setTimeout( + () => this.element.querySelector("button.close").classList.add("unlocked"), + G_IS_DEV ? 1000 : 10000 + ); + } + + cleanup() { + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } } requestClose() { @@ -126,10 +105,18 @@ export class HUDUnlockNotification extends BaseHUDPart { } close() { + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } this.visible = false; } update() { this.domAttach.update(this.visible); + if (!this.visible && this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } } } diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 1e7048a4..cac40524 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -33,6 +33,9 @@ export const KEYMAPPINGS = { toggleHud: { keyCode: 113 }, // F2 toggleFPSInfo: { keyCode: 115 }, // F1 + + mapZoomIn: { keyCode: 187, repeated: true }, // "+" + mapZoomOut: { keyCode: 189, repeated: true }, // "-" }, buildings: { @@ -194,13 +197,11 @@ export function getStringForKeyCode(code) { case 186: return ";"; case 187: - return "="; + return "+"; case 188: return ","; case 189: return "-"; - case 189: - return "."; case 191: return "/"; case 219: @@ -224,12 +225,14 @@ export class Keybinding { * @param {object} param0 * @param {number} param0.keyCode * @param {boolean=} param0.builtin + * @param {boolean=} param0.repeated */ - constructor(app, { keyCode, builtin = false }) { + constructor(app, { keyCode, builtin = false, repeated = false }) { assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode); this.app = app; this.keyCode = keyCode; this.builtin = builtin; + this.repeated = repeated; this.currentlyDown = false; @@ -364,7 +367,7 @@ export class KeyActionMapper { for (const key in this.keybindings) { /** @type {Keybinding} */ const binding = this.keybindings[key]; - if (binding.keyCode === keyCode && !binding.currentlyDown) { + if (binding.keyCode === keyCode && (!binding.currentlyDown || binding.repeated)) { binding.currentlyDown = true; /** @type {Signal} */ diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 93fbcb0d..b371de6e 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -88,7 +88,7 @@ export class HubSystem extends GameSystemWithFilter { context.font = "bold 11px GameFont"; context.fillStyle = "#fd0752"; context.textAlign = "center"; - context.fillText(T.storyRewards[goals.reward].toUpperCase(), pos.x, pos.y + 46); + context.fillText(T.storyRewards[goals.reward].title.toUpperCase(), pos.x, pos.y + 46); // Level context.font = "bold 11px GameFont"; diff --git a/src/js/game/tutorial_goals.js b/src/js/game/tutorial_goals.js index d22a72fe..83aca89c 100644 --- a/src/js/game/tutorial_goals.js +++ b/src/js/game/tutorial_goals.js @@ -2,6 +2,7 @@ import { ShapeDefinition } from "./shape_definition"; import { finalGameShape } from "./upgrades"; /** + * Don't forget to also update tutorial_goals_mappings.js as well as the translations! * @enum {string} */ export const enumHubGoalRewards = { @@ -25,6 +26,7 @@ export const enumHubGoalRewards = { reward_freeplay: "reward_freeplay", no_reward: "no_reward", + no_reward_freeplay: "no_reward_freeplay", }; export const tutorialGoals = [ diff --git a/src/js/game/tutorial_goals_mappings.js b/src/js/game/tutorial_goals_mappings.js new file mode 100644 index 00000000..905a623a --- /dev/null +++ b/src/js/game/tutorial_goals_mappings.js @@ -0,0 +1,51 @@ +import { MetaBuilding, defaultBuildingVariant } from "./meta_building"; +import { MetaCutterBuilding, enumCutterVariants } from "./buildings/cutter"; +import { MetaRotaterBuilding, enumRotaterVariants } from "./buildings/rotater"; +import { MetaPainterBuilding, enumPainterVariants } from "./buildings/painter"; +import { MetaMixerBuilding } from "./buildings/mixer"; +import { MetaStackerBuilding } from "./buildings/stacker"; +import { MetaSplitterBuilding, enumSplitterVariants } from "./buildings/splitter"; +import { MetaUndergroundBeltBuilding, enumUndergroundBeltVariants } from "./buildings/underground_belt"; +import { MetaMinerBuilding, enumMinerVariants } from "./buildings/miner"; +import { MetaTrashBuilding, enumTrashVariants } from "./buildings/trash"; + +/** @typedef {Array<[typeof MetaBuilding, string]>} TutorialGoalReward */ + +import { enumHubGoalRewards } from "./tutorial_goals"; + +/** + * Helper method for proper types + * @returns {TutorialGoalReward} + */ +const typed = x => x; + +/** + * Stores which reward unlocks what + * @enum {TutorialGoalReward?} + */ +export const enumHubGoalRewardsToContentUnlocked = { + [enumHubGoalRewards.reward_cutter_and_trash]: typed([[MetaCutterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_rotater]: typed([[MetaRotaterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_painter]: typed([[MetaPainterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_mixer]: typed([[MetaMixerBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_stacker]: typed([[MetaStackerBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_splitter]: typed([[MetaSplitterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_tunnel]: typed([[MetaUndergroundBeltBuilding, defaultBuildingVariant]]), + + [enumHubGoalRewards.reward_rotater_ccw]: typed([[MetaRotaterBuilding, enumRotaterVariants.ccw]]), + [enumHubGoalRewards.reward_miner_chainable]: typed([[MetaMinerBuilding, enumMinerVariants.chainable]]), + [enumHubGoalRewards.reward_underground_belt_tier_2]: typed([ + [MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2], + ]), + [enumHubGoalRewards.reward_splitter_compact]: typed([ + [MetaSplitterBuilding, enumSplitterVariants.compact], + ]), + [enumHubGoalRewards.reward_cutter_quad]: typed([[MetaCutterBuilding, enumCutterVariants.quad]]), + [enumHubGoalRewards.reward_painter_double]: typed([[MetaPainterBuilding, enumPainterVariants.double]]), + [enumHubGoalRewards.reward_painter_quad]: typed([[MetaPainterBuilding, enumPainterVariants.quad]]), + [enumHubGoalRewards.reward_storage]: typed([[MetaTrashBuilding, enumTrashVariants.storage]]), + + [enumHubGoalRewards.reward_freeplay]: null, + [enumHubGoalRewards.no_reward]: null, + [enumHubGoalRewards.no_reward_freeplay]: null, +}; diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 1164a230..2efc458f 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -23,7 +23,7 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { initialize() { this.syncKey = null; - setInterval(() => this.sendTimePoints(), 30 * 1000); + setInterval(() => this.sendTimePoints(), 120 * 1000); // Retrieve sync key from player return this.app.storage.readFileAsync(analyticsLocalFile).then( @@ -158,26 +158,29 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { const entities = root.entityMgr.getAllWithComponent(StaticMapEntityComponent); - for (let i = 0; i < entities.length; ++i) { - const entity = entities[i]; - const staticComp = entity.components.StaticMapEntity; - const payload = {}; - payload.origin = staticComp.origin; - payload.tileSize = staticComp.tileSize; - payload.rotation = staticComp.rotation; + // Limit the entities + if (entities.length < 5000) { + for (let i = 0; i < entities.length; ++i) { + const entity = entities[i]; + const staticComp = entity.components.StaticMapEntity; + const payload = {}; + payload.origin = staticComp.origin; + payload.tileSize = staticComp.tileSize; + payload.rotation = staticComp.rotation; - if (entity.components.Belt) { - payload.type = "belt"; - } else if (entity.components.UndergroundBelt) { - payload.type = "tunnel"; - } else if (entity.components.ItemProcessor) { - payload.type = entity.components.ItemProcessor.type; - } else if (entity.components.Miner) { - payload.type = "extractor"; - } else { - logger.warn("Unkown entity type", entity); + if (entity.components.Belt) { + payload.type = "belt"; + } else if (entity.components.UndergroundBelt) { + payload.type = "tunnel"; + } else if (entity.components.ItemProcessor) { + payload.type = entity.components.ItemProcessor.type; + } else if (entity.components.Miner) { + payload.type = "extractor"; + } else { + logger.warn("Unkown entity type", entity); + } + staticEntities.push(payload); } - staticEntities.push(payload); } return { diff --git a/src/js/platform/browser/sound.js b/src/js/platform/browser/sound.js index 7e58543b..32985bb5 100644 --- a/src/js/platform/browser/sound.js +++ b/src/js/platform/browser/sound.js @@ -104,7 +104,7 @@ class MusicInstance extends MusicInstanceInterface { }), new Promise((resolve, reject) => { this.howl = new Howl({ - src: cachebust("res/sounds/music/" + this.url), + src: cachebust("res/sounds/music/" + this.url + ".mp3"), autoplay: false, loop: true, html5: true, diff --git a/src/js/platform/sound.js b/src/js/platform/sound.js index f293ff94..dc6b073f 100644 --- a/src/js/platform/sound.js +++ b/src/js/platform/sound.js @@ -27,8 +27,8 @@ export const SOUNDS = { }; export const MUSIC = { - theme: "theme.mp3", - menu: "menu.mp3", + theme: "theme", + menu: "menu", }; export class SoundInstanceInterface { diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index ac5f60e9..45bc3346 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -111,13 +111,11 @@ export const allApplicationSettings = [ textGetter: rate => rate + " Hz", category: categoryGame, restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => {}, + changeCb: (app, id) => {}, enabled: !IS_DEMO, }), + + new BoolSetting("alwaysMultiplace", categoryGame, (app, value) => {}), ]; export function getApplicationSettingById(id) { @@ -134,6 +132,8 @@ class SettingsStorage { this.theme = "light"; this.refreshRate = "60"; + this.alwaysMultiplace = false; + /** * @type {Object.} */ @@ -291,14 +291,20 @@ export class ApplicationSettings extends ReadWriteProxy { } getCurrentVersion() { - return 5; + return 6; } migrate(data) { - // Simply reset - if (data.version < this.getCurrentVersion()) { + // Simply reset before + if (data.version < 5) { data.settings = new SettingsStorage(); data.version = this.getCurrentVersion(); + return ExplainedResult.good(); + } + + if (data.version < 6) { + data.alwaysMultiplace = false; + data.version = 6; } return ExplainedResult.good(); diff --git a/src/js/states/mobile_warning.js b/src/js/states/mobile_warning.js new file mode 100644 index 00000000..c6903164 --- /dev/null +++ b/src/js/states/mobile_warning.js @@ -0,0 +1,52 @@ +import { GameState } from "../core/game_state"; +import { cachebust } from "../core/cachebust"; +import { THIRDPARTY_URLS } from "../core/config"; + +export class MobileWarningState extends GameState { + constructor() { + super("MobileWarningState"); + } + + getInnerHTML() { + return ` + + + +

+ I'm sorry, but shapez.io is not yet available on mobile devices! + (There is also no estimate when this will change, but feel to make a contribution! It's +  open source!)

+ +

If you want to play on your computer, you can also get the standalone on itch.io:

+ + + Get the shapez.io standalone! + `; + } + + getThemeMusic() { + return null; + } + + getHasFadeIn() { + return false; + } + + onEnter() { + try { + if (window.gtag) { + window.gtag("event", "click", { + event_category: "ui", + event_label: "mobile_warning", + }); + } + } catch (ex) { + console.warn("Failed to track mobile click:", ex); + } + } + onLeave() { + // this.dialogs.cleanup(); + } +} diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 432de691..b910c304 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -367,27 +367,82 @@ buildings: storyRewards: # Those are the rewards gained from completing the store - reward_cutter_and_trash: Cutting Shapes - reward_rotater: Rotating - reward_painter: Painting - reward_mixer: Color Mixing - reward_stacker: Combiner - reward_splitter: Splitter/Merger - reward_tunnel: Tunnel + reward_cutter_and_trash: + title: Cutting Shapes + desc: You just unlocked the cutter - it cuts shapes half from top to bottom regardless of its orientation!

Be sure to get rid of the waste, or otherwise it will stall - For this purpose I gave you a trash, which destroys everything you put into it! - reward_rotater_ccw: CCW Rotating - reward_miner_chainable: Chaining Extractor - reward_underground_belt_tier_2: Tunnel Tier II - reward_splitter_compact: Compact Balancer - reward_cutter_quad: Quad Cutting - reward_painter_double: Double Painting - reward_painter_quad: Quad Painting - reward_storage: Storage Buffer + reward_rotater: + title: Rotating + desc: The rotater has been unlocked! It rotates shapes clockwise by 90 degrees. - reward_freeplay: Freeplay + reward_painter: + title: Painting + desc: >- + The painter has been unlocked - Extract some color veins (just as you do with shapes) and combine it with a shape in the painter to color them!

PS: If you are colorblind, I'm working on a solution already! + + reward_mixer: + title: Color Mixing + desc: The mixer has been unlocked - Combine two colors using additive blending with this building! + + reward_stacker: + title: Combiner + desc: You can now combine shapes with the combiner! Both inputs are combined, and if they can be put next to each other, they will be fused. If not, the right input is stacked on top of the left input! + + reward_splitter: + title: Splitter/Merger + desc: The multifunctional balancer has been unlocked - It can be used to build bigger factories by splitting and merging items onto multiple belts!

+ + reward_tunnel: + title: Tunnel + desc: The tunnel has been unlocked - You can now pipe items through belts and buildings with it! + + reward_rotater_ccw: + title: CCW Rotating + desc: You have unlocked a variant of the rotater - It allows to rotate counter clockwise! To build it, select the rotater and press 'T' to cycle its variants! + + reward_miner_chainable: + title: Chaining Extractor + desc: You have unlocked the chaining extractor! It can forward its resources to other extractors so you can more efficiently extract resources! + + reward_underground_belt_tier_2: + title: Tunnel Tier II + desc: You have unlocked a new variant of the tunnel - It has a bigger range, and you can also mix-n-match those tunnels now! + + reward_splitter_compact: + title: Compact Balancer + desc: >- + You have unlocked a compact variant of the balancer - It accepts two inputs and merges them into one! + + reward_cutter_quad: + title: Quad Cutting + desc: You have unlocked a variant of the cutter - It allows you to cut shapes in four parts instead of just two! + + reward_painter_double: + title: Double Painting + desc: You have unlocked a variant of the painter - It works as the regular painter but processes two shapes at once consuming just one color instead of two! + + reward_painter_quad: + title: Quad Painting + desc: You have unlocked a variant of the painter - It allows to paint each part of the shape individually! + + reward_storage: + title: Storage Buffer + desc: You have unlocked a variant of the trash - It allows to store items up to a given capacity! + + reward_freeplay: + title: Freeplay + desc: You did it! You unlocked the free-play mode! This means that shapes are now randomly generated! (No worries, more content is planned for the standalone!) # Special reward, which is shown when there is no reward actually - no_reward: Next level + no_reward: + title: Next level + desc: >- + This level gave you no reward, but the next one will!

PS: Better don't destroy your existing factory - You need all those shapes later again to unlock upgrades! + + no_reward_freeplay: + title: Next level + desc: >- + Congratulations! By the way, more content is planned for the standalone! settings: title: Settings @@ -432,6 +487,11 @@ settings: description: >- If you have a 144hz monitor, change the refresh rate here so the game will properly simulate at higher refresh rates. This might actually decrease the FPS if your computer is too slow. + alwaysMultiplace: + title: Multiplace + description: >- + If enabled, all buildings will stay selected after placement until you cancel it. This is equivalent to holding SHIFT permanently. + keybindings: title: Keybindings hint: >- @@ -457,6 +517,9 @@ keybindings: centerMap: Center Map + mapZoomIn: Zoom in + mapZoomOut: Zoom out + menuOpenShop: Upgrades menuOpenStats: Statistics