1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

Merge branch 'master' into waypoints

This commit is contained in:
dengr1065 2020-05-23 13:28:36 +03:00 committed by GitHub
commit 86e29fb34b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 608 additions and 160 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 26 KiB

BIN
res/videos/level_1.webm Normal file

Binary file not shown.

BIN
res/videos/level_2.webm Normal file

Binary file not shown.

View 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;
}
}
}
}

View File

@ -9,6 +9,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
pointer-events: all; pointer-events: all;
@include InlineAnimation(0.1s ease-in-out) { @include InlineAnimation(0.1s ease-in-out) {
0% { 0% {
opacity: 0; opacity: 0;
@ -16,7 +17,7 @@
} }
.dialog { .dialog {
background: rgba(#222428, 0.5); // background: rgba(#222428, 0.5);
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
@include S(padding, 30px); @include S(padding, 30px);
@ -32,7 +33,7 @@
.subTitle { .subTitle {
@include SuperHeading; @include SuperHeading;
text-transform: uppercase; text-transform: uppercase;
@include S(font-size, 50px); @include S(font-size, 40px);
@include InlineAnimation(0.5s ease-in-out) { @include InlineAnimation(0.5s ease-in-out) {
0% { 0% {
@ -48,11 +49,10 @@
} }
.subTitle { .subTitle {
@include Heading; @include PlainText;
background: $colorGreenBright;
display: inline-block; display: inline-block;
@include S(padding, 1px, 6px); @include S(margin, 0px, 0, 20px);
@include S(margin, 20px, 0, 20px); color: $colorGreenBright;
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
@include InlineAnimation(0.5s ease-in-out) { @include InlineAnimation(0.5s ease-in-out) {
@ -82,14 +82,15 @@
transform: translateX(-2vw); transform: translateX(-2vw);
} }
} }
display: grid; display: flex;
grid-template-columns: auto auto; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@include S(grid-gap, 10px); @include S(grid-gap, 10px);
.reward { .rewardName {
grid-column: 1 / 3; grid-column: 1 / 3;
display: none;
@include InlineAnimation(0.5s ease-in-out) { @include InlineAnimation(0.5s ease-in-out) {
0% { 0% {
transform: translateX(200vw); transform: translateX(200vw);
@ -104,29 +105,63 @@
} }
} }
.buildingExplanation { .rewardDesc {
@include S(width, 200px); grid-column: 1 / 3;
@include S(height, 200px); @include PlainText;
display: inline-block; @include S(margin-bottom, 15px);
background-position: center center; color: #aaacaf;
background-size: cover; @include S(width, 400px);
background-repeat: no-repeat; text-align: left;
@include S(border-radius, $globalBorderRadius); strong {
box-shadow: #{D(2px)} #{D(3px)} 0 0 rgba(0, 0, 0, 0.15); 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 { button.close {
border: 0; border: 0;
@include InlineAnimation(2s ease-in-out) { position: relative;
0% { @include S(margin-top, 30px);
opacity: 0;
} &:not(.unlocked) {
95% { pointer-events: none;
opacity: 0; 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);
} }
} }
} }

View File

@ -26,6 +26,7 @@
@import "states/keybindings"; @import "states/keybindings";
@import "states/settings"; @import "states/settings";
@import "states/about"; @import "states/about";
@import "states/mobile_warning";
@import "ingame_hud/buildings_toolbar"; @import "ingame_hud/buildings_toolbar";
@import "ingame_hud/building_placer"; @import "ingame_hud/building_placer";
@ -44,6 +45,7 @@
@import "ingame_hud/settings_menu"; @import "ingame_hud/settings_menu";
@import "ingame_hud/debug_info"; @import "ingame_hud/debug_info";
@import "ingame_hud/entity_debugger"; @import "ingame_hud/entity_debugger";
@import "ingame_hud/tutorial_hints";
// prettier-ignore // prettier-ignore
$elements: $elements:
@ -57,13 +59,14 @@ ingame_HUD_PlacerVariants,
// Regular hud // Regular hud
ingame_HUD_PinnedShapes, ingame_HUD_PinnedShapes,
ingame_HUD_buildings_toolbar,
ingame_HUD_GameMenu, ingame_HUD_GameMenu,
ingame_HUD_KeybindingOverlay, ingame_HUD_KeybindingOverlay,
ingame_HUD_Notifications, ingame_HUD_Notifications,
ingame_HUD_MassSelector, ingame_HUD_MassSelector,
ingame_HUD_DebugInfo, ingame_HUD_DebugInfo,
ingame_HUD_EntityDebugger, ingame_HUD_EntityDebugger,
ingame_HUD_TutorialHints,
ingame_HUD_buildings_toolbar,
// Overlays // Overlays
ingame_HUD_BetaOverlay, ingame_HUD_BetaOverlay,
@ -92,7 +95,8 @@ body.uiHidden {
#ingame_HUD_GameMenu, #ingame_HUD_GameMenu,
#ingame_HUD_MassSelector, #ingame_HUD_MassSelector,
#ingame_HUD_PinnedShapes, #ingame_HUD_PinnedShapes,
#ingame_HUD_Notifications { #ingame_HUD_Notifications,
#ingame_HUD_TutorialHints {
display: none !important; display: none !important;
} }
} }
@ -100,7 +104,7 @@ body.uiHidden {
body.modalDialogActive, body.modalDialogActive,
body.externalAdOpen, body.externalAdOpen,
body.ingameDialogOpen { 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; filter: blur(5px) !important;
} }
} }

View 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;
}
}
}

View File

@ -1,5 +1,5 @@
import { AnimationFrame } from "./core/animation_frame"; 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 { GameState } from "./core/game_state";
import { GLOBAL_APP, setGlobalApp } from "./core/globals"; import { GLOBAL_APP, setGlobalApp } from "./core/globals";
import { InputDistributor } from "./core/input_distributor"; import { InputDistributor } from "./core/input_distributor";
@ -36,6 +36,7 @@ import { KeybindingsState } from "./states/keybindings";
import { AboutState } from "./states/about"; import { AboutState } from "./states/about";
import { PlatformWrapperImplElectron } from "./platform/electron/wrapper"; import { PlatformWrapperImplElectron } from "./platform/electron/wrapper";
import { StorageImplElectron } from "./platform/electron/storage"; import { StorageImplElectron } from "./platform/electron/storage";
import { MobileWarningState } from "./states/mobile_warning";
const logger = createLogger("application"); const logger = createLogger("application");
@ -158,6 +159,7 @@ export class Application {
/** @type {Array<typeof GameState>} */ /** @type {Array<typeof GameState>} */
const states = [ const states = [
PreloadState, PreloadState,
MobileWarningState,
MainMenuState, MainMenuState,
InGameState, InGameState,
SettingsState, SettingsState,
@ -315,7 +317,12 @@ export class Application {
Loader.linkAppAfterBoot(this); Loader.linkAppAfterBoot(this);
this.stateMgr.moveToState("PreloadState"); // Check for mobile
if (IS_MOBILE) {
this.stateMgr.moveToState("MobileWarningState");
} else {
this.stateMgr.moveToState("PreloadState");
}
// Starting rendering // Starting rendering
this.ticker.frameEmitted.add(this.onFrameEmitted, this); this.ticker.frameEmitted.add(this.onFrameEmitted, this);

View File

@ -25,13 +25,8 @@ const essentialBareGameSprites = G_ALL_UI_IMAGES;
const essentialBareGameSounds = [MUSIC.theme]; const essentialBareGameSounds = [MUSIC.theme];
const additionalGameSprites = []; const additionalGameSprites = [];
const additionalGameSounds = []; // @ts-ignore
for (const key in SOUNDS) { const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)];
additionalGameSounds.push(SOUNDS[key]);
}
for (const key in MUSIC) {
additionalGameSounds.push(MUSIC[key]);
}
export class BackgroundResourcesLoader { export class BackgroundResourcesLoader {
/** /**

View File

@ -83,8 +83,8 @@ export const globalConfig = {
debug: { debug: {
/* dev:start */ /* dev:start */
// fastGameEnter: true, fastGameEnter: true,
// noArtificialDelays: true, noArtificialDelays: true,
// disableSavegameWrite: true, // disableSavegameWrite: true,
// showEntityBounds: true, // showEntityBounds: true,
// showAcceptorEjectors: true, // showAcceptorEjectors: true,

View File

@ -143,6 +143,7 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
const tier = enumUndergroundBeltVariantToTier[variant]; const tier = enumUndergroundBeltVariantToTier[variant];
const targetRotation = (rotation + 180) % 360; const targetRotation = (rotation + 180) % 360;
const targetSenderRotation = rotation;
for ( for (
let searchOffset = 1; 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 // If we encounter an underground receiver on our way which is also faced in our direction, we don't accept that
break; break;
} }
return { return {
rotation: targetRotation, rotation: targetRotation,
rotationVariant: 1, rotationVariant: 1,
connectedEntities: [contents], connectedEntities: [contents],
}; };
} else if (staticComp.rotation === targetSenderRotation) {
// Draw connections to receivers
if (undergroundComp.mode === enumUndergroundBeltMode.receiver) {
return {
rotation: rotation,
rotationVariant: 0,
connectedEntities: [contents],
};
}
} }
} }
} }

