1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

Merge branch 'master' into master

This commit is contained in:
EmeraldBlock 2020-09-28 11:04:58 -05:00 committed by GitHub
commit 1de6bdcf16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
124 changed files with 17755 additions and 13323 deletions

4
.gitattributes vendored
View File

@ -1,4 +0,0 @@
*.wav filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text

View File

@ -22,8 +22,6 @@ Your goal is to produce shapes by cutting, rotating, merging and painting parts
## Building
- Make sure git `git lfs` extension is on your path
- Run `git lfs pull` to download sound assets
- Make sure `ffmpeg` is on your path
- Install Node.js and Yarn
- Run `yarn` in the root folder

1
gulp/.gitattributes vendored
View File

@ -1 +0,0 @@
*.wav filter=lfs diff=lfs merge=lfs -text

View File

@ -25,6 +25,14 @@ module.exports = {
});
},
getTag() {
try {
return execSync("git describe --tag --exact-match").toString("ascii");
} catch (e) {
throw new Error('Current git HEAD is not a version tag');
}
},
getVersion() {
return trim(fs.readFileSync(path.join(__dirname, "..", "version")).toString());
},

12
gulp/entitlements.plist Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>

View File

@ -42,6 +42,10 @@ const envVars = [
"SHAPEZ_CLI_STAGING_FTP_PW",
"SHAPEZ_CLI_LIVE_FTP_USER",
"SHAPEZ_CLI_LIVE_FTP_PW",
"SHAPEZ_CLI_APPLE_ID",
"SHAPEZ_CLI_APPLE_CERT_NAME",
"SHAPEZ_CLI_GITHUB_USER",
"SHAPEZ_CLI_GITHUB_TOKEN",
];
for (let i = 0; i < envVars.length; ++i) {
@ -78,6 +82,9 @@ docs.gulptasksDocs($, gulp, buildFolder);
const standalone = require("./standalone");
standalone.gulptasksStandalone($, gulp, buildFolder);
const releaseUploader = require("./release-uploader");
releaseUploader.gulptasksReleaseUploader($, gulp, buildFolder);
const translations = require("./translations");
translations.gulptasksTranslations($, gulp, buildFolder);
@ -299,6 +306,17 @@ gulp.task(
gulp.series("utils.cleanup", "step.standalone-prod.all", "step.postbuild")
);
// OS X build and release upload
gulp.task(
"build.darwin64-prod",
gulp.series(
"build.standalone-prod",
"standalone.prepare",
"standalone.package.prod.darwin64",
"standalone.uploadRelease.darwin64"
)
);
// Deploying!
gulp.task(
"main.deploy.alpha",

View File

@ -54,19 +54,19 @@ function gulptasksHTML($, gulp, buildFolder) {
document.head.appendChild(css);
// Append async css
const asyncCss = document.createElement("link");
asyncCss.rel = "stylesheet";
asyncCss.type = "text/css";
asyncCss.media = "none";
asyncCss.setAttribute("onload", "this.media='all'");
asyncCss.href = cachebust("async-resources.css");
if (integrity) {
asyncCss.setAttribute(
"integrity",
computeIntegrityHash(path.join(buildFolder, "async-resources.css"))
);
}
document.head.appendChild(asyncCss);
// const asyncCss = document.createElement("link");
// asyncCss.rel = "stylesheet";
// asyncCss.type = "text/css";
// asyncCss.media = "none";
// asyncCss.setAttribute("onload", "this.media='all'");
// asyncCss.href = cachebust("async-resources.css");
// if (integrity) {
// asyncCss.setAttribute(
// "integrity",
// computeIntegrityHash(path.join(buildFolder, "async-resources.css"))
// );
// }
// document.head.appendChild(asyncCss);
if (app) {
// Append cordova link

66
gulp/release-uploader.js Normal file
View File

@ -0,0 +1,66 @@
const path = require("path");
const fs = require("fs");
const execSync = require("child_process").execSync;
const { Octokit } = require("@octokit/rest");
const buildutils = require("./buildutils");
function gulptasksReleaseUploader($, gulp, buildFolder) {
const standaloneDir = path.join(__dirname, "..", "tmp_standalone_files");
const darwinApp = path.join(standaloneDir, "shapez.io-standalone-darwin-x64", "shapez.io-standalone.app");
const dmgName = "shapez.io-standalone.dmg";
const dmgPath = path.join(standaloneDir, "shapez.io-standalone-darwin-x64", dmgName);
gulp.task("standalone.uploadRelease.darwin64.cleanup", () => {
return gulp.src(dmgPath, { read: false, allowEmpty: true }).pipe($.clean({ force: true }));
});
gulp.task("standalone.uploadRelease.darwin64.compress", cb => {
console.log("Packaging disk image", dmgPath);
execSync(`hdiutil create -format UDBZ -srcfolder ${darwinApp} ${dmgPath}`);
cb();
});
gulp.task("standalone.uploadRelease.darwin64.upload", async cb => {
const currentTag = buildutils.getTag();
const octokit = new Octokit({
auth: process.env.SHAPEZ_CLI_GITHUB_TOKEN
});
const createdRelease = await octokit.request("POST /repos/{owner}/{repo}/releases", {
owner: process.env.SHAPEZ_CLI_GITHUB_USER,
repo: "shapez.io",
tag_name: currentTag,
name: currentTag,
draft: true
});
const { data: { id, upload_url } } = createdRelease;
console.log(`Created release ${id} for tag ${currentTag}`);
const dmgContents = fs.readFileSync(dmgPath);
const dmgSize = fs.statSync(dmgPath).size;
console.log("Uploading", dmgContents.length / 1024 / 1024, "MB to", upload_url);
await octokit.request({
method: "POST",
url: upload_url,
headers: {
"content-type": "application/x-apple-diskimage"
},
name: dmgName,
data: dmgContents
});
cb();
});
gulp.task("standalone.uploadRelease.darwin64",
gulp.series(
"standalone.uploadRelease.darwin64.cleanup",
"standalone.uploadRelease.darwin64.compress",
"standalone.uploadRelease.darwin64.upload"
));
}
module.exports = { gulptasksReleaseUploader };

View File

@ -1,3 +1,4 @@
require('colors');
const packager = require("electron-packager");
const path = require("path");
const { getVersion } = require("./buildutils");
@ -80,8 +81,9 @@ function gulptasksStandalone($, gulp) {
* @param {'win32'|'linux'|'darwin'} platform
* @param {'x64'|'ia32'} arch
* @param {function():void} cb
* @param {boolean=} isRelease
*/
function packageStandalone(platform, arch, cb) {
function packageStandalone(platform, arch, cb, isRelease = true) {
const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml"));
packager({
@ -99,6 +101,20 @@ function gulptasksStandalone($, gulp) {
overwrite: true,
appBundleId: "io.shapez.standalone",
appCategoryType: "public.app-category.games",
...(isRelease && platform === "darwin" && {
osxSign: {
identity: process.env.SHAPEZ_CLI_APPLE_CERT_NAME,
"hardened-runtime": true,
hardenedRuntime: true,
entitlements: 'entitlements.plist',
'entitlements-inherit': 'entitlements.plist',
'signature-flags': 'library'
},
osxNotarize: {
appleId: process.env.SHAPEZ_CLI_APPLE_ID,
appleIdPassword: "@keychain:SHAPEZ_CLI_APPLE_ID"
}
})
}).then(
appPaths => {
console.log("Packages created:", appPaths);
@ -123,7 +139,11 @@ function gulptasksStandalone($, gulp) {
fs.chmodSync(path.join(appPath, "play.sh"), 0o775);
}
if (platform === "darwin") {
if (process.platform === "win32" && platform === "darwin") {
console.warn("Cross-building for macOS on Windows: dereferencing symlinks.\n".red +
"This will nearly double app size and make code signature invalid. Sorry!\n".red.bold +
"For more information, see " + "https://github.com/electron/electron-packager/issues/71".underline);
// Clear up framework folders
fs.writeFileSync(
path.join(appPath, "play.sh"),
@ -175,6 +195,7 @@ function gulptasksStandalone($, gulp) {
gulp.task("standalone.package.prod.linux64", cb => packageStandalone("linux", "x64", cb));
gulp.task("standalone.package.prod.linux32", cb => packageStandalone("linux", "ia32", cb));
gulp.task("standalone.package.prod.darwin64", cb => packageStandalone("darwin", "x64", cb));
gulp.task("standalone.package.prod.darwin64.unsigned", cb => packageStandalone("darwin", "x64", cb, false));
gulp.task(
"standalone.package.prod",

View File

@ -71,6 +71,7 @@
"yawn-yaml": "^1.5.0"
},
"devDependencies": {
"@octokit/rest": "^18.0.6",
"@typescript-eslint/eslint-plugin": "3.0.1",
"@typescript-eslint/parser": "3.0.1",
"autoprefixer": "^9.4.3",

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 114 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 283 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 KiB

After

Width:  |  Height:  |  Size: 706 KiB

View File

@ -118,7 +118,7 @@
<key>textureFormat</key>
<enum type="SettingsBase::TextureFormat">png</enum>
<key>borderPadding</key>
<uint>3</uint>
<uint>4</uint>
<key>maxTextureSize</key>
<QSize>
<key>width</key>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df7487fb5e8cb34cecee2519b9c3162a5107d2d7b1301c4a550904cfb108a015
size 223361394

BIN
res_raw/sounds/sfx/copy.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,8 +1,24 @@
#ingame_HUD_BetaOverlay {
position: fixed;
@include S(top, 10px);
@include S(right, 15px);
left: 50%;
transform: translateX(-50%);
color: $colorRedBright;
@include Heading;
text-transform: uppercase;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
h2 {
@include PlainText;
}
span {
color: #555;
@include SuperSmallText;
}
}

View File

@ -178,6 +178,27 @@
display: list-item;
}
}
.ingameItemChooser {
@include S(margin, 20px, 0);
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
@include S(grid-column-gap, 3px);
canvas {
pointer-events: all;
@include S(width, 25px);
@include S(height, 25px);
position: relative;
cursor: pointer;
@include IncreasedClickArea(3px);
&:hover {
opacity: 0.9;
}
}
}
}
> .buttons {
@ -220,7 +241,7 @@
content: " ";
display: inline-block;
background: rgba(#fff, 0.6);
@include InlineAnimation(5s linear) {
@include InlineAnimation(3s linear) {
0% {
width: 100%;
}

View File

@ -13,7 +13,7 @@ $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,
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored;
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator;
@each $building in $buildingsAndVariants {
[data-icon="building_tutorials/#{$building}.png"] {
/* @load-async */

View File

@ -1,43 +1,9 @@
export const CHANGELOG = [
{
version: "1.2.0",
date: "unreleased",
date: "09.10.2020",
entries: [
"WIRES",
"Reworked menu UI design (by dengr1605)",
"Allow holding ALT in belt planner to reverse direction (by jakobhellermann)",
"Clear cursor when trying to pipette the same building twice (by hexy)",
"Fixed level 18 stacker bug: If you experienced it already, you know it, if not, I don't want to spoiler (by hexy)",
"Added keybinding to close menus (by isaisstillalive / Sandwichs-del)",
"Fix rare crash regarding the buildings toolbar (by isaisstillalive)",
"Fixed some phrases (by EnderDoom77)",
"Zoom towards mouse cursor (by Dimava)",
"Added multiple settings to optimize the performance",
"Updated the soundtrack again, it is now 40 minutes in total!",
"Added a button to the statistics dialog to disable the sorting (by squeek502)",
"Tier 2 tunnels are now 9 tiles wide, so the gap between is 8 tiles (double the tier 1 range)",
"Updated and added new translations (Thanks to all contributors!)",
"Show connected chained miners on hover",
"Added setting to be able to delete buildings while placing (inspired by hexy)",
"You can now adjust the sound and music volumes! (inspired by Yoshie2000)",
"Some hud elements now have reduced opacity when hovering, so you can see through (inspired by mvb005)",
"Mark pinned shapes in statistics dialog and show them first (inspired by davidburhans)",
"Added setting to show chunk borders",
"Quad painters have been reworked! They now are integrated with the wires, and only paint the shape when the value is 1 (inspired by dengr1605)",
"There are now compact 1x1 balancers available to be unlocked!",
"Replaced level completion sound to be less distracting",
"Allow editing waypoints (by isaisstillalive)",
"Show confirmation when cutting area which is too expensive to get pasted again (by isaisstillalive)",
"Show mouse and camera tile on debug overlay (F4) (by dengr)",
"Fix belt planner placing the belt when a dialog opens in the meantime",
"Added confirmation when deleting a savegame",
"Make chained mainer the default and only option after unlocking it",
"Fixed tunnels entrances connecting to exits sometimes when they shouldn't",
"You can now pan the map with your mouse by moving the cursor to the edges of the screen!",
"Added setting to auto select the extractor when pipetting a resource patch (by Exund)",
"You can now change the unit (seconds / minutes / hours) in the statistics dialog",
"The initial belt planner direction is now based on the cursor movement (by MizardX)",
"Fix preferred variant not getting saved when clicking on the hud (by Danacus)",
"⚠This update is HUGE, view the full changelog <a href='https://shapez.io/wires/' target='_blank'>here</a>! ⚠️⚠️",
],
},
{

View File

@ -1,215 +1,232 @@
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { Loader } from "./loader";
import { createLogger } from "./logging";
import { Signal } from "./signal";
import { SOUNDS, MUSIC } from "../platform/sound";
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
const logger = createLogger("background_loader");
const essentialMainMenuSprites = [
"logo.png",
...G_ALL_UI_IMAGES.filter(src => src.startsWith("ui/") && src.indexOf(".gif") < 0),
];
const essentialMainMenuSounds = [
SOUNDS.uiClick,
SOUNDS.uiError,
SOUNDS.dialogError,
SOUNDS.dialogOk,
SOUNDS.swishShow,
SOUNDS.swishHide,
];
const essentialBareGameAtlases = atlasFiles;
const essentialBareGameSprites = G_ALL_UI_IMAGES.filter(src => src.indexOf(".gif") < 0);
const essentialBareGameSounds = [MUSIC.theme];
const additionalGameSprites = [];
// @ts-ignore
const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)];
export class BackgroundResourcesLoader {
/**
*
* @param {Application} app
*/
constructor(app) {
this.app = app;
this.registerReady = false;
this.mainMenuReady = false;
this.bareGameReady = false;
this.additionalReady = false;
this.signalMainMenuLoaded = new Signal();
this.signalBareGameLoaded = new Signal();
this.signalAdditionalLoaded = new Signal();
this.numAssetsLoaded = 0;
this.numAssetsToLoadTotal = 0;
// Avoid loading stuff twice
this.spritesLoaded = [];
this.soundsLoaded = [];
}
getNumAssetsLoaded() {
return this.numAssetsLoaded;
}
getNumAssetsTotal() {
return this.numAssetsToLoadTotal;
}
getPromiseForMainMenu() {
if (this.mainMenuReady) {
return Promise.resolve();
}
return new Promise(resolve => {
this.signalMainMenuLoaded.add(resolve);
});
}
getPromiseForBareGame() {
if (this.bareGameReady) {
return Promise.resolve();
}
return new Promise(resolve => {
this.signalBareGameLoaded.add(resolve);
});
}
startLoading() {
this.internalStartLoadingEssentialsForMainMenu();
}
internalStartLoadingEssentialsForMainMenu() {
logger.log("⏰ Start load: main menu");
this.internalLoadSpritesAndSounds(essentialMainMenuSprites, essentialMainMenuSounds)
.catch(err => {
logger.warn("⏰ Failed to load essentials for main menu:", err);
})
.then(() => {
logger.log("⏰ Finish load: main menu");
this.mainMenuReady = true;
this.signalMainMenuLoaded.dispatch();
this.internalStartLoadingEssentialsForBareGame();
});
}
internalStartLoadingEssentialsForBareGame() {
logger.log("⏰ Start load: bare game");
this.internalLoadSpritesAndSounds(
essentialBareGameSprites,
essentialBareGameSounds,
essentialBareGameAtlases
)
.catch(err => {
logger.warn("⏰ Failed to load essentials for bare game:", err);
})
.then(() => {
logger.log("⏰ Finish load: bare game");
this.bareGameReady = true;
initBuildingCodesAfterResourcesLoaded();
this.signalBareGameLoaded.dispatch();
this.internalStartLoadingAdditionalGameAssets();
});
}
internalStartLoadingAdditionalGameAssets() {
const additionalAtlases = [];
logger.log("⏰ Start load: additional assets (", additionalAtlases.length, "images)");
this.internalLoadSpritesAndSounds(additionalGameSprites, additionalGameSounds, additionalAtlases)
.catch(err => {
logger.warn("⏰ Failed to load additional assets:", err);
})
.then(() => {
logger.log("⏰ Finish load: additional assets");
this.additionalReady = true;
this.signalAdditionalLoaded.dispatch();
});
}
/**
* @param {Array<string>} sprites
* @param {Array<string>} sounds
* @param {Array<AtlasDefinition>} atlases
* @returns {Promise<void>}
*/
internalLoadSpritesAndSounds(sprites, sounds, atlases = []) {
this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length;
this.numAssetsLoaded = 0;
let promises = [];
for (let i = 0; i < sounds.length; ++i) {
if (this.soundsLoaded.indexOf(sounds[i]) >= 0) {
// Already loaded
continue;
}
this.soundsLoaded.push(sounds[i]);
promises.push(
this.app.sound
.loadSound(sounds[i])
.catch(err => {
logger.warn("Failed to load sound:", sounds[i]);
})
.then(() => {
this.numAssetsLoaded++;
})
);
}
for (let i = 0; i < sprites.length; ++i) {
if (this.spritesLoaded.indexOf(sprites[i]) >= 0) {
// Already loaded
continue;
}
this.spritesLoaded.push(sprites[i]);
promises.push(
Loader.preloadCSSSprite(sprites[i])
.catch(err => {
logger.warn("Failed to load css sprite:", sprites[i]);
})
.then(() => {
this.numAssetsLoaded++;
})
);
}
for (let i = 0; i < atlases.length; ++i) {
const atlas = atlases[i];
promises.push(
Loader.preloadAtlas(atlas)
.catch(err => {
logger.warn("Failed to load atlas:", atlas.sourceFileName);
})
.then(() => {
this.numAssetsLoaded++;
})
);
}
return (
Promise.all(promises)
// // Remove some pressure by waiting a bit
// .then(() => {
// return new Promise(resolve => {
// setTimeout(resolve, 200);
// });
// })
.then(() => {
this.numAssetsToLoadTotal = 0;
this.numAssetsLoaded = 0;
})
);
}
}
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { Loader } from "./loader";
import { createLogger } from "./logging";
import { Signal } from "./signal";
import { SOUNDS, MUSIC } from "../platform/sound";
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
import { cachebust } from "./cachebust";
const logger = createLogger("background_loader");
const essentialMainMenuSprites = [
"logo.png",
...G_ALL_UI_IMAGES.filter(src => src.startsWith("ui/") && src.indexOf(".gif") < 0),
];
const essentialMainMenuSounds = [
SOUNDS.uiClick,
SOUNDS.uiError,
SOUNDS.dialogError,
SOUNDS.dialogOk,
SOUNDS.swishShow,
SOUNDS.swishHide,
];
const essentialBareGameAtlases = atlasFiles;
const essentialBareGameSprites = G_ALL_UI_IMAGES.filter(src => src.indexOf(".gif") < 0);
const essentialBareGameSounds = [MUSIC.theme];
const additionalGameSprites = [];
// @ts-ignore
const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)];
export class BackgroundResourcesLoader {
/**
*
* @param {Application} app
*/
constructor(app) {
this.app = app;
this.registerReady = false;
this.mainMenuReady = false;
this.bareGameReady = false;
this.additionalReady = false;
this.signalMainMenuLoaded = new Signal();
this.signalBareGameLoaded = new Signal();
this.signalAdditionalLoaded = new Signal();
this.numAssetsLoaded = 0;
this.numAssetsToLoadTotal = 0;
// Avoid loading stuff twice
this.spritesLoaded = [];
this.soundsLoaded = [];
}
getNumAssetsLoaded() {
return this.numAssetsLoaded;
}
getNumAssetsTotal() {
return this.numAssetsToLoadTotal;
}
getPromiseForMainMenu() {
if (this.mainMenuReady) {
return Promise.resolve();
}
return new Promise(resolve => {
this.signalMainMenuLoaded.add(resolve);
});
}
getPromiseForBareGame() {
if (this.bareGameReady) {
return Promise.resolve();
}
return new Promise(resolve => {
this.signalBareGameLoaded.add(resolve);
});
}
startLoading() {
this.internalStartLoadingEssentialsForMainMenu();
}
internalStartLoadingEssentialsForMainMenu() {
logger.log("⏰ Start load: main menu");
this.internalLoadSpritesAndSounds(essentialMainMenuSprites, essentialMainMenuSounds)
.catch(err => {
logger.warn("⏰ Failed to load essentials for main menu:", err);
})
.then(() => {
logger.log("⏰ Finish load: main menu");
this.mainMenuReady = true;
this.signalMainMenuLoaded.dispatch();
this.internalStartLoadingEssentialsForBareGame();
});
}
internalStartLoadingEssentialsForBareGame() {
logger.log("⏰ Start load: bare game");
this.internalLoadSpritesAndSounds(
essentialBareGameSprites,
essentialBareGameSounds,
essentialBareGameAtlases
)
.then(() => this.internalPreloadCss("async-resources.scss"))
.catch(err => {
logger.warn("⏰ Failed to load essentials for bare game:", err);
})
.then(() => {
logger.log("⏰ Finish load: bare game");
this.bareGameReady = true;
initBuildingCodesAfterResourcesLoaded();
this.signalBareGameLoaded.dispatch();
this.internalStartLoadingAdditionalGameAssets();
});
}
internalStartLoadingAdditionalGameAssets() {
const additionalAtlases = [];
logger.log("⏰ Start load: additional assets (", additionalAtlases.length, "images)");
this.internalLoadSpritesAndSounds(additionalGameSprites, additionalGameSounds, additionalAtlases)
.catch(err => {
logger.warn("⏰ Failed to load additional assets:", err);
})
.then(() => {
logger.log("⏰ Finish load: additional assets");
this.additionalReady = true;
this.signalAdditionalLoaded.dispatch();
});
}
internalPreloadCss(name) {
return new Promise((resolve, reject) => {
const link = document.createElement("link");
link.onload = resolve;
link.onerror = reject;
link.setAttribute("rel", "stylesheet");
link.setAttribute("media", "all");
link.setAttribute("type", "text/css");
link.setAttribute("href", cachebust("async-resources.css"));
document.head.appendChild(link);
});
}
/**
* @param {Array<string>} sprites
* @param {Array<string>} sounds
* @param {Array<AtlasDefinition>} atlases
* @returns {Promise<void>}
*/
internalLoadSpritesAndSounds(sprites, sounds, atlases = []) {
this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length;
this.numAssetsLoaded = 0;
let promises = [];
for (let i = 0; i < sounds.length; ++i) {
if (this.soundsLoaded.indexOf(sounds[i]) >= 0) {
// Already loaded
continue;
}
this.soundsLoaded.push(sounds[i]);
promises.push(
this.app.sound
.loadSound(sounds[i])
.catch(err => {
logger.warn("Failed to load sound:", sounds[i]);
})
.then(() => {
this.numAssetsLoaded++;
})
);
}
for (let i = 0; i < sprites.length; ++i) {
if (this.spritesLoaded.indexOf(sprites[i]) >= 0) {
// Already loaded
continue;
}
this.spritesLoaded.push(sprites[i]);
promises.push(
Loader.preloadCSSSprite(sprites[i])
.catch(err => {
logger.warn("Failed to load css sprite:", sprites[i]);
})
.then(() => {
this.numAssetsLoaded++;
})
);
}
for (let i = 0; i < atlases.length; ++i) {
const atlas = atlases[i];
promises.push(
Loader.preloadAtlas(atlas)
.catch(err => {
logger.warn("Failed to load atlas:", atlas.sourceFileName);
})
.then(() => {
this.numAssetsLoaded++;
})
);
}
return (
Promise.all(promises)
// // Remove some pressure by waiting a bit
// .then(() => {
// return new Promise(resolve => {
// setTimeout(resolve, 200);
// });
// })
.then(() => {
this.numAssetsToLoadTotal = 0;
this.numAssetsLoaded = 0;
})
);
}
}

View File

@ -20,6 +20,7 @@ export const THIRDPARTY_URLS = {
discord: "https://discord.gg/HN7EVzV",
github: "https://github.com/tobspr/shapez.io",
reddit: "https://www.reddit.com/r/shapezio",
shapeViewer: "https://viewer.shapez.io",
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
};

View File

@ -60,6 +60,8 @@ export class Dialog {
this.buttonSignals[buttonId] = new Signal();
}
this.valueChosen = new Signal();
this.timeouts = [];
this.clickDetectors = [];
@ -164,7 +166,7 @@ export class Dialog {
const timeout = setTimeout(() => {
button.classList.remove("timedButton");
arrayDeleteValue(this.timeouts, timeout);
}, 5000);
}, 3000);
this.timeouts.push(timeout);
}
if (isEnter || isEscape) {
@ -431,10 +433,12 @@ 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.valueChosen.dispatch, this.valueChosen);
}
waitNextFrame().then(() => {
this.formElements[0].focus();
this.formElements[this.formElements.length - 1].focus();
});
return div;

View File

@ -1,150 +1,221 @@
import { ClickDetector } from "./click_detector";
export class FormElement {
constructor(id, label) {
this.id = id;
this.label = label;
}
getHtml() {
abstract;
return "";
}
getFormElement(parent) {
return parent.querySelector("[data-formId='" + this.id + "']");
}
bindEvents(parent, clickTrackers) {
abstract;
}
focus() {}
isValid() {
return true;
}
/** @returns {any} */
getValue() {
abstract;
}
}
export class FormElementInput extends FormElement {
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
super(id, label);
this.placeholder = placeholder;
this.defaultValue = defaultValue;
this.inputType = inputType;
this.validator = validator;
this.element = null;
}
getHtml() {
let classes = [];
let inputType = "text";
let maxlength = 256;
switch (this.inputType) {
case "text": {
classes.push("input-text");
break;
}
case "email": {
classes.push("input-email");
inputType = "email";
break;
}
case "token": {
classes.push("input-token");
inputType = "text";
maxlength = 4;
break;
}
}
return `
<div class="formElement input">
${this.label ? `<label>${this.label}</label>` : ""}
<input
type="${inputType}"
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
maxlength="${maxlength}"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
class="${classes.join(" ")}"
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
data-formId="${this.id}">
</div>
`;
}
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
this.element.addEventListener("input", event => this.updateErrorState());
this.updateErrorState();
}
updateErrorState() {
this.element.classList.toggle("errored", !this.isValid());
}
isValid() {
return !this.validator || this.validator(this.element.value);
}
getValue() {
return this.element.value;
}
focus() {
this.element.focus();
}
}
export class FormElementCheckbox extends FormElement {
constructor({ id, label, defaultValue = true }) {
super(id, label);
this.defaultValue = defaultValue;
this.value = this.defaultValue;
this.element = null;
}
getHtml() {
return `
<div class="formElement checkBoxFormElem">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
<span class="knob"></span >
</div >
</div>
`;
}
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
const detector = new ClickDetector(this.element, {
consumeEvents: false,
preventDefault: false,
});
clickTrackers.push(detector);
detector.click.add(this.toggle, this);
}
getValue() {
return this.value;
}
toggle() {
this.value = !this.value;
this.element.classList.toggle("checked", this.value);
}
focus(parent) {}
}
import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector";
import { Signal } from "./signal";
export class FormElement {
constructor(id, label) {
this.id = id;
this.label = label;
this.valueChosen = new Signal();
}
getHtml() {
abstract;
return "";
}
getFormElement(parent) {
return parent.querySelector("[data-formId='" + this.id + "']");
}
bindEvents(parent, clickTrackers) {
abstract;
}
focus() {}
isValid() {
return true;
}
/** @returns {any} */
getValue() {
abstract;
}
}
export class FormElementInput extends FormElement {
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
super(id, label);
this.placeholder = placeholder;
this.defaultValue = defaultValue;
this.inputType = inputType;
this.validator = validator;
this.element = null;
}
getHtml() {
let classes = [];
let inputType = "text";
let maxlength = 256;
switch (this.inputType) {
case "text": {
classes.push("input-text");
break;
}
case "email": {
classes.push("input-email");
inputType = "email";
break;
}
case "token": {
classes.push("input-token");
inputType = "text";
maxlength = 4;
break;
}
}
return `
<div class="formElement input">
${this.label ? `<label>${this.label}</label>` : ""}
<input
type="${inputType}"
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
maxlength="${maxlength}"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
class="${classes.join(" ")}"
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
data-formId="${this.id}">
</div>
`;
}
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
this.element.addEventListener("input", event => this.updateErrorState());
this.updateErrorState();
}
updateErrorState() {
this.element.classList.toggle("errored", !this.isValid());
}
isValid() {
return !this.validator || this.validator(this.element.value);
}
getValue() {
return this.element.value;
}
focus() {
this.element.focus();
}
}
export class FormElementCheckbox extends FormElement {
constructor({ id, label, defaultValue = true }) {
super(id, label);
this.defaultValue = defaultValue;
this.value = this.defaultValue;
this.element = null;
}
getHtml() {
return `
<div class="formElement checkBoxFormElem">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
<span class="knob"></span >
</div >
</div>
`;
}
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
const detector = new ClickDetector(this.element, {
consumeEvents: false,
preventDefault: false,
});
clickTrackers.push(detector);
detector.click.add(this.toggle, this);
}
getValue() {
return this.value;
}
toggle() {
this.value = !this.value;
this.element.classList.toggle("checked", this.value);
}
focus(parent) {}
}
export class FormElementItemChooser extends FormElement {
/**
*
* @param {object} param0
* @param {string} param0.id
* @param {string=} param0.label
* @param {Array<BaseItem>} param0.items
*/
constructor({ id, label, items = [] }) {
super(id, label);
this.items = items;
this.element = null;
/**
* @type {BaseItem}
*/
this.chosenItem = null;
}
getHtml() {
let classes = [];
return `
<div class="formElement">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
</div>
`;
}
/**
* @param {HTMLElement} parent
* @param {Array<ClickDetector>} clickTrackers
*/
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
item.drawFullSizeOnCanvas(context, 128);
this.element.appendChild(canvas);
const detector = new ClickDetector(canvas, {});
clickTrackers.push(detector);
detector.click.add(() => {
this.chosenItem = item;
this.valueChosen.dispatch(item);
});
}
}
isValid() {
return true;
}
getValue() {
return null;
}
focus() {}
}

View File

@ -667,3 +667,14 @@ export function safeModulo(n, m) {
export function smoothPulse(time) {
return Math.sin(time * 4) * 0.5 + 0.5;
}
/**
* Fills in a <link> tag
* @param {string} translation
* @param {string} link
*/
export function fillInLinkIntoTranslation(translation, link) {
return translation
.replace("<link>", "<a href='" + link + "' target='_blank'>")
.replace("</link>", "</a>");
}

View File

@ -1,82 +1,100 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { BasicSerializableObject } from "../savegame/serialization";
/** @type {ItemType[]} **/
export const itemTypes = ["shape", "color", "boolean"];
/**
* Class for items on belts etc. Not an entity for performance reasons
*/
export class BaseItem extends BasicSerializableObject {
constructor() {
super();
}
static getId() {
return "base_item";
}
/** @returns {object} */
static getSchema() {
return {};
}
/** @returns {ItemType} **/
getItemType() {
abstract;
return "shape";
}
/**
* Returns if the item equals the other itme
* @param {BaseItem} other
* @returns {boolean}
*/
equals(other) {
if (this.getItemType() !== other.getItemType()) {
return false;
}
return this.equalsImpl(other);
}
/**
* Override for custom comparison
* @abstract
* @param {BaseItem} other
* @returns {boolean}
*/
equalsImpl(other) {
abstract;
return false;
}
/**
* Draws the item at the given position
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
this.drawItemCenteredImpl(x, y, parameters, diameter);
}
}
/**
* INTERNAL
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
abstract;
}
getBackgroundColorAsResource() {
abstract;
return "";
}
}
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { BasicSerializableObject } from "../savegame/serialization";
/** @type {ItemType[]} **/
export const itemTypes = ["shape", "color", "boolean"];
/**
* Class for items on belts etc. Not an entity for performance reasons
*/
export class BaseItem extends BasicSerializableObject {
constructor() {
super();
}
static getId() {
return "base_item";
}
/** @returns {object} */
static getSchema() {
return {};
}
/** @returns {ItemType} **/
getItemType() {
abstract;
return "shape";
}
/**
* Returns a string id of the item
* @returns {string}
*/
getAsCopyableKey() {
abstract;
return "";
}
/**
* Returns if the item equals the other itme
* @param {BaseItem} other
* @returns {boolean}
*/
equals(other) {
if (this.getItemType() !== other.getItemType()) {
return false;
}
return this.equalsImpl(other);
}
/**
* Override for custom comparison
* @abstract
* @param {BaseItem} other
* @returns {boolean}
*/
equalsImpl(other) {
abstract;
return false;
}
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
abstract;
}
/**
* Draws the item at the given position
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
this.drawItemCenteredImpl(x, y, parameters, diameter);
}
}
/**
* INTERNAL
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
abstract;
}
getBackgroundColorAsResource() {
abstract;
return "";
}
}

View File

@ -1,12 +1,11 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { Loader } from "../core/loader";
import { createLogger } from "../core/logging";
import { findNiceIntegerValue } from "../core/utils";
import { Vector } from "../core/vector";
import { Entity } from "./entity";
import { GameRoot } from "./root";
import { findNiceIntegerValue } from "../core/utils";
import { blueprintShape } from "./upgrades";
import { globalConfig } from "../core/config";
const logger = createLogger("blueprint");

View File

@ -5,6 +5,7 @@ import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
const overlayMatrix = generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 0]);
@ -21,8 +22,7 @@ export class MetaAnalyzerBuilding extends MetaBuilding {
* @param {GameRoot} root
*/
getIsUnlocked(root) {
// @todo
return true;
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
}
/** @returns {"wires"} **/

View File

@ -4,6 +4,7 @@ import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
export class MetaComparatorBuilding extends MetaBuilding {
constructor() {
@ -18,8 +19,7 @@ export class MetaComparatorBuilding extends MetaBuilding {
* @param {GameRoot} root
*/
getIsUnlocked(root) {
// @todo
return true;
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
}
/** @returns {"wires"} **/

View File

@ -5,6 +5,7 @@ import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
import { GameRoot } from "../root";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { generateMatrixRotations } from "../../core/utils";
import { enumHubGoalRewards } from "../tutorial_goals";
/** @enum {string} */
export const enumLogicGateVariants = {
@ -48,8 +49,7 @@ export class MetaLogicGateBuilding extends MetaBuilding {
* @param {GameRoot} root
*/
getIsUnlocked(root) {
// @todo
return true;
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
}
/** @returns {"wires"} **/

View File

@ -5,6 +5,7 @@ import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
/** @enum {string} */
export const enumTransistorVariants = {
@ -29,8 +30,7 @@ export class MetaTransistorBuilding extends MetaBuilding {
* @param {GameRoot} root
*/
getIsUnlocked(root) {
// @todo
return true;
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
}
/** @returns {"wires"} **/

View File

@ -110,7 +110,12 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding {
pinComp.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
type: enumPinSlotType.logicalEjector,
},
{

View File

@ -53,6 +53,6 @@ export class MetaWireTunnelBuilding extends MetaBuilding {
* @param {Entity} entity
*/
setupEntityComponents(entity) {
entity.addComponent(new WireTunnelComponent({}));
entity.addComponent(new WireTunnelComponent());
}
}

View File

@ -1,8 +1,10 @@
import { globalConfig } from "../core/config";
import { RandomNumberGenerator } from "../core/rng";
import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils";
import { BasicSerializableObject, types } from "../savegame/serialization";
import { enumColors } from "./colors";
import { enumItemProcessorTypes } from "./components/item_processor";
import { enumAnalyticsDataSource } from "./production_analytics";
import { GameRoot } from "./root";
import { enumSubShape, ShapeDefinition } from "./shape_definition";
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals";
@ -18,12 +20,6 @@ export class HubGoals extends BasicSerializableObject {
level: types.uint,
storedShapes: types.keyValueMap(types.uint),
upgradeLevels: types.keyValueMap(types.uint),
currentGoal: types.structured({
definition: types.knownType(ShapeDefinition),
required: types.uint,
reward: types.nullable(types.enum(enumHubGoalRewards)),
}),
};
}
@ -53,15 +49,7 @@ export class HubGoals extends BasicSerializableObject {
}
// Compute current goal
const goal = tutorialGoals[this.level - 1];
if (goal) {
this.currentGoal = {
/** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(goal.shape),
required: goal.required,
reward: goal.reward,
};
}
this.computeNextGoal();
}
/**
@ -92,6 +80,11 @@ export class HubGoals extends BasicSerializableObject {
*/
this.upgradeLevels = {};
// Reset levels
for (const key in UPGRADES) {
this.upgradeLevels[key] = 0;
}
/**
* Stores the improvements for all upgrades
* @type {Object<string, number>}
@ -101,7 +94,7 @@ export class HubGoals extends BasicSerializableObject {
this.upgradeImprovements[key] = 1;
}
this.createNextGoal();
this.computeNextGoal();
// Allow quickly switching goals in dev mode
if (G_IS_DEV) {
@ -150,6 +143,13 @@ export class HubGoals extends BasicSerializableObject {
* Returns how much of the current goal was already delivered
*/
getCurrentGoalDelivered() {
if (this.currentGoal.throughputOnly) {
return this.root.productionAnalytics.getCurrentShapeRate(
enumAnalyticsDataSource.delivered,
this.currentGoal.definition
);
}
return this.getShapesStored(this.currentGoal.definition);
}
@ -184,9 +184,8 @@ export class HubGoals extends BasicSerializableObject {
this.root.signals.shapeDelivered.dispatch(definition);
// Check if we have enough for the next level
const targetHash = this.currentGoal.definition.getHash();
if (
this.storedShapes[targetHash] >= this.currentGoal.required ||
this.getCurrentGoalDelivered() >= this.currentGoal.required ||
(G_IS_DEV && globalConfig.debug.rewardsInstant)
) {
this.onGoalCompleted();
@ -196,24 +195,28 @@ export class HubGoals extends BasicSerializableObject {
/**
* Creates the next goal
*/
createNextGoal() {
computeNextGoal() {
const storyIndex = this.level - 1;
if (storyIndex < tutorialGoals.length) {
const { shape, required, reward } = tutorialGoals[storyIndex];
const { shape, required, reward, throughputOnly } = tutorialGoals[storyIndex];
this.currentGoal = {
/** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
required,
reward,
throughputOnly,
};
return;
}
const required = 4 + (this.level - 27) * 0.25;
this.currentGoal = {
/** @type {ShapeDefinition} */
definition: this.createRandomShape(),
required: findNiceIntegerValue(1000 + Math.pow(this.level * 2000, 0.8)),
definition: this.computeFreeplayShape(this.level),
required,
reward: enumHubGoalRewards.no_reward_freeplay,
throughputOnly: true,
};
}
@ -226,7 +229,7 @@ export class HubGoals extends BasicSerializableObject {
this.root.app.gameAnalytics.handleLevelCompleted(this.level);
++this.level;
this.createNextGoal();
this.computeNextGoal();
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
}
@ -320,15 +323,85 @@ export class HubGoals extends BasicSerializableObject {
}
/**
* Picks random colors which are close to each other
* @param {RandomNumberGenerator} rng
*/
generateRandomColorSet(rng, allowUncolored = false) {
const colorWheel = [
enumColors.red,
enumColors.yellow,
enumColors.green,
enumColors.cyan,
enumColors.blue,
enumColors.purple,
enumColors.red,
enumColors.yellow,
];
const universalColors = [enumColors.white];
if (allowUncolored) {
universalColors.push(enumColors.uncolored);
}
const index = rng.nextIntRangeInclusive(0, colorWheel.length - 3);
const pickedColors = colorWheel.slice(index, index + 3);
pickedColors.push(rng.choice(universalColors));
return pickedColors;
}
/**
* Creates a (seeded) random shape
* @param {number} level
* @returns {ShapeDefinition}
*/
createRandomShape() {
computeFreeplayShape(level) {
const layerCount = clamp(this.level / 25, 2, 4);
/** @type {Array<import("./shape_definition").ShapeLayer>} */
let layers = [];
const randomColor = () => randomChoice(Object.values(enumColors));
const randomShape = () => randomChoice(Object.values(enumSubShape));
const rng = new RandomNumberGenerator(this.root.map.seed + "/" + level);
const colors = this.generateRandomColorSet(rng, level > 35);
let pickedSymmetry = null; // pairs of quadrants that must be the same
let availableShapes = [enumSubShape.rect, enumSubShape.circle, enumSubShape.star];
if (rng.next() < 0.5) {
pickedSymmetry = [
// radial symmetry
[0, 2],
[1, 3],
];
availableShapes.push(enumSubShape.windmill); // windmill looks good only in radial symmetry
} else {
const symmetries = [
[
// horizontal axis
[0, 3],
[1, 2],
],
[
// vertical axis
[0, 1],
[2, 3],
],
[
// diagonal axis
[0, 2],
[1],
[3],
],
[
// other diagonal axis
[1, 3],
[0],
[2],
],
];
pickedSymmetry = rng.choice(symmetries);
}
const randomColor = () => rng.choice(colors);
const randomShape = () => rng.choice(Object.values(enumSubShape));
let anyIsMissingTwo = false;
@ -336,23 +409,24 @@ export class HubGoals extends BasicSerializableObject {
/** @type {import("./shape_definition").ShapeLayer} */
const layer = [null, null, null, null];
for (let quad = 0; quad < 4; ++quad) {
layer[quad] = {
subShape: randomShape(),
color: randomColor(),
};
}
// Sometimes shapes are missing
if (Math.random() > 0.85) {
layer[randomInt(0, 3)] = null;
for (let j = 0; j < pickedSymmetry.length; ++j) {
const group = pickedSymmetry[j];
const shape = randomShape();
const color = randomColor();
for (let k = 0; k < group.length; ++k) {
const quad = group[k];
layer[quad] = {
subShape: shape,
color,
};
}
}
// Sometimes they actually are missing *two* ones!
// Make sure at max only one layer is missing it though, otherwise we could
// create an uncreateable shape
if (Math.random() > 0.95 && !anyIsMissingTwo) {
layer[randomInt(0, 3)] = null;
if (level > 75 && rng.next() > 0.95 && !anyIsMissingTwo) {
layer[rng.nextIntRange(0, 4)] = null;
anyIsMissingTwo = true;
}

View File

@ -44,6 +44,8 @@ import { HUDWireInfo } from "./parts/wire_info";
import { HUDLeverToggle } from "./parts/lever_toggle";
import { HUDLayerPreview } from "./parts/layer_preview";
import { HUDMinerHighlight } from "./parts/miner_highlight";
import { HUDBetaOverlay } from "./parts/beta_overlay";
import { HUDPerformanceWarning } from "./parts/performance_warning";
export class GameHUD {
/**
@ -75,7 +77,6 @@ export class GameHUD {
pinnedShapes: new HUDPinnedShapes(this.root),
notifications: new HUDNotifications(this.root),
settingsMenu: new HUDSettingsMenu(this.root),
// betaOverlay: new HUDBetaOverlay(this.root),
debugInfo: new HUDDebugInfo(this.root),
dialogs: new HUDModalDialogs(this.root),
screenshotExporter: new HUDScreenshotExporter(this.root),
@ -85,6 +86,7 @@ export class GameHUD {
layerPreview: new HUDLayerPreview(this.root),
minerHighlight: new HUDMinerHighlight(this.root),
performanceWarning: new HUDPerformanceWarning(this.root),
// Typing hints
/* typehints:start */
@ -137,6 +139,10 @@ export class GameHUD {
this.parts.sandboxController = new HUDSandboxController(this.root);
}
if (!G_IS_RELEASE) {
this.parts.betaOverlay = new HUDBetaOverlay(this.root);
}
const frag = document.createDocumentFragment();
for (const key in this.parts) {
this.parts[key].createElements(frag);

View File

@ -3,7 +3,12 @@ import { makeDiv } from "../../../core/utils";
export class HUDBetaOverlay extends BaseHUDPart {
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_BetaOverlay", [], "CLOSED BETA");
this.element = makeDiv(
parent,
"ingame_HUD_BetaOverlay",
[],
"<h2>CLOSED BETA VERSION</h2><span>This version is unstable, might crash and is not final!</span>"
);
}
initialize() {}

View File

@ -334,7 +334,11 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
const tileBelow = this.root.map.getLowerLayerContentXY(tile.x, tile.y);
// Check if there's a shape or color item below, if so select the miner
if (tileBelow && this.root.app.settings.getAllSettings().pickMinerOnPatch) {
if (
tileBelow &&
this.root.app.settings.getAllSettings().pickMinerOnPatch &&
this.root.currentLayer === "regular"
) {
this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding));
// Select chained miner if available, since that's always desired once unlocked

View File

@ -0,0 +1,16 @@
import { T } from "../../../translations";
import { BaseHUDPart } from "../base_hud_part";
export class HUDPerformanceWarning extends BaseHUDPart {
initialize() {
this.warningShown = false;
this.root.signals.entityManuallyPlaced.add(this.checkAfterPlace, this);
}
checkAfterPlace() {
if (!this.warningShown && this.root.entityMgr.entities.length > 10000) {
this.root.hud.parts.dialogs.showInfo(T.dialogs.entityWarning.title, T.dialogs.entityWarning.desc);
this.warningShown = true;
}
}
}

View File

@ -4,6 +4,8 @@ import { ShapeDefinition } from "../../shape_definition";
import { BaseHUDPart } from "../base_hud_part";
import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { enumAnalyticsDataSource } from "../../production_analytics";
import { T } from "../../../translations";
/**
* Manages the pinned shapes on the left side of the screen
@ -22,11 +24,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
* convenient. Also allows for cleaning up handles.
* @type {Array<{
* key: string,
* definition: ShapeDefinition,
* amountLabel: HTMLElement,
* lastRenderedValue: string,
* element: HTMLElement,
* detector?: ClickDetector,
* infoDetector?: ClickDetector
* infoDetector?: ClickDetector,
* throughputOnly?: boolean
* }>}
*/
this.handles = [];
@ -163,29 +167,40 @@ export class HUDPinnedShapes extends BaseHUDPart {
this.handles = [];
// Pin story goal
this.internalPinShape(currentKey, false, "goal");
this.internalPinShape({
key: currentKey,
canUnpin: false,
className: "goal",
throughputOnly: currentGoal.throughputOnly,
});
// Pin blueprint shape as well
if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) {
this.internalPinShape(blueprintShape, false, "blueprint");
this.internalPinShape({
key: blueprintShape,
canUnpin: false,
className: "blueprint",
});
}
// Pin manually pinned shapes
for (let i = 0; i < this.pinnedShapes.length; ++i) {
const key = this.pinnedShapes[i];
if (key !== currentKey) {
this.internalPinShape(key);
this.internalPinShape({ key });
}
}
}
/**
* Pins a new shape
* @param {string} key
* @param {boolean} canUnpin
* @param {string=} className
* @param {object} param0
* @param {string} param0.key
* @param {boolean=} param0.canUnpin
* @param {string=} param0.className
* @param {boolean=} param0.throughputOnly
*/
internalPinShape(key, canUnpin = true, className = null) {
internalPinShape({ key, canUnpin = true, className = null, throughputOnly = false }) {
const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key);
const element = makeDiv(this.element, null, ["shape"]);
@ -229,11 +244,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
this.handles.push({
key,
definition,
element,
amountLabel,
lastRenderedValue: "",
detector,
infoDetector,
throughputOnly,
});
}
@ -244,8 +261,20 @@ export class HUDPinnedShapes extends BaseHUDPart {
for (let i = 0; i < this.handles.length; ++i) {
const handle = this.handles[i];
const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key);
const currentValueFormatted = formatBigNumber(currentValue);
let currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key);
let currentValueFormatted = formatBigNumber(currentValue);
if (handle.throughputOnly) {
currentValue = this.root.productionAnalytics.getCurrentShapeRate(
enumAnalyticsDataSource.delivered,
handle.definition
);
currentValueFormatted = T.ingame.statistics.shapesDisplayUnits.second.replace(
"<shapes>",
String(currentValue)
);
}
if (currentValueFormatted !== handle.lastRenderedValue) {
handle.lastRenderedValue = currentValueFormatted;
handle.amountLabel.innerText = currentValueFormatted;

View File

@ -113,7 +113,7 @@ export class HUDSandboxController extends BaseHUDPart {
modifyLevel(amount) {
const hubGoals = this.root.hubGoals;
hubGoals.level = Math.max(1, hubGoals.level + amount);
hubGoals.createNextGoal();
hubGoals.computeNextGoal();
// Clear all shapes of this level
hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0;

View File

@ -90,17 +90,15 @@ export class HUDShop extends BaseHUDPart {
// Max level
handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace(
"<currentMult>",
currentTierMultiplier.toString()
formatBigNumber(currentTierMultiplier)
);
continue;
}
// Set description
handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description
.replace("<currentMult>", currentTierMultiplier.toString())
.replace("<newMult>", (currentTierMultiplier + tierHandle.improvement).toString())
// Backwards compatibility
.replace("<gain>", (tierHandle.improvement * 100.0).toString());
.replace("<currentMult>", formatBigNumber(currentTierMultiplier))
.replace("<newMult>", formatBigNumber(currentTierMultiplier + tierHandle.improvement));
tierHandle.required.forEach(({ shape, amount }) => {
const container = makeDiv(handle.elemRequirements, null, ["requirement"]);

View File

@ -4,11 +4,12 @@ import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations";
import { defaultBuildingVariant } from "../../meta_building";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { enumHubGoalRewards, tutorialGoals } from "../../tutorial_goals";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
import { InputReceiver } from "../../../core/input_receiver";
import { enumNotificationType } from "./notifications";
export class HUDUnlockNotification extends BaseHUDPart {
initialize() {
@ -50,6 +51,14 @@ export class HUDUnlockNotification extends BaseHUDPart {
* @param {enumHubGoalRewards} reward
*/
showForLevel(level, reward) {
if (level > tutorialGoals.length) {
this.root.hud.signals.notification.dispatch(
T.ingame.notifications.freeplayLevelComplete.replace("<level>", String(level)),
enumNotificationType.success
);
return;
}
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace(
"<level>",

View File

@ -1,12 +1,18 @@
import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig, IS_DEMO } from "../../../core/config";
import { globalConfig, IS_DEMO, THIRDPARTY_URLS } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters";
import { Loader } from "../../../core/loader";
import { DialogWithForm } from "../../../core/modal_dialog_elements";
import { FormElementInput } from "../../../core/modal_dialog_forms";
import { Rectangle } from "../../../core/rectangle";
import { STOP_PROPAGATION } from "../../../core/signal";
import { arrayDeleteValue, lerp, makeDiv, removeAllChildren } from "../../../core/utils";
import {
arrayDeleteValue,
fillInLinkIntoTranslation,
lerp,
makeDiv,
removeAllChildren,
} from "../../../core/utils";
import { Vector } from "../../../core/vector";
import { T } from "../../../translations";
import { BaseItem } from "../../base_item";
@ -272,7 +278,7 @@ export class HUDWaypoints extends BaseHUDPart {
const dialog = new DialogWithForm({
app: this.root.app,
title: waypoint ? T.dialogs.createMarker.titleEdit : T.dialogs.createMarker.title,
desc: T.dialogs.createMarker.desc,
desc: fillInLinkIntoTranslation(T.dialogs.createMarker.desc, THIRDPARTY_URLS.shapeViewer),
formElements: [markerNameInput],
buttons: waypoint ? ["delete:bad", "cancel", "ok:good"] : ["cancel", "ok:good"],
});

View File

@ -1,13 +1,14 @@
import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { THEME } from "../../theme";
import { BaseHUDPart } from "../base_hud_part";
import { Loader } from "../../../core/loader";
import { lerp } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { BaseHUDPart } from "../base_hud_part";
const copy = require("clipboard-copy");
const wiresBackgroundDpi = 4;
export class HUDWiresOverlay extends BaseHUDPart {
@ -16,6 +17,7 @@ export class HUDWiresOverlay extends BaseHUDPart {
initialize() {
// Probably not the best location, but the one which makes most sense
this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.switchLayers).add(this.switchLayers, this);
this.root.keyMapper.getBinding(KEYMAPPINGS.placement.copyWireValue).add(this.copyWireValue, this);
this.generateTilePattern();
@ -54,7 +56,53 @@ export class HUDWiresOverlay extends BaseHUDPart {
update() {
const desiredAlpha = this.root.currentLayer === "wires" ? 1.0 : 0.0;
this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12);
// On low performance, skip the fade
if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) {
this.currentAlpha = desiredAlpha;
} else {
this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12);
}
}
/**
* Copies the wires value below the cursor
*/
copyWireValue() {
if (this.root.currentLayer !== "wires") {
return;
}
const mousePos = this.root.app.mousePosition;
if (!mousePos) {
return;
}
const tile = this.root.camera.screenToWorld(mousePos).toTileSpace();
const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "wires");
if (!contents) {
return;
}
let value = null;
if (contents.components.Wire) {
const network = contents.components.Wire.linkedNetwork;
if (network && network.hasValue()) {
value = network.currentValue;
}
}
if (contents.components.ConstantSignal) {
value = contents.components.ConstantSignal.signal;
}
if (value) {
copy(value.getAsCopyableKey());
this.root.soundProxy.playUi(SOUNDS.copy);
} else {
copy("");
this.root.soundProxy.playUiError();
}
}
/**

View File

@ -8,6 +8,9 @@ import { MetaVirtualProcessorBuilding } from "../../buildings/virtual_processor"
import { MetaTransistorBuilding } from "../../buildings/transistor";
import { MetaAnalyzerBuilding } from "../../buildings/analyzer";
import { MetaComparatorBuilding } from "../../buildings/comparator";
import { MetaReaderBuilding } from "../../buildings/reader";
import { MetaFilterBuilding } from "../../buildings/filter";
import { MetaDisplayBuilding } from "../../buildings/display";
export class HUDWiresToolbar extends HUDBaseToolbar {
constructor(root) {
@ -16,12 +19,17 @@ export class HUDWiresToolbar extends HUDBaseToolbar {
MetaWireBuilding,
MetaWireTunnelBuilding,
MetaConstantSignalBuilding,
MetaLeverBuilding,
MetaTransistorBuilding,
MetaLogicGateBuilding,
MetaAnalyzerBuilding,
MetaVirtualProcessorBuilding,
MetaAnalyzerBuilding,
MetaComparatorBuilding,
MetaTransistorBuilding,
],
secondaryBuildings: [
MetaReaderBuilding,
MetaLeverBuilding,
MetaFilterBuilding,
MetaDisplayBuilding,
],
visibilityCondition: () =>
!this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "wires",

View File

@ -26,6 +26,13 @@ export class BooleanItem extends BaseItem {
return "boolean";
}
/**
* @returns {string}
*/
getAsCopyableKey() {
return this.value ? "1" : "0";
}
/**
* @param {number} value
*/
@ -56,6 +63,21 @@ export class BooleanItem extends BaseItem {
}
sprite.drawCachedCentered(parameters, x, y, diameter);
}
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
let sprite;
if (this.value) {
sprite = Loader.getSprite("sprites/wires/boolean_true.png");
} else {
sprite = Loader.getSprite("sprites/wires/boolean_false.png");
}
sprite.drawCentered(context, size / 2, size / 2, size);
}
}
export const BOOL_FALSE_SINGLETON = new BooleanItem(0);

View File

@ -28,6 +28,13 @@ export class ColorItem extends BaseItem {
return "color";
}
/**
* @returns {string}
*/
getAsCopyableKey() {
return this.color;
}
/**
* @param {BaseItem} other
*/
@ -47,6 +54,18 @@ export class ColorItem extends BaseItem {
return THEME.map.resources[this.color];
}
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
if (!this.cachedSprite) {
this.cachedSprite = Loader.getSprite("sprites/colors/" + this.color + ".png");
}
this.cachedSprite.drawCentered(context, size / 2, size / 2, size);
}
/**
* @param {number} x
* @param {number} y

View File

@ -1,62 +1,78 @@
import { DrawParameters } from "../../core/draw_parameters";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { ShapeDefinition } from "../shape_definition";
import { THEME } from "../theme";
import { globalConfig } from "../../core/config";
export class ShapeItem extends BaseItem {
static getId() {
return "shape";
}
static getSchema() {
return types.string;
}
serialize() {
return this.definition.getHash();
}
deserialize(data) {
this.definition = ShapeDefinition.fromShortKey(data);
}
/** @returns {"shape"} **/
getItemType() {
return "shape";
}
/**
* @param {BaseItem} other
*/
equalsImpl(other) {
return this.definition.getHash() === /** @type {ShapeItem} */ (other).definition.getHash();
}
/**
* @param {ShapeDefinition} definition
*/
constructor(definition) {
super();
/**
* This property must not be modified on runtime, you have to clone the class in order to change the definition
*/
this.definition = definition;
}
getBackgroundColorAsResource() {
return THEME.map.resources.shape;
}
/**
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
this.definition.drawCentered(x, y, parameters, diameter);
}
}
import { DrawParameters } from "../../core/draw_parameters";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { ShapeDefinition } from "../shape_definition";
import { THEME } from "../theme";
import { globalConfig } from "../../core/config";
export class ShapeItem extends BaseItem {
static getId() {
return "shape";
}
static getSchema() {
return types.string;
}
serialize() {
return this.definition.getHash();
}
deserialize(data) {
this.definition = ShapeDefinition.fromShortKey(data);
}
/** @returns {"shape"} **/
getItemType() {
return "shape";
}
/**
* @returns {string}
*/
getAsCopyableKey() {
return this.definition.getHash();
}
/**
* @param {BaseItem} other
*/
equalsImpl(other) {
return this.definition.getHash() === /** @type {ShapeItem} */ (other).definition.getHash();
}
/**
* @param {ShapeDefinition} definition
*/
constructor(definition) {
super();
/**
* This property must not be modified on runtime, you have to clone the class in order to change the definition
*/
this.definition = definition;
}
getBackgroundColorAsResource() {
return THEME.map.resources.shape;
}
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
this.definition.drawFullSizeOnCanvas(context, size);
}
/**
* @param {number} x
* @param {number} y
* @param {DrawParameters} parameters
* @param {number=} diameter
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
this.definition.drawCentered(x, y, parameters, diameter);
}
}

View File

@ -67,12 +67,11 @@ export const KEYMAPPINGS = {
wire: { keyCode: key("1") },
wire_tunnel: { keyCode: key("2") },
constant_signal: { keyCode: key("3") },
lever_wires: { keyCode: key("4") },
logic_gate: { keyCode: key("5") },
virtual_processor: { keyCode: key("6") },
transistor: { keyCode: key("7") },
analyzer: { keyCode: key("8") },
comparator: { keyCode: key("9") },
logic_gate: { keyCode: key("4") },
virtual_processor: { keyCode: key("5") },
analyzer: { keyCode: key("6") },
comparator: { keyCode: key("7") },
transistor: { keyCode: key("8") },
},
placement: {
@ -82,6 +81,8 @@ export const KEYMAPPINGS = {
cycleBuildingVariants: { keyCode: key("T") },
cycleBuildings: { keyCode: 9 }, // TAB
switchDirectionLockSide: { keyCode: key("R") },
copyWireValue: { keyCode: key("Z") },
},
massSelect: {

View File

@ -297,6 +297,15 @@ export class ShapeDefinition extends BasicSerializableObject {
parameters.context.drawImage(canvas, x - diameter / 2, y - diameter / 2, diameter, diameter);
}
/**
* Draws the item to a canvas
* @param {CanvasRenderingContext2D} context
* @param {number} size
*/
drawFullSizeOnCanvas(context, size) {
this.internalGenerateShapeBuffer(null, context, size, size, 1);
}
/**
* Generates this shape as a canvas
* @param {number} size

View File

@ -1,6 +1,9 @@
import trim from "trim";
import { THIRDPARTY_URLS } from "../../core/config";
import { DialogWithForm } from "../../core/modal_dialog_elements";
import { FormElementInput } from "../../core/modal_dialog_forms";
import { FormElementInput, FormElementItemChooser } from "../../core/modal_dialog_forms";
import { fillInLinkIntoTranslation } from "../../core/utils";
import { T } from "../../translations";
import { BaseItem } from "../base_item";
import { enumColors } from "../colors";
import { ConstantSignalComponent } from "../components/constant_signal";
@ -9,6 +12,7 @@ import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeDefinition } from "../shape_definition";
import { blueprintShape } from "../upgrades";
export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) {
@ -41,23 +45,35 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
const signalValueInput = new FormElementInput({
id: "signalValue",
label: null,
label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer),
placeholder: "",
defaultValue: "",
validator: val => this.parseSignalCode(val),
});
const itemInput = new FormElementItemChooser({
id: "signalItem",
label: null,
items: [
BOOL_FALSE_SINGLETON,
BOOL_TRUE_SINGLETON,
...Object.values(COLOR_ITEM_SINGLETONS),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(blueprintShape),
],
});
const dialog = new DialogWithForm({
app: this.root.app,
title: "Set Signal",
desc: "Enter a shape code, color or '0' or '1'",
formElements: [signalValueInput],
title: T.dialogs.editSignal.title,
desc: T.dialogs.editSignal.descItems,
formElements: [itemInput, signalValueInput],
buttons: ["cancel:bad:escape", "ok:good:enter"],
closeButton: false,
});
this.root.hud.parts.dialogs.internalShowDialog(dialog);
// When confirmed, set the signal
dialog.buttonSignals.ok.add(() => {
const closeHandler = () => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
return;
@ -75,8 +91,16 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
return;
}
constantComp.signal = this.parseSignalCode(signalValueInput.getValue());
});
if (itemInput.chosenItem) {
console.log(itemInput.chosenItem);
constantComp.signal = itemInput.chosenItem;
} else {
constantComp.signal = this.parseSignalCode(signalValueInput.getValue());
}
};
dialog.buttonSignals.ok.add(closeHandler);
dialog.valueChosen.add(closeHandler);
// When cancelled, destroy the entity again
dialog.buttonSignals.cancel.add(() => {

View File

@ -1,15 +1,15 @@
import { globalConfig } from "../../core/config";
import { smoothenDpi } from "../../core/dpi_manager";
import { DrawParameters } from "../../core/draw_parameters";
import { drawSpriteClipped } from "../../core/draw_utils";
import { Loader } from "../../core/loader";
import { Rectangle } from "../../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../../core/sprites";
import { formatBigNumber } from "../../core/utils";
import { T } from "../../translations";
import { HubComponent } from "../components/hub";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { globalConfig } from "../../core/config";
import { smoothenDpi } from "../../core/dpi_manager";
import { drawSpriteClipped } from "../../core/draw_utils";
import { Rectangle } from "../../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../../core/sprites";
const HUB_SIZE_TILES = 4;
const HUB_SIZE_PIXELS = HUB_SIZE_TILES * globalConfig.tileSize;
@ -73,23 +73,36 @@ export class HubSystem extends GameSystemWithFilter {
const textOffsetX = 70;
const textOffsetY = 61;
// Deliver count
const delivered = this.root.hubGoals.getCurrentGoalDelivered();
const deliveredText = "" + formatBigNumber(delivered);
if (goals.throughputOnly) {
// Throughput
const deliveredText = T.ingame.statistics.shapesDisplayUnits.second.replace(
"<shapes>",
formatBigNumber(goals.required)
);
if (delivered > 999) {
context.font = "bold 16px GameFont";
context.font = "bold 12px GameFont";
context.fillStyle = "#64666e";
context.textAlign = "left";
context.fillText(deliveredText, textOffsetX, textOffsetY);
} else {
context.font = "bold 25px GameFont";
}
context.fillStyle = "#64666e";
context.textAlign = "left";
context.fillText(deliveredText, textOffsetX, textOffsetY);
// Deliver count
const delivered = this.root.hubGoals.getCurrentGoalDelivered();
const deliveredText = "" + formatBigNumber(delivered);
// Required
context.font = "13px GameFont";
context.fillStyle = "#a4a6b0";
context.fillText("/ " + formatBigNumber(goals.required), textOffsetX, textOffsetY + 13);
if (delivered > 999) {
context.font = "bold 16px GameFont";
} else {
context.font = "bold 25px GameFont";
}
context.fillStyle = "#64666e";
context.textAlign = "left";
context.fillText(deliveredText, textOffsetX, textOffsetY);
// Required
context.font = "13px GameFont";
context.fillStyle = "#a4a6b0";
context.fillText("/ " + formatBigNumber(goals.required), textOffsetX, textOffsetY + 13);
}
// Reward
const rewardText = T.storyRewards[goals.reward].title.toUpperCase();
@ -167,4 +180,4 @@ export class HubSystem extends GameSystemWithFilter {
originalH: HUB_SIZE_PIXELS * dpi,
});
}
}
}

View File

@ -3,8 +3,9 @@ import { enumColors } from "../colors";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType } from "../components/wired_pins";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON, isTruthyItem } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON, BooleanItem, isTruthyItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeItem } from "../items/shape_item";
import { ShapeDefinition } from "../shape_definition";
export class LogicGateSystem extends GameSystemWithFilter {
@ -153,18 +154,22 @@ export class LogicGateSystem extends GameSystemWithFilter {
/**
* @param {Array<BaseItem|null>} parameters
* @returns {BaseItem}
* @returns {[BaseItem, BaseItem]}
*/
compute_ROTATE(parameters) {
const item = parameters[0];
if (!item || item.getItemType() !== "shape") {
// Not a shape
return null;
return [null, null];
}
const definition = /** @type {ShapeItem} */ (item).definition;
const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCW(definition);
return this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinition);
const rotatedDefinitionCCW = this.root.shapeDefinitionMgr.shapeActionRotateCCW(definition);
const rotatedDefinitionCW = this.root.shapeDefinitionMgr.shapeActionRotateCW(definition);
return [
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinitionCCW),
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinitionCW),
];
}
/**

View File

@ -43,7 +43,7 @@ export const tutorialGoals = [
// Circle
{
shape: "CuCuCuCu", // belts t1
required: 40,
required: 30,
reward: enumHubGoalRewards.reward_cutter_and_trash,
},
@ -59,14 +59,14 @@ export const tutorialGoals = [
// Rectangle
{
shape: "RuRuRuRu", // miners t1
required: 85,
required: 70,
reward: enumHubGoalRewards.reward_balancer,
},
// 4
{
shape: "RuRu----", // processors t2
required: 100,
required: 70,
reward: enumHubGoalRewards.reward_rotater,
},
@ -74,14 +74,14 @@ export const tutorialGoals = [
// Rotater
{
shape: "Cu----Cu", // belts t2
required: 175,
required: 170,
reward: enumHubGoalRewards.reward_tunnel,
},
// 6
{
shape: "Cu------", // miners t2
required: 250,
required: 270,
reward: enumHubGoalRewards.reward_painter,
},
@ -89,14 +89,14 @@ export const tutorialGoals = [
// Painter
{
shape: "CrCrCrCr", // unused
required: 500,
required: 300,
reward: enumHubGoalRewards.reward_rotater_ccw,
},
// 8
{
shape: "RbRb----", // painter t2
required: 700,
required: 480,
reward: enumHubGoalRewards.reward_mixer,
},
@ -104,7 +104,7 @@ export const tutorialGoals = [
// Mixing (purple)
{
shape: "CpCpCpCp", // belts t3
required: 800,
required: 600,
reward: enumHubGoalRewards.reward_merger,
},
@ -112,7 +112,7 @@ export const tutorialGoals = [
// STACKER: Star shape + cyan
{
shape: "ScScScSc", // miners t3
required: 900,
required: 800,
reward: enumHubGoalRewards.reward_stacker,
},
@ -128,7 +128,7 @@ export const tutorialGoals = [
// Blueprints
{
shape: "CbCbCbRb:CwCwCwCw",
required: 1250,
required: 1000,
reward: enumHubGoalRewards.reward_blueprints,
},
@ -136,24 +136,24 @@ export const tutorialGoals = [
// Tunnel Tier 2
{
shape: "RpRpRpRp:CwCwCwCw", // painting t3
required: 5000,
required: 3800,
reward: enumHubGoalRewards.reward_underground_belt_tier_2,
},
// 14
// Belt reader
{
// @todo
shape: "CuCuCuCu",
required: 7000,
shape: "--Cg----:--Cr----", // unused
required: 16, // Per second!
reward: enumHubGoalRewards.reward_belt_reader,
throughputOnly: true,
},
// 15
// Storage
{
shape: "SrSrSrSr:CyCyCyCy", // unused
required: 7500,
required: 10000,
reward: enumHubGoalRewards.reward_storage,
},
@ -161,7 +161,7 @@ export const tutorialGoals = [
// Quad Cutter
{
shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants)
required: 12500,
required: 6000,
reward: enumHubGoalRewards.reward_cutter_quad,
},
@ -169,15 +169,14 @@ export const tutorialGoals = [
// Double painter
{
shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants)
required: 15000,
required: 20000,
reward: enumHubGoalRewards.reward_painter_double,
},
// 18
// Rotater (180deg)
{
// @TODO
shape: "CuCuCuCu",
shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused
required: 20000,
reward: enumHubGoalRewards.reward_rotater_180,
},
@ -185,8 +184,7 @@ export const tutorialGoals = [
// 19
// Compact splitter
{
// @TODO
shape: "CuCuCuCu",
shape: "CpRpCp--:SwSwSwSw",
required: 25000,
reward: enumHubGoalRewards.reward_splitter,
},
@ -195,15 +193,14 @@ export const tutorialGoals = [
// WIRES
{
shape: finalGameShape,
required: 50000,
required: 25000,
reward: enumHubGoalRewards.reward_wires_filters_and_levers,
},
// 21
// Display
{
// @TODO
shape: "CuCuCuCu",
shape: "CrCrCrCr:CwCwCwCw:CrCrCrCr:CwCwCwCw",
required: 25000,
reward: enumHubGoalRewards.reward_display,
},
@ -211,43 +208,37 @@ export const tutorialGoals = [
// 22
// Constant signal
{
// @TODO
shape: "CuCuCuCu",
required: 30000,
shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
required: 25000,
reward: enumHubGoalRewards.reward_constant_signal,
},
// 23
// Quad Painter
{
// @TODO
shape: "CuCuCuCu",
// shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", // processors t4 (two variants)
required: 35000,
shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
required: 5000,
reward: enumHubGoalRewards.reward_painter_quad,
},
// 24 Logic gates
{
// @TODO
shape: "CuCuCuCu",
required: 40000,
shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
required: 10000,
reward: enumHubGoalRewards.reward_logic_gates,
},
// 25 Virtual Processing
{
// @TODO
shape: "CuCuCuCu",
required: 45000,
shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg",
required: 10000,
reward: enumHubGoalRewards.reward_virtual_processing,
},
// 26 Freeplay
{
// @TODO
shape: "CuCuCuCu",
required: 100000,
shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw",
required: 10000,
reward: enumHubGoalRewards.reward_freeplay,
},
];

View File

@ -4,6 +4,7 @@ import { MetaConstantSignalBuilding } from "./buildings/constant_signal";
import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter";
import { MetaDisplayBuilding } from "./buildings/display";
import { MetaLeverBuilding } from "./buildings/lever";
import { MetaLogicGateBuilding } from "./buildings/logic_gate";
import { enumMinerVariants, MetaMinerBuilding } from "./buildings/miner";
import { MetaMixerBuilding } from "./buildings/mixer";
import { enumPainterVariants, MetaPainterBuilding } from "./buildings/painter";
@ -53,7 +54,7 @@ export const enumHubGoalRewardsToContentUnlocked = {
[enumHubGoalRewards.reward_constant_signal]: typed([
[MetaConstantSignalBuilding, defaultBuildingVariant],
]),
[enumHubGoalRewards.reward_logic_gates]: null, // @TODO!
[enumHubGoalRewards.reward_logic_gates]: typed([[MetaLogicGateBuilding, defaultBuildingVariant]]),
[enumHubGoalRewards.reward_virtual_processing]: null, // @TODO!
[enumHubGoalRewards.reward_wires_filters_and_levers]: typed([

View File

@ -2,10 +2,28 @@ import { findNiceIntegerValue } from "../core/utils";
import { ShapeDefinition } from "./shape_definition";
export const finalGameShape = "RuCw--Cw:----Ru--";
export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
export const blueprintShape = "CbCbCbRb:CwCwCwCw";
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 2];
const numEndgameUpgrades = G_IS_DEV || G_IS_STANDALONE ? 20 - fixedImprovements.length - 1 : 0;
function generateEndgameUpgrades() {
return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({
required: [
{ shape: blueprintShape, amount: 30000 + i * 10000 },
{ shape: finalGameShape, amount: 20000 + i * 5000 },
{ shape: rocketShape, amount: 20000 + i * 5000 },
],
excludePrevious: true,
}));
}
for (let i = 0; i < numEndgameUpgrades; ++i) {
fixedImprovements.push(0.1);
}
/** @typedef {{
* shape: string,
* amount: number
@ -23,95 +41,99 @@ const fixedImprovements = [0.5, 0.5, 1, 1, 2, 2];
export const UPGRADES = {
belt: [
{
required: [{ shape: "CuCuCuCu", amount: 150 }],
required: [{ shape: "CuCuCuCu", amount: 60 }],
},
{
required: [{ shape: "--CuCu--", amount: 1000 }],
required: [{ shape: "--CuCu--", amount: 500 }],
},
{
required: [{ shape: "CpCpCpCp", amount: 5000 }],
required: [{ shape: "CpCpCpCp", amount: 1000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 12000 }],
required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 6000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 20000 }],
required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 25000 }],
},
{
required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
miner: [
{
required: [{ shape: "RuRuRuRu", amount: 400 }],
required: [{ shape: "RuRuRuRu", amount: 300 }],
},
{
required: [{ shape: "Cu------", amount: 3000 }],
required: [{ shape: "Cu------", amount: 800 }],
},
{
required: [{ shape: "ScScScSc", amount: 7000 }],
required: [{ shape: "ScScScSc", amount: 3500 }],
},
{
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 15000 }],
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }],
},
{
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 30000 }],
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }],
},
{
required: [{ shape: finalGameShape, amount: 65000 }],
required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
processors: [
{
required: [{ shape: "SuSuSuSu", amount: 600 }],
required: [{ shape: "SuSuSuSu", amount: 500 }],
},
{
required: [{ shape: "RuRu----", amount: 2000 }],
required: [{ shape: "RuRu----", amount: 600 }],
},
{
required: [{ shape: "CgScScCg", amount: 15000 }],
required: [{ shape: "CgScScCg", amount: 3500 }],
},
{
required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 20000 }],
required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 25000 }],
},
{
required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 30000 }],
required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 50000 }],
},
{
required: [{ shape: finalGameShape, amount: 75000 }],
required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
painting: [
{
required: [{ shape: "RbRb----", amount: 1000 }],
required: [{ shape: "RbRb----", amount: 600 }],
},
{
required: [{ shape: "WrWrWrWr", amount: 3000 }],
required: [{ shape: "WrWrWrWr", amount: 3800 }],
},
{
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 15000 }],
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 20000 }],
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 30000 }],
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 50000 }],
},
{
required: [{ shape: finalGameShape, amount: 100000 }],
required: [{ shape: finalGameShape, amount: 50000 }],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
};
// Tiers need % of the previous tier as requirement too
const tierGrowth = 1.8;
const tierGrowth = 2.5;
// Automatically generate tier levels
for (const upgradeId in UPGRADES) {

3
src/js/globals.d.ts vendored
View File

@ -19,9 +19,6 @@ declare const G_BUILD_VERSION: string;
declare const G_ALL_UI_IMAGES: Array<string>;
declare const G_IS_RELEASE: boolean;
// Node require
declare function require(...args): any;
// Polyfills
declare interface String {
replaceAll(search: string, replacement: string): string;

View File

@ -25,6 +25,7 @@ export const SOUNDS = {
destroyBuilding: "destroy_building",
placeBuilding: "place_building",
placeBelt: "place_belt",
copy: "copy",
};
export const MUSIC = {

View File

@ -19,6 +19,7 @@ import { getCodeFromBuildingData } from "../../game/building_codes.js";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js";
import { Entity } from "../../game/entity.js";
import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js";
import { finalGameShape } from "../../game/upgrades.js";
import { SavegameInterface_V1005 } from "./1005.js";
const schema = require("./1006.json");
@ -151,11 +152,26 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 {
stored[shapeKey] = rebalance(stored[shapeKey]);
}
stored[finalGameShape] = 0;
// Reduce goals
if (dump.hubGoals.currentGoal) {
dump.hubGoals.currentGoal.required = rebalance(dump.hubGoals.currentGoal.required);
}
let level = Math.min(19, dump.hubGoals.level);
const levelMapping = {
14: 15,
15: 16,
16: 17,
17: 18,
18: 19,
19: 20,
};
dump.hubGoals.level = levelMapping[level] || level;
// Update entities
const entities = dump.entities;
for (let i = 0; i < entities.length; ++i) {

View File

@ -1,82 +1,80 @@
// Synchronizes all translations
const fs = require("fs");
const matchAll = require("match-all");
const path = require("path");
const YAWN = require("yawn-yaml/cjs");
const YAML = require("yaml");
const files = fs
.readdirSync(path.join(__dirname, "translations"))
.filter(x => x.endsWith(".yaml"))
.filter(x => x.indexOf("base-en") < 0);
const originalContents = fs
.readFileSync(path.join(__dirname, "translations", "base-en.yaml"))
.toString("utf-8");
const original = YAML.parse(originalContents);
const placeholderRegexp = /[[<]([a-zA-Z_0-9]+)[\]<]/gi;
function match(originalObj, translatedObj, path = "/") {
for (const key in originalObj) {
if (!translatedObj.hasOwnProperty(key)) {
console.warn(" | Missing key", path + key);
translatedObj[key] = originalObj[key];
continue;
}
const valueOriginal = originalObj[key];
const valueMatching = translatedObj[key];
if (typeof valueOriginal !== typeof valueMatching) {
console.warn(" | MISMATCHING type (obj|non-obj) in", path + key);
continue;
}
if (typeof valueOriginal === "object") {
match(valueOriginal, valueMatching, path + key + "/");
} else if (typeof valueOriginal === "string") {
// todo
const originalPlaceholders = matchAll(valueOriginal, placeholderRegexp).toArray();
const translatedPlaceholders = matchAll(valueMatching, placeholderRegexp).toArray();
if (originalPlaceholders.length !== translatedPlaceholders.length) {
console.warn(
" | Mismatching placeholders in",
path + key,
"->",
originalPlaceholders,
"vs",
translatedPlaceholders
);
translatedObj[key] = originalObj[key];
continue;
}
} else {
console.warn(" | Unknown type: ", typeof valueOriginal);
}
// const matching = translatedObj[key];
}
for (const key in translatedObj) {
if (!originalObj.hasOwnProperty(key)) {
console.warn(" | Obsolete key", path + key);
delete translatedObj[key];
}
}
}
for (let i = 0; i < files.length; ++i) {
const filePath = path.join(__dirname, "translations", files[i]);
console.log("Processing", files[i]);
const translatedContents = fs.readFileSync(filePath).toString("utf-8");
const translated = YAML.parse(translatedContents);
const handle = new YAWN(translatedContents);
const json = handle.json;
match(original, json, "/");
handle.json = json;
fs.writeFileSync(filePath, handle.yaml, "utf-8");
}
// Synchronizes all translations
const fs = require("fs");
const matchAll = require("match-all");
const path = require("path");
const YAML = require("yaml");
const files = fs
.readdirSync(path.join(__dirname, "translations"))
.filter(x => x.endsWith(".yaml"))
.filter(x => x.indexOf("base-en") < 0);
const originalContents = fs
.readFileSync(path.join(__dirname, "translations", "base-en.yaml"))
.toString("utf-8");
const original = YAML.parse(originalContents);
const placeholderRegexp = /[[<]([a-zA-Z_0-9]+)[\]<]/gi;
function match(originalObj, translatedObj, path = "/") {
for (const key in originalObj) {
if (!translatedObj.hasOwnProperty(key)) {
console.warn(" | Missing key", path + key);
translatedObj[key] = originalObj[key];
continue;
}
const valueOriginal = originalObj[key];
const valueMatching = translatedObj[key];
if (typeof valueOriginal !== typeof valueMatching) {
console.warn(" | MISMATCHING type (obj|non-obj) in", path + key);
continue;
}
if (typeof valueOriginal === "object") {
match(valueOriginal, valueMatching, path + key + "/");
} else if (typeof valueOriginal === "string") {
// todo
const originalPlaceholders = matchAll(valueOriginal, placeholderRegexp).toArray();
const translatedPlaceholders = matchAll(valueMatching, placeholderRegexp).toArray();
if (originalPlaceholders.length !== translatedPlaceholders.length) {
console.warn(
" | Mismatching placeholders in",
path + key,
"->",
originalPlaceholders,
"vs",
translatedPlaceholders
);
translatedObj[key] = originalObj[key];
continue;
}
} else {
console.warn(" | Unknown type: ", typeof valueOriginal);
}
}
for (const key in translatedObj) {
if (!originalObj.hasOwnProperty(key)) {
console.warn(" | Obsolete key", path + key);
delete translatedObj[key];
}
}
}
for (let i = 0; i < files.length; ++i) {
const filePath = path.join(__dirname, "translations", files[i]);
console.log("Processing", files[i]);
const translatedContents = fs.readFileSync(filePath).toString("utf-8");
const json = YAML.parse(translatedContents);
match(original, json, "/");
const stringified = YAML.stringify(json, {
indent: 4,
simpleKeys: true,
});
fs.writeFileSync(filePath, stringified, "utf-8");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -268,7 +268,13 @@ dialogs:
createMarker:
title: New Marker
titleEdit: Edit Marker
desc: Give it a meaningful name, you can also include a <strong>short key</strong> of a shape (Which you can generate <a href="https://viewer.shapez.io" target="_blank">here</a>)
desc: Give it a meaningful name, you can also include a <strong>short key</strong> of a shape (Which you can generate <link>here</link>)
editSignal:
title: Set Signal
descItems: >-
Choose a pre-defined item:
descShortKey: ... or enter the <strong>short key</strong> of a shape (Which you can generate <link>here</link>)
markerDemoLimit:
desc: You can only create two custom markers in the demo. Get the standalone for unlimited markers!
@ -281,6 +287,10 @@ dialogs:
title: Rename Savegame
desc: You can rename your savegame here.
entityWarning:
title: Performance Warning
desc: You have placed a lot of buildings, this is just a friendly reminder that the game can not handle an endless count of buildings - So try to keep your factories compact!
ingame:
# This is shown in the top left corner and displays useful keybindings in
# every situation
@ -350,6 +360,7 @@ ingame:
notifications:
newUpgrade: A new upgrade is available!
gameSaved: Your game has been saved.
freeplayLevelComplete: Level <level> has been completed!
# The "Upgrades" window
shop:
@ -360,7 +371,8 @@ ingame:
tier: Tier <x>
# The roman number for each tier
tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X]
tierLabels:
[I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX]
maximumLevel: MAXIMUM LEVEL (Speed x<currentMult>)
@ -573,7 +585,7 @@ buildings:
wire_tunnel:
default:
name: &wire_tunnel Wire Tunnel
name: &wire_tunnel Wire Crossing
description: Allows to cross two wires without connecting them.
constant_signal:
@ -589,16 +601,16 @@ buildings:
logic_gate:
default:
name: AND Gate
description: Emits a boolean "1" if both inputs are truthy.
description: Emits a boolean "1" if both inputs are truthy. (Truthy means shape, color or boolean "1")
not:
name: NOT Gate
description: Emits a boolean "1" if the input is not truthy.
description: Emits a boolean "1" if the input is not truthy. (Truthy means shape, color or boolean "1")
xor:
name: XOR Gate
description: Emits a boolean "1" if one of the inputs is truthy, but not both.
description: Emits a boolean "1" if one of the inputs is truthy, but not both. (Truthy means shape, color or boolean "1")
or:
name: OR Gate
description: Emits a boolean "1" if one of the inputs is truthy.
description: Emits a boolean "1" if one of the inputs is truthy. (Truthy means shape, color or boolean "1")
transistor:
default:
@ -622,7 +634,7 @@ buildings:
reader:
default:
name: &reader Belt Reader
description: Allows to measure belt throughput. Outputs the last read item on the wires layer (once unlocked).
description: Allows to measure the average belt throughput. Outputs the last read item on the wires layer (once unlocked).
analyzer:
default:
@ -632,20 +644,20 @@ buildings:
comparator:
default:
name: &comparator Compare
description: Returns boolean "1" if both items are exactly equal. Can compare shapes, items and booleans.
description: Returns boolean "1" if both signals are exactly equal. Can compare shapes, items and booleans.
virtual_processor:
default:
name: &virtual_processor Virtual Cutter
description: Computes
description: Virtually cuts the shape into two halves.
rotater:
name: Virtual Rotater
description: Virtually rotates the shape by 90 degrees clockwise.
description: Virtually rotates the shape, both clockwise and counter-clockwise.
unstacker:
name: Virtual Unstacker
description: Returns the topmost layer to the right, and the remaining ones on the left.
description: Virtually extracts the topmost layer to the right output and the remaining ones to the left.
stacker:
name: Virtual Stacker
@ -753,7 +765,7 @@ storyRewards:
reward_display:
title: Display
desc: >-
You have unlocked the <strong>Display</strong>! Connect a wires signal to it to present a color, shape or boolean!
You have unlocked the <strong>Display</strong> - Connect a signal on the wires layer to visualize its contents!
reward_constant_signal:
title: Constant Signal
@ -765,17 +777,19 @@ storyRewards:
title: Logic Gates
desc: >-
You unlocked <strong>logic gates</strong>! You don't have to be excited about this, but it's actually super cool!<br><br>
With those gates you can now perform AND, OR, XOR and NOT boolean operations!
With those gates you can now compute AND, OR, XOR and NOT operations.<br><br>
As a bonus on top I also just gave you a <strong>transistor</strong>!
reward_virtual_processing:
title: Virtual Processing
desc: >-
I just gave a whole bunch of new buildings which allow you to <strong>simulate the processing of shapes</strong>!<br><br>
You can now simulate a cutter, rotater, stacker and more on the wires layer!<br><br>
You can now simulate a cutter, rotater, stacker and more on the wires layer!
With this you now have three options to continue the game:<br><br>
- Build an <strong>automated machine</strong> to create any possible shape requested by the hub (This is cool, I swear!).<br><br>
- Build an <strong>automated machine</strong> to create any possible shape requested by the HUB (I recommend to try it!).<br><br>
- Build something cool with wires.<br><br>
- Continue to play regulary.
- Continue to play regulary.<br><br>
Whatever you choose, remember to have fun!
# Special reward, which is shown when there is no reward actually
no_reward:
@ -786,13 +800,14 @@ storyRewards:
no_reward_freeplay:
title: Next level
desc: >-
Congratulations! By the way, more content is planned for the standalone!
Congratulations!
reward_freeplay:
title: Freeplay
desc: >-
You did it! You unlocked the <strong>free-play mode</strong>! This means that shapes are now <strong>randomly</strong> generated!<br><br>
Since the hub will only require low quantities, I highly recommend to build a machine which automatically delivers the requested shape!
Since the hub will require a <strong>throughput</strong> from now on, I highly recommend to build a machine which automatically delivers the requested shape!<br><br>
The HUB outputs the requested shape on the wires layer, so all you have to do is to analyze it and automatically configure your factory based on that.
settings:
title: Settings
@ -1036,8 +1051,7 @@ keybindings:
wire: *wire
constant_signal: *constant_signal
logic_gate: Logic Gate
lever: Switch (regular)
lever_wires: Switch (wires)
lever: *lever
filter: *filter
wire_tunnel: *wire_tunnel
display: *display
@ -1059,7 +1073,8 @@ keybindings:
lockBeltDirection: Enable belt planner
switchDirectionLockSide: >-
Planner: Switch side
copyWireValue: >-
Wires: Copy value below cursor
massSelectStart: Hold and drag to start
massSelectSelectMultiple: Select multiple areas
massSelectCopy: Copy area
@ -1128,7 +1143,7 @@ tips:
- Organization is important. Try not to cross conveyors too much.
- Plan in advance, or it will be a huge chaos!
- Don't remove your old factories! You'll need them to unlock upgrades.
- Try beating level 18 on your own before seeking for help!
- Try beating level 20 on your own before seeking for help!
- Don't complicate things, try to stay simple and you'll go far.
- You may need to re-use factories later in the game. Plan your factories to be re-usable.
- Sometimes, you can find a needed shape in the map without creating it with stackers.
@ -1149,3 +1164,5 @@ tips:
- This game has a lot of settings, be sure to check them out!
- The marker to your hub has a small compass to indicate its direction!
- To clear belts, cut the area and then paste it at the same location.
- Press F4 to show your FPS and Tick Rate.
- Press F4 twice to show the tile of your mouse and camera.

File diff suppressed because it is too large Load Diff

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