Merge branch 'master' into waypoints
Before Width: | Height: | Size: 3.1 MiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 76 KiB |
BIN
artwork/logo.png
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
BIN
artwork/logo.psd
BIN
res/logo.png
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
BIN
res/videos/level_1.webm
Normal file
BIN
res/videos/level_2.webm
Normal file
68
src/css/ingame_hud/tutorial_hints.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,6 +105,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
@ -115,18 +130,38 @@
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
48
src/css/states/mobile_warning.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<typeof GameState>} */
|
||||
const states = [
|
||||
PreloadState,
|
||||
MobileWarningState,
|
||||
MainMenuState,
|
||||
InGameState,
|
||||
SettingsState,
|
||||
@ -315,7 +317,12 @@ export class Application {
|
||||
|
||||
Loader.linkAppAfterBoot(this);
|
||||
|
||||
// Check for mobile
|
||||
if (IS_MOBILE) {
|
||||
this.stateMgr.moveToState("MobileWarningState");
|
||||
} else {
|
||||
this.stateMgr.moveToState("PreloadState");
|
||||
}
|
||||
|
||||
// Starting rendering
|
||||
this.ticker.frameEmitted.add(this.onFrameEmitted, this);
|
||||
|
@ -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 {
|
||||
/**
|
||||
|
@ -83,8 +83,8 @@ export const globalConfig = {
|
||||
|
||||
debug: {
|
||||
/* dev:start */
|
||||
// fastGameEnter: true,
|
||||
// noArtificialDelays: true,
|
||||
fastGameEnter: true,
|
||||
noArtificialDelays: true,
|
||||
// disableSavegameWrite: true,
|
||||
// showEntityBounds: true,
|
||||
// showAcceptorEjectors: true,
|
||||
|
@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
101
src/js/game/hud/parts/tutorial_hints.js
Normal file
@ -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",
|
||||
[],
|
||||
`
|
||||
<div class="header">
|
||||
No idea what to do?
|
||||
<button class="styledButton toggleHint">
|
||||
<span class="show">Show hint</span>
|
||||
<span class="hide">Hide hint</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<video autoplay muted loop class="fullscreenBackgroundVideo">
|
||||
<source type="video/webm">
|
||||
</video>
|
||||
`
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =
|
||||
"<span class='reward'>" +
|
||||
T.ingame.levelCompleteNotification.unlockText.replace("<reward>", rewardText) +
|
||||
"</span>";
|
||||
let html = `
|
||||
<div class="rewardName">
|
||||
${T.ingame.levelCompleteNotification.unlockText.replace("<reward>", rewardName)}
|
||||
</div>
|
||||
|
||||
const addBuildingExplanation = metaBuildingClass => {
|
||||
<div class="rewardDesc">
|
||||
${T.storyRewards[reward].desc}
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
||||
html += "<div class='images'>";
|
||||
const gained = enumHubGoalRewardsToContentUnlocked[reward];
|
||||
if (gained) {
|
||||
gained.forEach(([metaBuildingClass, variant]) => {
|
||||
const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass);
|
||||
html += `<div class="buildingExplanation" data-icon="building_tutorials/${metaBuilding.getId()}.png"></div>`;
|
||||
};
|
||||
|
||||
switch (reward) {
|
||||
case enumHubGoalRewards.reward_cutter_and_trash: {
|
||||
addBuildingExplanation(MetaCutterBuilding);
|
||||
addBuildingExplanation(MetaTrashBuilding);
|
||||
break;
|
||||
html += `<div class="buildingExplanation" data-icon="building_tutorials/${
|
||||
metaBuilding.getId() + (variant === defaultBuildingVariant ? "" : "-" + variant)
|
||||
}.png"></div>`;
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// addBuildingExplanation(MetaSplitterBuilding);
|
||||
// addBuildingExplanation(MetaCutterBuilding);
|
||||
html += "</div>";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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} */
|
||||
|
@ -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";
|
||||
|
@ -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 = [
|
||||
|
51
src/js/game/tutorial_goals_mappings.js
Normal file
@ -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,
|
||||
};
|
@ -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,6 +158,8 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
|
||||
|
||||
const entities = root.entityMgr.getAllWithComponent(StaticMapEntityComponent);
|
||||
|
||||
// Limit the entities
|
||||
if (entities.length < 5000) {
|
||||
for (let i = 0; i < entities.length; ++i) {
|
||||
const entity = entities[i];
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
@ -179,6 +181,7 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
|
||||
}
|
||||
staticEntities.push(payload);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
storedShapes: root.hubGoals.storedShapes,
|
||||
|
@ -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,
|
||||
|
@ -27,8 +27,8 @@ export const SOUNDS = {
|
||||
};
|
||||
|
||||
export const MUSIC = {
|
||||
theme: "theme.mp3",
|
||||
menu: "menu.mp3",
|
||||
theme: "theme",
|
||||
menu: "menu",
|
||||
};
|
||||
|
||||
export class SoundInstanceInterface {
|
||||
|
@ -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.<string, number>}
|
||||
*/
|
||||
@ -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();
|
||||
|
52
src/js/states/mobile_warning.js
Normal file
@ -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 `
|
||||
|
||||
<img class="logo" src="${cachebust("res/logo.png")}" alt="shapez.io Logo">
|
||||
|
||||
<p>
|
||||
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
|
||||
<a href="https://github.com/tobspr/shapez.io" target="_blank">open source</a>!)</p>
|
||||
|
||||
<p>If you want to play on your computer, you can also get the standalone on itch.io:</p>
|
||||
|
||||
|
||||
<a href="${
|
||||
THIRDPARTY_URLS.standaloneStorePage
|
||||
}" class="standaloneLink" target="_blank">Get the shapez.io standalone!</a>
|
||||
`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -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 <strong>cutter</strong> - it cuts shapes half from <strong>top to bottom</strong> regardless of its orientation!<br><br>Be sure to get rid of the waste, or otherwise <strong>it will stall</strong> - 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 <strong>rotater</strong> has been unlocked! It rotates shapes clockwise by 90 degrees.
|
||||
|
||||
reward_freeplay: Freeplay
|
||||
reward_painter:
|
||||
title: Painting
|
||||
desc: >-
|
||||
The <strong>painter</strong> 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!<br><br>PS: If you are colorblind, I'm working on a solution already!
|
||||
|
||||
reward_mixer:
|
||||
title: Color Mixing
|
||||
desc: The <strong>mixer</strong> has been unlocked - Combine two colors using <strong>additive blending</strong> with this building!
|
||||
|
||||
reward_stacker:
|
||||
title: Combiner
|
||||
desc: You can now combine shapes with the <strong>combiner</strong>! Both inputs are combined, and if they can be put next to each other, they will be <strong>fused</strong>. If not, the right input is <strong>stacked on top</strong> of the left input!
|
||||
|
||||
reward_splitter:
|
||||
title: Splitter/Merger
|
||||
desc: The multifunctional <strong>balancer</strong> has been unlocked - It can be used to build bigger factories by <strong>splitting and merging items</strong> onto multiple belts!<br><br>
|
||||
|
||||
reward_tunnel:
|
||||
title: Tunnel
|
||||
desc: The <strong>tunnel</strong> 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 <strong>rotater</strong> - It allows to rotate counter clockwise! To build it, select the rotater and <strong>press 'T' to cycle its variants</strong>!
|
||||
|
||||
reward_miner_chainable:
|
||||
title: Chaining Extractor
|
||||
desc: You have unlocked the <strong>chaining extractor</strong>! It can <strong>forward its resources</strong> 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 <strong>tunnel</strong> - It has a <strong>bigger range</strong>, 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 <strong>balancer</strong> - It accepts two inputs and merges them into one!
|
||||
|
||||
reward_cutter_quad:
|
||||
title: Quad Cutting
|
||||
desc: You have unlocked a variant of the <strong>cutter</strong> - It allows you to cut shapes in <strong>four parts</strong> instead of just two!
|
||||
|
||||
reward_painter_double:
|
||||
title: Double Painting
|
||||
desc: You have unlocked a variant of the <strong>painter</strong> - It works as the regular painter but processes <strong>two shapes at once</strong> consuming just one color instead of two!
|
||||
|
||||
reward_painter_quad:
|
||||
title: Quad Painting
|
||||
desc: You have unlocked a variant of the <strong>painter</strong> - It allows to paint each part of the shape individually!
|
||||
|
||||
reward_storage:
|
||||
title: Storage Buffer
|
||||
desc: You have unlocked a variant of the <strong>trash</strong> - It allows to store items up to a given capacity!
|
||||
|
||||
reward_freeplay:
|
||||
title: Freeplay
|
||||
desc: You did it! You unlocked the <strong>free-play mode</strong>! 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! <br><br> PS: Better don't destroy your existing factory - You need <strong>all</strong> those shapes later again to <strong>unlock upgrades</strong>!
|
||||
|
||||
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
|
||||
|
||||
|