View File

@ -352,6 +352,9 @@ export class Camera extends BasicSerializableObject {
mapper.getBinding(KEYMAPPINGS.ingame.mapMoveRight).add(() => (this.keyboardForce.x = 1)); mapper.getBinding(KEYMAPPINGS.ingame.mapMoveRight).add(() => (this.keyboardForce.x = 1));
mapper.getBinding(KEYMAPPINGS.ingame.mapMoveLeft).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()); mapper.getBinding(KEYMAPPINGS.ingame.centerMap).add(() => this.centerOnMap());
} }
@ -832,7 +835,7 @@ export class Camera extends BasicSerializableObject {
internalUpdateZooming(now, dt) { internalUpdateZooming(now, dt) {
if (!this.currentlyPinching && this.desiredZoom !== null) { if (!this.currentlyPinching && this.desiredZoom !== null) {
const diff = this.zoomLevel - this.desiredZoom; const diff = this.zoomLevel - this.desiredZoom;
if (Math_abs(diff) > 0.05) { if (Math_abs(diff) > 0.0001) {
let fade = 0.94; let fade = 0.94;
if (diff > 0) { if (diff > 0) {
// Zoom out faster than in // Zoom out faster than in

View File

@ -188,13 +188,11 @@ export class HubGoals extends BasicSerializableObject {
return; return;
} }
const reward = enumHubGoalRewards.no_reward;
this.currentGoal = { this.currentGoal = {
/** @type {ShapeDefinition} */ /** @type {ShapeDefinition} */
definition: this.createRandomShape(), definition: this.createRandomShape(),
required: 1000 + findNiceIntegerValue(this.level * 47.5), 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); 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 * Returns whether a given upgrade can be unlocked
* @param {string} upgradeId * @param {string} upgradeId

View File

@ -27,7 +27,7 @@ import { HUDEntityDebugger } from "./parts/entity_debugger";
import { KEYMAPPINGS } from "../key_action_mapper"; import { KEYMAPPINGS } from "../key_action_mapper";
import { HUDWatermark } from "./parts/watermark"; import { HUDWatermark } from "./parts/watermark";
import { HUDModalDialogs } from "./parts/modal_dialogs"; import { HUDModalDialogs } from "./parts/modal_dialogs";
import { Vector } from "../../core/vector"; import { HUDPartTutorialHints } from "./parts/tutorial_hints";
export class GameHUD { export class GameHUD {
/** /**
@ -64,6 +64,8 @@ export class GameHUD {
notifications: new HUDNotifications(this.root), notifications: new HUDNotifications(this.root),
settingsMenu: new HUDSettingsMenu(this.root), settingsMenu: new HUDSettingsMenu(this.root),
tutorialHints: new HUDPartTutorialHints(this.root),
// betaOverlay: new HUDBetaOverlay(this.root), // betaOverlay: new HUDBetaOverlay(this.root),
debugInfo: new HUDDebugInfo(this.root), debugInfo: new HUDDebugInfo(this.root),

View File

@ -468,7 +468,11 @@ export class HUDBuildingPlacer extends BaseHUDPart {
this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; 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 // Stop placement
this.currentMetaBuilding.set(null); this.currentMetaBuilding.set(null);
} }

View 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();
}
}
}

View File

@ -2,18 +2,12 @@ import { globalConfig } from "../../../core/config";
import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound"; import { SOUNDS } from "../../../platform/sound";
import { MetaCutterBuilding } from "../../buildings/cutter"; import { T } from "../../../translations";
import { MetaMixerBuilding } from "../../buildings/mixer"; import { defaultBuildingVariant } from "../../meta_building";
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 { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { T } from "../../../translations"; import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
export class HUDUnlockNotification extends BaseHUDPart { export class HUDUnlockNotification extends BaseHUDPart {
initialize() { initialize() {
@ -26,10 +20,8 @@ export class HUDUnlockNotification extends BaseHUDPart {
if (!(G_IS_DEV && globalConfig.debug.disableUnlockDialog)) { if (!(G_IS_DEV && globalConfig.debug.disableUnlockDialog)) {
this.root.signals.storyGoalCompleted.add(this.showForLevel, this); this.root.signals.storyGoalCompleted.add(this.showForLevel, this);
} }
}
shouldPauseGame() { this.buttonShowTimeout = null;
return this.visible;
} }
createElements(parent) { createElements(parent) {
@ -60,63 +52,50 @@ export class HUDUnlockNotification extends BaseHUDPart {
("" + level).padStart(2, "0") ("" + level).padStart(2, "0")
); );
const rewardText = T.storyRewards[reward]; const rewardName = T.storyRewards[reward].title;
let html = let html = `
"<span class='reward'>" + <div class="rewardName">
T.ingame.levelCompleteNotification.unlockText.replace("<reward>", rewardText) + ${T.ingame.levelCompleteNotification.unlockText.replace("<reward>", rewardName)}
"</span>"; </div>
<div class="rewardDesc">
${T.storyRewards[reward].desc}
</div>
const addBuildingExplanation = metaBuildingClass => { `;
const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass);
html += `<div class="buildingExplanation" data-icon="building_tutorials/${metaBuilding.getId()}.png"></div>`;
};
switch (reward) { html += "<div class='images'>";
case enumHubGoalRewards.reward_cutter_and_trash: { const gained = enumHubGoalRewardsToContentUnlocked[reward];
addBuildingExplanation(MetaCutterBuilding); if (gained) {
addBuildingExplanation(MetaTrashBuilding); gained.forEach(([metaBuildingClass, variant]) => {
break; const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass);
} html += `<div class="buildingExplanation" data-icon="building_tutorials/${
case enumHubGoalRewards.reward_mixer: { metaBuilding.getId() + (variant === defaultBuildingVariant ? "" : "-" + variant)
addBuildingExplanation(MetaMixerBuilding); }.png"></div>`;
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 += "</div>";
// addBuildingExplanation(MetaSplitterBuilding);
// addBuildingExplanation(MetaCutterBuilding);
this.elemContents.innerHTML = html; this.elemContents.innerHTML = html;
this.visible = true; this.visible = true;
this.root.soundProxy.playUi(SOUNDS.levelComplete); 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() { requestClose() {
@ -126,10 +105,18 @@ export class HUDUnlockNotification extends BaseHUDPart {
} }
close() { close() {
if (this.buttonShowTimeout) {
clearTimeout(this.buttonShowTimeout);
this.buttonShowTimeout = null;
}
this.visible = false; this.visible = false;
} }
update() { update() {
this.domAttach.update(this.visible); this.domAttach.update(this.visible);
if (!this.visible && this.buttonShowTimeout) {
clearTimeout(this.buttonShowTimeout);
this.buttonShowTimeout = null;
}
} }
} }

View File

@ -33,6 +33,9 @@ export const KEYMAPPINGS = {
toggleHud: { keyCode: 113 }, // F2 toggleHud: { keyCode: 113 }, // F2
toggleFPSInfo: { keyCode: 115 }, // F1 toggleFPSInfo: { keyCode: 115 }, // F1
mapZoomIn: { keyCode: 187, repeated: true }, // "+"
mapZoomOut: { keyCode: 189, repeated: true }, // "-"
}, },
buildings: { buildings: {
@ -194,13 +197,11 @@ export function getStringForKeyCode(code) {
case 186: case 186:
return ";"; return ";";
case 187: case 187:
return "="; return "+";
case 188: case 188:
return ","; return ",";
case 189: case 189:
return "-"; return "-";
case 189:
return ".";
case 191: case 191:
return "/"; return "/";
case 219: case 219:
@ -224,12 +225,14 @@ export class Keybinding {
* @param {object} param0 * @param {object} param0
* @param {number} param0.keyCode * @param {number} param0.keyCode
* @param {boolean=} param0.builtin * @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); assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode);
this.app = app; this.app = app;
this.keyCode = keyCode; this.keyCode = keyCode;
this.builtin = builtin; this.builtin = builtin;
this.repeated = repeated;
this.currentlyDown = false; this.currentlyDown = false;
@ -364,7 +367,7 @@ export class KeyActionMapper {
for (const key in this.keybindings) { for (const key in this.keybindings) {
/** @type {Keybinding} */ /** @type {Keybinding} */
const binding = this.keybindings[key]; const binding = this.keybindings[key];
if (binding.keyCode === keyCode && !binding.currentlyDown) { if (binding.keyCode === keyCode && (!binding.currentlyDown || binding.repeated)) {
binding.currentlyDown = true; binding.currentlyDown = true;
/** @type {Signal} */ /** @type {Signal} */

View File

@ -88,7 +88,7 @@ export class HubSystem extends GameSystemWithFilter {
context.font = "bold 11px GameFont"; context.font = "bold 11px GameFont";
context.fillStyle = "#fd0752"; context.fillStyle = "#fd0752";
context.textAlign = "center"; 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 // Level
context.font = "bold 11px GameFont"; context.font = "bold 11px GameFont";

View File

@ -2,6 +2,7 @@ import { ShapeDefinition } from "./shape_definition";
import { finalGameShape } from "./upgrades"; import { finalGameShape } from "./upgrades";
/** /**
* Don't forget to also update tutorial_goals_mappings.js as well as the translations!
* @enum {string} * @enum {string}
*/ */
export const enumHubGoalRewards = { export const enumHubGoalRewards = {
@ -25,6 +26,7 @@ export const enumHubGoalRewards = {
reward_freeplay: "reward_freeplay", reward_freeplay: "reward_freeplay",
no_reward: "no_reward", no_reward: "no_reward",
no_reward_freeplay: "no_reward_freeplay",
}; };
export const tutorialGoals = [ export const tutorialGoals = [

View 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,
};

View File

@ -23,7 +23,7 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
initialize() { initialize() {
this.syncKey = null; this.syncKey = null;
setInterval(() => this.sendTimePoints(), 30 * 1000); setInterval(() => this.sendTimePoints(), 120 * 1000);
// Retrieve sync key from player // Retrieve sync key from player
return this.app.storage.readFileAsync(analyticsLocalFile).then( return this.app.storage.readFileAsync(analyticsLocalFile).then(
@ -158,26 +158,29 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
const entities = root.entityMgr.getAllWithComponent(StaticMapEntityComponent); const entities = root.entityMgr.getAllWithComponent(StaticMapEntityComponent);
for (let i = 0; i < entities.length; ++i) { // Limit the entities
const entity = entities[i]; if (entities.length < 5000) {
const staticComp = entity.components.StaticMapEntity; for (let i = 0; i < entities.length; ++i) {
const payload = {}; const entity = entities[i];
payload.origin = staticComp.origin; const staticComp = entity.components.StaticMapEntity;
payload.tileSize = staticComp.tileSize; const payload = {};
payload.rotation = staticComp.rotation; payload.origin = staticComp.origin;
payload.tileSize = staticComp.tileSize;
payload.rotation = staticComp.rotation;
if (entity.components.Belt) { if (entity.components.Belt) {
payload.type = "belt"; payload.type = "belt";
} else if (entity.components.UndergroundBelt) { } else if (entity.components.UndergroundBelt) {
payload.type = "tunnel"; payload.type = "tunnel";
} else if (entity.components.ItemProcessor) { } else if (entity.components.ItemProcessor) {
payload.type = entity.components.ItemProcessor.type; payload.type = entity.components.ItemProcessor.type;
} else if (entity.components.Miner) { } else if (entity.components.Miner) {
payload.type = "extractor"; payload.type = "extractor";
} else { } else {
logger.warn("Unkown entity type", entity); logger.warn("Unkown entity type", entity);
}
staticEntities.push(payload);
} }
staticEntities.push(payload);
} }
return { return {

View File

@ -104,7 +104,7 @@ class MusicInstance extends MusicInstanceInterface {
}), }),
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
this.howl = new Howl({ this.howl = new Howl({
src: cachebust("res/sounds/music/" + this.url), src: cachebust("res/sounds/music/" + this.url + ".mp3"),
autoplay: false, autoplay: false,
loop: true, loop: true,
html5: true, html5: true,

View File

@ -27,8 +27,8 @@ export const SOUNDS = {
}; };
export const MUSIC = { export const MUSIC = {
theme: "theme.mp3", theme: "theme",
menu: "menu.mp3", menu: "menu",
}; };
export class SoundInstanceInterface { export class SoundInstanceInterface {

View File

@ -111,13 +111,11 @@ export const allApplicationSettings = [
textGetter: rate => rate + " Hz", textGetter: rate => rate + " Hz",
category: categoryGame, category: categoryGame,
restartRequired: false, restartRequired: false,
changeCb: changeCb: (app, id) => {},
/**
* @param {Application} app
*/
(app, id) => {},
enabled: !IS_DEMO, enabled: !IS_DEMO,
}), }),
new BoolSetting("alwaysMultiplace", categoryGame, (app, value) => {}),
]; ];
export function getApplicationSettingById(id) { export function getApplicationSettingById(id) {
@ -134,6 +132,8 @@ class SettingsStorage {
this.theme = "light"; this.theme = "light";
this.refreshRate = "60"; this.refreshRate = "60";
this.alwaysMultiplace = false;
/** /**
* @type {Object.<string, number>} * @type {Object.<string, number>}
*/ */
@ -291,14 +291,20 @@ export class ApplicationSettings extends ReadWriteProxy {
} }
getCurrentVersion() { getCurrentVersion() {
return 5; return 6;
} }
migrate(data) { migrate(data) {
// Simply reset // Simply reset before
if (data.version < this.getCurrentVersion()) { if (data.version < 5) {
data.settings = new SettingsStorage(); data.settings = new SettingsStorage();
data.version = this.getCurrentVersion(); data.version = this.getCurrentVersion();
return ExplainedResult.good();
}
if (data.version < 6) {
data.alwaysMultiplace = false;
data.version = 6;
} }
return ExplainedResult.good(); return ExplainedResult.good();

View 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
&nbsp;<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();
}
}

View File

@ -367,27 +367,82 @@ buildings:
storyRewards: storyRewards:
# Those are the rewards gained from completing the store # Those are the rewards gained from completing the store
reward_cutter_and_trash: Cutting Shapes reward_cutter_and_trash:
reward_rotater: Rotating title: Cutting Shapes
reward_painter: Painting 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_mixer: Color Mixing
reward_stacker: Combiner
reward_splitter: Splitter/Merger
reward_tunnel: Tunnel
reward_rotater_ccw: CCW Rotating reward_rotater:
reward_miner_chainable: Chaining Extractor title: Rotating
reward_underground_belt_tier_2: Tunnel Tier II desc: The <strong>rotater</strong> has been unlocked! It rotates shapes clockwise by 90 degrees.
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_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 # 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: settings:
title: Settings title: Settings
@ -432,6 +487,11 @@ settings:
description: >- 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. 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: keybindings:
title: Keybindings title: Keybindings
hint: >- hint: >-
@ -457,6 +517,9 @@ keybindings:
centerMap: Center Map centerMap: Center Map
mapZoomIn: Zoom in
mapZoomOut: Zoom out
menuOpenShop: Upgrades menuOpenShop: Upgrades
menuOpenStats: Statistics menuOpenStats: Statistics