mirror of
https://github.com/tobspr/shapez.io.git
synced 2026-03-02 03:39:21 +00:00
Puzzle DLC (#1172)
* Puzzle mode (#1135) * Add mode button to main menu * [WIP] Add mode menu. Add factory-based gameMode creation * Add savefile migration, serialize, deserialize * Add hidden HUD elements, zone, and zoom, boundary constraints * Clean up lint issues * Add building, HUD exclusion, building exclusion, and refactor - [WIP] Add ConstantProducer building that combines ConstantSignal and ItemProducer functionality. Currently using temp assets. - Add pre-placement check to the zone - Use Rectangles for zone and boundary - Simplify zone drawing - Account for exclusion in savegame data - [WIP] Add puzzle play and edit buttons in puzzle mode menu * [WIP] Add building, component, and systems for producing and accepting user-specified items and checking goal criteria * Add ingame puzzle mode UI elements - Add minimal menus in puzzle mode for back, next navigation - Add lower menu for changing zone dimenensions Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com> * Performance optimizations (#1154) * 1.3.1 preparations * Minor fixes, update translations * Fix achievements not working * Lots of belt optimizations, ~15% performance boost * Puzzle mode, part 1 * Puzzle mode, part 2 * Fix missing import * Puzzle mode, part 3 * Fix typo * Puzzle mode, part 4 * Puzzle Mode fixes: Correct zone restrictions and more (#1155) * Hide Puzzle Editor Controls in regular game mode, fix typo * Disallow shrinking zone if there are buildings * Fix multi-tile buildings for shrinking * Puzzle mode, Refactor hud * Puzzle mode * Fixed typo in latest puzzle commit (#1156) * Allow completing puzzles * Puzzle mode, almost done * Bump version to 1.4.0 * Fixes * [puzzle] Prevent pipette cheats (miners, emitters) (#1158) * Puzzle mode, almost done * Allow clearing belts with 'B' * Multiple users for the puzzle dlc * Bump api key * Minor adjustments * Update * Minor fixes * Fix throughput * Fix belts * Minor puzzle adjustments * New difficulty * Minor puzzle improvements * Fix belt path * Update translations * Added a button to return to the menu after a puzzle is completed (#1170) * added another button to return to the menu * improved menu return * fixed continue button to not go back to menu * [Puzzle] Added ability to lock buildings in the puzzle editor! (#1164) * initial test * tried to get it to work * added icon * added test exclusion * reverted css * completed flow for building locking * added lock option * finalized look and changed locked building to same sprite * removed unused art * added clearing every goal acceptor on lock to prevent creating impossible puzzles * heavily improved validation and prevented autocompletion * validation only checks every 100 ticks to improve performance * validation only checks every 100 ticks to improve performance * removed clearing goal acceptors as it isn't needed because of validation * Add soundtrack, puzzle dlc fixes Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com> Co-authored-by: dengr1065 <dengr1065@gmail.com> Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#ingame_HUD_BetaOverlay {
|
||||
position: fixed;
|
||||
@include S(top, 10px);
|
||||
@include S(top, 70px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: $colorRedBright;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
.building {
|
||||
@include S(width, 30px);
|
||||
@include S(height, 22px);
|
||||
@include S(height, 30px);
|
||||
background-size: 45%;
|
||||
|
||||
&:not(.unlocked) {
|
||||
@@ -49,63 +49,97 @@
|
||||
}
|
||||
|
||||
.building {
|
||||
color: $accentColorDark;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(width, 40px);
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include S(padding, 5px);
|
||||
@include S(padding-bottom, 1px);
|
||||
@include S(width, 35px);
|
||||
@include S(height, 40px);
|
||||
.icon {
|
||||
color: $accentColorDark;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
background: center center / 70% no-repeat;
|
||||
background: center center / 70% no-repeat;
|
||||
}
|
||||
|
||||
&:not(.unlocked) {
|
||||
@include S(width, 20px);
|
||||
opacity: 0.15;
|
||||
background-image: none !important;
|
||||
|
||||
&::before {
|
||||
content: " ";
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)}
|
||||
no-repeat;
|
||||
@include S(width, 25px);
|
||||
.icon {
|
||||
opacity: 0.15;
|
||||
}
|
||||
&.editor {
|
||||
.icon {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgba(22, 30, 68, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.editor) {
|
||||
.icon {
|
||||
background-image: uiResource("locked_building.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
&.unlocked {
|
||||
pointer-events: all;
|
||||
transition: all 50ms ease-in-out;
|
||||
transition-property: background-color, transform;
|
||||
.icon {
|
||||
pointer-events: all;
|
||||
transition: all 50ms ease-in-out;
|
||||
transition-property: background-color, transform;
|
||||
cursor: pointer;
|
||||
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgba(30, 40, 90, 0.1);
|
||||
&:hover {
|
||||
background-color: rgba(30, 40, 90, 0.1);
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.9) !important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
// transform: scale(1.05);
|
||||
background-color: rgba(lighten($colorBlueBright, 9), 0.4);
|
||||
|
||||
.keybinding {
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.9) !important;
|
||||
}
|
||||
.puzzle-lock {
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("locked_building.png") center center / #{D(14px)} #{D(14px)}
|
||||
no-repeat;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
// transform: scale(1.05);
|
||||
background-color: rgba(lighten($colorBlueBright, 9), 0.4);
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
|
||||
.keybinding {
|
||||
color: #111;
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) !important;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
|
||||
@include S(width, 14px);
|
||||
@include S(height, 14px);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,14 @@
|
||||
* {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.text {
|
||||
text-transform: uppercase;
|
||||
@include S(margin-bottom, 10px);
|
||||
}
|
||||
}
|
||||
|
||||
> .dialogInner {
|
||||
@@ -168,6 +176,11 @@
|
||||
|
||||
&.errored {
|
||||
background-color: rgb(250, 206, 206);
|
||||
|
||||
&::placeholder {
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
src/css/ingame_hud/puzzle_back_to_menu.scss
Normal file
41
src/css/ingame_hud/puzzle_back_to_menu.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
#ingame_HUD_PuzzleBackToMenu {
|
||||
position: absolute;
|
||||
@include S(top, 10px);
|
||||
@include S(left, 0px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
backdrop-filter: blur(D(1px));
|
||||
padding: D(3px);
|
||||
|
||||
> .button {
|
||||
@include PlainText;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: #333438;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
text-transform: uppercase;
|
||||
@include PlainText;
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
opacity: 1;
|
||||
&:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.95) !important;
|
||||
}
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/state_back_button.png") center center / D(15px) no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/css/ingame_hud/puzzle_complete_notification.scss
Normal file
171
src/css/ingame_hud/puzzle_complete_notification.scss
Normal file
@@ -0,0 +1,171 @@
|
||||
#ingame_HUD_PuzzleCompleteNotification {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: rgba(#333538, 0.95) uiResource("dialog_bg_pattern.png") top left / #{D(10px)} repeat;
|
||||
}
|
||||
|
||||
@include InlineAnimation(0.1s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .dialog {
|
||||
// background: rgba(#222428, 0.5);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include S(padding, 30px);
|
||||
|
||||
@include InlineAnimation(0.5s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
|
||||
> .title {
|
||||
@include SuperHeading;
|
||||
text-transform: uppercase;
|
||||
@include S(font-size, 30px);
|
||||
@include S(margin-bottom, 40px);
|
||||
color: $colorGreenBright !important;
|
||||
|
||||
@include InlineAnimation(0.5s ease-in-out) {
|
||||
0% {
|
||||
transform: translateY(-50vh);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(5vh);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-2vh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .contents {
|
||||
@include InlineAnimation(0.5s ease-in-out) {
|
||||
0% {
|
||||
transform: translateX(-100vw);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(5vw);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(-2vw);
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .stepLike {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(margin-bottom, 10px);
|
||||
@include SuperSmallText;
|
||||
|
||||
> .buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include S(margin, 10px, 0);
|
||||
|
||||
> button {
|
||||
@include S(width, 60px);
|
||||
@include S(height, 60px);
|
||||
@include S(margin, 0, 10px);
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
transition: opacity 0.12s ease-in-out, background-color 0.12s ease-in-out;
|
||||
@include IncreasedClickArea(0px);
|
||||
|
||||
&.liked-yes {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_action_liked_yes.png") center 55% / 60%
|
||||
no-repeat;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $colorRedBright !important;
|
||||
@include InlineAnimation(0.3s ease-in-out) {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.active) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .buttonBar {
|
||||
display: flex;
|
||||
@include S(margin-top, 20px);
|
||||
|
||||
button.continue {
|
||||
background: #555;
|
||||
@include S(margin-right, 10px);
|
||||
}
|
||||
|
||||
button.menu {
|
||||
background-color: $colorGreenBright;
|
||||
}
|
||||
|
||||
> button {
|
||||
@include S(min-width, 100px);
|
||||
@include S(padding, 10px, 20px);
|
||||
@include IncreasedClickArea(0px);
|
||||
}
|
||||
}
|
||||
|
||||
> .actions {
|
||||
position: absolute;
|
||||
@include S(bottom, 40px);
|
||||
|
||||
display: grid;
|
||||
@include S(grid-gap, 15px);
|
||||
grid-auto-flow: column;
|
||||
|
||||
button {
|
||||
@include SuperSmallText;
|
||||
}
|
||||
.report {
|
||||
background-color: $accentColorDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/css/ingame_hud/puzzle_dlc_logo.scss
Normal file
19
src/css/ingame_hud/puzzle_dlc_logo.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
#ingame_HUD_PuzzleDLCLogo {
|
||||
position: absolute;
|
||||
@include S(width, 120px);
|
||||
@include S(height, 40px);
|
||||
@include S(left, 40px);
|
||||
@include S(top, 7px);
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("puzzle_dlc_logo.png") center center / contain no-repeat;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("puzzle_dlc_logo_inverse.png") center center / contain no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/css/ingame_hud/puzzle_editor_controls.scss
Normal file
36
src/css/ingame_hud/puzzle_editor_controls.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
#ingame_HUD_PuzzleEditorControls {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 70px);
|
||||
@include S(left, 10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include SuperDuperSmallText;
|
||||
@include S(width, 200px);
|
||||
|
||||
> span {
|
||||
@include S(margin-bottom, 10px);
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@include DarkThemeInvert;
|
||||
}
|
||||
|
||||
#ingame_HUD_PuzzleEditorTitle {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 18px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-transform: uppercase;
|
||||
@include Heading;
|
||||
text-align: center;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
50
src/css/ingame_hud/puzzle_editor_review.scss
Normal file
50
src/css/ingame_hud/puzzle_editor_review.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
#ingame_HUD_PuzzleEditorReview {
|
||||
position: absolute;
|
||||
@include S(top, 17px);
|
||||
@include S(right, 10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
backdrop-filter: blur(D(1px));
|
||||
padding: D(3px);
|
||||
|
||||
> .button {
|
||||
@include ButtonText;
|
||||
@include IncreasedClickArea(0px);
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: #333438;
|
||||
transition: all 0.12s ease-in-out;
|
||||
text-transform: uppercase;
|
||||
transition-property: opacity, transform;
|
||||
@include PlainText;
|
||||
@include S(padding-right, 25px);
|
||||
opacity: 1;
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.95) !important;
|
||||
}
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/state_next_button.png") right center / D(15px) no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
@include SuperDuperSmallText;
|
||||
@include S(width, 180px);
|
||||
@include S(padding-right, 25px);
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
color: $accentColorDark;
|
||||
}
|
||||
}
|
||||
62
src/css/ingame_hud/puzzle_editor_settings.scss
Normal file
62
src/css/ingame_hud/puzzle_editor_settings.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
#ingame_HUD_PuzzleEditorSettings {
|
||||
position: absolute;
|
||||
background: $ingameHudBg;
|
||||
@include S(padding, 10px);
|
||||
@include S(bottom, 60px);
|
||||
@include S(left, 10px);
|
||||
|
||||
@include SuperSmallText;
|
||||
color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
> .section {
|
||||
> label {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.plusMinus {
|
||||
@include S(margin-top, 5px);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
align-items: center;
|
||||
@include S(grid-gap, 5px);
|
||||
|
||||
label {
|
||||
@include S(margin-right, 10px);
|
||||
}
|
||||
|
||||
button {
|
||||
@include PlainText;
|
||||
@include S(padding, 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include S(width, 15px);
|
||||
@include S(height, 15px);
|
||||
@include IncreasedClickArea(0px);
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: center;
|
||||
@include S(min-width, 15px);
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
> .buttonBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include S(margin-top, 10px);
|
||||
> button {
|
||||
@include S(margin-right, 4px);
|
||||
@include SuperSmallText;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/css/ingame_hud/puzzle_play_metadata.scss
Normal file
129
src/css/ingame_hud/puzzle_play_metadata.scss
Normal file
@@ -0,0 +1,129 @@
|
||||
#ingame_HUD_PuzzlePlayMetadata {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 70px);
|
||||
@include S(left, 10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(width, 200px);
|
||||
|
||||
> .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include SuperSmallText;
|
||||
@include S(margin-bottom, 5px);
|
||||
|
||||
> label {
|
||||
text-transform: uppercase;
|
||||
@include SuperDuperSmallText;
|
||||
color: $accentColorDark;
|
||||
}
|
||||
> span {
|
||||
display: flex;
|
||||
color: darken($accentColorDark, 25);
|
||||
@include SuperSmallText;
|
||||
@include DarkThemeOverride {
|
||||
color: lighten($accentColorDark, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .plays {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
align-self: end;
|
||||
flex-direction: row;
|
||||
@include S(margin-bottom, 10px);
|
||||
opacity: 0.8;
|
||||
@include DarkThemeInvert;
|
||||
@include DarkThemeOverride {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
> .downloads {
|
||||
@include SuperSmallText;
|
||||
color: #000;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
font-weight: bold;
|
||||
@include S(margin-right, 10px);
|
||||
@include S(padding-left, 14px);
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_plays.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .likes {
|
||||
@include SuperSmallText;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
font-weight: bold;
|
||||
@include S(padding-left, 14px);
|
||||
opacity: 0.7;
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .key {
|
||||
button {
|
||||
@include S(margin-top, 2px);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include SuperSmallText;
|
||||
align-self: start;
|
||||
@include S(min-width, 50px);
|
||||
|
||||
&.report {
|
||||
background-color: $accentColorDark;
|
||||
@include SuperDuperSmallText;
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> button {
|
||||
@include S(margin-bottom, 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ingame_HUD_PuzzlePlayTitle {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 18px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-transform: uppercase;
|
||||
@include Heading;
|
||||
text-align: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .name {
|
||||
@include PlainText;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
23
src/css/ingame_hud/puzzle_play_settings.scss
Normal file
23
src/css/ingame_hud/puzzle_play_settings.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
#ingame_HUD_PuzzlePlaySettings {
|
||||
position: absolute;
|
||||
background: $ingameHudBg;
|
||||
@include S(padding, 10px);
|
||||
@include S(bottom, 60px);
|
||||
@include S(left, 10px);
|
||||
|
||||
@include SuperSmallText;
|
||||
color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
> .section {
|
||||
display: grid;
|
||||
@include S(grid-gap, 10px);
|
||||
grid-auto-flow: row;
|
||||
|
||||
> button {
|
||||
@include SuperSmallText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
@import "states/about";
|
||||
@import "states/mobile_warning";
|
||||
@import "states/changelog";
|
||||
@import "states/puzzle_menu";
|
||||
|
||||
@import "ingame_hud/buildings_toolbar";
|
||||
@import "ingame_hud/building_placer";
|
||||
@@ -55,12 +56,21 @@
|
||||
@import "ingame_hud/sandbox_controller";
|
||||
@import "ingame_hud/standalone_advantages";
|
||||
@import "ingame_hud/cat_memes";
|
||||
@import "ingame_hud/puzzle_back_to_menu";
|
||||
@import "ingame_hud/puzzle_editor_review";
|
||||
@import "ingame_hud/puzzle_dlc_logo";
|
||||
@import "ingame_hud/puzzle_editor_controls";
|
||||
@import "ingame_hud/puzzle_editor_settings";
|
||||
@import "ingame_hud/puzzle_play_settings";
|
||||
@import "ingame_hud/puzzle_play_metadata";
|
||||
@import "ingame_hud/puzzle_complete_notification";
|
||||
|
||||
// prettier-ignore
|
||||
$elements:
|
||||
// Base
|
||||
ingame_Canvas,
|
||||
ingame_VignetteOverlay,
|
||||
ingame_HUD_PuzzleDLCLogo,
|
||||
|
||||
// Ingame overlays
|
||||
ingame_HUD_Waypoints,
|
||||
@@ -71,6 +81,14 @@ ingame_HUD_PlacerVariants,
|
||||
ingame_HUD_PinnedShapes,
|
||||
ingame_HUD_GameMenu,
|
||||
ingame_HUD_KeybindingOverlay,
|
||||
ingame_HUD_PuzzleBackToMenu,
|
||||
ingame_HUD_PuzzleEditorReview,
|
||||
ingame_HUD_PuzzleEditorControls,
|
||||
ingame_HUD_PuzzleEditorTitle,
|
||||
ingame_HUD_PuzzleEditorSettings,
|
||||
ingame_HUD_PuzzlePlaySettings,
|
||||
ingame_HUD_PuzzlePlayMetadata,
|
||||
ingame_HUD_PuzzlePlayTitle,
|
||||
ingame_HUD_Notifications,
|
||||
ingame_HUD_DebugInfo,
|
||||
ingame_HUD_EntityDebugger,
|
||||
@@ -94,6 +112,7 @@ ingame_HUD_Statistics,
|
||||
ingame_HUD_ShapeViewer,
|
||||
ingame_HUD_StandaloneAdvantages,
|
||||
ingame_HUD_UnlockNotification,
|
||||
ingame_HUD_PuzzleCompleteNotification,
|
||||
ingame_HUD_SettingsMenu,
|
||||
ingame_HUD_ModalDialogs,
|
||||
ingame_HUD_CatMemes;
|
||||
@@ -113,6 +132,8 @@ body.uiHidden {
|
||||
#ingame_HUD_PlacementHints,
|
||||
#ingame_HUD_GameMenu,
|
||||
#ingame_HUD_PinnedShapes,
|
||||
#ingame_HUD_PuzzleBackToMenu,
|
||||
#ingame_HUD_PuzzleEditorReview,
|
||||
#ingame_HUD_Notifications,
|
||||
#ingame_HUD_TutorialHints,
|
||||
#ingame_HUD_Waypoints,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire,
|
||||
constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage,
|
||||
transistor, analyzer, comparator, item_producer;
|
||||
transistor, analyzer, comparator, item_producer, constant_producer, goal_acceptor, block;
|
||||
|
||||
@each $building in $buildings {
|
||||
[data-icon="building_icons/#{$building}.png"] {
|
||||
/* @load-async */
|
||||
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
|
||||
.icon {
|
||||
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +15,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2,
|
||||
cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage,
|
||||
reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not,
|
||||
logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, item_producer,
|
||||
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator;
|
||||
constant_producer, virtual_processor-stacker, virtual_processor-painter, wire-second, painter,
|
||||
painter-mirrored, comparator, goal_acceptor, block;
|
||||
@each $building in $buildingsAndVariants {
|
||||
[data-icon="building_tutorials/#{$building}.png"] {
|
||||
/* @load-async */
|
||||
@@ -67,7 +70,7 @@ $icons: notification_saved, notification_success, notification_upgrade;
|
||||
}
|
||||
|
||||
$languages: en, de, cs, da, et, es-419, fr, it, pt-BR, sv, tr, el, ru, uk, zh-TW, zh-CN, nb, mt-MT, ar, nl, vi,
|
||||
th, hu, pl, ja, kor, no, pt-PT, fi, ro;
|
||||
th, hu, pl, ja, kor, no, pt-PT, fi, ro, he;
|
||||
|
||||
@each $language in $languages {
|
||||
[data-languageicon="#{$language}"] {
|
||||
|
||||
@@ -88,11 +88,7 @@
|
||||
|
||||
@include S(grid-column-gap, 10px);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
&.demo {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.standaloneBanner {
|
||||
background: rgb(255, 75, 84);
|
||||
@@ -183,7 +179,7 @@
|
||||
.updateLabel {
|
||||
position: absolute;
|
||||
transform: translateX(50%) rotate(-5deg);
|
||||
color: #3291e9;
|
||||
color: #ff590b;
|
||||
@include Heading;
|
||||
font-weight: bold;
|
||||
@include S(right, 40px);
|
||||
@@ -223,9 +219,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.puzzleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: #4cc98a;
|
||||
grid-row: 1 / 2;
|
||||
grid-column: 2 / 3;
|
||||
@include S(padding, 20px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
> .dlcLogo {
|
||||
@include S(width, 200px);
|
||||
}
|
||||
|
||||
> button {
|
||||
@include S(margin-top, 20px);
|
||||
@include Heading;
|
||||
@include S(padding, 10px, 30px);
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-row: 1 / 2;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: #fafafa;
|
||||
@@ -242,6 +262,16 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modeButtons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@include S(grid-column-gap, 10px);
|
||||
align-items: start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.browserWarning {
|
||||
@include S(margin-bottom, 10px);
|
||||
background-color: $colorRedBright;
|
||||
@@ -285,6 +315,18 @@
|
||||
@include S(margin-left, 15px);
|
||||
}
|
||||
|
||||
.playModeButton {
|
||||
@include IncreasedClickArea(0px);
|
||||
@include S(margin-top, 15px);
|
||||
@include S(margin-left, 15px);
|
||||
}
|
||||
|
||||
.editModeButton {
|
||||
@include IncreasedClickArea(0px);
|
||||
@include S(margin-top, 15px);
|
||||
@include S(margin-left, 15px);
|
||||
}
|
||||
|
||||
.savegames {
|
||||
@include S(max-height, 105px);
|
||||
overflow-y: auto;
|
||||
@@ -439,6 +481,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bottomContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
@include S(padding-top, 10px);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@include S(grid-column-gap, 10px);
|
||||
align-items: start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@include S(border-radius, 3px);
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: #424242;
|
||||
background: #33343c;
|
||||
}
|
||||
|
||||
.version {
|
||||
|
||||
277
src/css/states/puzzle_menu.scss
Normal file
277
src/css/states/puzzle_menu.scss
Normal file
@@ -0,0 +1,277 @@
|
||||
#state_PuzzleMenuState {
|
||||
> .headerBar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
|
||||
> h1 {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.createPuzzle {
|
||||
background-color: $colorGreenBright;
|
||||
@include S(margin-left, 5px);
|
||||
}
|
||||
}
|
||||
|
||||
> .container {
|
||||
> .mainContent {
|
||||
overflow: hidden;
|
||||
|
||||
> .categoryChooser {
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
@include S(grid-gap, 2px);
|
||||
@include S(padding-right, 10px);
|
||||
|
||||
> .category {
|
||||
background: $accentColorBright;
|
||||
border-radius: 0;
|
||||
color: $accentColorDark;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, background-color, color;
|
||||
|
||||
&:first-child {
|
||||
@include S(border-top-left-radius, $globalBorderRadius);
|
||||
@include S(border-bottom-left-radius, $globalBorderRadius);
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: $globalBorderRadius;
|
||||
border-bottom-right-radius: $globalBorderRadius;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $colorBlueBright;
|
||||
opacity: 1 !important;
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: $accentColorDark;
|
||||
color: #bbbbc4;
|
||||
|
||||
&.active {
|
||||
background: $colorBlueBright;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .puzzles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(D(180px), 1fr));
|
||||
@include S(grid-auto-rows, 65px);
|
||||
@include S(grid-gap, 7px);
|
||||
@include S(margin-top, 10px);
|
||||
@include S(padding-right, 4px);
|
||||
@include S(height, 360px);
|
||||
overflow-y: scroll;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
|
||||
> .puzzle {
|
||||
width: 100%;
|
||||
@include S(height, 65px);
|
||||
background: #f3f3f8;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: D(15px) D(15px) 1fr;
|
||||
@include S(padding, 5px);
|
||||
@include S(grid-column-gap, 5px);
|
||||
box-sizing: border-box;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@include S(padding-left, 10px);
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: rgba(0, 0, 10, 0.2);
|
||||
}
|
||||
|
||||
@include InlineAnimation(0.12s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f8;
|
||||
}
|
||||
|
||||
> .title {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
@include PlainText;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
justify-self: start;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@include S(padding, 2px, 5px);
|
||||
@include S(height, 17px);
|
||||
}
|
||||
|
||||
> .author {
|
||||
grid-column: 2 / 2;
|
||||
grid-row: 2 / 3;
|
||||
@include SuperSmallText;
|
||||
color: $accentColorDark;
|
||||
align-self: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@include S(padding, 2px, 5px);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 4;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
@include S(width, 45px);
|
||||
@include S(height, 45px);
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> .stats {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 3 / 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
justify-content: center;
|
||||
align-self: end;
|
||||
@include S(height, 14px);
|
||||
|
||||
> .downloads {
|
||||
@include SuperSmallText;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
@include S(margin-right, 5px);
|
||||
@include S(padding-left, 12px);
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include DarkThemeInvert;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_plays.png") #{D(2px)} #{D(2.5px)} / #{D(
|
||||
8px
|
||||
)} #{D(8px)} no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .likes {
|
||||
@include SuperSmallText;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
@include S(padding-left, 14px);
|
||||
opacity: 0.7;
|
||||
@include DarkThemeInvert;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} #{D(2.4px)} / #{D(
|
||||
9px
|
||||
)} #{D(9px)} no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .difficulty {
|
||||
@include SuperSmallText;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
@include S(margin-right, 3px);
|
||||
opacity: 0.7;
|
||||
|
||||
&.stage--easy {
|
||||
color: $colorGreenBright;
|
||||
}
|
||||
&.stage--normal {
|
||||
color: #000;
|
||||
@include DarkThemeInvert;
|
||||
}
|
||||
&.stage--medium {
|
||||
color: $colorOrangeBright;
|
||||
}
|
||||
&.stage--hard {
|
||||
color: $colorRedBright;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
> .icon,
|
||||
> .stats,
|
||||
> .author,
|
||||
> .title {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@include S(top, 10px);
|
||||
@include S(right, 10px);
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
opacity: 0.1;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_complete_indicator.png") center center /
|
||||
contain no-repeat;
|
||||
}
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_complete_indicator_inverse.png") center
|
||||
center / contain no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .loader,
|
||||
> .empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $accentColorDark;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,10 @@ $textLineHeight: 21px;
|
||||
$plainTextFontSize: 13px;
|
||||
$plainTextLineHeight: 17px;
|
||||
|
||||
$supersmallTextFontSize: 10px;
|
||||
$supersmallTextLineHeight: 13px;
|
||||
$superDuperSmallTextFontSize: 8px;
|
||||
$superDuperSmallTextLineHeight: 9px;
|
||||
$superSmallTextFontSize: 10px;
|
||||
$superSmallTextLineHeight: 13px;
|
||||
$buttonFontSize: 14px;
|
||||
$buttonLineHeight: 18px;
|
||||
|
||||
@@ -33,6 +35,7 @@ $accentColorDark: #7d808a;
|
||||
$colorGreenBright: #66bb6a;
|
||||
$colorBlueBright: rgb(74, 151, 223);
|
||||
$colorRedBright: #ef5072;
|
||||
$colorOrangeBright: #ef9d50;
|
||||
$themeColor: #393747;
|
||||
$ingameHudBg: rgba(#333438, 0.9);
|
||||
|
||||
@@ -76,8 +79,16 @@ $mainFontScale: 1;
|
||||
// }
|
||||
}
|
||||
|
||||
@mixin SuperDuperSmallText {
|
||||
@include ScaleFont($superDuperSmallTextFontSize, $superDuperSmallTextLineHeight);
|
||||
font-weight: $mainFontWeight;
|
||||
font-family: $mainFont;
|
||||
letter-spacing: $mainFontSpacing;
|
||||
@include DebugText(green);
|
||||
}
|
||||
|
||||
@mixin SuperSmallText {
|
||||
@include ScaleFont($supersmallTextFontSize, $supersmallTextLineHeight);
|
||||
@include ScaleFont($superSmallTextFontSize, $superSmallTextLineHeight);
|
||||
font-weight: $mainFontWeight;
|
||||
font-family: $mainFont;
|
||||
letter-spacing: $mainFontSpacing;
|
||||
|
||||
@@ -31,6 +31,9 @@ import { PreloadState } from "./states/preload";
|
||||
import { SettingsState } from "./states/settings";
|
||||
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||
import { RestrictionManager } from "./core/restriction_manager";
|
||||
import { PuzzleMenuState } from "./states/puzzle_menu";
|
||||
import { ClientAPI } from "./platform/api";
|
||||
import { LoginState } from "./states/login";
|
||||
|
||||
/**
|
||||
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
||||
@@ -72,6 +75,7 @@ export class Application {
|
||||
this.savegameMgr = new SavegameManager(this);
|
||||
this.inputMgr = new InputDistributor(this);
|
||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||
this.clientApi = new ClientAPI(this);
|
||||
|
||||
// Restrictions (Like demo etc)
|
||||
this.restrictionMgr = new RestrictionManager(this);
|
||||
@@ -159,6 +163,8 @@ export class Application {
|
||||
KeybindingsState,
|
||||
AboutState,
|
||||
ChangelogState,
|
||||
PuzzleMenuState,
|
||||
LoginState,
|
||||
];
|
||||
|
||||
for (let i = 0; i < states.length; ++i) {
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
export const CHANGELOG = [
|
||||
{
|
||||
version: "1.3.1",
|
||||
date: "beta",
|
||||
version: "1.4.0",
|
||||
date: "UNRELEASED",
|
||||
entries: [
|
||||
"Fixed savegames getting corrupt in rare conditions",
|
||||
"Fixed game crashing sometimes since the achievements update",
|
||||
"Added puzzle mode",
|
||||
"Belts in blueprints should now always paste correctly",
|
||||
"You can now clear belts by selecting them, and then pressing 'B'",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.3.1",
|
||||
date: "16.04.2021",
|
||||
entries: G_CHINA_VERSION
|
||||
? [
|
||||
"第13关的交付目标更改为:中国古代指南针。(感谢玩家:凯风入心 创作并提供",
|
||||
"第17关的交付目标更改为:永乐通宝。(感谢玩家:金天赐 创作并提供",
|
||||
"第22关的交付目标更改为:凤凰。(感谢玩家:我没得眼镜 创作并提供",
|
||||
"第23关的交付目标更改为:古代车轮。(感谢玩家:我没得眼镜 创作并提供",
|
||||
"第24关的交付目标更改为:大熊猫。(感谢玩家:窝囸倪现任 创作并提供",
|
||||
|
||||
"修复了一些特定情况下偶尔会发生的存档损坏问题",
|
||||
"修复了成就更新后有时候游戏崩溃的问题",
|
||||
]
|
||||
: [
|
||||
"Fixed savegames getting corrupt in rare conditions",
|
||||
"Fixed game crashing sometimes since the achievements update",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.3.0",
|
||||
date: "12.03.2020",
|
||||
|
||||
@@ -51,9 +51,12 @@ export class AnimationFrame {
|
||||
dt = resetDtMs;
|
||||
}
|
||||
|
||||
this.frameEmitted.dispatch(dt);
|
||||
try {
|
||||
this.frameEmitted.dispatch(dt);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
this.lastTime = time;
|
||||
|
||||
window.requestAnimationFrame(this.boundMethod);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,13 @@ export const globalConfig = {
|
||||
|
||||
readerAnalyzeIntervalSeconds: 10,
|
||||
|
||||
goalAcceptorMinimumDurationSeconds: 5,
|
||||
goalAcceptorsPerProducer: 4.5,
|
||||
puzzleModeSpeed: 3,
|
||||
puzzleMinBoundsSize: 2,
|
||||
puzzleMaxBoundsSize: 20,
|
||||
puzzleValidationDurationSeconds: 30,
|
||||
|
||||
buildingSpeeds: {
|
||||
cutter: 1 / 4,
|
||||
cutterQuad: 1 / 4,
|
||||
@@ -93,7 +100,7 @@ export const globalConfig = {
|
||||
gameSpeed: 1,
|
||||
|
||||
warmupTimeSecondsFast: 0.5,
|
||||
warmupTimeSecondsRegular: 3,
|
||||
warmupTimeSecondsRegular: 1.5,
|
||||
|
||||
smoothing: {
|
||||
smoothMainCanvas: smoothCanvas && true,
|
||||
|
||||
@@ -62,6 +62,9 @@ export default {
|
||||
// Allows unlocked achievements to be logged to console in the local build
|
||||
// testAchievements: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables use of (some) existing flags within the puzzle mode context
|
||||
// testPuzzleMode: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the automatic switch to an overview when zooming out
|
||||
// disableMapOverview: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
@@ -123,4 +123,6 @@ function catchErrors(message, source, lineno, colno, error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
window.onerror = catchErrors;
|
||||
if (!G_IS_DEV) {
|
||||
window.onerror = catchErrors;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Factory } from "./factory";
|
||||
* @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed
|
||||
* @typedef {import("../game/component").Component} Component
|
||||
* @typedef {import("../game/base_item").BaseItem} BaseItem
|
||||
* @typedef {import("../game/game_mode").GameMode} GameMode
|
||||
* @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding
|
||||
|
||||
|
||||
@@ -19,6 +20,9 @@ export let gBuildingsByCategory = null;
|
||||
/** @type {FactoryTemplate<Component>} */
|
||||
export let gComponentRegistry = new Factory("component");
|
||||
|
||||
/** @type {FactoryTemplate<GameMode>} */
|
||||
export let gGameModeRegistry = new Factory("gameMode");
|
||||
|
||||
/** @type {FactoryTemplate<BaseGameSpeed>} */
|
||||
export let gGameSpeedRegistry = new Factory("gamespeed");
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ export class Dialog {
|
||||
* Dialog which simply shows a loading spinner
|
||||
*/
|
||||
export class DialogLoading extends Dialog {
|
||||
constructor(app) {
|
||||
constructor(app, text = "") {
|
||||
super({
|
||||
app,
|
||||
title: "",
|
||||
@@ -279,6 +279,8 @@ export class DialogLoading extends Dialog {
|
||||
// Loading dialog can not get closed with back button
|
||||
this.inputReciever.backButton.removeAll();
|
||||
this.inputReciever.context = "dialog-loading";
|
||||
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
createElement() {
|
||||
@@ -287,6 +289,13 @@ export class DialogLoading extends Dialog {
|
||||
elem.classList.add("loadingDialog");
|
||||
this.element = elem;
|
||||
|
||||
if (this.text) {
|
||||
const text = document.createElement("div");
|
||||
text.classList.add("text");
|
||||
text.innerText = this.text;
|
||||
elem.appendChild(text);
|
||||
}
|
||||
|
||||
const loader = document.createElement("div");
|
||||
loader.classList.add("prefab_LoadingTextWithAnim");
|
||||
loader.classList.add("loadingIndicator");
|
||||
@@ -309,7 +318,7 @@ export class DialogOptionChooser extends Dialog {
|
||||
<div class='option ${value === options.active ? "active" : ""} ${
|
||||
iconPrefix ? "hasIcon" : ""
|
||||
}' data-optionvalue='${value}'>
|
||||
${iconHtml}
|
||||
${iconHtml}
|
||||
<span class='title'>${text}</span>
|
||||
${descHtml}
|
||||
</div>
|
||||
@@ -444,7 +453,7 @@ export class DialogWithForm extends Dialog {
|
||||
for (let i = 0; i < this.formElements.length; ++i) {
|
||||
const elem = this.formElements[i];
|
||||
elem.bindEvents(div, this.clickDetectors);
|
||||
elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
|
||||
// elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
|
||||
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,11 @@ export class FormElementInput extends FormElement {
|
||||
return this.element.value;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.element.value = value;
|
||||
this.updateErrorState();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,15 @@ export class Rectangle {
|
||||
return new Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
*/
|
||||
static centered(width, height) {
|
||||
return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a intersects b
|
||||
* @param {Rectangle} a
|
||||
@@ -72,7 +81,7 @@ export class Rectangle {
|
||||
/**
|
||||
* Returns if this rectangle is equal to the other while taking an epsilon into account
|
||||
* @param {Rectangle} other
|
||||
* @param {number} epsilon
|
||||
* @param {number} [epsilon]
|
||||
*/
|
||||
equalsEpsilon(other, epsilon) {
|
||||
return (
|
||||
@@ -287,6 +296,15 @@ export class Rectangle {
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the rectangle fully intersects the given rectangle
|
||||
* @param {Rectangle} rect
|
||||
*/
|
||||
intersectsFully(rect) {
|
||||
const intersection = this.getIntersection(rect);
|
||||
return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the union of this rectangle with another
|
||||
* @param {Rectangle} rect
|
||||
|
||||
@@ -17,6 +17,17 @@ export class Signal {
|
||||
++this.modifyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new signal listener
|
||||
* @param {function} receiver
|
||||
* @param {object} scope
|
||||
*/
|
||||
addToTop(receiver, scope = null) {
|
||||
assert(receiver, "receiver is null");
|
||||
this.receivers.unshift({ receiver, scope });
|
||||
++this.modifyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the signal
|
||||
* @param {...any} payload
|
||||
|
||||
@@ -90,9 +90,9 @@ export class StateManager {
|
||||
dialogParent.classList.add("modalDialogParent");
|
||||
document.body.appendChild(dialogParent);
|
||||
|
||||
this.currentState.internalEnterCallback(payload);
|
||||
this.app.sound.playThemeMusic(this.currentState.getThemeMusic());
|
||||
|
||||
this.currentState.internalEnterCallback(payload);
|
||||
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
|
||||
|
||||
this.app.analytics.trackStateEnter(key);
|
||||
|
||||
@@ -11,6 +11,7 @@ export const itemTypes = ["shape", "color", "boolean"];
|
||||
export class BaseItem extends BasicSerializableObject {
|
||||
constructor() {
|
||||
super();
|
||||
this._type = this.getItemType();
|
||||
}
|
||||
|
||||
static getId() {
|
||||
|
||||
@@ -13,8 +13,6 @@ import { GameRoot } from "./root";
|
||||
const logger = createLogger("belt_path");
|
||||
|
||||
// Helpers for more semantic access into interleaved arrays
|
||||
const _nextDistance = 0;
|
||||
const _item = 1;
|
||||
|
||||
const DEBUG = G_IS_DEV && false;
|
||||
|
||||
@@ -110,6 +108,15 @@ export class BeltPath extends BasicSerializableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all items
|
||||
*/
|
||||
clearAllItems() {
|
||||
this.items = [];
|
||||
this.spacingToFirstItem = this.totalLength;
|
||||
this.numCompressedItemsAfterFirstItem = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this path can accept a new item
|
||||
* @returns {boolean}
|
||||
@@ -174,7 +181,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
* Recomputes cache variables once the path was changed
|
||||
*/
|
||||
onPathChanged() {
|
||||
this.acceptorTarget = this.computeAcceptingEntityAndSlot();
|
||||
this.boundAcceptor = this.computeAcceptingEntityAndSlot();
|
||||
|
||||
/**
|
||||
* How many items past the first item are compressed
|
||||
@@ -192,7 +199,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
/**
|
||||
* Finds the entity which accepts our items
|
||||
* @param {boolean=} debug_Silent Whether debug output should be silent
|
||||
* @return {{ entity: Entity, slot: number, direction?: enumDirection }}
|
||||
* @return { (BaseItem, number) => boolean }
|
||||
*/
|
||||
computeAcceptingEntityAndSlot(debug_Silent = false) {
|
||||
DEBUG && !debug_Silent && logger.log("Recomputing acceptor target");
|
||||
@@ -214,55 +221,142 @@ export class BeltPath extends BasicSerializableObject {
|
||||
"regular"
|
||||
);
|
||||
|
||||
if (targetEntity) {
|
||||
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
|
||||
const targetStaticComp = targetEntity.components.StaticMapEntity;
|
||||
const targetBeltComp = targetEntity.components.Belt;
|
||||
if (!targetEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for belts (special case)
|
||||
if (targetBeltComp) {
|
||||
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
|
||||
DEBUG &&
|
||||
!debug_Silent &&
|
||||
logger.log(
|
||||
" Entity is accepting items from",
|
||||
ejectSlotWsDirection,
|
||||
"vs",
|
||||
beltAcceptingDirection,
|
||||
"Rotation:",
|
||||
targetStaticComp.rotation
|
||||
const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts;
|
||||
|
||||
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
|
||||
const targetStaticComp = targetEntity.components.StaticMapEntity;
|
||||
const targetBeltComp = targetEntity.components.Belt;
|
||||
|
||||
// Check for belts (special case)
|
||||
if (targetBeltComp) {
|
||||
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
|
||||
DEBUG &&
|
||||
!debug_Silent &&
|
||||
logger.log(
|
||||
" Entity is accepting items from",
|
||||
ejectSlotWsDirection,
|
||||
"vs",
|
||||
beltAcceptingDirection,
|
||||
"Rotation:",
|
||||
targetStaticComp.rotation
|
||||
);
|
||||
if (ejectSlotWsDirection === beltAcceptingDirection) {
|
||||
return item => {
|
||||
const path = targetBeltComp.assignedPath;
|
||||
assert(path, "belt has no path");
|
||||
return path.tryAcceptItem(item);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for item acceptors
|
||||
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
||||
if (!targetAcceptorComp) {
|
||||
// Entity doesn't accept items
|
||||
return;
|
||||
}
|
||||
|
||||
const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection);
|
||||
const matchingSlot = targetAcceptorComp.findMatchingSlot(
|
||||
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
|
||||
ejectingDirection
|
||||
);
|
||||
|
||||
if (!matchingSlot) {
|
||||
// No matching slot found
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingSlotIndex = matchingSlot.index;
|
||||
const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex);
|
||||
if (!passOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingDirection = enumInvertedDirections[ejectingDirection];
|
||||
const filter = matchingSlot.slot.filter;
|
||||
|
||||
return function (item, remainingProgress = 0.0) {
|
||||
// Check if the acceptor has a filter
|
||||
if (filter && item._type !== filter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to pass over
|
||||
if (passOver(item, matchingSlotIndex)) {
|
||||
// Trigger animation on the acceptor comp
|
||||
if (noSimplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
matchingSlotIndex,
|
||||
matchingDirection,
|
||||
item,
|
||||
remainingProgress
|
||||
);
|
||||
if (ejectSlotWsDirection === beltAcceptingDirection) {
|
||||
return {
|
||||
entity: targetEntity,
|
||||
direction: null,
|
||||
slot: 0,
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// Check for item acceptors
|
||||
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
||||
if (!targetAcceptorComp) {
|
||||
// Entity doesn't accept items
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Computes a method to pass over the item to the entity
|
||||
* @param {Entity} entity
|
||||
* @param {number} matchingSlotIndex
|
||||
* @returns {(item: BaseItem, slotIndex: number) => boolean | void}
|
||||
*/
|
||||
computePassOverFunctionWithoutBelts(entity, matchingSlotIndex) {
|
||||
const systems = this.root.systemMgr.systems;
|
||||
const hubGoals = this.root.hubGoals;
|
||||
|
||||
const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection);
|
||||
const matchingSlot = targetAcceptorComp.findMatchingSlot(
|
||||
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
|
||||
ejectingDirection
|
||||
);
|
||||
// NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS
|
||||
|
||||
if (!matchingSlot) {
|
||||
// No matching slot found
|
||||
return;
|
||||
}
|
||||
const itemProcessorComp = entity.components.ItemProcessor;
|
||||
if (itemProcessorComp) {
|
||||
// Its an item processor ..
|
||||
return function (item) {
|
||||
// Check for potential filters
|
||||
if (!systems.itemProcessor.checkRequirements(entity, item, matchingSlotIndex)) {
|
||||
return;
|
||||
}
|
||||
return itemProcessorComp.tryTakeItem(item, matchingSlotIndex);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
entity: targetEntity,
|
||||
slot: matchingSlot.index,
|
||||
direction: enumInvertedDirections[ejectingDirection],
|
||||
const undergroundBeltComp = entity.components.UndergroundBelt;
|
||||
if (undergroundBeltComp) {
|
||||
// Its an underground belt. yay.
|
||||
return function (item) {
|
||||
return undergroundBeltComp.tryAcceptExternalItem(
|
||||
item,
|
||||
hubGoals.getUndergroundBeltBaseSpeed()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const storageComp = entity.components.Storage;
|
||||
if (storageComp) {
|
||||
// It's a storage
|
||||
return function (item) {
|
||||
if (storageComp.canAcceptItem(item)) {
|
||||
storageComp.takeItem(item);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const filterComp = entity.components.Filter;
|
||||
if (filterComp) {
|
||||
// It's a filter! Unfortunately the filter has to know a lot about it's
|
||||
// surrounding state and components, so it can't be within the component itself.
|
||||
return function (item) {
|
||||
if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -365,17 +459,17 @@ export class BeltPath extends BasicSerializableObject {
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
const item = this.items[i];
|
||||
|
||||
if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) {
|
||||
if (item[0 /* nextDistance */] < 0 || item[0 /* nextDistance */] > this.totalLength + 0.02) {
|
||||
return fail(
|
||||
"Item has invalid offset to next item: ",
|
||||
item[_nextDistance],
|
||||
item[0 /* nextDistance */],
|
||||
"(total length:",
|
||||
this.totalLength,
|
||||
")"
|
||||
);
|
||||
}
|
||||
|
||||
currentPos += item[_nextDistance];
|
||||
currentPos += item[0 /* nextDistance */];
|
||||
}
|
||||
|
||||
// Check the total sum matches
|
||||
@@ -387,7 +481,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
this.spacingToFirstItem,
|
||||
") and items does not match total length (",
|
||||
this.totalLength,
|
||||
") -> items: " + this.items.map(i => i[_nextDistance]).join("|")
|
||||
") -> items: " + this.items.map(i => i[0 /* nextDistance */]).join("|")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,43 +493,14 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
// Check acceptor
|
||||
const acceptor = this.computeAcceptingEntityAndSlot(true);
|
||||
if (!!acceptor !== !!this.acceptorTarget) {
|
||||
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.acceptorTarget);
|
||||
}
|
||||
|
||||
if (acceptor) {
|
||||
if (this.acceptorTarget.entity !== acceptor.entity) {
|
||||
return fail(
|
||||
"Mismatching entity on acceptor target:",
|
||||
acceptor.entity.uid,
|
||||
"vs",
|
||||
this.acceptorTarget.entity.uid
|
||||
);
|
||||
}
|
||||
|
||||
if (this.acceptorTarget.slot !== acceptor.slot) {
|
||||
return fail(
|
||||
"Mismatching entity on acceptor target:",
|
||||
acceptor.slot,
|
||||
"vs stored",
|
||||
this.acceptorTarget.slot
|
||||
);
|
||||
}
|
||||
|
||||
if (this.acceptorTarget.direction !== acceptor.direction) {
|
||||
return fail(
|
||||
"Mismatching direction on acceptor target:",
|
||||
acceptor.direction,
|
||||
"vs stored",
|
||||
this.acceptorTarget.direction
|
||||
);
|
||||
}
|
||||
if (!!acceptor !== !!this.boundAcceptor) {
|
||||
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.boundAcceptor);
|
||||
}
|
||||
|
||||
// Check first nonzero offset
|
||||
let firstNonzero = 0;
|
||||
for (let i = this.items.length - 2; i >= 0; --i) {
|
||||
if (this.items[i][_nextDistance] < globalConfig.itemSpacingOnBelts + 1e-5) {
|
||||
if (this.items[i][0 /* nextDistance */] < globalConfig.itemSpacingOnBelts + 1e-5) {
|
||||
++firstNonzero;
|
||||
} else {
|
||||
break;
|
||||
@@ -483,11 +548,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
" Extended spacing of last item from",
|
||||
lastItem[_nextDistance],
|
||||
lastItem[0 /* nextDistance */],
|
||||
"to",
|
||||
lastItem[_nextDistance] + additionalLength
|
||||
lastItem[0 /* nextDistance */] + additionalLength
|
||||
);
|
||||
lastItem[_nextDistance] += additionalLength;
|
||||
lastItem[0 /* nextDistance */] += additionalLength;
|
||||
}
|
||||
|
||||
// Assign reference
|
||||
@@ -618,7 +683,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"Old items are",
|
||||
this.items.map(i => i[_nextDistance])
|
||||
this.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// Create second path
|
||||
@@ -628,7 +693,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
let itemPos = this.spacingToFirstItem;
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
const item = this.items[i];
|
||||
const distanceToNext = item[_nextDistance];
|
||||
const distanceToNext = item[0 /* nextDistance */];
|
||||
|
||||
DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next");
|
||||
|
||||
@@ -643,7 +708,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Check if its on the second path (otherwise its on the removed belt and simply lost)
|
||||
if (itemPos >= secondPathStart) {
|
||||
// Put item on second path
|
||||
secondPath.items.push([distanceToNext, item[_item]]);
|
||||
secondPath.items.push([distanceToNext, item[1 /* item */]]);
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
" Put item to second path @",
|
||||
@@ -672,7 +737,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
"to",
|
||||
clampedDistanceToNext
|
||||
);
|
||||
item[_nextDistance] = clampedDistanceToNext;
|
||||
item[0 /* nextDistance */] = clampedDistanceToNext;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,13 +748,13 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"New items are",
|
||||
this.items.map(i => i[_nextDistance])
|
||||
this.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"And second path items are",
|
||||
secondPath.items.map(i => i[_nextDistance])
|
||||
secondPath.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// Adjust our total length
|
||||
@@ -776,9 +841,17 @@ export class BeltPath extends BasicSerializableObject {
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]);
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"Item",
|
||||
i,
|
||||
"is at",
|
||||
itemOffset,
|
||||
"with next offset",
|
||||
item[0 /* nextDistance */]
|
||||
);
|
||||
lastItemOffset = itemOffset;
|
||||
itemOffset += item[_nextDistance];
|
||||
itemOffset += item[0 /* nextDistance */];
|
||||
}
|
||||
|
||||
// If we still have an item, make sure the last item matches
|
||||
@@ -805,7 +878,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
this.totalLength,
|
||||
")"
|
||||
);
|
||||
this.items[this.items.length - 1][_nextDistance] = lastDistance;
|
||||
this.items[this.items.length - 1][0 /* nextDistance */] = lastDistance;
|
||||
} else {
|
||||
DEBUG && logger.log(" Removed all items so we'll update spacing to total length");
|
||||
|
||||
@@ -893,7 +966,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
" Items:",
|
||||
this.items.map(i => i[_nextDistance])
|
||||
this.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// Find offset to first item
|
||||
@@ -912,7 +985,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// This item must be dropped
|
||||
this.items.splice(i, 1);
|
||||
i -= 1;
|
||||
itemOffset += item[_nextDistance];
|
||||
itemOffset += item[0 /* nextDistance */];
|
||||
continue;
|
||||
} else {
|
||||
// This item can be kept, thus its the first we know
|
||||
@@ -990,9 +1063,13 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Now, update the distance of our last item
|
||||
if (this.items.length !== 0) {
|
||||
const lastItem = this.items[this.items.length - 1];
|
||||
lastItem[_nextDistance] += otherPath.spacingToFirstItem;
|
||||
lastItem[0 /* nextDistance */] += otherPath.spacingToFirstItem;
|
||||
DEBUG &&
|
||||
logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now");
|
||||
logger.log(
|
||||
" Add distance to last item, effectively being",
|
||||
lastItem[0 /* nextDistance */],
|
||||
"now"
|
||||
);
|
||||
} else {
|
||||
// Seems we have no items, update our first item distance
|
||||
this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem;
|
||||
@@ -1012,7 +1089,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Aaand push the other paths items
|
||||
for (let i = 0; i < otherPath.items.length; ++i) {
|
||||
const item = otherPath.items[i];
|
||||
this.items.push([item[_nextDistance], item[_item]]);
|
||||
this.items.push([item[0 /* nextDistance */], item[1 /* item */]]);
|
||||
}
|
||||
|
||||
// Update bounds
|
||||
@@ -1046,6 +1123,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
this.debug_checkIntegrity("pre-update");
|
||||
}
|
||||
|
||||
// Skip empty belts
|
||||
if (this.items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Divide by item spacing on belts since we use throughput and not speed
|
||||
let beltSpeed =
|
||||
this.root.hubGoals.getBeltBaseSpeed() *
|
||||
@@ -1074,30 +1156,40 @@ export class BeltPath extends BasicSerializableObject {
|
||||
lastItemProcessed === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts;
|
||||
|
||||
// Compute how much we can advance
|
||||
const clampedProgress = Math.max(
|
||||
0,
|
||||
Math.min(remainingVelocity, nextDistanceAndItem[_nextDistance] - minimumSpacing)
|
||||
);
|
||||
let clampedProgress = nextDistanceAndItem[0 /* nextDistance */] - minimumSpacing;
|
||||
|
||||
// Make sure we don't advance more than the remaining velocity has stored
|
||||
if (remainingVelocity < clampedProgress) {
|
||||
clampedProgress = remainingVelocity;
|
||||
}
|
||||
|
||||
// Make sure we don't advance back
|
||||
if (clampedProgress < 0) {
|
||||
clampedProgress = 0;
|
||||
}
|
||||
|
||||
// Reduce our velocity by the amount we consumed
|
||||
remainingVelocity -= clampedProgress;
|
||||
|
||||
// Reduce the spacing
|
||||
nextDistanceAndItem[_nextDistance] -= clampedProgress;
|
||||
nextDistanceAndItem[0 /* nextDistance */] -= clampedProgress;
|
||||
|
||||
// Advance all items behind by the progress we made
|
||||
this.spacingToFirstItem += clampedProgress;
|
||||
|
||||
// If the last item can be ejected, eject it and reduce the spacing, because otherwise
|
||||
// we lose velocity
|
||||
if (isFirstItemProcessed && nextDistanceAndItem[_nextDistance] < 1e-7) {
|
||||
if (isFirstItemProcessed && nextDistanceAndItem[0 /* nextDistance */] < 1e-7) {
|
||||
// Store how much velocity we "lost" because we bumped the item to the end of the
|
||||
// belt but couldn't move it any farther. We need this to tell the item acceptor
|
||||
// animation to start a tad later, so everything matches up. Yes I'm a perfectionist.
|
||||
const excessVelocity = beltSpeed - clampedProgress;
|
||||
|
||||
// Try to directly get rid of the item
|
||||
if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) {
|
||||
if (
|
||||
this.boundAcceptor &&
|
||||
this.boundAcceptor(nextDistanceAndItem[1 /* item */], excessVelocity)
|
||||
) {
|
||||
this.items.pop();
|
||||
|
||||
const itemBehind = this.items[lastItemProcessed - 1];
|
||||
@@ -1108,11 +1200,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Also see #999
|
||||
const fixupProgress = Math.max(
|
||||
0,
|
||||
Math.min(remainingVelocity, itemBehind[_nextDistance])
|
||||
Math.min(remainingVelocity, itemBehind[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// See above
|
||||
itemBehind[_nextDistance] -= fixupProgress;
|
||||
itemBehind[0 /* nextDistance */] -= fixupProgress;
|
||||
remainingVelocity -= fixupProgress;
|
||||
this.spacingToFirstItem += fixupProgress;
|
||||
}
|
||||
@@ -1145,8 +1237,8 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
// Check if we have an item which is ready to be emitted
|
||||
const lastItem = this.items[this.items.length - 1];
|
||||
if (lastItem && lastItem[_nextDistance] === 0 && this.acceptorTarget) {
|
||||
if (this.tryHandOverItem(lastItem[_item])) {
|
||||
if (lastItem && lastItem[0 /* nextDistance */] === 0) {
|
||||
if (this.boundAcceptor && this.boundAcceptor(lastItem[1 /* item */])) {
|
||||
this.items.pop();
|
||||
this.numCompressedItemsAfterFirstItem = Math.max(
|
||||
0,
|
||||
@@ -1160,50 +1252,6 @@ export class BeltPath extends BasicSerializableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to hand over the item to the end entity
|
||||
* @param {BaseItem} item
|
||||
*/
|
||||
tryHandOverItem(item, remainingProgress = 0.0) {
|
||||
if (!this.acceptorTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
|
||||
|
||||
// Check if the acceptor has a filter for example
|
||||
if (targetAcceptorComp && !targetAcceptorComp.canAcceptItem(this.acceptorTarget.slot, item)) {
|
||||
// Well, this item is not accepted
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to pass over
|
||||
if (
|
||||
this.root.systemMgr.systems.itemEjector.tryPassOverItem(
|
||||
item,
|
||||
this.acceptorTarget.entity,
|
||||
this.acceptorTarget.slot
|
||||
)
|
||||
) {
|
||||
// Trigger animation on the acceptor comp
|
||||
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
|
||||
if (targetAcceptorComp) {
|
||||
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
this.acceptorTarget.slot,
|
||||
this.acceptorTarget.direction,
|
||||
item,
|
||||
remainingProgress
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a world space position from the given progress
|
||||
* @param {number} progress
|
||||
@@ -1270,11 +1318,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
parameters.context.font = "6px GameFont";
|
||||
parameters.context.fillStyle = "#111";
|
||||
parameters.context.fillText(
|
||||
"" + round4Digits(nextDistanceAndItem[_nextDistance]),
|
||||
"" + round4Digits(nextDistanceAndItem[0 /* nextDistance */]),
|
||||
worldPos.x + 5,
|
||||
worldPos.y + 2
|
||||
);
|
||||
progress += nextDistanceAndItem[_nextDistance];
|
||||
progress += nextDistanceAndItem[0 /* nextDistance */];
|
||||
|
||||
if (this.items.length - 1 - this.numCompressedItemsAfterFirstItem === i) {
|
||||
parameters.context.fillStyle = "red";
|
||||
@@ -1370,7 +1418,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
const centerPos = staticComp.localTileToWorld(centerPosLocal).toWorldSpaceCenterOfTile();
|
||||
|
||||
parameters.context.globalAlpha = 0.5;
|
||||
firstItem[_item].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
|
||||
firstItem[1 /* item */].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
|
||||
parameters.context.globalAlpha = 1;
|
||||
}
|
||||
|
||||
@@ -1402,7 +1450,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
const distanceAndItem = this.items[currentItemIndex];
|
||||
|
||||
distanceAndItem[_item].drawItemCenteredClipped(
|
||||
distanceAndItem[1 /* item */].drawItemCenteredClipped(
|
||||
worldPos.x,
|
||||
worldPos.y,
|
||||
parameters,
|
||||
@@ -1410,7 +1458,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
);
|
||||
|
||||
// Check for the next item
|
||||
currentItemPos += distanceAndItem[_nextDistance];
|
||||
currentItemPos += distanceAndItem[0 /* nextDistance */];
|
||||
++currentItemIndex;
|
||||
|
||||
if (currentItemIndex >= this.items.length) {
|
||||
|
||||
@@ -149,29 +149,31 @@ export class Blueprint {
|
||||
*/
|
||||
tryPlace(root, tile) {
|
||||
return root.logic.performBulkOperation(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
||||
continue;
|
||||
return root.logic.performImmutableOperation(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clone = entity.clone();
|
||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||
root.map.placeStaticEntity(clone);
|
||||
root.entityMgr.registerEntity(clone);
|
||||
count++;
|
||||
}
|
||||
|
||||
const clone = entity.clone();
|
||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||
root.map.placeStaticEntity(clone);
|
||||
root.entityMgr.registerEntity(clone);
|
||||
count++;
|
||||
}
|
||||
root.signals.bulkAchievementCheck.dispatch(
|
||||
ACHIEVEMENTS.placeBlueprint,
|
||||
count,
|
||||
ACHIEVEMENTS.placeBp1000,
|
||||
count
|
||||
);
|
||||
|
||||
root.signals.bulkAchievementCheck.dispatch(
|
||||
ACHIEVEMENTS.placeBlueprint,
|
||||
count,
|
||||
ACHIEVEMENTS.placeBp1000,
|
||||
count
|
||||
);
|
||||
|
||||
return count !== 0;
|
||||
return count !== 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ export class MetaBalancerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let speedMultiplier = 2;
|
||||
switch (variant) {
|
||||
case enumBalancerVariants.merger:
|
||||
@@ -88,9 +92,11 @@ export class MetaBalancerBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getAvailableVariants(root) {
|
||||
let available = [defaultBuildingVariant];
|
||||
const deterministic = root.gameMode.getIsDeterministic();
|
||||
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
|
||||
let available = deterministic ? [] : [defaultBuildingVariant];
|
||||
|
||||
if (!deterministic && root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
|
||||
available.push(enumBalancerVariants.merger, enumBalancerVariants.mergerInverse);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ export class MetaBeltBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const beltSpeed = root.hubGoals.getBeltBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
|
||||
}
|
||||
|
||||
30
src/js/game/buildings/block.js
Normal file
30
src/js/game/buildings/block.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* typehints:start */
|
||||
import { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
|
||||
export class MetaBlockBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
super("block");
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#333";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root) {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setupEntityComponents(entity) {}
|
||||
}
|
||||
50
src/js/game/buildings/constant_producer.js
Normal file
50
src/js/game/buildings/constant_producer.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/* typehints:start */
|
||||
import { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProducerType, ItemProducerComponent } from "../components/item_producer";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
|
||||
export class MetaConstantProducerBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
super("constant_producer");
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#bfd630";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root) {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setupEntityComponents(entity) {
|
||||
entity.addComponent(
|
||||
new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
})
|
||||
);
|
||||
entity.addComponent(
|
||||
new ItemProducerComponent({
|
||||
type: enumItemProducerType.wireless,
|
||||
})
|
||||
);
|
||||
entity.addComponent(
|
||||
new ConstantSignalComponent({
|
||||
type: enumConstantSignalType.wireless,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ export class MetaCutterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(
|
||||
variant === enumCutterVariants.quad
|
||||
? enumItemProcessorTypes.cutterQuad
|
||||
|
||||
@@ -40,6 +40,9 @@ export class MetaFilterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const beltSpeed = root.hubGoals.getBeltBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
|
||||
}
|
||||
|
||||
56
src/js/game/buildings/goal_acceptor.js
Normal file
56
src/js/game/buildings/goal_acceptor.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/* typehints:start */
|
||||
import { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumBeltReaderType, BeltReaderComponent } from "../components/belt_reader";
|
||||
import { GoalAcceptorComponent } from "../components/goal_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
|
||||
export class MetaGoalAcceptorBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
super("goal_acceptor");
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#ce418a";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root) {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setupEntityComponents(entity) {
|
||||
entity.addComponent(
|
||||
new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
directions: [enumDirection.bottom],
|
||||
filter: "shape",
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(
|
||||
new ItemProcessorComponent({
|
||||
processorType: enumItemProcessorTypes.goal,
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(new GoalAcceptorComponent({}));
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,6 @@ export class MetaItemProducerBuilding extends MetaBuilding {
|
||||
],
|
||||
})
|
||||
);
|
||||
entity.addComponent(new ItemProducerComponent());
|
||||
entity.addComponent(new ItemProducerComponent({}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ export class MetaMinerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getMinerBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ export class MetaMixerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.mixer);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ export class MetaPainterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant:
|
||||
case enumPainterVariants.mirrored: {
|
||||
@@ -71,7 +74,10 @@ export class MetaPainterBuilding extends MetaBuilding {
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) {
|
||||
variants.push(enumPainterVariants.double);
|
||||
}
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers)) {
|
||||
if (
|
||||
root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) &&
|
||||
root.gameMode.getSupportsWires()
|
||||
) {
|
||||
variants.push(enumPainterVariants.quad);
|
||||
}
|
||||
return variants;
|
||||
|
||||
@@ -110,6 +110,6 @@ export class MetaReaderBuilding extends MetaBuilding {
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(new BeltReaderComponent());
|
||||
entity.addComponent(new BeltReaderComponent({}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ export class MetaRotaterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant: {
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater);
|
||||
|
||||
@@ -28,6 +28,9 @@ export class MetaStackerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.stacker);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
|
||||
@@ -72,13 +72,21 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
|
||||
globalConfig.undergroundBeltMaxTilesByTier[enumUndergroundBeltVariantToTier[variant]];
|
||||
|
||||
const beltSpeed = root.hubGoals.getUndergroundBeltBaseSpeed();
|
||||
return [
|
||||
|
||||
/** @type {Array<[string, string]>} */
|
||||
const stats = [
|
||||
[
|
||||
T.ingame.buildingPlacement.infoTexts.range,
|
||||
T.ingame.buildingPlacement.infoTexts.tiles.replace("<x>", "" + rangeTiles),
|
||||
],
|
||||
[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)],
|
||||
];
|
||||
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return stats;
|
||||
}
|
||||
stats.push([T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -392,13 +392,20 @@ export class Camera extends BasicSerializableObject {
|
||||
return rect.containsPoint(point.x, point.y);
|
||||
}
|
||||
|
||||
getMaximumZoom() {
|
||||
return this.root.gameMode.getMaximumZoom();
|
||||
}
|
||||
|
||||
getMinimumZoom() {
|
||||
return this.root.gameMode.getMinimumZoom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if we can further zoom in
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canZoomIn() {
|
||||
const maxLevel = this.root.app.platformWrapper.getMaximumZoom();
|
||||
return this.zoomLevel <= maxLevel - 0.01;
|
||||
return this.zoomLevel <= this.getMaximumZoom() - 0.01;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,8 +413,7 @@ export class Camera extends BasicSerializableObject {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canZoomOut() {
|
||||
const minLevel = this.root.app.platformWrapper.getMinimumZoom();
|
||||
return this.zoomLevel >= minLevel + 0.01;
|
||||
return this.zoomLevel >= this.getMinimumZoom() + 0.01;
|
||||
}
|
||||
|
||||
// EVENTS
|
||||
@@ -468,6 +474,7 @@ export class Camera extends BasicSerializableObject {
|
||||
|
||||
// Clamp everything afterwards
|
||||
this.clampZoomLevel();
|
||||
this.clampToBounds();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -743,17 +750,29 @@ export class Camera extends BasicSerializableObject {
|
||||
if (G_IS_DEV && globalConfig.debug.disableZoomLimits) {
|
||||
return;
|
||||
}
|
||||
const wrapper = this.root.app.platformWrapper;
|
||||
|
||||
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel);
|
||||
this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom());
|
||||
this.zoomLevel = clamp(this.zoomLevel, this.getMinimumZoom(), this.getMaximumZoom());
|
||||
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel);
|
||||
|
||||
if (this.desiredZoom) {
|
||||
this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom());
|
||||
this.desiredZoom = clamp(this.desiredZoom, this.getMinimumZoom(), this.getMaximumZoom());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps the center within set boundaries
|
||||
*/
|
||||
clampToBounds() {
|
||||
const bounds = this.root.gameMode.getCameraBounds();
|
||||
if (!bounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tileScaleBounds = this.root.gameMode.getCameraBounds().allScaled(globalConfig.tileSize);
|
||||
this.center.x = clamp(this.center.x, tileScaleBounds.x, tileScaleBounds.x + tileScaleBounds.w);
|
||||
this.center.y = clamp(this.center.y, tileScaleBounds.y, tileScaleBounds.y + tileScaleBounds.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the camera
|
||||
* @param {number} dt Delta time in milliseconds
|
||||
@@ -857,6 +876,7 @@ export class Camera extends BasicSerializableObject {
|
||||
// Panning
|
||||
this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06);
|
||||
this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel));
|
||||
this.clampToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,6 +941,8 @@ export class Camera extends BasicSerializableObject {
|
||||
((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed()
|
||||
)
|
||||
);
|
||||
|
||||
this.clampToBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1006,6 +1028,8 @@ export class Camera extends BasicSerializableObject {
|
||||
|
||||
this.center.x += moveAmount * forceX * movementSpeed;
|
||||
this.center.y += moveAmount * forceY * movementSpeed;
|
||||
|
||||
this.clampToBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ export class Component extends BasicSerializableObject {
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent) {}
|
||||
|
||||
/**
|
||||
* Clears all items and state
|
||||
*/
|
||||
clear() {}
|
||||
|
||||
/* dev:start */
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
import { FilterComponent } from "./components/filter";
|
||||
import { ItemProducerComponent } from "./components/item_producer";
|
||||
import { GoalAcceptorComponent } from "./components/goal_acceptor";
|
||||
|
||||
export function initComponentRegistry() {
|
||||
gComponentRegistry.register(StaticMapEntityComponent);
|
||||
@@ -41,6 +42,7 @@ export function initComponentRegistry() {
|
||||
gComponentRegistry.register(BeltReaderComponent);
|
||||
gComponentRegistry.register(FilterComponent);
|
||||
gComponentRegistry.register(ItemProducerComponent);
|
||||
gComponentRegistry.register(GoalAcceptorComponent);
|
||||
|
||||
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ export class BeltComponent extends Component {
|
||||
this.assignedPath = null;
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.assignedPath) {
|
||||
this.assignedPath.clearAllItems();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective length of this belt in tile space
|
||||
* @returns {number}
|
||||
|
||||
@@ -3,6 +3,12 @@ import { BaseItem } from "../base_item";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
import { types } from "../../savegame/serialization";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumBeltReaderType = {
|
||||
wired: "wired",
|
||||
wireless: "wireless",
|
||||
};
|
||||
|
||||
export class BeltReaderComponent extends Component {
|
||||
static getId() {
|
||||
return "BeltReader";
|
||||
@@ -10,13 +16,24 @@ export class BeltReaderComponent extends Component {
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
type: types.string,
|
||||
lastItem: types.nullable(typeItemSingleton),
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {string=} param0.type
|
||||
*/
|
||||
constructor({ type = enumBeltReaderType.wired }) {
|
||||
super();
|
||||
|
||||
this.type = type;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Which items went through the reader, we only store the time
|
||||
* @type {Array<number>}
|
||||
@@ -41,4 +58,8 @@ export class BeltReaderComponent extends Component {
|
||||
*/
|
||||
this.lastThroughputComputation = 0;
|
||||
}
|
||||
|
||||
isWireless() {
|
||||
return this.type === enumBeltReaderType.wireless;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@ import { Component } from "../component";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumConstantSignalType = {
|
||||
wired: "wired",
|
||||
wireless: "wireless",
|
||||
};
|
||||
|
||||
export class ConstantSignalComponent extends Component {
|
||||
static getId() {
|
||||
return "ConstantSignal";
|
||||
@@ -11,6 +17,7 @@ export class ConstantSignalComponent extends Component {
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
type: types.string,
|
||||
signal: types.nullable(typeItemSingleton),
|
||||
};
|
||||
}
|
||||
@@ -21,15 +28,22 @@ export class ConstantSignalComponent extends Component {
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent) {
|
||||
otherComponent.signal = this.signal;
|
||||
otherComponent.type = this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} param0
|
||||
* @param {string=} param0.type
|
||||
* @param {BaseItem=} param0.signal The signal to store
|
||||
*/
|
||||
constructor({ signal = null }) {
|
||||
constructor({ signal = null, type = enumConstantSignalType.wired }) {
|
||||
super();
|
||||
this.signal = signal;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
isWireless() {
|
||||
return this.type === enumConstantSignalType.wireless;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ export class FilterComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Items in queue to leave through
|
||||
* @type {Array<PendingFilterItem>}
|
||||
|
||||
49
src/js/game/components/goal_acceptor.js
Normal file
49
src/js/game/components/goal_acceptor.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
|
||||
export class GoalAcceptorComponent extends Component {
|
||||
static getId() {
|
||||
return "GoalAcceptor";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
item: typeItemSingleton,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {BaseItem=} param0.item
|
||||
* @param {number=} param0.rate
|
||||
*/
|
||||
constructor({ item = null, rate = null }) {
|
||||
super();
|
||||
|
||||
// ths item to produce
|
||||
/** @type {BaseItem | undefined} */
|
||||
this.item = item;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
// the last items we delivered
|
||||
/** @type {{ item: BaseItem; time: number; }[]} */
|
||||
this.deliveryHistory = [];
|
||||
|
||||
// Used for animations
|
||||
this.displayPercentage = 0;
|
||||
}
|
||||
|
||||
getRequiredDeliveryHistorySize() {
|
||||
return (
|
||||
(globalConfig.puzzleModeSpeed *
|
||||
globalConfig.goalAcceptorMinimumDurationSeconds *
|
||||
globalConfig.beltSpeedItemsPerSecond) /
|
||||
globalConfig.goalAcceptorsPerProducer
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,11 @@ export class ItemAcceptorComponent extends Component {
|
||||
constructor({ slots = [] }) {
|
||||
super();
|
||||
|
||||
this.setSlots(slots);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Fixes belt animations
|
||||
* @type {Array<{
|
||||
@@ -46,8 +51,6 @@ export class ItemAcceptorComponent extends Component {
|
||||
* }>}
|
||||
*/
|
||||
this.itemConsumptionAnimations = [];
|
||||
|
||||
this.setSlots(slots);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +74,8 @@ export class ItemAcceptorComponent extends Component {
|
||||
|
||||
/**
|
||||
* Returns if this acceptor can accept a new item at slot N
|
||||
*
|
||||
* NOTICE: The belt path ignores this for performance reasons and does his own check
|
||||
* @param {number} slotIndex
|
||||
* @param {BaseItem=} item
|
||||
*/
|
||||
|
||||
@@ -48,6 +48,13 @@ export class ItemEjectorComponent extends Component {
|
||||
this.renderFloatingItems = renderFloatingItems;
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const slot of this.slots) {
|
||||
slot.item = null;
|
||||
slot.progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@ export const enumItemProcessorTypes = {
|
||||
hub: "hub",
|
||||
filter: "filter",
|
||||
reader: "reader",
|
||||
goal: "goal",
|
||||
};
|
||||
|
||||
/** @enum {string} */
|
||||
@@ -63,10 +64,8 @@ export class ItemProcessorComponent extends Component {
|
||||
}) {
|
||||
super();
|
||||
|
||||
// Which slot to emit next, this is only a preference and if it can't emit
|
||||
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
|
||||
// sure the outputs always match
|
||||
this.nextOutputSlot = 0;
|
||||
// How many inputs we need for one charge
|
||||
this.inputsPerCharge = inputsPerCharge;
|
||||
|
||||
// Type of the processor
|
||||
this.type = processorType;
|
||||
@@ -74,8 +73,14 @@ export class ItemProcessorComponent extends Component {
|
||||
// Type of processing requirement
|
||||
this.processingRequirement = processingRequirement;
|
||||
|
||||
// How many inputs we need for one charge
|
||||
this.inputsPerCharge = inputsPerCharge;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
// Which slot to emit next, this is only a preference and if it can't emit
|
||||
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
|
||||
// sure the outputs always match
|
||||
this.nextOutputSlot = 0;
|
||||
|
||||
/**
|
||||
* Our current inputs
|
||||
@@ -104,7 +109,11 @@ export class ItemProcessorComponent extends Component {
|
||||
* @param {number} sourceSlot
|
||||
*/
|
||||
tryTakeItem(item, sourceSlot) {
|
||||
if (this.type === enumItemProcessorTypes.hub || this.type === enumItemProcessorTypes.trash) {
|
||||
if (
|
||||
this.type === enumItemProcessorTypes.hub ||
|
||||
this.type === enumItemProcessorTypes.trash ||
|
||||
this.type === enumItemProcessorTypes.goal
|
||||
) {
|
||||
// Hub has special logic .. not really nice but efficient.
|
||||
this.inputSlots.push({ item, sourceSlot });
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { Component } from "../component";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumItemProducerType = {
|
||||
wired: "wired",
|
||||
wireless: "wireless",
|
||||
};
|
||||
|
||||
export class ItemProducerComponent extends Component {
|
||||
static getId() {
|
||||
return "ItemProducer";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
type: types.string,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {string=} param0.type
|
||||
*/
|
||||
constructor({ type = enumItemProducerType.wired }) {
|
||||
super();
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
isWireless() {
|
||||
return this.type === enumItemProducerType.wireless;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,6 @@ export class MinerComponent extends Component {
|
||||
this.lastMiningTime = 0;
|
||||
this.chainable = chainable;
|
||||
|
||||
/**
|
||||
* Stores items from other miners which were chained to this
|
||||
* miner.
|
||||
* @type {Array<BaseItem>}
|
||||
*/
|
||||
this.itemChainBuffer = [];
|
||||
|
||||
/**
|
||||
* @type {BaseItem}
|
||||
*/
|
||||
@@ -42,6 +35,17 @@ export class MinerComponent extends Component {
|
||||
* @type {Entity|null|false}
|
||||
*/
|
||||
this.cachedChainedMiner = null;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Stores items from other miners which were chained to this
|
||||
* miner.
|
||||
* @type {Array<BaseItem>}
|
||||
*/
|
||||
this.itemChainBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,6 +71,14 @@ export class StaticMapEntityComponent extends Component {
|
||||
return getBuildingDataFromCode(this.code).variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the buildings rotation variant
|
||||
* @returns {number}
|
||||
*/
|
||||
getRotationVariant() {
|
||||
return getBuildingDataFromCode(this.code).rotationVariant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the current state to another component
|
||||
* @param {Component} otherComponent
|
||||
|
||||
@@ -41,6 +41,17 @@ export class UndergroundBeltComponent extends Component {
|
||||
this.mode = mode;
|
||||
this.tier = tier;
|
||||
|
||||
/**
|
||||
* The linked entity, used to speed up performance. This contains either
|
||||
* the entrance or exit depending on the tunnel type
|
||||
* @type {LinkedUndergroundBelt}
|
||||
*/
|
||||
this.cachedLinkedEntity = null;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/** @type {Array<{ item: BaseItem, progress: number }>} */
|
||||
this.consumptionAnimations = [];
|
||||
|
||||
@@ -51,13 +62,6 @@ export class UndergroundBeltComponent extends Component {
|
||||
* @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item]
|
||||
*/
|
||||
this.pendingItems = [];
|
||||
|
||||
/**
|
||||
* The linked entity, used to speed up performance. This contains either
|
||||
* the entrance or exit depending on the tunnel type
|
||||
* @type {LinkedUndergroundBelt}
|
||||
*/
|
||||
this.cachedLinkedEntity = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
|
||||
import { GameLogic } from "./logic";
|
||||
import { MapView } from "./map_view";
|
||||
import { defaultBuildingVariant } from "./meta_building";
|
||||
import { RegularGameMode } from "./modes/regular";
|
||||
import { GameMode } from "./game_mode";
|
||||
import { ProductionAnalytics } from "./production_analytics";
|
||||
import { GameRoot } from "./root";
|
||||
import { ShapeDefinitionManager } from "./shape_definition_manager";
|
||||
@@ -82,7 +82,9 @@ export class GameCore {
|
||||
* @param {import("../states/ingame").InGameState} parentState
|
||||
* @param {Savegame} savegame
|
||||
*/
|
||||
initializeRoot(parentState, savegame) {
|
||||
initializeRoot(parentState, savegame, gameModeId) {
|
||||
logger.log("initializing root");
|
||||
|
||||
// Construct the root element, this is the data representation of the game
|
||||
this.root = new GameRoot(this.app);
|
||||
this.root.gameState = parentState;
|
||||
@@ -100,12 +102,12 @@ export class GameCore {
|
||||
// This isn't nice, but we need it right here
|
||||
root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever);
|
||||
|
||||
// Init game mode
|
||||
root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters);
|
||||
|
||||
// Needs to come first
|
||||
root.dynamicTickrate = new DynamicTickrate(root);
|
||||
|
||||
// Init game mode
|
||||
root.gameMode = new RegularGameMode(root);
|
||||
|
||||
// Init classes
|
||||
root.camera = new Camera(root);
|
||||
root.map = new MapView(root);
|
||||
@@ -157,6 +159,8 @@ export class GameCore {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.log("root initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +172,10 @@ export class GameCore {
|
||||
this.root.gameIsFresh = true;
|
||||
this.root.map.seed = randomInt(0, 100000);
|
||||
|
||||
if (!this.root.gameMode.hasHub()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Place the hub
|
||||
const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({
|
||||
root: this.root,
|
||||
@@ -447,7 +455,9 @@ export class GameCore {
|
||||
systems.hub.draw(params);
|
||||
|
||||
// Green wires overlay
|
||||
root.hud.parts.wiresOverlay.draw(params);
|
||||
if (root.hud.parts.wiresOverlay) {
|
||||
root.hud.parts.wiresOverlay.draw(params);
|
||||
}
|
||||
|
||||
if (this.root.currentLayer === "wires") {
|
||||
// Static map entities
|
||||
|
||||
@@ -23,10 +23,16 @@ export class DynamicTickrate {
|
||||
|
||||
this.averageFps = 60;
|
||||
|
||||
this.setTickRate(this.root.app.settings.getDesiredFps());
|
||||
const fixedRate = this.root.gameMode.getFixedTickrate();
|
||||
if (fixedRate) {
|
||||
logger.log("Setting fixed tickrate of", fixedRate);
|
||||
this.setTickRate(fixedRate);
|
||||
} else {
|
||||
this.setTickRate(this.root.app.settings.getDesiredFps());
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
this.setTickRate(300);
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
this.setTickRate(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +105,7 @@ export class DynamicTickrate {
|
||||
|
||||
this.averageTickDuration = average;
|
||||
|
||||
const desiredFps = this.root.app.settings.getDesiredFps();
|
||||
|
||||
// Disabled for now: Dynamicall adjusting tick rate
|
||||
// Disabled for now: Dynamically adjusting tick rate
|
||||
// if (this.averageFps > desiredFps * 0.9) {
|
||||
// // if (average < maxTickDuration) {
|
||||
// this.increaseTickRate();
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
import { FilterComponent } from "./components/filter";
|
||||
import { ItemProducerComponent } from "./components/item_producer";
|
||||
import { GoalAcceptorComponent } from "./components/goal_acceptor";
|
||||
/* typehints:end */
|
||||
|
||||
/**
|
||||
@@ -89,6 +90,9 @@ export class EntityComponentStorage {
|
||||
/** @type {ItemProducerComponent} */
|
||||
this.ItemProducer;
|
||||
|
||||
/** @type {GoalAcceptorComponent} */
|
||||
this.GoalAcceptor;
|
||||
|
||||
/* typehints:end */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,192 @@
|
||||
/* typehints:start */
|
||||
import { enumHubGoalRewards } from "./tutorial_goals";
|
||||
import { GameRoot } from "./root";
|
||||
/* typehints:end */
|
||||
|
||||
import { GameRoot } from "./root";
|
||||
import { Rectangle } from "../core/rectangle";
|
||||
import { gGameModeRegistry } from "../core/global_registries";
|
||||
import { types, BasicSerializableObject } from "../savegame/serialization";
|
||||
import { MetaBuilding } from "./meta_building";
|
||||
import { MetaItemProducerBuilding } from "./buildings/item_producer";
|
||||
import { BaseHUDPart } from "./hud/base_hud_part";
|
||||
|
||||
/** @typedef {{
|
||||
* shape: string,
|
||||
* amount: number
|
||||
* }} UpgradeRequirement */
|
||||
/** @enum {string} */
|
||||
export const enumGameModeIds = {
|
||||
puzzleEdit: "puzzleEditMode",
|
||||
puzzlePlay: "puzzlePlayMode",
|
||||
regular: "regularMode",
|
||||
};
|
||||
|
||||
/** @typedef {{
|
||||
* required: Array<UpgradeRequirement>
|
||||
* improvement?: number,
|
||||
* excludePrevious?: boolean
|
||||
* }} TierRequirement */
|
||||
/** @enum {string} */
|
||||
export const enumGameModeTypes = {
|
||||
default: "defaultModeType",
|
||||
puzzle: "puzzleModeType",
|
||||
};
|
||||
|
||||
/** @typedef {Array<TierRequirement>} UpgradeTiers */
|
||||
export class GameMode extends BasicSerializableObject {
|
||||
/** @returns {string} */
|
||||
static getId() {
|
||||
abstract;
|
||||
return "unknownMode";
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
static getType() {
|
||||
abstract;
|
||||
return "unknownType";
|
||||
}
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
* @param {string} [id=Regular]
|
||||
* @param {object|undefined} payload
|
||||
*/
|
||||
static create(root, id = enumGameModeIds.regular, payload = undefined) {
|
||||
return new (gGameModeRegistry.findById(id))(root, payload);
|
||||
}
|
||||
|
||||
/** @typedef {{
|
||||
* shape: string,
|
||||
* required: number,
|
||||
* reward: enumHubGoalRewards,
|
||||
* throughputOnly?: boolean
|
||||
* }} LevelDefinition */
|
||||
|
||||
export class GameMode {
|
||||
/**
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
this.root = root;
|
||||
|
||||
/**
|
||||
* @type {Record<string, typeof BaseHUDPart>}
|
||||
*/
|
||||
this.additionalHudParts = {};
|
||||
|
||||
/** @type {typeof MetaBuilding[]} */
|
||||
this.hiddenBuildings = [MetaItemProducerBuilding];
|
||||
}
|
||||
|
||||
/** @returns {object} */
|
||||
serialize() {
|
||||
return {
|
||||
$: this.getId(),
|
||||
data: super.serialize(),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {object} savedata */
|
||||
deserialize({ data }) {
|
||||
super.deserialize(data, this.root);
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
getId() {
|
||||
// @ts-ignore
|
||||
return this.constructor.getId();
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
getType() {
|
||||
// @ts-ignore
|
||||
return this.constructor.getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return all available upgrades
|
||||
* @returns {Object<string, UpgradeTiers>}
|
||||
*/
|
||||
getUpgrades() {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the blueprint shape key
|
||||
* @returns {string}
|
||||
*/
|
||||
getBlueprintShapeKey() {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the goals for all levels including their reward
|
||||
* @returns {Array<LevelDefinition>}
|
||||
*/
|
||||
getLevelDefinitions() {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return whether free play is available or if the game stops
|
||||
* after the predefined levels
|
||||
* @param {typeof MetaBuilding} building - Class name of building
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIsFreeplayAvailable() {
|
||||
isBuildingExcluded(building) {
|
||||
return this.hiddenBuildings.indexOf(building) >= 0;
|
||||
}
|
||||
|
||||
/** @returns {undefined|Rectangle[]} */
|
||||
getBuildableZones() {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {Rectangle|undefined} */
|
||||
getCameraBounds() {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasHub() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasResources() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
getMinimumZoom() {
|
||||
return 0.1;
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
getMaximumZoom() {
|
||||
return 3.5;
|
||||
}
|
||||
|
||||
/** @returns {Object<string, Array>} */
|
||||
getUpgrades() {
|
||||
return {
|
||||
belt: [],
|
||||
miner: [],
|
||||
processors: [],
|
||||
painting: [],
|
||||
};
|
||||
}
|
||||
|
||||
throughputDoesNotMatter() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
*/
|
||||
adjustZone(w = 0, h = 0) {
|
||||
abstract;
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {array} */
|
||||
getLevelDefinitions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsFreeplayAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsSaveable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getSupportsCopyPaste() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getSupportsWires() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsDeterministic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns {number | undefined} */
|
||||
getFixedTickrate() {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
getBlueprintShapeKey() {
|
||||
return "CbCbCbRb:CwCwCwCw";
|
||||
}
|
||||
}
|
||||
|
||||
10
src/js/game/game_mode_registry.js
Normal file
10
src/js/game/game_mode_registry.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { gGameModeRegistry } from "../core/global_registries";
|
||||
import { PuzzleEditGameMode } from "./modes/puzzle_edit";
|
||||
import { PuzzlePlayGameMode } from "./modes/puzzle_play";
|
||||
import { RegularGameMode } from "./modes/regular";
|
||||
|
||||
export function initGameModeRegistry() {
|
||||
gGameModeRegistry.register(PuzzleEditGameMode);
|
||||
gGameModeRegistry.register(PuzzlePlayGameMode);
|
||||
gGameModeRegistry.register(RegularGameMode);
|
||||
}
|
||||
@@ -24,6 +24,9 @@ import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays";
|
||||
import { BeltReaderSystem } from "./systems/belt_reader";
|
||||
import { FilterSystem } from "./systems/filter";
|
||||
import { ItemProducerSystem } from "./systems/item_producer";
|
||||
import { ConstantProducerSystem } from "./systems/constant_producer";
|
||||
import { GoalAcceptorSystem } from "./systems/goal_acceptor";
|
||||
import { ZoneSystem } from "./systems/zone";
|
||||
|
||||
const logger = createLogger("game_system_manager");
|
||||
|
||||
@@ -100,6 +103,15 @@ export class GameSystemManager {
|
||||
/** @type {ItemProducerSystem} */
|
||||
itemProducer: null,
|
||||
|
||||
/** @type {ConstantProducerSystem} */
|
||||
ConstantProducer: null,
|
||||
|
||||
/** @type {GoalAcceptorSystem} */
|
||||
GoalAcceptor: null,
|
||||
|
||||
/** @type {ZoneSystem} */
|
||||
zone: null,
|
||||
|
||||
/* typehints:end */
|
||||
};
|
||||
this.systemUpdateOrder = [];
|
||||
@@ -138,7 +150,9 @@ export class GameSystemManager {
|
||||
|
||||
add("itemEjector", ItemEjectorSystem);
|
||||
|
||||
add("mapResources", MapResourcesSystem);
|
||||
if (this.root.gameMode.hasResources()) {
|
||||
add("mapResources", MapResourcesSystem);
|
||||
}
|
||||
|
||||
add("hub", HubSystem);
|
||||
|
||||
@@ -165,6 +179,14 @@ export class GameSystemManager {
|
||||
|
||||
add("itemProcessorOverlays", ItemProcessorOverlaysSystem);
|
||||
|
||||
add("constantProducer", ConstantProducerSystem);
|
||||
|
||||
add("goalAcceptor", GoalAcceptorSystem);
|
||||
|
||||
if (this.root.gameMode.getBuildableZones()) {
|
||||
add("zone", ZoneSystem);
|
||||
}
|
||||
|
||||
logger.log("📦 There are", this.systemUpdateOrder.length, "game systems");
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export class HubGoals extends BasicSerializableObject {
|
||||
// Allow quickly switching goals in dev mode
|
||||
if (G_IS_DEV) {
|
||||
window.addEventListener("keydown", ev => {
|
||||
if (ev.key === "b") {
|
||||
if (ev.key === "p") {
|
||||
// root is not guaranteed to exist within ~0.5s after loading in
|
||||
if (this.root && this.root.app && this.root.app.gameAnalytics) {
|
||||
if (!this.isEndOfDemoReached()) {
|
||||
@@ -195,6 +195,10 @@ export class HubGoals extends BasicSerializableObject {
|
||||
if (G_IS_DEV && globalConfig.debug.allBuildingsUnlocked) {
|
||||
return true;
|
||||
}
|
||||
if (this.root.gameMode.getLevelDefinitions().length < 1) {
|
||||
// no story, so always unlocked
|
||||
return true;
|
||||
}
|
||||
return !!this.gainedRewards[reward];
|
||||
}
|
||||
|
||||
@@ -472,6 +476,9 @@ export class HubGoals extends BasicSerializableObject {
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getBeltBaseSpeed() {
|
||||
if (this.root.gameMode.throughputDoesNotMatter()) {
|
||||
return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed;
|
||||
}
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
}
|
||||
|
||||
@@ -480,6 +487,9 @@ export class HubGoals extends BasicSerializableObject {
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getUndergroundBeltBaseSpeed() {
|
||||
if (this.root.gameMode.throughputDoesNotMatter()) {
|
||||
return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed;
|
||||
}
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
}
|
||||
|
||||
@@ -488,6 +498,9 @@ export class HubGoals extends BasicSerializableObject {
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getMinerBaseSpeed() {
|
||||
if (this.root.gameMode.throughputDoesNotMatter()) {
|
||||
return globalConfig.minerSpeedItemsPerSecond * globalConfig.puzzleModeSpeed;
|
||||
}
|
||||
return globalConfig.minerSpeedItemsPerSecond * this.upgradeImprovements.miner;
|
||||
}
|
||||
|
||||
@@ -497,9 +510,14 @@ export class HubGoals extends BasicSerializableObject {
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getProcessorBaseSpeed(processorType) {
|
||||
if (this.root.gameMode.throughputDoesNotMatter()) {
|
||||
return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed * 10;
|
||||
}
|
||||
|
||||
switch (processorType) {
|
||||
case enumItemProcessorTypes.trash:
|
||||
case enumItemProcessorTypes.hub:
|
||||
case enumItemProcessorTypes.goal:
|
||||
return 1e30;
|
||||
case enumItemProcessorTypes.balancer:
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;
|
||||
|
||||
@@ -1,54 +1,22 @@
|
||||
/* typehints:start */
|
||||
import { GameRoot } from "../root";
|
||||
/* typehints:end */
|
||||
|
||||
/* dev:start */
|
||||
import { TrailerMaker } from "./trailer_maker";
|
||||
/* dev:end */
|
||||
|
||||
import { Signal } from "../../core/signal";
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { DrawParameters } from "../../core/draw_parameters";
|
||||
import { Signal } from "../../core/signal";
|
||||
import { KEYMAPPINGS } from "../key_action_mapper";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { ShapeDefinition } from "../shape_definition";
|
||||
import { HUDBetaOverlay } from "./parts/beta_overlay";
|
||||
import { HUDBuildingsToolbar } from "./parts/buildings_toolbar";
|
||||
import { HUDBuildingPlacer } from "./parts/building_placer";
|
||||
import { HUDBlueprintPlacer } from "./parts/blueprint_placer";
|
||||
import { HUDKeybindingOverlay } from "./parts/keybinding_overlay";
|
||||
import { HUDUnlockNotification } from "./parts/unlock_notification";
|
||||
import { HUDGameMenu } from "./parts/game_menu";
|
||||
import { HUDShop } from "./parts/shop";
|
||||
import { IS_MOBILE, globalConfig } from "../../core/config";
|
||||
import { HUDMassSelector } from "./parts/mass_selector";
|
||||
import { HUDVignetteOverlay } from "./parts/vignette_overlay";
|
||||
import { HUDStatistics } from "./parts/statistics";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { HUDPinnedShapes } from "./parts/pinned_shapes";
|
||||
import { ShapeDefinition } from "../shape_definition";
|
||||
import { HUDNotifications, enumNotificationType } from "./parts/notifications";
|
||||
import { HUDSettingsMenu } from "./parts/settings_menu";
|
||||
import { HUDColorBlindHelper } from "./parts/color_blind_helper";
|
||||
import { HUDChangesDebugger } from "./parts/debug_changes";
|
||||
import { HUDDebugInfo } from "./parts/debug_info";
|
||||
import { HUDEntityDebugger } from "./parts/entity_debugger";
|
||||
import { KEYMAPPINGS } from "../key_action_mapper";
|
||||
import { HUDWatermark } from "./parts/watermark";
|
||||
import { HUDModalDialogs } from "./parts/modal_dialogs";
|
||||
import { HUDPartTutorialHints } from "./parts/tutorial_hints";
|
||||
import { HUDWaypoints } from "./parts/waypoints";
|
||||
import { HUDInteractiveTutorial } from "./parts/interactive_tutorial";
|
||||
import { HUDScreenshotExporter } from "./parts/screenshot_exporter";
|
||||
import { HUDColorBlindHelper } from "./parts/color_blind_helper";
|
||||
import { HUDShapeViewer } from "./parts/shape_viewer";
|
||||
import { HUDWiresOverlay } from "./parts/wires_overlay";
|
||||
import { HUDChangesDebugger } from "./parts/debug_changes";
|
||||
import { queryParamOptions } from "../../core/query_parameters";
|
||||
import { HUDSandboxController } from "./parts/sandbox_controller";
|
||||
import { HUDWiresToolbar } from "./parts/wires_toolbar";
|
||||
import { HUDWireInfo } from "./parts/wire_info";
|
||||
import { HUDLeverToggle } from "./parts/lever_toggle";
|
||||
import { HUDLayerPreview } from "./parts/layer_preview";
|
||||
import { HUDMinerHighlight } from "./parts/miner_highlight";
|
||||
import { HUDBetaOverlay } from "./parts/beta_overlay";
|
||||
import { HUDStandaloneAdvantages } from "./parts/standalone_advantages";
|
||||
import { HUDCatMemes } from "./parts/cat_memes";
|
||||
import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer";
|
||||
import { HUDConstantSignalEdit } from "./parts/constant_signal_edit";
|
||||
import { enumNotificationType } from "./parts/notifications";
|
||||
import { HUDSettingsMenu } from "./parts/settings_menu";
|
||||
import { HUDVignetteOverlay } from "./parts/vignette_overlay";
|
||||
import { TrailerMaker } from "./trailer_maker";
|
||||
|
||||
export class GameHUD {
|
||||
/**
|
||||
@@ -76,33 +44,12 @@ export class GameHUD {
|
||||
|
||||
this.parts = {
|
||||
buildingsToolbar: new HUDBuildingsToolbar(this.root),
|
||||
wiresToolbar: new HUDWiresToolbar(this.root),
|
||||
blueprintPlacer: new HUDBlueprintPlacer(this.root),
|
||||
buildingPlacer: new HUDBuildingPlacer(this.root),
|
||||
unlockNotification: new HUDUnlockNotification(this.root),
|
||||
gameMenu: new HUDGameMenu(this.root),
|
||||
massSelector: new HUDMassSelector(this.root),
|
||||
shop: new HUDShop(this.root),
|
||||
statistics: new HUDStatistics(this.root),
|
||||
waypoints: new HUDWaypoints(this.root),
|
||||
wireInfo: new HUDWireInfo(this.root),
|
||||
leverToggle: new HUDLeverToggle(this.root),
|
||||
constantSignalEdit: new HUDConstantSignalEdit(this.root),
|
||||
|
||||
// Must always exist
|
||||
pinnedShapes: new HUDPinnedShapes(this.root),
|
||||
notifications: new HUDNotifications(this.root),
|
||||
settingsMenu: new HUDSettingsMenu(this.root),
|
||||
debugInfo: new HUDDebugInfo(this.root),
|
||||
dialogs: new HUDModalDialogs(this.root),
|
||||
screenshotExporter: new HUDScreenshotExporter(this.root),
|
||||
shapeViewer: new HUDShapeViewer(this.root),
|
||||
|
||||
wiresOverlay: new HUDWiresOverlay(this.root),
|
||||
layerPreview: new HUDLayerPreview(this.root),
|
||||
|
||||
minerHighlight: new HUDMinerHighlight(this.root),
|
||||
tutorialVideoOffer: new HUDTutorialVideoOffer(this.root),
|
||||
|
||||
// Typing hints
|
||||
/* typehints:start */
|
||||
@@ -111,29 +58,14 @@ export class GameHUD {
|
||||
/* typehints:end */
|
||||
};
|
||||
|
||||
if (!IS_MOBILE) {
|
||||
this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root);
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.enableEntityInspector) {
|
||||
this.parts.entityDebugger = new HUDEntityDebugger(this.root);
|
||||
}
|
||||
|
||||
if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) {
|
||||
this.parts.watermark = new HUDWatermark(this.root);
|
||||
this.parts.standaloneAdvantages = new HUDStandaloneAdvantages(this.root);
|
||||
this.parts.catMemes = new HUDCatMemes(this.root);
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
this.parts.changesDebugger = new HUDChangesDebugger(this.root);
|
||||
}
|
||||
|
||||
if (this.root.app.settings.getAllSettings().offerHints) {
|
||||
this.parts.tutorialHints = new HUDPartTutorialHints(this.root);
|
||||
this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root);
|
||||
}
|
||||
|
||||
if (this.root.app.settings.getAllSettings().vignette) {
|
||||
this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root);
|
||||
}
|
||||
@@ -142,14 +74,15 @@ export class GameHUD {
|
||||
this.parts.colorBlindHelper = new HUDColorBlindHelper(this.root);
|
||||
}
|
||||
|
||||
if (queryParamOptions.sandboxMode || G_IS_DEV) {
|
||||
this.parts.sandboxController = new HUDSandboxController(this.root);
|
||||
}
|
||||
|
||||
if (!G_IS_RELEASE && !G_IS_DEV) {
|
||||
this.parts.betaOverlay = new HUDBetaOverlay(this.root);
|
||||
}
|
||||
|
||||
const additionalParts = this.root.gameMode.additionalHudParts;
|
||||
for (const [partId, part] of Object.entries(additionalParts)) {
|
||||
this.parts[partId] = new part(this.root);
|
||||
}
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const key in this.parts) {
|
||||
this.parts[key].createElements(frag);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { gMetaBuildingRegistry } from "../../../core/global_registries";
|
||||
import { STOP_PROPAGATION } from "../../../core/signal";
|
||||
import { makeDiv, safeModulo } from "../../../core/utils";
|
||||
import { MetaBlockBuilding } from "../../buildings/block";
|
||||
import { MetaConstantProducerBuilding } from "../../buildings/constant_producer";
|
||||
import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor";
|
||||
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { MetaBuilding } from "../../meta_building";
|
||||
import { GameRoot } from "../../root";
|
||||
@@ -23,8 +27,8 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
) {
|
||||
super(root);
|
||||
|
||||
this.primaryBuildings = primaryBuildings;
|
||||
this.secondaryBuildings = secondaryBuildings;
|
||||
this.primaryBuildings = this.filterBuildings(primaryBuildings);
|
||||
this.secondaryBuildings = this.filterBuildings(secondaryBuildings);
|
||||
this.visibilityCondition = visibilityCondition;
|
||||
this.htmlElementId = htmlElementId;
|
||||
this.layer = layer;
|
||||
@@ -35,6 +39,7 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
* selected: boolean,
|
||||
* element: HTMLElement,
|
||||
* index: number
|
||||
* puzzleLocked: boolean;
|
||||
* }>} */
|
||||
this.buildingHandles = {};
|
||||
}
|
||||
@@ -47,6 +52,24 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
this.element = makeDiv(parent, this.htmlElementId, ["ingame_buildingsToolbar"], "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<typeof MetaBuilding>} buildings
|
||||
* @returns {Array<typeof MetaBuilding>}
|
||||
*/
|
||||
filterBuildings(buildings) {
|
||||
const filtered = [];
|
||||
|
||||
for (let i = 0; i < buildings.length; i++) {
|
||||
if (this.root.gameMode.isBuildingExcluded(buildings[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filtered.push(buildings[i]);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all buildings
|
||||
* @returns {Array<typeof MetaBuilding>}
|
||||
@@ -87,19 +110,31 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
);
|
||||
itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png");
|
||||
itemContainer.setAttribute("data-id", metaBuilding.getId());
|
||||
|
||||
binding.add(() => this.selectBuildingForPlacement(metaBuilding));
|
||||
|
||||
this.trackClicks(itemContainer, () => this.selectBuildingForPlacement(metaBuilding), {
|
||||
const icon = makeDiv(itemContainer, null, ["icon"]);
|
||||
|
||||
this.trackClicks(icon, () => this.selectBuildingForPlacement(metaBuilding), {
|
||||
clickSound: null,
|
||||
});
|
||||
|
||||
//lock icon for puzzle editor
|
||||
if (this.root.gameMode.getIsEditor() && !this.inRequiredBuildings(metaBuilding)) {
|
||||
const puzzleLock = makeDiv(itemContainer, null, ["puzzle-lock"]);
|
||||
|
||||
itemContainer.classList.toggle("editor", true);
|
||||
this.trackClicks(puzzleLock, () => this.toggleBuildingLock(metaBuilding), {
|
||||
clickSound: null,
|
||||
});
|
||||
}
|
||||
|
||||
this.buildingHandles[metaBuilding.id] = {
|
||||
metaBuilding,
|
||||
metaBuilding: metaBuilding,
|
||||
element: itemContainer,
|
||||
unlocked: false,
|
||||
selected: false,
|
||||
index: i,
|
||||
puzzleLocked: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,7 +162,7 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
let recomputeSecondaryToolbarVisibility = false;
|
||||
for (const buildingId in this.buildingHandles) {
|
||||
const handle = this.buildingHandles[buildingId];
|
||||
const newStatus = handle.metaBuilding.getIsUnlocked(this.root);
|
||||
const newStatus = !handle.puzzleLocked && handle.metaBuilding.getIsUnlocked(this.root);
|
||||
if (handle.unlocked !== newStatus) {
|
||||
handle.unlocked = newStatus;
|
||||
handle.element.classList.toggle("unlocked", newStatus);
|
||||
@@ -216,6 +251,14 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
|
||||
const handle = this.buildingHandles[metaBuilding.getId()];
|
||||
if (handle.puzzleLocked) {
|
||||
handle.puzzleLocked = false;
|
||||
handle.element.classList.toggle("unlocked", false);
|
||||
this.root.soundProxy.playUiClick();
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow clicking an item again to deselect it
|
||||
for (const buildingId in this.buildingHandles) {
|
||||
const handle = this.buildingHandles[buildingId];
|
||||
@@ -229,4 +272,51 @@ export class HUDBaseToolbar extends BaseHUDPart {
|
||||
this.root.hud.signals.buildingSelectedForPlacement.dispatch(metaBuilding);
|
||||
this.onSelectedPlacementBuildingChanged(metaBuilding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MetaBuilding} metaBuilding
|
||||
*/
|
||||
toggleBuildingLock(metaBuilding) {
|
||||
if (!this.visibilityCondition()) {
|
||||
// Not active
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inRequiredBuildings(metaBuilding) || !metaBuilding.getIsUnlocked(this.root)) {
|
||||
this.root.soundProxy.playUiError();
|
||||
return STOP_PROPAGATION;
|
||||
}
|
||||
|
||||
const handle = this.buildingHandles[metaBuilding.getId()];
|
||||
handle.puzzleLocked = !handle.puzzleLocked;
|
||||
handle.element.classList.toggle("unlocked", !handle.puzzleLocked);
|
||||
this.root.soundProxy.playUiClick();
|
||||
|
||||
const entityManager = this.root.entityMgr;
|
||||
for (const entity of entityManager.getAllWithComponent(StaticMapEntityComponent)) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
if (staticComp.getMetaBuilding().id === metaBuilding.id) {
|
||||
this.root.map.removeStaticEntity(entity);
|
||||
entityManager.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
entityManager.processDestroyList();
|
||||
|
||||
const currentMetaBuilding = this.root.hud.parts.buildingPlacer.currentMetaBuilding;
|
||||
if (currentMetaBuilding.get() == metaBuilding) {
|
||||
currentMetaBuilding.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MetaBuilding} metaBuilding
|
||||
*/
|
||||
inRequiredBuildings(metaBuilding) {
|
||||
const requiredBuildings = [
|
||||
gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding),
|
||||
gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding),
|
||||
gMetaBuildingRegistry.findByClass(MetaBlockBuilding),
|
||||
];
|
||||
return requiredBuildings.includes(metaBuilding);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
draw(parameters) {
|
||||
if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
|
||||
if (this.root.camera.getIsMapOverlayActive()) {
|
||||
// Dont allow placing in overview mode
|
||||
this.domAttach.update(false);
|
||||
this.variantsAttach.update(false);
|
||||
@@ -275,11 +275,13 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
||||
const worldPosition = this.root.camera.screenToWorld(mousePosition);
|
||||
|
||||
// Draw peeker
|
||||
this.root.hud.parts.layerPreview.renderPreview(
|
||||
parameters,
|
||||
worldPosition,
|
||||
1 / this.root.camera.zoomLevel
|
||||
);
|
||||
if (this.root.hud.parts.layerPreview) {
|
||||
this.root.hud.parts.layerPreview.renderPreview(
|
||||
parameters,
|
||||
worldPosition,
|
||||
1 / this.root.camera.zoomLevel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -366,7 +366,8 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
if (
|
||||
tileBelow &&
|
||||
this.root.app.settings.getAllSettings().pickMinerOnPatch &&
|
||||
this.root.currentLayer === "regular"
|
||||
this.root.currentLayer === "regular" &&
|
||||
this.root.gameMode.hasResources()
|
||||
) {
|
||||
this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding));
|
||||
|
||||
@@ -390,6 +391,12 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disallow picking excluded buildings
|
||||
if (this.root.gameMode.isBuildingExcluded(extracted.metaClass)) {
|
||||
this.currentMetaBuilding.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the building we are picking is the same as the one we have, clear the cursor.
|
||||
if (
|
||||
this.currentMetaBuilding.get() &&
|
||||
@@ -430,7 +437,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
* @param {Vector} tile
|
||||
*/
|
||||
tryPlaceCurrentBuildingAt(tile) {
|
||||
if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
|
||||
if (this.root.camera.getIsMapOverlayActive()) {
|
||||
// Dont allow placing in overview mode
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,23 +15,28 @@ import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
|
||||
import { HUDBaseToolbar } from "./base_toolbar";
|
||||
import { MetaStorageBuilding } from "../../buildings/storage";
|
||||
import { MetaItemProducerBuilding } from "../../buildings/item_producer";
|
||||
import { queryParamOptions } from "../../../core/query_parameters";
|
||||
import { MetaConstantProducerBuilding } from "../../buildings/constant_producer";
|
||||
import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor";
|
||||
import { MetaBlockBuilding } from "../../buildings/block";
|
||||
|
||||
export class HUDBuildingsToolbar extends HUDBaseToolbar {
|
||||
constructor(root) {
|
||||
super(root, {
|
||||
primaryBuildings: [
|
||||
MetaConstantProducerBuilding,
|
||||
MetaGoalAcceptorBuilding,
|
||||
MetaBeltBuilding,
|
||||
MetaBalancerBuilding,
|
||||
MetaUndergroundBeltBuilding,
|
||||
MetaMinerBuilding,
|
||||
MetaBlockBuilding,
|
||||
MetaCutterBuilding,
|
||||
MetaRotaterBuilding,
|
||||
MetaStackerBuilding,
|
||||
MetaMixerBuilding,
|
||||
MetaPainterBuilding,
|
||||
MetaTrashBuilding,
|
||||
...(queryParamOptions.sandboxMode || G_IS_DEV ? [MetaItemProducerBuilding] : []),
|
||||
MetaItemProducerBuilding,
|
||||
],
|
||||
secondaryBuildings: [
|
||||
MetaStorageBuilding,
|
||||
|
||||
@@ -254,6 +254,13 @@ export class HUDKeybindingOverlay extends BaseHUDPart {
|
||||
condition: () => this.anythingSelectedOnMap,
|
||||
},
|
||||
|
||||
{
|
||||
// [SELECTION] Clear
|
||||
label: T.ingame.keybindingsOverlay.clearBelts,
|
||||
keys: [k.massSelect.massSelectClear],
|
||||
condition: () => this.anythingSelectedOnMap,
|
||||
},
|
||||
|
||||
{
|
||||
// Switch layers
|
||||
label: T.ingame.keybindingsOverlay.switchLayers,
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { Vector } from "../../../core/vector";
|
||||
import { STOP_PROPAGATION } from "../../../core/signal";
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { Entity } from "../../entity";
|
||||
import { Loader } from "../../../core/loader";
|
||||
import { globalConfig } from "../../../core/config";
|
||||
import { makeDiv, formatBigNumber, formatBigNumberFull } from "../../../core/utils";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { STOP_PROPAGATION } from "../../../core/signal";
|
||||
import { formatBigNumberFull } from "../../../core/utils";
|
||||
import { Vector } from "../../../core/vector";
|
||||
import { ACHIEVEMENTS } from "../../../platform/achievement_provider";
|
||||
import { enumMouseButton } from "../../camera";
|
||||
import { T } from "../../../translations";
|
||||
import { Blueprint } from "../../blueprint";
|
||||
import { enumMouseButton } from "../../camera";
|
||||
import { Component } from "../../component";
|
||||
import { Entity } from "../../entity";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { THEME } from "../../theme";
|
||||
import { enumHubGoalRewards } from "../../tutorial_goals";
|
||||
import { Blueprint } from "../../blueprint";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const logger = createLogger("hud/mass_selector");
|
||||
|
||||
@@ -33,12 +32,13 @@ export class HUDMassSelector extends BaseHUDPart {
|
||||
this.root.camera.movePreHandler.add(this.onMouseMove, this);
|
||||
this.root.camera.upPostHandler.add(this.onMouseUp, this);
|
||||
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.onBack, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).addToTop(this.onBack, this);
|
||||
this.root.keyMapper
|
||||
.getBinding(KEYMAPPINGS.massSelect.confirmMassDelete)
|
||||
.add(this.confirmDelete, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCut).add(this.confirmCut, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectCopy).add(this.startCopy, this);
|
||||
this.root.keyMapper.getBinding(KEYMAPPINGS.massSelect.massSelectClear).add(this.clearBelts, this);
|
||||
|
||||
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.clearSelection, this);
|
||||
this.root.signals.editModeChanged.add(this.clearSelection, this);
|
||||
@@ -142,6 +142,16 @@ export class HUDMassSelector extends BaseHUDPart {
|
||||
}
|
||||
}
|
||||
|
||||
clearBelts() {
|
||||
for (const uid of this.selectedUids) {
|
||||
const entity = this.root.entityMgr.findByUid(uid);
|
||||
for (const component of Object.values(entity.components)) {
|
||||
/** @type {Component} */ (component).clear();
|
||||
}
|
||||
}
|
||||
this.selectedUids = new Set();
|
||||
}
|
||||
|
||||
confirmCut() {
|
||||
if (!this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) {
|
||||
this.root.hud.parts.dialogs.showInfo(
|
||||
|
||||
@@ -29,11 +29,14 @@ export class HUDModalDialogs extends BaseHUDPart {
|
||||
}
|
||||
|
||||
shouldPauseRendering() {
|
||||
return this.dialogStack.length > 0;
|
||||
// return this.dialogStack.length > 0;
|
||||
// @todo: Check if change this affects anything
|
||||
return false;
|
||||
}
|
||||
|
||||
shouldPauseGame() {
|
||||
return this.shouldPauseRendering();
|
||||
// @todo: Check if this change affects anything
|
||||
return false;
|
||||
}
|
||||
|
||||
createElements(parent) {
|
||||
@@ -139,8 +142,8 @@ export class HUDModalDialogs extends BaseHUDPart {
|
||||
}
|
||||
|
||||
// Returns method to be called when laoding finishd
|
||||
showLoadingDialog() {
|
||||
const dialog = new DialogLoading(this.app);
|
||||
showLoadingDialog(text = "") {
|
||||
const dialog = new DialogLoading(this.app, text);
|
||||
this.internalShowDialog(dialog);
|
||||
return this.closeDialog.bind(this, dialog);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
*/
|
||||
deserialize(data) {
|
||||
if (!data || !data.shapes || !Array.isArray(data.shapes)) {
|
||||
return "Invalid pinned shapes data";
|
||||
return "Invalid pinned shapes data: " + JSON.stringify(data);
|
||||
}
|
||||
this.pinnedShapes = data.shapes;
|
||||
}
|
||||
|
||||
21
src/js/game/hud/parts/puzzle_back_to_menu.js
Normal file
21
src/js/game/hud/parts/puzzle_back_to_menu.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDPuzzleBackToMenu extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
const key = this.root.gameMode.getId();
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleBackToMenu");
|
||||
this.button = document.createElement("button");
|
||||
this.button.classList.add("button");
|
||||
this.element.appendChild(this.button);
|
||||
|
||||
this.trackClicks(this.button, this.back);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
back() {
|
||||
this.root.gameState.goBackToMenu();
|
||||
}
|
||||
}
|
||||
112
src/js/game/hud/parts/puzzle_complete_notification.js
Normal file
112
src/js/game/hud/parts/puzzle_complete_notification.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* typehints:start */
|
||||
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
|
||||
/* typehints:end */
|
||||
|
||||
import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { SOUNDS } from "../../../platform/sound";
|
||||
import { T } from "../../../translations";
|
||||
import { enumColors } from "../../colors";
|
||||
import { ColorItem } from "../../items/color_item";
|
||||
import { finalGameShape, rocketShape } from "../../modes/regular";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { ShapeItem } from "../../items/shape_item";
|
||||
import { ShapeDefinition } from "../../shape_definition";
|
||||
|
||||
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
||||
initialize() {
|
||||
this.visible = false;
|
||||
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element, {
|
||||
timeToKeepSeconds: 0,
|
||||
});
|
||||
|
||||
this.root.signals.puzzleComplete.add(this.show, this);
|
||||
|
||||
this.userDidLikePuzzle = false;
|
||||
this.timeOfCompletion = 0;
|
||||
}
|
||||
|
||||
createElements(parent) {
|
||||
this.inputReciever = new InputReceiver("puzzle-complete");
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleCompleteNotification", ["noBlur"]);
|
||||
|
||||
const dialog = makeDiv(this.element, null, ["dialog"]);
|
||||
|
||||
this.elemTitle = makeDiv(dialog, null, ["title"], T.ingame.puzzleCompletion.title);
|
||||
this.elemContents = makeDiv(dialog, null, ["contents"]);
|
||||
this.elemActions = makeDiv(dialog, null, ["actions"]);
|
||||
|
||||
const stepLike = makeDiv(this.elemContents, null, ["step", "stepLike"]);
|
||||
makeDiv(stepLike, null, ["title"], T.ingame.puzzleCompletion.titleLike);
|
||||
|
||||
const likeButtons = makeDiv(stepLike, null, ["buttons"]);
|
||||
|
||||
this.buttonLikeYes = document.createElement("button");
|
||||
this.buttonLikeYes.classList.add("liked-yes");
|
||||
likeButtons.appendChild(this.buttonLikeYes);
|
||||
this.trackClicks(this.buttonLikeYes, () => {
|
||||
this.userDidLikePuzzle = !this.userDidLikePuzzle;
|
||||
this.updateState();
|
||||
});
|
||||
|
||||
const buttonBar = document.createElement("div");
|
||||
buttonBar.classList.add("buttonBar");
|
||||
this.elemContents.appendChild(buttonBar);
|
||||
|
||||
this.continueBtn = document.createElement("button");
|
||||
this.continueBtn.classList.add("continue", "styledButton");
|
||||
this.continueBtn.innerText = T.ingame.puzzleCompletion.continueBtn;
|
||||
buttonBar.appendChild(this.continueBtn);
|
||||
this.trackClicks(this.continueBtn, () => {
|
||||
this.close(false);
|
||||
});
|
||||
|
||||
this.menuBtn = document.createElement("button");
|
||||
this.menuBtn.classList.add("menu", "styledButton");
|
||||
this.menuBtn.innerText = T.ingame.puzzleCompletion.menuBtn;
|
||||
buttonBar.appendChild(this.menuBtn);
|
||||
|
||||
this.trackClicks(this.menuBtn, () => {
|
||||
this.close(true);
|
||||
});
|
||||
}
|
||||
|
||||
updateState() {
|
||||
this.buttonLikeYes.classList.toggle("active", this.userDidLikePuzzle === true);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.root.soundProxy.playUi(SOUNDS.levelComplete);
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
this.visible = true;
|
||||
this.timeOfCompletion = this.root.time.now();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
}
|
||||
|
||||
isBlockingOverlay() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
close(toMenu) {
|
||||
/** @type {PuzzlePlayGameMode} */ (this.root.gameMode)
|
||||
.trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion))
|
||||
.then(() => {
|
||||
if (toMenu) {
|
||||
this.root.gameState.moveToState("PuzzleMenuState");
|
||||
} else {
|
||||
this.visible = false;
|
||||
this.cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.domAttach.update(this.visible);
|
||||
}
|
||||
}
|
||||
13
src/js/game/hud/parts/puzzle_dlc_logo.js
Normal file
13
src/js/game/hud/parts/puzzle_dlc_logo.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDPuzzleDLCLogo extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleDLCLogo");
|
||||
parent.appendChild(this.element);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
next() {}
|
||||
}
|
||||
18
src/js/game/hud/parts/puzzle_editor_controls.js
Normal file
18
src/js/game/hud/parts/puzzle_editor_controls.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
export class HUDPuzzleEditorControls extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorControls");
|
||||
|
||||
this.element.innerHTML = T.ingame.puzzleEditorControls.instructions
|
||||
.map(text => `<span>${text}</span>`)
|
||||
.join("");
|
||||
|
||||
this.titleElement = makeDiv(parent, "ingame_HUD_PuzzleEditorTitle");
|
||||
this.titleElement.innerText = T.ingame.puzzleEditorControls.title;
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
}
|
||||
233
src/js/game/hud/parts/puzzle_editor_review.js
Normal file
233
src/js/game/hud/parts/puzzle_editor_review.js
Normal file
@@ -0,0 +1,233 @@
|
||||
import { globalConfig, THIRDPARTY_URLS } from "../../../core/config";
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { DialogWithForm } from "../../../core/modal_dialog_elements";
|
||||
import { FormElementInput, FormElementItemChooser } from "../../../core/modal_dialog_forms";
|
||||
import { STOP_PROPAGATION } from "../../../core/signal";
|
||||
import { fillInLinkIntoTranslation, makeDiv } from "../../../core/utils";
|
||||
import { PuzzleSerializer } from "../../../savegame/puzzle_serializer";
|
||||
import { T } from "../../../translations";
|
||||
import { ConstantSignalComponent } from "../../components/constant_signal";
|
||||
import { GoalAcceptorComponent } from "../../components/goal_acceptor";
|
||||
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
||||
import { ShapeItem } from "../../items/shape_item";
|
||||
import { ShapeDefinition } from "../../shape_definition";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const trim = require("trim");
|
||||
const logger = createLogger("puzzle-review");
|
||||
|
||||
export class HUDPuzzleEditorReview extends BaseHUDPart {
|
||||
constructor(root) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
createElements(parent) {
|
||||
const key = this.root.gameMode.getId();
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorReview");
|
||||
this.button = document.createElement("button");
|
||||
this.button.classList.add("button");
|
||||
this.button.textContent = T.puzzleMenu.reviewPuzzle;
|
||||
this.element.appendChild(this.button);
|
||||
|
||||
this.trackClicks(this.button, this.startReview);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
startReview() {
|
||||
const validationError = this.validatePuzzle();
|
||||
if (validationError) {
|
||||
this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.validatingPuzzle);
|
||||
|
||||
// Wait a bit, so the user sees the puzzle actually got validated
|
||||
setTimeout(() => {
|
||||
// Manually simulate ticks
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
|
||||
const maxTicks =
|
||||
this.root.gameMode.getFixedTickrate() * globalConfig.puzzleValidationDurationSeconds;
|
||||
const deltaMs = this.root.dynamicTickrate.deltaMs;
|
||||
logger.log("Simulating up to", maxTicks, "ticks, start=", this.root.time.now().toFixed(1));
|
||||
const now = performance.now();
|
||||
|
||||
let simulatedTicks = 0;
|
||||
for (let i = 0; i < maxTicks; ++i) {
|
||||
// Perform logic tick
|
||||
this.root.time.performTicks(deltaMs, this.root.gameState.core.boundInternalTick);
|
||||
simulatedTicks++;
|
||||
|
||||
if (simulatedTicks % 100 == 0 && !this.validatePuzzle()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const duration = performance.now() - now;
|
||||
logger.log(
|
||||
"Simulated",
|
||||
simulatedTicks,
|
||||
"ticks, end=",
|
||||
this.root.time.now().toFixed(1),
|
||||
"duration=",
|
||||
duration.toFixed(2),
|
||||
"ms"
|
||||
);
|
||||
|
||||
console.log("duration: " + duration);
|
||||
closeLoading();
|
||||
|
||||
//if it took so little ticks that it must have autocompeted
|
||||
if (simulatedTicks <= 300) {
|
||||
this.root.hud.parts.dialogs.showWarning(
|
||||
T.puzzleMenu.validation.title,
|
||||
T.puzzleMenu.validation.autoComplete
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
//if we reached maximum ticks and the puzzle still isn't completed
|
||||
const validationError = this.validatePuzzle();
|
||||
if (simulatedTicks == maxTicks && validationError) {
|
||||
this.root.hud.parts.dialogs.showWarning(T.puzzleMenu.validation.title, validationError);
|
||||
return;
|
||||
}
|
||||
this.startSubmit();
|
||||
}, 750);
|
||||
}
|
||||
|
||||
startSubmit(title = "", shortKey = "") {
|
||||
const regex = /^[a-zA-Z0-9_\- ]{4,20}$/;
|
||||
const nameInput = new FormElementInput({
|
||||
id: "nameInput",
|
||||
label: T.dialogs.submitPuzzle.descName,
|
||||
placeholder: T.dialogs.submitPuzzle.placeholderName,
|
||||
defaultValue: title,
|
||||
validator: val => trim(val).match(regex) && trim(val).length > 0,
|
||||
});
|
||||
|
||||
let items = new Set();
|
||||
const acceptors = this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent);
|
||||
for (const acceptor of acceptors) {
|
||||
const item = acceptor.components.GoalAcceptor.item;
|
||||
if (item.getItemType() === "shape") {
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
while (items.size < 8) {
|
||||
// add some randoms
|
||||
const item = this.root.hubGoals.computeFreeplayShape(Math.round(10 + Math.random() * 10000));
|
||||
items.add(new ShapeItem(item));
|
||||
}
|
||||
|
||||
const itemInput = new FormElementItemChooser({
|
||||
id: "signalItem",
|
||||
label: fillInLinkIntoTranslation(T.dialogs.submitPuzzle.descIcon, THIRDPARTY_URLS.shapeViewer),
|
||||
items: Array.from(items),
|
||||
});
|
||||
|
||||
const shapeKeyInput = new FormElementInput({
|
||||
id: "shapeKeyInput",
|
||||
label: null,
|
||||
placeholder: "CuCuCuCu",
|
||||
defaultValue: shortKey,
|
||||
validator: val => ShapeDefinition.isValidShortKey(trim(val)),
|
||||
});
|
||||
|
||||
const dialog = new DialogWithForm({
|
||||
app: this.root.app,
|
||||
title: T.dialogs.submitPuzzle.title,
|
||||
desc: "",
|
||||
formElements: [nameInput, itemInput, shapeKeyInput],
|
||||
buttons: ["ok:good:enter"],
|
||||
});
|
||||
|
||||
itemInput.valueChosen.add(value => {
|
||||
shapeKeyInput.setValue(value.definition.getHash());
|
||||
});
|
||||
|
||||
this.root.hud.parts.dialogs.internalShowDialog(dialog);
|
||||
|
||||
dialog.buttonSignals.ok.add(() => {
|
||||
const title = trim(nameInput.getValue());
|
||||
const shortKey = trim(shapeKeyInput.getValue());
|
||||
this.doSubmitPuzzle(title, shortKey);
|
||||
});
|
||||
}
|
||||
|
||||
doSubmitPuzzle(title, shortKey) {
|
||||
const serialized = new PuzzleSerializer().generateDumpFromGameRoot(this.root);
|
||||
|
||||
logger.log("Submitting puzzle, title=", title, "shortKey=", shortKey);
|
||||
if (G_IS_DEV) {
|
||||
logger.log("Serialized data:", serialized);
|
||||
}
|
||||
|
||||
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.submittingPuzzle);
|
||||
|
||||
this.root.app.clientApi
|
||||
.apiSubmitPuzzle({
|
||||
title,
|
||||
shortKey,
|
||||
data: serialized,
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
closeLoading();
|
||||
const { ok } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.puzzleSubmitOk.title,
|
||||
T.dialogs.puzzleSubmitOk.desc
|
||||
);
|
||||
ok.add(() => this.root.gameState.moveToState("PuzzleMenuState"));
|
||||
},
|
||||
err => {
|
||||
closeLoading();
|
||||
logger.warn("Failed to submit puzzle:", err);
|
||||
const signals = this.root.hud.parts.dialogs.showWarning(
|
||||
T.dialogs.puzzleSubmitError.title,
|
||||
T.dialogs.puzzleSubmitError.desc + " " + err,
|
||||
["cancel", "retry:good"]
|
||||
);
|
||||
signals.retry.add(() => this.startSubmit(title, shortKey));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
validatePuzzle() {
|
||||
// Check there is at least one constant producer and goal acceptor
|
||||
const producers = this.root.entityMgr.getAllWithComponent(ConstantSignalComponent);
|
||||
const acceptors = this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent);
|
||||
|
||||
if (producers.length === 0) {
|
||||
return T.puzzleMenu.validation.noProducers;
|
||||
}
|
||||
|
||||
if (acceptors.length === 0) {
|
||||
return T.puzzleMenu.validation.noGoalAcceptors;
|
||||
}
|
||||
|
||||
// Check if all acceptors satisfy the constraints
|
||||
for (const acceptor of acceptors) {
|
||||
const goalComp = acceptor.components.GoalAcceptor;
|
||||
if (!goalComp.item) {
|
||||
return T.puzzleMenu.validation.goalAcceptorNoItem;
|
||||
}
|
||||
const required = goalComp.getRequiredDeliveryHistorySize();
|
||||
if (goalComp.deliveryHistory.length < required) {
|
||||
return T.puzzleMenu.validation.goalAcceptorRateNotMet;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all buildings are within the area
|
||||
const entities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent);
|
||||
for (const entity of entities) {
|
||||
if (this.root.systemMgr.systems.zone.prePlacementCheck(entity) === STOP_PROPAGATION) {
|
||||
return T.puzzleMenu.validation.buildingOutOfBounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/js/game/hud/parts/puzzle_editor_settings.js
Normal file
200
src/js/game/hud/parts/puzzle_editor_settings.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/* typehints:start */
|
||||
import { PuzzleGameMode } from "../../modes/puzzle";
|
||||
/* typehints:end */
|
||||
|
||||
import { globalConfig } from "../../../core/config";
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { Rectangle } from "../../../core/rectangle";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { StaticMapEntityComponent } from "../../components/static_map_entity";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const logger = createLogger("puzzle-editor");
|
||||
|
||||
export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorSettings");
|
||||
|
||||
if (this.root.gameMode.getBuildableZones()) {
|
||||
const bind = (selector, handler) =>
|
||||
this.trackClicks(this.element.querySelector(selector), handler);
|
||||
this.zone = makeDiv(
|
||||
this.element,
|
||||
null,
|
||||
["section", "zone"],
|
||||
`
|
||||
<label>${T.ingame.puzzleEditorSettings.zoneTitle}</label>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="zoneWidth plusMinus">
|
||||
<label>${T.ingame.puzzleEditorSettings.zoneWidth}</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<span class="value"></span>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
<div class="zoneHeight plusMinus">
|
||||
<label>${T.ingame.puzzleEditorSettings.zoneHeight}</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<span class="value"></span>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
<div class="buttonBar">
|
||||
<button class="styledButton trim">${T.ingame.puzzleEditorSettings.trimZone}</button>
|
||||
<button class="styledButton clear">${T.ingame.puzzleEditorSettings.clearItems}</button>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
|
||||
bind(".zoneWidth .minus", () => this.modifyZone(-1, 0));
|
||||
bind(".zoneWidth .plus", () => this.modifyZone(1, 0));
|
||||
bind(".zoneHeight .minus", () => this.modifyZone(0, -1));
|
||||
bind(".zoneHeight .plus", () => this.modifyZone(0, 1));
|
||||
bind("button.trim", this.trim);
|
||||
bind("button.clear", this.clear);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
}
|
||||
|
||||
trim() {
|
||||
// Now, find the center
|
||||
const buildings = this.root.entityMgr.entities.slice();
|
||||
|
||||
if (buildings.length === 0) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
let minRect = null;
|
||||
|
||||
for (const building of buildings) {
|
||||
const staticComp = building.components.StaticMapEntity;
|
||||
const bounds = staticComp.getTileSpaceBounds();
|
||||
|
||||
if (!minRect) {
|
||||
minRect = bounds;
|
||||
} else {
|
||||
minRect = minRect.getUnion(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||
const moveByInverse = minRect.getCenter().round();
|
||||
|
||||
// move buildings
|
||||
if (moveByInverse.length() > 0) {
|
||||
// increase area size
|
||||
mode.zoneWidth = globalConfig.puzzleMaxBoundsSize;
|
||||
mode.zoneHeight = globalConfig.puzzleMaxBoundsSize;
|
||||
|
||||
// First, remove any items etc
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
|
||||
this.root.logic.performImmutableOperation(() => {
|
||||
// 1. remove all buildings
|
||||
for (const building of buildings) {
|
||||
if (!this.root.logic.tryDeleteBuilding(building)) {
|
||||
assertAlways(false, "Failed to remove building in trim");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. place them again, but centered
|
||||
for (const building of buildings) {
|
||||
const staticComp = building.components.StaticMapEntity;
|
||||
const result = this.root.logic.tryPlaceBuilding({
|
||||
origin: staticComp.origin.sub(moveByInverse),
|
||||
building: staticComp.getMetaBuilding(),
|
||||
originalRotation: staticComp.originalRotation,
|
||||
rotation: staticComp.rotation,
|
||||
rotationVariant: staticComp.getRotationVariant(),
|
||||
variant: staticComp.getVariant(),
|
||||
});
|
||||
if (!result) {
|
||||
this.root.bulkOperationRunning = false;
|
||||
assertAlways(false, "Failed to re-place building in trim");
|
||||
}
|
||||
|
||||
if (building.components.ConstantSignal) {
|
||||
result.components.ConstantSignal.signal = building.components.ConstantSignal.signal;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Actually trim
|
||||
let w = mode.zoneWidth;
|
||||
let h = mode.zoneHeight;
|
||||
|
||||
while (!this.anyBuildingOutsideZone(w - 1, h)) {
|
||||
--w;
|
||||
}
|
||||
|
||||
while (!this.anyBuildingOutsideZone(w, h - 1)) {
|
||||
--h;
|
||||
}
|
||||
|
||||
mode.zoneWidth = w;
|
||||
mode.zoneHeight = h;
|
||||
this.updateZoneValues();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.visible = true;
|
||||
this.updateZoneValues();
|
||||
}
|
||||
|
||||
anyBuildingOutsideZone(width, height) {
|
||||
if (Math.min(width, height) < globalConfig.puzzleMinBoundsSize) {
|
||||
return true;
|
||||
}
|
||||
const newZone = Rectangle.centered(width, height);
|
||||
const entities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent);
|
||||
|
||||
for (const entity of entities) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const bounds = staticComp.getTileSpaceBounds();
|
||||
if (!newZone.intersectsFully(bounds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifyZone(deltaW, deltaH) {
|
||||
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||
|
||||
const newWidth = mode.zoneWidth + deltaW;
|
||||
const newHeight = mode.zoneHeight + deltaH;
|
||||
|
||||
if (Math.min(newWidth, newHeight) < globalConfig.puzzleMinBoundsSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.max(newWidth, newHeight) > globalConfig.puzzleMaxBoundsSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.anyBuildingOutsideZone(newWidth, newHeight)) {
|
||||
this.root.hud.parts.dialogs.showWarning(
|
||||
T.dialogs.puzzleResizeBadBuildings.title,
|
||||
T.dialogs.puzzleResizeBadBuildings.desc
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
mode.zoneWidth = newWidth;
|
||||
mode.zoneHeight = newHeight;
|
||||
this.updateZoneValues();
|
||||
}
|
||||
|
||||
updateZoneValues() {
|
||||
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||
|
||||
this.element.querySelector(".zoneWidth > .value").textContent = String(mode.zoneWidth);
|
||||
this.element.querySelector(".zoneHeight > .value").textContent = String(mode.zoneHeight);
|
||||
}
|
||||
}
|
||||
72
src/js/game/hud/parts/puzzle_play_metadata.js
Normal file
72
src/js/game/hud/parts/puzzle_play_metadata.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* typehints:start */
|
||||
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
|
||||
/* typehints:end */
|
||||
|
||||
import { formatBigNumberFull, formatSeconds, makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const copy = require("clipboard-copy");
|
||||
|
||||
export class HUDPuzzlePlayMetadata extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.titleElement = makeDiv(parent, "ingame_HUD_PuzzlePlayTitle");
|
||||
this.titleElement.innerText = "PUZZLE";
|
||||
|
||||
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
const puzzle = mode.puzzle;
|
||||
|
||||
this.puzzleNameElement = makeDiv(this.titleElement, null, ["name"]);
|
||||
this.puzzleNameElement.innerText = puzzle.meta.title;
|
||||
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzlePlayMetadata");
|
||||
this.element.innerHTML = `
|
||||
|
||||
<div class="plays">
|
||||
<span class="downloads">${formatBigNumberFull(puzzle.meta.downloads)}</span>
|
||||
<span class="likes">${formatBigNumberFull(puzzle.meta.likes)}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="info author"><label>${T.ingame.puzzleMetadata.author}</label><span></span></div>
|
||||
<div class="info key">
|
||||
<label>${T.ingame.puzzleMetadata.shortKey}</label><span>${puzzle.meta.shortKey}</span>
|
||||
</div>
|
||||
<div class="info rating">
|
||||
<label>${T.ingame.puzzleMetadata.averageDuration}</label>
|
||||
<span>${puzzle.meta.averageTime ? formatSeconds(puzzle.meta.averageTime) : "-"}</span>
|
||||
</div>
|
||||
<div class="info rating">
|
||||
<label>${T.ingame.puzzleMetadata.completionRate}</label>
|
||||
<span>${
|
||||
puzzle.meta.downloads > 0
|
||||
? ((puzzle.meta.completions / puzzle.meta.downloads) * 100.0).toFixed(1) + "%"
|
||||
: "-"
|
||||
}</span>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="styledButton share">${T.ingame.puzzleEditorSettings.share}</button>
|
||||
<button class="styledButton report">${T.ingame.puzzleEditorSettings.report}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.trackClicks(this.element.querySelector("button.share"), this.share);
|
||||
this.trackClicks(this.element.querySelector("button.report"), this.report);
|
||||
|
||||
/** @type {HTMLElement} */ (this.element.querySelector(".author span")).innerText =
|
||||
puzzle.meta.author;
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
share() {
|
||||
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
mode.sharePuzzle();
|
||||
}
|
||||
|
||||
report() {
|
||||
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||
mode.reportPuzzle();
|
||||
}
|
||||
}
|
||||
36
src/js/game/hud/parts/puzzle_play_settings.js
Normal file
36
src/js/game/hud/parts/puzzle_play_settings.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createLogger } from "../../../core/logging";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
const logger = createLogger("puzzle-play");
|
||||
|
||||
export class HUDPuzzlePlaySettings extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_PuzzlePlaySettings");
|
||||
|
||||
if (this.root.gameMode.getBuildableZones()) {
|
||||
const bind = (selector, handler) =>
|
||||
this.trackClicks(this.element.querySelector(selector), handler);
|
||||
makeDiv(
|
||||
this.element,
|
||||
null,
|
||||
["section"],
|
||||
`
|
||||
<button class="styledButton clear">${T.ingame.puzzleEditorSettings.clearItems}</button>
|
||||
|
||||
`
|
||||
);
|
||||
|
||||
bind("button.clear", this.clear);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.visible = true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { queryParamOptions } from "../../../core/query_parameters";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
@@ -19,25 +20,25 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesBelt plusMinus">
|
||||
<label>Upgrades → Belt</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesExtraction plusMinus">
|
||||
<label>Upgrades → Extraction</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesProcessing plusMinus">
|
||||
<label>Upgrades → Processing</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="upgradesPainting plusMinus">
|
||||
<label>Upgrades → Painting</label>
|
||||
<button class="styledButton minus">-</button>
|
||||
@@ -117,7 +118,9 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
// Clear all shapes of this level
|
||||
hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0;
|
||||
|
||||
this.root.hud.parts.pinnedShapes.rerenderFull();
|
||||
if (this.root.hud.parts.pinnedShapes) {
|
||||
this.root.hud.parts.pinnedShapes.rerenderFull();
|
||||
}
|
||||
|
||||
// Compute gained rewards
|
||||
hubGoals.gainedRewards = {};
|
||||
@@ -144,7 +147,7 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
}
|
||||
});
|
||||
|
||||
this.visible = !G_IS_DEV;
|
||||
this.visible = false;
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,19 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
|
||||
this.menuElement = makeDiv(this.background, null, ["menuElement"]);
|
||||
|
||||
this.statsElement = makeDiv(
|
||||
this.background,
|
||||
null,
|
||||
["statsElement"],
|
||||
`
|
||||
if (this.root.gameMode.hasHub()) {
|
||||
this.statsElement = makeDiv(
|
||||
this.background,
|
||||
null,
|
||||
["statsElement"],
|
||||
`
|
||||
<strong>${T.ingame.settingsMenu.beltsPlaced}</strong><span class="beltsPlaced"></span>
|
||||
<strong>${T.ingame.settingsMenu.buildingsPlaced}</strong><span class="buildingsPlaced"></span>
|
||||
<strong>${T.ingame.settingsMenu.playtime}</strong><span class="playtime"></span>
|
||||
|
||||
`
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]);
|
||||
|
||||
@@ -94,23 +96,25 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
|
||||
const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60);
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
const playtimeElement = this.statsElement.querySelector(".playtime");
|
||||
/** @type {HTMLElement} */
|
||||
const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced");
|
||||
/** @type {HTMLElement} */
|
||||
const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced");
|
||||
if (this.root.gameMode.hasHub()) {
|
||||
/** @type {HTMLElement} */
|
||||
const playtimeElement = this.statsElement.querySelector(".playtime");
|
||||
/** @type {HTMLElement} */
|
||||
const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced");
|
||||
/** @type {HTMLElement} */
|
||||
const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced");
|
||||
|
||||
playtimeElement.innerText = T.global.time.xMinutes.replace("<x>", `${totalMinutesPlayed}`);
|
||||
playtimeElement.innerText = T.global.time.xMinutes.replace("<x>", `${totalMinutesPlayed}`);
|
||||
|
||||
buildingsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
|
||||
buildingsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
|
||||
this.root.entityMgr.getAllWithComponent(BeltComponent).length
|
||||
);
|
||||
|
||||
beltsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(BeltComponent).length
|
||||
);
|
||||
|
||||
beltsPlacedElement.innerText = formatBigNumberFull(
|
||||
this.root.entityMgr.getAllWithComponent(BeltComponent).length
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
@@ -100,16 +100,14 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
|
||||
this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png");
|
||||
|
||||
/** @type {Array<Waypoint>}
|
||||
*/
|
||||
this.waypoints = [
|
||||
{
|
||||
label: null,
|
||||
center: { x: 0, y: 0 },
|
||||
zoomLevel: 3,
|
||||
layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(),
|
||||
},
|
||||
];
|
||||
/** @type {Array<Waypoint>} */
|
||||
this.waypoints = [];
|
||||
this.waypoints.push({
|
||||
label: null,
|
||||
center: { x: 0, y: 0 },
|
||||
zoomLevel: 3,
|
||||
layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(),
|
||||
});
|
||||
|
||||
// Create a buffer we can use to measure text
|
||||
this.dummyBuffer = makeOffscreenBuffer(1, 1, {
|
||||
|
||||
@@ -28,6 +28,9 @@ export class HUDWiresOverlay extends BaseHUDPart {
|
||||
* Switches between layers
|
||||
*/
|
||||
switchLayers() {
|
||||
if (!this.root.gameMode.getSupportsWires()) {
|
||||
return;
|
||||
}
|
||||
if (this.root.currentLayer === "regular") {
|
||||
if (
|
||||
this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) ||
|
||||
|
||||
@@ -49,6 +49,11 @@ export const KEYMAPPINGS = {
|
||||
},
|
||||
|
||||
buildings: {
|
||||
// Puzzle buildings
|
||||
constant_producer: { keyCode: key("H") },
|
||||
goal_acceptor: { keyCode: key("N") },
|
||||
block: { keyCode: key("4") },
|
||||
|
||||
// Primary Toolbar
|
||||
belt: { keyCode: key("1") },
|
||||
balancer: { keyCode: key("2") },
|
||||
@@ -102,6 +107,7 @@ export const KEYMAPPINGS = {
|
||||
massSelectSelectMultiple: { keyCode: 16 }, // SHIFT
|
||||
massSelectCopy: { keyCode: key("C") },
|
||||
massSelectCut: { keyCode: key("X") },
|
||||
massSelectClear: { keyCode: key("B") },
|
||||
confirmMassDelete: { keyCode: 46 }, // DEL
|
||||
pasteLastBlueprint: { keyCode: key("V") },
|
||||
},
|
||||
@@ -262,6 +268,8 @@ export function getStringForKeyCode(code) {
|
||||
return ".";
|
||||
case 191:
|
||||
return "/";
|
||||
case 192:
|
||||
return "`";
|
||||
case 219:
|
||||
return "[";
|
||||
case 220:
|
||||
@@ -322,6 +330,15 @@ export class Keybinding {
|
||||
this.signal.add(receiver, scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener
|
||||
* @param {function() : void} receiver
|
||||
* @param {object=} scope
|
||||
*/
|
||||
addToTop(receiver, scope = null) {
|
||||
this.signal.addToTop(receiver, scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} elem
|
||||
* @returns {HTMLElement} the created element, or null if the keybindings are not shown
|
||||
|
||||
@@ -4,6 +4,7 @@ import { STOP_PROPAGATION } from "../core/signal";
|
||||
import { round2Digits } from "../core/utils";
|
||||
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
|
||||
import { getBuildingDataFromCode } from "./building_codes";
|
||||
import { Component } from "./component";
|
||||
import { enumWireVariant } from "./components/wire";
|
||||
import { Entity } from "./entity";
|
||||
import { CHUNK_OVERLAY_RES } from "./map_chunk_view";
|
||||
@@ -161,13 +162,34 @@ export class GameLogic {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a immutable operation, causing no recalculations
|
||||
* @param {function} operation
|
||||
*/
|
||||
performImmutableOperation(operation) {
|
||||
logger.warn("Running immutable operation ...");
|
||||
assert(!this.root.immutableOperationRunning, "Can not run two immutalbe operations twice");
|
||||
this.root.immutableOperationRunning = true;
|
||||
const now = performance.now();
|
||||
const returnValue = operation();
|
||||
const duration = performance.now() - now;
|
||||
logger.log("Done in", round2Digits(duration), "ms");
|
||||
assert(
|
||||
this.root.immutableOperationRunning,
|
||||
"Immutable operation = false while immutable operation was running"
|
||||
);
|
||||
this.root.immutableOperationRunning = false;
|
||||
this.root.signals.immutableOperationFinished.dispatch();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given building can get removed
|
||||
* @param {Entity} building
|
||||
*/
|
||||
canDeleteBuilding(building) {
|
||||
const staticComp = building.components.StaticMapEntity;
|
||||
return staticComp.getMetaBuilding().getIsRemovable();
|
||||
return staticComp.getMetaBuilding().getIsRemovable(this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,8 +364,6 @@ export class GameLogic {
|
||||
return !!overlayMatrix[localPosition.x + localPosition.y * 3];
|
||||
}
|
||||
|
||||
g(tile, edge) {}
|
||||
|
||||
/**
|
||||
* Returns the acceptors and ejectors which affect the current tile
|
||||
* @param {Vector} tile
|
||||
@@ -425,4 +445,15 @@ export class GameLogic {
|
||||
}
|
||||
return { ejectors, acceptors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all belts and items
|
||||
*/
|
||||
clearAllBeltsAndItems() {
|
||||
for (const entity of this.root.entityMgr.entities) {
|
||||
for (const component of Object.values(entity.components)) {
|
||||
/** @type {Component} */ (component).clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,14 @@ export class MapChunkView extends MapChunk {
|
||||
*/
|
||||
drawBackgroundLayer(parameters) {
|
||||
const systems = this.root.systemMgr.systems;
|
||||
systems.mapResources.drawChunk(parameters, this);
|
||||
if (systems.zone) {
|
||||
systems.zone.drawChunk(parameters, this);
|
||||
}
|
||||
|
||||
if (this.root.gameMode.hasResources()) {
|
||||
systems.mapResources.drawChunk(parameters, this);
|
||||
}
|
||||
|
||||
systems.beltUnderlays.drawChunk(parameters, this);
|
||||
systems.belt.drawChunk(parameters, this);
|
||||
}
|
||||
@@ -69,6 +76,8 @@ export class MapChunkView extends MapChunk {
|
||||
systems.lever.drawChunk(parameters, this);
|
||||
systems.display.drawChunk(parameters, this);
|
||||
systems.storage.drawChunk(parameters, this);
|
||||
systems.constantProducer.drawChunk(parameters, this);
|
||||
systems.goalAcceptor.drawChunk(parameters, this);
|
||||
systems.itemProcessorOverlays.drawChunk(parameters, this);
|
||||
}
|
||||
|
||||
|
||||
@@ -108,9 +108,10 @@ export class MetaBuilding {
|
||||
|
||||
/**
|
||||
* Returns whether this building is removable
|
||||
* @param {GameRoot} root
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIsRemovable() {
|
||||
getIsRemovable(root) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import { T } from "../translations";
|
||||
import { MetaAnalyzerBuilding } from "./buildings/analyzer";
|
||||
import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer";
|
||||
import { MetaBeltBuilding } from "./buildings/belt";
|
||||
import { MetaBlockBuilding } from "./buildings/block";
|
||||
import { MetaComparatorBuilding } from "./buildings/comparator";
|
||||
import { MetaConstantProducerBuilding } from "./buildings/constant_producer";
|
||||
import { MetaConstantSignalBuilding } from "./buildings/constant_signal";
|
||||
import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter";
|
||||
import { MetaDisplayBuilding } from "./buildings/display";
|
||||
import { MetaFilterBuilding } from "./buildings/filter";
|
||||
import { MetaGoalAcceptorBuilding } from "./buildings/goal_acceptor";
|
||||
import { MetaHubBuilding } from "./buildings/hub";
|
||||
import { MetaItemProducerBuilding } from "./buildings/item_producer";
|
||||
import { MetaLeverBuilding } from "./buildings/lever";
|
||||
@@ -45,6 +48,7 @@ export function initMetaBuildingRegistry() {
|
||||
gMetaBuildingRegistry.register(MetaStorageBuilding);
|
||||
gMetaBuildingRegistry.register(MetaBeltBuilding);
|
||||
gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding);
|
||||
gMetaBuildingRegistry.register(MetaGoalAcceptorBuilding);
|
||||
gMetaBuildingRegistry.register(MetaHubBuilding);
|
||||
gMetaBuildingRegistry.register(MetaWireBuilding);
|
||||
gMetaBuildingRegistry.register(MetaConstantSignalBuilding);
|
||||
@@ -59,6 +63,8 @@ export function initMetaBuildingRegistry() {
|
||||
gMetaBuildingRegistry.register(MetaAnalyzerBuilding);
|
||||
gMetaBuildingRegistry.register(MetaComparatorBuilding);
|
||||
gMetaBuildingRegistry.register(MetaItemProducerBuilding);
|
||||
gMetaBuildingRegistry.register(MetaConstantProducerBuilding);
|
||||
gMetaBuildingRegistry.register(MetaBlockBuilding);
|
||||
|
||||
// Belt
|
||||
registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0);
|
||||
@@ -165,6 +171,15 @@ export function initMetaBuildingRegistry() {
|
||||
// Item producer
|
||||
registerBuildingVariant(61, MetaItemProducerBuilding);
|
||||
|
||||
// Constant producer
|
||||
registerBuildingVariant(62, MetaConstantProducerBuilding);
|
||||
|
||||
// Goal acceptor
|
||||
registerBuildingVariant(63, MetaGoalAcceptorBuilding);
|
||||
|
||||
// Block
|
||||
registerBuildingVariant(64, MetaBlockBuilding);
|
||||
|
||||
// Propagate instances
|
||||
for (const key in gBuildingVariants) {
|
||||
gBuildingVariants[key].metaInstance = gMetaBuildingRegistry.findByClass(
|
||||
|
||||
106
src/js/game/modes/puzzle.js
Normal file
106
src/js/game/modes/puzzle.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/* typehints:start */
|
||||
import { GameRoot } from "../root";
|
||||
/* typehints:end */
|
||||
|
||||
import { Rectangle } from "../../core/rectangle";
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { enumGameModeTypes, GameMode } from "../game_mode";
|
||||
import { HUDPuzzleBackToMenu } from "../hud/parts/puzzle_back_to_menu";
|
||||
import { HUDPuzzleDLCLogo } from "../hud/parts/puzzle_dlc_logo";
|
||||
|
||||
export class PuzzleGameMode extends GameMode {
|
||||
static getType() {
|
||||
return enumGameModeTypes.puzzle;
|
||||
}
|
||||
|
||||
/** @returns {object} */
|
||||
static getSchema() {
|
||||
return {
|
||||
zoneHeight: types.uint,
|
||||
zoneWidth: types.uint,
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {GameRoot} root */
|
||||
constructor(root) {
|
||||
super(root);
|
||||
|
||||
const data = this.getSaveData();
|
||||
|
||||
this.additionalHudParts = {
|
||||
puzzleBackToMenu: HUDPuzzleBackToMenu,
|
||||
puzzleDlcLogo: HUDPuzzleDLCLogo,
|
||||
};
|
||||
|
||||
this.zoneWidth = data.zoneWidth || 8;
|
||||
this.zoneHeight = data.zoneHeight || 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof import("../meta_building").MetaBuilding} building
|
||||
*/
|
||||
isBuildingExcluded(building) {
|
||||
return this.hiddenBuildings.indexOf(building) >= 0;
|
||||
}
|
||||
|
||||
getSaveData() {
|
||||
const save = this.root.savegame.getCurrentDump();
|
||||
if (!save) {
|
||||
return {};
|
||||
}
|
||||
return save.gameMode.data;
|
||||
}
|
||||
|
||||
getCameraBounds() {
|
||||
return Rectangle.centered(this.zoneWidth + 20, this.zoneHeight + 20);
|
||||
}
|
||||
|
||||
getBuildableZones() {
|
||||
return [Rectangle.centered(this.zoneWidth, this.zoneHeight)];
|
||||
}
|
||||
|
||||
hasHub() {
|
||||
return false;
|
||||
}
|
||||
|
||||
hasResources() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getMinimumZoom() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getMaximumZoom() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
getIsSaveable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSupportsCopyPaste() {
|
||||
return false;
|
||||
}
|
||||
|
||||
throughputDoesNotMatter() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getSupportsWires() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getFixedTickrate() {
|
||||
return 300;
|
||||
}
|
||||
|
||||
getIsDeterministic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsFreeplayAvailable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
66
src/js/game/modes/puzzle_edit.js
Normal file
66
src/js/game/modes/puzzle_edit.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* typehints:start */
|
||||
import { GameRoot } from "../root";
|
||||
/* typehints:end */
|
||||
|
||||
import { enumGameModeIds } from "../game_mode";
|
||||
import { PuzzleGameMode } from "./puzzle";
|
||||
import { MetaStorageBuilding } from "../buildings/storage";
|
||||
import { MetaReaderBuilding } from "../buildings/reader";
|
||||
import { MetaFilterBuilding } from "../buildings/filter";
|
||||
import { MetaDisplayBuilding } from "../buildings/display";
|
||||
import { MetaLeverBuilding } from "../buildings/lever";
|
||||
import { MetaItemProducerBuilding } from "../buildings/item_producer";
|
||||
import { MetaMinerBuilding } from "../buildings/miner";
|
||||
import { MetaWireBuilding } from "../buildings/wire";
|
||||
import { MetaWireTunnelBuilding } from "../buildings/wire_tunnel";
|
||||
import { MetaConstantSignalBuilding } from "../buildings/constant_signal";
|
||||
import { MetaLogicGateBuilding } from "../buildings/logic_gate";
|
||||
import { MetaVirtualProcessorBuilding } from "../buildings/virtual_processor";
|
||||
import { MetaAnalyzerBuilding } from "../buildings/analyzer";
|
||||
import { MetaComparatorBuilding } from "../buildings/comparator";
|
||||
import { MetaTransistorBuilding } from "../buildings/transistor";
|
||||
import { HUDPuzzleEditorControls } from "../hud/parts/puzzle_editor_controls";
|
||||
import { HUDPuzzleEditorReview } from "../hud/parts/puzzle_editor_review";
|
||||
import { HUDPuzzleEditorSettings } from "../hud/parts/puzzle_editor_settings";
|
||||
|
||||
export class PuzzleEditGameMode extends PuzzleGameMode {
|
||||
static getId() {
|
||||
return enumGameModeIds.puzzleEdit;
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @param {GameRoot} root */
|
||||
constructor(root) {
|
||||
super(root);
|
||||
|
||||
this.hiddenBuildings = [
|
||||
MetaStorageBuilding,
|
||||
MetaReaderBuilding,
|
||||
MetaFilterBuilding,
|
||||
MetaDisplayBuilding,
|
||||
MetaLeverBuilding,
|
||||
MetaItemProducerBuilding,
|
||||
MetaMinerBuilding,
|
||||
|
||||
MetaWireBuilding,
|
||||
MetaWireTunnelBuilding,
|
||||
MetaConstantSignalBuilding,
|
||||
MetaLogicGateBuilding,
|
||||
MetaVirtualProcessorBuilding,
|
||||
MetaAnalyzerBuilding,
|
||||
MetaComparatorBuilding,
|
||||
MetaTransistorBuilding,
|
||||
];
|
||||
|
||||
this.additionalHudParts.puzzleEditorControls = HUDPuzzleEditorControls;
|
||||
this.additionalHudParts.puzzleEditorReview = HUDPuzzleEditorReview;
|
||||
this.additionalHudParts.puzzleEditorSettings = HUDPuzzleEditorSettings;
|
||||
}
|
||||
|
||||
getIsEditor() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
193
src/js/game/modes/puzzle_play.js
Normal file
193
src/js/game/modes/puzzle_play.js
Normal file
@@ -0,0 +1,193 @@
|
||||
/* typehints:start */
|
||||
import { GameRoot } from "../root";
|
||||
/* typehints:end */
|
||||
|
||||
import { enumGameModeIds } from "../game_mode";
|
||||
import { PuzzleGameMode } from "./puzzle";
|
||||
import { MetaStorageBuilding } from "../buildings/storage";
|
||||
import { MetaReaderBuilding } from "../buildings/reader";
|
||||
import { MetaFilterBuilding } from "../buildings/filter";
|
||||
import { MetaDisplayBuilding } from "../buildings/display";
|
||||
import { MetaLeverBuilding } from "../buildings/lever";
|
||||
import { MetaItemProducerBuilding } from "../buildings/item_producer";
|
||||
import { MetaMinerBuilding } from "../buildings/miner";
|
||||
import { MetaWireBuilding } from "../buildings/wire";
|
||||
import { MetaWireTunnelBuilding } from "../buildings/wire_tunnel";
|
||||
import { MetaConstantSignalBuilding } from "../buildings/constant_signal";
|
||||
import { MetaLogicGateBuilding } from "../buildings/logic_gate";
|
||||
import { MetaVirtualProcessorBuilding } from "../buildings/virtual_processor";
|
||||
import { MetaAnalyzerBuilding } from "../buildings/analyzer";
|
||||
import { MetaComparatorBuilding } from "../buildings/comparator";
|
||||
import { MetaTransistorBuilding } from "../buildings/transistor";
|
||||
import { MetaConstantProducerBuilding } from "../buildings/constant_producer";
|
||||
import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor";
|
||||
import { PuzzleSerializer } from "../../savegame/puzzle_serializer";
|
||||
import { T } from "../../translations";
|
||||
import { HUDPuzzlePlayMetadata } from "../hud/parts/puzzle_play_metadata";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { HUDPuzzleCompleteNotification } from "../hud/parts/puzzle_complete_notification";
|
||||
import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings";
|
||||
import { MetaBlockBuilding } from "../buildings/block";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { gMetaBuildingRegistry } from "../../core/global_registries";
|
||||
|
||||
const logger = createLogger("puzzle-play");
|
||||
const copy = require("clipboard-copy");
|
||||
|
||||
export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||
static getId() {
|
||||
return enumGameModeIds.puzzlePlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
* @param {object} payload
|
||||
* @param {import("../../savegame/savegame_typedefs").PuzzleFullData} payload.puzzle
|
||||
*/
|
||||
constructor(root, { puzzle }) {
|
||||
super(root);
|
||||
|
||||
/** @type {Array<typeof MetaBuilding>} */
|
||||
let excludedBuildings = [
|
||||
MetaConstantProducerBuilding,
|
||||
MetaGoalAcceptorBuilding,
|
||||
MetaBlockBuilding,
|
||||
|
||||
MetaStorageBuilding,
|
||||
MetaReaderBuilding,
|
||||
MetaFilterBuilding,
|
||||
MetaDisplayBuilding,
|
||||
MetaLeverBuilding,
|
||||
MetaItemProducerBuilding,
|
||||
MetaMinerBuilding,
|
||||
|
||||
MetaWireBuilding,
|
||||
MetaWireTunnelBuilding,
|
||||
MetaConstantSignalBuilding,
|
||||
MetaLogicGateBuilding,
|
||||
MetaVirtualProcessorBuilding,
|
||||
MetaAnalyzerBuilding,
|
||||
MetaComparatorBuilding,
|
||||
MetaTransistorBuilding,
|
||||
];
|
||||
|
||||
if (puzzle.game.excludedBuildings) {
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const puzzleHidden = puzzle.game.excludedBuildings
|
||||
.map(id => {
|
||||
if (!gMetaBuildingRegistry.hasId(id)) {
|
||||
return;
|
||||
}
|
||||
return gMetaBuildingRegistry.findById(id).constructor;
|
||||
})
|
||||
.filter(x => !!x);
|
||||
excludedBuildings = excludedBuildings.concat(puzzleHidden);
|
||||
}
|
||||
|
||||
this.hiddenBuildings = excludedBuildings;
|
||||
|
||||
this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata;
|
||||
this.additionalHudParts.puzzlePlaySettings = HUDPuzzlePlaySettings;
|
||||
this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification;
|
||||
|
||||
root.signals.postLoadHook.add(this.loadPuzzle, this);
|
||||
|
||||
this.puzzle = puzzle;
|
||||
}
|
||||
|
||||
loadPuzzle() {
|
||||
let errorText;
|
||||
logger.log("Loading puzzle", this.puzzle);
|
||||
|
||||
try {
|
||||
this.zoneWidth = this.puzzle.game.bounds.w;
|
||||
this.zoneHeight = this.puzzle.game.bounds.h;
|
||||
errorText = new PuzzleSerializer().deserializePuzzle(this.root, this.puzzle.game);
|
||||
} catch (ex) {
|
||||
errorText = ex.message || ex;
|
||||
}
|
||||
|
||||
if (errorText) {
|
||||
this.root.gameState.moveToState("PuzzleMenuState", {
|
||||
error: {
|
||||
title: T.dialogs.puzzleLoadError.title,
|
||||
desc: T.dialogs.puzzleLoadError.desc + " " + errorText,
|
||||
},
|
||||
});
|
||||
// const signals = this.root.hud.parts.dialogs.showWarning(
|
||||
// T.dialogs.puzzleLoadError.title,
|
||||
// T.dialogs.puzzleLoadError.desc + " " + errorText
|
||||
// );
|
||||
// signals.ok.add(() => this.root.gameState.moveToState("PuzzleMenuState"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {boolean} liked
|
||||
* @param {number} time
|
||||
*/
|
||||
trackCompleted(liked, time) {
|
||||
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog();
|
||||
|
||||
return this.root.app.clientApi
|
||||
.apiCompletePuzzle(this.puzzle.meta.id, {
|
||||
time,
|
||||
liked,
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn("Failed to complete puzzle:", err);
|
||||
})
|
||||
.then(() => {
|
||||
closeLoading();
|
||||
});
|
||||
}
|
||||
|
||||
sharePuzzle() {
|
||||
copy(this.puzzle.meta.shortKey);
|
||||
|
||||
this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.puzzleShare.title,
|
||||
T.dialogs.puzzleShare.desc.replace("<key>", this.puzzle.meta.shortKey)
|
||||
);
|
||||
}
|
||||
|
||||
reportPuzzle() {
|
||||
const { optionSelected } = this.root.hud.parts.dialogs.showOptionChooser(
|
||||
T.dialogs.puzzleReport.title,
|
||||
{
|
||||
options: [
|
||||
{ value: "profane", text: T.dialogs.puzzleReport.options.profane },
|
||||
{ value: "unsolvable", text: T.dialogs.puzzleReport.options.unsolvable },
|
||||
{ value: "trolling", text: T.dialogs.puzzleReport.options.trolling },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise(resolve => {
|
||||
optionSelected.add(option => {
|
||||
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog();
|
||||
|
||||
this.root.app.clientApi.apiReportPuzzle(this.puzzle.meta.id, option).then(
|
||||
() => {
|
||||
closeLoading();
|
||||
const { ok } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.puzzleReportComplete.title,
|
||||
T.dialogs.puzzleReportComplete.desc
|
||||
);
|
||||
ok.add(resolve);
|
||||
},
|
||||
err => {
|
||||
closeLoading();
|
||||
const { ok } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.puzzleReportError.title,
|
||||
T.dialogs.puzzleReportError.desc + " " + err
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,74 @@
|
||||
/* typehints:start */
|
||||
import { GameRoot } from "../root";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
/* typehints:end */
|
||||
|
||||
import { findNiceIntegerValue } from "../../core/utils";
|
||||
import { GameMode } from "../game_mode";
|
||||
import { MetaConstantProducerBuilding } from "../buildings/constant_producer";
|
||||
import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor";
|
||||
import { enumGameModeIds, enumGameModeTypes, GameMode } from "../game_mode";
|
||||
import { ShapeDefinition } from "../shape_definition";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { HUDWiresToolbar } from "../hud/parts/wires_toolbar";
|
||||
import { HUDBlueprintPlacer } from "../hud/parts/blueprint_placer";
|
||||
import { HUDUnlockNotification } from "../hud/parts/unlock_notification";
|
||||
import { HUDMassSelector } from "../hud/parts/mass_selector";
|
||||
import { HUDShop } from "../hud/parts/shop";
|
||||
import { HUDWaypoints } from "../hud/parts/waypoints";
|
||||
import { HUDStatistics } from "../hud/parts/statistics";
|
||||
import { HUDWireInfo } from "../hud/parts/wire_info";
|
||||
import { HUDLeverToggle } from "../hud/parts/lever_toggle";
|
||||
import { HUDPinnedShapes } from "../hud/parts/pinned_shapes";
|
||||
import { HUDNotifications } from "../hud/parts/notifications";
|
||||
import { HUDScreenshotExporter } from "../hud/parts/screenshot_exporter";
|
||||
import { HUDWiresOverlay } from "../hud/parts/wires_overlay";
|
||||
import { HUDShapeViewer } from "../hud/parts/shape_viewer";
|
||||
import { HUDLayerPreview } from "../hud/parts/layer_preview";
|
||||
import { HUDTutorialVideoOffer } from "../hud/parts/tutorial_video_offer";
|
||||
import { HUDMinerHighlight } from "../hud/parts/miner_highlight";
|
||||
import { HUDGameMenu } from "../hud/parts/game_menu";
|
||||
import { HUDConstantSignalEdit } from "../hud/parts/constant_signal_edit";
|
||||
import { IS_MOBILE } from "../../core/config";
|
||||
import { HUDKeybindingOverlay } from "../hud/parts/keybinding_overlay";
|
||||
import { HUDWatermark } from "../hud/parts/watermark";
|
||||
import { HUDStandaloneAdvantages } from "../hud/parts/standalone_advantages";
|
||||
import { HUDCatMemes } from "../hud/parts/cat_memes";
|
||||
import { HUDPartTutorialHints } from "../hud/parts/tutorial_hints";
|
||||
import { HUDInteractiveTutorial } from "../hud/parts/interactive_tutorial";
|
||||
import { HUDSandboxController } from "../hud/parts/sandbox_controller";
|
||||
import { queryParamOptions } from "../../core/query_parameters";
|
||||
import { MetaBlockBuilding } from "../buildings/block";
|
||||
|
||||
const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
||||
const finalGameShape = "RuCw--Cw:----Ru--";
|
||||
/** @typedef {{
|
||||
* shape: string,
|
||||
* amount: number
|
||||
* }} UpgradeRequirement */
|
||||
|
||||
/** @typedef {{
|
||||
* required: Array<UpgradeRequirement>
|
||||
* improvement?: number,
|
||||
* excludePrevious?: boolean
|
||||
* }} TierRequirement */
|
||||
|
||||
/** @typedef {Array<TierRequirement>} UpgradeTiers */
|
||||
|
||||
/** @typedef {{
|
||||
* shape: string,
|
||||
* required: number,
|
||||
* reward: enumHubGoalRewards,
|
||||
* throughputOnly?: boolean
|
||||
* }} LevelDefinition */
|
||||
|
||||
export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
||||
export const finalGameShape = "RuCw--Cw:----Ru--";
|
||||
const preparementShape = "CpRpCp--:SwSwSwSw";
|
||||
const blueprintShape = "CbCbCbRb:CwCwCwCw";
|
||||
|
||||
// Tiers need % of the previous tier as requirement too
|
||||
const tierGrowth = 2.5;
|
||||
|
||||
/**
|
||||
* Generates all upgrades
|
||||
* @returns {Object<string, import("../game_mode").UpgradeTiers>} */
|
||||
* @returns {Object<string, UpgradeTiers>} */
|
||||
function generateUpgrades(limitedVersion = false) {
|
||||
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
|
||||
const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1;
|
||||
@@ -87,7 +142,14 @@ function generateUpgrades(limitedVersion = false) {
|
||||
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }],
|
||||
},
|
||||
{
|
||||
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }],
|
||||
required: [
|
||||
{
|
||||
shape: G_CHINA_VERSION
|
||||
? "CyCyCyCy:CyCyCyCy:RyRyRyRy:RuRuRuRu"
|
||||
: "CbRbRbCb:CwCwCwCw:WbWbWbWb",
|
||||
amount: 50000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
required: [{ shape: preparementShape, amount: 25000 }],
|
||||
@@ -141,7 +203,12 @@ function generateUpgrades(limitedVersion = false) {
|
||||
required: [{ shape: "WrWrWrWr", amount: 3800 }],
|
||||
},
|
||||
{
|
||||
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }],
|
||||
required: [
|
||||
{
|
||||
shape: G_CHINA_VERSION ? "CuCuCuCu:CwCwCwCw:Sb--Sr--" : "RpRpRpRp:CwCwCwCw",
|
||||
amount: 6500,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }],
|
||||
@@ -315,7 +382,7 @@ export function generateLevelDefinitions(limitedVersion = false) {
|
||||
// 13
|
||||
// Tunnel Tier 2
|
||||
{
|
||||
shape: "RpRpRpRp:CwCwCwCw", // painting t3
|
||||
shape: G_CHINA_VERSION ? "CuCuCuCu:CwCwCwCw:Sb--Sr--" : "RpRpRpRp:CwCwCwCw", // painting t3
|
||||
required: 3800,
|
||||
reward: enumHubGoalRewards.reward_underground_belt_tier_2,
|
||||
},
|
||||
@@ -324,7 +391,7 @@ export function generateLevelDefinitions(limitedVersion = false) {
|
||||
...(limitedVersion
|
||||
? [
|
||||
{
|
||||
shape: "RpRpRpRp:CwCwCwCw",
|
||||
shape: G_CHINA_VERSION ? "CuCuCuCu:CwCwCwCw:Sb--Sr--" : "RpRpRpRp:CwCwCwCw",
|
||||
required: 0,
|
||||
reward: enumHubGoalRewards.reward_demo_end,
|
||||
},
|
||||
@@ -358,7 +425,9 @@ export function generateLevelDefinitions(limitedVersion = false) {
|
||||
// 17
|
||||
// Double painter
|
||||
{
|
||||
shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants)
|
||||
shape: G_CHINA_VERSION
|
||||
? "CyCyCyCy:CyCyCyCy:RyRyRyRy:RuRuRuRu"
|
||||
: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants)
|
||||
required: 20000,
|
||||
reward: enumHubGoalRewards.reward_painter_double,
|
||||
},
|
||||
@@ -398,7 +467,9 @@ export function generateLevelDefinitions(limitedVersion = false) {
|
||||
// 22
|
||||
// Constant signal
|
||||
{
|
||||
shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
|
||||
shape: G_CHINA_VERSION
|
||||
? "RrSySrSy:RyCrCwCr:CyCyRyCy"
|
||||
: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
|
||||
required: 25000,
|
||||
reward: enumHubGoalRewards.reward_constant_signal,
|
||||
},
|
||||
@@ -406,14 +477,18 @@ export function generateLevelDefinitions(limitedVersion = false) {
|
||||
// 23
|
||||
// Display
|
||||
{
|
||||
shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
|
||||
shape: G_CHINA_VERSION
|
||||
? "CrCrCrCr:CwCwCwCw:WwWwWwWw:CrCrCrCr"
|
||||
: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
|
||||
required: 25000,
|
||||
reward: enumHubGoalRewards.reward_display,
|
||||
},
|
||||
|
||||
// 24 Logic gates
|
||||
{
|
||||
shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
|
||||
shape: G_CHINA_VERSION
|
||||
? "Su----Su:RwRwRwRw:Cu----Cu:CwCwCwCw"
|
||||
: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
|
||||
required: 25000,
|
||||
reward: enumHubGoalRewards.reward_logic_gates,
|
||||
},
|
||||
@@ -454,27 +529,90 @@ const fullVersionLevels = generateLevelDefinitions(false);
|
||||
const demoVersionLevels = generateLevelDefinitions(true);
|
||||
|
||||
export class RegularGameMode extends GameMode {
|
||||
constructor(root) {
|
||||
super(root);
|
||||
static getId() {
|
||||
return enumGameModeIds.regular;
|
||||
}
|
||||
|
||||
static getType() {
|
||||
return enumGameModeTypes.default;
|
||||
}
|
||||
|
||||
/** @param {GameRoot} root */
|
||||
constructor(root) {
|
||||
super(root);
|
||||
|
||||
this.additionalHudParts = {
|
||||
wiresToolbar: HUDWiresToolbar,
|
||||
blueprintPlacer: HUDBlueprintPlacer,
|
||||
unlockNotification: HUDUnlockNotification,
|
||||
massSelector: HUDMassSelector,
|
||||
shop: HUDShop,
|
||||
statistics: HUDStatistics,
|
||||
waypoints: HUDWaypoints,
|
||||
wireInfo: HUDWireInfo,
|
||||
leverToggle: HUDLeverToggle,
|
||||
pinnedShapes: HUDPinnedShapes,
|
||||
notifications: HUDNotifications,
|
||||
screenshotExporter: HUDScreenshotExporter,
|
||||
wiresOverlay: HUDWiresOverlay,
|
||||
shapeViewer: HUDShapeViewer,
|
||||
layerPreview: HUDLayerPreview,
|
||||
minerHighlight: HUDMinerHighlight,
|
||||
tutorialVideoOffer: HUDTutorialVideoOffer,
|
||||
gameMenu: HUDGameMenu,
|
||||
constantSignalEdit: HUDConstantSignalEdit,
|
||||
};
|
||||
|
||||
if (!IS_MOBILE) {
|
||||
this.additionalHudParts.keybindingOverlay = HUDKeybindingOverlay;
|
||||
}
|
||||
|
||||
if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) {
|
||||
this.additionalHudParts.watermark = HUDWatermark;
|
||||
this.additionalHudParts.standaloneAdvantages = HUDStandaloneAdvantages;
|
||||
this.additionalHudParts.catMemes = HUDCatMemes;
|
||||
}
|
||||
|
||||
if (this.root.app.settings.getAllSettings().offerHints) {
|
||||
this.additionalHudParts.tutorialHints = HUDPartTutorialHints;
|
||||
this.additionalHudParts.interactiveTutorial = HUDInteractiveTutorial;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (queryParamOptions.sandboxMode || window.sandboxMode || G_IS_DEV) {
|
||||
this.additionalHudParts.sandboxController = HUDSandboxController;
|
||||
}
|
||||
|
||||
/** @type {(typeof MetaBuilding)[]} */
|
||||
this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding];
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return all available upgrades
|
||||
* @returns {Object<string, UpgradeTiers>}
|
||||
*/
|
||||
getUpgrades() {
|
||||
return this.root.app.restrictionMgr.getHasExtendedUpgrades()
|
||||
? fullVersionUpgrades
|
||||
: demoVersionUpgrades;
|
||||
}
|
||||
|
||||
getIsFreeplayAvailable() {
|
||||
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay();
|
||||
}
|
||||
|
||||
getBlueprintShapeKey() {
|
||||
return blueprintShape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the goals for all levels including their reward
|
||||
* @returns {Array<LevelDefinition>}
|
||||
*/
|
||||
getLevelDefinitions() {
|
||||
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay()
|
||||
? fullVersionLevels
|
||||
: demoVersionLevels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return whether free play is available or if the game stops
|
||||
* after the predefined levels
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIsFreeplayAvailable() {
|
||||
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,11 @@ export class GameRoot {
|
||||
*/
|
||||
this.bulkOperationRunning = false;
|
||||
|
||||
/**
|
||||
* Whether a immutable operation is running
|
||||
*/
|
||||
this.immutableOperationRunning = false;
|
||||
|
||||
//////// Other properties ///////
|
||||
|
||||
/** @type {Camera} */
|
||||
@@ -169,6 +174,7 @@ export class GameRoot {
|
||||
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()),
|
||||
|
||||
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
|
||||
immutableOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
|
||||
|
||||
editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()),
|
||||
|
||||
@@ -183,6 +189,9 @@ export class GameRoot {
|
||||
// Called with an achievement key and necessary args to validate it can be unlocked.
|
||||
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
|
||||
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()),
|
||||
|
||||
// Puzzle mode
|
||||
puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()),
|
||||
};
|
||||
|
||||
// RNG's
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user