* 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>
pull/1185/head
tobspr 3 years ago committed by GitHub
parent 5f0a95ba11
commit 931c8a5821
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -74,20 +74,8 @@ function createWindow() {
win.on("closed", () => {
console.log("Window closed");
win = null;
app.quit();
});
function handleWindowBeforeunload(event) {
const confirmed = dialog.showMessageBox(remote.getCurrentWindow(), options) === 1;
if (confirmed) {
remote.getCurrentWindow().close();
} else {
event.returnValue = false;
}
}
win.on("", handleWindowBeforeunload);
if (isDev) {
menu = new Menu();

@ -10,10 +10,10 @@
"start": "electron --disable-direct-composition --in-process-gpu ."
},
"devDependencies": {
"electron": "10.4.0"
"electron": "10.4.3"
},
"optionalDependencies": {
"shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v85"
"shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v82"
},
"dependencies": {
"async-lock": "^1.2.8"

@ -1,5 +1,5 @@
const fs = require('fs');
const path = require('path');
const fs = require("fs");
const path = require("path");
const { ipcMain } = require("electron");
let greenworks = null;
@ -11,10 +11,10 @@ try {
appId = parseInt(fs.readFileSync(path.join(__dirname, "steam_appid.txt"), "utf8"));
} catch (err) {
// greenworks is not installed
// throw err;
console.warn("Failed to load steam api:", err);
}
function init (isDev) {
function init(isDev) {
if (!greenworks) {
return;
}
@ -34,11 +34,16 @@ function init (isDev) {
initialized = true;
}
function listen () {
function listen() {
ipcMain.handle("steam:is-initialized", isInitialized);
if (!greenworks || !initialized) {
console.log("Ignoring Steam IPC events");
if (!initialized) {
console.warn("Steam not initialized, won't be able to listen");
return;
}
if (!greenworks) {
console.warn("Greenworks not loaded, won't be able to listen");
return;
}
@ -53,7 +58,7 @@ function isInitialized(event) {
function getAchievementNames(event) {
return new Promise((resolve, reject) => {
try {
const achievements = greenworks.getAchievementNames()
const achievements = greenworks.getAchievementNames();
resolve(achievements);
} catch (err) {
reject(err);
@ -63,11 +68,15 @@ function getAchievementNames(event) {
function activateAchievement(event, id) {
return new Promise((resolve, reject) => {
greenworks.activateAchievement(id, () => resolve(), err => reject(err))
greenworks.activateAchievement(
id,
() => resolve(),
err => reject(err)
);
});
}
module.exports = {
init,
listen
listen,
};

@ -146,10 +146,10 @@ duplexer3@^0.1.4:
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
electron@10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.4.0.tgz#018385914474b56110a5a43087a53c114b67c08d"
integrity sha512-qK8OOCWuNvEFWThmjkukkqDwIpBqULlDuMXVC9MC/2P4UaWJEjIYvBmBuTyxtFcKoE3kWvcWyeRDUuvzVxxXjA==
electron@10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.4.3.tgz#8d1c0f5e562d1b78dcec8074c0d59e58137fd508"
integrity sha512-qL8XZBII9KQHr1+YmVMj1AqyTR2I8/lxozvKEWoKKSkF8Hl6GzzxrLXRfISP7aDAvsJEyyhc6b2/42ME8hG5JA==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^12.0.12"
@ -503,9 +503,9 @@ serialize-error@^7.0.1:
dependencies:
type-fest "^0.13.1"
"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v85":
"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v82":
version "0.1.0"
resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#63adf7e0ea4b90c2a29053ce1f0ec9d573b3ac0a"
resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#8aa3bfd3b569eb5695fc8a585a3f2ee3ed2db290"
sprintf-js@^1.1.2:
version "1.1.2"

@ -54,8 +54,11 @@
document.documentElement.appendChild(element);
}
window.addEventListener("error", errorHandler);
window.addEventListener("unhandledrejection", errorHandler);
if (window.location.host.indexOf("localhost") < 0) {
window.addEventListener("error", errorHandler);
window.addEventListener("unhandledrejection", errorHandler);
}
function makeJsTag(src, integrity) {
var script = document.createElement("script");

@ -40,7 +40,7 @@ module.exports = ({
G_ALL_UI_IMAGES: JSON.stringify(getAllResourceImages()),
};
const minifyNames = environment === "prod";
const minifyNames = false;
return {
mode: "production",

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<rect style="fill:#41479B;" width="512" height="512"/>
<rect y="93.86" style="fill:#F5F5F5;" width="512" height="324.28"/>
<path style="fill:#41479B;" d="M317.474,256l30.734-53.234h-61.469L256,149.523l-30.739,53.243h-61.469L194.526,256l-30.734,53.234
h61.469L256,362.477l30.739-53.243h61.469L317.474,256z M318.054,220.176l-10.632,18.415l-10.632-18.415H318.054z M297.371,256
l-20.683,35.824h-41.376L214.629,256l20.683-35.824h41.376L297.371,256z M256,184.344l10.636,18.422h-21.272L256,184.344z
M193.946,220.176h21.264l-10.632,18.415L193.946,220.176z M193.946,291.824l10.632-18.415l10.632,18.415H193.946z M256,327.656
l-10.636-18.422h21.272L256,327.656z M307.423,273.409l10.632,18.415h-21.264L307.423,273.409z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -41,7 +41,7 @@ def process_image(data, outfilename, src_image):
if isWire:
targetR = 255
targetG = 104
targetB = 232
targetB = 232
for x in range(img.width):
for y in range(img.height):
@ -85,6 +85,8 @@ def generate_blueprint_sprite(infilename, outfilename):
buildings = listdir("buildings")
for buildingId in buildings:
if not ".png" in buildingId:
continue
if "hub" in buildingId:
continue
if "wire-" in buildingId:

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

@ -37,7 +37,7 @@
.building {
@include S(width, 30px);
@include S(height, 22px);
@include S(height, 30px);
background-size: 45%;
&:not(.unlocked) {
@ -49,63 +49,97 @@
}
.building {
color: $accentColorDark;
display: flex;
flex-direction: column;
@include S(width, 40px);
position: relative;
align-items: center;
justify-content: center;
@include S(padding, 5px);
@include S(padding-bottom, 1px);
@include S(width, 35px);
@include S(height, 40px);
background: center center / 70% no-repeat;
.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;
}
&:not(.unlocked) {
@include S(width, 20px);
opacity: 0.15;
background-image: none !important;
@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;
}
}
}
&::before {
content: " ";
&.unlocked {
.icon {
pointer-events: all;
transition: all 50ms ease-in-out;
transition-property: background-color, transform;
cursor: pointer;
&:hover {
background-color: rgba(30, 40, 90, 0.1);
}
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 4;
&.pressed {
transform: scale(0.9) !important;
}
&.selected {
// transform: scale(1.05);
background-color: rgba(lighten($colorBlueBright, 9), 0.4);
.keybinding {
color: #111;
}
}
}
.puzzle-lock {
& {
/* @load-async */
background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)}
background: uiResource("locked_building.png") center center / #{D(14px)} #{D(14px)}
no-repeat;
}
}
}
@include S(border-radius, $globalBorderRadius);
&.unlocked {
pointer-events: all;
transition: all 50ms ease-in-out;
transition-property: background-color, transform;
display: grid;
grid-auto-flow: column;
cursor: pointer;
&:hover {
background-color: rgba(30, 40, 90, 0.1);
}
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%) !important;
transition: all 0.12s ease-in-out;
transition-property: opacity, transform;
&.pressed {
transform: scale(0.9) !important;
}
cursor: pointer;
pointer-events: all;
&.selected {
// transform: scale(1.05);
background-color: rgba(lighten($colorBlueBright, 9), 0.4);
@include S(width, 14px);
@include S(height, 14px);
.keybinding {
color: #111;
&:hover {
opacity: 0.5;
}
}
}

@ -67,6 +67,14 @@
* {
color: #fff;
}
display: flex;
flex-direction: column;
.text {
text-transform: uppercase;
@include S(margin-bottom, 10px);
}
}
> .dialogInner {
@ -168,6 +176,11 @@
&.errored {
background-color: rgb(250, 206, 206);
&::placeholder {
color: #fff;
opacity: 0.8;
}
}
}

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

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

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

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

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

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

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

@ -0,0 +1,23 @@
#ingame_HUD_PuzzlePlaySettings {
position: absolute;
background: $ingameHudBg;
@include S(padding, 10px);
@include S(bottom, 60px);
@include S(left, 10px);
@include SuperSmallText;
color: #eee;
display: flex;
flex-direction: column;
@include S(border-radius, $globalBorderRadius);
> .section {
display: grid;
@include S(grid-gap, 10px);
grid-auto-flow: row;
> button {
@include SuperSmallText;
}
}
}

@ -29,6 +29,7 @@
@import "states/about";
@import "states/mobile_warning";
@import "states/changelog";
@import "states/puzzle_menu";
@import "ingame_hud/buildings_toolbar";
@import "ingame_hud/building_placer";
@ -55,12 +56,21 @@
@import "ingame_hud/sandbox_controller";
@import "ingame_hud/standalone_advantages";
@import "ingame_hud/cat_memes";
@import "ingame_hud/puzzle_back_to_menu";
@import "ingame_hud/puzzle_editor_review";
@import "ingame_hud/puzzle_dlc_logo";
@import "ingame_hud/puzzle_editor_controls";
@import "ingame_hud/puzzle_editor_settings";
@import "ingame_hud/puzzle_play_settings";
@import "ingame_hud/puzzle_play_metadata";
@import "ingame_hud/puzzle_complete_notification";
// prettier-ignore
$elements:
// Base
ingame_Canvas,
ingame_VignetteOverlay,
ingame_HUD_PuzzleDLCLogo,
// Ingame overlays
ingame_HUD_Waypoints,
@ -71,6 +81,14 @@ ingame_HUD_PlacerVariants,
ingame_HUD_PinnedShapes,
ingame_HUD_GameMenu,
ingame_HUD_KeybindingOverlay,
ingame_HUD_PuzzleBackToMenu,
ingame_HUD_PuzzleEditorReview,
ingame_HUD_PuzzleEditorControls,
ingame_HUD_PuzzleEditorTitle,
ingame_HUD_PuzzleEditorSettings,
ingame_HUD_PuzzlePlaySettings,
ingame_HUD_PuzzlePlayMetadata,
ingame_HUD_PuzzlePlayTitle,
ingame_HUD_Notifications,
ingame_HUD_DebugInfo,
ingame_HUD_EntityDebugger,
@ -94,6 +112,7 @@ ingame_HUD_Statistics,
ingame_HUD_ShapeViewer,
ingame_HUD_StandaloneAdvantages,
ingame_HUD_UnlockNotification,
ingame_HUD_PuzzleCompleteNotification,
ingame_HUD_SettingsMenu,
ingame_HUD_ModalDialogs,
ingame_HUD_CatMemes;
@ -113,6 +132,8 @@ body.uiHidden {
#ingame_HUD_PlacementHints,
#ingame_HUD_GameMenu,
#ingame_HUD_PinnedShapes,
#ingame_HUD_PuzzleBackToMenu,
#ingame_HUD_PuzzleEditorReview,
#ingame_HUD_Notifications,
#ingame_HUD_TutorialHints,
#ingame_HUD_Waypoints,

@ -1,11 +1,13 @@
$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire,
constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage,
transistor, analyzer, comparator, item_producer;
transistor, analyzer, comparator, item_producer, constant_producer, goal_acceptor, block;
@each $building in $buildings {
[data-icon="building_icons/#{$building}.png"] {
/* @load-async */
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
.icon {
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
}
}
}
@ -13,7 +15,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2,
cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage,
reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not,
logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, item_producer,
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator;
constant_producer, virtual_processor-stacker, virtual_processor-painter, wire-second, painter,
painter-mirrored, comparator, goal_acceptor, block;
@each $building in $buildingsAndVariants {
[data-icon="building_tutorials/#{$building}.png"] {
/* @load-async */
@ -67,7 +70,7 @@ $icons: notification_saved, notification_success, notification_upgrade;
}
$languages: en, de, cs, da, et, es-419, fr, it, pt-BR, sv, tr, el, ru, uk, zh-TW, zh-CN, nb, mt-MT, ar, nl, vi,
th, hu, pl, ja, kor, no, pt-PT, fi, ro;
th, hu, pl, ja, kor, no, pt-PT, fi, ro, he;
@each $language in $languages {
[data-languageicon="#{$language}"] {

@ -88,11 +88,7 @@
@include S(grid-column-gap, 10px);
display: grid;
grid-template-columns: 1fr;
&.demo {
grid-template-columns: 1fr 1fr;
}
grid-template-columns: 1fr 1fr;
.standaloneBanner {
background: rgb(255, 75, 84);
@ -183,7 +179,7 @@
.updateLabel {
position: absolute;
transform: translateX(50%) rotate(-5deg);
color: #3291e9;
color: #ff590b;
@include Heading;
font-weight: bold;
@include S(right, 40px);
@ -223,9 +219,33 @@
}
}
.puzzleContainer {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: #4cc98a;
grid-row: 1 / 2;
grid-column: 2 / 3;
@include S(padding, 20px);
@include S(border-radius, $globalBorderRadius);
> .dlcLogo {
@include S(width, 200px);
}
> button {
@include S(margin-top, 20px);
@include Heading;
@include S(padding, 10px, 30px);
background-color: #333;
color: #fff;
}
}
.mainContainer {
display: flex;
align-items: center;
grid-row: 1 / 2;
justify-content: center;
flex-direction: column;
background: #fafafa;
@ -242,6 +262,16 @@
align-items: center;
}
.modeButtons {
display: grid;
grid-template-columns: repeat(2, 1fr);
@include S(grid-column-gap, 10px);
align-items: start;
height: 100%;
width: 100%;
box-sizing: border-box;
}
.browserWarning {
@include S(margin-bottom, 10px);
background-color: $colorRedBright;
@ -285,6 +315,18 @@
@include S(margin-left, 15px);
}
.playModeButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
}
.editModeButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
}
.savegames {
@include S(max-height, 105px);
overflow-y: auto;
@ -439,6 +481,27 @@
}
}
.bottomContainer {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
@include S(padding-top, 10px);
height: 100%;
width: 100%;
box-sizing: border-box;
.buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
@include S(grid-column-gap, 10px);
align-items: start;
height: 100%;
width: 100%;
box-sizing: border-box;
}
}
.footer {
display: grid;
flex-grow: 1;

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

@ -0,0 +1,277 @@
#state_PuzzleMenuState {
> .headerBar {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
> h1 {
justify-self: start;
}
.createPuzzle {
background-color: $colorGreenBright;
@include S(margin-left, 5px);
}
}
> .container {
> .mainContent {
overflow: hidden;
> .categoryChooser {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
@include S(grid-gap, 2px);
@include S(padding-right, 10px);
> .category {
background: $accentColorBright;
border-radius: 0;
color: $accentColorDark;
transition: all 0.12s ease-in-out;
transition-property: opacity, background-color, color;
&:first-child {
@include S(border-top-left-radius, $globalBorderRadius);
@include S(border-bottom-left-radius, $globalBorderRadius);
}
&:last-child {
border-top-right-radius: $globalBorderRadius;
border-bottom-right-radius: $globalBorderRadius;
}
&.active {
background: $colorBlueBright;
opacity: 1 !important;
color: #fff;
cursor: default;
}
@include DarkThemeOverride {
background: $accentColorDark;
color: #bbbbc4;
&.active {
background: $colorBlueBright;
color: #fff;
}
}
}
}
> .puzzles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(D(180px), 1fr));
@include S(grid-auto-rows, 65px);
@include S(grid-gap, 7px);
@include S(margin-top, 10px);
@include S(padding-right, 4px);
@include S(height, 360px);
overflow-y: scroll;
pointer-events: all;
position: relative;
> .puzzle {
width: 100%;
@include S(height, 65px);
background: #f3f3f8;
@include S(border-radius, $globalBorderRadius);
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: D(15px) D(15px) 1fr;
@include S(padding, 5px);
@include S(grid-column-gap, 5px);
box-sizing: border-box;
pointer-events: all;
cursor: pointer;
position: relative;
@include S(padding-left, 10px);
@include DarkThemeOverride {
background: rgba(0, 0, 10, 0.2);
}
@include InlineAnimation(0.12s ease-in-out) {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
&:hover {
background: #f0f0f8;
}
> .title {
grid-column: 2 / 3;
grid-row: 1 / 2;
@include PlainText;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
align-self: center;
justify-self: start;
width: 100%;
box-sizing: border-box;
@include S(padding, 2px, 5px);
@include S(height, 17px);
}
> .author {
grid-column: 2 / 2;
grid-row: 2 / 3;
@include SuperSmallText;
color: $accentColorDark;
align-self: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@include S(padding, 2px, 5px);
}
> .icon {
grid-column: 1 / 2;
grid-row: 1 / 4;
align-self: center;
justify-self: center;
@include S(width, 45px);
@include S(height, 45px);
canvas {
width: 100%;
height: 100%;
}
}
> .stats {
grid-column: 2 / 3;
grid-row: 3 / 4;
display: flex;
align-items: center;
justify-self: end;
justify-content: center;
align-self: end;
@include S(height, 14px);
> .downloads {
@include SuperSmallText;
color: #000;
font-weight: bold;
@include S(margin-right, 5px);
@include S(padding-left, 12px);
opacity: 0.7;
display: inline-flex;
align-items: center;
justify-content: center;
@include DarkThemeInvert;
& {
/* @load-async */
background: uiResource("icons/puzzle_plays.png") #{D(2px)} #{D(2.5px)} / #{D(
8px
)} #{D(8px)} no-repeat;
}
}
> .likes {
@include SuperSmallText;
align-items: center;
justify-content: center;
color: #000;
font-weight: bold;
@include S(padding-left, 14px);
opacity: 0.7;
@include DarkThemeInvert;
& {
/* @load-async */
background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} #{D(2.4px)} / #{D(
9px
)} #{D(9px)} no-repeat;
}
}
> .difficulty {
@include SuperSmallText;
align-items: center;
justify-content: center;
color: #000;
font-weight: bold;
@include S(margin-right, 3px);
opacity: 0.7;
&.stage--easy {
color: $colorGreenBright;
}
&.stage--normal {
color: #000;
@include DarkThemeInvert;
}
&.stage--medium {
color: $colorOrangeBright;
}
&.stage--hard {
color: $colorRedBright;
}
}
}
&.completed {
> .icon,
> .stats,
> .author,
> .title {
opacity: 0.3;
}
background: #fafafa;
@include DarkThemeOverride {
background: rgba(0, 0, 0, 0.05);
}
&::after {
content: "";
position: absolute;
@include S(top, 10px);
@include S(right, 10px);
@include S(width, 30px);
@include S(height, 30px);
opacity: 0.1;
& {
/* @load-async */
background: uiResource("icons/puzzle_complete_indicator.png") center center /
contain no-repeat;
}
}
@include DarkThemeOverride {
&::after {
/* @load-async */
background: uiResource("icons/puzzle_complete_indicator_inverse.png") center
center / contain no-repeat;
}
}
}
}
> .loader,
> .empty {
display: flex;
align-items: center;
color: $accentColorDark;
justify-content: center;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
}
}
}

@ -18,8 +18,10 @@ $textLineHeight: 21px;
$plainTextFontSize: 13px;
$plainTextLineHeight: 17px;
$supersmallTextFontSize: 10px;
$supersmallTextLineHeight: 13px;
$superDuperSmallTextFontSize: 8px;
$superDuperSmallTextLineHeight: 9px;
$superSmallTextFontSize: 10px;
$superSmallTextLineHeight: 13px;
$buttonFontSize: 14px;
$buttonLineHeight: 18px;
@ -33,6 +35,7 @@ $accentColorDark: #7d808a;
$colorGreenBright: #66bb6a;
$colorBlueBright: rgb(74, 151, 223);
$colorRedBright: #ef5072;
$colorOrangeBright: #ef9d50;
$themeColor: #393747;
$ingameHudBg: rgba(#333438, 0.9);
@ -76,8 +79,16 @@ $mainFontScale: 1;
// }
}
@mixin SuperDuperSmallText {
@include ScaleFont($superDuperSmallTextFontSize, $superDuperSmallTextLineHeight);
font-weight: $mainFontWeight;
font-family: $mainFont;
letter-spacing: $mainFontSpacing;
@include DebugText(green);
}
@mixin SuperSmallText {
@include ScaleFont($supersmallTextFontSize, $supersmallTextLineHeight);
@include ScaleFont($superSmallTextFontSize, $superSmallTextLineHeight);
font-weight: $mainFontWeight;
font-family: $mainFont;
letter-spacing: $mainFontSpacing;

@ -31,6 +31,9 @@ import { PreloadState } from "./states/preload";
import { SettingsState } from "./states/settings";
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
import { RestrictionManager } from "./core/restriction_manager";
import { PuzzleMenuState } from "./states/puzzle_menu";
import { ClientAPI } from "./platform/api";
import { LoginState } from "./states/login";
/**
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
@ -72,6 +75,7 @@ export class Application {
this.savegameMgr = new SavegameManager(this);
this.inputMgr = new InputDistributor(this);
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
this.clientApi = new ClientAPI(this);
// Restrictions (Like demo etc)
this.restrictionMgr = new RestrictionManager(this);
@ -159,6 +163,8 @@ export class Application {
KeybindingsState,
AboutState,
ChangelogState,
PuzzleMenuState,
LoginState,
];
for (let i = 0; i < states.length; ++i) {

@ -1,12 +1,32 @@
export const CHANGELOG = [
{
version: "1.3.1",
date: "beta",
version: "1.4.0",
date: "UNRELEASED",
entries: [
"Fixed savegames getting corrupt in rare conditions",
"Fixed game crashing sometimes since the achievements update",
"Added puzzle mode",
"Belts in blueprints should now always paste correctly",
"You can now clear belts by selecting them, and then pressing 'B'",
],
},
{
version: "1.3.1",
date: "16.04.2021",
entries: G_CHINA_VERSION
? [
"第13关的交付目标更改为中国古代指南针。感谢玩家凯风入心 创作并提供",
"第17关的交付目标更改为永乐通宝。感谢玩家金天赐 创作并提供",
"第22关的交付目标更改为凤凰。感谢玩家我没得眼镜 创作并提供",
"第23关的交付目标更改为古代车轮。感谢玩家我没得眼镜 创作并提供",
"第24关的交付目标更改为大熊猫。感谢玩家窝囸倪现任 创作并提供",
"修复了一些特定情况下偶尔会发生的存档损坏问题",
"修复了成就更新后有时候游戏崩溃的问题",
]
: [
"Fixed savegames getting corrupt in rare conditions",
"Fixed game crashing sometimes since the achievements update",
],
},
{
version: "1.3.0",
date: "12.03.2020",

@ -51,9 +51,12 @@ export class AnimationFrame {
dt = resetDtMs;
}
this.frameEmitted.dispatch(dt);
try {
this.frameEmitted.dispatch(dt);
} catch (ex) {
console.error(ex);
}
this.lastTime = time;
window.requestAnimationFrame(this.boundMethod);
}
}

@ -71,6 +71,13 @@ export const globalConfig = {
readerAnalyzeIntervalSeconds: 10,
goalAcceptorMinimumDurationSeconds: 5,
goalAcceptorsPerProducer: 4.5,
puzzleModeSpeed: 3,
puzzleMinBoundsSize: 2,
puzzleMaxBoundsSize: 20,
puzzleValidationDurationSeconds: 30,
buildingSpeeds: {
cutter: 1 / 4,
cutterQuad: 1 / 4,
@ -93,7 +100,7 @@ export const globalConfig = {
gameSpeed: 1,
warmupTimeSecondsFast: 0.5,
warmupTimeSecondsRegular: 3,
warmupTimeSecondsRegular: 1.5,
smoothing: {
smoothMainCanvas: smoothCanvas && true,

@ -62,6 +62,9 @@ export default {
// Allows unlocked achievements to be logged to console in the local build
// testAchievements: true,
// -----------------------------------------------------------------------------------
// Enables use of (some) existing flags within the puzzle mode context
// testPuzzleMode: true,
// -----------------------------------------------------------------------------------
// Disables the automatic switch to an overview when zooming out
// disableMapOverview: true,
// -----------------------------------------------------------------------------------

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

@ -5,6 +5,7 @@ import { Factory } from "./factory";
* @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed
* @typedef {import("../game/component").Component} Component
* @typedef {import("../game/base_item").BaseItem} BaseItem
* @typedef {import("../game/game_mode").GameMode} GameMode
* @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding
@ -19,6 +20,9 @@ export let gBuildingsByCategory = null;
/** @type {FactoryTemplate<Component>} */
export let gComponentRegistry = new Factory("component");
/** @type {FactoryTemplate<GameMode>} */
export let gGameModeRegistry = new Factory("gameMode");
/** @type {FactoryTemplate<BaseGameSpeed>} */
export let gGameSpeedRegistry = new Factory("gamespeed");

@ -267,7 +267,7 @@ export class Dialog {
* Dialog which simply shows a loading spinner
*/
export class DialogLoading extends Dialog {
constructor(app) {
constructor(app, text = "") {
super({
app,
title: "",
@ -279,6 +279,8 @@ export class DialogLoading extends Dialog {
// Loading dialog can not get closed with back button
this.inputReciever.backButton.removeAll();
this.inputReciever.context = "dialog-loading";
this.text = text;
}
createElement() {
@ -287,6 +289,13 @@ export class DialogLoading extends Dialog {
elem.classList.add("loadingDialog");
this.element = elem;
if (this.text) {
const text = document.createElement("div");
text.classList.add("text");
text.innerText = this.text;
elem.appendChild(text);
}
const loader = document.createElement("div");
loader.classList.add("prefab_LoadingTextWithAnim");
loader.classList.add("loadingIndicator");
@ -309,7 +318,7 @@ export class DialogOptionChooser extends Dialog {
<div class='option ${value === options.active ? "active" : ""} ${
iconPrefix ? "hasIcon" : ""
}' data-optionvalue='${value}'>
${iconHtml}
${iconHtml}
<span class='title'>${text}</span>
${descHtml}
</div>
@ -444,7 +453,7 @@ export class DialogWithForm extends Dialog {
for (let i = 0; i < this.formElements.length; ++i) {
const elem = this.formElements[i];
elem.bindEvents(div, this.clickDetectors);
elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
// elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
}

@ -117,6 +117,11 @@ export class FormElementInput extends FormElement {
return this.element.value;
}
setValue(value) {
this.element.value = value;
this.updateErrorState();
}
focus() {
this.element.focus();
}

@ -44,6 +44,15 @@ export class Rectangle {
return new Rectangle(left, top, right - left, bottom - top);
}
/**
*
* @param {number} width
* @param {number} height
*/
static centered(width, height) {
return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height);
}
/**
* Returns if a intersects b
* @param {Rectangle} a
@ -72,7 +81,7 @@ export class Rectangle {
/**
* Returns if this rectangle is equal to the other while taking an epsilon into account
* @param {Rectangle} other
* @param {number} epsilon
* @param {number} [epsilon]
*/
equalsEpsilon(other, epsilon) {
return (
@ -287,6 +296,15 @@ export class Rectangle {
return Rectangle.fromTRBL(top, right, bottom, left);
}
/**
* Returns whether the rectangle fully intersects the given rectangle
* @param {Rectangle} rect
*/
intersectsFully(rect) {
const intersection = this.getIntersection(rect);
return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001;
}
/**
* Returns the union of this rectangle with another
* @param {Rectangle} rect

@ -17,6 +17,17 @@ export class Signal {
++this.modifyCount;
}
/**
* Adds a new signal listener
* @param {function} receiver
* @param {object} scope
*/
addToTop(receiver, scope = null) {
assert(receiver, "receiver is null");
this.receivers.unshift({ receiver, scope });
++this.modifyCount;
}
/**
* Dispatches the signal
* @param {...any} payload

@ -90,9 +90,9 @@ export class StateManager {
dialogParent.classList.add("modalDialogParent");
document.body.appendChild(dialogParent);
this.currentState.internalEnterCallback(payload);
this.app.sound.playThemeMusic(this.currentState.getThemeMusic());
this.currentState.internalEnterCallback(payload);
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
this.app.analytics.trackStateEnter(key);

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

@ -13,8 +13,6 @@ import { GameRoot } from "./root";
const logger = createLogger("belt_path");
// Helpers for more semantic access into interleaved arrays
const _nextDistance = 0;
const _item = 1;
const DEBUG = G_IS_DEV && false;
@ -110,6 +108,15 @@ export class BeltPath extends BasicSerializableObject {
}
}
/**
* Clears all items
*/
clearAllItems() {
this.items = [];
this.spacingToFirstItem = this.totalLength;
this.numCompressedItemsAfterFirstItem = 0;
}
/**
* Returns whether this path can accept a new item
* @returns {boolean}
@ -174,7 +181,7 @@ export class BeltPath extends BasicSerializableObject {
* Recomputes cache variables once the path was changed
*/
onPathChanged() {
this.acceptorTarget = this.computeAcceptingEntityAndSlot();
this.boundAcceptor = this.computeAcceptingEntityAndSlot();
/**
* How many items past the first item are compressed
@ -192,7 +199,7 @@ export class BeltPath extends BasicSerializableObject {
/**
* Finds the entity which accepts our items
* @param {boolean=} debug_Silent Whether debug output should be silent
* @return {{ entity: Entity, slot: number, direction?: enumDirection }}
* @return { (BaseItem, number) => boolean }
*/
computeAcceptingEntityAndSlot(debug_Silent = false) {
DEBUG && !debug_Silent && logger.log("Recomputing acceptor target");
@ -214,55 +221,142 @@ export class BeltPath extends BasicSerializableObject {
"regular"
);
if (targetEntity) {
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
const targetStaticComp = targetEntity.components.StaticMapEntity;
const targetBeltComp = targetEntity.components.Belt;
if (!targetEntity) {
return;
}
const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts;
// 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
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;
// NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS
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);
};
}
const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection);
const matchingSlot = targetAcceptorComp.findMatchingSlot(
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
ejectingDirection
);
const undergroundBeltComp = entity.components.UndergroundBelt;
if (undergroundBeltComp) {
// Its an underground belt. yay.
return function (item) {
return undergroundBeltComp.tryAcceptExternalItem(
item,
hubGoals.getUndergroundBeltBaseSpeed()
);
};
}
if (!matchingSlot) {
// No matching slot found
return;
}
const storageComp = entity.components.Storage;
if (storageComp) {
// It's a storage
return function (item) {
if (storageComp.canAcceptItem(item)) {
storageComp.takeItem(item);
return true;
}
};
}
return {
entity: targetEntity,
slot: matchingSlot.index,
direction: enumInvertedDirections[ejectingDirection],
const filterComp = entity.components.Filter;
if (filterComp) {
// It's a filter! Unfortunately the filter has to know a lot about it's
// surrounding state and components, so it can't be within the component itself.
return function (item) {
if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) {
return true;
}
};
}
}
@ -365,17 +459,17 @@ export class BeltPath extends BasicSerializableObject {
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) {
if (item[0 /* nextDistance */] < 0 || item[0 /* nextDistance */] > this.totalLength + 0.02) {
return fail(
"Item has invalid offset to next item: ",
item[_nextDistance],
item[0 /* nextDistance */],
"(total length:",
this.totalLength,
")"
);
}
currentPos += item[_nextDistance];
currentPos += item[0 /* nextDistance */];
}
// Check the total sum matches
@ -387,7 +481,7 @@ export class BeltPath extends BasicSerializableObject {
this.spacingToFirstItem,
") and items does not match total length (",
this.totalLength,
") -> items: " + this.items.map(i => i[_nextDistance]).join("|")
") -> items: " + this.items.map(i => i[0 /* nextDistance */]).join("|")
);
}
@ -399,43 +493,14 @@ export class BeltPath extends BasicSerializableObject {
// Check acceptor
const acceptor = this.computeAcceptingEntityAndSlot(true);
if (!!acceptor !== !!this.acceptorTarget) {
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.acceptorTarget);
}
if (acceptor) {
if (this.acceptorTarget.entity !== acceptor.entity) {
return fail(
"Mismatching entity on acceptor target:",
acceptor.entity.uid,
"vs",
this.acceptorTarget.entity.uid
);
}
if (this.acceptorTarget.slot !== acceptor.slot) {
return fail(
"Mismatching entity on acceptor target:",
acceptor.slot,
"vs stored",
this.acceptorTarget.slot
);
}
if (this.acceptorTarget.direction !== acceptor.direction) {
return fail(
"Mismatching direction on acceptor target:",
acceptor.direction,
"vs stored",
this.acceptorTarget.direction
);
}
if (!!acceptor !== !!this.boundAcceptor) {
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.boundAcceptor);
}
// Check first nonzero offset
let firstNonzero = 0;
for (let i = this.items.length - 2; i >= 0; --i) {
if (this.items[i][_nextDistance] < globalConfig.itemSpacingOnBelts + 1e-5) {
if (this.items[i][0 /* nextDistance */] < globalConfig.itemSpacingOnBelts + 1e-5) {
++firstNonzero;
} else {
break;
@ -483,11 +548,11 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
" Extended spacing of last item from",
lastItem[_nextDistance],
lastItem[0 /* nextDistance */],
"to",
lastItem[_nextDistance] + additionalLength
lastItem[0 /* nextDistance */] + additionalLength
);
lastItem[_nextDistance] += additionalLength;
lastItem[0 /* nextDistance */] += additionalLength;
}
// Assign reference
@ -618,7 +683,7 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
"Old items are",
this.items.map(i => i[_nextDistance])
this.items.map(i => i[0 /* nextDistance */])
);
// Create second path
@ -628,7 +693,7 @@ export class BeltPath extends BasicSerializableObject {
let itemPos = this.spacingToFirstItem;
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
const distanceToNext = item[_nextDistance];
const distanceToNext = item[0 /* nextDistance */];
DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next");
@ -643,7 +708,7 @@ export class BeltPath extends BasicSerializableObject {
// Check if its on the second path (otherwise its on the removed belt and simply lost)
if (itemPos >= secondPathStart) {
// Put item on second path
secondPath.items.push([distanceToNext, item[_item]]);
secondPath.items.push([distanceToNext, item[1 /* item */]]);
DEBUG &&
logger.log(
" Put item to second path @",
@ -672,7 +737,7 @@ export class BeltPath extends BasicSerializableObject {
"to",
clampedDistanceToNext
);
item[_nextDistance] = clampedDistanceToNext;
item[0 /* nextDistance */] = clampedDistanceToNext;
}
}
@ -683,13 +748,13 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
"New items are",
this.items.map(i => i[_nextDistance])
this.items.map(i => i[0 /* nextDistance */])
);
DEBUG &&
logger.log(
"And second path items are",
secondPath.items.map(i => i[_nextDistance])
secondPath.items.map(i => i[0 /* nextDistance */])
);
// Adjust our total length
@ -776,9 +841,17 @@ export class BeltPath extends BasicSerializableObject {
continue;
}
DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]);
DEBUG &&
logger.log(
"Item",
i,
"is at",
itemOffset,
"with next offset",
item[0 /* nextDistance */]
);
lastItemOffset = itemOffset;
itemOffset += item[_nextDistance];
itemOffset += item[0 /* nextDistance */];
}
// If we still have an item, make sure the last item matches
@ -805,7 +878,7 @@ export class BeltPath extends BasicSerializableObject {
this.totalLength,
")"
);
this.items[this.items.length - 1][_nextDistance] = lastDistance;
this.items[this.items.length - 1][0 /* nextDistance */] = lastDistance;
} else {
DEBUG && logger.log(" Removed all items so we'll update spacing to total length");
@ -893,7 +966,7 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
" Items:",
this.items.map(i => i[_nextDistance])
this.items.map(i => i[0 /* nextDistance */])
);
// Find offset to first item
@ -912,7 +985,7 @@ export class BeltPath extends BasicSerializableObject {
// This item must be dropped
this.items.splice(i, 1);
i -= 1;
itemOffset += item[_nextDistance];
itemOffset += item[0 /* nextDistance */];
continue;
} else {
// This item can be kept, thus its the first we know
@ -990,9 +1063,13 @@ export class BeltPath extends BasicSerializableObject {
// Now, update the distance of our last item
if (this.items.length !== 0) {
const lastItem = this.items[this.items.length - 1];
lastItem[_nextDistance] += otherPath.spacingToFirstItem;
lastItem[0 /* nextDistance */] += otherPath.spacingToFirstItem;
DEBUG &&
logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now");
logger.log(
" Add distance to last item, effectively being",
lastItem[0 /* nextDistance */],
"now"
);
} else {
// Seems we have no items, update our first item distance
this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem;
@ -1012,7 +1089,7 @@ export class BeltPath extends BasicSerializableObject {
// Aaand push the other paths items
for (let i = 0; i < otherPath.items.length; ++i) {
const item = otherPath.items[i];
this.items.push([item[_nextDistance], item[_item]]);
this.items.push([item[0 /* nextDistance */], item[1 /* item */]]);
}
// Update bounds
@ -1046,6 +1123,11 @@ export class BeltPath extends BasicSerializableObject {
this.debug_checkIntegrity("pre-update");
}
// Skip empty belts
if (this.items.length === 0) {
return;
}
// Divide by item spacing on belts since we use throughput and not speed
let beltSpeed =
this.root.hubGoals.getBeltBaseSpeed() *
@ -1074,30 +1156,40 @@ export class BeltPath extends BasicSerializableObject {
lastItemProcessed === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts;
// Compute how much we can advance
const clampedProgress = Math.max(
0,
Math.min(remainingVelocity, nextDistanceAndItem[_nextDistance] - minimumSpacing)
);
let clampedProgress = nextDistanceAndItem[0 /* nextDistance */] - minimumSpacing;
// Make sure we don't advance more than the remaining velocity has stored
if (remainingVelocity < clampedProgress) {
clampedProgress = remainingVelocity;
}
// Make sure we don't advance back
if (clampedProgress < 0) {
clampedProgress = 0;
}
// Reduce our velocity by the amount we consumed
remainingVelocity -= clampedProgress;
// Reduce the spacing
nextDistanceAndItem[_nextDistance] -= clampedProgress;
nextDistanceAndItem[0 /* nextDistance */] -= clampedProgress;
// Advance all items behind by the progress we made
this.spacingToFirstItem += clampedProgress;
// If the last item can be ejected, eject it and reduce the spacing, because otherwise
// we lose velocity
if (isFirstItemProcessed && nextDistanceAndItem[_nextDistance] < 1e-7) {
if (isFirstItemProcessed && nextDistanceAndItem[0 /* nextDistance */] < 1e-7) {
// Store how much velocity we "lost" because we bumped the item to the end of the
// belt but couldn't move it any farther. We need this to tell the item acceptor
// animation to start a tad later, so everything matches up. Yes I'm a perfectionist.
const excessVelocity = beltSpeed - clampedProgress;
// Try to directly get rid of the item
if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) {
if (
this.boundAcceptor &&
this.boundAcceptor(nextDistanceAndItem[1 /* item */], excessVelocity)
) {
this.items.pop();
const itemBehind = this.items[lastItemProcessed - 1];
@ -1108,11 +1200,11 @@ export class BeltPath extends BasicSerializableObject {
// Also see #999
const fixupProgress = Math.max(
0,
Math.min(remainingVelocity, itemBehind[_nextDistance])
Math.min(remainingVelocity, itemBehind[0 /* nextDistance */])
);
// See above
itemBehind[_nextDistance] -= fixupProgress;
itemBehind[0 /* nextDistance */] -= fixupProgress;
remainingVelocity -= fixupProgress;
this.spacingToFirstItem += fixupProgress;
}
@ -1145,8 +1237,8 @@ export class BeltPath extends BasicSerializableObject {
// Check if we have an item which is ready to be emitted
const lastItem = this.items[this.items.length - 1];
if (lastItem && lastItem[_nextDistance] === 0 && this.acceptorTarget) {
if (this.tryHandOverItem(lastItem[_item])) {
if (lastItem && lastItem[0 /* nextDistance */] === 0) {
if (this.boundAcceptor && this.boundAcceptor(lastItem[1 /* item */])) {
this.items.pop();
this.numCompressedItemsAfterFirstItem = Math.max(
0,
@ -1160,50 +1252,6 @@ export class BeltPath extends BasicSerializableObject {
}
}
/**
* Tries to hand over the item to the end entity
* @param {BaseItem} item
*/
tryHandOverItem(item, remainingProgress = 0.0) {
if (!this.acceptorTarget) {
return;
}
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
// Check if the acceptor has a filter for example
if (targetAcceptorComp && !targetAcceptorComp.canAcceptItem(this.acceptorTarget.slot, item)) {
// Well, this item is not accepted
return false;
}
// Try to pass over
if (
this.root.systemMgr.systems.itemEjector.tryPassOverItem(
item,
this.acceptorTarget.entity,
this.acceptorTarget.slot
)
) {
// Trigger animation on the acceptor comp
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
if (targetAcceptorComp) {
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
targetAcceptorComp.onItemAccepted(
this.acceptorTarget.slot,
this.acceptorTarget.direction,
item,
remainingProgress
);
}
}
return true;
}
return false;
}
/**
* Computes a world space position from the given progress
* @param {number} progress
@ -1270,11 +1318,11 @@ export class BeltPath extends BasicSerializableObject {
parameters.context.font = "6px GameFont";
parameters.context.fillStyle = "#111";
parameters.context.fillText(
"" + round4Digits(nextDistanceAndItem[_nextDistance]),
"" + round4Digits(nextDistanceAndItem[0 /* nextDistance */]),
worldPos.x + 5,
worldPos.y + 2
);
progress += nextDistanceAndItem[_nextDistance];
progress += nextDistanceAndItem[0 /* nextDistance */];
if (this.items.length - 1 - this.numCompressedItemsAfterFirstItem === i) {
parameters.context.fillStyle = "red";
@ -1370,7 +1418,7 @@ export class BeltPath extends BasicSerializableObject {
const centerPos = staticComp.localTileToWorld(centerPosLocal).toWorldSpaceCenterOfTile();
parameters.context.globalAlpha = 0.5;
firstItem[_item].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
firstItem[1 /* item */].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
parameters.context.globalAlpha = 1;
}
@ -1402,7 +1450,7 @@ export class BeltPath extends BasicSerializableObject {
const distanceAndItem = this.items[currentItemIndex];
distanceAndItem[_item].drawItemCenteredClipped(
distanceAndItem[1 /* item */].drawItemCenteredClipped(
worldPos.x,
worldPos.y,
parameters,
@ -1410,7 +1458,7 @@ export class BeltPath extends BasicSerializableObject {
);
// Check for the next item
currentItemPos += distanceAndItem[_nextDistance];
currentItemPos += distanceAndItem[0 /* nextDistance */];
++currentItemIndex;
if (currentItemIndex >= this.items.length) {

@ -149,29 +149,31 @@ export class Blueprint {
*/
tryPlace(root, tile) {
return root.logic.performBulkOperation(() => {
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
continue;
return root.logic.performImmutableOperation(() => {
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
continue;
}
const clone = entity.clone();
clone.components.StaticMapEntity.origin.addInplace(tile);
root.logic.freeEntityAreaBeforeBuild(clone);
root.map.placeStaticEntity(clone);
root.entityMgr.registerEntity(clone);
count++;
}
const clone = entity.clone();
clone.components.StaticMapEntity.origin.addInplace(tile);
root.logic.freeEntityAreaBeforeBuild(clone);
root.map.placeStaticEntity(clone);
root.entityMgr.registerEntity(clone);
count++;
}
root.signals.bulkAchievementCheck.dispatch(
ACHIEVEMENTS.placeBlueprint,
count,
ACHIEVEMENTS.placeBp1000,
count
);
root.signals.bulkAchievementCheck.dispatch(
ACHIEVEMENTS.placeBlueprint,
count,
ACHIEVEMENTS.placeBp1000,
count
);
return count !== 0;
return count !== 0;
});
});
}
}

@ -66,6 +66,10 @@ export class MetaBalancerBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
let speedMultiplier = 2;
switch (variant) {
case enumBalancerVariants.merger:
@ -88,9 +92,11 @@ export class MetaBalancerBuilding extends MetaBuilding {
* @param {GameRoot} root
*/
getAvailableVariants(root) {
let available = [defaultBuildingVariant];
const deterministic = root.gameMode.getIsDeterministic();
let available = deterministic ? [] : [defaultBuildingVariant];
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
if (!deterministic && root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
available.push(enumBalancerVariants.merger, enumBalancerVariants.mergerInverse);
}

@ -55,6 +55,9 @@ export class MetaBeltBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const beltSpeed = root.hubGoals.getBeltBaseSpeed();
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
}

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

@ -0,0 +1,50 @@
/* typehints:start */
import { Entity } from "../entity";
/* typehints:end */
import { enumDirection, Vector } from "../../core/vector";
import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProducerType, ItemProducerComponent } from "../components/item_producer";
import { MetaBuilding } from "../meta_building";
export class MetaConstantProducerBuilding extends MetaBuilding {
constructor() {
super("constant_producer");
}
getSilhouetteColor() {
return "#bfd630";
}
/**
*
* @param {import("../../savegame/savegame_serializer").GameRoot} root
* @returns
*/
getIsRemovable(root) {
return root.gameMode.getIsEditor();
}
/**
* Creates the entity at the given location
* @param {Entity} entity
*/
setupEntityComponents(entity) {
entity.addComponent(
new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
})
);
entity.addComponent(
new ItemProducerComponent({
type: enumItemProducerType.wireless,
})
);
entity.addComponent(
new ConstantSignalComponent({
type: enumConstantSignalType.wireless,
})
);
}
}

@ -38,6 +38,9 @@ export class MetaCutterBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed = root.hubGoals.getProcessorBaseSpeed(
variant === enumCutterVariants.quad
? enumItemProcessorTypes.cutterQuad

@ -40,6 +40,9 @@ export class MetaFilterBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const beltSpeed = root.hubGoals.getBeltBaseSpeed();
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
}

@ -0,0 +1,56 @@
/* typehints:start */
import { Entity } from "../entity";
/* typehints:end */
import { enumDirection, Vector } from "../../core/vector";
import { enumBeltReaderType, BeltReaderComponent } from "../components/belt_reader";
import { GoalAcceptorComponent } from "../components/goal_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { MetaBuilding } from "../meta_building";
export class MetaGoalAcceptorBuilding extends MetaBuilding {
constructor() {
super("goal_acceptor");
}
getSilhouetteColor() {
return "#ce418a";
}
/**
*
* @param {import("../../savegame/savegame_serializer").GameRoot} root
* @returns
*/
getIsRemovable(root) {
return root.gameMode.getIsEditor();
}
/**
* Creates the entity at the given location
* @param {Entity} entity
*/
setupEntityComponents(entity) {
entity.addComponent(
new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
filter: "shape",
},
],
})
);
entity.addComponent(
new ItemProcessorComponent({
processorType: enumItemProcessorTypes.goal,
})
);
entity.addComponent(new GoalAcceptorComponent({}));
}
}

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

@ -31,6 +31,9 @@ export class MetaMinerBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed = root.hubGoals.getMinerBaseSpeed();
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}

@ -35,6 +35,9 @@ export class MetaMixerBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.mixer);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}

@ -46,6 +46,9 @@ export class MetaPainterBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
switch (variant) {
case defaultBuildingVariant:
case enumPainterVariants.mirrored: {
@ -71,7 +74,10 @@ export class MetaPainterBuilding extends MetaBuilding {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) {
variants.push(enumPainterVariants.double);
}
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers)) {
if (
root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) &&
root.gameMode.getSupportsWires()
) {
variants.push(enumPainterVariants.quad);
}
return variants;

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

@ -48,6 +48,9 @@ export class MetaRotaterBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
switch (variant) {
case defaultBuildingVariant: {
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater);

@ -28,6 +28,9 @@ export class MetaStackerBuilding extends MetaBuilding {
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (root.gameMode.throughputDoesNotMatter()) {
return [];
}
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.stacker);
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
}

@ -72,13 +72,21 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
globalConfig.undergroundBeltMaxTilesByTier[enumUndergroundBeltVariantToTier[variant]];
const beltSpeed = root.hubGoals.getUndergroundBeltBaseSpeed();
return [
/** @type {Array<[string, string]>} */
const stats = [
[
T.ingame.buildingPlacement.infoTexts.range,
T.ingame.buildingPlacement.infoTexts.tiles.replace("<x>", "" + rangeTiles),
],
[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)],
];
if (root.gameMode.throughputDoesNotMatter()) {
return stats;
}
stats.push([T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]);
return stats;
}
/**

@ -392,13 +392,20 @@ export class Camera extends BasicSerializableObject {
return rect.containsPoint(point.x, point.y);
}
getMaximumZoom() {
return this.root.gameMode.getMaximumZoom();
}
getMinimumZoom() {
return this.root.gameMode.getMinimumZoom();
}
/**
* Returns if we can further zoom in
* @returns {boolean}
*/
canZoomIn() {
const maxLevel = this.root.app.platformWrapper.getMaximumZoom();
return this.zoomLevel <= maxLevel - 0.01;
return this.zoomLevel <= this.getMaximumZoom() - 0.01;
}
/**
@ -406,8 +413,7 @@ export class Camera extends BasicSerializableObject {
* @returns {boolean}
*/
canZoomOut() {
const minLevel = this.root.app.platformWrapper.getMinimumZoom();
return this.zoomLevel >= minLevel + 0.01;
return this.zoomLevel >= this.getMinimumZoom() + 0.01;
}
// EVENTS
@ -468,6 +474,7 @@ export class Camera extends BasicSerializableObject {
// Clamp everything afterwards
this.clampZoomLevel();
this.clampToBounds();
return false;
}
@ -743,15 +750,27 @@ 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);
}
/**
@ -857,6 +876,7 @@ export class Camera extends BasicSerializableObject {
// Panning
this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06);
this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel));
this.clampToBounds();
}
}
@ -921,6 +941,8 @@ export class Camera extends BasicSerializableObject {
((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed()
)
);
this.clampToBounds();
}
/**
@ -1006,6 +1028,8 @@ export class Camera extends BasicSerializableObject {
this.center.x += moveAmount * forceX * movementSpeed;
this.center.y += moveAmount * forceY * movementSpeed;
this.clampToBounds();
}
}
}

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

@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
export function initComponentRegistry() {
gComponentRegistry.register(StaticMapEntityComponent);
@ -41,6 +42,7 @@ export function initComponentRegistry() {
gComponentRegistry.register(BeltReaderComponent);
gComponentRegistry.register(FilterComponent);
gComponentRegistry.register(ItemProducerComponent);
gComponentRegistry.register(GoalAcceptorComponent);
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS

@ -57,6 +57,12 @@ export class BeltComponent extends Component {
this.assignedPath = null;
}
clear() {
if (this.assignedPath) {
this.assignedPath.clearAllItems();
}
}
/**
* Returns the effective length of this belt in tile space
* @returns {number}

@ -3,6 +3,12 @@ import { BaseItem } from "../base_item";
import { typeItemSingleton } from "../item_resolver";
import { types } from "../../savegame/serialization";
/** @enum {string} */
export const enumBeltReaderType = {
wired: "wired",
wireless: "wireless",
};
export class BeltReaderComponent extends Component {
static getId() {
return "BeltReader";
@ -10,13 +16,24 @@ export class BeltReaderComponent extends Component {
static getSchema() {
return {
type: types.string,
lastItem: types.nullable(typeItemSingleton),
};
}
constructor() {
/**
* @param {object} param0
* @param {string=} param0.type
*/
constructor({ type = enumBeltReaderType.wired }) {
super();
this.type = type;
this.clear();
}
clear() {
/**
* Which items went through the reader, we only store the time
* @type {Array<number>}
@ -41,4 +58,8 @@ export class BeltReaderComponent extends Component {
*/
this.lastThroughputComputation = 0;
}
isWireless() {
return this.type === enumBeltReaderType.wireless;
}
}

@ -4,6 +4,12 @@ import { Component } from "../component";
import { BaseItem } from "../base_item";
import { typeItemSingleton } from "../item_resolver";
/** @enum {string} */
export const enumConstantSignalType = {
wired: "wired",
wireless: "wireless",
};
export class ConstantSignalComponent extends Component {
static getId() {
return "ConstantSignal";
@ -11,6 +17,7 @@ export class ConstantSignalComponent extends Component {
static getSchema() {
return {
type: types.string,
signal: types.nullable(typeItemSingleton),
};
}
@ -21,15 +28,22 @@ export class ConstantSignalComponent extends Component {
*/
copyAdditionalStateTo(otherComponent) {
otherComponent.signal = this.signal;
otherComponent.type = this.type;
}
/**
*
* @param {object} param0
* @param {string=} param0.type
* @param {BaseItem=} param0.signal The signal to store
*/
constructor({ signal = null }) {
constructor({ signal = null, type = enumConstantSignalType.wired }) {
super();
this.signal = signal;
this.type = type;
}
isWireless() {
return this.type === enumConstantSignalType.wireless;
}
}

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

@ -0,0 +1,49 @@
import { globalConfig } from "../../core/config";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
export class GoalAcceptorComponent extends Component {
static getId() {
return "GoalAcceptor";
}
static getSchema() {
return {
item: typeItemSingleton,
};
}
/**
* @param {object} param0
* @param {BaseItem=} param0.item
* @param {number=} param0.rate
*/
constructor({ item = null, rate = null }) {
super();
// ths item to produce
/** @type {BaseItem | undefined} */
this.item = item;
this.clear();
}
clear() {
// the last items we delivered
/** @type {{ item: BaseItem; time: number; }[]} */
this.deliveryHistory = [];
// Used for animations
this.displayPercentage = 0;
}
getRequiredDeliveryHistorySize() {
return (
(globalConfig.puzzleModeSpeed *
globalConfig.goalAcceptorMinimumDurationSeconds *
globalConfig.beltSpeedItemsPerSecond) /
globalConfig.goalAcceptorsPerProducer
);
}
}

@ -36,6 +36,11 @@ export class ItemAcceptorComponent extends Component {
constructor({ slots = [] }) {
super();
this.setSlots(slots);
this.clear();
}
clear() {
/**
* Fixes belt animations
* @type {Array<{
@ -46,8 +51,6 @@ export class ItemAcceptorComponent extends Component {
* }>}
*/
this.itemConsumptionAnimations = [];
this.setSlots(slots);
}
/**
@ -71,6 +74,8 @@ export class ItemAcceptorComponent extends Component {
/**
* Returns if this acceptor can accept a new item at slot N
*
* NOTICE: The belt path ignores this for performance reasons and does his own check
* @param {number} slotIndex
* @param {BaseItem=} item
*/

@ -48,6 +48,13 @@ export class ItemEjectorComponent extends Component {
this.renderFloatingItems = renderFloatingItems;
}
clear() {
for (const slot of this.slots) {
slot.item = null;
slot.progress = 0;
}
}
/**
* @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on
*/

@ -19,6 +19,7 @@ export const enumItemProcessorTypes = {
hub: "hub",
filter: "filter",
reader: "reader",
goal: "goal",
};
/** @enum {string} */
@ -63,10 +64,8 @@ export class ItemProcessorComponent extends Component {
}) {
super();
// Which slot to emit next, this is only a preference and if it can't emit
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
// sure the outputs always match
this.nextOutputSlot = 0;
// How many inputs we need for one charge
this.inputsPerCharge = inputsPerCharge;
// Type of the processor
this.type = processorType;
@ -74,8 +73,14 @@ export class ItemProcessorComponent extends Component {
// Type of processing requirement
this.processingRequirement = processingRequirement;
// How many inputs we need for one charge
this.inputsPerCharge = inputsPerCharge;
this.clear();
}
clear() {
// Which slot to emit next, this is only a preference and if it can't emit
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
// sure the outputs always match
this.nextOutputSlot = 0;
/**
* Our current inputs
@ -104,7 +109,11 @@ export class ItemProcessorComponent extends Component {
* @param {number} sourceSlot
*/
tryTakeItem(item, sourceSlot) {
if (this.type === enumItemProcessorTypes.hub || this.type === enumItemProcessorTypes.trash) {
if (
this.type === enumItemProcessorTypes.hub ||
this.type === enumItemProcessorTypes.trash ||
this.type === enumItemProcessorTypes.goal
) {
// Hub has special logic .. not really nice but efficient.
this.inputSlots.push({ item, sourceSlot });
return true;

@ -1,7 +1,33 @@
import { types } from "../../savegame/serialization";
import { Component } from "../component";
/** @enum {string} */
export const enumItemProducerType = {
wired: "wired",
wireless: "wireless",
};
export class ItemProducerComponent extends Component {
static getId() {
return "ItemProducer";
}
static getSchema() {
return {
type: types.string,
};
}
/**
* @param {object} param0
* @param {string=} param0.type
*/
constructor({ type = enumItemProducerType.wired }) {
super();
this.type = type;
}
isWireless() {
return this.type === enumItemProducerType.wireless;
}
}

@ -24,13 +24,6 @@ export class MinerComponent extends Component {
this.lastMiningTime = 0;
this.chainable = chainable;
/**
* Stores items from other miners which were chained to this
* miner.
* @type {Array<BaseItem>}
*/
this.itemChainBuffer = [];
/**
* @type {BaseItem}
*/
@ -42,6 +35,17 @@ export class MinerComponent extends Component {
* @type {Entity|null|false}
*/
this.cachedChainedMiner = null;
this.clear();
}
clear() {
/**
* Stores items from other miners which were chained to this
* miner.
* @type {Array<BaseItem>}
*/
this.itemChainBuffer = [];
}
/**

@ -71,6 +71,14 @@ export class StaticMapEntityComponent extends Component {
return getBuildingDataFromCode(this.code).variant;
}
/**
* Returns the buildings rotation variant
* @returns {number}
*/
getRotationVariant() {
return getBuildingDataFromCode(this.code).rotationVariant;
}
/**
* Copy the current state to another component
* @param {Component} otherComponent

@ -41,6 +41,17 @@ export class UndergroundBeltComponent extends Component {
this.mode = mode;
this.tier = tier;
/**
* The linked entity, used to speed up performance. This contains either
* the entrance or exit depending on the tunnel type
* @type {LinkedUndergroundBelt}
*/
this.cachedLinkedEntity = null;
this.clear();
}
clear() {
/** @type {Array<{ item: BaseItem, progress: number }>} */
this.consumptionAnimations = [];
@ -51,13 +62,6 @@ export class UndergroundBeltComponent extends Component {
* @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item]
*/
this.pendingItems = [];
/**
* The linked entity, used to speed up performance. This contains either
* the entrance or exit depending on the tunnel type
* @type {LinkedUndergroundBelt}
*/
this.cachedLinkedEntity = null;
}
/**

@ -31,7 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
import { GameLogic } from "./logic";
import { MapView } from "./map_view";
import { defaultBuildingVariant } from "./meta_building";
import { RegularGameMode } from "./modes/regular";
import { GameMode } from "./game_mode";
import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager";
@ -82,7 +82,9 @@ export class GameCore {
* @param {import("../states/ingame").InGameState} parentState
* @param {Savegame} savegame
*/
initializeRoot(parentState, savegame) {
initializeRoot(parentState, savegame, gameModeId) {
logger.log("initializing root");
// Construct the root element, this is the data representation of the game
this.root = new GameRoot(this.app);
this.root.gameState = parentState;
@ -100,12 +102,12 @@ export class GameCore {
// This isn't nice, but we need it right here
root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever);
// Init game mode
root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters);
// Needs to come first
root.dynamicTickrate = new DynamicTickrate(root);
// Init game mode
root.gameMode = new RegularGameMode(root);
// Init classes
root.camera = new Camera(root);
root.map = new MapView(root);
@ -157,6 +159,8 @@ export class GameCore {
}
});
}
logger.log("root initialized");
}
/**
@ -168,6 +172,10 @@ export class GameCore {
this.root.gameIsFresh = true;
this.root.map.seed = randomInt(0, 100000);
if (!this.root.gameMode.hasHub()) {
return;
}
// Place the hub
const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({
root: this.root,
@ -447,7 +455,9 @@ export class GameCore {
systems.hub.draw(params);
// Green wires overlay
root.hud.parts.wiresOverlay.draw(params);
if (root.hud.parts.wiresOverlay) {
root.hud.parts.wiresOverlay.draw(params);
}
if (this.root.currentLayer === "wires") {
// Static map entities

@ -23,10 +23,16 @@ export class DynamicTickrate {
this.averageFps = 60;
this.setTickRate(this.root.app.settings.getDesiredFps());
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
this.setTickRate(300);
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);
}
}
}
@ -99,9 +105,7 @@ export class DynamicTickrate {
this.averageTickDuration = average;
const desiredFps = this.root.app.settings.getDesiredFps();
// Disabled for now: Dynamicall adjusting tick rate
// Disabled for now: Dynamically adjusting tick rate
// if (this.averageFps > desiredFps * 0.9) {
// // if (average < maxTickDuration) {
// this.increaseTickRate();

@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
/* typehints:end */
/**
@ -89,6 +90,9 @@ export class EntityComponentStorage {
/** @type {ItemProducerComponent} */
this.ItemProducer;
/** @type {GoalAcceptorComponent} */
this.GoalAcceptor;
/* typehints:end */
}
}

@ -1,71 +1,192 @@
/* typehints:start */
import { enumHubGoalRewards } from "./tutorial_goals";
import { GameRoot } from "./root";
/* typehints:end */
import { GameRoot } from "./root";
import { Rectangle } from "../core/rectangle";
import { gGameModeRegistry } from "../core/global_registries";
import { types, BasicSerializableObject } from "../savegame/serialization";
import { MetaBuilding } from "./meta_building";
import { MetaItemProducerBuilding } from "./buildings/item_producer";
import { BaseHUDPart } from "./hud/base_hud_part";
/** @typedef {{
* shape: string,
* amount: number
* }} UpgradeRequirement */
/** @enum {string} */
export const enumGameModeIds = {
puzzleEdit: "puzzleEditMode",
puzzlePlay: "puzzlePlayMode",
regular: "regularMode",
};
/** @typedef {{
* required: Array<UpgradeRequirement>
* improvement?: number,
* excludePrevious?: boolean
* }} TierRequirement */
/** @enum {string} */
export const enumGameModeTypes = {
default: "defaultModeType",
puzzle: "puzzleModeType",
};
/** @typedef {Array<TierRequirement>} UpgradeTiers */
export class GameMode extends BasicSerializableObject {
/** @returns {string} */
static getId() {
abstract;
return "unknownMode";
}
/** @typedef {{
* shape: string,
* required: number,
* reward: enumHubGoalRewards,
* throughputOnly?: boolean
* }} LevelDefinition */
/** @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);
}
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>}
* @param {typeof MetaBuilding} building - Class name of building
* @returns {boolean}
*/
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() {
abstract;
return null;
return {
belt: [],
miner: [],
processors: [],
painting: [],
};
}
throughputDoesNotMatter() {
return false;
}
/**
* Returns the blueprint shape key
* @returns {string}
* @param {number} w
* @param {number} h
*/
getBlueprintShapeKey() {
adjustZone(w = 0, h = 0) {
abstract;
return null;
return;
}
/**
* Returns the goals for all levels including their reward
* @returns {Array<LevelDefinition>}
*/
/** @returns {array} */
getLevelDefinitions() {
abstract;
return null;
return [];
}
/**
* Should return whether free play is available or if the game stops
* after the predefined levels
* @returns {boolean}
*/
/** @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";
}
}

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save