1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-12 09:41:51 +00:00

merged from upstream

This commit is contained in:
dgs4349 2020-10-08 18:16:09 -04:00
commit 28fef86abe
185 changed files with 26572 additions and 20125 deletions

8
.editorconfig Executable file
View File

@ -0,0 +1,8 @@
root = true
[{src, translations}/*]
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8

4
.gitattributes vendored
View File

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

View File

@ -35,19 +35,23 @@ jobs:
cd gulp/ cd gulp/
yarn yarn
cd .. cd ..
- name: Lint - name: Lint
run: | run: |
yarn lint yarn lint
- name: YAML Lint
uses: ibiqlik/action-yamllint@v1.0.0
with:
file_or_dir: translations/*.yaml
- name: TSLint - name: TSLint
run: | run: |
cd gulp cd gulp
yarn gulp translations.fullBuild yarn gulp translations.fullBuild
cd .. cd ..
yarn tslint yarn tslint
yaml-lint:
name: yaml-lint
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: YAML Lint
uses: ibiqlik/action-yamllint@v1.0.0
with:
file_or_dir: translations/*.yaml

66
.gitignore vendored
View File

@ -15,34 +15,11 @@ pids
*.seed *.seed
*.pid.lock *.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html) # Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release build/Release
# Dependency directories # Dependency directories
node_modules/ node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
@ -53,18 +30,9 @@ typings/
# Optional eslint cache # Optional eslint cache
.eslintcache .eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
@ -72,41 +40,11 @@ typings/
.env .env
.env.test .env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Buildfiles # Buildfiles
build build
res_built
gulp/runnable-texturepacker.jar
tmp_standalone_files tmp_standalone_files
# Local config # Local config

View File

@ -4,3 +4,4 @@ rules:
line-length: line-length:
level: warning level: warning
max: 200 max: 200
document-start: disable

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
FROM node:12 as base
EXPOSE 3001 3005
WORKDIR /shapez.io
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg default-jre \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY package.json yarn.lock ./
RUN yarn
COPY gulp ./gulp
WORKDIR /shapez.io/gulp
RUN yarn
WORKDIR /shapez.io
COPY res ./res
COPY src/html ./src/html
COPY src/css ./src/css
COPY version ./version
COPY sync-translations.js ./
COPY translations ./translations
COPY src/js ./src/js
COPY res_raw ./res_raw
COPY .git ./.git
WORKDIR /shapez.io/gulp
ENTRYPOINT ["yarn", "gulp"]

View File

@ -22,15 +22,14 @@ Your goal is to produce shapes by cutting, rotating, merging and painting parts
## Building ## Building
- Make sure git `git lfs` extension is on your path
- Run `git lfs pull` to download sound assets
- Make sure `ffmpeg` is on your path - Make sure `ffmpeg` is on your path
- Install Node.js and Yarn - Install Node.js and Yarn
- Install Java (required for textures)
- Run `yarn` in the root folder - Run `yarn` in the root folder
- Cd into `gulp` folder - Cd into `gulp` folder
- Run `yarn` and then `yarn gulp` - it should now open in your browser - Run `yarn` and then `yarn gulp` - it should now open in your browser
**Notice**: This will produce a debug build with several debugging flags enabled. If you want to disable them, modify `config.js`. **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).
## Helping translate ## Helping translate
@ -116,8 +115,8 @@ This is a quick checklist, if a new building is added this points should be fulf
### Assets ### Assets
For most assets I use Adobe Photoshop, you can find them in `assets/`. For most assets I use Adobe Photoshop, you can find them <a href="//github.com/tobspr/shapez.io-artwork" target="_blank">here</a>.
You will need a <a href="https://www.codeandweb.com/texturepacker" target="_blank">Texture Packer</a> license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when that's done. All assets will be automatically rebuilt into the atlas once changed (Thanks to dengr1065!)
<img src="https://i.imgur.com/W25Fkl0.png" alt="shapez.io Screenshot"> <img src="https://i.imgur.com/W25Fkl0.png" alt="shapez.io Screenshot">

View File

@ -1,3 +0,0 @@
The artwork can be found here:
https://github.com/tobspr/shapez.io-artwork

View File

@ -1,16 +1,16 @@
{ {
"name": "electron", "name": "electron",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {
"startDev": "electron --disable-direct-composition --in-process-gpu . --dev --local", "startDev": "electron --disable-direct-composition --in-process-gpu . --dev --local",
"startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local", "startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local",
"start": "electron --disable-direct-composition --in-process-gpu ." "start": "electron --disable-direct-composition --in-process-gpu ."
}, },
"devDependencies": { "devDependencies": {
"electron": "^6.1.12" "electron": "10.1.3"
}, },
"dependencies": {} "dependencies": {}
} }

File diff suppressed because it is too large Load Diff

1
gulp/.gitattributes vendored
View File

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

3
gulp/.gitignore vendored
View File

@ -1,2 +1 @@
additional_build_files additional_build_files
steampipe

127
gulp/atlas2json.js Normal file
View File

@ -0,0 +1,127 @@
const { join, resolve } = require("path");
const { readFileSync, readdirSync, writeFileSync } = require("fs");
const suffixToScale = {
lq: "0.25",
mq: "0.5",
hq: "0.75"
};
function convert(srcDir) {
const full = resolve(srcDir);
const srcFiles = readdirSync(full)
.filter(n => n.endsWith(".atlas"))
.map(n => join(full, n));
for (const atlas of srcFiles) {
console.log(`Processing: ${atlas}`);
// Read all text, split it into line array
// and filter all empty lines
const lines = readFileSync(atlas, "utf-8")
.split("\n")
.filter(n => n.trim());
// Get source image name
const image = lines.shift();
const srcMeta = {};
// Read all metadata (supports only one page)
while (true) {
const kv = lines.shift().split(":");
if (kv.length != 2) {
lines.unshift(kv[0]);
break;
}
srcMeta[kv[0]] = kv[1].trim();
}
const frames = {};
let current = null;
lines.push("Dummy line to make it convert last frame");
for (const line of lines) {
if (!line.startsWith(" ")) {
// New frame, convert previous if it exists
if (current != null) {
let { name, rotate, xy, size, orig, offset, index } = current;
// Convert to arrays because Node.js doesn't
// support latest JS features
xy = xy.split(",").map(v => Number(v));
size = size.split(",").map(v => Number(v));
orig = orig.split(",").map(v => Number(v));
offset = offset.split(",").map(v => Number(v));
// GDX TexturePacker removes index suffixes
const indexSuff = index != -1 ? `_${index}` : "";
const isTrimmed = size != orig;
frames[`${name}${indexSuff}.png`] = {
// Bounds on atlas
frame: {
x: xy[0],
y: xy[1],
w: size[0],
h: size[1]
},
// Whether image was rotated
rotated: rotate == "true",
trimmed: isTrimmed,
// How is the image trimmed
spriteSourceSize: {
x: offset[0],
y: (orig[1] - size[1]) - offset[1],
w: size[0],
h: size[1]
},
sourceSize: {
w: orig[0],
h: orig[1]
}
}
}
// Simple object that will hold other metadata
current = {
name: line
};
} else {
// Read and set current image metadata
const kv = line.split(":").map(v => v.trim());
current[kv[0]] = isNaN(Number(kv[1])) ? kv[1] : Number(kv[1]);
}
}
const atlasSize = srcMeta.size.split(",").map(v => Number(v));
const atlasScale = suffixToScale[atlas.match(/_(\w+)\.atlas$/)[1]];
const result = JSON.stringify({
frames,
meta: {
image,
format: srcMeta.format,
size: {
w: atlasSize[0],
h: atlasSize[1]
},
scale: atlasScale.toString()
}
});
writeFileSync(atlas.replace(".atlas", ".json"), result, {
encoding: "utf-8"
});
}
}
if (require.main == module) {
convert(process.argv[2]);
}
module.exports = { convert };

View File

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

12
gulp/entitlements.plist Normal file
View File

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

View File

@ -8,23 +8,6 @@ const path = require("path");
const deleteEmpty = require("delete-empty"); const deleteEmpty = require("delete-empty");
const execSync = require("child_process").execSync; const execSync = require("child_process").execSync;
const lfsOutput = execSync("git lfs install", { encoding: "utf-8" });
if (!lfsOutput.toLowerCase().includes("git lfs initialized")) {
console.error(`
Git LFS is not installed, unable to build.
To install Git LFS on Linux:
- Arch:
sudo pacman -S git-lfs
- Debian/Ubuntu:
sudo apt install git-lfs
For other systems, see:
https://github.com/git-lfs/git-lfs/wiki/Installation
`);
process.exit(1);
}
// Load other plugins dynamically // Load other plugins dynamically
const $ = require("gulp-load-plugins")({ const $ = require("gulp-load-plugins")({
scope: ["devDependencies"], scope: ["devDependencies"],
@ -42,6 +25,10 @@ const envVars = [
"SHAPEZ_CLI_STAGING_FTP_PW", "SHAPEZ_CLI_STAGING_FTP_PW",
"SHAPEZ_CLI_LIVE_FTP_USER", "SHAPEZ_CLI_LIVE_FTP_USER",
"SHAPEZ_CLI_LIVE_FTP_PW", "SHAPEZ_CLI_LIVE_FTP_PW",
"SHAPEZ_CLI_APPLE_ID",
"SHAPEZ_CLI_APPLE_CERT_NAME",
"SHAPEZ_CLI_GITHUB_USER",
"SHAPEZ_CLI_GITHUB_TOKEN",
]; ];
for (let i = 0; i < envVars.length; ++i) { for (let i = 0; i < envVars.length; ++i) {
@ -78,13 +65,12 @@ docs.gulptasksDocs($, gulp, buildFolder);
const standalone = require("./standalone"); const standalone = require("./standalone");
standalone.gulptasksStandalone($, gulp, buildFolder); standalone.gulptasksStandalone($, gulp, buildFolder);
const releaseUploader = require("./release-uploader");
releaseUploader.gulptasksReleaseUploader($, gulp, buildFolder);
const translations = require("./translations"); const translations = require("./translations");
translations.gulptasksTranslations($, gulp, buildFolder); translations.gulptasksTranslations($, gulp, buildFolder);
// FIXME
// const cordova = require("./cordova");
// cordova.gulptasksCordova($, gulp, buildFolder);
///////////////////// BUILD TASKS ///////////////////// ///////////////////// BUILD TASKS /////////////////////
// Cleans up everything // Cleans up everything
@ -96,8 +82,16 @@ gulp.task("utils.cleanBuildTempFolder", () => {
.src(path.join(__dirname, "..", "src", "js", "built-temp"), { read: false, allowEmpty: true }) .src(path.join(__dirname, "..", "src", "js", "built-temp"), { read: false, allowEmpty: true })
.pipe($.clean({ force: true })); .pipe($.clean({ force: true }));
}); });
gulp.task("utils.cleanImageBuildFolder", () => {
return gulp
.src(path.join(__dirname, "res_built"), { read: false, allowEmpty: true })
.pipe($.clean({ force: true }));
});
gulp.task("utils.cleanup", gulp.series("utils.cleanBuildFolder", "utils.cleanBuildTempFolder")); gulp.task(
"utils.cleanup",
gulp.series("utils.cleanBuildFolder", "utils.cleanImageBuildFolder", "utils.cleanBuildTempFolder")
);
// Requires no uncomitted files // Requires no uncomitted files
gulp.task("utils.requireCleanWorkingTree", cb => { gulp.task("utils.requireCleanWorkingTree", cb => {
@ -184,10 +178,12 @@ function serve({ standalone }) {
); );
// Watch resource files and copy them on change // Watch resource files and copy them on change
gulp.watch(imgres.rawImageResourcesGlobs, gulp.series("imgres.buildAtlas"));
gulp.watch(imgres.nonImageResourcesGlobs, gulp.series("imgres.copyNonImageResources")); gulp.watch(imgres.nonImageResourcesGlobs, gulp.series("imgres.copyNonImageResources"));
gulp.watch(imgres.imageResourcesGlobs, gulp.series("imgres.copyImageResources")); gulp.watch(imgres.imageResourcesGlobs, gulp.series("imgres.copyImageResources"));
// Watch .atlas files and recompile the atlas on change // Watch .atlas files and recompile the atlas on change
gulp.watch("../res_built/atlas/*.atlas", gulp.series("imgres.atlasToJson"));
gulp.watch("../res_built/atlas/*.json", gulp.series("imgres.atlas")); gulp.watch("../res_built/atlas/*.json", gulp.series("imgres.atlas"));
// Watch the build folder and reload when anything changed // Watch the build folder and reload when anything changed
@ -225,6 +221,8 @@ gulp.task(
gulp.series( gulp.series(
"utils.cleanup", "utils.cleanup",
"utils.copyAdditionalBuildFiles", "utils.copyAdditionalBuildFiles",
"imgres.buildAtlas",
"imgres.atlasToJson",
"imgres.atlas", "imgres.atlas",
"sounds.dev", "sounds.dev",
"imgres.copyImageResources", "imgres.copyImageResources",
@ -240,12 +238,13 @@ gulp.task(
"build.standalone.dev", "build.standalone.dev",
gulp.series( gulp.series(
"utils.cleanup", "utils.cleanup",
"imgres.buildAtlas",
"imgres.atlasToJson",
"imgres.atlas", "imgres.atlas",
"sounds.dev", "sounds.dev",
"imgres.copyImageResources", "imgres.copyImageResources",
"imgres.copyNonImageResources", "imgres.copyNonImageResources",
"translations.fullBuild", "translations.fullBuild",
"js.standalone-dev",
"css.dev", "css.dev",
"html.standalone-dev" "html.standalone-dev"
) )
@ -299,6 +298,17 @@ gulp.task(
gulp.series("utils.cleanup", "step.standalone-prod.all", "step.postbuild") gulp.series("utils.cleanup", "step.standalone-prod.all", "step.postbuild")
); );
// OS X build and release upload
gulp.task(
"build.darwin64-prod",
gulp.series(
"build.standalone-prod",
"standalone.prepare",
"standalone.package.prod.darwin64",
"standalone.uploadRelease.darwin64"
)
);
// Deploying! // Deploying!
gulp.task( gulp.task(
"main.deploy.alpha", "main.deploy.alpha",

View File

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

View File

@ -1,5 +1,15 @@
const { existsSync } = require("fs");
// @ts-ignore // @ts-ignore
const path = require("path"); const path = require("path");
const atlasToJson = require("./atlas2json");
const execute = command =>
require("child_process").execSync(command, {
encoding: "utf-8",
});
// Globs for atlas resources
const rawImageResourcesGlobs = ["../res_raw/atlas.json", "../res_raw/**/*.png"];
// Globs for non-ui resources // Globs for non-ui resources
const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/*.webm"]; const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/*.webm"];
@ -7,6 +17,9 @@ const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/
// Globs for ui resources // Globs for ui resources
const imageResourcesGlobs = ["../res/**/*.png", "../res/**/*.svg", "../res/**/*.jpg", "../res/**/*.gif"]; const imageResourcesGlobs = ["../res/**/*.png", "../res/**/*.svg", "../res/**/*.jpg", "../res/**/*.gif"];
// Link to download LibGDX runnable-texturepacker.jar
const runnableTPSource = "https://libgdx.badlogicgames.com/ci/nightlies/runnables/runnable-texturepacker.jar";
function gulptasksImageResources($, gulp, buildFolder) { function gulptasksImageResources($, gulp, buildFolder) {
// Lossless options // Lossless options
const minifyImagesOptsLossless = () => [ const minifyImagesOptsLossless = () => [
@ -59,6 +72,54 @@ function gulptasksImageResources($, gulp, buildFolder) {
/////////////// ATLAS ///////////////////// /////////////// ATLAS /////////////////////
gulp.task("imgres.buildAtlas", cb => {
const config = JSON.stringify("../res_raw/atlas.json");
const source = JSON.stringify("../res_raw");
const dest = JSON.stringify("../res_built/atlas");
try {
// First check whether Java is installed
execute("java -version");
// Now check and try downloading runnable-texturepacker.jar (22MB)
if (!existsSync("./runnable-texturepacker.jar")) {
const safeLink = JSON.stringify(runnableTPSource);
const commands = [
// linux/macos if installed
`wget -O runnable-texturepacker.jar ${safeLink}`,
// linux/macos, latest windows 10
`curl -o runnable-texturepacker.jar ${safeLink}`,
// windows 10 / updated windows 7+
"powershell.exe -Command (new-object System.Net.WebClient)" +
`.DownloadFile(${safeLink.replace(/"/g, "'")}, 'runnable-texturepacker.jar')`,
// windows 7+, vulnerability exploit
`certutil.exe -urlcache -split -f ${safeLink} runnable-texturepacker.jar`,
];
while (commands.length) {
try {
execute(commands.shift());
break;
} catch {
if (!commands.length) {
throw new Error("Failed to download runnable-texturepacker.jar!");
}
}
}
}
execute(`java -jar runnable-texturepacker.jar ${source} ${dest} atlas0 ${config}`);
} catch {
console.warn("Building atlas failed. Java not found / unsupported version?");
}
cb();
});
// Converts .atlas LibGDX files to JSON
gulp.task("imgres.atlasToJson", cb => {
atlasToJson.convert("../res_built/atlas");
cb();
});
// Copies the atlas to the final destination // Copies the atlas to the final destination
gulp.task("imgres.atlas", () => { gulp.task("imgres.atlas", () => {
return gulp.src(["../res_built/atlas/*.png"]).pipe(gulp.dest(resourcesDestFolder)); return gulp.src(["../res_built/atlas/*.png"]).pipe(gulp.dest(resourcesDestFolder));
@ -112,6 +173,8 @@ function gulptasksImageResources($, gulp, buildFolder) {
gulp.task( gulp.task(
"imgres.allOptimized", "imgres.allOptimized",
gulp.parallel( gulp.parallel(
"imgres.buildAtlas",
"imgres.atlasToJson",
"imgres.atlasOptimized", "imgres.atlasOptimized",
"imgres.copyNonImageResources", "imgres.copyNonImageResources",
"imgres.copyImageResourcesOptimized" "imgres.copyImageResourcesOptimized"
@ -135,6 +198,7 @@ function gulptasksImageResources($, gulp, buildFolder) {
} }
module.exports = { module.exports = {
rawImageResourcesGlobs,
nonImageResourcesGlobs, nonImageResourcesGlobs,
imageResourcesGlobs, imageResourcesGlobs,
gulptasksImageResources, gulptasksImageResources,

View File

@ -47,6 +47,7 @@
"serialize-error": "^3.0.0", "serialize-error": "^3.0.0",
"strictdom": "^1.0.1", "strictdom": "^1.0.1",
"string-replace-webpack-plugin": "^0.1.3", "string-replace-webpack-plugin": "^0.1.3",
"strip-indent": "^3.0.0",
"terser-webpack-plugin": "^1.1.0", "terser-webpack-plugin": "^1.1.0",
"through2": "^3.0.1", "through2": "^3.0.1",
"uglify-template-string-loader": "^1.1.0", "uglify-template-string-loader": "^1.1.0",
@ -66,7 +67,6 @@
"babel-plugin-danger-remove-unused-import": "^1.1.2", "babel-plugin-danger-remove-unused-import": "^1.1.2",
"css-mqpacker": "^7.0.0", "css-mqpacker": "^7.0.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"postcss-critical-split": "^2.5.3",
"electron-packager": "^14.0.6", "electron-packager": "^14.0.6",
"faster.js": "^1.1.0", "faster.js": "^1.1.0",
"glob": "^7.1.3", "glob": "^7.1.3",
@ -99,6 +99,7 @@
"jimp": "^0.6.1", "jimp": "^0.6.1",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"postcss-assets": "^5.0.0", "postcss-assets": "^5.0.0",
"postcss-critical-split": "^2.5.3",
"postcss-preset-env": "^6.5.0", "postcss-preset-env": "^6.5.0",
"postcss-round-subpixels": "^1.2.0", "postcss-round-subpixels": "^1.2.0",
"postcss-unprefix": "^2.1.3", "postcss-unprefix": "^2.1.3",

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

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

View File

@ -1,8 +1,10 @@
require("colors");
const packager = require("electron-packager"); const packager = require("electron-packager");
const path = require("path"); const path = require("path");
const { getVersion } = require("./buildutils"); const { getVersion } = require("./buildutils");
const fs = require("fs"); const fs = require("fs");
const fse = require("fs-extra"); const fse = require("fs-extra");
const buildutils = require("./buildutils");
const execSync = require("child_process").execSync; const execSync = require("child_process").execSync;
function gulptasksStandalone($, gulp) { function gulptasksStandalone($, gulp) {
@ -46,6 +48,20 @@ function gulptasksStandalone($, gulp) {
cb(); cb();
}); });
gulp.task("standalone.prepareVDF", cb => {
const hash = buildutils.getRevision();
const steampipeDir = path.join(__dirname, "steampipe", "scripts");
const templateContents = fs
.readFileSync(path.join(steampipeDir, "app.vdf.template"), { encoding: "utf-8" })
.toString();
const convertedContents = templateContents.replace("$DESC$", "Commit " + hash);
fs.writeFileSync(path.join(steampipeDir, "app.vdf"), convertedContents);
cb();
});
gulp.task("standalone.prepare.minifyCode", () => { gulp.task("standalone.prepare.minifyCode", () => {
return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir)); return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir));
}); });
@ -80,8 +96,9 @@ function gulptasksStandalone($, gulp) {
* @param {'win32'|'linux'|'darwin'} platform * @param {'win32'|'linux'|'darwin'} platform
* @param {'x64'|'ia32'} arch * @param {'x64'|'ia32'} arch
* @param {function():void} cb * @param {function():void} cb
* @param {boolean=} isRelease
*/ */
function packageStandalone(platform, arch, cb) { function packageStandalone(platform, arch, cb, isRelease = true) {
const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml")); const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml"));
packager({ packager({
@ -99,6 +116,21 @@ function gulptasksStandalone($, gulp) {
overwrite: true, overwrite: true,
appBundleId: "io.shapez.standalone", appBundleId: "io.shapez.standalone",
appCategoryType: "public.app-category.games", 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( }).then(
appPaths => { appPaths => {
console.log("Packages created:", appPaths); console.log("Packages created:", appPaths);
@ -123,7 +155,15 @@ function gulptasksStandalone($, gulp) {
fs.chmodSync(path.join(appPath, "play.sh"), 0o775); fs.chmodSync(path.join(appPath, "play.sh"), 0o775);
} }
if (platform === "darwin") { if (process.platform === "win32" && platform === "darwin") {
console.warn(
"Cross-building for macOS on Windows: dereferencing symlinks.\n".red +
"This will nearly double app size and make code signature invalid. Sorry!\n"
.red.bold +
"For more information, see " +
"https://github.com/electron/electron-packager/issues/71".underline
);
// Clear up framework folders // Clear up framework folders
fs.writeFileSync( fs.writeFileSync(
path.join(appPath, "play.sh"), path.join(appPath, "play.sh"),
@ -175,6 +215,9 @@ function gulptasksStandalone($, gulp) {
gulp.task("standalone.package.prod.linux64", cb => packageStandalone("linux", "x64", 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.linux32", cb => packageStandalone("linux", "ia32", cb));
gulp.task("standalone.package.prod.darwin64", cb => packageStandalone("darwin", "x64", 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( gulp.task(
"standalone.package.prod", "standalone.package.prod",

2
gulp/steampipe/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
steamtemp
app.vdf

View File

@ -0,0 +1,15 @@
"appbuild"
{
"appid" "1318690"
"desc" "$DESC$"
"buildoutput" "C:\work\shapez\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"
}
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
@echo off
cmd /c 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

@ -1,22 +1,89 @@
const path = require("path"); const path = require("path");
const fs = require("fs");
const yaml = require("gulp-yaml"); const gulpYaml = require("gulp-yaml");
const YAML = require("yaml");
const translationsSourceDir = path.join(__dirname, "..", "translations"); const stripIndent = require("strip-indent");
const translationsJsonDir = path.join(__dirname, "..", "src", "js", "built-temp"); const trim = require("trim");
function gulptasksTranslations($, gulp) { const translationsSourceDir = path.join(__dirname, "..", "translations");
gulp.task("translations.convertToJson", () => { const translationsJsonDir = path.join(__dirname, "..", "src", "js", "built-temp");
return gulp
.src(path.join(translationsSourceDir, "*.yaml")) function gulptasksTranslations($, gulp) {
.pipe($.plumber()) gulp.task("translations.convertToJson", () => {
.pipe(yaml({ space: 2, safe: true })) return gulp
.pipe(gulp.dest(translationsJsonDir)); .src(path.join(translationsSourceDir, "*.yaml"))
}); .pipe($.plumber())
.pipe(gulpYaml({ space: 2, safe: true }))
gulp.task("translations.fullBuild", gulp.series("translations.convertToJson")); .pipe(gulp.dest(translationsJsonDir));
} });
module.exports = { gulp.task("translations.fullBuild", gulp.series("translations.convertToJson"));
gulptasksTranslations,
}; gulp.task("translations.prepareSteamPage", cb => {
const files = fs.readdirSync(translationsSourceDir);
files
.filter(name => name.endsWith(".yaml"))
.forEach(fname => {
const languageName = fname.replace(".yaml", "");
const abspath = path.join(translationsSourceDir, fname);
const destpath = path.join(translationsSourceDir, "tmp", languageName + "-store.txt");
const contents = fs.readFileSync(abspath, { encoding: "utf-8" });
const data = YAML.parse(contents);
const storePage = data.steamPage;
const content = `
[img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img]
${storePage.intro.replace(/\n/gi, "\n\n")}
[h2]${storePage.title_advantages}[/h2]
[list]
${storePage.advantages
.map(x => "[*] " + x.replace(/<b>/, "[b]").replace(/<\/b>/, "[/b]"))
.join("\n")}
[/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")), {
encoding: "utf-8",
});
});
cb();
});
}
module.exports = {
gulptasksTranslations,
};

View File

@ -8198,6 +8198,11 @@ min-document@^2.19.0:
dependencies: dependencies:
dom-walk "^0.1.0" dom-walk "^0.1.0"
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@ -11945,6 +11950,13 @@ strip-indent@^1.0.1:
dependencies: dependencies:
get-stdin "^4.0.1" get-stdin "^4.0.1"
strip-indent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
dependencies:
min-indent "^1.0.0"
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"

View File

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

View File

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

View File

@ -1,39 +1,40 @@
#ingame_HUD_BlueprintPlacer { #ingame_HUD_BlueprintPlacer {
position: absolute; position: absolute;
@include S(top, 50px); @include S(top, 70px);
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
color: #333; color: #333;
z-index: 9999; z-index: 9999;
background: $ingameHudBg; background: $ingameHudBg;
@include S(padding, 5px); @include S(padding, 5px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: #fff; color: #fff;
@include S(width, 120px); @include S(width, 120px);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@include S(border-radius, $globalBorderRadius);
.label {
@include PlainText; .label {
text-transform: uppercase; @include PlainText;
} text-transform: uppercase;
.costContainer { }
display: flex; .costContainer {
align-items: center; display: flex;
@include Heading; align-items: center;
@include Heading;
> canvas {
@include S(margin-left, 5px); > canvas {
@include S(width, 30px); @include S(margin-left, 5px);
@include S(height, 30px); @include S(width, 30px);
} @include S(height, 30px);
} }
}
&:not(.canAfford) {
background: rgba(98, 27, 41, 0.8); &:not(.canAfford) {
// .costContainer { background: rgba(98, 27, 41, 0.8);
color: rgb(255, 97, 128); // .costContainer {
// } color: rgb(255, 97, 128);
} // }
} }
}

View File

@ -27,7 +27,7 @@
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
@include DarkThemeOverride { @include DarkThemeOverride {
background-color: rgba(darken($darkModeGameBackground, 15), 0.4); background-color: rgba(darken($darkModeGameBackground, 15), 0.95);
} }
&.secondary { &.secondary {

View File

@ -0,0 +1,23 @@
#ingame_HUD_CatMemes {
position: absolute;
@include S(width, 150px);
@include S(height, 150px);
background: transparent center center / contain no-repeat;
right: 0;
@include S(bottom, 150px);
& {
/* @load-async */
background-image: uiResource("res/ui/memes/cat1.png") !important;
}
@include InlineAnimation(0.5s ease-in-out) {
0% {
transform: translateX(100%);
}
100% {
transform: none;
}
}
}

View File

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

View File

@ -17,13 +17,10 @@
grid-template-rows: 1fr 1fr; grid-template-rows: 1fr 1fr;
@include S(margin-bottom, 4px); @include S(margin-bottom, 4px);
color: #333438; color: #333438;
// text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2);
&.unpinable { &.removable {
> canvas { cursor: pointer;
cursor: pointer; pointer-events: all;
pointer-events: all;
}
} }
> canvas { > canvas {
@ -31,16 +28,9 @@
@include S(height, 25px); @include S(height, 25px);
grid-column: 1 / 2; grid-column: 1 / 2;
grid-row: 1 / 3; grid-row: 1 / 3;
pointer-events: all; pointer-events: none;
transition: transform 0.1s ease-in-out;
transform-origin: D(2px) center;
will-change: transform;
position: relative;
z-index: 20; z-index: 20;
&:hover { position: relative;
transform: scale(2);
z-index: 21;
}
} }
> .amountLabel, > .amountLabel,

View File

@ -0,0 +1,169 @@
#ingame_HUD_StandaloneAdvantages {
.content {
@include S(width, 440px);
@include S(min-height, 300px);
}
p {
@include PlainText;
}
.points {
display: grid;
grid-template-columns: 1fr 1fr;
@include S(grid-column-gap, 10px);
@include S(grid-row-gap, 20px);
@include S(margin, 10px, 0, 20px);
grid-template-rows: #{D(40px)};
align-items: center;
}
.lowerBar {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
> button {
transition: opacity 0.12s ease-in-out;
&:hover {
opacity: 0.85;
}
}
.otherCloseButton {
@include SuperSmallText;
@include S(margin-right, 30px);
color: #aaa;
@include S(margin, 0);
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include InlineAnimation(5s ease-in-out) {
0% {
opacity: 0.05;
}
50% {
opacity: 0.05;
}
100% {
opacity: 1;
}
}
}
.steamLinkButton {
@include IncreasedClickArea(5px);
@include S(margin, 0);
@include S(width, 180px);
@include S(height, 40px);
background: #171a23 center center / contain no-repeat;
@include S(border-radius, $globalBorderRadius);
}
}
.point {
display: grid;
grid-template-columns: #{D(55px)} auto;
grid-template-rows: 1fr 1fr;
> strong {
grid-column: 2 / 3;
grid-row: 1 / 2;
@include PlainText;
text-transform: uppercase;
font-weight: bold;
}
> p {
grid-column: 2 / 3;
grid-row: 2 / 3;
@include SuperSmallText;
opacity: 0.8;
}
background: transparent #{D(10px)} center / #{D(30px)} no-repeat;
&.levels {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_new_levels.png");
}
> strong {
color: #f13555;
}
}
&.upgrades {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_upgrades.png");
}
> strong {
color: #8a00ff;
}
}
&.buildings {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_buildings.png");
}
> strong {
color: #3fce8b;
}
}
&.wires {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_wires.png");
}
> strong {
color: #ef2fdb;
}
}
&.markers {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_markers.png");
}
> strong {
color: #4294ff;
}
}
&.savegames {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_savegames.png");
}
> strong {
color: #ff9500;
}
}
&.darkmode {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_dark_mode.png");
}
> strong {
color: #292c32;
}
}
&.support {
& {
/* @load-async */
background-image: uiResource("res/ui/icons/advantage_support.png");
}
> strong {
color: #e72d2d;
}
}
}
}

View File

@ -1,22 +1,85 @@
#ingame_HUD_Watermark { #ingame_HUD_Watermark {
position: absolute; position: absolute;
& {
/* @load-async */
background: uiResource("get_on_steam.png") center center / contain no-repeat;
}
@include S(width, 110px); @include S(border-radius, $globalBorderRadius);
@include S(height, 40px); @include S(top, 70px);
@include S(top, 10px);
pointer-events: all; pointer-events: all;
cursor: pointer; cursor: pointer;
@include S(left, 160px); left: 50%;
text-align: center;
background: rgba(207, 65, 65, 0.8);
color: #fff;
transform: translateX(-50%);
@include PlainText;
@include S(padding, 10px);
transition: all 0.12s ease-in;
transition-property: opacity, transform;
transform: skewX(-0.5deg);
&:hover { &:hover {
transform: skewX(-1deg) scale(1.02); transform: translateX(-50%) scale(1.02) !important;
opacity: 0.9; }
> strong {
@include PlainText;
text-transform: uppercase;
}
> p {
@include SuperSmallText;
opacity: 0.7;
}
opacity: 0;
&.visible {
@include InlineAnimation(0.5s ease-in-out) {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
opacity: 1;
}
&:not(.visible) {
@include InlineAnimation(0.5s ease-in-out) {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
}
#ingame_HUD_WatermarkClicker {
@include S(top, 55px);
position: absolute;
left: 50%;
transform: translateX(-50%) !important;
@include SuperSmallText;
color: $colorBlueBright;
text-transform: uppercase;
pointer-events: all;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
opacity: 0.9;
}
&::after {
@include S(margin-left, 4px);
content: "";
@include S(width, 10px);
@include S(height, 10px);
display: inline-flex;
background: center center / contain no-repeat;
& {
/* @load-async */
background-image: uiResource("res/ui/icons/demo_steam_link_indicator.png");
}
} }
} }

View File

@ -52,6 +52,8 @@
@import "ingame_hud/color_blind_helper"; @import "ingame_hud/color_blind_helper";
@import "ingame_hud/shape_viewer"; @import "ingame_hud/shape_viewer";
@import "ingame_hud/sandbox_controller"; @import "ingame_hud/sandbox_controller";
@import "ingame_hud/standalone_advantages";
@import "ingame_hud/cat_memes";
// prettier-ignore // prettier-ignore
$elements: $elements:
@ -71,12 +73,13 @@ ingame_HUD_KeybindingOverlay,
ingame_HUD_Notifications, ingame_HUD_Notifications,
ingame_HUD_DebugInfo, ingame_HUD_DebugInfo,
ingame_HUD_EntityDebugger, ingame_HUD_EntityDebugger,
ingame_HUD_InteractiveTutorial,
ingame_HUD_TutorialHints, ingame_HUD_TutorialHints,
ingame_HUD_buildings_toolbar, ingame_HUD_InteractiveTutorial,
ingame_HUD_BuildingsToolbar,
ingame_HUD_wires_toolbar, ingame_HUD_wires_toolbar,
ingame_HUD_BlueprintPlacer, ingame_HUD_BlueprintPlacer,
ingame_HUD_Waypoints_Hint, ingame_HUD_Waypoints_Hint,
ingame_HUD_WatermarkClicker,
ingame_HUD_Watermark, ingame_HUD_Watermark,
ingame_HUD_ColorBlindBelowTileHelper, ingame_HUD_ColorBlindBelowTileHelper,
ingame_HUD_SandboxController, ingame_HUD_SandboxController,
@ -88,9 +91,11 @@ ingame_HUD_BetaOverlay,
ingame_HUD_Shop, ingame_HUD_Shop,
ingame_HUD_Statistics, ingame_HUD_Statistics,
ingame_HUD_ShapeViewer, ingame_HUD_ShapeViewer,
ingame_HUD_StandaloneAdvantages,
ingame_HUD_UnlockNotification, ingame_HUD_UnlockNotification,
ingame_HUD_SettingsMenu, ingame_HUD_SettingsMenu,
ingame_HUD_ModalDialogs; ingame_HUD_ModalDialogs,
ingame_HUD_CatMemes;
$zindex: 100; $zindex: 100;

View File

@ -1,6 +1,6 @@
$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire, $buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire,
constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage, constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage,
transistor, analyzer, comparator; transistor, analyzer, comparator, item_producer;
@each $building in $buildings { @each $building in $buildings {
[data-icon="building_icons/#{$building}.png"] { [data-icon="building_icons/#{$building}.png"] {
@ -12,8 +12,8 @@ $buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, tra
$buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2, miner, miner-chainable, $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2, miner, miner-chainable,
cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage, cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage,
reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not, reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not,
logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, item_producer,
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored; virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator;
@each $building in $buildingsAndVariants { @each $building in $buildingsAndVariants {
[data-icon="building_tutorials/#{$building}.png"] { [data-icon="building_tutorials/#{$building}.png"] {
/* @load-async */ /* @load-async */
@ -75,3 +75,17 @@ $languages: en, de, cs, da, et, es-419, fr, it, pt-BR, sv, tr, el, ru, uk, zh-TW
background-image: uiResource("languages/#{$language}.svg") !important; background-image: uiResource("languages/#{$language}.svg") !important;
} }
} }
/*
PRICE
*/
.steam_1_pr {
/* @load-async */
background-image: uiResource("get_on_steam_with_price.png") !important;
}
.steam_2_npr {
/* @load-async */
background-image: uiResource("get_on_steam.png") !important;
}

View File

@ -133,10 +133,7 @@
width: 100%; width: 100%;
@include S(height, 40px); @include S(height, 40px);
@include S(width, 180px); @include S(width, 180px);
& { background: #171a23 center center / contain no-repeat;
/* @load-async */
background: #171a23 uiResource("get_on_steam.png") center center / contain no-repeat;
}
overflow: hidden; overflow: hidden;
display: block; display: block;
text-indent: -999em; text-indent: -999em;

View File

@ -29,6 +29,7 @@ import { MobileWarningState } from "./states/mobile_warning";
import { PreloadState } from "./states/preload"; import { PreloadState } from "./states/preload";
import { SettingsState } from "./states/settings"; import { SettingsState } from "./states/settings";
import { ShapezGameAnalytics } from "./platform/browser/game_analytics"; import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
import { RestrictionManager } from "./core/restriction_manager";
/** /**
* @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface * @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface
@ -70,6 +71,9 @@ export class Application {
this.inputMgr = new InputDistributor(this); this.inputMgr = new InputDistributor(this);
this.backgroundResourceLoader = new BackgroundResourcesLoader(this); this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
// Restrictions (Like demo etc)
this.restrictionMgr = new RestrictionManager(this);
// Platform dependent stuff // Platform dependent stuff
/** @type {StorageInterface} */ /** @type {StorageInterface} */

View File

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

View File

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

View File

@ -1,467 +1,465 @@
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { Signal } from "../core/signal"; import { Signal } from "../core/signal";
import { fastArrayDelete, fastArrayDeleteValueIfContained } from "./utils"; import { fastArrayDelete, fastArrayDeleteValueIfContained } from "./utils";
import { Vector } from "./vector"; import { Vector } from "./vector";
import { IS_MOBILE, SUPPORT_TOUCH } from "./config"; import { IS_MOBILE, SUPPORT_TOUCH } from "./config";
import { SOUNDS } from "../platform/sound"; import { SOUNDS } from "../platform/sound";
import { GLOBAL_APP } from "./globals"; import { GLOBAL_APP } from "./globals";
const logger = createLogger("click_detector"); const logger = createLogger("click_detector");
export const MAX_MOVE_DISTANCE_PX = IS_MOBILE ? 20 : 80; export const MAX_MOVE_DISTANCE_PX = IS_MOBILE ? 20 : 80;
// For debugging // For debugging
const registerClickDetectors = G_IS_DEV && true; const registerClickDetectors = G_IS_DEV && true;
if (registerClickDetectors) { if (registerClickDetectors) {
/** @type {Array<ClickDetector>} */ /** @type {Array<ClickDetector>} */
window.activeClickDetectors = []; window.activeClickDetectors = [];
} }
// Store active click detectors so we can cancel them // Store active click detectors so we can cancel them
/** @type {Array<ClickDetector>} */ /** @type {Array<ClickDetector>} */
const ongoingClickDetectors = []; const ongoingClickDetectors = [];
// Store when the last touch event was registered, to avoid accepting a touch *and* a click event // Store when the last touch event was registered, to avoid accepting a touch *and* a click event
export let clickDetectorGlobals = { export let clickDetectorGlobals = {
lastTouchTime: -1000, lastTouchTime: -1000,
}; };
/** /**
* Click detector creation payload typehints * Click detector creation payload typehints
* @typedef {{ * @typedef {{
* consumeEvents?: boolean, * consumeEvents?: boolean,
* preventDefault?: boolean, * preventDefault?: boolean,
* applyCssClass?: string, * applyCssClass?: string,
* captureTouchmove?: boolean, * captureTouchmove?: boolean,
* targetOnly?: boolean, * targetOnly?: boolean,
* maxDistance?: number, * maxDistance?: number,
* clickSound?: string, * clickSound?: string,
* preventClick?: boolean, * preventClick?: boolean,
* }} ClickDetectorConstructorArgs * }} ClickDetectorConstructorArgs
*/ */
// Detects clicks // Detects clicks
export class ClickDetector { export class ClickDetector {
/** /**
* *
* @param {Element} element * @param {Element} element
* @param {object} param1 * @param {object} param1
* @param {boolean=} param1.consumeEvents Whether to call stopPropagation * @param {boolean=} param1.consumeEvents Whether to call stopPropagation
* (Useful for nested elements where the parent has a click handler as wel) * (Useful for nested elements where the parent has a click handler as wel)
* @param {boolean=} param1.preventDefault Whether to call preventDefault (Usually makes the handler faster) * @param {boolean=} param1.preventDefault Whether to call preventDefault (Usually makes the handler faster)
* @param {string=} param1.applyCssClass The css class to add while the element is pressed * @param {string=} param1.applyCssClass The css class to add while the element is pressed
* @param {boolean=} param1.captureTouchmove Whether to capture touchmove events as well * @param {boolean=} param1.captureTouchmove Whether to capture touchmove events as well
* @param {boolean=} param1.targetOnly Whether to also accept clicks on child elements (e.target !== element) * @param {boolean=} param1.targetOnly Whether to also accept clicks on child elements (e.target !== element)
* @param {number=} param1.maxDistance The maximum distance in pixels to accept clicks * @param {number=} param1.maxDistance The maximum distance in pixels to accept clicks
* @param {string=} param1.clickSound Sound key to play on touchdown * @param {string=} param1.clickSound Sound key to play on touchdown
* @param {boolean=} param1.preventClick Whether to prevent click events * @param {boolean=} param1.preventClick Whether to prevent click events
*/ */
constructor( constructor(
element, element,
{ {
consumeEvents = false, consumeEvents = false,
preventDefault = true, preventDefault = true,
applyCssClass = "pressed", applyCssClass = "pressed",
captureTouchmove = false, captureTouchmove = false,
targetOnly = false, targetOnly = false,
maxDistance = MAX_MOVE_DISTANCE_PX, maxDistance = MAX_MOVE_DISTANCE_PX,
clickSound = SOUNDS.uiClick, clickSound = SOUNDS.uiClick,
preventClick = false, preventClick = false,
} }
) { ) {
assert(element, "No element given!"); assert(element, "No element given!");
this.clickDownPosition = null; this.clickDownPosition = null;
this.consumeEvents = consumeEvents; this.consumeEvents = consumeEvents;
this.preventDefault = preventDefault; this.preventDefault = preventDefault;
this.applyCssClass = applyCssClass; this.applyCssClass = applyCssClass;
this.captureTouchmove = captureTouchmove; this.captureTouchmove = captureTouchmove;
this.targetOnly = targetOnly; this.targetOnly = targetOnly;
this.clickSound = clickSound; this.clickSound = clickSound;
this.maxDistance = maxDistance; this.maxDistance = maxDistance;
this.preventClick = preventClick; this.preventClick = preventClick;
// Signals // Signals
this.click = new Signal(); this.click = new Signal();
this.rightClick = new Signal(); this.rightClick = new Signal();
this.touchstart = new Signal(); this.touchstart = new Signal();
this.touchmove = new Signal(); this.touchmove = new Signal();
this.touchend = new Signal(); this.touchend = new Signal();
this.touchcancel = new Signal(); this.touchcancel = new Signal();
// Simple signals which just receive the touch position // Simple signals which just receive the touch position
this.touchstartSimple = new Signal(); this.touchstartSimple = new Signal();
this.touchmoveSimple = new Signal(); this.touchmoveSimple = new Signal();
this.touchendSimple = new Signal(); this.touchendSimple = new Signal();
// Store time of touch start // Store time of touch start
this.clickStartTime = null; this.clickStartTime = null;
// A click can be cancelled if another detector registers a click // A click can be cancelled if another detector registers a click
this.cancelled = false; this.cancelled = false;
this.internalBindTo(/** @type {HTMLElement} */ (element)); this.internalBindTo(/** @type {HTMLElement} */ (element));
} }
/** /**
* Cleans up all event listeners of this detector * Cleans up all event listeners of this detector
*/ */
cleanup() { cleanup() {
if (this.element) { if (this.element) {
if (registerClickDetectors) { if (registerClickDetectors) {
const index = window.activeClickDetectors.indexOf(this); const index = window.activeClickDetectors.indexOf(this);
if (index < 0) { if (index < 0) {
logger.error("Click detector cleanup but is not active"); logger.error("Click detector cleanup but is not active");
} else { } else {
window.activeClickDetectors.splice(index, 1); window.activeClickDetectors.splice(index, 1);
} }
} }
const options = this.internalGetEventListenerOptions(); const options = this.internalGetEventListenerOptions();
if (SUPPORT_TOUCH) { if (SUPPORT_TOUCH) {
this.element.removeEventListener("touchstart", this.handlerTouchStart, options); this.element.removeEventListener("touchstart", this.handlerTouchStart, options);
this.element.removeEventListener("touchend", this.handlerTouchEnd, options); this.element.removeEventListener("touchend", this.handlerTouchEnd, options);
this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options); this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options);
} }
this.element.removeEventListener("mouseup", this.handlerTouchStart, options); this.element.removeEventListener("mouseup", this.handlerTouchStart, options);
this.element.removeEventListener("mousedown", this.handlerTouchEnd, options); this.element.removeEventListener("mousedown", this.handlerTouchEnd, options);
this.element.removeEventListener("mouseout", this.handlerTouchCancel, options); this.element.removeEventListener("mouseout", this.handlerTouchCancel, options);
if (this.captureTouchmove) { if (this.captureTouchmove) {
if (SUPPORT_TOUCH) { if (SUPPORT_TOUCH) {
this.element.removeEventListener("touchmove", this.handlerTouchMove, options); this.element.removeEventListener("touchmove", this.handlerTouchMove, options);
} }
this.element.removeEventListener("mousemove", this.handlerTouchMove, options); this.element.removeEventListener("mousemove", this.handlerTouchMove, options);
} }
if (this.preventClick) { if (this.preventClick) {
this.element.removeEventListener("click", this.handlerPreventClick, options); this.element.removeEventListener("click", this.handlerPreventClick, options);
} }
this.click.removeAll(); this.click.removeAll();
this.touchstart.removeAll(); this.touchstart.removeAll();
this.touchmove.removeAll(); this.touchmove.removeAll();
this.touchend.removeAll(); this.touchend.removeAll();
this.touchcancel.removeAll(); this.touchcancel.removeAll();
// TODO: Remove pointer captures this.element = null;
}
this.element = null; }
}
} // INTERNAL METHODS
// INTERNAL METHODS /**
*
/** * @param {Event} event
* */
* @param {Event} event internalPreventClick(event) {
*/ window.focus();
internalPreventClick(event) { event.preventDefault();
window.focus(); }
event.preventDefault();
} /**
* Internal method to get the options to pass to an event listener
/** */
* Internal method to get the options to pass to an event listener internalGetEventListenerOptions() {
*/ return {
internalGetEventListenerOptions() { capture: this.consumeEvents,
return { passive: !this.preventDefault,
capture: this.consumeEvents, };
passive: !this.preventDefault, }
};
} /**
* Binds the click detector to an element
/** * @param {HTMLElement} element
* Binds the click detector to an element */
* @param {HTMLElement} element internalBindTo(element) {
*/ const options = this.internalGetEventListenerOptions();
internalBindTo(element) {
const options = this.internalGetEventListenerOptions(); this.handlerTouchStart = this.internalOnPointerDown.bind(this);
this.handlerTouchEnd = this.internalOnPointerEnd.bind(this);
this.handlerTouchStart = this.internalOnPointerDown.bind(this); this.handlerTouchMove = this.internalOnPointerMove.bind(this);
this.handlerTouchEnd = this.internalOnPointerEnd.bind(this); this.handlerTouchCancel = this.internalOnTouchCancel.bind(this);
this.handlerTouchMove = this.internalOnPointerMove.bind(this);
this.handlerTouchCancel = this.internalOnTouchCancel.bind(this); if (this.preventClick) {
this.handlerPreventClick = this.internalPreventClick.bind(this);
if (this.preventClick) { element.addEventListener("click", this.handlerPreventClick, options);
this.handlerPreventClick = this.internalPreventClick.bind(this); }
element.addEventListener("click", this.handlerPreventClick, options);
} if (SUPPORT_TOUCH) {
element.addEventListener("touchstart", this.handlerTouchStart, options);
if (SUPPORT_TOUCH) { element.addEventListener("touchend", this.handlerTouchEnd, options);
element.addEventListener("touchstart", this.handlerTouchStart, options); element.addEventListener("touchcancel", this.handlerTouchCancel, options);
element.addEventListener("touchend", this.handlerTouchEnd, options); }
element.addEventListener("touchcancel", this.handlerTouchCancel, options);
} element.addEventListener("mousedown", this.handlerTouchStart, options);
element.addEventListener("mouseup", this.handlerTouchEnd, options);
element.addEventListener("mousedown", this.handlerTouchStart, options); element.addEventListener("mouseout", this.handlerTouchCancel, options);
element.addEventListener("mouseup", this.handlerTouchEnd, options);
element.addEventListener("mouseout", this.handlerTouchCancel, options); if (this.captureTouchmove) {
if (SUPPORT_TOUCH) {
if (this.captureTouchmove) { element.addEventListener("touchmove", this.handlerTouchMove, options);
if (SUPPORT_TOUCH) { }
element.addEventListener("touchmove", this.handlerTouchMove, options); element.addEventListener("mousemove", this.handlerTouchMove, options);
} }
element.addEventListener("mousemove", this.handlerTouchMove, options);
} if (registerClickDetectors) {
window.activeClickDetectors.push(this);
if (registerClickDetectors) { }
window.activeClickDetectors.push(this); this.element = element;
} }
this.element = element;
} /**
* Returns if the bound element is currently in the DOM.
/** */
* Returns if the bound element is currently in the DOM. internalIsDomElementAttached() {
*/ return this.element && document.documentElement.contains(this.element);
internalIsDomElementAttached() { }
return this.element && document.documentElement.contains(this.element);
} /**
* Checks if the given event is relevant for this detector
/** * @param {TouchEvent|MouseEvent} event
* Checks if the given event is relevant for this detector */
* @param {TouchEvent|MouseEvent} event internalEventPreHandler(event, expectedRemainingTouches = 1) {
*/ if (!this.element) {
internalEventPreHandler(event, expectedRemainingTouches = 1) { // Already cleaned up
if (!this.element) { return false;
// Already cleaned up }
return false;
} if (this.targetOnly && event.target !== this.element) {
// Clicked a child element
if (this.targetOnly && event.target !== this.element) { return false;
// Clicked a child element }
return false;
} // Stop any propagation and defaults if configured
if (this.consumeEvents && event.cancelable) {
// Stop any propagation and defaults if configured event.stopPropagation();
if (this.consumeEvents && event.cancelable) { }
event.stopPropagation();
} if (this.preventDefault && event.cancelable) {
event.preventDefault();
if (this.preventDefault && event.cancelable) { }
event.preventDefault();
} if (window.TouchEvent && event instanceof TouchEvent) {
clickDetectorGlobals.lastTouchTime = performance.now();
if (window.TouchEvent && event instanceof TouchEvent) {
clickDetectorGlobals.lastTouchTime = performance.now(); // console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches);
if (event.targetTouches.length !== expectedRemainingTouches) {
// console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches); return false;
if (event.targetTouches.length !== expectedRemainingTouches) { }
return false; }
}
} if (event instanceof MouseEvent) {
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
if (event instanceof MouseEvent) { return false;
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) { }
return false; }
}
} return true;
}
return true;
} /**
* Extracts the mous position from an event
/** * @param {TouchEvent|MouseEvent} event
* Extracts the mous position from an event * @returns {Vector} The client space position
* @param {TouchEvent|MouseEvent} event */
* @returns {Vector} The client space position static extractPointerPosition(event) {
*/ if (window.TouchEvent && event instanceof TouchEvent) {
static extractPointerPosition(event) { if (event.changedTouches.length !== 1) {
if (window.TouchEvent && event instanceof TouchEvent) { logger.warn(
if (event.changedTouches.length !== 1) { "Got unexpected target touches:",
logger.warn( event.targetTouches.length,
"Got unexpected target touches:", "->",
event.targetTouches.length, event.targetTouches
"->", );
event.targetTouches return new Vector(0, 0);
); }
return new Vector(0, 0);
} const touch = event.changedTouches[0];
return new Vector(touch.clientX, touch.clientY);
const touch = event.changedTouches[0]; }
return new Vector(touch.clientX, touch.clientY);
} if (event instanceof MouseEvent) {
return new Vector(event.clientX, event.clientY);
if (event instanceof MouseEvent) { }
return new Vector(event.clientX, event.clientY);
} assertAlways(false, "Got unknown event: " + event);
assertAlways(false, "Got unknown event: " + event); return new Vector(0, 0);
}
return new Vector(0, 0);
} /**
* Cacnels all ongoing events on this detector
/** */
* Cacnels all ongoing events on this detector cancelOngoingEvents() {
*/ if (this.applyCssClass && this.element) {
cancelOngoingEvents() { this.element.classList.remove(this.applyCssClass);
if (this.applyCssClass && this.element) { }
this.element.classList.remove(this.applyCssClass); this.clickDownPosition = null;
} this.clickStartTime = null;
this.clickDownPosition = null; this.cancelled = true;
this.clickStartTime = null; fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
this.cancelled = true; }
fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
} /**
* Internal pointer down handler
/** * @param {TouchEvent|MouseEvent} event
* Internal pointer down handler */
* @param {TouchEvent|MouseEvent} event internalOnPointerDown(event) {
*/ window.focus();
internalOnPointerDown(event) {
window.focus(); if (!this.internalEventPreHandler(event, 1)) {
return false;
if (!this.internalEventPreHandler(event, 1)) { }
return false;
} const position = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
const position = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event); if (event instanceof MouseEvent) {
const isRightClick = event.button === 2;
if (event instanceof MouseEvent) { if (isRightClick) {
const isRightClick = event.button === 2; // Ignore right clicks
if (isRightClick) { this.rightClick.dispatch(position, event);
// Ignore right clicks this.cancelled = true;
this.rightClick.dispatch(position, event); this.clickDownPosition = null;
this.cancelled = true; return;
this.clickDownPosition = null; }
return; }
}
} if (this.clickDownPosition) {
logger.warn("Ignoring double click");
if (this.clickDownPosition) { return false;
logger.warn("Ignoring double click"); }
return false;
} this.cancelled = false;
this.touchstart.dispatch(event);
this.cancelled = false;
this.touchstart.dispatch(event); // Store where the touch started
this.clickDownPosition = position;
// Store where the touch started this.clickStartTime = performance.now();
this.clickDownPosition = position; this.touchstartSimple.dispatch(this.clickDownPosition.x, this.clickDownPosition.y);
this.clickStartTime = performance.now();
this.touchstartSimple.dispatch(this.clickDownPosition.x, this.clickDownPosition.y); // If we are not currently within a click, register it
if (ongoingClickDetectors.indexOf(this) < 0) {
// If we are not currently within a click, register it ongoingClickDetectors.push(this);
if (ongoingClickDetectors.indexOf(this) < 0) { } else {
ongoingClickDetectors.push(this); logger.warn("Click detector got pointer down of active pointer twice");
} else { }
logger.warn("Click detector got pointer down of active pointer twice");
} // If we should apply any classes, do this now
if (this.applyCssClass) {
// If we should apply any classes, do this now this.element.classList.add(this.applyCssClass);
if (this.applyCssClass) { }
this.element.classList.add(this.applyCssClass);
} // If we should play any sound, do this
if (this.clickSound) {
// If we should play any sound, do this GLOBAL_APP.sound.playUiSound(this.clickSound);
if (this.clickSound) { }
GLOBAL_APP.sound.playUiSound(this.clickSound);
} return false;
}
return false;
} /**
* Internal pointer move handler
/** * @param {TouchEvent|MouseEvent} event
* Internal pointer move handler */
* @param {TouchEvent|MouseEvent} event internalOnPointerMove(event) {
*/ if (!this.internalEventPreHandler(event, 1)) {
internalOnPointerMove(event) { return false;
if (!this.internalEventPreHandler(event, 1)) { }
return false; this.touchmove.dispatch(event);
} const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
this.touchmove.dispatch(event); this.touchmoveSimple.dispatch(pos.x, pos.y);
const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event); return false;
this.touchmoveSimple.dispatch(pos.x, pos.y); }
return false;
} /**
* Internal pointer end handler
/** * @param {TouchEvent|MouseEvent} event
* Internal pointer end handler */
* @param {TouchEvent|MouseEvent} event internalOnPointerEnd(event) {
*/ window.focus();
internalOnPointerEnd(event) {
window.focus(); if (!this.internalEventPreHandler(event, 0)) {
return false;
if (!this.internalEventPreHandler(event, 0)) { }
return false;
} if (this.cancelled) {
// warn(this, "Not dispatching touchend on cancelled listener");
if (this.cancelled) { return false;
// warn(this, "Not dispatching touchend on cancelled listener"); }
return false;
} if (event instanceof MouseEvent) {
const isRightClick = event.button === 2;
if (event instanceof MouseEvent) { if (isRightClick) {
const isRightClick = event.button === 2; return;
if (isRightClick) { }
return; }
}
} const index = ongoingClickDetectors.indexOf(this);
if (index < 0) {
const index = ongoingClickDetectors.indexOf(this); logger.warn("Got pointer end but click detector is not in pressed state");
if (index < 0) { } else {
logger.warn("Got pointer end but click detector is not in pressed state"); fastArrayDelete(ongoingClickDetectors, index);
} else { }
fastArrayDelete(ongoingClickDetectors, index);
} let dispatchClick = false;
let dispatchClickPos = null;
let dispatchClick = false;
let dispatchClickPos = null; // Check for correct down position, otherwise must have pinched or so
if (this.clickDownPosition) {
// Check for correct down position, otherwise must have pinched or so const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
if (this.clickDownPosition) { const distance = pos.distance(this.clickDownPosition);
const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event); if (!IS_MOBILE || distance <= this.maxDistance) {
const distance = pos.distance(this.clickDownPosition); dispatchClick = true;
if (!IS_MOBILE || distance <= this.maxDistance) { dispatchClickPos = pos;
dispatchClick = true; } else {
dispatchClickPos = pos; console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")");
} else { }
console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")"); }
}
} this.clickDownPosition = null;
this.clickStartTime = null;
this.clickDownPosition = null;
this.clickStartTime = null; if (this.applyCssClass) {
this.element.classList.remove(this.applyCssClass);
if (this.applyCssClass) { }
this.element.classList.remove(this.applyCssClass);
} // Dispatch in the end to avoid the element getting invalidated
// Also make sure that the element is still in the dom
// Dispatch in the end to avoid the element getting invalidated if (this.internalIsDomElementAttached()) {
// Also make sure that the element is still in the dom this.touchend.dispatch(event);
if (this.internalIsDomElementAttached()) { this.touchendSimple.dispatch();
this.touchend.dispatch(event);
this.touchendSimple.dispatch(); if (dispatchClick) {
const detectors = ongoingClickDetectors.slice();
if (dispatchClick) { for (let i = 0; i < detectors.length; ++i) {
const detectors = ongoingClickDetectors.slice(); detectors[i].cancelOngoingEvents();
for (let i = 0; i < detectors.length; ++i) { }
detectors[i].cancelOngoingEvents(); this.click.dispatch(dispatchClickPos, event);
} }
this.click.dispatch(dispatchClickPos, event); }
} return false;
} }
return false;
} /**
* Internal touch cancel handler
/** * @param {TouchEvent|MouseEvent} event
* Internal touch cancel handler */
* @param {TouchEvent|MouseEvent} event internalOnTouchCancel(event) {
*/ if (!this.internalEventPreHandler(event, 0)) {
internalOnTouchCancel(event) { return false;
if (!this.internalEventPreHandler(event, 0)) { }
return false;
} if (this.cancelled) {
// warn(this, "Not dispatching touchcancel on cancelled listener");
if (this.cancelled) { return false;
// warn(this, "Not dispatching touchcancel on cancelled listener"); }
return false;
} this.cancelOngoingEvents();
this.touchcancel.dispatch(event);
this.cancelOngoingEvents(); this.touchendSimple.dispatch(event);
this.touchcancel.dispatch(event); return false;
this.touchendSimple.dispatch(event); }
return false; }
}
}

View File

@ -1,5 +1,3 @@
import { queryParamOptions } from "./query_parameters";
export const IS_DEBUG = export const IS_DEBUG =
G_IS_DEV && G_IS_DEV &&
typeof window !== "undefined" && typeof window !== "undefined" &&
@ -7,23 +5,23 @@ export const IS_DEBUG =
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) && (window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
window.location.search.indexOf("nodebug") < 0; window.location.search.indexOf("nodebug") < 0;
export const IS_DEMO = queryParamOptions.fullVersion
? false
: (!G_IS_DEV && !G_IS_STANDALONE) ||
(typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0);
export const SUPPORT_TOUCH = false; export const SUPPORT_TOUCH = false;
export const IS_MAC = navigator.platform.toLowerCase().indexOf("mac") >= 0;
const smoothCanvas = true; const smoothCanvas = true;
export const THIRDPARTY_URLS = { export const THIRDPARTY_URLS = {
discord: "https://discord.gg/HN7EVzV", discord: "https://discord.gg/HN7EVzV",
github: "https://github.com/tobspr/shapez.io", github: "https://github.com/tobspr/shapez.io",
reddit: "https://www.reddit.com/r/shapezio", reddit: "https://www.reddit.com/r/shapezio",
shapeViewer: "https://viewer.shapez.io",
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/", standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
}; };
export const A_B_TESTING_LINK_TYPE = Math.random() > 0.5 ? "steam_1_pr" : "steam_2_npr";
export const globalConfig = { export const globalConfig = {
// Size of a single tile in Pixels. // Size of a single tile in Pixels.
// NOTICE: Update webpack.production.config too! // NOTICE: Update webpack.production.config too!
@ -61,7 +59,7 @@ export const globalConfig = {
undergroundBeltMaxTilesByTier: [5, 9], undergroundBeltMaxTilesByTier: [5, 9],
readerAnalyzeIntervalSeconds: G_IS_DEV ? 3 : 10, readerAnalyzeIntervalSeconds: 10,
buildingSpeeds: { buildingSpeeds: {
cutter: 1 / 4, cutter: 1 / 4,
@ -134,3 +132,8 @@ if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
if (globalConfig.debug.fastGameEnter) { if (globalConfig.debug.fastGameEnter) {
globalConfig.debug.noArtificialDelays = true; globalConfig.debug.noArtificialDelays = true;
} }
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
globalConfig.warmupTimeSecondsFast = 0;
globalConfig.warmupTimeSecondsRegular = 0;
}

View File

@ -1,26 +1,25 @@
import { globalConfig } from "./config"; import { globalConfig } from "./config";
/** /**
* @typedef {import("../game/root").GameRoot} GameRoot * @typedef {import("../game/root").GameRoot} GameRoot
* @typedef {import("./rectangle").Rectangle} Rectangle * @typedef {import("./rectangle").Rectangle} Rectangle
*/ */
export class DrawParameters { export class DrawParameters {
constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) { constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) {
/** @type {CanvasRenderingContext2D} */ /** @type {CanvasRenderingContext2D} */
this.context = context; this.context = context;
/** @type {Rectangle} */ /** @type {Rectangle} */
this.visibleRect = visibleRect; this.visibleRect = visibleRect;
/** @type {string} */ /** @type {string} */
this.desiredAtlasScale = desiredAtlasScale; this.desiredAtlasScale = desiredAtlasScale;
/** @type {number} */ /** @type {number} */
this.zoomLevel = zoomLevel; this.zoomLevel = zoomLevel;
// FIXME: Not really nice /** @type {GameRoot} */
/** @type {GameRoot} */ this.root = root;
this.root = root; }
} }
}

View File

@ -13,6 +13,17 @@ import { getStringForKeyCode } from "../game/key_action_mapper";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { T } from "../translations"; import { T } from "../translations";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
const kbEnter = 13; const kbEnter = 13;
const kbCancel = 27; const kbCancel = 27;
@ -60,6 +71,8 @@ export class Dialog {
this.buttonSignals[buttonId] = new Signal(); this.buttonSignals[buttonId] = new Signal();
} }
this.valueChosen = new Signal();
this.timeouts = []; this.timeouts = [];
this.clickDetectors = []; this.clickDetectors = [];
@ -164,7 +177,7 @@ export class Dialog {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
button.classList.remove("timedButton"); button.classList.remove("timedButton");
arrayDeleteValue(this.timeouts, timeout); arrayDeleteValue(this.timeouts, timeout);
}, 5000); }, 3000);
this.timeouts.push(timeout); this.timeouts.push(timeout);
} }
if (isEnter || isEscape) { if (isEnter || isEscape) {
@ -431,10 +444,12 @@ export class DialogWithForm extends Dialog {
for (let i = 0; i < this.formElements.length; ++i) { for (let i = 0; i < this.formElements.length; ++i) {
const elem = this.formElements[i]; const elem = this.formElements[i];
elem.bindEvents(div, this.clickDetectors); elem.bindEvents(div, this.clickDetectors);
elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
} }
waitNextFrame().then(() => { waitNextFrame().then(() => {
this.formElements[0].focus(); this.formElements[this.formElements.length - 1].focus();
}); });
return div; return div;

View File

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

View File

@ -81,10 +81,6 @@ export class ReadWriteProxy {
return this.writeAsync(); return this.writeAsync();
} }
getCurrentData() {
return this.currentData;
}
/** /**
* *
* @param {object} obj * @param {object} obj
@ -173,7 +169,7 @@ export class ReadWriteProxy {
// Check for errors during read // Check for errors during read
.catch(err => { .catch(err => {
if (err === FILE_NOT_FOUND) { if (err === FILE_NOT_FOUND) {
logger.log("File not found, using default data"); logger.error("File not found, using default data");
// File not found or unreadable, assume default file // File not found or unreadable, assume default file
return Promise.resolve(null); return Promise.resolve(null);

View File

@ -0,0 +1,154 @@
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { IS_MAC } from "./config";
import { ExplainedResult } from "./explained_result";
import { queryParamOptions } from "./query_parameters";
import { ReadWriteProxy } from "./read_write_proxy";
export class RestrictionManager extends ReadWriteProxy {
/**
* @param {Application} app
*/
constructor(app) {
super(app, "restriction-flags.bin");
this.currentData = this.getDefaultData();
}
// -- RW Proxy Impl
/**
* @param {any} data
*/
verify(data) {
return ExplainedResult.good();
}
/**
*/
getDefaultData() {
return {
version: this.getCurrentVersion(),
savegameV1119Imported: false,
};
}
/**
*/
getCurrentVersion() {
return 1;
}
/**
* @param {any} data
*/
migrate(data) {
return ExplainedResult.good();
}
initialize() {
return this.readAsync().then(() => {
if (this.currentData.savegameV1119Imported) {
console.warn("Levelunlock is granted to current user due to past savegame");
}
});
}
// -- End RW Proxy Impl
/**
* Checks if there are any savegames from the 1.1.19 version
*/
onHasLegacySavegamesChanged(has119Savegames = false) {
if (has119Savegames && !this.currentData.savegameV1119Imported) {
this.currentData.savegameV1119Imported = true;
console.warn("Current user now has access to all levels due to 1119 savegame");
return this.writeAsync();
}
return Promise.resolve();
}
/**
* Returns if the app is currently running as the limited version
* @returns {boolean}
*/
isLimitedVersion() {
if (IS_MAC) {
// On mac, the full version is always active
return false;
}
if (G_IS_STANDALONE) {
// Standalone is never limited
return false;
}
if (queryParamOptions.fullVersion) {
// Full version is activated via flag
return false;
}
if (G_IS_DEV) {
return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0;
}
return true;
}
/**
* Returns if the app markets the standalone version on steam
* @returns {boolean}
*/
getIsStandaloneMarketingActive() {
return this.isLimitedVersion();
}
/**
* Returns if exporting the base as a screenshot is possible
* @returns {boolean}
*/
getIsExportingScreenshotsPossible() {
return !this.isLimitedVersion();
}
/**
* Returns the maximum number of supported waypoints
* @returns {number}
*/
getMaximumWaypoints() {
return this.isLimitedVersion() ? 2 : 1e20;
}
/**
* Returns if the user has unlimited savegames
* @returns {boolean}
*/
getHasUnlimitedSavegames() {
return !this.isLimitedVersion();
}
/**
* Returns if the app has all settings available
* @returns {boolean}
*/
getHasExtendedSettings() {
return !this.isLimitedVersion();
}
/**
* Returns if all upgrades are available
* @returns {boolean}
*/
getHasExtendedUpgrades() {
return !this.isLimitedVersion() || this.currentData.savegameV1119Imported;
}
/**
* Returns if all levels & freeplay is available
* @returns {boolean}
*/
getHasExtendedLevelsAndFreeplay() {
return !this.isLimitedVersion() || this.currentData.savegameV1119Imported;
}
}

View File

@ -108,17 +108,6 @@ export class RandomNumberGenerator {
assert(max > min, "rng: max <= min"); assert(max > min, "rng: max <= min");
return Math.floor(this.next() * (max - min) + min); return Math.floor(this.next() * (max - min) + min);
} }
/**
* @param {number} min
* @param {number} max
* @returns {number} Integer in range [min, max]
*/
nextIntRangeInclusive(min, max) {
assert(Number.isFinite(min), "Minimum is no integer");
assert(Number.isFinite(max), "Maximum is no integer");
assert(max > min, "rng: max <= min");
return Math.round(this.next() * (max - min) + min);
}
/** /**
* @param {number} min * @param {number} min

View File

@ -708,3 +708,83 @@ export function dirInterval(key, frames, object, premessage, ...args) {
} }
logIntervals[key] = interval; logIntervals[key] = interval;
} }
/**
* Fills in a <link> tag
* @param {string} translation
* @param {string} link
*/
export function fillInLinkIntoTranslation(translation, link) {
return translation
.replace("<link>", "<a href='" + link + "' target='_blank'>")
.replace("</link>", "</a>");
}
/**
* Generates a file download
* @param {string} filename
* @param {string} text
*/
export function generateFileDownload(filename, text) {
var element = document.createElement("a");
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/**
* Starts a file chooser
* @param {string} acceptedType
*/
export function startFileChoose(acceptedType = ".bin") {
var input = document.createElement("input");
input.type = "file";
input.accept = acceptedType;
return new Promise(resolve => {
input.onchange = _ => resolve(input.files[0]);
input.click();
});
}
const romanLiterals = [
"0", // NULL
"I",
"II",
"III",
"IV",
"V",
"VI",
"VII",
"VIII",
"IX",
"X",
"XI",
"XII",
"XIII",
"XIV",
"XV",
"XVI",
"XVII",
"XVIII",
"XIX",
"XX",
];
/**
*
* @param {number} number
* @returns {string}
*/
export function getRomanNumber(number) {
number = Math.max(0, Math.round(number));
if (number < romanLiterals.length) {
return romanLiterals[number];
}
return String(number);
}

View File

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

View File

@ -1112,7 +1112,7 @@ export class BeltPath extends BasicSerializableObject {
isFirstItemProcessed = false; isFirstItemProcessed = false;
this.spacingToFirstItem += clampedProgress; this.spacingToFirstItem += clampedProgress;
if (remainingVelocity < 0.01) { if (remainingVelocity < 1e-7) {
break; break;
} }
} }

View File

@ -1,14 +1,9 @@
import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters"; import { DrawParameters } from "../core/draw_parameters";
import { Loader } from "../core/loader"; import { findNiceIntegerValue } from "../core/utils";
import { createLogger } from "../core/logging";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
import { Entity } from "./entity"; import { Entity } from "./entity";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { findNiceIntegerValue } from "../core/utils";
import { blueprintShape } from "./upgrades";
import { globalConfig } from "../core/config";
const logger = createLogger("blueprint");
export class Blueprint { export class Blueprint {
/** /**
@ -174,7 +169,7 @@ export class Blueprint {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
canAfford(root) { canAfford(root) {
return root.hubGoals.getShapesStoredByKey(blueprintShape) >= this.getCost(); return root.hubGoals.getShapesStoredByKey(root.gameMode.getBlueprintShapeKey()) >= this.getCost();
} }
/** /**

View File

@ -41,7 +41,7 @@ const variantsCache = new Map();
export function registerBuildingVariant( export function registerBuildingVariant(
code, code,
meta, meta,
variant = "default" /* FIXME: Circular dependency, actually its defaultBuildingVariant */, variant = "default" /* @TODO: Circular dependency, actually its defaultBuildingVariant */,
rotationVariant = 0 rotationVariant = 0
) { ) {
assert(!gBuildingVariants[code], "Duplicate id: " + code); assert(!gBuildingVariants[code], "Duplicate id: " + code);

View File

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

View File

@ -7,6 +7,7 @@ import { BeltComponent } from "../components/belt";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building"; import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root"; import { GameRoot } from "../root";
import { THEME } from "../theme";
export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right]; export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right];
@ -22,7 +23,7 @@ export class MetaBeltBuilding extends MetaBuilding {
} }
getSilhouetteColor() { getSilhouetteColor() {
return "#777"; return THEME.map.chunkOverview.beltColor;
} }
getPlacementSound() { getPlacementSound() {

View File

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

View File

@ -23,7 +23,7 @@ export class MetaFilterBuilding extends MetaBuilding {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
getIsUnlocked(root) { getIsUnlocked(root) {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers); return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_filter);
} }
getDimensions() { getDimensions() {

View File

@ -0,0 +1,44 @@
import { enumDirection, Vector } from "../../core/vector";
import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProducerComponent } from "../components/item_producer";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
export class MetaItemProducerBuilding extends MetaBuilding {
constructor() {
super("item_producer");
}
getSilhouetteColor() {
return "#b37dcd";
}
getShowWiresLayerPreview() {
return true;
}
/**
* Creates the entity at the given location
* @param {Entity} entity
*/
setupEntityComponents(entity) {
entity.addComponent(
new ItemEjectorComponent({
slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }],
})
);
entity.addComponent(
new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
type: enumPinSlotType.logicalAcceptor,
direction: enumDirection.bottom,
},
],
})
);
entity.addComponent(new ItemProducerComponent());
}
}

View File

@ -20,7 +20,7 @@ export class MetaLeverBuilding extends MetaBuilding {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
getIsUnlocked(root) { getIsUnlocked(root) {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers); return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
} }
getDimensions() { getDimensions() {

View File

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

View File

@ -71,7 +71,7 @@ export class MetaPainterBuilding extends MetaBuilding {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) { if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) {
variants.push(enumPainterVariants.double); variants.push(enumPainterVariants.double);
} }
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_quad)) { if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers)) {
variants.push(enumPainterVariants.quad); variants.push(enumPainterVariants.quad);
} }
return variants; return variants;

View File

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

View File

@ -191,7 +191,6 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
) { ) {
tile = tile.addScalars(searchVector.x, searchVector.y); tile = tile.addScalars(searchVector.x, searchVector.y);
/* WIRES: FIXME */
const contents = root.map.getTileContent(tile, "regular"); const contents = root.map.getTileContent(tile, "regular");
if (contents) { if (contents) {
const undergroundComp = contents.components.UndergroundBelt; const undergroundComp = contents.components.UndergroundBelt;

View File

@ -4,6 +4,7 @@ import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root"; import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
import { MetaCutterBuilding } from "./cutter"; import { MetaCutterBuilding } from "./cutter";
import { MetaPainterBuilding } from "./painter"; import { MetaPainterBuilding } from "./painter";
import { MetaRotaterBuilding } from "./rotater"; import { MetaRotaterBuilding } from "./rotater";
@ -47,8 +48,7 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
getIsUnlocked(root) { getIsUnlocked(root) {
// @todo return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
return true;
} }
/** @returns {"wires"} **/ /** @returns {"wires"} **/

View File

@ -82,7 +82,7 @@ export class MetaWireBuilding extends MetaBuilding {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
getIsUnlocked(root) { getIsUnlocked(root) {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers); return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
} }
/** /**

View File

@ -21,7 +21,7 @@ export class MetaWireTunnelBuilding extends MetaBuilding {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
getIsUnlocked(root) { getIsUnlocked(root) {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers); return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
} }
/** /**
@ -53,6 +53,6 @@ export class MetaWireTunnelBuilding extends MetaBuilding {
* @param {Entity} entity * @param {Entity} entity
*/ */
setupEntityComponents(entity) { setupEntityComponents(entity) {
entity.addComponent(new WireTunnelComponent({})); entity.addComponent(new WireTunnelComponent());
} }
} }

View File

@ -353,7 +353,7 @@ export class Camera extends BasicSerializableObject {
.add(() => (this.desiredZoom = this.zoomLevel * 1.2)); .add(() => (this.desiredZoom = this.zoomLevel * 1.2));
mapper mapper
.getBinding(KEYMAPPINGS.navigation.mapZoomOut) .getBinding(KEYMAPPINGS.navigation.mapZoomOut)
.add(() => (this.desiredZoom = this.zoomLevel * 0.8)); .add(() => (this.desiredZoom = this.zoomLevel / 1.2));
mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add(() => this.centerOnMap()); mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add(() => this.centerOnMap());
} }
@ -502,16 +502,20 @@ export class Camera extends BasicSerializableObject {
} }
const prevZoom = this.zoomLevel; const prevZoom = this.zoomLevel;
const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity(); const scale = 1 + 0.15 * this.root.app.settings.getScrollWheelSensitivity();
assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY); assert(Number.isFinite(scale), "Got invalid scale in mouse wheel event: " + event.deltaY);
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel); assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel);
this.zoomLevel *= 1 + delta; this.zoomLevel *= event.deltaY < 0 ? scale : 1 / scale;
assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel); assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel);
this.clampZoomLevel(); this.clampZoomLevel();
this.desiredZoom = null; this.desiredZoom = null;
const mousePosition = this.root.app.mousePosition; let mousePosition = this.root.app.mousePosition;
if (!this.root.app.settings.getAllSettings().zoomToCursor) {
mousePosition = new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2);
}
if (mousePosition) { if (mousePosition) {
const worldPos = this.root.camera.screenToWorld(mousePosition); const worldPos = this.root.camera.screenToWorld(mousePosition);
const worldDelta = worldPos.sub(this.center); const worldDelta = worldPos.sub(this.center);
@ -939,6 +943,7 @@ export class Camera extends BasicSerializableObject {
this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade); this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade);
assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel); assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel);
} else { } else {
this.zoomLevel = this.desiredZoom;
this.desiredZoom = null; this.desiredZoom = null;
} }
} }

View File

@ -18,6 +18,7 @@ import { WireTunnelComponent } from "./components/wire_tunnel";
import { DisplayComponent } from "./components/display"; import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader"; import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter"; import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer";
export function initComponentRegistry() { export function initComponentRegistry() {
gComponentRegistry.register(StaticMapEntityComponent); gComponentRegistry.register(StaticMapEntityComponent);
@ -39,6 +40,7 @@ export function initComponentRegistry() {
gComponentRegistry.register(DisplayComponent); gComponentRegistry.register(DisplayComponent);
gComponentRegistry.register(BeltReaderComponent); gComponentRegistry.register(BeltReaderComponent);
gComponentRegistry.register(FilterComponent); gComponentRegistry.register(FilterComponent);
gComponentRegistry.register(ItemProducerComponent);
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS

View File

@ -26,7 +26,7 @@ export class ItemEjectorComponent extends Component {
static getSchema() { static getSchema() {
// The cachedDestSlot, cachedTargetEntity fields are not serialized. // The cachedDestSlot, cachedTargetEntity fields are not serialized.
return { return {
slots: types.array( slots: types.fixedSizeArray(
types.structured({ types.structured({
item: types.nullable(typeItemSingleton), item: types.nullable(typeItemSingleton),
progress: types.float, progress: types.float,

View File

@ -0,0 +1,7 @@
import { Component } from "../component";
export class ItemProducerComponent extends Component {
static getId() {
return "ItemProducer";
}
}

View File

@ -31,7 +31,7 @@ export class WiredPinsComponent extends Component {
static getSchema() { static getSchema() {
return { return {
slots: types.array( slots: types.fixedSizeArray(
types.structured({ types.structured({
value: types.nullable(typeItemSingleton), value: types.nullable(typeItemSingleton),
}) })

View File

@ -31,6 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
import { GameLogic } from "./logic"; import { GameLogic } from "./logic";
import { MapView } from "./map_view"; import { MapView } from "./map_view";
import { defaultBuildingVariant } from "./meta_building"; import { defaultBuildingVariant } from "./meta_building";
import { RegularGameMode } from "./modes/regular";
import { ProductionAnalytics } from "./production_analytics"; import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager"; import { ShapeDefinitionManager } from "./shape_definition_manager";
@ -101,6 +102,9 @@ export class GameCore {
// Needs to come first // Needs to come first
root.dynamicTickrate = new DynamicTickrate(root); root.dynamicTickrate = new DynamicTickrate(root);
// Init game mode
root.gameMode = new RegularGameMode(root);
// Init classes // Init classes
root.camera = new Camera(root); root.camera = new Camera(root);
root.map = new MapView(root); root.map = new MapView(root);

View File

@ -18,6 +18,7 @@ import { WireTunnelComponent } from "./components/wire_tunnel";
import { DisplayComponent } from "./components/display"; import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader"; import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter"; import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer";
/* typehints:end */ /* typehints:end */
/** /**
@ -85,6 +86,9 @@ export class EntityComponentStorage {
/** @type {FilterComponent} */ /** @type {FilterComponent} */
this.Filter; this.Filter;
/** @type {ItemProducerComponent} */
this.ItemProducer;
/* typehints:end */ /* typehints:end */
} }
} }

View File

@ -188,26 +188,6 @@ export class EntityManager extends BasicSerializableObject {
else return [...set.values()]; else return [...set.values()];
} }
// Deprecated lol
// /**
// * Return all of a given class. This is SLOW!
// * @param {object} entityClass
// * @returns {Array<Entity>} entities
// */
// getAllOfClass(entityClass) {
// // FIXME: Slow
// // Fine! I will!
// const result = [];
// const entities = [...this.entities.values()];
// for (let i = entities.length; i >= 0; --i) {
// const entity = this.entities[i];
// if (entity instanceof entityClass) {
// result.push(entity);
// }
// }
// return result;
// }
/** /**
* Unregisters all components of an entity from the component to entity mapping * Unregisters all components of an entity from the component to entity mapping
* @param {Entity} entity * @param {Entity} entity

71
src/js/game/game_mode.js Normal file
View File

@ -0,0 +1,71 @@
/* typehints:start */
import { enumHubGoalRewards } from "./tutorial_goals";
/* typehints:end */
import { GameRoot } from "./root";
/** @typedef {{
* shape: string,
* amount: number
* }} UpgradeRequirement */
/** @typedef {{
* required: Array<UpgradeRequirement>
* improvement?: number,
* excludePrevious?: boolean
* }} TierRequirement */
/** @typedef {Array<TierRequirement>} UpgradeTiers */
/** @typedef {{
* shape: string,
* required: number,
* reward: enumHubGoalRewards,
* throughputOnly?: boolean
* }} LevelDefinition */
export class GameMode {
/**
*
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
}
/**
* Should return all available upgrades
* @returns {Object<string, UpgradeTiers>}
*/
getUpgrades() {
abstract;
return null;
}
/**
* Returns the blueprint shape key
* @returns {string}
*/
getBlueprintShapeKey() {
abstract;
return null;
}
/**
* Returns the goals for all levels including their reward
* @returns {Array<LevelDefinition>}
*/
getLevelDefinitions() {
abstract;
return null;
}
/**
* Should return whether free play is available or if the game stops
* after the predefined levels
* @returns {boolean}
*/
getIsFreeplayAvailable() {
return true;
}
}

View File

@ -23,6 +23,7 @@ import { DisplaySystem } from "./systems/display";
import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays"; import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays";
import { BeltReaderSystem } from "./systems/belt_reader"; import { BeltReaderSystem } from "./systems/belt_reader";
import { FilterSystem } from "./systems/filter"; import { FilterSystem } from "./systems/filter";
import { ItemProducerSystem } from "./systems/item_producer";
const logger = createLogger("game_system_manager"); const logger = createLogger("game_system_manager");
@ -96,6 +97,9 @@ export class GameSystemManager {
/** @type {FilterSystem} */ /** @type {FilterSystem} */
filter: null, filter: null,
/** @type {ItemProducerSystem} */
itemProducer: null,
/* typehints:end */ /* typehints:end */
}; };
this.systemUpdateOrder = []; this.systemUpdateOrder = [];
@ -130,6 +134,8 @@ export class GameSystemManager {
add("filter", FilterSystem); add("filter", FilterSystem);
add("itemProducer", ItemProducerSystem);
add("itemEjector", ItemEjectorSystem); add("itemEjector", ItemEjectorSystem);
add("mapResources", MapResourcesSystem); add("mapResources", MapResourcesSystem);

View File

@ -1,12 +1,13 @@
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils"; import { RandomNumberGenerator } from "../core/rng";
import { clamp } from "../core/utils";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
import { enumColors } from "./colors"; import { enumColors } from "./colors";
import { enumItemProcessorTypes } from "./components/item_processor"; import { enumItemProcessorTypes } from "./components/item_processor";
import { enumAnalyticsDataSource } from "./production_analytics";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { enumSubShape, ShapeDefinition } from "./shape_definition"; import { enumSubShape, ShapeDefinition } from "./shape_definition";
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; import { enumHubGoalRewards } from "./tutorial_goals";
import { UPGRADES } from "./upgrades";
export class HubGoals extends BasicSerializableObject { export class HubGoals extends BasicSerializableObject {
static getId() { static getId() {
@ -18,32 +19,39 @@ export class HubGoals extends BasicSerializableObject {
level: types.uint, level: types.uint,
storedShapes: types.keyValueMap(types.uint), storedShapes: types.keyValueMap(types.uint),
upgradeLevels: types.keyValueMap(types.uint), upgradeLevels: types.keyValueMap(types.uint),
currentGoal: types.structured({
definition: types.knownType(ShapeDefinition),
required: types.uint,
reward: types.nullable(types.enum(enumHubGoalRewards)),
}),
}; };
} }
deserialize(data) { /**
*
* @param {*} data
* @param {GameRoot} root
*/
deserialize(data, root) {
const errorCode = super.deserialize(data); const errorCode = super.deserialize(data);
if (errorCode) { if (errorCode) {
return errorCode; return errorCode;
} }
const levels = root.gameMode.getLevelDefinitions();
// If freeplay is not available, clamp the level
if (!root.gameMode.getIsFreeplayAvailable()) {
this.level = Math.min(this.level, levels.length);
}
// Compute gained rewards // Compute gained rewards
for (let i = 0; i < this.level - 1; ++i) { for (let i = 0; i < this.level - 1; ++i) {
if (i < tutorialGoals.length) { if (i < levels.length) {
const reward = tutorialGoals[i].reward; const reward = levels[i].reward;
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1; this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
} }
} }
// Compute upgrade improvements // Compute upgrade improvements
for (const upgradeId in UPGRADES) { const upgrades = this.root.gameMode.getUpgrades();
const tiers = UPGRADES[upgradeId]; for (const upgradeId in upgrades) {
const tiers = upgrades[upgradeId];
const level = this.upgradeLevels[upgradeId] || 0; const level = this.upgradeLevels[upgradeId] || 0;
let totalImprovement = 1; let totalImprovement = 1;
for (let i = 0; i < level; ++i) { for (let i = 0; i < level; ++i) {
@ -53,15 +61,7 @@ export class HubGoals extends BasicSerializableObject {
} }
// Compute current goal // Compute current goal
const goal = tutorialGoals[this.level - 1]; this.computeNextGoal();
if (goal) {
this.currentGoal = {
/** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(goal.shape),
required: goal.required,
reward: goal.reward,
};
}
} }
/** /**
@ -97,11 +97,15 @@ export class HubGoals extends BasicSerializableObject {
* @type {Object<string, number>} * @type {Object<string, number>}
*/ */
this.upgradeImprovements = {}; this.upgradeImprovements = {};
for (const key in UPGRADES) {
// Reset levels first
const upgrades = this.root.gameMode.getUpgrades();
for (const key in upgrades) {
this.upgradeLevels[key] = 0;
this.upgradeImprovements[key] = 1; this.upgradeImprovements[key] = 1;
} }
this.createNextGoal(); this.computeNextGoal();
// Allow quickly switching goals in dev mode // Allow quickly switching goals in dev mode
if (G_IS_DEV) { if (G_IS_DEV) {
@ -109,13 +113,26 @@ export class HubGoals extends BasicSerializableObject {
if (ev.key === "b") { if (ev.key === "b") {
// root is not guaranteed to exist within ~0.5s after loading in // root is not guaranteed to exist within ~0.5s after loading in
if (this.root && this.root.app && this.root.app.gameAnalytics) { if (this.root && this.root.app && this.root.app.gameAnalytics) {
this.onGoalCompleted(); if (!this.isEndOfDemoReached()) {
this.onGoalCompleted();
}
} }
} }
}); });
} }
} }
/**
* Returns whether the end of the demo is reached
* @returns {boolean}
*/
isEndOfDemoReached() {
return (
!this.root.gameMode.getIsFreeplayAvailable() &&
this.level >= this.root.gameMode.getLevelDefinitions().length
);
}
/** /**
* Returns how much of the current shape is stored * Returns how much of the current shape is stored
* @param {ShapeDefinition} definition * @param {ShapeDefinition} definition
@ -150,6 +167,15 @@ export class HubGoals extends BasicSerializableObject {
* Returns how much of the current goal was already delivered * Returns how much of the current goal was already delivered
*/ */
getCurrentGoalDelivered() { getCurrentGoalDelivered() {
if (this.currentGoal.throughputOnly) {
return (
this.root.productionAnalytics.getCurrentShapeRate(
enumAnalyticsDataSource.delivered,
this.currentGoal.definition
) / globalConfig.analyticsSliceDurationSeconds
);
}
return this.getShapesStored(this.currentGoal.definition); return this.getShapesStored(this.currentGoal.definition);
} }
@ -184,36 +210,40 @@ export class HubGoals extends BasicSerializableObject {
this.root.signals.shapeDelivered.dispatch(definition); this.root.signals.shapeDelivered.dispatch(definition);
// Check if we have enough for the next level // Check if we have enough for the next level
const targetHash = this.currentGoal.definition.getHash();
if ( if (
this.storedShapes[targetHash] >= this.currentGoal.required || this.getCurrentGoalDelivered() >= this.currentGoal.required ||
(G_IS_DEV && globalConfig.debug.rewardsInstant) (G_IS_DEV && globalConfig.debug.rewardsInstant)
) { ) {
this.onGoalCompleted(); if (!this.isEndOfDemoReached()) {
this.onGoalCompleted();
}
} }
} }
/** /**
* Creates the next goal * Creates the next goal
*/ */
createNextGoal() { computeNextGoal() {
const storyIndex = this.level - 1; const storyIndex = this.level - 1;
if (storyIndex < tutorialGoals.length) { const levels = this.root.gameMode.getLevelDefinitions();
const { shape, required, reward } = tutorialGoals[storyIndex]; if (storyIndex < levels.length) {
const { shape, required, reward, throughputOnly } = levels[storyIndex];
this.currentGoal = { this.currentGoal = {
/** @type {ShapeDefinition} */ /** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape), definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
required, required,
reward, reward,
throughputOnly,
}; };
return; return;
} }
const required = Math.min(200, 4 + (this.level - 27) * 0.25);
this.currentGoal = { this.currentGoal = {
/** @type {ShapeDefinition} */ definition: this.computeFreeplayShape(this.level),
definition: this.createRandomShape(), required,
required: findNiceIntegerValue(1000 + Math.pow(this.level * 2000, 0.8)),
reward: enumHubGoalRewards.no_reward_freeplay, reward: enumHubGoalRewards.no_reward_freeplay,
throughputOnly: true,
}; };
} }
@ -226,7 +256,7 @@ export class HubGoals extends BasicSerializableObject {
this.root.app.gameAnalytics.handleLevelCompleted(this.level); this.root.app.gameAnalytics.handleLevelCompleted(this.level);
++this.level; ++this.level;
this.createNextGoal(); this.computeNextGoal();
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward); this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
} }
@ -235,7 +265,7 @@ export class HubGoals extends BasicSerializableObject {
* Returns whether we are playing in free-play * Returns whether we are playing in free-play
*/ */
isFreePlay() { isFreePlay() {
return this.level >= tutorialGoals.length; return this.level >= this.root.gameMode.getLevelDefinitions().length;
} }
/** /**
@ -243,7 +273,7 @@ export class HubGoals extends BasicSerializableObject {
* @param {string} upgradeId * @param {string} upgradeId
*/ */
canUnlockUpgrade(upgradeId) { canUnlockUpgrade(upgradeId) {
const tiers = UPGRADES[upgradeId]; const tiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentLevel = this.getUpgradeLevel(upgradeId); const currentLevel = this.getUpgradeLevel(upgradeId);
if (currentLevel >= tiers.length) { if (currentLevel >= tiers.length) {
@ -272,7 +302,7 @@ export class HubGoals extends BasicSerializableObject {
*/ */
getAvailableUpgradeCount() { getAvailableUpgradeCount() {
let count = 0; let count = 0;
for (const upgradeId in UPGRADES) { for (const upgradeId in this.root.gameMode.getUpgrades()) {
if (this.canUnlockUpgrade(upgradeId)) { if (this.canUnlockUpgrade(upgradeId)) {
++count; ++count;
} }
@ -290,7 +320,7 @@ export class HubGoals extends BasicSerializableObject {
return false; return false;
} }
const upgradeTiers = UPGRADES[upgradeId]; const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentLevel = this.getUpgradeLevel(upgradeId); const currentLevel = this.getUpgradeLevel(upgradeId);
const tierData = upgradeTiers[currentLevel]; const tierData = upgradeTiers[currentLevel];
@ -320,15 +350,85 @@ export class HubGoals extends BasicSerializableObject {
} }
/** /**
* Picks random colors which are close to each other
* @param {RandomNumberGenerator} rng
*/
generateRandomColorSet(rng, allowUncolored = false) {
const colorWheel = [
enumColors.red,
enumColors.yellow,
enumColors.green,
enumColors.cyan,
enumColors.blue,
enumColors.purple,
enumColors.red,
enumColors.yellow,
];
const universalColors = [enumColors.white];
if (allowUncolored) {
universalColors.push(enumColors.uncolored);
}
const index = rng.nextIntRange(0, colorWheel.length - 2);
const pickedColors = colorWheel.slice(index, index + 3);
pickedColors.push(rng.choice(universalColors));
return pickedColors;
}
/**
* Creates a (seeded) random shape
* @param {number} level
* @returns {ShapeDefinition} * @returns {ShapeDefinition}
*/ */
createRandomShape() { computeFreeplayShape(level) {
const layerCount = clamp(this.level / 25, 2, 4); const layerCount = clamp(this.level / 25, 2, 4);
/** @type {Array<import("./shape_definition").ShapeLayer>} */ /** @type {Array<import("./shape_definition").ShapeLayer>} */
let layers = []; let layers = [];
const randomColor = () => randomChoice(Object.values(enumColors)); const rng = new RandomNumberGenerator(this.root.map.seed + "/" + level);
const randomShape = () => randomChoice(Object.values(enumSubShape));
const colors = this.generateRandomColorSet(rng, level > 35);
let pickedSymmetry = null; // pairs of quadrants that must be the same
let availableShapes = [enumSubShape.rect, enumSubShape.circle, enumSubShape.star];
if (rng.next() < 0.5) {
pickedSymmetry = [
// radial symmetry
[0, 2],
[1, 3],
];
availableShapes.push(enumSubShape.windmill); // windmill looks good only in radial symmetry
} else {
const symmetries = [
[
// horizontal axis
[0, 3],
[1, 2],
],
[
// vertical axis
[0, 1],
[2, 3],
],
[
// diagonal axis
[0, 2],
[1],
[3],
],
[
// other diagonal axis
[1, 3],
[0],
[2],
],
];
pickedSymmetry = rng.choice(symmetries);
}
const randomColor = () => rng.choice(colors);
const randomShape = () => rng.choice(Object.values(enumSubShape));
let anyIsMissingTwo = false; let anyIsMissingTwo = false;
@ -336,23 +436,24 @@ export class HubGoals extends BasicSerializableObject {
/** @type {import("./shape_definition").ShapeLayer} */ /** @type {import("./shape_definition").ShapeLayer} */
const layer = [null, null, null, null]; const layer = [null, null, null, null];
for (let quad = 0; quad < 4; ++quad) { for (let j = 0; j < pickedSymmetry.length; ++j) {
layer[quad] = { const group = pickedSymmetry[j];
subShape: randomShape(), const shape = randomShape();
color: randomColor(), const color = randomColor();
}; for (let k = 0; k < group.length; ++k) {
} const quad = group[k];
layer[quad] = {
// Sometimes shapes are missing subShape: shape,
if (Math.random() > 0.85) { color,
layer[randomInt(0, 3)] = null; };
}
} }
// Sometimes they actually are missing *two* ones! // Sometimes they actually are missing *two* ones!
// Make sure at max only one layer is missing it though, otherwise we could // Make sure at max only one layer is missing it though, otherwise we could
// create an uncreateable shape // create an uncreateable shape
if (Math.random() > 0.95 && !anyIsMissingTwo) { if (level > 75 && rng.next() > 0.95 && !anyIsMissingTwo) {
layer[randomInt(0, 3)] = null; layer[rng.nextIntRange(0, 4)] = null;
anyIsMissingTwo = true; anyIsMissingTwo = true;
} }

View File

@ -15,7 +15,7 @@ import { HUDKeybindingOverlay } from "./parts/keybinding_overlay";
import { HUDUnlockNotification } from "./parts/unlock_notification"; import { HUDUnlockNotification } from "./parts/unlock_notification";
import { HUDGameMenu } from "./parts/game_menu"; import { HUDGameMenu } from "./parts/game_menu";
import { HUDShop } from "./parts/shop"; import { HUDShop } from "./parts/shop";
import { IS_MOBILE, globalConfig, IS_DEMO } from "../../core/config"; import { IS_MOBILE, globalConfig } from "../../core/config";
import { HUDMassSelector } from "./parts/mass_selector"; import { HUDMassSelector } from "./parts/mass_selector";
import { HUDVignetteOverlay } from "./parts/vignette_overlay"; import { HUDVignetteOverlay } from "./parts/vignette_overlay";
import { HUDStatistics } from "./parts/statistics"; import { HUDStatistics } from "./parts/statistics";
@ -45,6 +45,9 @@ import { HUDLeverToggle } from "./parts/lever_toggle";
import { HUDLayerPreview } from "./parts/layer_preview"; import { HUDLayerPreview } from "./parts/layer_preview";
import { HUDMinerHighlight } from "./parts/miner_highlight"; import { HUDMinerHighlight } from "./parts/miner_highlight";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { HUDBetaOverlay } from "./parts/beta_overlay";
import { HUDStandaloneAdvantages } from "./parts/standalone_advantages";
import { HUDCatMemes } from "./parts/cat_memes";
export class GameHUD { export class GameHUD {
/** /**
@ -76,7 +79,6 @@ export class GameHUD {
pinnedShapes: new HUDPinnedShapes(this.root), pinnedShapes: new HUDPinnedShapes(this.root),
notifications: new HUDNotifications(this.root), notifications: new HUDNotifications(this.root),
settingsMenu: new HUDSettingsMenu(this.root), settingsMenu: new HUDSettingsMenu(this.root),
// betaOverlay: new HUDBetaOverlay(this.root),
debugInfo: new HUDDebugInfo(this.root), debugInfo: new HUDDebugInfo(this.root),
dialogs: new HUDModalDialogs(this.root), dialogs: new HUDModalDialogs(this.root),
screenshotExporter: new HUDScreenshotExporter(this.root), screenshotExporter: new HUDScreenshotExporter(this.root),
@ -113,8 +115,10 @@ export class GameHUD {
this.parts.entityDebugger = new HUDEntityDebugger(this.root); this.parts.entityDebugger = new HUDEntityDebugger(this.root);
} }
if (IS_DEMO) { if (this.root.app.restrictionMgr.getIsStandaloneMarketingActive()) {
this.parts.watermark = new HUDWatermark(this.root); this.parts.watermark = new HUDWatermark(this.root);
this.parts.standaloneAdvantages = new HUDStandaloneAdvantages(this.root);
this.parts.catMemes = new HUDCatMemes(this.root);
} }
if (G_IS_DEV && globalConfig.debug.renderChanges) { if (G_IS_DEV && globalConfig.debug.renderChanges) {
@ -138,6 +142,10 @@ export class GameHUD {
this.parts.sandboxController = new HUDSandboxController(this.root); this.parts.sandboxController = new HUDSandboxController(this.root);
} }
if (!G_IS_RELEASE && !G_IS_DEV) {
this.parts.betaOverlay = new HUDBetaOverlay(this.root);
}
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
for (const key in this.parts) { for (const key in this.parts) {
this.parts[key].createElements(frag); this.parts[key].createElements(frag);

View File

@ -3,7 +3,12 @@ import { makeDiv } from "../../../core/utils";
export class HUDBetaOverlay extends BaseHUDPart { export class HUDBetaOverlay extends BaseHUDPart {
createElements(parent) { createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_BetaOverlay", [], "CLOSED BETA"); this.element = makeDiv(
parent,
"ingame_HUD_BetaOverlay",
[],
"<h2>UNSTABLE BETA VERSION</h2><span>Unfinalized & potential buggy content!</span>"
);
} }
initialize() {} initialize() {}

View File

@ -6,7 +6,6 @@ import { Vector } from "../../../core/vector";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { enumMouseButton } from "../../camera"; import { enumMouseButton } from "../../camera";
import { KEYMAPPINGS } from "../../key_action_mapper"; import { KEYMAPPINGS } from "../../key_action_mapper";
import { blueprintShape } from "../../upgrades";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { Blueprint } from "../../blueprint"; import { Blueprint } from "../../blueprint";
@ -15,7 +14,9 @@ import { Entity } from "../../entity";
export class HUDBlueprintPlacer extends BaseHUDPart { export class HUDBlueprintPlacer extends BaseHUDPart {
createElements(parent) { createElements(parent) {
const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(blueprintShape); const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
);
const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80); const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80);
this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``); this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``);
@ -124,7 +125,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
const tile = worldPos.toTileSpace(); const tile = worldPos.toTileSpace();
if (blueprint.tryPlace(this.root, tile)) { if (blueprint.tryPlace(this.root, tile)) {
const cost = blueprint.getCost(); const cost = blueprint.getCost();
this.root.hubGoals.takeShapeByKey(blueprintShape, cost); this.root.hubGoals.takeShapeByKey(this.root.gameMode.getBlueprintShapeKey(), cost);
this.root.soundProxy.playUi(SOUNDS.placeBuilding); this.root.soundProxy.playUi(SOUNDS.placeBuilding);
} }
} }

View File

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

View File

@ -14,6 +14,8 @@ import { MetaTrashBuilding } from "../../buildings/trash";
import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
import { HUDBaseToolbar } from "./base_toolbar"; import { HUDBaseToolbar } from "./base_toolbar";
import { MetaStorageBuilding } from "../../buildings/storage"; import { MetaStorageBuilding } from "../../buildings/storage";
import { MetaItemProducerBuilding } from "../../buildings/item_producer";
import { queryParamOptions } from "../../../core/query_parameters";
export class HUDBuildingsToolbar extends HUDBaseToolbar { export class HUDBuildingsToolbar extends HUDBaseToolbar {
constructor(root) { constructor(root) {
@ -29,6 +31,7 @@ export class HUDBuildingsToolbar extends HUDBaseToolbar {
MetaMixerBuilding, MetaMixerBuilding,
MetaPainterBuilding, MetaPainterBuilding,
MetaTrashBuilding, MetaTrashBuilding,
...(queryParamOptions.sandboxMode || G_IS_DEV ? [MetaItemProducerBuilding] : []),
], ],
secondaryBuildings: [ secondaryBuildings: [
MetaStorageBuilding, MetaStorageBuilding,
@ -39,7 +42,7 @@ export class HUDBuildingsToolbar extends HUDBaseToolbar {
], ],
visibilityCondition: () => visibilityCondition: () =>
!this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "regular", !this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "regular",
htmlElementId: "ingame_HUD_buildings_toolbar", htmlElementId: "ingame_HUD_BuildingsToolbar",
}); });
} }
} }

View File

@ -0,0 +1,21 @@
import { makeDiv } from "../../../core/utils";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
const memeShowIntervalSeconds = 70 * 60;
const memeShowDuration = 5;
export class HUDCatMemes extends BaseHUDPart {
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_CatMemes");
}
initialize() {
this.domAttach = new DynamicDomAttach(this.root, this.element);
}
update() {
const now = this.root.time.realtimeNow();
this.domAttach.update(now % memeShowIntervalSeconds > memeShowIntervalSeconds - memeShowDuration);
}
}

View File

@ -6,23 +6,25 @@ import { DynamicDomAttach } from "../dynamic_dom_attach";
import { TrackedState } from "../../../core/tracked_state"; import { TrackedState } from "../../../core/tracked_state";
import { cachebust } from "../../../core/cachebust"; import { cachebust } from "../../../core/cachebust";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../../components/item_processor";
import { ShapeItem } from "../../items/shape_item";
import { WireComponent } from "../../components/wire";
import { LeverComponent } from "../../components/lever";
// @todo: Make dictionary
const tutorialsByLevel = [ const tutorialsByLevel = [
// Level 1 // Level 1
[ [
// 1.1. place an extractor // 1.1. place an extractor
{ {
id: "1_1_extractor", id: "1_1_extractor",
condition: /** @param {GameRoot} root */ root => { condition: /** @param {GameRoot} root */ root =>
return root.entityMgr.getAllWithComponent(MinerComponent).length === 0; root.entityMgr.getAllWithComponent(MinerComponent).length === 0,
},
}, },
// 1.2. connect to hub // 1.2. connect to hub
{ {
id: "1_2_conveyor", id: "1_2_conveyor",
condition: /** @param {GameRoot} root */ root => { condition: /** @param {GameRoot} root */ root => root.hubGoals.getCurrentGoalDelivered() === 0,
return root.hubGoals.getCurrentGoalDelivered() === 0;
},
}, },
// 1.3 wait for completion // 1.3 wait for completion
{ {
@ -30,6 +32,108 @@ const tutorialsByLevel = [
condition: () => true, condition: () => true,
}, },
], ],
// Level 2
[
// 2.1 place a cutter
{
id: "2_1_place_cutter",
condition: /** @param {GameRoot} root */ root =>
root.entityMgr
.getAllWithComponent(ItemProcessorComponent)
.filter(e => e.components.ItemProcessor.type === enumItemProcessorTypes.cutter).length ===
0,
},
// 2.2 place trash
{
id: "2_2_place_trash",
condition: /** @param {GameRoot} root */ root =>
root.entityMgr
.getAllWithComponent(ItemProcessorComponent)
.filter(e => e.components.ItemProcessor.type === enumItemProcessorTypes.trash).length ===
0,
},
// 2.3 place more cutters
{
id: "2_3_more_cutters",
condition: /** @param {GameRoot} root */ root =>
root.entityMgr
.getAllWithComponent(ItemProcessorComponent)
.filter(e => e.components.ItemProcessor.type === enumItemProcessorTypes.cutter).length <
3,
},
],
// Level 3
[
// 3.1. rectangles
{
id: "3_1_rectangles",
condition: /** @param {GameRoot} root */ root =>
// 4 miners placed above rectangles and 10 delivered
root.hubGoals.getCurrentGoalDelivered() < 10 ||
root.entityMgr.getAllWithComponent(MinerComponent).filter(entity => {
const tile = entity.components.StaticMapEntity.origin;
const below = root.map.getLowerLayerContentXY(tile.x, tile.y);
if (below && below.getItemType() === "shape") {
const shape = /** @type {ShapeItem} */ (below).definition.getHash();
return shape === "RuRuRuRu";
}
return false;
}).length < 4,
},
],
[], // Level 4
[], // Level 5
[], // Level 6
[], // Level 7
[], // Level 8
[], // Level 9
[], // Level 10
[], // Level 11
[], // Level 12
[], // Level 13
[], // Level 14
[], // Level 15
[], // Level 16
[], // Level 17
[], // Level 18
[], // Level 19
[], // Level 20
// Level 21
[
// 21.1 place quad painter
{
id: "21_1_place_quad_painter",
condition: /** @param {GameRoot} root */ root =>
root.entityMgr
.getAllWithComponent(ItemProcessorComponent)
.filter(e => e.components.ItemProcessor.type === enumItemProcessorTypes.painterQuad)
.length === 0,
},
// 21.2 switch to wires layer
{
id: "21_2_switch_to_wires",
condition: /** @param {GameRoot} root */ root =>
root.entityMgr.getAllWithComponent(WireComponent).length < 5,
},
// 21.3 place button
{
id: "21_3_place_button",
condition: /** @param {GameRoot} root */ root =>
root.entityMgr.getAllWithComponent(LeverComponent).length === 0,
},
// 21.4 activate button
{
id: "21_4_press_button",
condition: /** @param {GameRoot} root */ root =>
root.entityMgr.getAllWithComponent(LeverComponent).some(e => !e.components.Lever.toggled),
},
],
]; ];
export class HUDInteractiveTutorial extends BaseHUDPart { export class HUDInteractiveTutorial extends BaseHUDPart {

View File

@ -259,7 +259,7 @@ export class HUDKeybindingOverlay extends BaseHUDPart {
label: T.ingame.keybindingsOverlay.switchLayers, label: T.ingame.keybindingsOverlay.switchLayers,
keys: [k.ingame.switchLayers], keys: [k.ingame.switchLayers],
condition: () => condition: () =>
this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers), this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers),
}, },
]; ];

View File

@ -122,7 +122,7 @@ export class HUDModalDialogs extends BaseHUDPart {
dialog.buttonSignals.getStandalone.add(() => { dialog.buttonSignals.getStandalone.add(() => {
this.app.analytics.trackUiClick("demo_dialog_click"); this.app.analytics.trackUiClick("demo_dialog_click");
window.open(THIRDPARTY_URLS.standaloneStorePage); window.open(THIRDPARTY_URLS.standaloneStorePage + "?ref=ddc");
}); });
return dialog.buttonSignals; return dialog.buttonSignals;

View File

@ -1,9 +1,11 @@
import { ClickDetector } from "../../../core/click_detector"; import { ClickDetector } from "../../../core/click_detector";
import { formatBigNumber, makeDiv, arrayDeleteValue } from "../../../core/utils"; import { globalConfig } from "../../../core/config";
import { arrayDeleteValue, formatBigNumber, makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { enumAnalyticsDataSource } from "../../production_analytics";
import { ShapeDefinition } from "../../shape_definition"; import { ShapeDefinition } from "../../shape_definition";
import { BaseHUDPart } from "../base_hud_part";
import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
import { BaseHUDPart } from "../base_hud_part";
/** /**
* Manages the pinned shapes on the left side of the screen * Manages the pinned shapes on the left side of the screen
@ -22,11 +24,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
* convenient. Also allows for cleaning up handles. * convenient. Also allows for cleaning up handles.
* @type {Array<{ * @type {Array<{
* key: string, * key: string,
* definition: ShapeDefinition,
* amountLabel: HTMLElement, * amountLabel: HTMLElement,
* lastRenderedValue: string, * lastRenderedValue: string,
* element: HTMLElement, * element: HTMLElement,
* detector?: ClickDetector, * detector?: ClickDetector,
* infoDetector?: ClickDetector * infoDetector?: ClickDetector,
* throughputOnly?: boolean
* }>} * }>}
*/ */
this.handles = []; this.handles = [];
@ -77,7 +81,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
updateShapesAfterUpgrade() { updateShapesAfterUpgrade() {
for (let i = 0; i < this.pinnedShapes.length; ++i) { for (let i = 0; i < this.pinnedShapes.length; ++i) {
const key = this.pinnedShapes[i]; const key = this.pinnedShapes[i];
if (key === blueprintShape) { if (key === this.root.gameMode.getBlueprintShapeKey()) {
// Ignore blueprint shapes // Ignore blueprint shapes
continue; continue;
} }
@ -102,13 +106,14 @@ export class HUDPinnedShapes extends BaseHUDPart {
if (key === this.root.hubGoals.currentGoal.definition.getHash()) { if (key === this.root.hubGoals.currentGoal.definition.getHash()) {
return this.root.hubGoals.currentGoal.required; return this.root.hubGoals.currentGoal.required;
} }
if (key === blueprintShape) { if (key === this.root.gameMode.getBlueprintShapeKey()) {
return null; return null;
} }
// Check if this shape is required for any upgrade // Check if this shape is required for any upgrade
for (const upgradeId in UPGRADES) { const upgrades = this.root.gameMode.getUpgrades();
const upgradeTiers = UPGRADES[upgradeId]; for (const upgradeId in upgrades) {
const upgradeTiers = upgrades[upgradeId];
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
const tierHandle = upgradeTiers[currentTier]; const tierHandle = upgradeTiers[currentTier];
@ -133,7 +138,10 @@ export class HUDPinnedShapes extends BaseHUDPart {
* @param {string} key * @param {string} key
*/ */
isShapePinned(key) { isShapePinned(key) {
if (key === this.root.hubGoals.currentGoal.definition.getHash() || key === blueprintShape) { if (
key === this.root.hubGoals.currentGoal.definition.getHash() ||
key === this.root.gameMode.getBlueprintShapeKey()
) {
// This is a "special" shape which is always pinned // This is a "special" shape which is always pinned
return true; return true;
} }
@ -163,29 +171,40 @@ export class HUDPinnedShapes extends BaseHUDPart {
this.handles = []; this.handles = [];
// Pin story goal // Pin story goal
this.internalPinShape(currentKey, false, "goal"); this.internalPinShape({
key: currentKey,
canUnpin: false,
className: "goal",
throughputOnly: currentGoal.throughputOnly,
});
// Pin blueprint shape as well // Pin blueprint shape as well
if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) {
this.internalPinShape(blueprintShape, false, "blueprint"); this.internalPinShape({
key: this.root.gameMode.getBlueprintShapeKey(),
canUnpin: false,
className: "blueprint",
});
} }
// Pin manually pinned shapes // Pin manually pinned shapes
for (let i = 0; i < this.pinnedShapes.length; ++i) { for (let i = 0; i < this.pinnedShapes.length; ++i) {
const key = this.pinnedShapes[i]; const key = this.pinnedShapes[i];
if (key !== currentKey) { if (key !== currentKey) {
this.internalPinShape(key); this.internalPinShape({ key });
} }
} }
} }
/** /**
* Pins a new shape * Pins a new shape
* @param {string} key * @param {object} param0
* @param {boolean} canUnpin * @param {string} param0.key
* @param {string=} className * @param {boolean=} param0.canUnpin
* @param {string=} param0.className
* @param {boolean=} param0.throughputOnly
*/ */
internalPinShape(key, canUnpin = true, className = null) { internalPinShape({ key, canUnpin = true, className = null, throughputOnly = false }) {
const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key);
const element = makeDiv(this.element, null, ["shape"]); const element = makeDiv(this.element, null, ["shape"]);
@ -198,11 +217,11 @@ export class HUDPinnedShapes extends BaseHUDPart {
let detector = null; let detector = null;
if (canUnpin) { if (canUnpin) {
element.classList.add("unpinable"); element.classList.add("removable");
detector = new ClickDetector(element, { detector = new ClickDetector(element, {
consumeEvents: true, consumeEvents: true,
preventDefault: true, preventDefault: true,
targetOnly: true, targetOnly: false,
}); });
detector.click.add(() => this.unpinShape(key)); detector.click.add(() => this.unpinShape(key));
} else { } else {
@ -229,11 +248,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
this.handles.push({ this.handles.push({
key, key,
definition,
element, element,
amountLabel, amountLabel,
lastRenderedValue: "", lastRenderedValue: "",
detector, detector,
infoDetector, infoDetector,
throughputOnly,
}); });
} }
@ -244,8 +265,21 @@ export class HUDPinnedShapes extends BaseHUDPart {
for (let i = 0; i < this.handles.length; ++i) { for (let i = 0; i < this.handles.length; ++i) {
const handle = this.handles[i]; const handle = this.handles[i];
const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); let currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key);
const currentValueFormatted = formatBigNumber(currentValue); let currentValueFormatted = formatBigNumber(currentValue);
if (handle.throughputOnly) {
currentValue =
this.root.productionAnalytics.getCurrentShapeRate(
enumAnalyticsDataSource.delivered,
handle.definition
) / globalConfig.analyticsSliceDurationSeconds;
currentValueFormatted = T.ingame.statistics.shapesDisplayUnits.second.replace(
"<shapes>",
String(currentValue)
);
}
if (currentValueFormatted !== handle.lastRenderedValue) { if (currentValueFormatted !== handle.lastRenderedValue) {
handle.lastRenderedValue = currentValueFormatted; handle.lastRenderedValue = currentValueFormatted;
handle.amountLabel.innerText = currentValueFormatted; handle.amountLabel.innerText = currentValueFormatted;
@ -260,6 +294,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
* @param {string} key * @param {string} key
*/ */
unpinShape(key) { unpinShape(key) {
console.log("unpin", key);
arrayDeleteValue(this.pinnedShapes, key); arrayDeleteValue(this.pinnedShapes, key);
this.rerenderFull(); this.rerenderFull();
} }
@ -275,7 +310,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
return; return;
} }
if (key === blueprintShape) { if (key === this.root.gameMode.getBlueprintShapeKey()) {
// Can not pin the blueprint shape // Can not pin the blueprint shape
return; return;
} }

View File

@ -1,9 +1,7 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumNotificationType } from "./notifications"; import { enumNotificationType } from "./notifications";
import { tutorialGoals } from "../../tutorial_goals";
export class HUDSandboxController extends BaseHUDPart { export class HUDSandboxController extends BaseHUDPart {
createElements(parent) { createElements(parent) {
@ -75,10 +73,11 @@ export class HUDSandboxController extends BaseHUDPart {
} }
giveBlueprints() { giveBlueprints() {
if (!this.root.hubGoals.storedShapes[blueprintShape]) { const shape = this.root.gameMode.getBlueprintShapeKey();
this.root.hubGoals.storedShapes[blueprintShape] = 0; if (!this.root.hubGoals.storedShapes[shape]) {
this.root.hubGoals.storedShapes[shape] = 0;
} }
this.root.hubGoals.storedShapes[blueprintShape] += 1e9; this.root.hubGoals.storedShapes[shape] += 1e9;
} }
maxOutAll() { maxOutAll() {
@ -89,7 +88,7 @@ export class HUDSandboxController extends BaseHUDPart {
} }
modifyUpgrade(id, amount) { modifyUpgrade(id, amount) {
const upgradeTiers = UPGRADES[id]; const upgradeTiers = this.root.gameMode.getUpgrades()[id];
const maxLevel = upgradeTiers.length; const maxLevel = upgradeTiers.length;
this.root.hubGoals.upgradeLevels[id] = Math.max( this.root.hubGoals.upgradeLevels[id] = Math.max(
@ -113,7 +112,7 @@ export class HUDSandboxController extends BaseHUDPart {
modifyLevel(amount) { modifyLevel(amount) {
const hubGoals = this.root.hubGoals; const hubGoals = this.root.hubGoals;
hubGoals.level = Math.max(1, hubGoals.level + amount); hubGoals.level = Math.max(1, hubGoals.level + amount);
hubGoals.createNextGoal(); hubGoals.computeNextGoal();
// Clear all shapes of this level // Clear all shapes of this level
hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0; hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0;
@ -122,9 +121,10 @@ export class HUDSandboxController extends BaseHUDPart {
// Compute gained rewards // Compute gained rewards
hubGoals.gainedRewards = {}; hubGoals.gainedRewards = {};
const levels = this.root.gameMode.getLevelDefinitions();
for (let i = 0; i < hubGoals.level - 1; ++i) { for (let i = 0; i < hubGoals.level - 1; ++i) {
if (i < tutorialGoals.length) { if (i < levels.length) {
const reward = tutorialGoals[i].reward; const reward = levels[i].reward;
hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1; hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1;
} }
} }

View File

@ -1,13 +1,13 @@
import { BaseHUDPart } from "../base_hud_part";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { IS_DEMO, globalConfig } from "../../../core/config";
import { T } from "../../../translations";
import { createLogger } from "../../../core/logging";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { Vector } from "../../../core/vector";
import { makeOffscreenBuffer } from "../../../core/buffer_utils"; import { makeOffscreenBuffer } from "../../../core/buffer_utils";
import { globalConfig } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters"; import { DrawParameters } from "../../../core/draw_parameters";
import { createLogger } from "../../../core/logging";
import { Rectangle } from "../../../core/rectangle"; import { Rectangle } from "../../../core/rectangle";
import { Vector } from "../../../core/vector";
import { T } from "../../../translations";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part";
const logger = createLogger("screenshot_exporter"); const logger = createLogger("screenshot_exporter");
@ -19,7 +19,7 @@ export class HUDScreenshotExporter extends BaseHUDPart {
} }
startExport() { startExport() {
if (IS_DEMO) { if (!this.root.app.restrictionMgr.getIsExportingScreenshotsPossible()) {
this.root.hud.parts.dialogs.showFeatureRestrictionInfo(T.demo.features.exportingBase); this.root.hud.parts.dialogs.showFeatureRestrictionInfo(T.demo.features.exportingBase);
return; return;
} }
@ -87,7 +87,7 @@ export class HUDScreenshotExporter extends BaseHUDPart {
const parameters = new DrawParameters({ const parameters = new DrawParameters({
context, context,
visibleRect, visibleRect,
desiredAtlasScale: chunkScale, desiredAtlasScale: 0.25,
root: this.root, root: this.root,
zoomLevel: chunkScale, zoomLevel: chunkScale,
}); });

View File

@ -43,7 +43,7 @@ export class HUDSettingsMenu extends BaseHUDPart {
]; ];
for (let i = 0; i < buttons.length; ++i) { for (let i = 0; i < buttons.length; ++i) {
const { title, action, id } = buttons[i]; const { action, id } = buttons[i];
const element = document.createElement("button"); const element = document.createElement("button");
element.classList.add("styledButton"); element.classList.add("styledButton");
@ -88,13 +88,8 @@ export class HUDSettingsMenu extends BaseHUDPart {
this.close(); this.close();
} }
cleanup() {
document.body.classList.remove("ingameDialogOpen");
}
show() { show() {
this.visible = true; this.visible = true;
document.body.classList.add("ingameDialogOpen");
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60); const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60);
@ -120,7 +115,6 @@ export class HUDSettingsMenu extends BaseHUDPart {
close() { close() {
this.visible = false; this.visible = false;
document.body.classList.remove("ingameDialogOpen");
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update(); this.update();
} }

View File

@ -67,7 +67,6 @@ export class HUDShapeViewer extends BaseHUDPart {
*/ */
close() { close() {
this.visible = false; this.visible = false;
document.body.classList.remove("ingameDialogOpen");
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update(); this.update();
} }
@ -78,7 +77,6 @@ export class HUDShapeViewer extends BaseHUDPart {
*/ */
renderForShape(definition) { renderForShape(definition) {
this.visible = true; this.visible = true;
document.body.classList.add("ingameDialogOpen");
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
removeAllChildren(this.renderArea); removeAllChildren(this.renderArea);
@ -124,13 +122,6 @@ export class HUDShapeViewer extends BaseHUDPart {
} }
} }
/**
* Cleans up everything
*/
cleanup() {
document.body.classList.remove("ingameDialogOpen");
}
update() { update() {
this.domAttach.update(this.visible); this.domAttach.update(this.visible);
} }

View File

@ -1,9 +1,8 @@
import { ClickDetector } from "../../../core/click_detector"; import { ClickDetector } from "../../../core/click_detector";
import { InputReceiver } from "../../../core/input_receiver"; import { InputReceiver } from "../../../core/input_receiver";
import { formatBigNumber, makeDiv } from "../../../core/utils"; import { formatBigNumber, getRomanNumber, makeDiv } from "../../../core/utils";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { UPGRADES } from "../../upgrades";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
@ -21,7 +20,7 @@ export class HUDShop extends BaseHUDPart {
this.upgradeToElements = {}; this.upgradeToElements = {};
// Upgrades // Upgrades
for (const upgradeId in UPGRADES) { for (const upgradeId in this.root.gameMode.getUpgrades()) {
const handle = {}; const handle = {};
handle.requireIndexToElement = []; handle.requireIndexToElement = [];
@ -59,7 +58,7 @@ export class HUDShop extends BaseHUDPart {
rerenderFull() { rerenderFull() {
for (const upgradeId in this.upgradeToElements) { for (const upgradeId in this.upgradeToElements) {
const handle = this.upgradeToElements[upgradeId]; const handle = this.upgradeToElements[upgradeId];
const upgradeTiers = UPGRADES[upgradeId]; const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId]; const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId];
@ -68,7 +67,7 @@ export class HUDShop extends BaseHUDPart {
// Set tier // Set tier
handle.elemTierLabel.innerText = T.ingame.shop.tier.replace( handle.elemTierLabel.innerText = T.ingame.shop.tier.replace(
"<x>", "<x>",
"" + T.ingame.shop.tierLabels[currentTier] getRomanNumber(currentTier + 1)
); );
handle.elemTierLabel.setAttribute("data-tier", currentTier); handle.elemTierLabel.setAttribute("data-tier", currentTier);
@ -90,17 +89,15 @@ export class HUDShop extends BaseHUDPart {
// Max level // Max level
handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace( handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace(
"<currentMult>", "<currentMult>",
currentTierMultiplier.toString() formatBigNumber(currentTierMultiplier)
); );
continue; continue;
} }
// Set description // Set description
handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description
.replace("<currentMult>", currentTierMultiplier.toString()) .replace("<currentMult>", formatBigNumber(currentTierMultiplier))
.replace("<newMult>", (currentTierMultiplier + tierHandle.improvement).toString()) .replace("<newMult>", formatBigNumber(currentTierMultiplier + tierHandle.improvement));
// Backwards compatibility
.replace("<gain>", (tierHandle.improvement * 100.0).toString());
tierHandle.required.forEach(({ shape, amount }) => { tierHandle.required.forEach(({ shape, amount }) => {
const container = makeDiv(handle.elemRequirements, null, ["requirement"]); const container = makeDiv(handle.elemRequirements, null, ["requirement"]);
@ -207,8 +204,6 @@ export class HUDShop extends BaseHUDPart {
} }
cleanup() { cleanup() {
document.body.classList.remove("ingameDialogOpen");
// Cleanup detectors // Cleanup detectors
for (const upgradeId in this.upgradeToElements) { for (const upgradeId in this.upgradeToElements) {
const handle = this.upgradeToElements[upgradeId]; const handle = this.upgradeToElements[upgradeId];
@ -224,15 +219,12 @@ export class HUDShop extends BaseHUDPart {
show() { show() {
this.visible = true; this.visible = true;
document.body.classList.add("ingameDialogOpen");
// this.background.classList.add("visible");
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
this.rerenderFull(); this.rerenderFull();
} }
close() { close() {
this.visible = false; this.visible = false;
document.body.classList.remove("ingameDialogOpen");
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update(); this.update();
} }

View File

@ -0,0 +1,85 @@
import { A_B_TESTING_LINK_TYPE, THIRDPARTY_URLS } from "../../../core/config";
import { InputReceiver } from "../../../core/input_receiver";
import { makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
const showIntervalSeconds = 30 * 60;
export class HUDStandaloneAdvantages extends BaseHUDPart {
createElements(parent) {
this.background = makeDiv(parent, "ingame_HUD_StandaloneAdvantages", ["ingameDialog"]);
// DIALOG Inner / Wrapper
this.dialogInner = makeDiv(this.background, null, ["dialogInner"]);
this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.standaloneAdvantages.title);
this.contentDiv = makeDiv(
this.dialogInner,
null,
["content"],
`
<div class="points">
${Object.entries(T.ingame.standaloneAdvantages.points)
.map(
([key, trans]) => `
<div class="point ${key}">
<strong>${trans.title}</strong>
<p>${trans.desc}</p>
</div>`
)
.join("")}
</div>
<div class="lowerBar">
<button class="steamLinkButton ${A_B_TESTING_LINK_TYPE}"></button>
<button class="otherCloseButton">${T.ingame.standaloneAdvantages.no_thanks}</button>
</div>
`
);
this.trackClicks(this.contentDiv.querySelector("button.steamLinkButton"), () => {
this.root.app.analytics.trackUiClick("standalone_advantage_visit_steam");
this.root.app.platformWrapper.openExternalLink(
THIRDPARTY_URLS.standaloneStorePage + "?ref=savs&prc=" + A_B_TESTING_LINK_TYPE
);
this.close();
});
this.trackClicks(this.contentDiv.querySelector("button.otherCloseButton"), () => {
this.root.app.analytics.trackUiClick("standalone_advantage_no_thanks");
this.close();
});
}
initialize() {
this.domAttach = new DynamicDomAttach(this.root, this.background, {
attachClass: "visible",
});
this.inputReciever = new InputReceiver("standalone-advantages");
this.close();
this.lastShown = this.root.gameIsFresh ? this.root.time.now() : 0;
}
show() {
this.lastShown = this.root.time.now();
this.visible = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
}
close() {
this.visible = false;
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update();
}
update() {
if (!this.visible && this.root.time.now() - this.lastShown > showIntervalSeconds) {
this.show();
}
this.domAttach.update(this.visible);
}
}

View File

@ -151,17 +151,12 @@ export class HUDStatistics extends BaseHUDPart {
} }
} }
cleanup() {
document.body.classList.remove("ingameDialogOpen");
}
isBlockingOverlay() { isBlockingOverlay() {
return this.visible; return this.visible;
} }
show() { show() {
this.visible = true; this.visible = true;
document.body.classList.add("ingameDialogOpen");
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
this.rerenderFull(); this.rerenderFull();
this.update(); this.update();
@ -169,7 +164,6 @@ export class HUDStatistics extends BaseHUDPart {
close() { close() {
this.visible = false; this.visible = false;
document.body.classList.remove("ingameDialogOpen");
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update(); this.update();
} }

View File

@ -1,109 +1,106 @@
import { InputReceiver } from "../../../core/input_receiver"; import { InputReceiver } from "../../../core/input_receiver";
import { TrackedState } from "../../../core/tracked_state"; import { TrackedState } from "../../../core/tracked_state";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { T } from "../../../translations"; import { T } from "../../../translations";
const tutorialVideos = [2, 3, 4, 5, 6, 7, 9, 10, 11]; const tutorialVideos = [2, 3, 4, 5, 6, 7, 9, 10, 11];
export class HUDPartTutorialHints extends BaseHUDPart { export class HUDPartTutorialHints extends BaseHUDPart {
createElements(parent) { createElements(parent) {
this.element = makeDiv( this.element = makeDiv(
parent, parent,
"ingame_HUD_TutorialHints", "ingame_HUD_TutorialHints",
[], [],
` `
<div class="header"> <div class="header">
<span>${T.ingame.tutorialHints.title}</span> <span>${T.ingame.tutorialHints.title}</span>
<button class="styledButton toggleHint"> <button class="styledButton toggleHint">
<span class="show">${T.ingame.tutorialHints.showHint}</span> <span class="show">${T.ingame.tutorialHints.showHint}</span>
<span class="hide">${T.ingame.tutorialHints.hideHint}</span> <span class="hide">${T.ingame.tutorialHints.hideHint}</span>
</button> </button>
</div> </div>
<video autoplay muted loop class="fullscreenBackgroundVideo"> <video autoplay muted loop class="fullscreenBackgroundVideo">
<source type="video/webm"> <source type="video/webm">
</video> </video>
` `
); );
this.videoElement = this.element.querySelector("video"); this.videoElement = this.element.querySelector("video");
} }
shouldPauseGame() { shouldPauseGame() {
return this.enlarged; return this.enlarged;
} }
initialize() { initialize() {
this.trackClicks(this.element.querySelector(".toggleHint"), this.toggleHintEnlarged); this.trackClicks(this.element.querySelector(".toggleHint"), this.toggleHintEnlarged);
this.videoAttach = new DynamicDomAttach(this.root, this.videoElement, { this.videoAttach = new DynamicDomAttach(this.root, this.videoElement, {
timeToKeepSeconds: 0.3, timeToKeepSeconds: 0.3,
}); });
this.videoAttach.update(false); this.videoAttach.update(false);
this.enlarged = false; this.enlarged = false;
this.inputReciever = new InputReceiver("tutorial_hints"); this.inputReciever = new InputReceiver("tutorial_hints");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.domAttach = new DynamicDomAttach(this.root, this.element); this.domAttach = new DynamicDomAttach(this.root, this.element);
this.currentShownLevel = new TrackedState(this.updateVideoUrl, this); this.currentShownLevel = new TrackedState(this.updateVideoUrl, this);
} }
updateVideoUrl(level) { updateVideoUrl(level) {
if (tutorialVideos.indexOf(level) < 0) { if (tutorialVideos.indexOf(level) < 0) {
this.videoElement.querySelector("source").setAttribute("src", ""); this.videoElement.querySelector("source").setAttribute("src", "");
this.videoElement.pause(); this.videoElement.pause();
} else { } else {
this.videoElement this.videoElement
.querySelector("source") .querySelector("source")
.setAttribute("src", "https://static.shapez.io/tutorial_videos/level_" + level + ".webm"); .setAttribute("src", "https://static.shapez.io/tutorial_videos/level_" + level + ".webm");
this.videoElement.currentTime = 0; this.videoElement.currentTime = 0;
this.videoElement.load(); this.videoElement.load();
} }
} }
close() { close() {
this.enlarged = false; this.enlarged = false;
document.body.classList.remove("ingameDialogOpen"); this.element.classList.remove("enlarged", "noBlur");
this.element.classList.remove("enlarged", "noBlur"); this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.update();
this.update(); }
}
show() {
show() { this.root.app.analytics.trackUiClick("tutorial_hint_show");
this.root.app.analytics.trackUiClick("tutorial_hint_show"); this.root.app.analytics.trackUiClick("tutorial_hint_show_lvl_" + this.root.hubGoals.level);
this.root.app.analytics.trackUiClick("tutorial_hint_show_lvl_" + this.root.hubGoals.level); this.element.classList.add("enlarged", "noBlur");
this.enlarged = true;
document.body.classList.add("ingameDialogOpen"); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
this.element.classList.add("enlarged", "noBlur"); this.update();
this.enlarged = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.videoElement.currentTime = 0;
this.update(); this.videoElement.play();
}
this.videoElement.currentTime = 0;
this.videoElement.play(); update() {
} this.videoAttach.update(this.enlarged);
update() { this.currentShownLevel.set(this.root.hubGoals.level);
this.videoAttach.update(this.enlarged);
const tutorialVisible = tutorialVideos.indexOf(this.root.hubGoals.level) >= 0;
this.currentShownLevel.set(this.root.hubGoals.level); this.domAttach.update(tutorialVisible);
}
const tutorialVisible = tutorialVideos.indexOf(this.root.hubGoals.level) >= 0;
this.domAttach.update(tutorialVisible); toggleHintEnlarged() {
} if (this.enlarged) {
this.close();
toggleHintEnlarged() { } else {
if (this.enlarged) { this.show();
this.close(); }
} else { }
this.show(); }
}
}
}

View File

@ -1,14 +1,15 @@
import { globalConfig } from "../../../core/config"; import { globalConfig } from "../../../core/config";
import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { InputReceiver } from "../../../core/input_receiver";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound"; import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { defaultBuildingVariant } from "../../meta_building"; import { defaultBuildingVariant } from "../../meta_building";
import { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; import { enumNotificationType } from "./notifications";
import { InputReceiver } from "../../../core/input_receiver";
export class HUDUnlockNotification extends BaseHUDPart { export class HUDUnlockNotification extends BaseHUDPart {
initialize() { initialize() {
@ -50,6 +51,18 @@ export class HUDUnlockNotification extends BaseHUDPart {
* @param {enumHubGoalRewards} reward * @param {enumHubGoalRewards} reward
*/ */
showForLevel(level, reward) { showForLevel(level, reward) {
this.root.soundProxy.playUi(SOUNDS.levelComplete);
const levels = this.root.gameMode.getLevelDefinitions();
// Don't use getIsFreeplay() because we want the freeplay level up to show
if (level > levels.length) {
this.root.hud.signals.notification.dispatch(
T.ingame.notifications.freeplayLevelComplete.replace("<level>", String(level)),
enumNotificationType.success
);
return;
}
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace(
"<level>", "<level>",
@ -83,7 +96,6 @@ export class HUDUnlockNotification extends BaseHUDPart {
this.elemContents.innerHTML = html; this.elemContents.innerHTML = html;
this.visible = true; this.visible = true;
this.root.soundProxy.playUi(SOUNDS.levelComplete);
if (this.buttonShowTimeout) { if (this.buttonShowTimeout) {
clearTimeout(this.buttonShowTimeout); clearTimeout(this.buttonShowTimeout);

View File

@ -1,44 +1,72 @@
import { BaseHUDPart } from "../base_hud_part"; import { THIRDPARTY_URLS } from "../../../core/config";
import { DrawParameters } from "../../../core/draw_parameters"; import { makeDiv } from "../../../core/utils";
import { makeDiv } from "../../../core/utils"; import { T } from "../../../translations";
import { THIRDPARTY_URLS } from "../../../core/config"; import { BaseHUDPart } from "../base_hud_part";
import { T } from "../../../translations"; import { DynamicDomAttach } from "../dynamic_dom_attach";
export class HUDWatermark extends BaseHUDPart { const watermarkShowIntervalSeconds = G_IS_DEV ? 120 : 7 * 60;
createElements(parent) { const watermarkShowDuration = 5;
this.element = makeDiv(parent, "ingame_HUD_Watermark");
} export class HUDWatermark extends BaseHUDPart {
createElements(parent) {
initialize() { this.element = makeDiv(
this.trackClicks(this.element, this.onWatermarkClick); parent,
} "ingame_HUD_Watermark",
[],
onWatermarkClick() { `
this.root.app.analytics.trackUiClick("watermark_click_2"); <strong>${T.ingame.watermark.title}</strong>
this.root.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage); <p>${T.ingame.watermark.desc}</p>
} `
);
/**
* this.linkElement = makeDiv(
* @param {DrawParameters} parameters parent,
*/ "ingame_HUD_WatermarkClicker",
drawOverlays(parameters) { [],
const w = this.root.gameWidth; T.ingame.watermark.get_on_steam
const x = 280 * this.root.app.getEffectiveUiScale(); );
this.trackClicks(this.linkElement, () => {
parameters.context.fillStyle = "#f77"; this.root.app.analytics.trackUiClick("watermark_click_2_direct");
parameters.context.font = "bold " + this.root.app.getEffectiveUiScale() * 17 + "px GameFont"; this.root.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage + "?ref=wtmd");
// parameters.context.textAlign = "center"; });
parameters.context.fillText( }
T.demoBanners.title.toUpperCase(),
x, initialize() {
this.root.app.getEffectiveUiScale() * 27 this.trackClicks(this.element, this.onWatermarkClick);
);
this.domAttach = new DynamicDomAttach(this.root, this.element, {
parameters.context.font = "bold " + this.root.app.getEffectiveUiScale() * 12 + "px GameFont"; attachClass: "visible",
// parameters.context.textAlign = "center"; timeToKeepSeconds: 0.5,
parameters.context.fillText(T.demoBanners.intro, x, this.root.app.getEffectiveUiScale() * 45); });
}
// parameters.context.textAlign = "left";
} update() {
} this.domAttach.update(
this.root.time.realtimeNow() % watermarkShowIntervalSeconds < watermarkShowDuration
);
}
onWatermarkClick() {
this.root.app.analytics.trackUiClick("watermark_click_2_new");
this.root.hud.parts.standaloneAdvantages.show();
}
/**
*
* @param {import("../../../core/draw_utils").DrawParameters} parameters
*/
drawOverlays(parameters) {
const w = this.root.gameWidth;
parameters.context.fillStyle = "rgba(230, 230, 230, 0.9)";
parameters.context.font = "bold " + this.root.app.getEffectiveUiScale() * 40 + "px GameFont";
parameters.context.textAlign = "center";
parameters.context.fillText(
T.demoBanners.title.toUpperCase(),
w / 2,
this.root.app.getEffectiveUiScale() * 50
);
parameters.context.textAlign = "left";
}
}

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