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>
@ -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",
|
||||
|
BIN
res/puzzle_dlc_logo.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
res/ui/building_icons/block.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
res/ui/building_icons/constant_producer.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
res/ui/building_icons/goal_acceptor.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
res/ui/building_tutorials/block.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
res/ui/building_tutorials/constant_producer.png
Normal file
After Width: | Height: | Size: 129 KiB |
BIN
res/ui/building_tutorials/goal_acceptor.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
res/ui/icons/puzzle_action_liked_no.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
res/ui/icons/puzzle_action_liked_yes.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
res/ui/icons/puzzle_complete_indicator.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
res/ui/icons/puzzle_complete_indicator_inverse.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
res/ui/icons/puzzle_completion_rate.png
Normal file
After Width: | Height: | Size: 680 B |
BIN
res/ui/icons/puzzle_plays.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
res/ui/icons/puzzle_upvotes.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
res/ui/icons/state_next_button.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
42
res/ui/languages/he.svg
Normal 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
After Width: | Height: | Size: 32 KiB |
BIN
res/ui/puzzle_dlc_logo_inverse.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
res_raw/sounds/music/puzzle-full.mp3
Normal file
BIN
res_raw/sprites/blueprints/block.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
res_raw/sprites/blueprints/constant_producer.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
res_raw/sprites/blueprints/goal_acceptor.png
Normal file
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
BIN
res_raw/sprites/buildings/block.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
res_raw/sprites/buildings/constant_producer.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
res_raw/sprites/buildings/goal_acceptor.png
Normal file
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);
|
||||
.icon {
|
||||
color: $accentColorDark;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
background: center center / 70% no-repeat;
|
||||
background: center center / 70% no-repeat;
|
||||
}
|
||||
|
||||
&:not(.unlocked) {
|
||||
@include S(width, 20px);
|
||||
opacity: 0.15;
|
||||
background-image: none !important;
|
||||
|
||||
&::before {
|
||||
content: " ";
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)}
|
||||
no-repeat;
|
||||
@include S(width, 25px);
|
||||
.icon {
|
||||
opacity: 0.15;
|
||||
}
|
||||
&.editor {
|
||||
.icon {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgba(22, 30, 68, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.editor) {
|
||||
.icon {
|
||||
background-image: uiResource("locked_building.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
&.unlocked {
|
||||
pointer-events: all;
|
||||
transition: all 50ms ease-in-out;
|
||||
transition-property: background-color, transform;
|
||||
.icon {
|
||||
pointer-events: all;
|
||||
transition: all 50ms ease-in-out;
|
||||
transition-property: background-color, transform;
|
||||
cursor: pointer;
|
||||
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgba(30, 40, 90, 0.1);
|
||||
&:hover {
|
||||
background-color: rgba(30, 40, 90, 0.1);
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.9) !important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
// transform: scale(1.05);
|
||||
background-color: rgba(lighten($colorBlueBright, 9), 0.4);
|
||||
|
||||
.keybinding {
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.9) !important;
|
||||
}
|
||||
.puzzle-lock {
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("locked_building.png") center center / #{D(14px)} #{D(14px)}
|
||||
no-repeat;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
// transform: scale(1.05);
|
||||
background-color: rgba(lighten($colorBlueBright, 9), 0.4);
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
|
||||
.keybinding {
|
||||
color: #111;
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) !important;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
|
||||
@include S(width, 14px);
|
||||
@include S(height, 14px);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,14 @@
|
||||
* {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.text {
|
||||
text-transform: uppercase;
|
||||
@include S(margin-bottom, 10px);
|
||||
}
|
||||
}
|
||||
|
||||
> .dialogInner {
|
||||
@ -168,6 +176,11 @@
|
||||
|
||||
&.errored {
|
||||
background-color: rgb(250, 206, 206);
|
||||
|
||||
&::placeholder {
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
41
src/css/ingame_hud/puzzle_back_to_menu.scss
Normal file
@ -0,0 +1,41 @@
|
||||
#ingame_HUD_PuzzleBackToMenu {
|
||||
position: absolute;
|
||||
@include S(top, 10px);
|
||||
@include S(left, 0px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
backdrop-filter: blur(D(1px));
|
||||
padding: D(3px);
|
||||
|
||||
> .button {
|
||||
@include PlainText;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: #333438;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
text-transform: uppercase;
|
||||
@include PlainText;
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
opacity: 1;
|
||||
&:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.95) !important;
|
||||
}
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/state_back_button.png") center center / D(15px) no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
171
src/css/ingame_hud/puzzle_complete_notification.scss
Normal file
@ -0,0 +1,171 @@
|
||||
#ingame_HUD_PuzzleCompleteNotification {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: rgba(#333538, 0.95) uiResource("dialog_bg_pattern.png") top left / #{D(10px)} repeat;
|
||||
}
|
||||
|
||||
@include InlineAnimation(0.1s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .dialog {
|
||||
// background: rgba(#222428, 0.5);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include S(padding, 30px);
|
||||
|
||||
@include InlineAnimation(0.5s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
|
||||
> .title {
|
||||
@include SuperHeading;
|
||||
text-transform: uppercase;
|
||||
@include S(font-size, 30px);
|
||||
@include S(margin-bottom, 40px);
|
||||
color: $colorGreenBright !important;
|
||||
|
||||
@include InlineAnimation(0.5s ease-in-out) {
|
||||
0% {
|
||||
transform: translateY(-50vh);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(5vh);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-2vh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .contents {
|
||||
@include InlineAnimation(0.5s ease-in-out) {
|
||||
0% {
|
||||
transform: translateX(-100vw);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(5vw);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(-2vw);
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .stepLike {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(margin-bottom, 10px);
|
||||
@include SuperSmallText;
|
||||
|
||||
> .buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include S(margin, 10px, 0);
|
||||
|
||||
> button {
|
||||
@include S(width, 60px);
|
||||
@include S(height, 60px);
|
||||
@include S(margin, 0, 10px);
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
transition: opacity 0.12s ease-in-out, background-color 0.12s ease-in-out;
|
||||
@include IncreasedClickArea(0px);
|
||||
|
||||
&.liked-yes {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_action_liked_yes.png") center 55% / 60%
|
||||
no-repeat;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $colorRedBright !important;
|
||||
@include InlineAnimation(0.3s ease-in-out) {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.active) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .buttonBar {
|
||||
display: flex;
|
||||
@include S(margin-top, 20px);
|
||||
|
||||
button.continue {
|
||||
background: #555;
|
||||
@include S(margin-right, 10px);
|
||||
}
|
||||
|
||||
button.menu {
|
||||
background-color: $colorGreenBright;
|
||||
}
|
||||
|
||||
> button {
|
||||
@include S(min-width, 100px);
|
||||
@include S(padding, 10px, 20px);
|
||||
@include IncreasedClickArea(0px);
|
||||
}
|
||||
}
|
||||
|
||||
> .actions {
|
||||
position: absolute;
|
||||
@include S(bottom, 40px);
|
||||
|
||||
display: grid;
|
||||
@include S(grid-gap, 15px);
|
||||
grid-auto-flow: column;
|
||||
|
||||
button {
|
||||
@include SuperSmallText;
|
||||
}
|
||||
.report {
|
||||
background-color: $accentColorDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
src/css/ingame_hud/puzzle_dlc_logo.scss
Normal file
@ -0,0 +1,19 @@
|
||||
#ingame_HUD_PuzzleDLCLogo {
|
||||
position: absolute;
|
||||
@include S(width, 120px);
|
||||
@include S(height, 40px);
|
||||
@include S(left, 40px);
|
||||
@include S(top, 7px);
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("puzzle_dlc_logo.png") center center / contain no-repeat;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("puzzle_dlc_logo_inverse.png") center center / contain no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
36
src/css/ingame_hud/puzzle_editor_controls.scss
Normal file
@ -0,0 +1,36 @@
|
||||
#ingame_HUD_PuzzleEditorControls {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 70px);
|
||||
@include S(left, 10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include SuperDuperSmallText;
|
||||
@include S(width, 200px);
|
||||
|
||||
> span {
|
||||
@include S(margin-bottom, 10px);
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@include DarkThemeInvert;
|
||||
}
|
||||
|
||||
#ingame_HUD_PuzzleEditorTitle {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 18px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-transform: uppercase;
|
||||
@include Heading;
|
||||
text-align: center;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
50
src/css/ingame_hud/puzzle_editor_review.scss
Normal file
@ -0,0 +1,50 @@
|
||||
#ingame_HUD_PuzzleEditorReview {
|
||||
position: absolute;
|
||||
@include S(top, 17px);
|
||||
@include S(right, 10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
backdrop-filter: blur(D(1px));
|
||||
padding: D(3px);
|
||||
|
||||
> .button {
|
||||
@include ButtonText;
|
||||
@include IncreasedClickArea(0px);
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: #333438;
|
||||
transition: all 0.12s ease-in-out;
|
||||
text-transform: uppercase;
|
||||
transition-property: opacity, transform;
|
||||
@include PlainText;
|
||||
@include S(padding-right, 25px);
|
||||
opacity: 1;
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: scale(0.95) !important;
|
||||
}
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/state_next_button.png") right center / D(15px) no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
@include SuperDuperSmallText;
|
||||
@include S(width, 180px);
|
||||
@include S(padding-right, 25px);
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
color: $accentColorDark;
|
||||
}
|
||||
}
|
62
src/css/ingame_hud/puzzle_editor_settings.scss
Normal file
@ -0,0 +1,62 @@
|
||||
#ingame_HUD_PuzzleEditorSettings {
|
||||
position: absolute;
|
||||
background: $ingameHudBg;
|
||||
@include S(padding, 10px);
|
||||
@include S(bottom, 60px);
|
||||
@include S(left, 10px);
|
||||
|
||||
@include SuperSmallText;
|
||||
color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
> .section {
|
||||
> label {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.plusMinus {
|
||||
@include S(margin-top, 5px);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
align-items: center;
|
||||
@include S(grid-gap, 5px);
|
||||
|
||||
label {
|
||||
@include S(margin-right, 10px);
|
||||
}
|
||||
|
||||
button {
|
||||
@include PlainText;
|
||||
@include S(padding, 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include S(width, 15px);
|
||||
@include S(height, 15px);
|
||||
@include IncreasedClickArea(0px);
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: center;
|
||||
@include S(min-width, 15px);
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
> .buttonBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include S(margin-top, 10px);
|
||||
> button {
|
||||
@include S(margin-right, 4px);
|
||||
@include SuperSmallText;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
129
src/css/ingame_hud/puzzle_play_metadata.scss
Normal file
@ -0,0 +1,129 @@
|
||||
#ingame_HUD_PuzzlePlayMetadata {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 70px);
|
||||
@include S(left, 10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(width, 200px);
|
||||
|
||||
> .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include SuperSmallText;
|
||||
@include S(margin-bottom, 5px);
|
||||
|
||||
> label {
|
||||
text-transform: uppercase;
|
||||
@include SuperDuperSmallText;
|
||||
color: $accentColorDark;
|
||||
}
|
||||
> span {
|
||||
display: flex;
|
||||
color: darken($accentColorDark, 25);
|
||||
@include SuperSmallText;
|
||||
@include DarkThemeOverride {
|
||||
color: lighten($accentColorDark, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .plays {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
align-self: end;
|
||||
flex-direction: row;
|
||||
@include S(margin-bottom, 10px);
|
||||
opacity: 0.8;
|
||||
@include DarkThemeInvert;
|
||||
@include DarkThemeOverride {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
> .downloads {
|
||||
@include SuperSmallText;
|
||||
color: #000;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
font-weight: bold;
|
||||
@include S(margin-right, 10px);
|
||||
@include S(padding-left, 14px);
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_plays.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .likes {
|
||||
@include SuperSmallText;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
font-weight: bold;
|
||||
@include S(padding-left, 14px);
|
||||
opacity: 0.7;
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .key {
|
||||
button {
|
||||
@include S(margin-top, 2px);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include SuperSmallText;
|
||||
align-self: start;
|
||||
@include S(min-width, 50px);
|
||||
|
||||
&.report {
|
||||
background-color: $accentColorDark;
|
||||
@include SuperDuperSmallText;
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> button {
|
||||
@include S(margin-bottom, 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ingame_HUD_PuzzlePlayTitle {
|
||||
position: absolute;
|
||||
|
||||
@include S(top, 18px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-transform: uppercase;
|
||||
@include Heading;
|
||||
text-align: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .name {
|
||||
@include PlainText;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
23
src/css/ingame_hud/puzzle_play_settings.scss
Normal file
@ -0,0 +1,23 @@
|
||||
#ingame_HUD_PuzzlePlaySettings {
|
||||
position: absolute;
|
||||
background: $ingameHudBg;
|
||||
@include S(padding, 10px);
|
||||
@include S(bottom, 60px);
|
||||
@include S(left, 10px);
|
||||
|
||||
@include SuperSmallText;
|
||||
color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
> .section {
|
||||
display: grid;
|
||||
@include S(grid-gap, 10px);
|
||||
grid-auto-flow: row;
|
||||
|
||||
> button {
|
||||
@include SuperSmallText;
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@
|
||||
@import "states/about";
|
||||
@import "states/mobile_warning";
|
||||
@import "states/changelog";
|
||||
@import "states/puzzle_menu";
|
||||
|
||||
@import "ingame_hud/buildings_toolbar";
|
||||
@import "ingame_hud/building_placer";
|
||||
@ -55,12 +56,21 @@
|
||||
@import "ingame_hud/sandbox_controller";
|
||||
@import "ingame_hud/standalone_advantages";
|
||||
@import "ingame_hud/cat_memes";
|
||||
@import "ingame_hud/puzzle_back_to_menu";
|
||||
@import "ingame_hud/puzzle_editor_review";
|
||||
@import "ingame_hud/puzzle_dlc_logo";
|
||||
@import "ingame_hud/puzzle_editor_controls";
|
||||
@import "ingame_hud/puzzle_editor_settings";
|
||||
@import "ingame_hud/puzzle_play_settings";
|
||||
@import "ingame_hud/puzzle_play_metadata";
|
||||
@import "ingame_hud/puzzle_complete_notification";
|
||||
|
||||
// prettier-ignore
|
||||
$elements:
|
||||
// Base
|
||||
ingame_Canvas,
|
||||
ingame_VignetteOverlay,
|
||||
ingame_HUD_PuzzleDLCLogo,
|
||||
|
||||
// Ingame overlays
|
||||
ingame_HUD_Waypoints,
|
||||
@ -71,6 +81,14 @@ ingame_HUD_PlacerVariants,
|
||||
ingame_HUD_PinnedShapes,
|
||||
ingame_HUD_GameMenu,
|
||||
ingame_HUD_KeybindingOverlay,
|
||||
ingame_HUD_PuzzleBackToMenu,
|
||||
ingame_HUD_PuzzleEditorReview,
|
||||
ingame_HUD_PuzzleEditorControls,
|
||||
ingame_HUD_PuzzleEditorTitle,
|
||||
ingame_HUD_PuzzleEditorSettings,
|
||||
ingame_HUD_PuzzlePlaySettings,
|
||||
ingame_HUD_PuzzlePlayMetadata,
|
||||
ingame_HUD_PuzzlePlayTitle,
|
||||
ingame_HUD_Notifications,
|
||||
ingame_HUD_DebugInfo,
|
||||
ingame_HUD_EntityDebugger,
|
||||
@ -94,6 +112,7 @@ ingame_HUD_Statistics,
|
||||
ingame_HUD_ShapeViewer,
|
||||
ingame_HUD_StandaloneAdvantages,
|
||||
ingame_HUD_UnlockNotification,
|
||||
ingame_HUD_PuzzleCompleteNotification,
|
||||
ingame_HUD_SettingsMenu,
|
||||
ingame_HUD_ModalDialogs,
|
||||
ingame_HUD_CatMemes;
|
||||
@ -113,6 +132,8 @@ body.uiHidden {
|
||||
#ingame_HUD_PlacementHints,
|
||||
#ingame_HUD_GameMenu,
|
||||
#ingame_HUD_PinnedShapes,
|
||||
#ingame_HUD_PuzzleBackToMenu,
|
||||
#ingame_HUD_PuzzleEditorReview,
|
||||
#ingame_HUD_Notifications,
|
||||
#ingame_HUD_TutorialHints,
|
||||
#ingame_HUD_Waypoints,
|
||||
|
@ -1,11 +1,13 @@
|
||||
$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire,
|
||||
constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage,
|
||||
transistor, analyzer, comparator, item_producer;
|
||||
transistor, analyzer, comparator, item_producer, constant_producer, goal_acceptor, block;
|
||||
|
||||
@each $building in $buildings {
|
||||
[data-icon="building_icons/#{$building}.png"] {
|
||||
/* @load-async */
|
||||
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
|
||||
.icon {
|
||||
background-image: uiResource("res/ui/building_icons/#{$building}.png") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +15,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2,
|
||||
cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage,
|
||||
reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not,
|
||||
logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, item_producer,
|
||||
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator;
|
||||
constant_producer, virtual_processor-stacker, virtual_processor-painter, wire-second, painter,
|
||||
painter-mirrored, comparator, goal_acceptor, block;
|
||||
@each $building in $buildingsAndVariants {
|
||||
[data-icon="building_tutorials/#{$building}.png"] {
|
||||
/* @load-async */
|
||||
@ -67,7 +70,7 @@ $icons: notification_saved, notification_success, notification_upgrade;
|
||||
}
|
||||
|
||||
$languages: en, de, cs, da, et, es-419, fr, it, pt-BR, sv, tr, el, ru, uk, zh-TW, zh-CN, nb, mt-MT, ar, nl, vi,
|
||||
th, hu, pl, ja, kor, no, pt-PT, fi, ro;
|
||||
th, hu, pl, ja, kor, no, pt-PT, fi, ro, he;
|
||||
|
||||
@each $language in $languages {
|
||||
[data-languageicon="#{$language}"] {
|
||||
|
@ -88,11 +88,7 @@
|
||||
|
||||
@include S(grid-column-gap, 10px);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
&.demo {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.standaloneBanner {
|
||||
background: rgb(255, 75, 84);
|
||||
@ -183,7 +179,7 @@
|
||||
.updateLabel {
|
||||
position: absolute;
|
||||
transform: translateX(50%) rotate(-5deg);
|
||||
color: #3291e9;
|
||||
color: #ff590b;
|
||||
@include Heading;
|
||||
font-weight: bold;
|
||||
@include S(right, 40px);
|
||||
@ -223,9 +219,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.puzzleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: #4cc98a;
|
||||
grid-row: 1 / 2;
|
||||
grid-column: 2 / 3;
|
||||
@include S(padding, 20px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
> .dlcLogo {
|
||||
@include S(width, 200px);
|
||||
}
|
||||
|
||||
> button {
|
||||
@include S(margin-top, 20px);
|
||||
@include Heading;
|
||||
@include S(padding, 10px, 30px);
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-row: 1 / 2;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: #fafafa;
|
||||
@ -242,6 +262,16 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modeButtons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@include S(grid-column-gap, 10px);
|
||||
align-items: start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.browserWarning {
|
||||
@include S(margin-bottom, 10px);
|
||||
background-color: $colorRedBright;
|
||||
@ -285,6 +315,18 @@
|
||||
@include S(margin-left, 15px);
|
||||
}
|
||||
|
||||
.playModeButton {
|
||||
@include IncreasedClickArea(0px);
|
||||
@include S(margin-top, 15px);
|
||||
@include S(margin-left, 15px);
|
||||
}
|
||||
|
||||
.editModeButton {
|
||||
@include IncreasedClickArea(0px);
|
||||
@include S(margin-top, 15px);
|
||||
@include S(margin-left, 15px);
|
||||
}
|
||||
|
||||
.savegames {
|
||||
@include S(max-height, 105px);
|
||||
overflow-y: auto;
|
||||
@ -439,6 +481,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bottomContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
@include S(padding-top, 10px);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@include S(grid-column-gap, 10px);
|
||||
align-items: start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
|
@ -17,7 +17,7 @@
|
||||
@include S(border-radius, 3px);
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: #424242;
|
||||
background: #33343c;
|
||||
}
|
||||
|
||||
.version {
|
||||
|
277
src/css/states/puzzle_menu.scss
Normal file
@ -0,0 +1,277 @@
|
||||
#state_PuzzleMenuState {
|
||||
> .headerBar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
|
||||
> h1 {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.createPuzzle {
|
||||
background-color: $colorGreenBright;
|
||||
@include S(margin-left, 5px);
|
||||
}
|
||||
}
|
||||
|
||||
> .container {
|
||||
> .mainContent {
|
||||
overflow: hidden;
|
||||
|
||||
> .categoryChooser {
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
@include S(grid-gap, 2px);
|
||||
@include S(padding-right, 10px);
|
||||
|
||||
> .category {
|
||||
background: $accentColorBright;
|
||||
border-radius: 0;
|
||||
color: $accentColorDark;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, background-color, color;
|
||||
|
||||
&:first-child {
|
||||
@include S(border-top-left-radius, $globalBorderRadius);
|
||||
@include S(border-bottom-left-radius, $globalBorderRadius);
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: $globalBorderRadius;
|
||||
border-bottom-right-radius: $globalBorderRadius;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $colorBlueBright;
|
||||
opacity: 1 !important;
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: $accentColorDark;
|
||||
color: #bbbbc4;
|
||||
|
||||
&.active {
|
||||
background: $colorBlueBright;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .puzzles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(D(180px), 1fr));
|
||||
@include S(grid-auto-rows, 65px);
|
||||
@include S(grid-gap, 7px);
|
||||
@include S(margin-top, 10px);
|
||||
@include S(padding-right, 4px);
|
||||
@include S(height, 360px);
|
||||
overflow-y: scroll;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
|
||||
> .puzzle {
|
||||
width: 100%;
|
||||
@include S(height, 65px);
|
||||
background: #f3f3f8;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: D(15px) D(15px) 1fr;
|
||||
@include S(padding, 5px);
|
||||
@include S(grid-column-gap, 5px);
|
||||
box-sizing: border-box;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@include S(padding-left, 10px);
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: rgba(0, 0, 10, 0.2);
|
||||
}
|
||||
|
||||
@include InlineAnimation(0.12s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f8;
|
||||
}
|
||||
|
||||
> .title {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
@include PlainText;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
justify-self: start;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@include S(padding, 2px, 5px);
|
||||
@include S(height, 17px);
|
||||
}
|
||||
|
||||
> .author {
|
||||
grid-column: 2 / 2;
|
||||
grid-row: 2 / 3;
|
||||
@include SuperSmallText;
|
||||
color: $accentColorDark;
|
||||
align-self: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@include S(padding, 2px, 5px);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 4;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
@include S(width, 45px);
|
||||
@include S(height, 45px);
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> .stats {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 3 / 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
justify-content: center;
|
||||
align-self: end;
|
||||
@include S(height, 14px);
|
||||
|
||||
> .downloads {
|
||||
@include SuperSmallText;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
@include S(margin-right, 5px);
|
||||
@include S(padding-left, 12px);
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include DarkThemeInvert;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_plays.png") #{D(2px)} #{D(2.5px)} / #{D(
|
||||
8px
|
||||
)} #{D(8px)} no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .likes {
|
||||
@include SuperSmallText;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
@include S(padding-left, 14px);
|
||||
opacity: 0.7;
|
||||
@include DarkThemeInvert;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} #{D(2.4px)} / #{D(
|
||||
9px
|
||||
)} #{D(9px)} no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> .difficulty {
|
||||
@include SuperSmallText;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
@include S(margin-right, 3px);
|
||||
opacity: 0.7;
|
||||
|
||||
&.stage--easy {
|
||||
color: $colorGreenBright;
|
||||
}
|
||||
&.stage--normal {
|
||||
color: #000;
|
||||
@include DarkThemeInvert;
|
||||
}
|
||||
&.stage--medium {
|
||||
color: $colorOrangeBright;
|
||||
}
|
||||
&.stage--hard {
|
||||
color: $colorRedBright;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
> .icon,
|
||||
> .stats,
|
||||
> .author,
|
||||
> .title {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@include S(top, 10px);
|
||||
@include S(right, 10px);
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
opacity: 0.1;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_complete_indicator.png") center center /
|
||||
contain no-repeat;
|
||||
}
|
||||
}
|
||||
@include DarkThemeOverride {
|
||||
&::after {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_complete_indicator_inverse.png") center
|
||||
center / contain no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .loader,
|
||||
> .empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $accentColorDark;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,8 +18,10 @@ $textLineHeight: 21px;
|
||||
$plainTextFontSize: 13px;
|
||||
$plainTextLineHeight: 17px;
|
||||
|
||||
$supersmallTextFontSize: 10px;
|
||||
$supersmallTextLineHeight: 13px;
|
||||
$superDuperSmallTextFontSize: 8px;
|
||||
$superDuperSmallTextLineHeight: 9px;
|
||||
$superSmallTextFontSize: 10px;
|
||||
$superSmallTextLineHeight: 13px;
|
||||
$buttonFontSize: 14px;
|
||||
$buttonLineHeight: 18px;
|
||||
|
||||
@ -33,6 +35,7 @@ $accentColorDark: #7d808a;
|
||||
$colorGreenBright: #66bb6a;
|
||||
$colorBlueBright: rgb(74, 151, 223);
|
||||
$colorRedBright: #ef5072;
|
||||
$colorOrangeBright: #ef9d50;
|
||||
$themeColor: #393747;
|
||||
$ingameHudBg: rgba(#333438, 0.9);
|
||||
|
||||
@ -76,8 +79,16 @@ $mainFontScale: 1;
|
||||
// }
|
||||
}
|
||||
|
||||
@mixin SuperDuperSmallText {
|
||||
@include ScaleFont($superDuperSmallTextFontSize, $superDuperSmallTextLineHeight);
|
||||
font-weight: $mainFontWeight;
|
||||
font-family: $mainFont;
|
||||
letter-spacing: $mainFontSpacing;
|
||||
@include DebugText(green);
|
||||
}
|
||||
|
||||
@mixin SuperSmallText {
|
||||
@include ScaleFont($supersmallTextFontSize, $supersmallTextLineHeight);
|
||||
@include ScaleFont($superSmallTextFontSize, $superSmallTextLineHeight);
|
||||
font-weight: $mainFontWeight;
|
||||
font-family: $mainFont;
|
||||
letter-spacing: $mainFontSpacing;
|
||||
|
@ -31,6 +31,9 @@ import { PreloadState } from "./states/preload";
|
||||
import { SettingsState } from "./states/settings";
|
||||
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||
import { RestrictionManager } from "./core/restriction_manager";
|
||||
import { PuzzleMenuState } from "./states/puzzle_menu";
|
||||
import { ClientAPI } from "./platform/api";
|
||||
import { LoginState } from "./states/login";
|
||||
|
||||
/**
|
||||
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
||||
@ -72,6 +75,7 @@ export class Application {
|
||||
this.savegameMgr = new SavegameManager(this);
|
||||
this.inputMgr = new InputDistributor(this);
|
||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||
this.clientApi = new ClientAPI(this);
|
||||
|
||||
// Restrictions (Like demo etc)
|
||||
this.restrictionMgr = new RestrictionManager(this);
|
||||
@ -159,6 +163,8 @@ export class Application {
|
||||
KeybindingsState,
|
||||
AboutState,
|
||||
ChangelogState,
|
||||
PuzzleMenuState,
|
||||
LoginState,
|
||||
];
|
||||
|
||||
for (let i = 0; i < states.length; ++i) {
|
||||
|
@ -1,12 +1,32 @@
|
||||
export const CHANGELOG = [
|
||||
{
|
||||
version: "1.3.1",
|
||||
date: "beta",
|
||||
version: "1.4.0",
|
||||
date: "UNRELEASED",
|
||||
entries: [
|
||||
"Fixed savegames getting corrupt in rare conditions",
|
||||
"Fixed game crashing sometimes since the achievements update",
|
||||
"Added puzzle mode",
|
||||
"Belts in blueprints should now always paste correctly",
|
||||
"You can now clear belts by selecting them, and then pressing 'B'",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.3.1",
|
||||
date: "16.04.2021",
|
||||
entries: G_CHINA_VERSION
|
||||
? [
|
||||
"第13关的交付目标更改为:中国古代指南针。(感谢玩家:凯风入心 创作并提供",
|
||||
"第17关的交付目标更改为:永乐通宝。(感谢玩家:金天赐 创作并提供",
|
||||
"第22关的交付目标更改为:凤凰。(感谢玩家:我没得眼镜 创作并提供",
|
||||
"第23关的交付目标更改为:古代车轮。(感谢玩家:我没得眼镜 创作并提供",
|
||||
"第24关的交付目标更改为:大熊猫。(感谢玩家:窝囸倪现任 创作并提供",
|
||||
|
||||
"修复了一些特定情况下偶尔会发生的存档损坏问题",
|
||||
"修复了成就更新后有时候游戏崩溃的问题",
|
||||
]
|
||||
: [
|
||||
"Fixed savegames getting corrupt in rare conditions",
|
||||
"Fixed game crashing sometimes since the achievements update",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.3.0",
|
||||
date: "12.03.2020",
|
||||
|
@ -51,9 +51,12 @@ export class AnimationFrame {
|
||||
dt = resetDtMs;
|
||||
}
|
||||
|
||||
this.frameEmitted.dispatch(dt);
|
||||
try {
|
||||
this.frameEmitted.dispatch(dt);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
this.lastTime = time;
|
||||
|
||||
window.requestAnimationFrame(this.boundMethod);
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,13 @@ export const globalConfig = {
|
||||
|
||||
readerAnalyzeIntervalSeconds: 10,
|
||||
|
||||
goalAcceptorMinimumDurationSeconds: 5,
|
||||
goalAcceptorsPerProducer: 4.5,
|
||||
puzzleModeSpeed: 3,
|
||||
puzzleMinBoundsSize: 2,
|
||||
puzzleMaxBoundsSize: 20,
|
||||
puzzleValidationDurationSeconds: 30,
|
||||
|
||||
buildingSpeeds: {
|
||||
cutter: 1 / 4,
|
||||
cutterQuad: 1 / 4,
|
||||
@ -93,7 +100,7 @@ export const globalConfig = {
|
||||
gameSpeed: 1,
|
||||
|
||||
warmupTimeSecondsFast: 0.5,
|
||||
warmupTimeSecondsRegular: 3,
|
||||
warmupTimeSecondsRegular: 1.5,
|
||||
|
||||
smoothing: {
|
||||
smoothMainCanvas: smoothCanvas && true,
|
||||
|
@ -62,6 +62,9 @@ export default {
|
||||
// Allows unlocked achievements to be logged to console in the local build
|
||||
// testAchievements: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Enables use of (some) existing flags within the puzzle mode context
|
||||
// testPuzzleMode: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
// Disables the automatic switch to an overview when zooming out
|
||||
// disableMapOverview: true,
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
@ -123,4 +123,6 @@ function catchErrors(message, source, lineno, colno, error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
window.onerror = catchErrors;
|
||||
if (!G_IS_DEV) {
|
||||
window.onerror = catchErrors;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { Factory } from "./factory";
|
||||
* @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed
|
||||
* @typedef {import("../game/component").Component} Component
|
||||
* @typedef {import("../game/base_item").BaseItem} BaseItem
|
||||
* @typedef {import("../game/game_mode").GameMode} GameMode
|
||||
* @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding
|
||||
|
||||
|
||||
@ -19,6 +20,9 @@ export let gBuildingsByCategory = null;
|
||||
/** @type {FactoryTemplate<Component>} */
|
||||
export let gComponentRegistry = new Factory("component");
|
||||
|
||||
/** @type {FactoryTemplate<GameMode>} */
|
||||
export let gGameModeRegistry = new Factory("gameMode");
|
||||
|
||||
/** @type {FactoryTemplate<BaseGameSpeed>} */
|
||||
export let gGameSpeedRegistry = new Factory("gamespeed");
|
||||
|
||||
|
@ -267,7 +267,7 @@ export class Dialog {
|
||||
* Dialog which simply shows a loading spinner
|
||||
*/
|
||||
export class DialogLoading extends Dialog {
|
||||
constructor(app) {
|
||||
constructor(app, text = "") {
|
||||
super({
|
||||
app,
|
||||
title: "",
|
||||
@ -279,6 +279,8 @@ export class DialogLoading extends Dialog {
|
||||
// Loading dialog can not get closed with back button
|
||||
this.inputReciever.backButton.removeAll();
|
||||
this.inputReciever.context = "dialog-loading";
|
||||
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
createElement() {
|
||||
@ -287,6 +289,13 @@ export class DialogLoading extends Dialog {
|
||||
elem.classList.add("loadingDialog");
|
||||
this.element = elem;
|
||||
|
||||
if (this.text) {
|
||||
const text = document.createElement("div");
|
||||
text.classList.add("text");
|
||||
text.innerText = this.text;
|
||||
elem.appendChild(text);
|
||||
}
|
||||
|
||||
const loader = document.createElement("div");
|
||||
loader.classList.add("prefab_LoadingTextWithAnim");
|
||||
loader.classList.add("loadingIndicator");
|
||||
@ -309,7 +318,7 @@ export class DialogOptionChooser extends Dialog {
|
||||
<div class='option ${value === options.active ? "active" : ""} ${
|
||||
iconPrefix ? "hasIcon" : ""
|
||||
}' data-optionvalue='${value}'>
|
||||
${iconHtml}
|
||||
${iconHtml}
|
||||
<span class='title'>${text}</span>
|
||||
${descHtml}
|
||||
</div>
|
||||
@ -444,7 +453,7 @@ export class DialogWithForm extends Dialog {
|
||||
for (let i = 0; i < this.formElements.length; ++i) {
|
||||
const elem = this.formElements[i];
|
||||
elem.bindEvents(div, this.clickDetectors);
|
||||
elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
|
||||
// elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
|
||||
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,11 @@ export class FormElementInput extends FormElement {
|
||||
return this.element.value;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.element.value = value;
|
||||
this.updateErrorState();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
@ -44,6 +44,15 @@ export class Rectangle {
|
||||
return new Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
*/
|
||||
static centered(width, height) {
|
||||
return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a intersects b
|
||||
* @param {Rectangle} a
|
||||
@ -72,7 +81,7 @@ export class Rectangle {
|
||||
/**
|
||||
* Returns if this rectangle is equal to the other while taking an epsilon into account
|
||||
* @param {Rectangle} other
|
||||
* @param {number} epsilon
|
||||
* @param {number} [epsilon]
|
||||
*/
|
||||
equalsEpsilon(other, epsilon) {
|
||||
return (
|
||||
@ -287,6 +296,15 @@ export class Rectangle {
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the rectangle fully intersects the given rectangle
|
||||
* @param {Rectangle} rect
|
||||
*/
|
||||
intersectsFully(rect) {
|
||||
const intersection = this.getIntersection(rect);
|
||||
return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the union of this rectangle with another
|
||||
* @param {Rectangle} rect
|
||||
|
@ -17,6 +17,17 @@ export class Signal {
|
||||
++this.modifyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new signal listener
|
||||
* @param {function} receiver
|
||||
* @param {object} scope
|
||||
*/
|
||||
addToTop(receiver, scope = null) {
|
||||
assert(receiver, "receiver is null");
|
||||
this.receivers.unshift({ receiver, scope });
|
||||
++this.modifyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the signal
|
||||
* @param {...any} payload
|
||||
|
@ -90,9 +90,9 @@ export class StateManager {
|
||||
dialogParent.classList.add("modalDialogParent");
|
||||
document.body.appendChild(dialogParent);
|
||||
|
||||
this.currentState.internalEnterCallback(payload);
|
||||
this.app.sound.playThemeMusic(this.currentState.getThemeMusic());
|
||||
|
||||
this.currentState.internalEnterCallback(payload);
|
||||
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
|
||||
|
||||
this.app.analytics.trackStateEnter(key);
|
||||
|
@ -11,6 +11,7 @@ export const itemTypes = ["shape", "color", "boolean"];
|
||||
export class BaseItem extends BasicSerializableObject {
|
||||
constructor() {
|
||||
super();
|
||||
this._type = this.getItemType();
|
||||
}
|
||||
|
||||
static getId() {
|
||||
|
@ -13,8 +13,6 @@ import { GameRoot } from "./root";
|
||||
const logger = createLogger("belt_path");
|
||||
|
||||
// Helpers for more semantic access into interleaved arrays
|
||||
const _nextDistance = 0;
|
||||
const _item = 1;
|
||||
|
||||
const DEBUG = G_IS_DEV && false;
|
||||
|
||||
@ -110,6 +108,15 @@ export class BeltPath extends BasicSerializableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all items
|
||||
*/
|
||||
clearAllItems() {
|
||||
this.items = [];
|
||||
this.spacingToFirstItem = this.totalLength;
|
||||
this.numCompressedItemsAfterFirstItem = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this path can accept a new item
|
||||
* @returns {boolean}
|
||||
@ -174,7 +181,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
* Recomputes cache variables once the path was changed
|
||||
*/
|
||||
onPathChanged() {
|
||||
this.acceptorTarget = this.computeAcceptingEntityAndSlot();
|
||||
this.boundAcceptor = this.computeAcceptingEntityAndSlot();
|
||||
|
||||
/**
|
||||
* How many items past the first item are compressed
|
||||
@ -192,7 +199,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
/**
|
||||
* Finds the entity which accepts our items
|
||||
* @param {boolean=} debug_Silent Whether debug output should be silent
|
||||
* @return {{ entity: Entity, slot: number, direction?: enumDirection }}
|
||||
* @return { (BaseItem, number) => boolean }
|
||||
*/
|
||||
computeAcceptingEntityAndSlot(debug_Silent = false) {
|
||||
DEBUG && !debug_Silent && logger.log("Recomputing acceptor target");
|
||||
@ -214,55 +221,142 @@ export class BeltPath extends BasicSerializableObject {
|
||||
"regular"
|
||||
);
|
||||
|
||||
if (targetEntity) {
|
||||
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
|
||||
const targetStaticComp = targetEntity.components.StaticMapEntity;
|
||||
const targetBeltComp = targetEntity.components.Belt;
|
||||
if (!targetEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for belts (special case)
|
||||
if (targetBeltComp) {
|
||||
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
|
||||
DEBUG &&
|
||||
!debug_Silent &&
|
||||
logger.log(
|
||||
" Entity is accepting items from",
|
||||
ejectSlotWsDirection,
|
||||
"vs",
|
||||
beltAcceptingDirection,
|
||||
"Rotation:",
|
||||
targetStaticComp.rotation
|
||||
const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts;
|
||||
|
||||
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
|
||||
const targetStaticComp = targetEntity.components.StaticMapEntity;
|
||||
const targetBeltComp = targetEntity.components.Belt;
|
||||
|
||||
// Check for belts (special case)
|
||||
if (targetBeltComp) {
|
||||
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
|
||||
DEBUG &&
|
||||
!debug_Silent &&
|
||||
logger.log(
|
||||
" Entity is accepting items from",
|
||||
ejectSlotWsDirection,
|
||||
"vs",
|
||||
beltAcceptingDirection,
|
||||
"Rotation:",
|
||||
targetStaticComp.rotation
|
||||
);
|
||||
if (ejectSlotWsDirection === beltAcceptingDirection) {
|
||||
return item => {
|
||||
const path = targetBeltComp.assignedPath;
|
||||
assert(path, "belt has no path");
|
||||
return path.tryAcceptItem(item);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for item acceptors
|
||||
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
||||
if (!targetAcceptorComp) {
|
||||
// Entity doesn't accept items
|
||||
return;
|
||||
}
|
||||
|
||||
const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection);
|
||||
const matchingSlot = targetAcceptorComp.findMatchingSlot(
|
||||
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
|
||||
ejectingDirection
|
||||
);
|
||||
|
||||
if (!matchingSlot) {
|
||||
// No matching slot found
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingSlotIndex = matchingSlot.index;
|
||||
const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex);
|
||||
if (!passOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingDirection = enumInvertedDirections[ejectingDirection];
|
||||
const filter = matchingSlot.slot.filter;
|
||||
|
||||
return function (item, remainingProgress = 0.0) {
|
||||
// Check if the acceptor has a filter
|
||||
if (filter && item._type !== filter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to pass over
|
||||
if (passOver(item, matchingSlotIndex)) {
|
||||
// Trigger animation on the acceptor comp
|
||||
if (noSimplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
matchingSlotIndex,
|
||||
matchingDirection,
|
||||
item,
|
||||
remainingProgress
|
||||
);
|
||||
if (ejectSlotWsDirection === beltAcceptingDirection) {
|
||||
return {
|
||||
entity: targetEntity,
|
||||
direction: null,
|
||||
slot: 0,
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// Check for item acceptors
|
||||
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
||||
if (!targetAcceptorComp) {
|
||||
// Entity doesn't accept items
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Computes a method to pass over the item to the entity
|
||||
* @param {Entity} entity
|
||||
* @param {number} matchingSlotIndex
|
||||
* @returns {(item: BaseItem, slotIndex: number) => boolean | void}
|
||||
*/
|
||||
computePassOverFunctionWithoutBelts(entity, matchingSlotIndex) {
|
||||
const systems = this.root.systemMgr.systems;
|
||||
const hubGoals = this.root.hubGoals;
|
||||
|
||||
const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection);
|
||||
const matchingSlot = targetAcceptorComp.findMatchingSlot(
|
||||
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
|
||||
ejectingDirection
|
||||
);
|
||||
// NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS
|
||||
|
||||
if (!matchingSlot) {
|
||||
// No matching slot found
|
||||
return;
|
||||
}
|
||||
const itemProcessorComp = entity.components.ItemProcessor;
|
||||
if (itemProcessorComp) {
|
||||
// Its an item processor ..
|
||||
return function (item) {
|
||||
// Check for potential filters
|
||||
if (!systems.itemProcessor.checkRequirements(entity, item, matchingSlotIndex)) {
|
||||
return;
|
||||
}
|
||||
return itemProcessorComp.tryTakeItem(item, matchingSlotIndex);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
entity: targetEntity,
|
||||
slot: matchingSlot.index,
|
||||
direction: enumInvertedDirections[ejectingDirection],
|
||||
const undergroundBeltComp = entity.components.UndergroundBelt;
|
||||
if (undergroundBeltComp) {
|
||||
// Its an underground belt. yay.
|
||||
return function (item) {
|
||||
return undergroundBeltComp.tryAcceptExternalItem(
|
||||
item,
|
||||
hubGoals.getUndergroundBeltBaseSpeed()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const storageComp = entity.components.Storage;
|
||||
if (storageComp) {
|
||||
// It's a storage
|
||||
return function (item) {
|
||||
if (storageComp.canAcceptItem(item)) {
|
||||
storageComp.takeItem(item);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const filterComp = entity.components.Filter;
|
||||
if (filterComp) {
|
||||
// It's a filter! Unfortunately the filter has to know a lot about it's
|
||||
// surrounding state and components, so it can't be within the component itself.
|
||||
return function (item) {
|
||||
if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -365,17 +459,17 @@ export class BeltPath extends BasicSerializableObject {
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
const item = this.items[i];
|
||||
|
||||
if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) {
|
||||
if (item[0 /* nextDistance */] < 0 || item[0 /* nextDistance */] > this.totalLength + 0.02) {
|
||||
return fail(
|
||||
"Item has invalid offset to next item: ",
|
||||
item[_nextDistance],
|
||||
item[0 /* nextDistance */],
|
||||
"(total length:",
|
||||
this.totalLength,
|
||||
")"
|
||||
);
|
||||
}
|
||||
|
||||
currentPos += item[_nextDistance];
|
||||
currentPos += item[0 /* nextDistance */];
|
||||
}
|
||||
|
||||
// Check the total sum matches
|
||||
@ -387,7 +481,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
this.spacingToFirstItem,
|
||||
") and items does not match total length (",
|
||||
this.totalLength,
|
||||
") -> items: " + this.items.map(i => i[_nextDistance]).join("|")
|
||||
") -> items: " + this.items.map(i => i[0 /* nextDistance */]).join("|")
|
||||
);
|
||||
}
|
||||
|
||||
@ -399,43 +493,14 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
// Check acceptor
|
||||
const acceptor = this.computeAcceptingEntityAndSlot(true);
|
||||
if (!!acceptor !== !!this.acceptorTarget) {
|
||||
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.acceptorTarget);
|
||||
}
|
||||
|
||||
if (acceptor) {
|
||||
if (this.acceptorTarget.entity !== acceptor.entity) {
|
||||
return fail(
|
||||
"Mismatching entity on acceptor target:",
|
||||
acceptor.entity.uid,
|
||||
"vs",
|
||||
this.acceptorTarget.entity.uid
|
||||
);
|
||||
}
|
||||
|
||||
if (this.acceptorTarget.slot !== acceptor.slot) {
|
||||
return fail(
|
||||
"Mismatching entity on acceptor target:",
|
||||
acceptor.slot,
|
||||
"vs stored",
|
||||
this.acceptorTarget.slot
|
||||
);
|
||||
}
|
||||
|
||||
if (this.acceptorTarget.direction !== acceptor.direction) {
|
||||
return fail(
|
||||
"Mismatching direction on acceptor target:",
|
||||
acceptor.direction,
|
||||
"vs stored",
|
||||
this.acceptorTarget.direction
|
||||
);
|
||||
}
|
||||
if (!!acceptor !== !!this.boundAcceptor) {
|
||||
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.boundAcceptor);
|
||||
}
|
||||
|
||||
// Check first nonzero offset
|
||||
let firstNonzero = 0;
|
||||
for (let i = this.items.length - 2; i >= 0; --i) {
|
||||
if (this.items[i][_nextDistance] < globalConfig.itemSpacingOnBelts + 1e-5) {
|
||||
if (this.items[i][0 /* nextDistance */] < globalConfig.itemSpacingOnBelts + 1e-5) {
|
||||
++firstNonzero;
|
||||
} else {
|
||||
break;
|
||||
@ -483,11 +548,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
" Extended spacing of last item from",
|
||||
lastItem[_nextDistance],
|
||||
lastItem[0 /* nextDistance */],
|
||||
"to",
|
||||
lastItem[_nextDistance] + additionalLength
|
||||
lastItem[0 /* nextDistance */] + additionalLength
|
||||
);
|
||||
lastItem[_nextDistance] += additionalLength;
|
||||
lastItem[0 /* nextDistance */] += additionalLength;
|
||||
}
|
||||
|
||||
// Assign reference
|
||||
@ -618,7 +683,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"Old items are",
|
||||
this.items.map(i => i[_nextDistance])
|
||||
this.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// Create second path
|
||||
@ -628,7 +693,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
let itemPos = this.spacingToFirstItem;
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
const item = this.items[i];
|
||||
const distanceToNext = item[_nextDistance];
|
||||
const distanceToNext = item[0 /* nextDistance */];
|
||||
|
||||
DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next");
|
||||
|
||||
@ -643,7 +708,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Check if its on the second path (otherwise its on the removed belt and simply lost)
|
||||
if (itemPos >= secondPathStart) {
|
||||
// Put item on second path
|
||||
secondPath.items.push([distanceToNext, item[_item]]);
|
||||
secondPath.items.push([distanceToNext, item[1 /* item */]]);
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
" Put item to second path @",
|
||||
@ -672,7 +737,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
"to",
|
||||
clampedDistanceToNext
|
||||
);
|
||||
item[_nextDistance] = clampedDistanceToNext;
|
||||
item[0 /* nextDistance */] = clampedDistanceToNext;
|
||||
}
|
||||
}
|
||||
|
||||
@ -683,13 +748,13 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"New items are",
|
||||
this.items.map(i => i[_nextDistance])
|
||||
this.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"And second path items are",
|
||||
secondPath.items.map(i => i[_nextDistance])
|
||||
secondPath.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// Adjust our total length
|
||||
@ -776,9 +841,17 @@ export class BeltPath extends BasicSerializableObject {
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]);
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
"Item",
|
||||
i,
|
||||
"is at",
|
||||
itemOffset,
|
||||
"with next offset",
|
||||
item[0 /* nextDistance */]
|
||||
);
|
||||
lastItemOffset = itemOffset;
|
||||
itemOffset += item[_nextDistance];
|
||||
itemOffset += item[0 /* nextDistance */];
|
||||
}
|
||||
|
||||
// If we still have an item, make sure the last item matches
|
||||
@ -805,7 +878,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
this.totalLength,
|
||||
")"
|
||||
);
|
||||
this.items[this.items.length - 1][_nextDistance] = lastDistance;
|
||||
this.items[this.items.length - 1][0 /* nextDistance */] = lastDistance;
|
||||
} else {
|
||||
DEBUG && logger.log(" Removed all items so we'll update spacing to total length");
|
||||
|
||||
@ -893,7 +966,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
DEBUG &&
|
||||
logger.log(
|
||||
" Items:",
|
||||
this.items.map(i => i[_nextDistance])
|
||||
this.items.map(i => i[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// Find offset to first item
|
||||
@ -912,7 +985,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// This item must be dropped
|
||||
this.items.splice(i, 1);
|
||||
i -= 1;
|
||||
itemOffset += item[_nextDistance];
|
||||
itemOffset += item[0 /* nextDistance */];
|
||||
continue;
|
||||
} else {
|
||||
// This item can be kept, thus its the first we know
|
||||
@ -990,9 +1063,13 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Now, update the distance of our last item
|
||||
if (this.items.length !== 0) {
|
||||
const lastItem = this.items[this.items.length - 1];
|
||||
lastItem[_nextDistance] += otherPath.spacingToFirstItem;
|
||||
lastItem[0 /* nextDistance */] += otherPath.spacingToFirstItem;
|
||||
DEBUG &&
|
||||
logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now");
|
||||
logger.log(
|
||||
" Add distance to last item, effectively being",
|
||||
lastItem[0 /* nextDistance */],
|
||||
"now"
|
||||
);
|
||||
} else {
|
||||
// Seems we have no items, update our first item distance
|
||||
this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem;
|
||||
@ -1012,7 +1089,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Aaand push the other paths items
|
||||
for (let i = 0; i < otherPath.items.length; ++i) {
|
||||
const item = otherPath.items[i];
|
||||
this.items.push([item[_nextDistance], item[_item]]);
|
||||
this.items.push([item[0 /* nextDistance */], item[1 /* item */]]);
|
||||
}
|
||||
|
||||
// Update bounds
|
||||
@ -1046,6 +1123,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
this.debug_checkIntegrity("pre-update");
|
||||
}
|
||||
|
||||
// Skip empty belts
|
||||
if (this.items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Divide by item spacing on belts since we use throughput and not speed
|
||||
let beltSpeed =
|
||||
this.root.hubGoals.getBeltBaseSpeed() *
|
||||
@ -1074,30 +1156,40 @@ export class BeltPath extends BasicSerializableObject {
|
||||
lastItemProcessed === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts;
|
||||
|
||||
// Compute how much we can advance
|
||||
const clampedProgress = Math.max(
|
||||
0,
|
||||
Math.min(remainingVelocity, nextDistanceAndItem[_nextDistance] - minimumSpacing)
|
||||
);
|
||||
let clampedProgress = nextDistanceAndItem[0 /* nextDistance */] - minimumSpacing;
|
||||
|
||||
// Make sure we don't advance more than the remaining velocity has stored
|
||||
if (remainingVelocity < clampedProgress) {
|
||||
clampedProgress = remainingVelocity;
|
||||
}
|
||||
|
||||
// Make sure we don't advance back
|
||||
if (clampedProgress < 0) {
|
||||
clampedProgress = 0;
|
||||
}
|
||||
|
||||
// Reduce our velocity by the amount we consumed
|
||||
remainingVelocity -= clampedProgress;
|
||||
|
||||
// Reduce the spacing
|
||||
nextDistanceAndItem[_nextDistance] -= clampedProgress;
|
||||
nextDistanceAndItem[0 /* nextDistance */] -= clampedProgress;
|
||||
|
||||
// Advance all items behind by the progress we made
|
||||
this.spacingToFirstItem += clampedProgress;
|
||||
|
||||
// If the last item can be ejected, eject it and reduce the spacing, because otherwise
|
||||
// we lose velocity
|
||||
if (isFirstItemProcessed && nextDistanceAndItem[_nextDistance] < 1e-7) {
|
||||
if (isFirstItemProcessed && nextDistanceAndItem[0 /* nextDistance */] < 1e-7) {
|
||||
// Store how much velocity we "lost" because we bumped the item to the end of the
|
||||
// belt but couldn't move it any farther. We need this to tell the item acceptor
|
||||
// animation to start a tad later, so everything matches up. Yes I'm a perfectionist.
|
||||
const excessVelocity = beltSpeed - clampedProgress;
|
||||
|
||||
// Try to directly get rid of the item
|
||||
if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) {
|
||||
if (
|
||||
this.boundAcceptor &&
|
||||
this.boundAcceptor(nextDistanceAndItem[1 /* item */], excessVelocity)
|
||||
) {
|
||||
this.items.pop();
|
||||
|
||||
const itemBehind = this.items[lastItemProcessed - 1];
|
||||
@ -1108,11 +1200,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
// Also see #999
|
||||
const fixupProgress = Math.max(
|
||||
0,
|
||||
Math.min(remainingVelocity, itemBehind[_nextDistance])
|
||||
Math.min(remainingVelocity, itemBehind[0 /* nextDistance */])
|
||||
);
|
||||
|
||||
// See above
|
||||
itemBehind[_nextDistance] -= fixupProgress;
|
||||
itemBehind[0 /* nextDistance */] -= fixupProgress;
|
||||
remainingVelocity -= fixupProgress;
|
||||
this.spacingToFirstItem += fixupProgress;
|
||||
}
|
||||
@ -1145,8 +1237,8 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
// Check if we have an item which is ready to be emitted
|
||||
const lastItem = this.items[this.items.length - 1];
|
||||
if (lastItem && lastItem[_nextDistance] === 0 && this.acceptorTarget) {
|
||||
if (this.tryHandOverItem(lastItem[_item])) {
|
||||
if (lastItem && lastItem[0 /* nextDistance */] === 0) {
|
||||
if (this.boundAcceptor && this.boundAcceptor(lastItem[1 /* item */])) {
|
||||
this.items.pop();
|
||||
this.numCompressedItemsAfterFirstItem = Math.max(
|
||||
0,
|
||||
@ -1160,50 +1252,6 @@ export class BeltPath extends BasicSerializableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to hand over the item to the end entity
|
||||
* @param {BaseItem} item
|
||||
*/
|
||||
tryHandOverItem(item, remainingProgress = 0.0) {
|
||||
if (!this.acceptorTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
|
||||
|
||||
// Check if the acceptor has a filter for example
|
||||
if (targetAcceptorComp && !targetAcceptorComp.canAcceptItem(this.acceptorTarget.slot, item)) {
|
||||
// Well, this item is not accepted
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to pass over
|
||||
if (
|
||||
this.root.systemMgr.systems.itemEjector.tryPassOverItem(
|
||||
item,
|
||||
this.acceptorTarget.entity,
|
||||
this.acceptorTarget.slot
|
||||
)
|
||||
) {
|
||||
// Trigger animation on the acceptor comp
|
||||
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
|
||||
if (targetAcceptorComp) {
|
||||
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
this.acceptorTarget.slot,
|
||||
this.acceptorTarget.direction,
|
||||
item,
|
||||
remainingProgress
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a world space position from the given progress
|
||||
* @param {number} progress
|
||||
@ -1270,11 +1318,11 @@ export class BeltPath extends BasicSerializableObject {
|
||||
parameters.context.font = "6px GameFont";
|
||||
parameters.context.fillStyle = "#111";
|
||||
parameters.context.fillText(
|
||||
"" + round4Digits(nextDistanceAndItem[_nextDistance]),
|
||||
"" + round4Digits(nextDistanceAndItem[0 /* nextDistance */]),
|
||||
worldPos.x + 5,
|
||||
worldPos.y + 2
|
||||
);
|
||||
progress += nextDistanceAndItem[_nextDistance];
|
||||
progress += nextDistanceAndItem[0 /* nextDistance */];
|
||||
|
||||
if (this.items.length - 1 - this.numCompressedItemsAfterFirstItem === i) {
|
||||
parameters.context.fillStyle = "red";
|
||||
@ -1370,7 +1418,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
const centerPos = staticComp.localTileToWorld(centerPosLocal).toWorldSpaceCenterOfTile();
|
||||
|
||||
parameters.context.globalAlpha = 0.5;
|
||||
firstItem[_item].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
|
||||
firstItem[1 /* item */].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
|
||||
parameters.context.globalAlpha = 1;
|
||||
}
|
||||
|
||||
@ -1402,7 +1450,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
const distanceAndItem = this.items[currentItemIndex];
|
||||
|
||||
distanceAndItem[_item].drawItemCenteredClipped(
|
||||
distanceAndItem[1 /* item */].drawItemCenteredClipped(
|
||||
worldPos.x,
|
||||
worldPos.y,
|
||||
parameters,
|
||||
@ -1410,7 +1458,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
);
|
||||
|
||||
// Check for the next item
|
||||
currentItemPos += distanceAndItem[_nextDistance];
|
||||
currentItemPos += distanceAndItem[0 /* nextDistance */];
|
||||
++currentItemIndex;
|
||||
|
||||
if (currentItemIndex >= this.items.length) {
|
||||
|
@ -149,29 +149,31 @@ export class Blueprint {
|
||||
*/
|
||||
tryPlace(root, tile) {
|
||||
return root.logic.performBulkOperation(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
||||
continue;
|
||||
return root.logic.performImmutableOperation(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clone = entity.clone();
|
||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||
root.map.placeStaticEntity(clone);
|
||||
root.entityMgr.registerEntity(clone);
|
||||
count++;
|
||||
}
|
||||
|
||||
const clone = entity.clone();
|
||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||
root.map.placeStaticEntity(clone);
|
||||
root.entityMgr.registerEntity(clone);
|
||||
count++;
|
||||
}
|
||||
root.signals.bulkAchievementCheck.dispatch(
|
||||
ACHIEVEMENTS.placeBlueprint,
|
||||
count,
|
||||
ACHIEVEMENTS.placeBp1000,
|
||||
count
|
||||
);
|
||||
|
||||
root.signals.bulkAchievementCheck.dispatch(
|
||||
ACHIEVEMENTS.placeBlueprint,
|
||||
count,
|
||||
ACHIEVEMENTS.placeBp1000,
|
||||
count
|
||||
);
|
||||
|
||||
return count !== 0;
|
||||
return count !== 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,10 @@ export class MetaBalancerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let speedMultiplier = 2;
|
||||
switch (variant) {
|
||||
case enumBalancerVariants.merger:
|
||||
@ -88,9 +92,11 @@ export class MetaBalancerBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getAvailableVariants(root) {
|
||||
let available = [defaultBuildingVariant];
|
||||
const deterministic = root.gameMode.getIsDeterministic();
|
||||
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
|
||||
let available = deterministic ? [] : [defaultBuildingVariant];
|
||||
|
||||
if (!deterministic && root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) {
|
||||
available.push(enumBalancerVariants.merger, enumBalancerVariants.mergerInverse);
|
||||
}
|
||||
|
||||
|
@ -55,6 +55,9 @@ export class MetaBeltBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const beltSpeed = root.hubGoals.getBeltBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
|
||||
}
|
||||
|
30
src/js/game/buildings/block.js
Normal file
@ -0,0 +1,30 @@
|
||||
/* typehints:start */
|
||||
import { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
|
||||
export class MetaBlockBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
super("block");
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#333";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root) {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setupEntityComponents(entity) {}
|
||||
}
|
50
src/js/game/buildings/constant_producer.js
Normal file
@ -0,0 +1,50 @@
|
||||
/* typehints:start */
|
||||
import { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProducerType, ItemProducerComponent } from "../components/item_producer";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
|
||||
export class MetaConstantProducerBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
super("constant_producer");
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#bfd630";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root) {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setupEntityComponents(entity) {
|
||||
entity.addComponent(
|
||||
new ItemEjectorComponent({
|
||||
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
|
||||
})
|
||||
);
|
||||
entity.addComponent(
|
||||
new ItemProducerComponent({
|
||||
type: enumItemProducerType.wireless,
|
||||
})
|
||||
);
|
||||
entity.addComponent(
|
||||
new ConstantSignalComponent({
|
||||
type: enumConstantSignalType.wireless,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -38,6 +38,9 @@ export class MetaCutterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(
|
||||
variant === enumCutterVariants.quad
|
||||
? enumItemProcessorTypes.cutterQuad
|
||||
|
@ -40,6 +40,9 @@ export class MetaFilterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const beltSpeed = root.hubGoals.getBeltBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]];
|
||||
}
|
||||
|
56
src/js/game/buildings/goal_acceptor.js
Normal file
@ -0,0 +1,56 @@
|
||||
/* typehints:start */
|
||||
import { Entity } from "../entity";
|
||||
/* typehints:end */
|
||||
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { enumBeltReaderType, BeltReaderComponent } from "../components/belt_reader";
|
||||
import { GoalAcceptorComponent } from "../components/goal_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
|
||||
export class MetaGoalAcceptorBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
super("goal_acceptor");
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#ce418a";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../../savegame/savegame_serializer").GameRoot} root
|
||||
* @returns
|
||||
*/
|
||||
getIsRemovable(root) {
|
||||
return root.gameMode.getIsEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setupEntityComponents(entity) {
|
||||
entity.addComponent(
|
||||
new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
directions: [enumDirection.bottom],
|
||||
filter: "shape",
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(
|
||||
new ItemProcessorComponent({
|
||||
processorType: enumItemProcessorTypes.goal,
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(new GoalAcceptorComponent({}));
|
||||
}
|
||||
}
|
@ -39,6 +39,6 @@ export class MetaItemProducerBuilding extends MetaBuilding {
|
||||
],
|
||||
})
|
||||
);
|
||||
entity.addComponent(new ItemProducerComponent());
|
||||
entity.addComponent(new ItemProducerComponent({}));
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,9 @@ export class MetaMinerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getMinerBaseSpeed();
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
|
@ -35,6 +35,9 @@ export class MetaMixerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.mixer);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
|
@ -46,6 +46,9 @@ export class MetaPainterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant:
|
||||
case enumPainterVariants.mirrored: {
|
||||
@ -71,7 +74,10 @@ export class MetaPainterBuilding extends MetaBuilding {
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) {
|
||||
variants.push(enumPainterVariants.double);
|
||||
}
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers)) {
|
||||
if (
|
||||
root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) &&
|
||||
root.gameMode.getSupportsWires()
|
||||
) {
|
||||
variants.push(enumPainterVariants.quad);
|
||||
}
|
||||
return variants;
|
||||
|
@ -110,6 +110,6 @@ export class MetaReaderBuilding extends MetaBuilding {
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(new BeltReaderComponent());
|
||||
entity.addComponent(new BeltReaderComponent({}));
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,9 @@ export class MetaRotaterBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
switch (variant) {
|
||||
case defaultBuildingVariant: {
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater);
|
||||
|
@ -28,6 +28,9 @@ export class MetaStackerBuilding extends MetaBuilding {
|
||||
* @returns {Array<[string, string]>}
|
||||
*/
|
||||
getAdditionalStatistics(root, variant) {
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return [];
|
||||
}
|
||||
const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.stacker);
|
||||
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
|
||||
}
|
||||
|
@ -72,13 +72,21 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
|
||||
globalConfig.undergroundBeltMaxTilesByTier[enumUndergroundBeltVariantToTier[variant]];
|
||||
|
||||
const beltSpeed = root.hubGoals.getUndergroundBeltBaseSpeed();
|
||||
return [
|
||||
|
||||
/** @type {Array<[string, string]>} */
|
||||
const stats = [
|
||||
[
|
||||
T.ingame.buildingPlacement.infoTexts.range,
|
||||
T.ingame.buildingPlacement.infoTexts.tiles.replace("<x>", "" + rangeTiles),
|
||||
],
|
||||
[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)],
|
||||
];
|
||||
|
||||
if (root.gameMode.throughputDoesNotMatter()) {
|
||||
return stats;
|
||||
}
|
||||
stats.push([T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -392,13 +392,20 @@ export class Camera extends BasicSerializableObject {
|
||||
return rect.containsPoint(point.x, point.y);
|
||||
}
|
||||
|
||||
getMaximumZoom() {
|
||||
return this.root.gameMode.getMaximumZoom();
|
||||
}
|
||||
|
||||
getMinimumZoom() {
|
||||
return this.root.gameMode.getMinimumZoom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if we can further zoom in
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canZoomIn() {
|
||||
const maxLevel = this.root.app.platformWrapper.getMaximumZoom();
|
||||
return this.zoomLevel <= maxLevel - 0.01;
|
||||
return this.zoomLevel <= this.getMaximumZoom() - 0.01;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -406,8 +413,7 @@ export class Camera extends BasicSerializableObject {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canZoomOut() {
|
||||
const minLevel = this.root.app.platformWrapper.getMinimumZoom();
|
||||
return this.zoomLevel >= minLevel + 0.01;
|
||||
return this.zoomLevel >= this.getMinimumZoom() + 0.01;
|
||||
}
|
||||
|
||||
// EVENTS
|
||||
@ -468,6 +474,7 @@ export class Camera extends BasicSerializableObject {
|
||||
|
||||
// Clamp everything afterwards
|
||||
this.clampZoomLevel();
|
||||
this.clampToBounds();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -743,17 +750,29 @@ export class Camera extends BasicSerializableObject {
|
||||
if (G_IS_DEV && globalConfig.debug.disableZoomLimits) {
|
||||
return;
|
||||
}
|
||||
const wrapper = this.root.app.platformWrapper;
|
||||
|
||||
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel);
|
||||
this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom());
|
||||
this.zoomLevel = clamp(this.zoomLevel, this.getMinimumZoom(), this.getMaximumZoom());
|
||||
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel);
|
||||
|
||||
if (this.desiredZoom) {
|
||||
this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom());
|
||||
this.desiredZoom = clamp(this.desiredZoom, this.getMinimumZoom(), this.getMaximumZoom());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps the center within set boundaries
|
||||
*/
|
||||
clampToBounds() {
|
||||
const bounds = this.root.gameMode.getCameraBounds();
|
||||
if (!bounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tileScaleBounds = this.root.gameMode.getCameraBounds().allScaled(globalConfig.tileSize);
|
||||
this.center.x = clamp(this.center.x, tileScaleBounds.x, tileScaleBounds.x + tileScaleBounds.w);
|
||||
this.center.y = clamp(this.center.y, tileScaleBounds.y, tileScaleBounds.y + tileScaleBounds.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the camera
|
||||
* @param {number} dt Delta time in milliseconds
|
||||
@ -857,6 +876,7 @@ export class Camera extends BasicSerializableObject {
|
||||
// Panning
|
||||
this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06);
|
||||
this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel));
|
||||
this.clampToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
@ -921,6 +941,8 @@ export class Camera extends BasicSerializableObject {
|
||||
((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed()
|
||||
)
|
||||
);
|
||||
|
||||
this.clampToBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1006,6 +1028,8 @@ export class Camera extends BasicSerializableObject {
|
||||
|
||||
this.center.x += moveAmount * forceX * movementSpeed;
|
||||
this.center.y += moveAmount * forceY * movementSpeed;
|
||||
|
||||
this.clampToBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,11 @@ export class Component extends BasicSerializableObject {
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent) {}
|
||||
|
||||
/**
|
||||
* Clears all items and state
|
||||
*/
|
||||
clear() {}
|
||||
|
||||
/* dev:start */
|
||||
|
||||
/**
|
||||
|
@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
import { FilterComponent } from "./components/filter";
|
||||
import { ItemProducerComponent } from "./components/item_producer";
|
||||
import { GoalAcceptorComponent } from "./components/goal_acceptor";
|
||||
|
||||
export function initComponentRegistry() {
|
||||
gComponentRegistry.register(StaticMapEntityComponent);
|
||||
@ -41,6 +42,7 @@ export function initComponentRegistry() {
|
||||
gComponentRegistry.register(BeltReaderComponent);
|
||||
gComponentRegistry.register(FilterComponent);
|
||||
gComponentRegistry.register(ItemProducerComponent);
|
||||
gComponentRegistry.register(GoalAcceptorComponent);
|
||||
|
||||
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
|
||||
|
||||
|
@ -57,6 +57,12 @@ export class BeltComponent extends Component {
|
||||
this.assignedPath = null;
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.assignedPath) {
|
||||
this.assignedPath.clearAllItems();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective length of this belt in tile space
|
||||
* @returns {number}
|
||||
|
@ -3,6 +3,12 @@ import { BaseItem } from "../base_item";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
import { types } from "../../savegame/serialization";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumBeltReaderType = {
|
||||
wired: "wired",
|
||||
wireless: "wireless",
|
||||
};
|
||||
|
||||
export class BeltReaderComponent extends Component {
|
||||
static getId() {
|
||||
return "BeltReader";
|
||||
@ -10,13 +16,24 @@ export class BeltReaderComponent extends Component {
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
type: types.string,
|
||||
lastItem: types.nullable(typeItemSingleton),
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {string=} param0.type
|
||||
*/
|
||||
constructor({ type = enumBeltReaderType.wired }) {
|
||||
super();
|
||||
|
||||
this.type = type;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Which items went through the reader, we only store the time
|
||||
* @type {Array<number>}
|
||||
@ -41,4 +58,8 @@ export class BeltReaderComponent extends Component {
|
||||
*/
|
||||
this.lastThroughputComputation = 0;
|
||||
}
|
||||
|
||||
isWireless() {
|
||||
return this.type === enumBeltReaderType.wireless;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,12 @@ import { Component } from "../component";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumConstantSignalType = {
|
||||
wired: "wired",
|
||||
wireless: "wireless",
|
||||
};
|
||||
|
||||
export class ConstantSignalComponent extends Component {
|
||||
static getId() {
|
||||
return "ConstantSignal";
|
||||
@ -11,6 +17,7 @@ export class ConstantSignalComponent extends Component {
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
type: types.string,
|
||||
signal: types.nullable(typeItemSingleton),
|
||||
};
|
||||
}
|
||||
@ -21,15 +28,22 @@ export class ConstantSignalComponent extends Component {
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent) {
|
||||
otherComponent.signal = this.signal;
|
||||
otherComponent.type = this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} param0
|
||||
* @param {string=} param0.type
|
||||
* @param {BaseItem=} param0.signal The signal to store
|
||||
*/
|
||||
constructor({ signal = null }) {
|
||||
constructor({ signal = null, type = enumConstantSignalType.wired }) {
|
||||
super();
|
||||
this.signal = signal;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
isWireless() {
|
||||
return this.type === enumConstantSignalType.wireless;
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,10 @@ export class FilterComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Items in queue to leave through
|
||||
* @type {Array<PendingFilterItem>}
|
||||
|
49
src/js/game/components/goal_acceptor.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
|
||||
export class GoalAcceptorComponent extends Component {
|
||||
static getId() {
|
||||
return "GoalAcceptor";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
item: typeItemSingleton,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {BaseItem=} param0.item
|
||||
* @param {number=} param0.rate
|
||||
*/
|
||||
constructor({ item = null, rate = null }) {
|
||||
super();
|
||||
|
||||
// ths item to produce
|
||||
/** @type {BaseItem | undefined} */
|
||||
this.item = item;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
// the last items we delivered
|
||||
/** @type {{ item: BaseItem; time: number; }[]} */
|
||||
this.deliveryHistory = [];
|
||||
|
||||
// Used for animations
|
||||
this.displayPercentage = 0;
|
||||
}
|
||||
|
||||
getRequiredDeliveryHistorySize() {
|
||||
return (
|
||||
(globalConfig.puzzleModeSpeed *
|
||||
globalConfig.goalAcceptorMinimumDurationSeconds *
|
||||
globalConfig.beltSpeedItemsPerSecond) /
|
||||
globalConfig.goalAcceptorsPerProducer
|
||||
);
|
||||
}
|
||||
}
|
@ -36,6 +36,11 @@ export class ItemAcceptorComponent extends Component {
|
||||
constructor({ slots = [] }) {
|
||||
super();
|
||||
|
||||
this.setSlots(slots);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Fixes belt animations
|
||||
* @type {Array<{
|
||||
@ -46,8 +51,6 @@ export class ItemAcceptorComponent extends Component {
|
||||
* }>}
|
||||
*/
|
||||
this.itemConsumptionAnimations = [];
|
||||
|
||||
this.setSlots(slots);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,6 +74,8 @@ export class ItemAcceptorComponent extends Component {
|
||||
|
||||
/**
|
||||
* Returns if this acceptor can accept a new item at slot N
|
||||
*
|
||||
* NOTICE: The belt path ignores this for performance reasons and does his own check
|
||||
* @param {number} slotIndex
|
||||
* @param {BaseItem=} item
|
||||
*/
|
||||
|
@ -48,6 +48,13 @@ export class ItemEjectorComponent extends Component {
|
||||
this.renderFloatingItems = renderFloatingItems;
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const slot of this.slots) {
|
||||
slot.item = null;
|
||||
slot.progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on
|
||||
*/
|
||||
|
@ -19,6 +19,7 @@ export const enumItemProcessorTypes = {
|
||||
hub: "hub",
|
||||
filter: "filter",
|
||||
reader: "reader",
|
||||
goal: "goal",
|
||||
};
|
||||
|
||||
/** @enum {string} */
|
||||
@ -63,10 +64,8 @@ export class ItemProcessorComponent extends Component {
|
||||
}) {
|
||||
super();
|
||||
|
||||
// Which slot to emit next, this is only a preference and if it can't emit
|
||||
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
|
||||
// sure the outputs always match
|
||||
this.nextOutputSlot = 0;
|
||||
// How many inputs we need for one charge
|
||||
this.inputsPerCharge = inputsPerCharge;
|
||||
|
||||
// Type of the processor
|
||||
this.type = processorType;
|
||||
@ -74,8 +73,14 @@ export class ItemProcessorComponent extends Component {
|
||||
// Type of processing requirement
|
||||
this.processingRequirement = processingRequirement;
|
||||
|
||||
// How many inputs we need for one charge
|
||||
this.inputsPerCharge = inputsPerCharge;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
// Which slot to emit next, this is only a preference and if it can't emit
|
||||
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
|
||||
// sure the outputs always match
|
||||
this.nextOutputSlot = 0;
|
||||
|
||||
/**
|
||||
* Our current inputs
|
||||
@ -104,7 +109,11 @@ export class ItemProcessorComponent extends Component {
|
||||
* @param {number} sourceSlot
|
||||
*/
|
||||
tryTakeItem(item, sourceSlot) {
|
||||
if (this.type === enumItemProcessorTypes.hub || this.type === enumItemProcessorTypes.trash) {
|
||||
if (
|
||||
this.type === enumItemProcessorTypes.hub ||
|
||||
this.type === enumItemProcessorTypes.trash ||
|
||||
this.type === enumItemProcessorTypes.goal
|
||||
) {
|
||||
// Hub has special logic .. not really nice but efficient.
|
||||
this.inputSlots.push({ item, sourceSlot });
|
||||
return true;
|
||||
|
@ -1,7 +1,33 @@
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { Component } from "../component";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumItemProducerType = {
|
||||
wired: "wired",
|
||||
wireless: "wireless",
|
||||
};
|
||||
|
||||
export class ItemProducerComponent extends Component {
|
||||
static getId() {
|
||||
return "ItemProducer";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
type: types.string,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {string=} param0.type
|
||||
*/
|
||||
constructor({ type = enumItemProducerType.wired }) {
|
||||
super();
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
isWireless() {
|
||||
return this.type === enumItemProducerType.wireless;
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +24,6 @@ export class MinerComponent extends Component {
|
||||
this.lastMiningTime = 0;
|
||||
this.chainable = chainable;
|
||||
|
||||
/**
|
||||
* Stores items from other miners which were chained to this
|
||||
* miner.
|
||||
* @type {Array<BaseItem>}
|
||||
*/
|
||||
this.itemChainBuffer = [];
|
||||
|
||||
/**
|
||||
* @type {BaseItem}
|
||||
*/
|
||||
@ -42,6 +35,17 @@ export class MinerComponent extends Component {
|
||||
* @type {Entity|null|false}
|
||||
*/
|
||||
this.cachedChainedMiner = null;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Stores items from other miners which were chained to this
|
||||
* miner.
|
||||
* @type {Array<BaseItem>}
|
||||
*/
|
||||
this.itemChainBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,6 +71,14 @@ export class StaticMapEntityComponent extends Component {
|
||||
return getBuildingDataFromCode(this.code).variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the buildings rotation variant
|
||||
* @returns {number}
|
||||
*/
|
||||
getRotationVariant() {
|
||||
return getBuildingDataFromCode(this.code).rotationVariant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the current state to another component
|
||||
* @param {Component} otherComponent
|
||||
|
@ -41,6 +41,17 @@ export class UndergroundBeltComponent extends Component {
|
||||
this.mode = mode;
|
||||
this.tier = tier;
|
||||
|
||||
/**
|
||||
* The linked entity, used to speed up performance. This contains either
|
||||
* the entrance or exit depending on the tunnel type
|
||||
* @type {LinkedUndergroundBelt}
|
||||
*/
|
||||
this.cachedLinkedEntity = null;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/** @type {Array<{ item: BaseItem, progress: number }>} */
|
||||
this.consumptionAnimations = [];
|
||||
|
||||
@ -51,13 +62,6 @@ export class UndergroundBeltComponent extends Component {
|
||||
* @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item]
|
||||
*/
|
||||
this.pendingItems = [];
|
||||
|
||||
/**
|
||||
* The linked entity, used to speed up performance. This contains either
|
||||
* the entrance or exit depending on the tunnel type
|
||||
* @type {LinkedUndergroundBelt}
|
||||
*/
|
||||
this.cachedLinkedEntity = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,7 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
|
||||
import { GameLogic } from "./logic";
|
||||
import { MapView } from "./map_view";
|
||||
import { defaultBuildingVariant } from "./meta_building";
|
||||
import { RegularGameMode } from "./modes/regular";
|
||||
import { GameMode } from "./game_mode";
|
||||
import { ProductionAnalytics } from "./production_analytics";
|
||||
import { GameRoot } from "./root";
|
||||
import { ShapeDefinitionManager } from "./shape_definition_manager";
|
||||
@ -82,7 +82,9 @@ export class GameCore {
|
||||
* @param {import("../states/ingame").InGameState} parentState
|
||||
* @param {Savegame} savegame
|
||||
*/
|
||||
initializeRoot(parentState, savegame) {
|
||||
initializeRoot(parentState, savegame, gameModeId) {
|
||||
logger.log("initializing root");
|
||||
|
||||
// Construct the root element, this is the data representation of the game
|
||||
this.root = new GameRoot(this.app);
|
||||
this.root.gameState = parentState;
|
||||
@ -100,12 +102,12 @@ export class GameCore {
|
||||
// This isn't nice, but we need it right here
|
||||
root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever);
|
||||
|
||||
// Init game mode
|
||||
root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters);
|
||||
|
||||
// Needs to come first
|
||||
root.dynamicTickrate = new DynamicTickrate(root);
|
||||
|
||||
// Init game mode
|
||||
root.gameMode = new RegularGameMode(root);
|
||||
|
||||
// Init classes
|
||||
root.camera = new Camera(root);
|
||||
root.map = new MapView(root);
|
||||
@ -157,6 +159,8 @@ export class GameCore {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.log("root initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,6 +172,10 @@ export class GameCore {
|
||||
this.root.gameIsFresh = true;
|
||||
this.root.map.seed = randomInt(0, 100000);
|
||||
|
||||
if (!this.root.gameMode.hasHub()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Place the hub
|
||||
const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({
|
||||
root: this.root,
|
||||
@ -447,7 +455,9 @@ export class GameCore {
|
||||
systems.hub.draw(params);
|
||||
|
||||
// Green wires overlay
|
||||
root.hud.parts.wiresOverlay.draw(params);
|
||||
if (root.hud.parts.wiresOverlay) {
|
||||
root.hud.parts.wiresOverlay.draw(params);
|
||||
}
|
||||
|
||||
if (this.root.currentLayer === "wires") {
|
||||
// Static map entities
|
||||
|
@ -23,10 +23,16 @@ export class DynamicTickrate {
|
||||
|
||||
this.averageFps = 60;
|
||||
|
||||
this.setTickRate(this.root.app.settings.getDesiredFps());
|
||||
const fixedRate = this.root.gameMode.getFixedTickrate();
|
||||
if (fixedRate) {
|
||||
logger.log("Setting fixed tickrate of", fixedRate);
|
||||
this.setTickRate(fixedRate);
|
||||
} else {
|
||||
this.setTickRate(this.root.app.settings.getDesiredFps());
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
this.setTickRate(300);
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
this.setTickRate(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,9 +105,7 @@ export class DynamicTickrate {
|
||||
|
||||
this.averageTickDuration = average;
|
||||
|
||||
const desiredFps = this.root.app.settings.getDesiredFps();
|
||||
|
||||
// Disabled for now: Dynamicall adjusting tick rate
|
||||
// Disabled for now: Dynamically adjusting tick rate
|
||||
// if (this.averageFps > desiredFps * 0.9) {
|
||||
// // if (average < maxTickDuration) {
|
||||
// this.increaseTickRate();
|
||||
|
@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
import { FilterComponent } from "./components/filter";
|
||||
import { ItemProducerComponent } from "./components/item_producer";
|
||||
import { GoalAcceptorComponent } from "./components/goal_acceptor";
|
||||
/* typehints:end */
|
||||
|
||||
/**
|
||||
@ -89,6 +90,9 @@ export class EntityComponentStorage {
|
||||
/** @type {ItemProducerComponent} */
|
||||
this.ItemProducer;
|
||||
|
||||
/** @type {GoalAcceptorComponent} */
|
||||
this.GoalAcceptor;
|
||||
|
||||
/* typehints:end */
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +1,192 @@
|
||||
/* typehints:start */
|
||||
import { enumHubGoalRewards } from "./tutorial_goals";
|
||||
import { GameRoot } from "./root";
|
||||
/* typehints:end */
|
||||
|
||||
import { GameRoot } from "./root";
|
||||
import { Rectangle } from "../core/rectangle";
|
||||
import { gGameModeRegistry } from "../core/global_registries";
|
||||
import { types, BasicSerializableObject } from "../savegame/serialization";
|
||||
import { MetaBuilding } from "./meta_building";
|
||||
import { MetaItemProducerBuilding } from "./buildings/item_producer";
|
||||
import { BaseHUDPart } from "./hud/base_hud_part";
|
||||
|
||||
/** @typedef {{
|
||||
* shape: string,
|
||||
* amount: number
|
||||
* }} UpgradeRequirement */
|
||||
/** @enum {string} */
|
||||
export const enumGameModeIds = {
|
||||
puzzleEdit: "puzzleEditMode",
|
||||
puzzlePlay: "puzzlePlayMode",
|
||||
regular: "regularMode",
|
||||
};
|
||||
|
||||
/** @typedef {{
|
||||
* required: Array<UpgradeRequirement>
|
||||
* improvement?: number,
|
||||
* excludePrevious?: boolean
|
||||
* }} TierRequirement */
|
||||
/** @enum {string} */
|
||||
export const enumGameModeTypes = {
|
||||
default: "defaultModeType",
|
||||
puzzle: "puzzleModeType",
|
||||
};
|
||||
|
||||
/** @typedef {Array<TierRequirement>} UpgradeTiers */
|
||||
export class GameMode extends BasicSerializableObject {
|
||||
/** @returns {string} */
|
||||
static getId() {
|
||||
abstract;
|
||||
return "unknownMode";
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
static getType() {
|
||||
abstract;
|
||||
return "unknownType";
|
||||
}
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
* @param {string} [id=Regular]
|
||||
* @param {object|undefined} payload
|
||||
*/
|
||||
static create(root, id = enumGameModeIds.regular, payload = undefined) {
|
||||
return new (gGameModeRegistry.findById(id))(root, payload);
|
||||
}
|
||||
|
||||
/** @typedef {{
|
||||
* shape: string,
|
||||
* required: number,
|
||||
* reward: enumHubGoalRewards,
|
||||
* throughputOnly?: boolean
|
||||
* }} LevelDefinition */
|
||||
|
||||
export class GameMode {
|
||||
/**
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
this.root = root;
|
||||
|
||||
/**
|
||||
* @type {Record<string, typeof BaseHUDPart>}
|
||||
*/
|
||||
this.additionalHudParts = {};
|
||||
|
||||
/** @type {typeof MetaBuilding[]} */
|
||||
this.hiddenBuildings = [MetaItemProducerBuilding];
|
||||
}
|
||||
|
||||
/** @returns {object} */
|
||||
serialize() {
|
||||
return {
|
||||
$: this.getId(),
|
||||
data: super.serialize(),
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {object} savedata */
|
||||
deserialize({ data }) {
|
||||
super.deserialize(data, this.root);
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
getId() {
|
||||
// @ts-ignore
|
||||
return this.constructor.getId();
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
getType() {
|
||||
// @ts-ignore
|
||||
return this.constructor.getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return all available upgrades
|
||||
* @returns {Object<string, UpgradeTiers>}
|
||||
*/
|
||||
getUpgrades() {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the blueprint shape key
|
||||
* @returns {string}
|
||||
*/
|
||||
getBlueprintShapeKey() {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the goals for all levels including their reward
|
||||
* @returns {Array<LevelDefinition>}
|
||||
*/
|
||||
getLevelDefinitions() {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return whether free play is available or if the game stops
|
||||
* after the predefined levels
|
||||
* @param {typeof MetaBuilding} building - Class name of building
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIsFreeplayAvailable() {
|
||||
isBuildingExcluded(building) {
|
||||
return this.hiddenBuildings.indexOf(building) >= 0;
|
||||
}
|
||||
|
||||
/** @returns {undefined|Rectangle[]} */
|
||||
getBuildableZones() {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {Rectangle|undefined} */
|
||||
getCameraBounds() {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasHub() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasResources() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
getMinimumZoom() {
|
||||
return 0.1;
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
getMaximumZoom() {
|
||||
return 3.5;
|
||||
}
|
||||
|
||||
/** @returns {Object<string, Array>} */
|
||||
getUpgrades() {
|
||||
return {
|
||||
belt: [],
|
||||
miner: [],
|
||||
processors: [],
|
||||
painting: [],
|
||||
};
|
||||
}
|
||||
|
||||
throughputDoesNotMatter() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
*/
|
||||
adjustZone(w = 0, h = 0) {
|
||||
abstract;
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {array} */
|
||||
getLevelDefinitions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsFreeplayAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsSaveable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getSupportsCopyPaste() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getSupportsWires() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsDeterministic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns {number | undefined} */
|
||||
getFixedTickrate() {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
getBlueprintShapeKey() {
|
||||
return "CbCbCbRb:CwCwCwCw";
|
||||
}
|
||||
}
|
||||
|
10
src/js/game/game_mode_registry.js
Normal file
@ -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);
|
||||
}
|