1
0
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:
tobspr
2021-05-23 16:32:05 +02:00
committed by GitHub
parent 5f0a95ba11
commit 931c8a5821
167 changed files with 14001 additions and 8193 deletions

View File

@@ -1,6 +1,6 @@
#ingame_HUD_BetaOverlay {
position: fixed;
@include S(top, 10px);
@include S(top, 70px);
left: 50%;
transform: translateX(-50%);
color: $colorRedBright;

View File

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

View File

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

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

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

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

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

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

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

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

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

View File

@@ -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,

View File

@@ -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}"] {

View File

@@ -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;

View File

@@ -17,7 +17,7 @@
@include S(border-radius, 3px);
@include DarkThemeOverride {
background: #424242;
background: #33343c;
}
.version {

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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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,
// -----------------------------------------------------------------------------------

View File

@@ -123,4 +123,6 @@ function catchErrors(message, source, lineno, colno, error) {
return true;
}
window.onerror = catchErrors;
if (!G_IS_DEV) {
window.onerror = catchErrors;
}

View File

@@ -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");

View File

@@ -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);
}

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -11,6 +11,7 @@ export const itemTypes = ["shape", "color", "boolean"];
export class BaseItem extends BasicSerializableObject {
constructor() {
super();
this._type = this.getItemType();
}
static getId() {

View File

@@ -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) {

View File

@@ -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;
});
});
}
}

View File

@@ -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);
}

View File

@@ -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)]];
}

View 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) {}
}

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

View File

@@ -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

View File

@@ -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)]];
}

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

View File

@@ -39,6 +39,6 @@ export class MetaItemProducerBuilding extends MetaBuilding {
],
})
);
entity.addComponent(new ItemProducerComponent());
entity.addComponent(new ItemProducerComponent({}));
}
}

View File

@@ -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)]];
}

View File

@@ -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)]];
}

View File

@@ -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;

View File

@@ -110,6 +110,6 @@ export class MetaReaderBuilding extends MetaBuilding {
})
);
entity.addComponent(new BeltReaderComponent());
entity.addComponent(new BeltReaderComponent({}));
}
}

View File

@@ -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);

View File

@@ -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)]];
}

View File

@@ -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;
}
/**

View File

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

View File

@@ -23,6 +23,11 @@ export class Component extends BasicSerializableObject {
*/
copyAdditionalStateTo(otherComponent) {}
/**
* Clears all items and state
*/
clear() {}
/* dev:start */
/**

View File

@@ -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

View File

@@ -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}

View File

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

View File

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

View File

@@ -40,6 +40,10 @@ export class FilterComponent extends Component {
constructor() {
super();
this.clear();
}
clear() {
/**
* Items in queue to leave through
* @type {Array<PendingFilterItem>}

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

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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;

View File

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

View File

@@ -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 = [];
}
/**

View File

@@ -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

View File

@@ -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;
}
/**

View File

@@ -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

View File

@@ -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();

View File

@@ -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 */
}
}

View File

@@ -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";
}
}

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

View File

@@ -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");
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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
);
}
}
/**

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(

View File

@@ -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);
}

View File

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

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

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

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

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

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

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

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

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

View File

@@ -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 &rarr; Belt</label>
<button class="styledButton minus">-</button>
<button class="styledButton plus">+</button>
</div>
<div class="upgradesExtraction plusMinus">
<label>Upgrades &rarr; Extraction</label>
<button class="styledButton minus">-</button>
<button class="styledButton plus">+</button>
</div>
<div class="upgradesProcessing plusMinus">
<label>Upgrades &rarr; Processing</label>
<button class="styledButton minus">-</button>
<button class="styledButton plus">+</button>
</div>
<div class="upgradesPainting plusMinus">
<label>Upgrades &rarr; 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);
}

View File

@@ -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() {

View File

@@ -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, {

View File

@@ -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) ||

View File

@@ -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

View File

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

View File

@@ -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);
}

View File

@@ -108,9 +108,10 @@ export class MetaBuilding {
/**
* Returns whether this building is removable
* @param {GameRoot} root
* @returns {boolean}
*/
getIsRemovable() {
getIsRemovable(root) {
return true;
}

View File

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

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

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

View File

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

View File

@@ -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