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

Merge branch 'master' of https://github.com/tobspr/shapez.io into master

This commit is contained in:
dgs4349 2021-04-16 12:19:47 -04:00
commit 77e0160661
145 changed files with 30447 additions and 29238 deletions

View File

@ -42,6 +42,7 @@ jobs:
run: |
cd gulp
yarn gulp translations.fullBuild
yarn gulp localConfig.findOrCreate
cd ..
yarn tslint

7
.gitignore vendored
View File

@ -46,7 +46,14 @@ res_built
gulp/runnable-texturepacker.jar
tmp_standalone_files
tmp_standalone_files_china
# Local config
config.local.js
.DS_Store
# Editor artifacts
*.*.swp
*.*.swo
.history/

4
.gitpod.Dockerfile vendored Normal file
View File

@ -0,0 +1,4 @@
FROM gitpod/workspace-full
RUN sudo apt-get update \
&& sudo apt install ffmpeg -yq

10
.gitpod.yml Normal file
View File

@ -0,0 +1,10 @@
image:
file: .gitpod.Dockerfile
tasks:
- init: yarn && gp sync-done boot
- before: cd gulp
init: gp sync-await boot && yarn
command: yarn gulp
ports:
- port: 3005
onOpen: open-preview

View File

@ -1,3 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.trimTrailingWhitespace": true,
"editor.formatOnSave": true
}

View File

@ -31,6 +31,16 @@ Your goal is to produce shapes by cutting, rotating, merging and painting parts
**Notice**: This will produce a debug build with several debugging flags enabled. If you want to disable them, modify [`src/js/core/config.js`](src/js/core/config.js).
## Build Online with one-click setup
You can use [Gitpod](https://www.gitpod.io/) (an Online Open Source VS Code-like IDE which is free for Open Source) for working on issues and making PRs to this project. With a single click it will start a workspace and automatically:
- clone the `shapez.io` repo.
- install all of the dependencies.
- start `gulp` in `gulp/` directory.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/from-referrer/)
## Helping translate
Please checkout the [Translations readme](translations/).

View File

