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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
167 changed files with 14001 additions and 8193 deletions

View File

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

View File

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

View File

@ -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,7 +11,7 @@ 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) {
@ -37,8 +37,13 @@ function init (isDev) {
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,
};

View File

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

View File

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

View File

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

BIN
res/puzzle_dlc_logo.png Normal file

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

42
res/ui/languages/he.svg Normal file
View File

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

BIN
res/ui/puzzle_dlc_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

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

View File

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

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,49 +49,54 @@
}
.building {
display: flex;
@include S(width, 40px);
position: relative;
@include S(height, 40px);
.icon {
color: $accentColorDark;
display: flex;
flex-direction: column;
flex-direction: column-reverse;
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;
&: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;
}
}
}
width: 100%;
height: 100%;
padding: 0;
margin: 0;
@include S(border-radius, $globalBorderRadius);
background: center center / 70% no-repeat;
}
&:not(.unlocked) {
@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;
}
}
}
&.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);
}
@ -109,6 +114,35 @@
}
}
}
.puzzle-lock {
& {
/* @load-async */
background: uiResource("locked_building.png") center center / #{D(14px)} #{D(14px)}
no-repeat;
}
display: grid;
grid-auto-flow: column;
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,19 +1,22 @@
$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 */
.icon {
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
}
}
}
$buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2, miner, miner-chainable,
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;
}
.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,8 +1,28 @@
export const CHANGELOG = [
{
version: "1.3.1",
date: "beta",
version: "1.4.0",
date: "UNRELEASED",
entries: [
"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",
],

View File

@ -51,9 +51,12 @@ export class AnimationFrame {
dt = resetDtMs;
}
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;
}
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");
@ -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,7 +221,12 @@ export class BeltPath extends BasicSerializableObject {
"regular"
);
if (targetEntity) {
if (!targetEntity) {
return;
}
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;
@ -233,10 +245,10 @@ export class BeltPath extends BasicSerializableObject {
targetStaticComp.rotation
);
if (ejectSlotWsDirection === beltAcceptingDirection) {
return {
entity: targetEntity,
direction: null,
slot: 0,
return item => {
const path = targetBeltComp.assignedPath;
assert(path, "belt has no path");
return path.tryAcceptItem(item);
};
}
}
@ -259,10 +271,92 @@ export class BeltPath extends BasicSerializableObject {
return;
}
return {
entity: targetEntity,
slot: matchingSlot.index,
direction: enumInvertedDirections[ejectingDirection],
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
);
}
return true;
}
return false;
};
}
/**
* 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 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,6 +149,7 @@ export class Blueprint {
*/
tryPlace(root, tile) {
return root.logic.performBulkOperation(() => {
return root.logic.performImmutableOperation(() => {
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
@ -173,5 +174,6 @@ export class Blueprint {
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
if (root.hud.parts.wiresOverlay) {
root.hud.parts.wiresOverlay.draw(params);
}
if (this.root.currentLayer === "wires") {
// Static map entities

View File

@ -23,12 +23,18 @@ export class DynamicTickrate {
this.averageFps = 60;
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);
}
}
}
onFrameRendered() {
++this.accumulatedFps;
@ -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;
}
/**
* Should return all available upgrades
* @returns {Object<string, UpgradeTiers>}
* @type {Record<string, typeof BaseHUDPart>}
*/
getUpgrades() {
abstract;
return null;
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();
}
/**
* 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);
}

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