@ -1,11 +1,12 @@
/* eslint-disable quotes,no-undef */
const { app, BrowserWindow, Menu, MenuItem, session } = require("electron");
const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell } = require("electron");
const path = require("path");
const url = require("url");
const childProcess = require("child_process");
const { ipcMain } = require("electron");
const fs = require("fs");
const steam = require("./steam");
const asyncLock = require("async-lock");
const isDev = process.argv.indexOf("--dev") >= 0;
const isLocal = process.argv.indexOf("--local") >= 0;
@ -67,11 +68,7 @@ function createWindow() {
win.webContents.on("new-window", (event, pth) => {
event.preventDefault();
if (process.platform == "win32") {
childProcess.execSync("start " + pth);
} else if (process.platform == "linux") {
childProcess.execSync("xdg-open " + pth);
}
shell.openExternal(pth);
});
win.on("closed", () => {
@ -156,7 +153,82 @@ ipcMain.on("exit-app", (event, flag) => {
app.quit();
});
function performFsJob(job) {
let renameCounter = 1;
const fileLock = new asyncLock({
timeout: 30000,
maxPending: 1000,
});
function niceFileName(filename) {
return filename.replace(storePath, "@");
}
async function writeFileSafe(filename, contents) {
++renameCounter;
const prefix = "[ " + renameCounter + ":" + niceFileName(filename) + " ] ";
const transactionId = String(new Date().getTime()) + "." + renameCounter;
if (fileLock.isBusy()) {
console.warn(prefix, "Concurrent write process on", filename);
}
fileLock.acquire(filename, async () => {
console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId);
if (!fs.existsSync(filename)) {
// this one is easy
console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename));
await fs.promises.writeFile(filename, contents, { encoding: "utf8" });
return;
}
// first, write a temporary file (.tmp-XXX)
const tempName = filename + ".tmp-" + transactionId;
console.log(prefix, "Writing temporary file", niceFileName(tempName));
await fs.promises.writeFile(tempName, contents, { encoding: "utf8" });
// now, rename the original file to (.backup-XXX)
const oldTemporaryName = filename + ".backup-" + transactionId;
console.log(
prefix,
"Renaming old file",
niceFileName(filename),
"to",
niceFileName(oldTemporaryName)
);
await fs.promises.rename(filename, oldTemporaryName);
// now, rename the temporary file (.tmp-XXX) to the target
console.log(
prefix,
"Renaming the temporary file",
niceFileName(tempName),
"to the original",
niceFileName(filename)
);
await fs.promises.rename(tempName, filename);
// we are done now, try to create a backup, but don't fail if the backup fails
try {
// check if there is an old backup file
const backupFileName = filename + ".backup";
if (fs.existsSync(backupFileName)) {
console.log(prefix, "Deleting old backup file", niceFileName(backupFileName));
// delete the old backup
await fs.promises.unlink(backupFileName);
}
// rename the old file to the new backup file
console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location");
await fs.promises.rename(oldTemporaryName, backupFileName);
} catch (ex) {
console.error(prefix, "Failed to switch backup files:", ex);
}
});
}
async function performFsJob(job) {
const fname = path.join(storePath, job.filename);
switch (job.type) {
@ -168,38 +240,35 @@ function performFsJob(job) {
};
}
let contents = "";
try {
contents = fs.readFileSync(fname, { encoding: "utf8" });
const data = await fs.promises.readFile(fname, { encoding: "utf8" });
return {
success: true,
data,
};
} catch (ex) {
return {
error: ex,
};
}
return {
success: true,
data: contents,
};
}
case "write": {
try {
fs.writeFileSync(fname, job.contents);
} catch (ex) {
return {
error: ex,
};
}
await writeFileSafe(fname, job.contents);
return {
success: true,
data: job.contents,
};
} catch (ex) {
return {
error: ex,
};
}
}
case "delete": {
try {
fs.unlinkSync(fname);
await fs.promises.unlink(fname);
} catch (ex) {
return {
error: ex,
@ -217,12 +286,7 @@ function performFsJob(job) {
}
}
ipcMain.on("fs-job", (event, arg) => {
const result = performFsJob(arg);
event.reply("fs-response", { id: arg.id, result });
});
ipcMain.handle("fs-job", (event, arg) => performFsJob(arg));
ipcMain.on("fs-sync-job", (event, arg) => {
const result = performFsJob(arg);
event.returnValue = result;
});
steam.init(isDev);
steam.listen();

View File

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

73
electron/steam.js Normal file
View File

@ -0,0 +1,73 @@
const fs = require('fs');
const path = require('path');
const { ipcMain } = require("electron");
let greenworks = null;
let appId = null;
let initialized = false;
try {
greenworks = require("shapez.io-private-artifacts/steam/greenworks");
appId = parseInt(fs.readFileSync(path.join(__dirname, "steam_appid.txt"), "utf8"));
} catch (err) {
// greenworks is not installed
// throw err;
}
function init (isDev) {
if (!greenworks) {
return;
}
if (!isDev) {
if (greenworks.restartAppIfNecessary(appId)) {
console.log("Restarting ...");
process.exit(0);
}
}
if (!greenworks.init()) {
console.log("Failed to initialize greenworks");
process.exit(1);
}
initialized = true;
}
function listen () {
ipcMain.handle("steam:is-initialized", isInitialized);
if (!greenworks || !initialized) {
console.log("Ignoring Steam IPC events");
return;
}
ipcMain.handle("steam:get-achievement-names", getAchievementNames);
ipcMain.handle("steam:activate-achievement", activateAchievement);
}
function isInitialized(event) {
return Promise.resolve(initialized);
}
function getAchievementNames(event) {
return new Promise((resolve, reject) => {
try {
const achievements = greenworks.getAchievementNames()
resolve(achievements);
} catch (err) {
reject(err);
}
});
}
function activateAchievement(event, id) {
return new Promise((resolve, reject) => {
greenworks.activateAchievement(id, () => resolve(), err => reject(err))
});
}
module.exports = {
init,
listen
};

View File

@ -1 +1 @@
1134480
1318690

View File

@ -3,16 +3,16 @@
"@electron/get@^1.0.1":
version "1.12.2"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.2.tgz#6442066afb99be08cefb9a281e4b4692b33764f3"
integrity sha512-vAuHUbfvBQpYTJ5wB7uVIDq5c/Ry0fiTBMs7lnEYAo/qXXppIVcWdfBr57u6eRnKdVso7KSiH6p/LbQAG6Izrg==
version "1.12.4"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.4.tgz#a5971113fc1bf8fa12a8789dc20152a7359f06ab"
integrity sha512-6nr9DbJPUR9Xujw6zD3y+rS95TyItEVM0NVjt1EehY2vUWfIgPiIPVHxCvaTS0xr2B+DRxovYVKbuOWqC35kjg==
dependencies:
debug "^4.1.1"
env-paths "^2.2.0"
fs-extra "^8.1.0"
got "^9.6.0"
progress "^2.0.3"
sanitize-filename "^1.6.2"
semver "^6.2.0"
sumchecker "^3.0.1"
optionalDependencies:
global-agent "^2.0.2"
@ -31,14 +31,24 @@
defer-to-connect "^1.0.1"
"@types/node@^12.0.12":
version "12.12.62"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.62.tgz#733923d73669188d35950253dd18a21570085d2b"
integrity sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg==
version "12.20.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.5.tgz#4ca82a766f05c359fd6c77505007e5a272f4bb9b"
integrity sha512-5Oy7tYZnu3a4pnJ//d4yVvOImExl4Vtwf0D40iKUlU+XlUsyV9iyFWyCFlwy489b72FMAik/EFwRkNLjjOdSPg==
async-lock@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.8.tgz#7b02bdfa2de603c0713acecd11184cf97bbc7c4c"
integrity sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ==
boolean@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.1.tgz#35ecf2b4a2ee191b0b44986f14eb5f052a5cbb4f"
integrity sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==
version "3.0.2"
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.2.tgz#df1baa18b6a2b0e70840475e1d93ec8fe75b2570"
integrity sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g==
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0:
version "1.1.1"
@ -65,7 +75,7 @@ clone-response@^1.0.2:
dependencies:
mimic-response "^1.0.0"
concat-stream@1.6.2:
concat-stream@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@ -84,16 +94,16 @@ config-chain@^1.1.11:
proto-list "~1.2.1"
core-js@^3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
version "3.9.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae"
integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
debug@2.6.9:
debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@ -101,9 +111,9 @@ debug@2.6.9:
ms "2.0.0"
debug@^4.1.0, debug@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
@ -136,10 +146,10 @@ duplexer3@^0.1.4:
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
electron@10.1.3:
version "10.1.3"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.1.3.tgz#7e276e373bf30078bd4cb1184850a91268dc0e6c"
integrity sha512-CR8LrlG47MdAp317SQ3vGYa2o2cIMdMSMPYH46OVitFLk35dwE9fn3VqvhUIXhCHYcNWIAPzMhkVHpkoFdKWuw==
electron@10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.4.0.tgz#018385914474b56110a5a43087a53c114b67c08d"
integrity sha512-qK8OOCWuNvEFWThmjkukkqDwIpBqULlDuMXVC9MC/2P4UaWJEjIYvBmBuTyxtFcKoE3kWvcWyeRDUuvzVxxXjA==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^12.0.12"
@ -158,9 +168,9 @@ end-of-stream@^1.1.0:
once "^1.4.0"
env-paths@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
es6-error@^4.1.1:
version "4.1.1"
@ -173,19 +183,19 @@ escape-string-regexp@^4.0.0:
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
extract-zip@^1.0.3:
version "1.6.7"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "1.6.2"
debug "2.6.9"
mkdirp "0.5.1"
yauzl "2.4.1"
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
fd-slicer@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
@ -236,9 +246,9 @@ global-tunnel-ng@^2.7.1:
tunnel "^0.0.6"
globalthis@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9"
integrity sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==
version "1.0.2"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.2.tgz#2a235d34f4d8036219f7e34929b5de9e18166b8b"
integrity sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==
dependencies:
define-properties "^1.1.3"
@ -259,15 +269,10 @@ got@^9.6.0:
to-readable-stream "^1.0.0"
url-parse-lax "^3.0.0"
graceful-fs@^4.1.6:
version "4.2.2"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
graceful-fs@^4.2.0:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.6"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
http-cache-semantics@^4.0.0:
version "4.1.0"
@ -280,9 +285,9 @@ inherits@^2.0.3, inherits@~2.0.3:
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
isarray@~1.0.0:
version "1.0.0"
@ -314,9 +319,9 @@ keyv@^3.0.0:
json-buffer "3.0.0"
lodash@^4.17.10:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
@ -328,6 +333,13 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
matcher@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
@ -340,17 +352,17 @@ mimic-response@^1.0.0, mimic-response@^1.0.1:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mkdirp@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
mkdirp@^0.5.4:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "0.0.8"
minimist "^1.2.5"
ms@2.0.0:
version "2.0.0"
@ -431,9 +443,9 @@ pump@^3.0.0:
once "^1.3.1"
readable-stream@^2.2.2:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
@ -467,22 +479,22 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
sanitize-filename@^1.6.2:
version "1.6.3"
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
dependencies:
truncate-utf8-bytes "^1.0.0"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
semver@^6.2.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.2:
version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
version "7.3.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
dependencies:
lru-cache "^6.0.0"
serialize-error@^7.0.1:
version "7.0.1"
@ -491,6 +503,10 @@ serialize-error@^7.0.1:
dependencies:
type-fest "^0.13.1"
"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v85":
version "0.1.0"
resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#63adf7e0ea4b90c2a29053ce1f0ec9d573b3ac0a"
sprintf-js@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
@ -515,13 +531,6 @@ to-readable-stream@^1.0.0:
resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
truncate-utf8-bytes@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys=
dependencies:
utf8-byte-length "^1.0.1"
tunnel@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
@ -549,11 +558,6 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"
utf8-byte-length@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@ -564,9 +568,15 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
yauzl@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
fd-slicer "~1.0.1"
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"

View File

@ -21,7 +21,6 @@ function gulptasksCSS($, gulp, buildFolder, browserSync) {
const plugins = [postcssAssetsPlugin(cachebust)];
if (prod) {
plugins.unshift(
$.postcssUnprefix(),
$.postcssPresetEnv({
browsers: ["> 0.1%"],
})
@ -62,7 +61,7 @@ function gulptasksCSS($, gulp, buildFolder, browserSync) {
return gulp
.src("../src/css/main.scss", { cwd: __dirname })
.pipe($.plumber())
.pipe($.sass.sync().on("error", $.sass.logError))
.pipe($.dartSass.sync().on("error", $.dartSass.logError))
.pipe(
$.postcss([
$.postcssCriticalSplit({
@ -95,7 +94,7 @@ function gulptasksCSS($, gulp, buildFolder, browserSync) {
return gulp
.src("../src/css/main.scss", { cwd: __dirname })
.pipe($.plumber())
.pipe($.sass.sync().on("error", $.sass.logError))
.pipe($.dartSass.sync().on("error", $.dartSass.logError))
.pipe(
$.postcss([
$.postcssCriticalSplit({

View File

@ -50,6 +50,9 @@ css.gulptasksCSS($, gulp, buildFolder, browserSync);
const sounds = require("./sounds");
sounds.gulptasksSounds($, gulp, buildFolder);
const localConfig = require("./local-config");
localConfig.gulptasksLocalConfig($, gulp);
const js = require("./js");
js.gulptasksJS($, gulp, buildFolder, browserSync);
@ -136,7 +139,7 @@ gulp.task("main.webserver", () => {
);
});
function serve({ standalone }) {
function serve({ standalone, chineseVersion = false }) {
browserSync.init({
server: buildFolder,
port: 3005,
@ -199,10 +202,14 @@ function serve({ standalone }) {
// Start the webpack watching server (Will never return)
if (standalone) {
gulp.series("js.standalone-dev.watch")(() => true);
} else {
if (chineseVersion) {
gulp.series("china.js.dev.watch")(() => true);
} else {
gulp.series("js.dev.watch")(() => true);
}
}
}
///////////////////// RUNNABLE TASKS /////////////////////
@ -221,6 +228,7 @@ gulp.task(
gulp.series(
"utils.cleanup",
"utils.copyAdditionalBuildFiles",
"localConfig.findOrCreate",
"imgres.buildAtlas",
"imgres.atlasToJson",
"imgres.atlas",
@ -238,6 +246,7 @@ gulp.task(
"build.standalone.dev",
gulp.series(
"utils.cleanup",
"localConfig.findOrCreate",
"imgres.buildAtlas",
"imgres.atlasToJson",
"imgres.atlas",
@ -284,31 +293,29 @@ gulp.task(
);
// Builds everything (standalone-prod)
for (const prefix of ["", "china."]) {
gulp.task(
"step.standalone-prod.code",
gulp.series("sounds.fullbuildHQ", "translations.fullBuild", "js.standalone-prod")
);
gulp.task("step.standalone-prod.mainbuild", gulp.parallel("step.baseResources", "step.standalone-prod.code"));
gulp.task(
"step.standalone-prod.all",
gulp.series("step.standalone-prod.mainbuild", "css.prod-standalone", "html.standalone-prod")
);
gulp.task(
"build.standalone-prod",
gulp.series("utils.cleanup", "step.standalone-prod.all", "step.postbuild")
prefix + "step.standalone-prod.code",
gulp.series("sounds.fullbuildHQ", "translations.fullBuild", prefix + "js.standalone-prod")
);
// 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"
)
prefix + "step.standalone-prod.mainbuild",
gulp.parallel("step.baseResources", prefix + "step.standalone-prod.code")
);
gulp.task(
prefix + "step.standalone-prod.all",
gulp.series(prefix + "step.standalone-prod.mainbuild", "css.prod-standalone", "html.standalone-prod")
);
gulp.task(
prefix + "build.standalone-prod",
gulp.series("utils.cleanup", prefix + "step.standalone-prod.all", "step.postbuild")
);
}
// Deploying!
gulp.task(
"main.deploy.alpha",
@ -320,7 +327,12 @@ gulp.task(
);
gulp.task("main.deploy.prod", gulp.series("utils.requireCleanWorkingTree", "build.prod", "ftp.upload.prod"));
gulp.task("main.deploy.all", gulp.series("main.deploy.staging", "main.deploy.prod"));
gulp.task("main.standalone", gulp.series("build.standalone-prod", "standalone.package.prod"));
gulp.task("regular.main.standalone", gulp.series("build.standalone-prod", "standalone.package.prod"));
gulp.task(
"china.main.standalone",
gulp.series("china.build.standalone-prod", "china.standalone.package.prod")
);
gulp.task("standalone.all", gulp.series("regular.main.standalone", "china.main.standalone"));
// Live-development
gulp.task(
@ -331,5 +343,9 @@ gulp.task(
"main.serveStandalone",
gulp.series("build.standalone.dev", () => serve({ standalone: true }))
);
gulp.task(
"china.main.serveDev",
gulp.series("build.dev", () => serve({ standalone: false, chineseVersion: true }))
);
gulp.task("default", gulp.series("main.serveDev"));

View File

@ -6,7 +6,6 @@ function requireUncached(module) {
}
function gulptasksJS($, gulp, buildFolder, browserSync) {
//// DEV
gulp.task("js.dev.watch", () => {
@ -30,6 +29,36 @@ function gulptasksJS($, gulp, buildFolder, browserSync) {
.pipe(gulp.dest(buildFolder));
});
//// DEV CHINA
gulp.task("china.js.dev.watch", () => {
return gulp
.src("../src/js/main.js")
.pipe(
$.webpackStream(
requireUncached("./webpack.config.js")({
watch: true,
chineseVersion: true,
})
)
)
.pipe(gulp.dest(buildFolder))
.pipe(browserSync.stream());
});
gulp.task("china.js.dev", () => {
return gulp
.src("../src/js/main.js")
.pipe(
$.webpackStream(
requireUncached("./webpack.config.js")({
chineseVersion: true,
})
)
)
.pipe(gulp.dest(buildFolder));
});
//// STAGING
gulp.task("js.staging.transpiled", () => {
@ -162,6 +191,23 @@ function gulptasksJS($, gulp, buildFolder, browserSync) {
)
.pipe(gulp.dest(buildFolder));
});
gulp.task("china.js.standalone-prod", () => {
return gulp
.src("../src/js/main.js")
.pipe(
$.webpackStream(
requireUncached("./webpack.production.config.js")({
enableAssert: false,
environment: "prod",
es6: true,
standalone: true,
chineseVersion: true,
})
)
)
.pipe(gulp.dest(buildFolder));
});
}
module.exports = {

18
gulp/local-config.js Normal file
View File

@ -0,0 +1,18 @@
const path = require("path");
const fs = require("fs");
const fse = require("fs-extra");
const configTemplatePath = path.join(__dirname, "../src/js/core/config.local.template.js");
const configPath = path.join(__dirname, "../src/js/core/config.local.js");
function gulptasksLocalConfig($, gulp) {
gulp.task("localConfig.findOrCreate", cb => {
if (!fs.existsSync(configPath)) {
fse.copySync(configTemplatePath, configPath);
}
cb();
});
}
module.exports = { gulptasksLocalConfig };

View File

@ -18,6 +18,7 @@
"@types/node": "^12.7.5",
"ajv": "^6.10.2",
"audiosprite": "^0.7.2",
"babel-core": "^6.26.3",
"babel-loader": "^8.1.0",
"browser-sync": "^2.26.10",
"circular-dependency-plugin": "^5.0.2",
@ -33,6 +34,7 @@
"fastdom": "^1.0.9",
"flatted": "^2.0.1",
"fs-extra": "^8.1.0",
"gifsicle": "^5.2.0",
"gulp-audiosprite": "^1.1.0",
"howler": "^2.1.2",
"html-loader": "^0.5.5",
@ -41,10 +43,12 @@
"markdown-loader": "^5.1.0",
"node-sri": "^1.1.1",
"phonegap-plugin-mobile-accessibility": "^1.0.5",
"postcss": ">=5.0.0",
"promise-polyfill": "^8.1.0",
"query-string": "^6.8.1",
"rusha": "^0.8.13",
"serialize-error": "^3.0.0",
"stream-browserify": "^3.0.0",
"strictdom": "^1.0.1",
"string-replace-webpack-plugin": "^0.1.3",
"strip-indent": "^3.0.0",
@ -58,7 +62,8 @@
"webpack-plugin-replace": "^1.1.1",
"webpack-strip-block": "^0.2.0",
"whatwg-fetch": "^3.0.0",
"worker-loader": "^2.0.0"
"worker-loader": "^2.0.0",
"yaml": "^1.10.0"
},
"devDependencies": {
"autoprefixer": "^9.4.3",
@ -74,6 +79,7 @@
"gulp-cache": "^1.1.3",
"gulp-cached": "^1.1.1",
"gulp-clean": "^0.4.0",
"gulp-dart-sass": "^1.0.2",
"gulp-dom": "^1.0.0",
"gulp-flatten": "^0.4.0",
"gulp-fluent-ffmpeg": "^2.0.0",
@ -87,7 +93,6 @@
"gulp-pngquant": "^1.0.13",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^2.0.0",
"gulp-sass": "^4.1.0",
"gulp-sass-lint": "^1.4.0",
"gulp-sftp": "git+https://git@github.com/webksde/gulp-sftp",
"gulp-terser": "^1.2.0",

View File

@ -1,5 +1,6 @@
require("colors");
const packager = require("electron-packager");
const pj = require("../electron/package.json");
const path = require("path");
const { getVersion } = require("./buildutils");
const fs = require("fs");
@ -9,20 +10,31 @@ const execSync = require("child_process").execSync;
function gulptasksStandalone($, gulp) {
const electronBaseDir = path.join(__dirname, "..", "electron");
const targets = [
{
tempDestDir: path.join(__dirname, "..", "tmp_standalone_files"),
suffix: "",
taskPrefix: "",
},
{
tempDestDir: path.join(__dirname, "..", "tmp_standalone_files_china"),
suffix: "china",
taskPrefix: "china.",
},
];
const tempDestDir = path.join(__dirname, "..", "tmp_standalone_files");
for (const { tempDestDir, suffix, taskPrefix } of targets) {
const tempDestBuildDir = path.join(tempDestDir, "built");
gulp.task("standalone.prepare.cleanup", () => {
gulp.task(taskPrefix + "standalone.prepare.cleanup", () => {
return gulp.src(tempDestDir, { read: false, allowEmpty: true }).pipe($.clean({ force: true }));
});
gulp.task("standalone.prepare.copyPrefab", () => {
// const requiredFiles = $.glob.sync("../electron/");
gulp.task(taskPrefix + "standalone.prepare.copyPrefab", () => {
const requiredFiles = [
path.join(electronBaseDir, "lib", "**", "*.node"),
path.join(electronBaseDir, "node_modules", "**", "*.*"),
path.join(electronBaseDir, "node_modules", "**", ".*"),
path.join(electronBaseDir, "steam_appid.txt"),
path.join(electronBaseDir, "favicon*"),
// fails on platforms which support symlinks
@ -32,23 +44,26 @@ function gulptasksStandalone($, gulp) {
return gulp.src(requiredFiles, { base: electronBaseDir }).pipe(gulp.dest(tempDestBuildDir));
});
gulp.task("standalone.prepare.writePackageJson", cb => {
fs.writeFileSync(
path.join(tempDestBuildDir, "package.json"),
JSON.stringify(
gulp.task(taskPrefix + "standalone.prepare.writePackageJson", cb => {
const packageJsonString = JSON.stringify(
{
devDependencies: {
electron: "6.1.12",
scripts: {
start: pj.scripts.start,
},
devDependencies: pj.devDependencies,
dependencies: pj.dependencies,
optionalDependencies: pj.optionalDependencies,
},
null,
4
)
);
fs.writeFileSync(path.join(tempDestBuildDir, "package.json"), packageJsonString);
cb();
});
gulp.task("standalone.prepareVDF", cb => {
gulp.task(taskPrefix + "standalone.prepareVDF", cb => {
const hash = buildutils.getRevision();
const steampipeDir = path.join(__dirname, "steampipe", "scripts");
@ -62,15 +77,15 @@ function gulptasksStandalone($, gulp) {
cb();
});
gulp.task("standalone.prepare.minifyCode", () => {
gulp.task(taskPrefix + "standalone.prepare.minifyCode", () => {
return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir));
});
gulp.task("standalone.prepare.copyGamefiles", () => {
gulp.task(taskPrefix + "standalone.prepare.copyGamefiles", () => {
return gulp.src("../build/**/*.*", { base: "../build" }).pipe(gulp.dest(tempDestBuildDir));
});
gulp.task("standalone.killRunningInstances", cb => {
gulp.task(taskPrefix + "standalone.killRunningInstances", cb => {
try {
execSync("taskkill /F /IM shapezio.exe");
} catch (ex) {
@ -80,26 +95,33 @@ function gulptasksStandalone($, gulp) {
});
gulp.task(
"standalone.prepare",
taskPrefix + "standalone.prepare",
gulp.series(
"standalone.killRunningInstances",
"standalone.prepare.cleanup",
"standalone.prepare.copyPrefab",
"standalone.prepare.writePackageJson",
"standalone.prepare.minifyCode",
"standalone.prepare.copyGamefiles"
taskPrefix + "standalone.killRunningInstances",
taskPrefix + "standalone.prepare.cleanup",
taskPrefix + "standalone.prepare.copyPrefab",
taskPrefix + "standalone.prepare.writePackageJson",
taskPrefix + "standalone.prepare.minifyCode",
taskPrefix + "standalone.prepare.copyGamefiles"
)
);
/**
*
* @param {'win32'|'linux'|'darwin'} platform
* @param {'win32'|'linux'} platform
* @param {'x64'|'ia32'} arch
* @param {function():void} cb
* @param {boolean=} isRelease
*/
function packageStandalone(platform, arch, cb, isRelease = true) {
function packageStandalone(platform, arch, cb) {
const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml"));
const privateArtifactsPath = "node_modules/shapez.io-private-artifacts";
let asar;
if (fs.existsSync(path.join(tempDestBuildDir, privateArtifactsPath))) {
asar = { unpackDir: privateArtifactsPath };
} else {
asar = true;
}
packager({
dir: tempDestBuildDir,
@ -108,29 +130,14 @@ function gulptasksStandalone($, gulp) {
buildVersion: "1.0.0",
arch,
platform,
asar: true,
asar: asar,
executableName: "shapezio",
icon: path.join(electronBaseDir, "favicon"),
name: "shapez.io-standalone",
name: "shapez.io-standalone" + suffix,
out: tempDestDir,
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);
@ -145,6 +152,11 @@ function gulptasksStandalone($, gulp) {
fs.readFileSync(path.join(__dirname, "..", "LICENSE"))
);
fse.copySync(
path.join(tempDestBuildDir, "steam_appid.txt"),
path.join(appPath, "steam_appid.txt")
);
fs.writeFileSync(path.join(appPath, ".itch.toml"), tomlFile);
if (platform === "linux") {
@ -154,51 +166,6 @@ function gulptasksStandalone($, gulp) {
);
fs.chmodSync(path.join(appPath, "play.sh"), 0o775);
}
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"),
'#!/usr/bin/env bash\n./shapez.io-standalone.app/Contents/MacOS/shapezio --no-sandbox "$@"\n'
);
fs.chmodSync(path.join(appPath, "play.sh"), 0o775);
fs.chmodSync(
path.join(appPath, "shapez.io-standalone.app", "Contents", "MacOS", "shapezio"),
0o775
);
const finalPath = path.join(appPath, "shapez.io-standalone.app");
const frameworksDir = path.join(finalPath, "Contents", "Frameworks");
const frameworkFolders = fs
.readdirSync(frameworksDir)
.filter(fname => fname.endsWith(".framework"));
for (let i = 0; i < frameworkFolders.length; ++i) {
const folderName = frameworkFolders[i];
const frameworkFolder = path.join(frameworksDir, folderName);
console.log(" -> ", frameworkFolder);
const filesToDelete = fs
.readdirSync(frameworkFolder)
.filter(fname => fname.toLowerCase() !== "versions");
filesToDelete.forEach(fname => {
console.log(" -> Deleting", fname);
fs.unlinkSync(path.join(frameworkFolder, fname));
});
const frameworkSourceDir = path.join(frameworkFolder, "Versions", "A");
fse.copySync(frameworkSourceDir, frameworkFolder);
}
}
});
cb();
@ -210,26 +177,22 @@ function gulptasksStandalone($, gulp) {
);
}
gulp.task("standalone.package.prod.win64", cb => packageStandalone("win32", "x64", cb));
gulp.task("standalone.package.prod.win32", cb => packageStandalone("win32", "ia32", cb));
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(taskPrefix + "standalone.package.prod.win64", cb => packageStandalone("win32", "x64", cb));
gulp.task(taskPrefix + "standalone.package.prod.linux64", cb =>
packageStandalone("linux", "x64", cb)
);
gulp.task(
"standalone.package.prod",
taskPrefix + "standalone.package.prod",
gulp.series(
"standalone.prepare",
taskPrefix + "standalone.prepare",
gulp.parallel(
"standalone.package.prod.win64",
"standalone.package.prod.linux64",
"standalone.package.prod.darwin64"
taskPrefix + "standalone.package.prod.win64",
taskPrefix + "standalone.package.prod.linux64"
)
)
);
}
}
module.exports = { gulptasksStandalone };

View File

@ -2,14 +2,16 @@
{
"appid" "1318690"
"desc" "$DESC$"
"buildoutput" "C:\work\shapez\shapez.io\gulp\steampipe\steamtemp"
"buildoutput" "C:\work\shapez.io\gulp\steampipe\steamtemp"
"contentroot" ""
"setlive" ""
"preview" "0"
"local" ""
"depots"
{
"1318691" "C:\work\shapez\shapez.io\gulp\steampipe\scripts\windows.vdf"
"1318692" "C:\work\shapez\shapez.io\gulp\steampipe\scripts\linux.vdf"
"1318691" "C:\work\shapez.io\gulp\steampipe\scripts\windows.vdf"
"1318694" "C:\work\shapez.io\gulp\steampipe\scripts\china-windows.vdf"
"1318692" "C:\work\shapez.io\gulp\steampipe\scripts\linux.vdf"
"1318695" "C:\work\shapez.io\gulp\steampipe\scripts\china-linux.vdf"
}
}

View File

@ -0,0 +1,12 @@
"DepotBuildConfig"
{
"DepotID" "1318695"
"contentroot" "C:\work\shapez.io\tmp_standalone_files_china\shapez.io-standalonechina-linux-x64"
"FileMapping"
{
"LocalPath" "*"
"DepotPath" "."
"recursive" "1"
}
"FileExclusion" "*.pdb"
}

View File

@ -0,0 +1,12 @@
"DepotBuildConfig"
{
"DepotID" "1318694"
"contentroot" "C:\work\shapez.io\tmp_standalone_files_china\shapez.io-standalonechina-win32-x64"
"FileMapping"
{
"LocalPath" "*"
"DepotPath" "."
"recursive" "1"
}
"FileExclusion" "*.pdb"
}

View File

@ -1,7 +1,7 @@
"DepotBuildConfig"
{
"DepotID" "1318692"
"contentroot" "C:\work\shapez\shapez.io\tmp_standalone_files\shapez.io-standalone-linux-x64"
"contentroot" "C:\work\shapez.io\tmp_standalone_files\shapez.io-standalone-linux-x64"
"FileMapping"
{
"LocalPath" "*"

View File

@ -1,7 +1,7 @@
"DepotBuildConfig"
{
"DepotID" "1318691"
"contentroot" "C:\work\shapez\shapez.io\tmp_standalone_files\shapez.io-standalone-win32-x64"
"contentroot" "C:\work\shapez.io\tmp_standalone_files\shapez.io-standalone-win32-x64"
"FileMapping"
{
"LocalPath" "*"

View File

@ -1,4 +1,4 @@
@echo off
cmd /c gulp standalone.prepareVDF
cmd /c yarn gulp standalone.prepareVDF
steamcmd +login %STEAM_UPLOAD_SHAPEZ_ID% %STEAM_UPLOAD_SHAPEZ_USER% +run_app_build %cd%/scripts/app.vdf +quit
start https://partner.steamgames.com/apps/builds/1318690

View File

@ -25,6 +25,7 @@ function gulptasksTranslations($, gulp) {
files
.filter(name => name.endsWith(".yaml"))
.forEach(fname => {
console.log("Loading", fname);
const languageName = fname.replace(".yaml", "");
const abspath = path.join(translationsSourceDir, fname);
@ -40,39 +41,13 @@ function gulptasksTranslations($, gulp) {
${storePage.intro.replace(/\n/gi, "\n\n")}
[h2]${storePage.title_advantages}[/h2]
[h2]${storePage.what_others_say}[/h2]
[list]
${storePage.advantages
.map(x => "[*] " + x.replace(/<b>/, "[b]").replace(/<\/b>/, "[/b]"))
.join("\n")}
[*] [i]${storePage.nothernlion_comment}[/i] [b]- Northernlion, YouTube[/b]
[*] [i]${storePage.notch_comment}[/i] [b]- Notch[/b]
[*] [i]${storePage.steam_review_comment}[/i] [b]- Steam User[/b]
[/list]
[h2]${storePage.title_future}[/h2]
[list]
${storePage.planned
.map(x => "[*] " + x.replace(/<b>/, "[b]").replace(/<\/b>/, "[/b]"))
.join("\n")}
[/list]
[h2]${storePage.title_open_source}[/h2]
${storePage.text_open_source.replace(/\n/gi, "\n\n")}
[h2]${storePage.title_links}[/h2]
[list]
[*] [url=https://discord.com/invite/HN7EVzV]${storePage.links.discord}[/url]
[*] [url=https://trello.com/b/ISQncpJP/shapezio]${storePage.links.roadmap}[/url]
[*] [url=https://www.reddit.com/r/shapezio]${storePage.links.subreddit}[/url]
[*] [url=https://github.com/tobspr/shapez.io]${storePage.links.source_code}[/url]
[*] [url=https://github.com/tobspr/shapez.io/blob/master/translations/README.md]${
storePage.links.translate
}[/url]
[/list]
`;
fs.writeFileSync(destpath, trim(content.replace(/(\n[ \t\r]*)/gi, "\n")), {

View File

@ -6,7 +6,7 @@ const { getRevision, getVersion, getAllResourceImages } = require("./buildutils"
const lzString = require("lz-string");
const CircularDependencyPlugin = require("circular-dependency-plugin");
module.exports = ({ watch = false, standalone = false }) => {
module.exports = ({ watch = false, standalone = false, chineseVersion = false }) => {
return {
mode: "development",
devtool: "cheap-source-map",
@ -34,6 +34,7 @@ module.exports = ({ watch = false, standalone = false }) => {
G_TRACKING_ENDPOINT: JSON.stringify(
lzString.compressToEncodedURIComponent("http://localhost:10005/v1")
),
G_CHINA_VERSION: JSON.stringify(chineseVersion),
G_IS_DEV: "true",
G_IS_RELEASE: "false",
G_IS_MOBILE_APP: "false",

View File

@ -16,12 +16,15 @@ module.exports = ({
standalone = false,
isBrowser = true,
mobileApp = false,
chineseVersion = false,
}) => {
const globalDefs = {
assert: enableAssert ? "window.assert" : "false && window.assert",
assertAlways: "window.assert",
abstract: "window.assert(false, 'abstract method called');",
G_IS_DEV: "false",
G_CHINA_VERSION: JSON.stringify(chineseVersion),
G_IS_RELEASE: environment === "prod" ? "true" : "false",
G_IS_STANDALONE: standalone ? "true" : "false",
G_IS_BROWSER: isBrowser ? "true" : "false",

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
"private": true,
"scripts": {
"dev": "cd gulp && yarn gulp main.serveDev",
"devStandalone": "cd gulp && yarn gulp main.serveStandalone",
"tslint": "cd src/js && tsc",
"lint": "eslint src/js",
"prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*",
@ -28,6 +29,7 @@
"@types/cordova": "^0.0.34",
"@types/filesystem": "^0.0.29",
"ajv": "^6.10.2",
"babel-core": "^6.26.3",
"babel-loader": "^8.0.4",
"circular-dependency-plugin": "^5.0.2",
"circular-json": "^0.5.9",
@ -49,6 +51,7 @@
"markdown-loader": "^4.0.0",
"match-all": "^1.2.5",
"phonegap-plugin-mobile-accessibility": "^1.0.5",
"postcss": ">=5.0.0",
"promise-polyfill": "^8.1.0",
"query-string": "^6.8.1",
"rusha": "^0.8.13",

BIN
res/logo_cn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

38
res/ui/languages/fi.svg Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<rect style="fill:#F5F5F5;" width="512" height="512"/>
<polygon style="fill:#41479B;" points="512,229.517 211.862,229.517 211.862,0 158.897,0 158.897,229.517 0,229.517 0,282.483
158.897,282.483 158.897,512 211.862,512 211.862,282.483 512,282.483 "/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 786 B

38
res/ui/languages/ro.svg Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<rect style="fill:#41479B;" width="170.67" height="512"/>
<rect x="170.67" style="fill:#FFE15A;" width="170.67" height="512"/>
<rect x="341.33" style="fill:#FF4B55;" width="170.67" height="512"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -14,6 +14,12 @@
"vetur.format.defaultFormatter.js": "vscode-typescript",
"vetur.format.defaultFormatter.ts": "vscode-typescript",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"workbench.colorCustomizations": {
"activityBar.background": "#163328",
"titleBar.activeBackground": "#1F4738",
"titleBar.activeForeground": "#F7FBFA"
}
}
}

View File

@ -1,4 +1,5 @@
@include MakeAnimationWrappedEvenOdd(0.2s ease-in-out, "changeAnim") {
@each $animName in ("changeAnimEven", "changeAnimOdd") {
@keyframes #{$animName} {
0% {
transform: scale(1, 1);
}
@ -11,3 +12,8 @@
transform: scale(1, 1);
}
}
.#{$animName} {
animation: $animName 0.2s ease-in-out;
}
}

View File

@ -0,0 +1,18 @@
[data-changelog-skin="achievements"] {
background: #f8f8f8;
@include DarkThemeOverride {
background: rgba(0, 10, 20, 0.2);
}
@include S(border-radius, 5px);
&::before {
content: " ";
width: 100%;
display: block;
background: uiResource("changelog_skins/achievements.noinline.png") center center / cover no-repeat !important;
@include S(height, 80px);
@include S(border-radius, 5px);
@include S(margin-bottom, 5px);
}
}

View File

@ -19,7 +19,6 @@
color: #333438;
&.removable {
cursor: pointer;
pointer-events: all;
}
@ -86,6 +85,28 @@
}
}
> .unpinButton {
@include S(width, 8px);
@include S(height, 8px);
position: absolute;
opacity: 0.7;
@include S(top, 3px);
@include S(left, -7px);
@include DarkThemeInvert;
@include IncreasedClickArea(2px);
transition: opacity 0.12s ease-in-out;
z-index: 100;
&:hover {
opacity: 0.8;
}
& {
/* @load-async */
background: uiResource("icons/unpin_shape.png") center center / 80% no-repeat;
}
}
&.goal,
&.blueprint {
.amountLabel::after {

View File

@ -165,5 +165,15 @@
color: #e72d2d;
}
}
&.achievements {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_achievements.png");
}
> strong {
color: #ffac0f;
}
}
}
}

View File

@ -4,9 +4,7 @@
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
pointer-events: all;
& {
@ -33,7 +31,6 @@
display: flex;
align-items: center;
flex-direction: column;
max-height: 100vh;
color: #fff;
text-align: center;

View File

@ -61,6 +61,12 @@
/* @load-async */
background: uiResource("icons/waypoint.png") left 50% / #{D(8px)} no-repeat;
}
&.layer--wires {
/* @load-async */
background-image: uiResource("icons/waypoint_wires.png");
}
opacity: 0.7;
@include S(margin-bottom, 1px);
font-weight: bold;

View File

@ -19,6 +19,7 @@
@import "application_error";
@import "textual_game_state";
@import "adinplay";
@import "changelog_skins";
@import "states/preload";
@import "states/main_menu";
@ -119,11 +120,3 @@ body.uiHidden {
display: none !important;
}
}
body.modalDialogActive,
body.externalAdOpen,
body.ingameDialogOpen {
> *:not(.ingameDialog):not(.modalDialogParent):not(.loadingDialog):not(.gameLoadingOverlay):not(#ingame_HUD_ModalDialogs):not(.noBlur) {
// filter: blur(5px) !important;
}
}

View File

@ -67,7 +67,7 @@ $icons: notification_saved, notification_success, notification_upgrade;
}
$languages: en, de, cs, da, et, es-419, fr, it, pt-BR, sv, tr, el, ru, uk, zh-TW, zh-CN, nb, mt-MT, ar, nl, vi,
th, hu, pl, ja, kor, no, pt-PT;
th, hu, pl, ja, kor, no, pt-PT, fi, ro;
@each $language in $languages {
[data-languageicon="#{$language}"] {

View File

@ -43,9 +43,10 @@
.languageChoose {
@include S(border-radius, 8px);
border: solid #222428;
background-color: #fff;
@include S(border-width, 2px);
background-size: cover;
background-color: #222428 !important;
background-size: contain !important;
background-position: center center !important;
opacity: 0.8;
}
}
@ -94,9 +95,11 @@
}
.standaloneBanner {
background: rgb(255, 234, 245);
@include S(border-radius, $globalBorderRadius);
background: rgb(255, 75, 84);
@include S(border-radius, $globalBorderRadius + 4);
box-sizing: border-box;
border: solid rgba(#fff, 0.15);
@include S(border-width, 4px);
@include S(padding, 15px);
display: flex;
@ -110,13 +113,14 @@
h3 {
@include Heading;
font-weight: bold;
@include S(margin-bottom, 5px);
@include S(margin-bottom, 20px);
text-transform: uppercase;
color: $colorRedBright;
color: #fff;
}
p {
@include Text;
color: #fff;
}
ul {
@ -138,16 +142,14 @@
display: block;
text-indent: -999em;
cursor: pointer;
@include S(margin-top, 20px);
@include S(margin-top, 30px);
pointer-events: all;
transition: all 0.12s ease-in;
transition-property: opacity, transform;
transform: skewX(-0.5deg);
@include S(border-radius, $globalBorderRadius);
&:hover {
transform: scale(1.02);
opacity: 0.9;
}
}
@ -181,9 +183,8 @@
.updateLabel {
position: absolute;
transform: translateX(50%) rotate(-5deg);
color: $colorRedBright;
color: #3291e9;
@include Heading;
text-transform: uppercase;
font-weight: bold;
@include S(right, 40px);
@include S(bottom, 20px);
@ -449,6 +450,10 @@
box-sizing: border-box;
@include S(grid-gap, 4px);
&.china {
grid-template-columns: auto 1fr;
}
.author {
flex-grow: 1;
text-align: right;

View File

@ -14,6 +14,7 @@
padding: 10px;
box-sizing: border-box;
background: #eef1f4;
@include S(border-radius, 3px);
@include DarkThemeOverride {
background: #424242;

View File

@ -12,6 +12,7 @@ import { getPlatformName, waitNextFrame } from "./core/utils";
import { Vector } from "./core/vector";
import { AdProviderInterface } from "./platform/ad_provider";
import { NoAdProvider } from "./platform/ad_providers/no_ad_provider";
import { NoAchievementProvider } from "./platform/browser/no_achievement_provider";
import { AnalyticsInterface } from "./platform/analytics";
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
import { SoundImplBrowser } from "./platform/browser/sound";
@ -32,6 +33,7 @@ import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
import { RestrictionManager } from "./core/restriction_manager";
/**
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
* @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface
* @typedef {import("./platform/sound").SoundInterface} SoundInterface
* @typedef {import("./platform/storage").StorageInterface} StorageInterface
@ -85,6 +87,9 @@ export class Application {
/** @type {PlatformWrapperInterface} */
this.platformWrapper = null;
/** @type {AchievementProviderInterface} */
this.achievementProvider = null;
/** @type {AdProviderInterface} */
this.adProvider = null;
@ -137,6 +142,7 @@ export class Application {
this.sound = new SoundImplBrowser(this);
this.analytics = new GoogleAnalyticsImpl(this);
this.gameAnalytics = new ShapezGameAnalytics(this);
this.achievementProvider = new NoAchievementProvider(this);
}
/**

View File

@ -1,4 +1,56 @@
export const CHANGELOG = [
{
version: "1.3.1",
date: "beta",
entries: [
"Fixed savegames getting corrupt in rare conditions",
"Fixed game crashing sometimes since the achievements update",
],
},
{
version: "1.3.0",
date: "12.03.2020",
skin: "achievements",
entries: [
"There are now <strong>45 Steam Achievements!</strong>",
"Fixed constant signals being editable from the regular layer",
"Fixed items still overlapping sometimes between buildings and belts",
"The game is now available in finnish, italian, romanian and ukrainian! (Thanks to all contributors!)",
"Updated translations (Thanks to all contributors!)",
],
},
{
version: "1.2.2",
date: "07.12.2020",
entries: [
"Fix item readers and some other buildings slowing up belts, especially if they stalled (inspired by Keterr's fix)",
"Added the ability to edit constant signals by left clicking them",
"Prevent items from being rendered on each other when a belt stalls (inspired by Keterr)",
"You can now add markers in the wire layer (partially by daanbreur)",
"Allow to cycle backwards in the toolbar with SHIFT + Tab (idea by EmeraldBlock)",
"Allow to cycle variants backwards with SHIFT + T",
"Upgrade numbers now use roman numerals until tier 50 (by LeopoldTal)",
"Add button to unpin shapes from the left side (by artemisSystem)",
"Fix middle mouse button also placing blueprints (by Eiim)",
"Hide wires grid when using the 'Disable Grid' setting (by EmeraldBlock)",
"Fix UI using multiple different save icons",
"Updated translations (Thanks to all contributors!)",
],
},
{
version: "1.2.1",
date: "31.10.2020",
entries: [
"Fixed stacking bug for level 26 which required restarting the game",
"Fix reward notification being too long sometimes (by LeopoldTal)",
"Use locale decimal separator on belt reader display (by LeopoldTal)",
"Vastly improved performance when saving games (by LeopoldTal)",
"Prevent some antivirus programs blocking the opening of external links (by LeopoldTal)",
"Match tutorials to the correct painter variants (by LeopoldTal)",
"Prevent throughput goals containing fractional numbers (by CEbbinghaus)",
"Updated translations and added Hungarian",
],
},
{
version: "1.2.0",
date: "09.10.2020",

View File

@ -13,7 +13,7 @@ import { cachebust } from "./cachebust";
const logger = createLogger("background_loader");
const essentialMainMenuSprites = [
"logo.png",
G_CHINA_VERSION ? "logo_cn.png" : "logo.png",
...G_ALL_UI_IMAGES.filter(src => src.startsWith("ui/") && src.indexOf(".gif") < 0),
];
const essentialMainMenuSounds = [

View File

@ -26,7 +26,8 @@ export const THIRDPARTY_URLS = {
},
};
export const A_B_TESTING_LINK_TYPE = Math.random() > 0.5 ? "steam_1_pr" : "steam_2_npr";
// export const A_B_TESTING_LINK_TYPE = Math.random() > 0.95 ? "steam_1_pr" : "steam_2_npr";
export const A_B_TESTING_LINK_TYPE = "steam_2_npr";
export const globalConfig = {
// Size of a single tile in Pixels.
@ -39,6 +40,9 @@ export const globalConfig = {
assetsSharpness: 1.5,
shapesSharpness: 1.4,
// Achievements
achievementSliceDuration: 10, // Seconds
// Production analytics
statisticsGraphDpi: 2.5,
statisticsGraphSlices: 100,

View File

@ -59,6 +59,9 @@ export default {
// Enables ads in the local build (normally they are deactivated there)
// testAds: true,
// -----------------------------------------------------------------------------------
// Allows unlocked achievements to be logged to console in the local build
// testAchievements: true,
// -----------------------------------------------------------------------------------
// Disables the automatic switch to an overview when zooming out
// disableMapOverview: true,
// -----------------------------------------------------------------------------------

View File

@ -558,7 +558,16 @@ export function formatSeconds(secs) {
}
/**
* Formats a number like 2.5 to "2.5 items / s"
* Formats a number like 2.51 to "2.5"
* @param {number} speed
* @param {string=} separator The decimal separator for numbers like 50.1 (separator='.')
*/
export function round1DigitLocalized(speed, separator = T.global.decimalSeparator) {
return round1Digit(speed).toString().replace(".", separator);
}
/**
* Formats a number like 2.51 to "2.51 items / s"
* @param {number} speed
* @param {boolean=} double
* @param {string=} separator The decimal separator for numbers like 50.1 (separator='.')
@ -752,29 +761,8 @@ export function startFileChoose(acceptedType = ".bin") {
});
}
const romanLiterals = [
"0", // NULL
"I",
"II",
"III",
"IV",
"V",
"VI",
"VII",
"VIII",
"IX",
"X",
"XI",
"XII",
"XIII",
"XIV",
"XV",
"XVI",
"XVII",
"XVIII",
"XIX",
"XX",
];
const MAX_ROMAN_NUMBER = 49;
const romanLiteralsCache = ["0"];
/**
*
@ -783,8 +771,52 @@ const romanLiterals = [
*/
export function getRomanNumber(number) {
number = Math.max(0, Math.round(number));
if (number < romanLiterals.length) {
return romanLiterals[number];
if (romanLiteralsCache[number]) {
return romanLiteralsCache[number];
}
if (number > MAX_ROMAN_NUMBER) {
return String(number);
}
function formatDigit(digit, unit, quintuple, decuple) {
switch (digit) {
case 0:
return "";
case 1: // I
return unit;
case 2: // II
return unit + unit;
case 3: // III
return unit + unit + unit;
case 4: // IV
return unit + quintuple;
case 9: // IX
return unit + decuple;
default:
// V, VI, VII, VIII
return quintuple + formatDigit(digit - 5, unit, quintuple, decuple);
}
}
let thousands = Math.floor(number / 1000);
let thousandsPart = "";
while (thousands > 0) {
thousandsPart += "M";
thousands -= 1;
}
const hundreds = Math.floor((number % 1000) / 100);
const hundredsPart = formatDigit(hundreds, "C", "D", "M");
const tens = Math.floor((number % 100) / 10);
const tensPart = formatDigit(tens, "X", "L", "C");
const units = number % 10;
const unitsPart = formatDigit(units, "I", "V", "X");
const formatted = thousandsPart + hundredsPart + tensPart + unitsPart;
romanLiteralsCache[number] = formatted;
return formatted;
}

View File

@ -0,0 +1,148 @@
/* typehints:start */
import { Entity } from "./entity";
import { GameRoot } from "./root";
/* typehints:end */
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { ACHIEVEMENTS } from "../platform/achievement_provider";
import { getBuildingDataFromCode } from "./building_codes";
const logger = createLogger("achievement_proxy");
const ROTATER = "rotater";
const DEFAULT = "default";
export class AchievementProxy {
/** @param {GameRoot} root */
constructor(root) {
this.root = root;
this.provider = this.root.app.achievementProvider;
this.disabled = true;
if (G_IS_DEV && globalConfig.debug.testAchievements) {
// still enable the proxy
} else if (!this.provider.hasAchievements()) {
return;
}
this.sliceTime = 0;
this.root.signals.postLoadHook.add(this.onLoad, this);
}
onLoad() {
this.provider
.onLoad(this.root)
.then(() => {
this.disabled = false;
logger.log("Recieving achievement signals");
this.initialize();
})
.catch(err => {
this.disabled = true;
logger.error("Ignoring achievement signals", err);
});
}
initialize() {
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.darkMode, null);
if (this.has(ACHIEVEMENTS.mam)) {
this.root.signals.entityAdded.add(this.onMamFailure, this);
this.root.signals.entityDestroyed.add(this.onMamFailure, this);
this.root.signals.storyGoalCompleted.add(this.onStoryGoalCompleted, this);
}
if (this.has(ACHIEVEMENTS.noInverseRotater)) {
this.root.signals.entityAdded.add(this.onEntityAdded, this);
}
this.startSlice();
}
startSlice() {
this.sliceTime = this.root.time.now();
this.root.signals.bulkAchievementCheck.dispatch(
ACHIEVEMENTS.storeShape,
this.sliceTime,
ACHIEVEMENTS.throughputBp25,
this.sliceTime,
ACHIEVEMENTS.throughputBp50,
this.sliceTime,
ACHIEVEMENTS.throughputLogo25,
this.sliceTime,
ACHIEVEMENTS.throughputLogo50,
this.sliceTime,
ACHIEVEMENTS.throughputRocket10,
this.sliceTime,
ACHIEVEMENTS.throughputRocket20,
this.sliceTime,
ACHIEVEMENTS.play1h,
this.sliceTime,
ACHIEVEMENTS.play10h,
this.sliceTime,
ACHIEVEMENTS.play20h,
this.sliceTime
);
}
update() {
if (this.disabled) {
return;
}
if (this.root.time.now() - this.sliceTime > globalConfig.achievementSliceDuration) {
this.startSlice();
}
}
/**
* @param {string} key
* @returns {boolean}
*/
has(key) {
if (!this.provider.collection) {
return false;
}
return this.provider.collection.map.has(key);
}
/** @param {Entity} entity */
onEntityAdded(entity) {
if (!entity.components.StaticMapEntity) {
return;
}
const building = getBuildingDataFromCode(entity.components.StaticMapEntity.code);
if (building.metaInstance.id !== ROTATER) {
return;
}
if (building.variant === DEFAULT) {
return;
}
this.root.savegame.currentData.stats.usedInverseRotater = true;
this.root.signals.entityAdded.remove(this.onEntityAdded);
}
/** @param {number} level */
onStoryGoalCompleted(level) {
if (level > 26) {
this.root.signals.entityAdded.add(this.onMamFailure, this);
this.root.signals.entityDestroyed.add(this.onMamFailure, this);
}
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.mam, null);
// reset on every level
this.root.savegame.currentData.stats.failedMam = false;
}
onMamFailure() {
this.root.savegame.currentData.stats.failedMam = true;
}
}

View File

@ -1086,6 +1086,9 @@ export class BeltPath extends BasicSerializableObject {
// Reduce the spacing
nextDistanceAndItem[_nextDistance] -= clampedProgress;
// Advance all items behind by the progress we made
this.spacingToFirstItem += clampedProgress;
// If the last item can be ejected, eject it and reduce the spacing, because otherwise
// we lose velocity
if (isFirstItemProcessed && nextDistanceAndItem[_nextDistance] < 1e-7) {
@ -1098,6 +1101,24 @@ export class BeltPath extends BasicSerializableObject {
if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) {
this.items.pop();
const itemBehind = this.items[lastItemProcessed - 1];
if (itemBehind && this.numCompressedItemsAfterFirstItem > 0) {
// So, with the next tick we will skip this item, but it actually has the potential
// to process farther -> If we don't advance here, we loose a tiny bit of progress
// every tick which causes the belt to be slower than it actually is.
// Also see #999
const fixupProgress = Math.max(
0,
Math.min(remainingVelocity, itemBehind[_nextDistance])
);
// See above
itemBehind[_nextDistance] -= fixupProgress;
remainingVelocity -= fixupProgress;
this.spacingToFirstItem += fixupProgress;
}
// Reduce the number of compressed items since the first item no longer exists
this.numCompressedItemsAfterFirstItem = Math.max(
0,
this.numCompressedItemsAfterFirstItem - 1
@ -1111,7 +1132,6 @@ export class BeltPath extends BasicSerializableObject {
}
isFirstItemProcessed = false;
this.spacingToFirstItem += clampedProgress;
if (remainingVelocity < 1e-7) {
break;
}

View File

@ -3,6 +3,7 @@ import { DrawParameters } from "../core/draw_parameters";
import { findNiceIntegerValue } from "../core/utils";
import { Vector } from "../core/vector";
import { Entity } from "./entity";
import { ACHIEVEMENTS } from "../platform/achievement_provider";
import { GameRoot } from "./root";
export class Blueprint {
@ -179,7 +180,7 @@ export class Blueprint {
*/
tryPlace(root, tile) {
return root.logic.performBulkOperation(() => {
let anyPlaced = false;
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
@ -191,9 +192,17 @@ export class Blueprint {
root.logic.freeEntityAreaBeforeBuild(clone);
root.map.placeStaticEntity(clone);
root.entityMgr.registerEntity(clone);
anyPlaced = true;
count++;
}
return anyPlaced;
root.signals.bulkAchievementCheck.dispatch(
ACHIEVEMENTS.placeBlueprint,
count,
ACHIEVEMENTS.placeBp1000,
count
);
return count !== 0;
});
}
}

View File

@ -1,5 +1,6 @@
import { generateMatrixRotations } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
@ -37,6 +38,25 @@ export class MetaTrashBuilding extends MetaBuilding {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash);
}
addAchievementReceiver(entity) {
if (!entity.root) {
return;
}
const itemProcessor = entity.components.ItemProcessor;
const tryTakeItem = itemProcessor.tryTakeItem.bind(itemProcessor);
itemProcessor.tryTakeItem = () => {
const taken = tryTakeItem(...arguments);
if (taken) {
entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1);
}
return taken;
};
}
/**
* Creates the entity at the given location
* @param {Entity} entity
@ -57,11 +77,14 @@ export class MetaTrashBuilding extends MetaBuilding {
],
})
);
entity.addComponent(
new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.trash,
})
);
this.addAchievementReceiver(entity);
}
}

View File

@ -35,6 +35,7 @@ import { RegularGameMode } from "./modes/regular";
import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager";
import { AchievementProxy } from "./achievement_proxy";
import { SoundProxy } from "./sound_proxy";
import { GameTime } from "./time/game_time";
@ -111,6 +112,7 @@ export class GameCore {
root.logic = new GameLogic(root);
root.hud = new GameHUD(root);
root.time = new GameTime(root);
root.achievementProxy = new AchievementProxy(root);
root.automaticSave = new AutomaticSave(root);
root.soundProxy = new SoundProxy(root);
@ -149,6 +151,9 @@ export class GameCore {
// Update analytics
root.productionAnalytics.update();
// Check achievements
root.achievementProxy.update();
}
});
}
@ -274,6 +279,9 @@ export class GameCore {
// Update analytics
root.productionAnalytics.update();
// Check achievements
root.achievementProxy.update();
}
// Update automatic save after everything finished

View File

@ -169,7 +169,7 @@ export class HubGoals extends BasicSerializableObject {
getCurrentGoalDelivered() {
if (this.currentGoal.throughputOnly) {
return (
this.root.productionAnalytics.getCurrentShapeRate(
this.root.productionAnalytics.getCurrentShapeRateRaw(
enumAnalyticsDataSource.delivered,
this.currentGoal.definition
) / globalConfig.analyticsSliceDurationSeconds
@ -238,7 +238,8 @@ export class HubGoals extends BasicSerializableObject {
return;
}
const required = Math.min(200, 4 + (this.level - 27) * 0.25);
//Floor Required amount to remove confusion
const required = Math.min(200, Math.floor(4 + (this.level - 27) * 0.25));
this.currentGoal = {
definition: this.computeFreeplayShape(this.level),
required,

View File

@ -48,6 +48,7 @@ import { HUDBetaOverlay } from "./parts/beta_overlay";
import { HUDStandaloneAdvantages } from "./parts/standalone_advantages";
import { HUDCatMemes } from "./parts/cat_memes";
import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer";
import { HUDConstantSignalEdit } from "./parts/constant_signal_edit";
export class GameHUD {
/**
@ -86,6 +87,7 @@ export class GameHUD {
waypoints: new HUDWaypoints(this.root),
wireInfo: new HUDWireInfo(this.root),
leverToggle: new HUDLeverToggle(this.root),
constantSignalEdit: new HUDConstantSignalEdit(this.root),
// Must always exist
pinnedShapes: new HUDPinnedShapes(this.root),

View File

@ -1,6 +1,6 @@
import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { Signal, STOP_PROPAGATION } from "../../../core/signal";
import { makeDiv } from "../../../core/utils";
import { STOP_PROPAGATION } from "../../../core/signal";
import { makeDiv, safeModulo } from "../../../core/utils";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { MetaBuilding } from "../../meta_building";
import { GameRoot } from "../../root";
@ -161,8 +161,12 @@ export class HUDBaseToolbar extends BaseHUDPart {
let newBuildingFound = false;
let newIndex = this.lastSelectedIndex;
for (let i = 0; i < this.primaryBuildings.length; ++i, ++newIndex) {
newIndex %= this.primaryBuildings.length;
const direction = this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed
? -1
: 1;
for (let i = 0; i <= this.primaryBuildings.length; ++i) {
newIndex = safeModulo(newIndex + direction, this.primaryBuildings.length);
const metaBuilding = gMetaBuildingRegistry.findByClass(this.primaryBuildings[newIndex]);
const handle = this.buildingHandles[metaBuilding.id];
if (!handle.selected && handle.unlocked) {

View File

@ -110,8 +110,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
this.abortPlacement();
return STOP_PROPAGATION;
}
}
} else if (button === enumMouseButton.left) {
const blueprint = this.currentBlueprint.get();
if (!blueprint) {
return;
@ -129,6 +128,8 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
this.root.hubGoals.takeShapeByKey(this.root.gameMode.getBlueprintShapeKey(), cost);
this.root.soundProxy.playUi(SOUNDS.placeBuilding);
}
return STOP_PROPAGATION;
}
}
/**

View File

@ -216,8 +216,8 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
const dimensions = metaBuilding.getDimensions(variant);
const sprite = metaBuilding.getPreviewSprite(0, variant);
const spriteWrapper = makeDiv(element, null, ["iconWrap"]);
spriteWrapper.setAttribute("data-tile-w", dimensions.x);
spriteWrapper.setAttribute("data-tile-h", dimensions.y);
spriteWrapper.setAttribute("data-tile-w", String(dimensions.x));
spriteWrapper.setAttribute("data-tile-h", String(dimensions.y));
spriteWrapper.innerHTML = sprite.getAsHTML(iconSize * dimensions.x, iconSize * dimensions.y);

View File

@ -14,6 +14,7 @@ import { MetaMinerBuilding, enumMinerVariants } from "../../buildings/miner";
import { enumHubGoalRewards } from "../../tutorial_goals";
import { getBuildingDataFromCode, getCodeFromBuildingData } from "../../building_codes";
import { MetaHubBuilding } from "../../buildings/hub";
import { safeModulo } from "../../../core/utils";
/**
* Contains all logic for the building placer - this doesn't include the rendering
@ -109,6 +110,12 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
// KEYBINDINGS
const keyActionMapper = this.root.keyMapper;
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateToUp).add(this.trySetRotate, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateToDown).add(this.trySetRotate, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateToRight).add(this.trySetRotate, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateToLeft).add(this.trySetRotate, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this);
keyActionMapper
.getBinding(KEYMAPPINGS.placement.switchDirectionLockSide)
@ -289,6 +296,28 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
staticComp.rotation = this.currentBaseRotation;
}
}
/**
* Rotates the current building to the specified direction.
*/
trySetRotate() {
const selectedBuilding = this.currentMetaBuilding.get();
if (selectedBuilding) {
if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateToUp).pressed) {
this.currentBaseRotation = 0;
} else if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateToDown).pressed) {
this.currentBaseRotation = 180;
} else if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateToRight).pressed) {
this.currentBaseRotation = 90;
} else if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateToLeft).pressed) {
this.currentBaseRotation = 270;
}
const staticComp = this.fakeEntity.components.StaticMapEntity;
staticComp.rotation = this.currentBaseRotation;
}
}
/**
* Tries to delete the building under the mouse
*/
@ -467,7 +496,12 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
index = 0;
console.warn("Invalid variant selected:", this.currentVariant.get());
}
const newIndex = (index + 1) % availableVariants.length;
const direction = this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier)
.pressed
? -1
: 1;
const newIndex = safeModulo(index + direction, availableVariants.length);
const newVariant = availableVariants[newIndex];
this.setVariant(newVariant);
}

View File

@ -0,0 +1,34 @@
import { STOP_PROPAGATION } from "../../../core/signal";
import { Vector } from "../../../core/vector";
import { enumMouseButton } from "../../camera";
import { BaseHUDPart } from "../base_hud_part";
export class HUDConstantSignalEdit extends BaseHUDPart {
initialize() {
this.root.camera.downPreHandler.add(this.downPreHandler, this);
}
/**
* @param {Vector} pos
* @param {enumMouseButton} button
*/
downPreHandler(pos, button) {
if (this.root.currentLayer !== "wires") {
return;
}
const tile = this.root.camera.screenToWorld(pos).toTileSpace();
const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "wires");
if (contents) {
const constantComp = contents.components.ConstantSignal;
if (constantComp) {
if (button === enumMouseButton.left) {
this.root.systemMgr.systems.constantSignal.editConstantSignal(contents, {
deleteOnCancel: false,
});
return STOP_PROPAGATION;
}
}
}
}
}

View File

@ -4,9 +4,10 @@ import { createLogger } from "../../../core/logging";
import { STOP_PROPAGATION } from "../../../core/signal";
import { formatBigNumberFull } from "../../../core/utils";
import { Vector } from "../../../core/vector";
import { ACHIEVEMENTS } from "../../../platform/achievement_provider";
import { enumMouseButton } from "../../camera";
import { T } from "../../../translations";
import { Blueprint } from "../../blueprint";
import { enumMouseButton } from "../../camera";
import { Entity } from "../../entity";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { THEME } from "../../theme";
@ -101,13 +102,18 @@ export class HUDMassSelector extends BaseHUDPart {
* @type {Map<number, Entity>}
*/
let count = 0;
this.root.logic.performBulkOperation(() => {
const arr = [...this.selectedEntities.values()];
for (let i = arr.length - 1; i >= 0; --i) {
if (!this.root.logic.tryDeleteBuilding(arr[i])) {
logger.error("Error in mass delete, could not remove building");
} else {
count++;
}
}
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.destroy1000, count);
});
this.clear();

View File

@ -217,11 +217,14 @@ export class HUDPinnedShapes extends BaseHUDPart {
let detector = null;
if (canUnpin) {
const unpinButton = document.createElement("button");
unpinButton.classList.add("unpinButton");
element.appendChild(unpinButton);
element.classList.add("removable");
detector = new ClickDetector(element, {
detector = new ClickDetector(unpinButton, {
consumeEvents: true,
preventDefault: true,
targetOnly: false,
targetOnly: true,
});
detector.click.add(() => this.unpinShape(key));
} else {
@ -270,7 +273,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
if (handle.throughputOnly) {
currentValue =
this.root.productionAnalytics.getCurrentShapeRate(
this.root.productionAnalytics.getCurrentShapeRateRaw(
enumAnalyticsDataSource.delivered,
handle.definition
) / globalConfig.analyticsSliceDurationSeconds;

View File

@ -5,7 +5,7 @@ import { T } from "../../../translations";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
const showIntervalSeconds = 30 * 60;
const showIntervalSeconds = 9 * 60;
export class HUDStandaloneAdvantages extends BaseHUDPart {
createElements(parent) {
@ -60,7 +60,7 @@ export class HUDStandaloneAdvantages extends BaseHUDPart {
this.inputReciever = new InputReceiver("standalone-advantages");
this.close();
this.lastShown = this.root.gameIsFresh ? this.root.time.now() : 0;
this.lastShown = -1e10;
}
show() {

View File

@ -209,7 +209,9 @@ export class HUDStatistics extends BaseHUDPart {
}
case enumAnalyticsDataSource.produced:
case enumAnalyticsDataSource.delivered: {
entries = Object.entries(this.root.productionAnalytics.getCurrentShapeRates(this.dataSource));
entries = Object.entries(
this.root.productionAnalytics.getCurrentShapeRatesRaw(this.dataSource)
);
break;
}
}

View File

@ -100,7 +100,7 @@ export class HUDShapeStatisticsHandle {
case enumAnalyticsDataSource.delivered:
case enumAnalyticsDataSource.produced: {
let rate =
this.root.productionAnalytics.getCurrentShapeRate(dataSource, this.definition) /
this.root.productionAnalytics.getCurrentShapeRateRaw(dataSource, this.definition) /
globalConfig.analyticsSliceDurationSeconds;
this.counter.innerText = T.ingame.statistics.shapesDisplayUnits[unit].replace(

View File

@ -1,6 +1,7 @@
import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig, THIRDPARTY_URLS } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters";
import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { Loader } from "../../../core/loader";
import { DialogWithForm } from "../../../core/modal_dialog_elements";
import { FormElementInput } from "../../../core/modal_dialog_forms";
@ -14,8 +15,10 @@ import {
removeAllChildren,
} from "../../../core/utils";
import { Vector } from "../../../core/vector";
import { ACHIEVEMENTS } from "../../../platform/achievement_provider";
import { T } from "../../../translations";
import { BaseItem } from "../../base_item";
import { MetaHubBuilding } from "../../buildings/hub";
import { enumMouseButton } from "../../camera";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { ShapeDefinition } from "../../shape_definition";
@ -26,7 +29,8 @@ import { enumNotificationType } from "./notifications";
/** @typedef {{
* label: string | null,
* center: { x: number, y: number },
* zoomLevel: number
* zoomLevel: number,
* layer: Layer,
* }} Waypoint */
/**
@ -88,7 +92,12 @@ export class HUDWaypoints extends BaseHUDPart {
*/
initialize() {
// Cache the sprite for the waypoints
this.waypointSprite = Loader.getSprite("sprites/misc/waypoint.png");
this.waypointSprites = {
regular: Loader.getSprite("sprites/misc/waypoint.png"),
wires: Loader.getSprite("sprites/misc/waypoint_wires.png"),
};
this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png");
/** @type {Array<Waypoint>}
@ -98,6 +107,7 @@ export class HUDWaypoints extends BaseHUDPart {
label: null,
center: { x: 0, y: 0 },
zoomLevel: 3,
layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(),
},
];
@ -187,7 +197,10 @@ export class HUDWaypoints extends BaseHUDPart {
const waypoint = this.waypoints[i];
const label = this.getWaypointLabel(waypoint);
const element = makeDiv(this.waypointsListElement, null, ["waypoint"]);
const element = makeDiv(this.waypointsListElement, null, [
"waypoint",
"layer--" + waypoint.layer,
]);
if (ShapeDefinition.isValidShortKey(label)) {
const canvas = this.getWaypointCanvas(waypoint);
@ -228,6 +241,7 @@ export class HUDWaypoints extends BaseHUDPart {
* @param {Waypoint} waypoint
*/
moveToWaypoint(waypoint) {
this.root.currentLayer = waypoint.layer;
this.root.camera.setDesiredCenter(new Vector(waypoint.center.x, waypoint.center.y));
this.root.camera.setDesiredZoom(waypoint.zoomLevel);
}
@ -326,6 +340,7 @@ export class HUDWaypoints extends BaseHUDPart {
label,
center: { x: position.x, y: position.y },
zoomLevel: this.root.camera.zoomLevel,
layer: this.root.currentLayer,
});
this.sortWaypoints();
@ -335,6 +350,10 @@ export class HUDWaypoints extends BaseHUDPart {
T.ingame.waypoints.creationSuccessNotification,
enumNotificationType.success
);
this.root.signals.achievementCheck.dispatch(
ACHIEVEMENTS.mapMarkers15,
this.waypoints.length - 1 // Disregard HUB
);
// Re-render the list and thus add it
this.rerenderWaypointList();
@ -537,7 +556,7 @@ export class HUDWaypoints extends BaseHUDPart {
const iconOpacity = 1 - this.currentCompassOpacity;
if (iconOpacity > 0.01) {
context.globalAlpha = iconOpacity;
this.waypointSprite.drawCentered(context, dims / 2, dims / 2, dims * 0.7);
this.waypointSprites.regular.drawCentered(context, dims / 2, dims / 2, dims * 0.7);
context.globalAlpha = 1;
}
}
@ -616,11 +635,11 @@ export class HUDWaypoints extends BaseHUDPart {
}
// Render the small icon on the left
this.waypointSprite.drawCentered(
this.waypointSprites[waypoint.layer].drawCentered(
parameters.context,
bounds.x + contentPaddingX,
bounds.y + bounds.h / 2,
bounds.h * 0.7
bounds.h * 0.6
);
}

View File

@ -118,7 +118,8 @@ export class HUDWiresOverlay extends BaseHUDPart {
return;
}
if (!this.cachedPatternBackground) {
const hasTileGrid = !this.root.app.settings.getAllSettings().disableTileGrid;
if (hasTileGrid && !this.cachedPatternBackground) {
this.cachedPatternBackground = parameters.context.createPattern(this.tilePatternCanvas, "repeat");
}
@ -133,7 +134,9 @@ export class HUDWiresOverlay extends BaseHUDPart {
parameters.context.globalCompositeOperation = "source-over";
parameters.context.scale(scaleFactor, scaleFactor);
parameters.context.fillStyle = this.cachedPatternBackground;
parameters.context.fillStyle = hasTileGrid
? this.cachedPatternBackground
: "rgba(78, 137, 125, 0.75)";
parameters.context.fillRect(
bounds.x / scaleFactor,
bounds.y / scaleFactor,

View File

@ -11,6 +11,11 @@ function key(str) {
return str.toUpperCase().charCodeAt(0);
}
const KEYCODE_UP_ARROW = 38;
const KEYCODE_DOWN_ARROW = 40;
const KEYCODE_LEFT_ARROW = 37;
const KEYCODE_RIGHT_ARROW = 39;
export const KEYMAPPINGS = {
general: {
confirm: { keyCode: 13 }, // enter
@ -81,6 +86,10 @@ export const KEYMAPPINGS = {
pipette: { keyCode: key("Q") },
rotateWhilePlacing: { keyCode: key("R") },
rotateInverseModifier: { keyCode: 16 }, // SHIFT
rotateToUp: { keyCode: KEYCODE_UP_ARROW },
rotateToDown: { keyCode: KEYCODE_DOWN_ARROW },
rotateToRight: { keyCode: KEYCODE_RIGHT_ARROW },
rotateToLeft: { keyCode: KEYCODE_LEFT_ARROW },
cycleBuildingVariants: { keyCode: key("T") },
cycleBuildings: { keyCode: 9 }, // TAB
switchDirectionLockSide: { keyCode: key("R") },
@ -162,13 +171,13 @@ export function getStringForKeyCode(code) {
return "END";
case 36:
return "HOME";
case 37:
case KEYCODE_LEFT_ARROW:
return "⬅";
case 38:
case KEYCODE_UP_ARROW:
return "⬆";
case 39:
case KEYCODE_RIGHT_ARROW:
return "➡";
case 40:
case KEYCODE_DOWN_ARROW:
return "⬇";
case 44:
return "PRNT";

View File

@ -83,7 +83,7 @@ export class ProductionAnalytics extends BasicSerializableObject {
* @param {enumAnalyticsDataSource} dataSource
* @param {ShapeDefinition} definition
*/
getCurrentShapeRate(dataSource, definition) {
getCurrentShapeRateRaw(dataSource, definition) {
const slices = this.history[dataSource];
return slices[slices.length - 2][definition.getHash()] || 0;
}
@ -108,7 +108,7 @@ export class ProductionAnalytics extends BasicSerializableObject {
* Returns the rates of all shapes
* @param {enumAnalyticsDataSource} dataSource
*/
getCurrentShapeRates(dataSource) {
getCurrentShapeRatesRaw(dataSource) {
const slices = this.history[dataSource];
// First, copy current slice

View File

@ -8,6 +8,7 @@ import { createLogger } from "../core/logging";
import { GameTime } from "./time/game_time";
import { EntityManager } from "./entity_manager";
import { GameSystemManager } from "./game_system_manager";
import { AchievementProxy } from "./achievement_proxy";
import { GameHUD } from "./hud/hud";
import { MapView } from "./map_view";
import { Camera } from "./camera";
@ -119,6 +120,9 @@ export class GameRoot {
/** @type {SoundProxy} */
this.soundProxy = null;
/** @type {AchievementProxy} */
this.achievementProxy = null;
/** @type {ShapeDefinitionManager} */
this.shapeDefinitionMgr = null;
@ -175,6 +179,10 @@ export class GameRoot {
// Called before actually placing an entity, use to perform additional logic
// for freeing space before actually placing.
freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
// Called with an achievement key and necessary args to validate it can be unlocked.
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()),
};
// RNG's

View File

@ -4,6 +4,7 @@ import { enumColors } from "./colors";
import { ShapeItem } from "./items/shape_item";
import { GameRoot } from "./root";
import { enumSubShape, ShapeDefinition } from "./shape_definition";
import { ACHIEVEMENTS } from "../platform/achievement_provider";
const logger = createLogger("shape_definition_manager");
@ -31,7 +32,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
*/
this.shapeKeyToItem = {};
// Caches operations in the form of 'operation:def1[:def2]'
// Caches operations in the form of 'operation/def1[/def2]'
/** @type {Object.<string, Array<ShapeDefinition>|ShapeDefinition>} */
this.operationCache = {};
}
@ -89,13 +90,15 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {[ShapeDefinition, ShapeDefinition]}
*/
shapeActionCutHalf(definition) {
const key = "cut:" + definition.getHash();
const key = "cut/" + definition.getHash();
if (this.operationCache[key]) {
return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key]);
}
const rightSide = definition.cloneFilteredByQuadrants([2, 3]);
const leftSide = definition.cloneFilteredByQuadrants([0, 1]);
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.cutShape, null);
return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key] = [
this.registerOrReturnHandle(rightSide),
this.registerOrReturnHandle(leftSide),
@ -108,7 +111,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]}
*/
shapeActionCutQuad(definition) {
const key = "cut-quad:" + definition.getHash();
const key = "cut-quad/" + definition.getHash();
if (this.operationCache[key]) {
return /** @type {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} */ (this
.operationCache[key]);
@ -130,13 +133,15 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
shapeActionRotateCW(definition) {
const key = "rotate-cw:" + definition.getHash();
const key = "rotate-cw/" + definition.getHash();
if (this.operationCache[key]) {
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
}
const rotated = definition.cloneRotateCW();
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.rotateShape, null);
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
rotated
));
@ -148,7 +153,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
shapeActionRotateCCW(definition) {
const key = "rotate-ccw:" + definition.getHash();
const key = "rotate-ccw/" + definition.getHash();
if (this.operationCache[key]) {
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
}
@ -166,7 +171,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
shapeActionRotate180(definition) {
const key = "rotate-fl:" + definition.getHash();
const key = "rotate-fl/" + definition.getHash();
if (this.operationCache[key]) {
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
}
@ -185,10 +190,13 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
shapeActionStack(lowerDefinition, upperDefinition) {
const key = "stack:" + lowerDefinition.getHash() + ":" + upperDefinition.getHash();
const key = "stack/" + lowerDefinition.getHash() + "/" + upperDefinition.getHash();
if (this.operationCache[key]) {
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
}
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.stackShape, null);
const stacked = lowerDefinition.cloneAndStackWith(upperDefinition);
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
stacked
@ -202,10 +210,13 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
shapeActionPaintWith(definition, color) {
const key = "paint:" + definition.getHash() + ":" + color;
const key = "paint/" + definition.getHash() + "/" + color;
if (this.operationCache[key]) {
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
}
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.paintShape, null);
const colorized = definition.cloneAndPaintWith(color);
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
colorized
@ -219,7 +230,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
* @returns {ShapeDefinition}
*/
shapeActionPaintWith4Colors(definition, colors) {
const key = "paint4:" + definition.getHash() + ":" + colors.join(",");
const key = "paint4/" + definition.getHash() + "/" + colors.join(",");
if (this.operationCache[key]) {
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
}

View File

@ -48,7 +48,7 @@ export class BeltReaderSystem extends GameSystemWithFilter {
throughput = 1 / (averageSpacing / averageSpacingNum);
}
readerComp.lastThroughput = Math.min(30, throughput);
readerComp.lastThroughput = Math.min(globalConfig.beltSpeedItemsPerSecond * 23.9, throughput);
}
}
}

View File

@ -17,7 +17,9 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ConstantSignalComponent]);
this.root.signals.entityManuallyPlaced.add(this.querySigalValue, this);
this.root.signals.entityManuallyPlaced.add(entity =>
this.editConstantSignal(entity, { deleteOnCancel: true })
);
}
update() {
@ -33,8 +35,10 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
/**
* Asks the entity to enter a valid signal code
* @param {Entity} entity
* @param {object} param0
* @param {boolean=} param0.deleteOnCancel
*/
querySigalValue(entity) {
editConstantSignal(entity, { deleteOnCancel = true }) {
if (!entity.components.ConstantSignal) {
return;
}
@ -110,6 +114,7 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
dialog.valueChosen.add(closeHandler);
// When cancelled, destroy the entity again
if (deleteOnCancel) {
dialog.buttonSignals.cancel.add(() => {
if (!this.root || !this.root.entityMgr) {
// Game got stopped
@ -131,6 +136,7 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
this.root.logic.tryDeleteBuilding(entityRef);
});
}
}
/**
* Tries to parse a signal code

View File

@ -5,6 +5,7 @@ import { Rectangle } from "../../core/rectangle";
import { StaleAreaDetector } from "../../core/stale_area_detector";
import { dirInterval } from "../../core/utils";
import { enumDirection, enumDirectionToVector } from "../../core/vector";
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { BaseItem } from "../base_item";
import { BeltComponent } from "../components/belt";
import { ItemAcceptorComponent } from "../components/item_acceptor";
@ -329,6 +330,64 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
continue;
}
// Limit the progress to the maximum available space on the next belt (also see #1000)
let progress = slot.progress;
const nextBeltPath = slot.cachedBeltPath;
if (nextBeltPath) {
/*
If you imagine the track between the center of the building and the center of the first belt as
a range from 0 to 1:
Building Belt
| X | X |
| 0...................1 |
And for example the first item on belt has a distance of 0.4 to the beginning of the belt:
Building Belt
| X | X |
| 0...................1 |
^ item
Then the space towards this first item is always 0.5 (the distance from the center of the building to the beginning of the belt)
PLUS the spacing to the item, so in this case 0.5 + 0.4 = 0.9:
Building Belt
| X | X |
| 0...................1 |
^ item @ 0.9
Since items must not get clashed, we need to substract some spacing (lets assume it is 0.6, exact value see globalConfig.itemSpacingOnBelts),
So we do 0.9 - globalConfig.itemSpacingOnBelts = 0.3
Building Belt
| X | X |
| 0...................1 |
^ ^ item @ 0.9
^ max progress = 0.3
Because now our range actually only goes to the end of the building, and not towards the center of the building, we need to multiply
all values by 2:
Building Belt
| X | X |
| 0.........1.........2 |
^ ^ item @ 1.8
^ max progress = 0.6
And that's it! If you summarize the calculations from above into a formula, you get the one below.
*/
const maxProgress =
(0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts) * 2;
progress = Math.min(maxProgress, progress);
}
// Skip if the item would barely be visible
if (progress < 0.05) {
continue;
}
const realPosition = staticComp.localTileToWorld(slot.pos);
if (!chunk.tileSpaceRectangle.containsPoint(realPosition.x, realPosition.y)) {
// Not within this chunk
@ -338,8 +397,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
const realDirection = staticComp.localDirectionToWorld(slot.direction);
const realDirectionVector = enumDirectionToVector[realDirection];
const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * slot.progress;
const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * slot.progress;
const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * progress;
const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * progress;
const worldX = tileX * globalConfig.tileSize;
const worldY = tileY * globalConfig.tileSize;

View File

@ -1,6 +1,6 @@
import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { smoothPulse } from "../../core/utils";
import { round1DigitLocalized, smoothPulse } from "../../core/utils";
import { enumItemProcessorRequirements, enumItemProcessorTypes } from "../components/item_processor";
import { Entity } from "../entity";
import { GameSystem } from "../game_system";
@ -92,7 +92,7 @@ export class ItemProcessorOverlaysSystem extends GameSystem {
parameters.context.textAlign = "center";
parameters.context.font = "bold 10px GameFont";
parameters.context.fillText(
"" + Math.round(readerComp.lastThroughput * 10) / 10,
round1DigitLocalized(readerComp.lastThroughput),
(staticComp.origin.x + 0.5) * globalConfig.tileSize,
(staticComp.origin.y + 0.62) * globalConfig.tileSize
);

View File

@ -13,6 +13,7 @@ import {
enumInvertedDirections,
Vector,
} from "../../core/vector";
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { BaseItem } from "../base_item";
import { arrayWireRotationVariantToType, MetaWireBuilding } from "../buildings/wire";
import { getCodeFromBuildingData } from "../building_codes";
@ -697,6 +698,8 @@ export class WireSystem extends GameSystemWithFilter {
return;
}
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.place5000Wires, entity);
// Invalidate affected area
const originalRect = staticComp.getTileSpaceBounds();
const affectedArea = originalRect.expandedInAllDirections(1);

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

@ -19,6 +19,8 @@ declare const G_BUILD_VERSION: string;
declare const G_ALL_UI_IMAGES: Array<string>;
declare const G_IS_RELEASE: boolean;
declare const G_CHINA_VERSION: boolean;
// Polyfills
declare interface String {
replaceAll(search: string, replacement: string): string;

View File

@ -8,113 +8,180 @@ export const LANGUAGES = {
code: "en",
region: "",
},
"de": {
name: "Deutsch",
data: require("./built-temp/base-de.json"),
code: "de",
region: "",
},
"fr": {
name: "Français",
data: require("./built-temp/base-fr.json"),
code: "fr",
region: "",
},
"ja": {
name: "日本語",
data: require("./built-temp/base-ja.json"),
code: "ja",
region: "",
},
"pt-PT": {
name: "Português (Portugal)",
data: require("./built-temp/base-pt-PT.json"),
code: "pt",
region: "PT",
},
"pt-BR": {
name: "Português (Brasil)",
data: require("./built-temp/base-pt-BR.json"),
code: "pt",
region: "BR",
},
"ru": {
name: "Русский",
data: require("./built-temp/base-ru.json"),
code: "ru",
region: "",
},
"cs": {
name: "Čeština",
data: require("./built-temp/base-cz.json"),
code: "cs",
region: "",
},
"es-419": {
name: "Español",
data: require("./built-temp/base-es.json"),
code: "es",
region: "",
},
"pl": {
name: "Polski",
data: require("./built-temp/base-pl.json"),
code: "pl",
region: "",
},
"kor": {
name: "한국어",
data: require("./built-temp/base-kor.json"),
code: "kor",
region: "",
},
"nl": {
name: "Nederlands",
data: require("./built-temp/base-nl.json"),
code: "nl",
region: "",
},
"no": {
name: "Norsk",
data: require("./built-temp/base-no.json"),
code: "no",
region: "",
},
"tr": {
name: "Türkçe",
data: require("./built-temp/base-tr.json"),
code: "tr",
region: "",
},
"zh-CN": {
// simplified
name: "中文简体",
// simplified chinese
name: "简体中文",
data: require("./built-temp/base-zh-CN.json"),
code: "zh",
region: "CN",
},
"zh-TW": {
// traditional
name: "中文繁體",
// traditional chinese
name: "繁體中文",
data: require("./built-temp/base-zh-TW.json"),
code: "zh",
region: "TW",
},
"ja": {
// japanese
name: "日本語",
data: require("./built-temp/base-ja.json"),
code: "ja",
region: "",
},
"kor": {
// korean
name: "한국어",
data: require("./built-temp/base-kor.json"),
code: "kor",
region: "",
},
"cs": {
// czech
name: "Čeština",
data: require("./built-temp/base-cz.json"),
code: "cs",
region: "",
},
"da": {
// danish
name: "Dansk",
data: require("./built-temp/base-da.json"),
code: "da",
region: "",
},
"de": {
// german
name: "Deutsch",
data: require("./built-temp/base-de.json"),
code: "de",
region: "",
},
"es-419": {
// spanish
name: "Español",
data: require("./built-temp/base-es.json"),
code: "es",
region: "",
},
"fr": {
// french
name: "Français",
data: require("./built-temp/base-fr.json"),
code: "fr",
region: "",
},
"it": {
// italian
name: "Italiano",
data: require("./built-temp/base-it.json"),
code: "it",
region: "",
},
"hu": {
// hungarian
name: "Magyar",
data: require("./built-temp/base-hu.json"),
code: "hu",
region: "",
},
"nl": {
// dutch
name: "Nederlands",
data: require("./built-temp/base-nl.json"),
code: "nl",
region: "",
},
"no": {
// norwegian
name: "Norsk",
data: require("./built-temp/base-no.json"),
code: "no",
region: "",
},
"pl": {
// polish
name: "Polski",
data: require("./built-temp/base-pl.json"),
code: "pl",
region: "",
},
"pt-PT": {
// portuguese
name: "Português",
data: require("./built-temp/base-pt-PT.json"),
code: "pt",
region: "PT",
},
"pt-BR": {
// portuguese - brazil
name: "Português - Brasil",
data: require("./built-temp/base-pt-BR.json"),
code: "pt",
region: "BR",
},
"ro": {
// romanian
name: "Română",
data: require("./built-temp/base-ro.json"),
code: "pt",
region: "BR",
},
"ru": {
// russian
name: "Русский",
data: require("./built-temp/base-ru.json"),
code: "ru",
region: "",
},
"fi": {
// finish
name: "Suomi",
data: require("./built-temp/base-fi.json"),
code: "fi",
region: "",
},
"sv": {
// swedish
name: "Svenska",
data: require("./built-temp/base-sv.json"),
code: "sv",
region: "",
},
"da": {
name: "Dansk",
data: require("./built-temp/base-da.json"),
code: "da",
"tr": {
// turkish
name: "Türkçe",
data: require("./built-temp/base-tr.json"),
code: "tr",
region: "",
},
"uk": {
// ukrainian
name: "Українська",
data: require("./built-temp/base-uk.json"),
code: "uk",
region: "",
},
};

View File

@ -0,0 +1,642 @@
/* typehints:start */
import { Application } from "../application";
import { Entity } from "../game/entity";
import { GameRoot } from "../game/root";
import { THEMES } from "../game/theme";
/* typehints:end */
import { enumAnalyticsDataSource } from "../game/production_analytics";
import { ShapeDefinition } from "../game/shape_definition";
import { ShapeItem } from "../game/items/shape_item";
import { globalConfig } from "../core/config";
export const ACHIEVEMENTS = {
belt500Tiles: "belt500Tiles",
blueprint100k: "blueprint100k",
blueprint1m: "blueprint1m",
completeLvl26: "completeLvl26",
cutShape: "cutShape",
darkMode: "darkMode",
destroy1000: "destroy1000",
irrelevantShape: "irrelevantShape",
level100: "level100",
level50: "level50",
logoBefore18: "logoBefore18",
mam: "mam",
mapMarkers15: "mapMarkers15",
noBeltUpgradesUntilBp: "noBeltUpgradesUntilBp",
noInverseRotater: "noInverseRotater",
oldLevel17: "oldLevel17",
openWires: "openWires",
paintShape: "paintShape",
place5000Wires: "place5000Wires",
placeBlueprint: "placeBlueprint",
placeBp1000: "placeBp1000",
play1h: "play1h",
play10h: "play10h",
play20h: "play20h",
produceLogo: "produceLogo",
produceMsLogo: "produceMsLogo",
produceRocket: "produceRocket",
rotateShape: "rotateShape",
speedrunBp30: "speedrunBp30",
speedrunBp60: "speedrunBp60",
speedrunBp120: "speedrunBp120",
stack4Layers: "stack4Layers",
stackShape: "stackShape",
store100Unique: "store100Unique",
storeShape: "storeShape",
throughputBp25: "throughputBp25",
throughputBp50: "throughputBp50",
throughputLogo25: "throughputLogo25",
throughputLogo50: "throughputLogo50",
throughputRocket10: "throughputRocket10",
throughputRocket20: "throughputRocket20",
trash1000: "trash1000",
unlockWires: "unlockWires",
upgradesTier5: "upgradesTier5",
upgradesTier8: "upgradesTier8",
};
/** @type {keyof typeof THEMES} */
const DARK_MODE = "dark";
const HOUR_1 = 3600; // Seconds
const HOUR_10 = HOUR_1 * 10;
const HOUR_20 = HOUR_1 * 20;
const ITEM_SHAPE = ShapeItem.getId();
const MINUTE_30 = 1800; // Seconds
const MINUTE_60 = MINUTE_30 * 2;
const MINUTE_120 = MINUTE_30 * 4;
const ROTATER_CCW_CODE = 12;
const ROTATER_180_CODE = 13;
const SHAPE_BP = "CbCbCbRb:CwCwCwCw";
const SHAPE_LOGO = "RuCw--Cw:----Ru--";
const SHAPE_MS_LOGO = "RgRyRbRr";
const SHAPE_OLD_LEVEL_17 = "WrRgWrRg:CwCrCwCr:SgSgSgSg";
const SHAPE_ROCKET = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
/** @type {Layer} */
const WIRE_LAYER = "wires";
export class AchievementProviderInterface {
/* typehints:start */
collection = /** @type {AchievementCollection|undefined} */ (null);
/* typehints:end */
/** @param {Application} app */
constructor(app) {
this.app = app;
}
/**
* Initializes the achievement provider.
* @returns {Promise<void>}
*/
initialize() {
abstract;
return Promise.reject();
}
/**
* Opportunity to do additional initialization work with the GameRoot.
* @param {GameRoot} root
* @returns {Promise<void>}
*/
onLoad(root) {
abstract;
return Promise.reject();
}
/** @returns {boolean} */
hasLoaded() {
abstract;
return false;
}
/**
* Call to activate an achievement with the provider
* @param {string} key - Maps to an Achievement
* @returns {Promise<void>}
*/
activate(key) {
abstract;
return Promise.reject();
}
/**
* Checks if achievements are supported in the current build
* @returns {boolean}
*/
hasAchievements() {
abstract;
return false;
}
}
export class Achievement {
/** @param {string} key - An ACHIEVEMENTS key */
constructor(key) {
this.key = key;
this.activate = null;
this.activatePromise = null;
this.receiver = null;
this.signal = null;
}
init() {}
isValid() {
return true;
}
unlock() {
if (!this.activatePromise) {
this.activatePromise = this.activate(this.key);
}
return this.activatePromise;
}
}
export class AchievementCollection {
/**
* @param {function} activate - Resolves when provider activation is complete
*/
constructor(activate) {
this.map = new Map();
this.activate = activate;
this.add(ACHIEVEMENTS.belt500Tiles, {
isValid: this.isBelt500TilesValid,
signal: "entityAdded",
});
this.add(ACHIEVEMENTS.blueprint100k, this.createBlueprintOptions(100000));
this.add(ACHIEVEMENTS.blueprint1m, this.createBlueprintOptions(1000000));
this.add(ACHIEVEMENTS.completeLvl26, this.createLevelOptions(26));
this.add(ACHIEVEMENTS.cutShape);
this.add(ACHIEVEMENTS.darkMode, {
isValid: this.isDarkModeValid,
});
this.add(ACHIEVEMENTS.destroy1000, {
isValid: this.isDestroy1000Valid,
});
this.add(ACHIEVEMENTS.irrelevantShape, {
isValid: this.isIrrelevantShapeValid,
signal: "shapeDelivered",
});
this.add(ACHIEVEMENTS.level100, this.createLevelOptions(100));
this.add(ACHIEVEMENTS.level50, this.createLevelOptions(50));
this.add(ACHIEVEMENTS.logoBefore18, {
isValid: this.isLogoBefore18Valid,
signal: "itemProduced",
});
this.add(ACHIEVEMENTS.mam, {
isValid: this.isMamValid,
});
this.add(ACHIEVEMENTS.mapMarkers15, {
isValid: this.isMapMarkers15Valid,
});
this.add(ACHIEVEMENTS.noBeltUpgradesUntilBp, {
isValid: this.isNoBeltUpgradesUntilBpValid,
signal: "storyGoalCompleted",
});
this.add(ACHIEVEMENTS.noInverseRotater, {
init: this.initNoInverseRotater,
isValid: this.isNoInverseRotaterValid,
signal: "storyGoalCompleted",
});
this.add(ACHIEVEMENTS.oldLevel17, this.createShapeOptions(SHAPE_OLD_LEVEL_17));
this.add(ACHIEVEMENTS.openWires, {
isValid: this.isOpenWiresValid,
signal: "editModeChanged",
});
this.add(ACHIEVEMENTS.paintShape);
this.add(ACHIEVEMENTS.place5000Wires, {
isValid: this.isPlace5000WiresValid,
});
this.add(ACHIEVEMENTS.placeBlueprint, {
isValid: this.isPlaceBlueprintValid,
});
this.add(ACHIEVEMENTS.placeBp1000, {
isValid: this.isPlaceBp1000Valid,
});
this.add(ACHIEVEMENTS.play1h, this.createTimeOptions(HOUR_1));
this.add(ACHIEVEMENTS.play10h, this.createTimeOptions(HOUR_10));
this.add(ACHIEVEMENTS.play20h, this.createTimeOptions(HOUR_20));
this.add(ACHIEVEMENTS.produceLogo, this.createShapeOptions(SHAPE_LOGO));
this.add(ACHIEVEMENTS.produceRocket, this.createShapeOptions(SHAPE_ROCKET));
this.add(ACHIEVEMENTS.produceMsLogo, this.createShapeOptions(SHAPE_MS_LOGO));
this.add(ACHIEVEMENTS.rotateShape);
this.add(ACHIEVEMENTS.speedrunBp30, this.createSpeedOptions(12, MINUTE_30));
this.add(ACHIEVEMENTS.speedrunBp60, this.createSpeedOptions(12, MINUTE_60));
this.add(ACHIEVEMENTS.speedrunBp120, this.createSpeedOptions(12, MINUTE_120));
this.add(ACHIEVEMENTS.stack4Layers, {
isValid: this.isStack4LayersValid,
signal: "itemProduced",
});
this.add(ACHIEVEMENTS.stackShape);
this.add(ACHIEVEMENTS.store100Unique, {
init: this.initStore100Unique,
isValid: this.isStore100UniqueValid,
signal: "shapeDelivered",
});
this.add(ACHIEVEMENTS.storeShape, {
init: this.initStoreShape,
isValid: this.isStoreShapeValid,
});
this.add(ACHIEVEMENTS.throughputBp25, this.createRateOptions(SHAPE_BP, 25));
this.add(ACHIEVEMENTS.throughputBp50, this.createRateOptions(SHAPE_BP, 50));
this.add(ACHIEVEMENTS.throughputLogo25, this.createRateOptions(SHAPE_LOGO, 25));
this.add(ACHIEVEMENTS.throughputLogo50, this.createRateOptions(SHAPE_LOGO, 50));
this.add(ACHIEVEMENTS.throughputRocket10, this.createRateOptions(SHAPE_ROCKET, 10));
this.add(ACHIEVEMENTS.throughputRocket20, this.createRateOptions(SHAPE_ROCKET, 20));
this.add(ACHIEVEMENTS.trash1000, {
init: this.initTrash1000,
isValid: this.isTrash1000Valid,
});
this.add(ACHIEVEMENTS.unlockWires, this.createLevelOptions(20));
this.add(ACHIEVEMENTS.upgradesTier5, this.createUpgradeOptions(5));
this.add(ACHIEVEMENTS.upgradesTier8, this.createUpgradeOptions(8));
}
/** @param {GameRoot} root */
initialize(root) {
this.root = root;
this.root.signals.achievementCheck.add(this.unlock, this);
this.root.signals.bulkAchievementCheck.add(this.bulkUnlock, this);
for (let [key, achievement] of this.map.entries()) {
if (achievement.signal) {
achievement.receiver = this.unlock.bind(this, key);
this.root.signals[achievement.signal].add(achievement.receiver);
}
if (achievement.init) {
achievement.init();
}
}
if (!this.hasDefaultReceivers()) {
this.root.signals.achievementCheck.remove(this.unlock);
this.root.signals.bulkAchievementCheck.remove(this.bulkUnlock);
}
}
/**
* @param {string} key - Maps to an Achievement
* @param {object} [options]
* @param {function} [options.init]
* @param {function} [options.isValid]
* @param {string} [options.signal]
*/
add(key, options = {}) {
if (G_IS_DEV) {
assert(ACHIEVEMENTS[key], "Achievement key not found: ", key);
}
const achievement = new Achievement(key);
achievement.activate = this.activate;
if (options.init) {
achievement.init = options.init.bind(this, achievement);
}
if (options.isValid) {
achievement.isValid = options.isValid.bind(this);
}
if (options.signal) {
achievement.signal = options.signal;
}
this.map.set(key, achievement);
}
bulkUnlock() {
for (let i = 0; i < arguments.length; i += 2) {
this.unlock(arguments[i], arguments[i + 1]);
}
}
/**
* @param {string} key - Maps to an Achievement
* @param {any} data - Data received from signal dispatches for validation
*/
unlock(key, data) {
if (!this.map.has(key)) {
return;
}
const achievement = this.map.get(key);
if (!achievement.isValid(data)) {
return;
}
achievement
.unlock()
.then(() => {
this.onActivate(null, key);
})
.catch(err => {
this.onActivate(err, key);
});
}
/**
* Cleans up after achievement activation attempt with the provider. Could
* utilize err to retry some number of times if needed.
* @param {?Error} err - Error is null if activation was successful
* @param {string} key - Maps to an Achievement
*/
onActivate(err, key) {
this.remove(key);
if (!this.hasDefaultReceivers()) {
this.root.signals.achievementCheck.remove(this.unlock);
}
}
/** @param {string} key - Maps to an Achievement */
remove(key) {
const achievement = this.map.get(key);
if (achievement) {
if (achievement.receiver) {
this.root.signals[achievement.signal].remove(achievement.receiver);
}
this.map.delete(key);
}
}
/**
* Check if the collection-level achievementCheck receivers are still
* necessary.
*/
hasDefaultReceivers() {
if (!this.map.size) {
return false;
}
for (let achievement of this.map.values()) {
if (!achievement.signal) {
return true;
}
}
return false;
}
/*
* Remaining methods exist to extend Achievement instances within the
* collection.
*/
hasAllUpgradesAtLeastAtTier(tier) {
const upgrades = this.root.gameMode.getUpgrades();
for (let upgradeId in upgrades) {
if (this.root.hubGoals.getUpgradeLevel(upgradeId) < tier - 1) {
return false;
}
}
return true;
}
/**
* @param {ShapeItem} item
* @param {string} shape
* @returns {boolean}
*/
isShape(item, shape) {
return item.getItemType() === ITEM_SHAPE && item.definition.getHash() === shape;
}
createBlueprintOptions(count) {
return {
init: ({ key }) => this.unlock(key, ShapeDefinition.fromShortKey(SHAPE_BP)),
isValid: definition =>
definition.cachedHash === SHAPE_BP && this.root.hubGoals.storedShapes[SHAPE_BP] >= count,
signal: "shapeDelivered",
};
}
createLevelOptions(level) {
return {
init: ({ key }) => this.unlock(key, this.root.hubGoals.level),
isValid: currentLevel => currentLevel >= level,
signal: "storyGoalCompleted",
};
}
createRateOptions(shape, rate) {
return {
isValid: () => {
return (
this.root.productionAnalytics.getCurrentShapeRateRaw(
enumAnalyticsDataSource.delivered,
this.root.shapeDefinitionMgr.getShapeFromShortKey(shape)
) /
globalConfig.analyticsSliceDurationSeconds >=
rate
);
},
};
}
createShapeOptions(shape) {
return {
isValid: item => this.isShape(item, shape),
signal: "itemProduced",
};
}
createSpeedOptions(level, time) {
return {
isValid: currentLevel => currentLevel >= level && this.root.time.now() < time,
signal: "storyGoalCompleted",
};
}
createTimeOptions(duration) {
return {
isValid: () => this.root.time.now() >= duration,
};
}
createUpgradeOptions(tier) {
return {
init: ({ key }) => this.unlock(key, null),
isValid: () => this.hasAllUpgradesAtLeastAtTier(tier),
signal: "upgradePurchased",
};
}
/** @param {Entity} entity @returns {boolean} */
isBelt500TilesValid(entity) {
return entity.components.Belt && entity.components.Belt.assignedPath.totalLength >= 500;
}
/** @returns {boolean} */
isDarkModeValid() {
return this.root.app.settings.currentData.settings.theme === DARK_MODE;
}
/** @param {number} count @returns {boolean} */
isDestroy1000Valid(count) {
return count >= 1000;
}
/** @param {ShapeDefinition} definition @returns {boolean} */
isIrrelevantShapeValid(definition) {
const levels = this.root.gameMode.getLevelDefinitions();
for (let i = 0; i < levels.length; i++) {
if (definition.cachedHash === levels[i].shape) {
return false;
}
}
const upgrades = this.root.gameMode.getUpgrades();
for (let upgradeId in upgrades) {
for (const tier in upgrades[upgradeId]) {
const requiredShapes = upgrades[upgradeId][tier].required;
for (let i = 0; i < requiredShapes.length; i++) {
if (definition.cachedHash === requiredShapes[i].shape) {
return false;
}
}
}
}
return true;
}
/** @param {ShapeItem} item @returns {boolean} */
isLogoBefore18Valid(item) {
return this.root.hubGoals.level < 18 && this.isShape(item, SHAPE_LOGO);
}
/** @returns {boolean} */
isMamValid() {
return this.root.hubGoals.level > 27 && !this.root.savegame.currentData.stats.failedMam;
}
/** @param {number} count @returns {boolean} */
isMapMarkers15Valid(count) {
return count >= 15;
}
/**
* @param {number} level
* @returns {boolean}
*/
isNoBeltUpgradesUntilBpValid(level) {
return level >= 12 && this.root.hubGoals.upgradeLevels.belt === 0;
}
initNoInverseRotater() {
if (this.root.savegame.currentData.stats.usedInverseRotater === true) {
return;
}
const entities = this.root.entityMgr.componentToEntity.StaticMapEntity;
let usedInverseRotater = false;
for (var i = 0; i < entities.length; i++) {
const entity = entities[i].components.StaticMapEntity;
if (entity.code === ROTATER_CCW_CODE || entity.code === ROTATER_180_CODE) {
usedInverseRotater = true;
break;
}
}
this.root.savegame.currentData.stats.usedInverseRotater = usedInverseRotater;
}
/** @param {number} level @returns {boolean} */
isNoInverseRotaterValid(level) {
return level >= 14 && !this.root.savegame.currentData.stats.usedInverseRotater;
}
/** @param {string} currentLayer @returns {boolean} */
isOpenWiresValid(currentLayer) {
return currentLayer === WIRE_LAYER;
}
/** @param {Entity} entity @returns {boolean} */
isPlace5000WiresValid(entity) {
return (
entity.components.Wire &&
entity.registered &&
entity.root.entityMgr.componentToEntity.Wire.length >= 5000
);
}
/** @param {number} count @returns {boolean} */
isPlaceBlueprintValid(count) {
return count != 0;
}
/** @param {number} count @returns {boolean} */
isPlaceBp1000Valid(count) {
return count >= 1000;
}
/** @param {ShapeItem} item @returns {boolean} */
isStack4LayersValid(item) {
return item.getItemType() === ITEM_SHAPE && item.definition.layers.length === 4;
}
/** @param {Achievement} achievement */
initStore100Unique({ key }) {
this.unlock(key, null);
}
/** @returns {boolean} */
isStore100UniqueValid() {
return Object.keys(this.root.hubGoals.storedShapes).length >= 100;
}
/** @param {Achievement} achievement */
initStoreShape({ key }) {
this.unlock(key, null);
}
/** @returns {boolean} */
isStoreShapeValid() {
const entities = this.root.systemMgr.systems.storage.allEntities;
if (entities.length === 0) {
return false;
}
for (var i = 0; i < entities.length; i++) {
if (entities[i].components.Storage.storedCount > 0) {
return true;
}
}
return false;
}
/** @param {Achievement} achievement */
initTrash1000({ key }) {
if (Number(this.root.savegame.currentData.stats.trashedCount)) {
this.unlock(key, 0);
return;
}
this.root.savegame.currentData.stats.trashedCount = 0;
}
/** @param {number} count @returns {boolean} */
isTrash1000Valid(count) {
this.root.savegame.currentData.stats.trashedCount += count;
return this.root.savegame.currentData.stats.trashedCount >= 1000;
}
}

View File

@ -0,0 +1,23 @@
import { AchievementProviderInterface } from "../achievement_provider";
export class NoAchievementProvider extends AchievementProviderInterface {
hasAchievements() {
return false;
}
hasLoaded() {
return false;
}
initialize() {
return Promise.resolve();
}
onLoad() {
return Promise.reject(new Error("No achievements to load"));
}
activate() {
return Promise.resolve();
}
}

View File

@ -58,11 +58,6 @@ export class StorageImplBrowser extends StorageInterface {
});
}
writeFileSyncIfSupported(filename, contents) {
window.localStorage.setItem(filename, contents);
return true;
}
readFileAsync(filename) {
if (this.currentBusyFilename === filename) {
logger.warn("Attempt to read", filename, "while write progress on it is ongoing!");

View File

@ -94,12 +94,6 @@ export class StorageImplBrowserIndexedDB extends StorageInterface {
});
}
writeFileSyncIfSupported(filename, contents) {
// Not supported
this.writeFileAsync(filename, contents);
return true;
}
readFileAsync(filename) {
if (!this.database) {
return Promise.reject("Storage not ready");

View File

@ -4,7 +4,9 @@ import { queryParamOptions } from "../../core/query_parameters";
import { clamp } from "../../core/utils";
import { GamedistributionAdProvider } from "../ad_providers/gamedistribution";
import { NoAdProvider } from "../ad_providers/no_ad_provider";
import { SteamAchievementProvider } from "../electron/steam_achievement_provider";
import { PlatformWrapperInterface } from "../wrapper";
import { NoAchievementProvider } from "./no_achievement_provider";
import { StorageImplBrowser } from "./storage";
import { StorageImplBrowserIndexedDB } from "./storage_indexed_db";
@ -71,6 +73,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
return this.detectStorageImplementation()
.then(() => this.initializeAdProvider())
.then(() => this.initializeAchievementProvider())
.then(() => super.initialize());
}
@ -196,6 +199,20 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
});
}
initializeAchievementProvider() {
if (G_IS_DEV && globalConfig.debug.testAchievements) {
this.app.achievementProvider = new SteamAchievementProvider(this.app);
return this.app.achievementProvider.initialize().catch(err => {
logger.error("Failed to initialize achievement provider, disabling:", err);
this.app.achievementProvider = new NoAchievementProvider(this.app);
});
}
return this.app.achievementProvider.initialize();
}
exitApp() {
// Can not exit app
}

View File

@ -0,0 +1,143 @@
/* typehints:start */
import { Application } from "../../application";
import { GameRoot } from "../../game/root";
/* typehints:end */
import { createLogger } from "../../core/logging";
import { getIPCRenderer } from "../../core/utils";
import { ACHIEVEMENTS, AchievementCollection, AchievementProviderInterface } from "../achievement_provider";
const logger = createLogger("achievements/steam");
const ACHIEVEMENT_IDS = {
[ACHIEVEMENTS.belt500Tiles]: "belt_500_tiles",
[ACHIEVEMENTS.blueprint100k]: "blueprint_100k",
[ACHIEVEMENTS.blueprint1m]: "blueprint_1m",
[ACHIEVEMENTS.completeLvl26]: "complete_lvl_26",
[ACHIEVEMENTS.cutShape]: "cut_shape",
[ACHIEVEMENTS.darkMode]: "dark_mode",
[ACHIEVEMENTS.destroy1000]: "destroy_1000",
[ACHIEVEMENTS.irrelevantShape]: "irrelevant_shape",
[ACHIEVEMENTS.level100]: "level_100",
[ACHIEVEMENTS.level50]: "level_50",
[ACHIEVEMENTS.logoBefore18]: "logo_before_18",
[ACHIEVEMENTS.mam]: "mam",
[ACHIEVEMENTS.mapMarkers15]: "map_markers_15",
[ACHIEVEMENTS.openWires]: "open_wires",
[ACHIEVEMENTS.oldLevel17]: "old_level_17",
[ACHIEVEMENTS.noBeltUpgradesUntilBp]: "no_belt_upgrades_until_bp",
[ACHIEVEMENTS.noInverseRotater]: "no_inverse_rotator", // [sic]
[ACHIEVEMENTS.paintShape]: "paint_shape",
[ACHIEVEMENTS.place5000Wires]: "place_5000_wires",
[ACHIEVEMENTS.placeBlueprint]: "place_blueprint",
[ACHIEVEMENTS.placeBp1000]: "place_bp_1000",
[ACHIEVEMENTS.play1h]: "play_1h",
[ACHIEVEMENTS.play10h]: "play_10h",
[ACHIEVEMENTS.play20h]: "play_20h",
[ACHIEVEMENTS.produceLogo]: "produce_logo",
[ACHIEVEMENTS.produceMsLogo]: "produce_ms_logo",
[ACHIEVEMENTS.produceRocket]: "produce_rocket",
[ACHIEVEMENTS.rotateShape]: "rotate_shape",
[ACHIEVEMENTS.speedrunBp30]: "speedrun_bp_30",
[ACHIEVEMENTS.speedrunBp60]: "speedrun_bp_60",
[ACHIEVEMENTS.speedrunBp120]: "speedrun_bp_120",
[ACHIEVEMENTS.stack4Layers]: "stack_4_layers",
[ACHIEVEMENTS.stackShape]: "stack_shape",
[ACHIEVEMENTS.store100Unique]: "store_100_unique",
[ACHIEVEMENTS.storeShape]: "store_shape",
[ACHIEVEMENTS.throughputBp25]: "throughput_bp_25",
[ACHIEVEMENTS.throughputBp50]: "throughput_bp_50",
[ACHIEVEMENTS.throughputLogo25]: "throughput_logo_25",
[ACHIEVEMENTS.throughputLogo50]: "throughput_logo_50",
[ACHIEVEMENTS.throughputRocket10]: "throughput_rocket_10",
[ACHIEVEMENTS.throughputRocket20]: "throughput_rocket_20",
[ACHIEVEMENTS.trash1000]: "trash_1000",
[ACHIEVEMENTS.unlockWires]: "unlock_wires",
[ACHIEVEMENTS.upgradesTier5]: "upgrades_tier_5",
[ACHIEVEMENTS.upgradesTier8]: "upgrades_tier_8",
};
export class SteamAchievementProvider extends AchievementProviderInterface {
/** @param {Application} app */
constructor(app) {
super(app);
this.initialized = false;
this.collection = new AchievementCollection(this.activate.bind(this));
if (G_IS_DEV) {
for (let key in ACHIEVEMENT_IDS) {
assert(this.collection.map.has(key), "Key not found in collection: " + key);
}
}
logger.log("Collection created with", this.collection.map.size, "achievements");
}
/** @returns {boolean} */
hasAchievements() {
return true;
}
/**
* @param {GameRoot} root
* @returns {Promise<void>}
*/
onLoad(root) {
this.root = root;
try {
this.collection = new AchievementCollection(this.activate.bind(this));
this.collection.initialize(root);
logger.log("Initialized", this.collection.map.size, "relevant achievements");
return Promise.resolve();
} catch (err) {
logger.error("Failed to initialize the collection");
return Promise.reject(err);
}
}
/** @returns {Promise<void>} */
initialize() {
if (!G_IS_STANDALONE) {
logger.warn("Steam unavailable. Achievements won't sync.");
return Promise.resolve();
}
this.ipc = getIPCRenderer();
return this.ipc.invoke("steam:is-initialized").then(initialized => {
this.initialized = initialized;
if (!this.initialized) {
logger.warn("Steam failed to intialize. Achievements won't sync.");
} else {
logger.log("Steam achievement provider initialized");
}
});
}
/**
* @param {string} key
* @returns {Promise<void>}
*/
activate(key) {
let promise;
if (!this.initialized) {
promise = Promise.resolve();
} else {
promise = this.ipc.invoke("steam:activate-achievement", ACHIEVEMENT_IDS[key]);
}
return promise
.then(() => {
logger.log("Achievement activated:", key);
})
.catch(err => {
logger.error("Failed to activate achievement:", key, err);
throw err;
});
}
}

View File

@ -7,24 +7,6 @@ const logger = createLogger("electron-storage");
export class StorageImplElectron extends StorageInterface {
constructor(app) {
super(app);
/** @type {Object.<number, {resolve:Function, reject: Function}>} */
this.jobs = {};
this.jobId = 0;
getIPCRenderer().on("fs-response", (event, arg) => {
const id = arg.id;
if (!this.jobs[id]) {
logger.warn("Got unhandled FS response, job not known:", id);
return;
}
const { resolve, reject } = this.jobs[id];
if (arg.result.success) {
resolve(arg.result.data);
} else {
reject(arg.result.error);
}
});
}
initialize() {
@ -33,50 +15,52 @@ export class StorageImplElectron extends StorageInterface {
writeFileAsync(filename, contents) {
return new Promise((resolve, reject) => {
// ipcMain
const jobId = ++this.jobId;
this.jobs[jobId] = { resolve, reject };
getIPCRenderer().send("fs-job", {
getIPCRenderer()
.invoke("fs-job", {
type: "write",
filename,
contents,
id: jobId,
});
});
})
.then(result => {
if (result.success) {
resolve(result.data);
} else {
reject(result.error);
}
writeFileSyncIfSupported(filename, contents) {
return getIPCRenderer().sendSync("fs-sync-job", {
type: "write",
filename,
contents,
});
});
}
readFileAsync(filename) {
return new Promise((resolve, reject) => {
// ipcMain
const jobId = ++this.jobId;
this.jobs[jobId] = { resolve, reject };
getIPCRenderer().send("fs-job", {
getIPCRenderer()
.invoke("fs-job", {
type: "read",
filename,
id: jobId,
})
.then(result => {
if (result.success) {
resolve(result.data);
} else {
reject(result.error);
}
});
});
}
deleteFileAsync(filename) {
return new Promise((resolve, reject) => {
// ipcMain
const jobId = ++this.jobId;
this.jobs[jobId] = { resolve, reject };
getIPCRenderer().send("fs-job", {
getIPCRenderer()
.invoke("fs-job", {
type: "delete",
filename,
id: jobId,
})
.then(result => {
if (result.success) {
resolve(result.data);
} else {
reject(result.error);
}
});
});
}

View File

@ -1,15 +1,35 @@
import { NoAchievementProvider } from "../browser/no_achievement_provider";
import { PlatformWrapperImplBrowser } from "../browser/wrapper";
import { getIPCRenderer } from "../../core/utils";
import { createLogger } from "../../core/logging";
import { StorageImplElectron } from "./storage";
import { SteamAchievementProvider } from "./steam_achievement_provider";
import { PlatformWrapperInterface } from "../wrapper";
const logger = createLogger("electron-wrapper");
export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
initialize() {
this.steamOverlayCanvasFix = document.createElement("canvas");
this.steamOverlayCanvasFix.width = 1;
this.steamOverlayCanvasFix.height = 1;
this.steamOverlayCanvasFix.id = "steamOverlayCanvasFix";
this.steamOverlayContextFix = this.steamOverlayCanvasFix.getContext("2d");
document.documentElement.appendChild(this.steamOverlayCanvasFix);
this.app.ticker.frameEmitted.add(this.steamOverlayFixRedrawCanvas, this);
this.app.storage = new StorageImplElectron(this);
return PlatformWrapperInterface.prototype.initialize.call(this);
this.app.achievementProvider = new SteamAchievementProvider(this.app);
return this.initializeAchievementProvider().then(() =>
PlatformWrapperInterface.prototype.initialize.call(this)
);
}
steamOverlayFixRedrawCanvas() {
this.steamOverlayContextFix.clearRect(0, 0, 1, 1);
}
getId() {
@ -38,6 +58,14 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
return Promise.resolve();
}
initializeAchievementProvider() {
return this.app.achievementProvider.initialize().catch(err => {
logger.error("Failed to initialize achievement provider, disabling:", err);
this.app.achievementProvider = new NoAchievementProvider(this.app);
});
}
getSupportsFullscreen() {
return true;
}

View File

@ -30,16 +30,6 @@ export class StorageInterface {
return Promise.reject();
}
/**
* Tries to write a file synchronously, used in unload handler
* @param {string} filename
* @param {string} contents
*/
writeFileSyncIfSupported(filename, contents) {
abstract;
return false;
}
/**
* Reads a string asynchronously. Returns Promise<FILE_NOT_FOUND> if file was not found.
* @param {string} filename

View File

@ -12,6 +12,8 @@ import { SavegameInterface_V1003 } from "./schemas/1003";
import { SavegameInterface_V1004 } from "./schemas/1004";
import { SavegameInterface_V1005 } from "./schemas/1005";
import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008";
const logger = createLogger("savegame");
@ -52,7 +54,7 @@ export class Savegame extends ReadWriteProxy {
* @returns {number}
*/
static getCurrentVersion() {
return 1006;
return 1008;
}
/**
@ -77,7 +79,11 @@ export class Savegame extends ReadWriteProxy {
return {
version: this.getCurrentVersion(),
dump: null,
stats: {},
stats: {
failedMam: false,
trashedCount: 0,
usedInverseRotater: false,
},
lastUpdate: Date.now(),
};
}
@ -121,6 +127,16 @@ export class Savegame extends ReadWriteProxy {
data.version = 1006;
}
if (data.version === 1006) {
SavegameInterface_V1007.migrate1006to1007(data);
data.version = 1007;
}
if (data.version === 1007) {
SavegameInterface_V1008.migrate1007to1008(data);
data.version = 1008;
}
return ExplainedResult.good();
}

View File

@ -59,7 +59,13 @@ if (G_IS_DEV) {
}
}
function compressObjectInternal(obj, keys = [], values = []) {
/**
* @param {any} obj
* @param {Map} keys
* @param {Map} values
* @returns {any[]|object|number|string}
*/
function compressObjectInternal(obj, keys, values) {
if (Array.isArray(obj)) {
let result = [];
for (let i = 0; i < obj.length; ++i) {
@ -69,37 +75,58 @@ function compressObjectInternal(obj, keys = [], values = []) {
} else if (typeof obj === "object" && obj !== null) {
let result = {};
for (const key in obj) {
let index = keys.indexOf(key);
if (index < 0) {
keys.push(key);
index = keys.length - 1;
let index = keys.get(key);
if (index === undefined) {
index = keys.size;
keys.set(key, index);
}
const value = obj[key];
result[compressInt(index)] = compressObjectInternal(value, keys, values);
}
return result;
} else if (typeof obj === "string") {
let index = values.indexOf(obj);
if (index < 0) {
values.push(obj);
index = values.length - 1;
let index = values.get(obj);
if (index === undefined) {
index = values.size;
values.set(obj, index);
}
return compressInt(index);
}
return obj;
}
/**
* @param {Map} hashMap
* @returns {Array}
*/
function indexMapToArray(hashMap) {
const result = new Array(hashMap.size);
hashMap.forEach((index, key) => {
result[index] = key;
});
return result;
}
/**
* @param {object} obj
*/
export function compressObject(obj) {
const keys = [];
const values = [];
const keys = new Map();
const values = new Map();
const data = compressObjectInternal(obj, keys, values);
return {
keys,
values,
keys: indexMapToArray(keys),
values: indexMapToArray(values),
data,
};
}
/**
* @param {object} obj
* @param {string[]} keys
* @param {any[]} values
* @returns {object}
*/
function decompressObjectInternal(obj, keys = [], values = []) {
if (Array.isArray(obj)) {
let result = [];
@ -122,6 +149,9 @@ function decompressObjectInternal(obj, keys = [], values = []) {
return obj;
}
/**
* @param {object} obj
*/
export function decompressObject(obj) {
if (obj.keys && obj.values && obj.data) {
const keys = obj.keys;

View File

@ -7,6 +7,8 @@ import { SavegameInterface_V1003 } from "./schemas/1003";
import { SavegameInterface_V1004 } from "./schemas/1004";
import { SavegameInterface_V1005 } from "./schemas/1005";
import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008";
/** @type {Object.<number, typeof BaseSavegameInterface>} */
export const savegameInterfaces = {
@ -17,6 +19,8 @@ export const savegameInterfaces = {
1004: SavegameInterface_V1004,
1005: SavegameInterface_V1005,
1006: SavegameInterface_V1006,
1007: SavegameInterface_V1007,
1008: SavegameInterface_V1008,
};
const logger = createLogger("savegame_interface_registry");

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