mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
merged from upstream
This commit is contained in:
commit
28fef86abe
8
.editorconfig
Executable file
8
.editorconfig
Executable 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
4
.gitattributes
vendored
@ -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
|
||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@ -35,19 +35,23 @@ jobs:
|
||||
cd gulp/
|
||||
yarn
|
||||
cd ..
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
yarn lint
|
||||
|
||||
- name: YAML Lint
|
||||
uses: ibiqlik/action-yamllint@v1.0.0
|
||||
with:
|
||||
file_or_dir: translations/*.yaml
|
||||
|
||||
- name: TSLint
|
||||
run: |
|
||||
cd gulp
|
||||
yarn gulp translations.fullBuild
|
||||
cd ..
|
||||
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
66
.gitignore
vendored
@ -15,34 +15,11 @@ pids
|
||||
*.seed
|
||||
*.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)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
@ -53,18 +30,9 @@ typings/
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
@ -72,41 +40,11 @@ typings/
|
||||
.env
|
||||
.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
|
||||
build
|
||||
res_built
|
||||
|
||||
gulp/runnable-texturepacker.jar
|
||||
tmp_standalone_files
|
||||
|
||||
# Local config
|
||||
|
||||
@ -4,3 +4,4 @@ rules:
|
||||
line-length:
|
||||
level: warning
|
||||
max: 200
|
||||
document-start: disable
|
||||
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal 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"]
|
||||
@ -22,15 +22,14 @@ Your goal is to produce shapes by cutting, rotating, merging and painting parts
|
||||
|
||||
## Building
|
||||
|
||||
- Make sure git `git lfs` extension is on your path
|
||||
- Run `git lfs pull` to download sound assets
|
||||
- Make sure `ffmpeg` is on your path
|
||||
- Install Node.js and Yarn
|
||||
- Install Java (required for textures)
|
||||
- Run `yarn` in the root folder
|
||||
- Cd into `gulp` folder
|
||||
- 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
|
||||
|
||||
@ -116,8 +115,8 @@ This is a quick checklist, if a new building is added this points should be fulf
|
||||
|
||||
### 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">
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
The artwork can be found here:
|
||||
|
||||
https://github.com/tobspr/shapez.io-artwork
|
||||
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "electron",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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",
|
||||
"start": "electron --disable-direct-composition --in-process-gpu ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^6.1.12"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
{
|
||||
"name": "electron",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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",
|
||||
"start": "electron --disable-direct-composition --in-process-gpu ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "10.1.3"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
1533
electron/yarn.lock
1533
electron/yarn.lock
File diff suppressed because it is too large
Load Diff
1
gulp/.gitattributes
vendored
1
gulp/.gitattributes
vendored
@ -1 +0,0 @@
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
3
gulp/.gitignore
vendored
3
gulp/.gitignore
vendored
@ -1,2 +1 @@
|
||||
additional_build_files
|
||||
steampipe
|
||||
additional_build_files
|
||||
|
||||
127
gulp/atlas2json.js
Normal file
127
gulp/atlas2json.js
Normal 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 };
|
||||
@ -25,6 +25,14 @@ module.exports = {
|
||||
});
|
||||
},
|
||||
|
||||
getTag() {
|
||||
try {
|
||||
return execSync("git describe --tag --exact-match").toString("ascii");
|
||||
} catch (e) {
|
||||
throw new Error('Current git HEAD is not a version tag');
|
||||
}
|
||||
},
|
||||
|
||||
getVersion() {
|
||||
return trim(fs.readFileSync(path.join(__dirname, "..", "version")).toString());
|
||||
},
|
||||
|
||||
12
gulp/entitlements.plist
Normal file
12
gulp/entitlements.plist
Normal 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>
|
||||
@ -8,23 +8,6 @@ const path = require("path");
|
||||
const deleteEmpty = require("delete-empty");
|
||||
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
|
||||
const $ = require("gulp-load-plugins")({
|
||||
scope: ["devDependencies"],
|
||||
@ -42,6 +25,10 @@ const envVars = [
|
||||
"SHAPEZ_CLI_STAGING_FTP_PW",
|
||||
"SHAPEZ_CLI_LIVE_FTP_USER",
|
||||
"SHAPEZ_CLI_LIVE_FTP_PW",
|
||||
"SHAPEZ_CLI_APPLE_ID",
|
||||
"SHAPEZ_CLI_APPLE_CERT_NAME",
|
||||
"SHAPEZ_CLI_GITHUB_USER",
|
||||
"SHAPEZ_CLI_GITHUB_TOKEN",
|
||||
];
|
||||
|
||||
for (let i = 0; i < envVars.length; ++i) {
|
||||
@ -78,13 +65,12 @@ docs.gulptasksDocs($, gulp, buildFolder);
|
||||
const standalone = require("./standalone");
|
||||
standalone.gulptasksStandalone($, gulp, buildFolder);
|
||||
|
||||
const releaseUploader = require("./release-uploader");
|
||||
releaseUploader.gulptasksReleaseUploader($, gulp, buildFolder);
|
||||
|
||||
const translations = require("./translations");
|
||||
translations.gulptasksTranslations($, gulp, buildFolder);
|
||||
|
||||
// FIXME
|
||||
// const cordova = require("./cordova");
|
||||
// cordova.gulptasksCordova($, gulp, buildFolder);
|
||||
|
||||
///////////////////// BUILD TASKS /////////////////////
|
||||
|
||||
// Cleans up everything
|
||||
@ -96,8 +82,16 @@ gulp.task("utils.cleanBuildTempFolder", () => {
|
||||
.src(path.join(__dirname, "..", "src", "js", "built-temp"), { read: false, allowEmpty: 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
|
||||
gulp.task("utils.requireCleanWorkingTree", cb => {
|
||||
@ -184,10 +178,12 @@ function serve({ standalone }) {
|
||||
);
|
||||
|
||||
// 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.imageResourcesGlobs, gulp.series("imgres.copyImageResources"));
|
||||
|
||||
// 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"));
|
||||
|
||||
// Watch the build folder and reload when anything changed
|
||||
@ -225,6 +221,8 @@ gulp.task(
|
||||
gulp.series(
|
||||
"utils.cleanup",
|
||||
"utils.copyAdditionalBuildFiles",
|
||||
"imgres.buildAtlas",
|
||||
"imgres.atlasToJson",
|
||||
"imgres.atlas",
|
||||
"sounds.dev",
|
||||
"imgres.copyImageResources",
|
||||
@ -240,12 +238,13 @@ gulp.task(
|
||||
"build.standalone.dev",
|
||||
gulp.series(
|
||||
"utils.cleanup",
|
||||
"imgres.buildAtlas",
|
||||
"imgres.atlasToJson",
|
||||
"imgres.atlas",
|
||||
"sounds.dev",
|
||||
"imgres.copyImageResources",
|
||||
"imgres.copyNonImageResources",
|
||||
"translations.fullBuild",
|
||||
"js.standalone-dev",
|
||||
"css.dev",
|
||||
"html.standalone-dev"
|
||||
)
|
||||
@ -299,6 +298,17 @@ gulp.task(
|
||||
gulp.series("utils.cleanup", "step.standalone-prod.all", "step.postbuild")
|
||||
);
|
||||
|
||||
// OS X build and release upload
|
||||
gulp.task(
|
||||
"build.darwin64-prod",
|
||||
gulp.series(
|
||||
"build.standalone-prod",
|
||||
"standalone.prepare",
|
||||
"standalone.package.prod.darwin64",
|
||||
"standalone.uploadRelease.darwin64"
|
||||
)
|
||||
);
|
||||
|
||||
// Deploying!
|
||||
gulp.task(
|
||||
"main.deploy.alpha",
|
||||
|
||||
26
gulp/html.js
26
gulp/html.js
@ -54,19 +54,19 @@ function gulptasksHTML($, gulp, buildFolder) {
|
||||
document.head.appendChild(css);
|
||||
|
||||
// Append async css
|
||||
const asyncCss = document.createElement("link");
|
||||
asyncCss.rel = "stylesheet";
|
||||
asyncCss.type = "text/css";
|
||||
asyncCss.media = "none";
|
||||
asyncCss.setAttribute("onload", "this.media='all'");
|
||||
asyncCss.href = cachebust("async-resources.css");
|
||||
if (integrity) {
|
||||
asyncCss.setAttribute(
|
||||
"integrity",
|
||||
computeIntegrityHash(path.join(buildFolder, "async-resources.css"))
|
||||
);
|
||||
}
|
||||
document.head.appendChild(asyncCss);
|
||||
// const asyncCss = document.createElement("link");
|
||||
// asyncCss.rel = "stylesheet";
|
||||
// asyncCss.type = "text/css";
|
||||
// asyncCss.media = "none";
|
||||
// asyncCss.setAttribute("onload", "this.media='all'");
|
||||
// asyncCss.href = cachebust("async-resources.css");
|
||||
// if (integrity) {
|
||||
// asyncCss.setAttribute(
|
||||
// "integrity",
|
||||
// computeIntegrityHash(path.join(buildFolder, "async-resources.css"))
|
||||
// );
|
||||
// }
|
||||
// document.head.appendChild(asyncCss);
|
||||
|
||||
if (app) {
|
||||
// Append cordova link
|
||||
|
||||
@ -1,5 +1,15 @@
|
||||
const { existsSync } = require("fs");
|
||||
// @ts-ignore
|
||||
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
|
||||
const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/*.webm"];
|
||||
@ -7,6 +17,9 @@ const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/
|
||||
// Globs for ui resources
|
||||
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) {
|
||||
// Lossless options
|
||||
const minifyImagesOptsLossless = () => [
|
||||
@ -59,6 +72,54 @@ function gulptasksImageResources($, gulp, buildFolder) {
|
||||
|
||||
/////////////// 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
|
||||
gulp.task("imgres.atlas", () => {
|
||||
return gulp.src(["../res_built/atlas/*.png"]).pipe(gulp.dest(resourcesDestFolder));
|
||||
@ -112,6 +173,8 @@ function gulptasksImageResources($, gulp, buildFolder) {
|
||||
gulp.task(
|
||||
"imgres.allOptimized",
|
||||
gulp.parallel(
|
||||
"imgres.buildAtlas",
|
||||
"imgres.atlasToJson",
|
||||
"imgres.atlasOptimized",
|
||||
"imgres.copyNonImageResources",
|
||||
"imgres.copyImageResourcesOptimized"
|
||||
@ -135,6 +198,7 @@ function gulptasksImageResources($, gulp, buildFolder) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
rawImageResourcesGlobs,
|
||||
nonImageResourcesGlobs,
|
||||
imageResourcesGlobs,
|
||||
gulptasksImageResources,
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
"serialize-error": "^3.0.0",
|
||||
"strictdom": "^1.0.1",
|
||||
"string-replace-webpack-plugin": "^0.1.3",
|
||||
"strip-indent": "^3.0.0",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"through2": "^3.0.1",
|
||||
"uglify-template-string-loader": "^1.1.0",
|
||||
@ -66,7 +67,6 @@
|
||||
"babel-plugin-danger-remove-unused-import": "^1.1.2",
|
||||
"css-mqpacker": "^7.0.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"postcss-critical-split": "^2.5.3",
|
||||
"electron-packager": "^14.0.6",
|
||||
"faster.js": "^1.1.0",
|
||||
"glob": "^7.1.3",
|
||||
@ -99,6 +99,7 @@
|
||||
"jimp": "^0.6.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"postcss-assets": "^5.0.0",
|
||||
"postcss-critical-split": "^2.5.3",
|
||||
"postcss-preset-env": "^6.5.0",
|
||||
"postcss-round-subpixels": "^1.2.0",
|
||||
"postcss-unprefix": "^2.1.3",
|
||||
|
||||
66
gulp/release-uploader.js
Normal file
66
gulp/release-uploader.js
Normal 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 };
|
||||
@ -1,8 +1,10 @@
|
||||
require("colors");
|
||||
const packager = require("electron-packager");
|
||||
const path = require("path");
|
||||
const { getVersion } = require("./buildutils");
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
const buildutils = require("./buildutils");
|
||||
const execSync = require("child_process").execSync;
|
||||
|
||||
function gulptasksStandalone($, gulp) {
|
||||
@ -46,6 +48,20 @@ function gulptasksStandalone($, gulp) {
|
||||
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", () => {
|
||||
return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir));
|
||||
});
|
||||
@ -80,8 +96,9 @@ function gulptasksStandalone($, gulp) {
|
||||
* @param {'win32'|'linux'|'darwin'} platform
|
||||
* @param {'x64'|'ia32'} arch
|
||||
* @param {function():void} cb
|
||||
* @param {boolean=} isRelease
|
||||
*/
|
||||
function packageStandalone(platform, arch, cb) {
|
||||
function packageStandalone(platform, arch, cb, isRelease = true) {
|
||||
const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml"));
|
||||
|
||||
packager({
|
||||
@ -99,6 +116,21 @@ function gulptasksStandalone($, gulp) {
|
||||
overwrite: true,
|
||||
appBundleId: "io.shapez.standalone",
|
||||
appCategoryType: "public.app-category.games",
|
||||
...(isRelease &&
|
||||
platform === "darwin" && {
|
||||
osxSign: {
|
||||
"identity": process.env.SHAPEZ_CLI_APPLE_CERT_NAME,
|
||||
"hardened-runtime": true,
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "entitlements.plist",
|
||||
"entitlements-inherit": "entitlements.plist",
|
||||
"signature-flags": "library",
|
||||
},
|
||||
osxNotarize: {
|
||||
appleId: process.env.SHAPEZ_CLI_APPLE_ID,
|
||||
appleIdPassword: "@keychain:SHAPEZ_CLI_APPLE_ID",
|
||||
},
|
||||
}),
|
||||
}).then(
|
||||
appPaths => {
|
||||
console.log("Packages created:", appPaths);
|
||||
@ -123,7 +155,15 @@ function gulptasksStandalone($, gulp) {
|
||||
fs.chmodSync(path.join(appPath, "play.sh"), 0o775);
|
||||
}
|
||||
|
||||
if (platform === "darwin") {
|
||||
if (process.platform === "win32" && platform === "darwin") {
|
||||
console.warn(
|
||||
"Cross-building for macOS on Windows: dereferencing symlinks.\n".red +
|
||||
"This will nearly double app size and make code signature invalid. Sorry!\n"
|
||||
.red.bold +
|
||||
"For more information, see " +
|
||||
"https://github.com/electron/electron-packager/issues/71".underline
|
||||
);
|
||||
|
||||
// Clear up framework folders
|
||||
fs.writeFileSync(
|
||||
path.join(appPath, "play.sh"),
|
||||
@ -175,6 +215,9 @@ function gulptasksStandalone($, gulp) {
|
||||
gulp.task("standalone.package.prod.linux64", cb => packageStandalone("linux", "x64", cb));
|
||||
gulp.task("standalone.package.prod.linux32", cb => packageStandalone("linux", "ia32", cb));
|
||||
gulp.task("standalone.package.prod.darwin64", cb => packageStandalone("darwin", "x64", cb));
|
||||
gulp.task("standalone.package.prod.darwin64.unsigned", cb =>
|
||||
packageStandalone("darwin", "x64", cb, false)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"standalone.package.prod",
|
||||
|
||||
2
gulp/steampipe/.gitignore
vendored
Normal file
2
gulp/steampipe/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
steamtemp
|
||||
app.vdf
|
||||
15
gulp/steampipe/scripts/app.vdf.template
Normal file
15
gulp/steampipe/scripts/app.vdf.template
Normal 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"
|
||||
}
|
||||
}
|
||||
12
gulp/steampipe/scripts/linux.vdf
Normal file
12
gulp/steampipe/scripts/linux.vdf
Normal 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"
|
||||
}
|
||||
12
gulp/steampipe/scripts/windows.vdf
Normal file
12
gulp/steampipe/scripts/windows.vdf
Normal 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"
|
||||
}
|
||||
4
gulp/steampipe/upload.bat
Normal file
4
gulp/steampipe/upload.bat
Normal 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
|
||||
@ -1,22 +1,89 @@
|
||||
const path = require("path");
|
||||
|
||||
const yaml = require("gulp-yaml");
|
||||
|
||||
const translationsSourceDir = path.join(__dirname, "..", "translations");
|
||||
const translationsJsonDir = path.join(__dirname, "..", "src", "js", "built-temp");
|
||||
|
||||
function gulptasksTranslations($, gulp) {
|
||||
gulp.task("translations.convertToJson", () => {
|
||||
return gulp
|
||||
.src(path.join(translationsSourceDir, "*.yaml"))
|
||||
.pipe($.plumber())
|
||||
.pipe(yaml({ space: 2, safe: true }))
|
||||
.pipe(gulp.dest(translationsJsonDir));
|
||||
});
|
||||
|
||||
gulp.task("translations.fullBuild", gulp.series("translations.convertToJson"));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
gulptasksTranslations,
|
||||
};
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const gulpYaml = require("gulp-yaml");
|
||||
const YAML = require("yaml");
|
||||
const stripIndent = require("strip-indent");
|
||||
const trim = require("trim");
|
||||
|
||||
const translationsSourceDir = path.join(__dirname, "..", "translations");
|
||||
const translationsJsonDir = path.join(__dirname, "..", "src", "js", "built-temp");
|
||||
|
||||
function gulptasksTranslations($, gulp) {
|
||||
gulp.task("translations.convertToJson", () => {
|
||||
return gulp
|
||||
.src(path.join(translationsSourceDir, "*.yaml"))
|
||||
.pipe($.plumber())
|
||||
.pipe(gulpYaml({ space: 2, safe: true }))
|
||||
.pipe(gulp.dest(translationsJsonDir));
|
||||
});
|
||||
|
||||
gulp.task("translations.fullBuild", gulp.series("translations.convertToJson"));
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@ -8198,6 +8198,11 @@ min-document@^2.19.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
@ -11945,6 +11950,13 @@ strip-indent@^1.0.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
"yawn-yaml": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "3.0.1",
|
||||
"@typescript-eslint/parser": "3.0.1",
|
||||
"autoprefixer": "^9.4.3",
|
||||
|
||||
@ -1,8 +1,24 @@
|
||||
#ingame_HUD_BetaOverlay {
|
||||
position: fixed;
|
||||
@include S(top, 10px);
|
||||
@include S(right, 15px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: $colorRedBright;
|
||||
@include Heading;
|
||||
text-transform: uppercase;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
@include PlainText;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #555;
|
||||
@include SuperSmallText;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,40 @@
|
||||
#ingame_HUD_BlueprintPlacer {
|
||||
position: absolute;
|
||||
@include S(top, 50px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: #333;
|
||||
z-index: 9999;
|
||||
background: $ingameHudBg;
|
||||
@include S(padding, 5px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
@include S(width, 120px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.label {
|
||||
@include PlainText;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.costContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include Heading;
|
||||
|
||||
> canvas {
|
||||
@include S(margin-left, 5px);
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.canAfford) {
|
||||
background: rgba(98, 27, 41, 0.8);
|
||||
// .costContainer {
|
||||
color: rgb(255, 97, 128);
|
||||
// }
|
||||
}
|
||||
}
|
||||
#ingame_HUD_BlueprintPlacer {
|
||||
position: absolute;
|
||||
@include S(top, 70px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: #333;
|
||||
z-index: 9999;
|
||||
background: $ingameHudBg;
|
||||
@include S(padding, 5px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
@include S(width, 120px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
.label {
|
||||
@include PlainText;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.costContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include Heading;
|
||||
|
||||
> canvas {
|
||||
@include S(margin-left, 5px);
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.canAfford) {
|
||||
background: rgba(98, 27, 41, 0.8);
|
||||
// .costContainer {
|
||||
color: rgb(255, 97, 128);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
|
||||
@include DarkThemeOverride {
|
||||
background-color: rgba(darken($darkModeGameBackground, 15), 0.4);
|
||||
background-color: rgba(darken($darkModeGameBackground, 15), 0.95);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
|
||||
23
src/css/ingame_hud/cat_memes.scss
Normal file
23
src/css/ingame_hud/cat_memes.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -178,6 +178,29 @@
|
||||
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 {
|
||||
@ -220,7 +243,7 @@
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
background: rgba(#fff, 0.6);
|
||||
@include InlineAnimation(5s linear) {
|
||||
@include InlineAnimation(3s linear) {
|
||||
0% {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -17,13 +17,10 @@
|
||||
grid-template-rows: 1fr 1fr;
|
||||
@include S(margin-bottom, 4px);
|
||||
color: #333438;
|
||||
// text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2);
|
||||
|
||||
&.unpinable {
|
||||
> canvas {
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
&.removable {
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
> canvas {
|
||||
@ -31,16 +28,9 @@
|
||||
@include S(height, 25px);
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 3;
|
||||
pointer-events: all;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
transform-origin: D(2px) center;
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
&:hover {
|
||||
transform: scale(2);
|
||||
z-index: 21;
|
||||
}
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> .amountLabel,
|
||||
|
||||
169
src/css/ingame_hud/standalone_advantages.scss
Normal file
169
src/css/ingame_hud/standalone_advantages.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,85 @@
|
||||
#ingame_HUD_Watermark {
|
||||
position: absolute;
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("get_on_steam.png") center center / contain no-repeat;
|
||||
}
|
||||
|
||||
@include S(width, 110px);
|
||||
@include S(height, 40px);
|
||||
@include S(top, 10px);
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include S(top, 70px);
|
||||
pointer-events: all;
|
||||
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 {
|
||||
transform: skewX(-1deg) scale(1.02);
|
||||
opacity: 0.9;
|
||||
transform: translateX(-50%) scale(1.02) !important;
|
||||
}
|
||||
|
||||
> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,8 @@
|
||||
@import "ingame_hud/color_blind_helper";
|
||||
@import "ingame_hud/shape_viewer";
|
||||
@import "ingame_hud/sandbox_controller";
|
||||
@import "ingame_hud/standalone_advantages";
|
||||
@import "ingame_hud/cat_memes";
|
||||
|
||||
// prettier-ignore
|
||||
$elements:
|
||||
@ -71,12 +73,13 @@ ingame_HUD_KeybindingOverlay,
|
||||
ingame_HUD_Notifications,
|
||||
ingame_HUD_DebugInfo,
|
||||
ingame_HUD_EntityDebugger,
|
||||
ingame_HUD_InteractiveTutorial,
|
||||
ingame_HUD_TutorialHints,
|
||||
ingame_HUD_buildings_toolbar,
|
||||
ingame_HUD_InteractiveTutorial,
|
||||
ingame_HUD_BuildingsToolbar,
|
||||
ingame_HUD_wires_toolbar,
|
||||
ingame_HUD_BlueprintPlacer,
|
||||
ingame_HUD_Waypoints_Hint,
|
||||
ingame_HUD_WatermarkClicker,
|
||||
ingame_HUD_Watermark,
|
||||
ingame_HUD_ColorBlindBelowTileHelper,
|
||||
ingame_HUD_SandboxController,
|
||||
@ -88,9 +91,11 @@ ingame_HUD_BetaOverlay,
|
||||
ingame_HUD_Shop,
|
||||
ingame_HUD_Statistics,
|
||||
ingame_HUD_ShapeViewer,
|
||||
ingame_HUD_StandaloneAdvantages,
|
||||
ingame_HUD_UnlockNotification,
|
||||
ingame_HUD_SettingsMenu,
|
||||
ingame_HUD_ModalDialogs;
|
||||
ingame_HUD_ModalDialogs,
|
||||
ingame_HUD_CatMemes;
|
||||
|
||||
$zindex: 100;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire,
|
||||
constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage,
|
||||
transistor, analyzer, comparator;
|
||||
transistor, analyzer, comparator, item_producer;
|
||||
|
||||
@each $building in $buildings {
|
||||
[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,
|
||||
cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage,
|
||||
reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not,
|
||||
logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker,
|
||||
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored;
|
||||
logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, item_producer,
|
||||
virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator;
|
||||
@each $building in $buildingsAndVariants {
|
||||
[data-icon="building_tutorials/#{$building}.png"] {
|
||||
/* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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;
|
||||
}
|
||||
|
||||
@ -133,10 +133,7 @@
|
||||
width: 100%;
|
||||
@include S(height, 40px);
|
||||
@include S(width, 180px);
|
||||
& {
|
||||
/* @load-async */
|
||||
background: #171a23 uiResource("get_on_steam.png") center center / contain no-repeat;
|
||||
}
|
||||
background: #171a23 center center / contain no-repeat;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
text-indent: -999em;
|
||||
|
||||
@ -29,6 +29,7 @@ import { MobileWarningState } from "./states/mobile_warning";
|
||||
import { PreloadState } from "./states/preload";
|
||||
import { SettingsState } from "./states/settings";
|
||||
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||
import { RestrictionManager } from "./core/restriction_manager";
|
||||
|
||||
/**
|
||||
* @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface
|
||||
@ -70,6 +71,9 @@ export class Application {
|
||||
this.inputMgr = new InputDistributor(this);
|
||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||
|
||||
// Restrictions (Like demo etc)
|
||||
this.restrictionMgr = new RestrictionManager(this);
|
||||
|
||||
// Platform dependent stuff
|
||||
|
||||
/** @type {StorageInterface} */
|
||||
|
||||
@ -1,51 +1,9 @@
|
||||
export const CHANGELOG = [
|
||||
{
|
||||
version: "1.2.0",
|
||||
date: "unreleased",
|
||||
date: "09.10.2020",
|
||||
entries: [
|
||||
"WIRES",
|
||||
"Reworked menu UI design (by dengr1605)",
|
||||
"Allow holding ALT in belt planner to reverse direction (by jakobhellermann)",
|
||||
"Clear cursor when trying to pipette the same building twice (by hexy)",
|
||||
"Fixed level 18 stacker bug: If you experienced it already, you know it, if not, I don't want to spoiler (by hexy)",
|
||||
"Added keybinding to close menus (by isaisstillalive / Sandwichs-del)",
|
||||
"Fix rare crash regarding the buildings toolbar (by isaisstillalive)",
|
||||
"Fixed some phrases (by EnderDoom77)",
|
||||
"Zoom towards mouse cursor (by Dimava)",
|
||||
"Added multiple settings to optimize the performance",
|
||||
"Updated the soundtrack again, it is now 40 minutes in total!",
|
||||
"Added a button to the statistics dialog to disable the sorting (by squeek502)",
|
||||
"Tier 2 tunnels are now 9 tiles wide, so the gap between is 8 tiles (double the tier 1 range)",
|
||||
"Updated and added new translations (Thanks to all contributors!)",
|
||||
"Show connected chained miners on hover",
|
||||
"Added setting to be able to delete buildings while placing (inspired by hexy)",
|
||||
"You can now adjust the sound and music volumes! (inspired by Yoshie2000)",
|
||||
"Some hud elements now have reduced opacity when hovering, so you can see through (inspired by mvb005)",
|
||||
"Mark pinned shapes in statistics dialog and show them first (inspired by davidburhans)",
|
||||
"Added setting to show chunk borders",
|
||||
"Quad painters have been reworked! They now are integrated with the wires, and only paint the shape when the value is 1 (inspired by dengr1605)",
|
||||
"There are now compact 1x1 balancers available to be unlocked!",
|
||||
"Replaced level completion sound to be less distracting",
|
||||
"Allow editing waypoints (by isaisstillalive)",
|
||||
"Show confirmation when cutting area which is too expensive to get pasted again (by isaisstillalive)",
|
||||
"Show mouse and camera tile on debug overlay (F4) (by dengr)",
|
||||
"Fix belt planner placing the belt when a dialog opens in the meantime",
|
||||
"Added confirmation when deleting a savegame",
|
||||
"Make chained mainer the default and only option after unlocking it",
|
||||
"Fixed tunnels entrances connecting to exits sometimes when they shouldn't",
|
||||
"You can now pan the map with your mouse by moving the cursor to the edges of the screen!",
|
||||
"Added setting to auto select the extractor when pipetting a resource patch (by Exund)",
|
||||
"You can now change the unit (seconds / minutes / hours) in the statistics dialog",
|
||||
"The initial belt planner direction is now based on the cursor movement (by MizardX)",
|
||||
"Fix preferred variant not getting saved when clicking on the hud (by Danacus)",
|
||||
],
|
||||
},
|
||||
{
|
||||
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!",
|
||||
"⚠️⚠️This update is HUGE, view the full changelog <a href='https://shapez.io/wires/' target='_blank'>here</a>! ⚠️⚠️",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,215 +1,232 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { Loader } from "./loader";
|
||||
import { createLogger } from "./logging";
|
||||
import { Signal } from "./signal";
|
||||
import { SOUNDS, MUSIC } from "../platform/sound";
|
||||
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
|
||||
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
|
||||
|
||||
const logger = createLogger("background_loader");
|
||||
|
||||
const essentialMainMenuSprites = [
|
||||
"logo.png",
|
||||
...G_ALL_UI_IMAGES.filter(src => src.startsWith("ui/") && src.indexOf(".gif") < 0),
|
||||
];
|
||||
const essentialMainMenuSounds = [
|
||||
SOUNDS.uiClick,
|
||||
SOUNDS.uiError,
|
||||
SOUNDS.dialogError,
|
||||
SOUNDS.dialogOk,
|
||||
SOUNDS.swishShow,
|
||||
SOUNDS.swishHide,
|
||||
];
|
||||
|
||||
const essentialBareGameAtlases = atlasFiles;
|
||||
const essentialBareGameSprites = G_ALL_UI_IMAGES.filter(src => src.indexOf(".gif") < 0);
|
||||
const essentialBareGameSounds = [MUSIC.theme];
|
||||
|
||||
const additionalGameSprites = [];
|
||||
// @ts-ignore
|
||||
const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)];
|
||||
|
||||
export class BackgroundResourcesLoader {
|
||||
/**
|
||||
*
|
||||
* @param {Application} app
|
||||
*/
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.registerReady = false;
|
||||
this.mainMenuReady = false;
|
||||
this.bareGameReady = false;
|
||||
this.additionalReady = false;
|
||||
|
||||
this.signalMainMenuLoaded = new Signal();
|
||||
this.signalBareGameLoaded = new Signal();
|
||||
this.signalAdditionalLoaded = new Signal();
|
||||
|
||||
this.numAssetsLoaded = 0;
|
||||
this.numAssetsToLoadTotal = 0;
|
||||
|
||||
// Avoid loading stuff twice
|
||||
this.spritesLoaded = [];
|
||||
this.soundsLoaded = [];
|
||||
}
|
||||
|
||||
getNumAssetsLoaded() {
|
||||
return this.numAssetsLoaded;
|
||||
}
|
||||
|
||||
getNumAssetsTotal() {
|
||||
return this.numAssetsToLoadTotal;
|
||||
}
|
||||
|
||||
getPromiseForMainMenu() {
|
||||
if (this.mainMenuReady) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.signalMainMenuLoaded.add(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
getPromiseForBareGame() {
|
||||
if (this.bareGameReady) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.signalBareGameLoaded.add(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
startLoading() {
|
||||
this.internalStartLoadingEssentialsForMainMenu();
|
||||
}
|
||||
|
||||
internalStartLoadingEssentialsForMainMenu() {
|
||||
logger.log("⏰ Start load: main menu");
|
||||
this.internalLoadSpritesAndSounds(essentialMainMenuSprites, essentialMainMenuSounds)
|
||||
.catch(err => {
|
||||
logger.warn("⏰ Failed to load essentials for main menu:", err);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("⏰ Finish load: main menu");
|
||||
this.mainMenuReady = true;
|
||||
this.signalMainMenuLoaded.dispatch();
|
||||
this.internalStartLoadingEssentialsForBareGame();
|
||||
});
|
||||
}
|
||||
|
||||
internalStartLoadingEssentialsForBareGame() {
|
||||
logger.log("⏰ Start load: bare game");
|
||||
this.internalLoadSpritesAndSounds(
|
||||
essentialBareGameSprites,
|
||||
essentialBareGameSounds,
|
||||
essentialBareGameAtlases
|
||||
)
|
||||
.catch(err => {
|
||||
logger.warn("⏰ Failed to load essentials for bare game:", err);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("⏰ Finish load: bare game");
|
||||
this.bareGameReady = true;
|
||||
initBuildingCodesAfterResourcesLoaded();
|
||||
this.signalBareGameLoaded.dispatch();
|
||||
this.internalStartLoadingAdditionalGameAssets();
|
||||
});
|
||||
}
|
||||
|
||||
internalStartLoadingAdditionalGameAssets() {
|
||||
const additionalAtlases = [];
|
||||
logger.log("⏰ Start load: additional assets (", additionalAtlases.length, "images)");
|
||||
this.internalLoadSpritesAndSounds(additionalGameSprites, additionalGameSounds, additionalAtlases)
|
||||
.catch(err => {
|
||||
logger.warn("⏰ Failed to load additional assets:", err);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("⏰ Finish load: additional assets");
|
||||
this.additionalReady = true;
|
||||
this.signalAdditionalLoaded.dispatch();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} sprites
|
||||
* @param {Array<string>} sounds
|
||||
* @param {Array<AtlasDefinition>} atlases
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
internalLoadSpritesAndSounds(sprites, sounds, atlases = []) {
|
||||
this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length;
|
||||
this.numAssetsLoaded = 0;
|
||||
|
||||
let promises = [];
|
||||
|
||||
for (let i = 0; i < sounds.length; ++i) {
|
||||
if (this.soundsLoaded.indexOf(sounds[i]) >= 0) {
|
||||
// Already loaded
|
||||
continue;
|
||||
}
|
||||
|
||||
this.soundsLoaded.push(sounds[i]);
|
||||
promises.push(
|
||||
this.app.sound
|
||||
.loadSound(sounds[i])
|
||||
.catch(err => {
|
||||
logger.warn("Failed to load sound:", sounds[i]);
|
||||
})
|
||||
.then(() => {
|
||||
this.numAssetsLoaded++;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < sprites.length; ++i) {
|
||||
if (this.spritesLoaded.indexOf(sprites[i]) >= 0) {
|
||||
// Already loaded
|
||||
continue;
|
||||
}
|
||||
this.spritesLoaded.push(sprites[i]);
|
||||
promises.push(
|
||||
Loader.preloadCSSSprite(sprites[i])
|
||||
.catch(err => {
|
||||
logger.warn("Failed to load css sprite:", sprites[i]);
|
||||
})
|
||||
.then(() => {
|
||||
this.numAssetsLoaded++;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < atlases.length; ++i) {
|
||||
const atlas = atlases[i];
|
||||
promises.push(
|
||||
Loader.preloadAtlas(atlas)
|
||||
.catch(err => {
|
||||
logger.warn("Failed to load atlas:", atlas.sourceFileName);
|
||||
})
|
||||
.then(() => {
|
||||
this.numAssetsLoaded++;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
Promise.all(promises)
|
||||
|
||||
// // Remove some pressure by waiting a bit
|
||||
// .then(() => {
|
||||
// return new Promise(resolve => {
|
||||
// setTimeout(resolve, 200);
|
||||
// });
|
||||
// })
|
||||
.then(() => {
|
||||
this.numAssetsToLoadTotal = 0;
|
||||
this.numAssetsLoaded = 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { Loader } from "./loader";
|
||||
import { createLogger } from "./logging";
|
||||
import { Signal } from "./signal";
|
||||
import { SOUNDS, MUSIC } from "../platform/sound";
|
||||
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
|
||||
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
|
||||
import { cachebust } from "./cachebust";
|
||||
|
||||
const logger = createLogger("background_loader");
|
||||
|
||||
const essentialMainMenuSprites = [
|
||||
"logo.png",
|
||||
...G_ALL_UI_IMAGES.filter(src => src.startsWith("ui/") && src.indexOf(".gif") < 0),
|
||||
];
|
||||
const essentialMainMenuSounds = [
|
||||
SOUNDS.uiClick,
|
||||
SOUNDS.uiError,
|
||||
SOUNDS.dialogError,
|
||||
SOUNDS.dialogOk,
|
||||
SOUNDS.swishShow,
|
||||
SOUNDS.swishHide,
|
||||
];
|
||||
|
||||
const essentialBareGameAtlases = atlasFiles;
|
||||
const essentialBareGameSprites = G_ALL_UI_IMAGES.filter(src => src.indexOf(".gif") < 0);
|
||||
const essentialBareGameSounds = [MUSIC.theme];
|
||||
|
||||
const additionalGameSprites = [];
|
||||
// @ts-ignore
|
||||
const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)];
|
||||
|
||||
export class BackgroundResourcesLoader {
|
||||
/**
|
||||
*
|
||||
* @param {Application} app
|
||||
*/
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.registerReady = false;
|
||||
this.mainMenuReady = false;
|
||||
this.bareGameReady = false;
|
||||
this.additionalReady = false;
|
||||
|
||||
this.signalMainMenuLoaded = new Signal();
|
||||
this.signalBareGameLoaded = new Signal();
|
||||
this.signalAdditionalLoaded = new Signal();
|
||||
|
||||
this.numAssetsLoaded = 0;
|
||||
this.numAssetsToLoadTotal = 0;
|
||||
|
||||
// Avoid loading stuff twice
|
||||
this.spritesLoaded = [];
|
||||
this.soundsLoaded = [];
|
||||
}
|
||||
|
||||
getNumAssetsLoaded() {
|
||||
return this.numAssetsLoaded;
|
||||
}
|
||||
|
||||
getNumAssetsTotal() {
|
||||
return this.numAssetsToLoadTotal;
|
||||
}
|
||||
|
||||
getPromiseForMainMenu() {
|
||||
if (this.mainMenuReady) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.signalMainMenuLoaded.add(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
getPromiseForBareGame() {
|
||||
if (this.bareGameReady) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.signalBareGameLoaded.add(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
startLoading() {
|
||||
this.internalStartLoadingEssentialsForMainMenu();
|
||||
}
|
||||
|
||||
internalStartLoadingEssentialsForMainMenu() {
|
||||
logger.log("⏰ Start load: main menu");
|
||||
this.internalLoadSpritesAndSounds(essentialMainMenuSprites, essentialMainMenuSounds)
|
||||
.catch(err => {
|
||||
logger.warn("⏰ Failed to load essentials for main menu:", err);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("⏰ Finish load: main menu");
|
||||
this.mainMenuReady = true;
|
||||
this.signalMainMenuLoaded.dispatch();
|
||||
this.internalStartLoadingEssentialsForBareGame();
|
||||
});
|
||||
}
|
||||
|
||||
internalStartLoadingEssentialsForBareGame() {
|
||||
logger.log("⏰ Start load: bare game");
|
||||
this.internalLoadSpritesAndSounds(
|
||||
essentialBareGameSprites,
|
||||
essentialBareGameSounds,
|
||||
essentialBareGameAtlases
|
||||
)
|
||||
.then(() => this.internalPreloadCss("async-resources.scss"))
|
||||
.catch(err => {
|
||||
logger.warn("⏰ Failed to load essentials for bare game:", err);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("⏰ Finish load: bare game");
|
||||
this.bareGameReady = true;
|
||||
initBuildingCodesAfterResourcesLoaded();
|
||||
this.signalBareGameLoaded.dispatch();
|
||||
this.internalStartLoadingAdditionalGameAssets();
|
||||
});
|
||||
}
|
||||
|
||||
internalStartLoadingAdditionalGameAssets() {
|
||||
const additionalAtlases = [];
|
||||
logger.log("⏰ Start load: additional assets (", additionalAtlases.length, "images)");
|
||||
this.internalLoadSpritesAndSounds(additionalGameSprites, additionalGameSounds, additionalAtlases)
|
||||
.catch(err => {
|
||||
logger.warn("⏰ Failed to load additional assets:", err);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("⏰ Finish load: additional assets");
|
||||
this.additionalReady = true;
|
||||
this.signalAdditionalLoaded.dispatch();
|
||||
});
|
||||
}
|
||||
|
||||
internalPreloadCss(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const link = document.createElement("link");
|
||||
|
||||
link.onload = resolve;
|
||||
link.onerror = reject;
|
||||
|
||||
link.setAttribute("rel", "stylesheet");
|
||||
link.setAttribute("media", "all");
|
||||
link.setAttribute("type", "text/css");
|
||||
link.setAttribute("href", cachebust("async-resources.css"));
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<string>} sprites
|
||||
* @param {Array<string>} sounds
|
||||
* @param {Array<AtlasDefinition>} atlases
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
internalLoadSpritesAndSounds(sprites, sounds, atlases = []) {
|
||||
this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length;
|
||||
this.numAssetsLoaded = 0;
|
||||
|
||||
let promises = [];
|
||||
|
||||
for (let i = 0; i < sounds.length; ++i) {
|
||||
if (this.soundsLoaded.indexOf(sounds[i]) >= 0) {
|
||||
// Already loaded
|
||||
continue;
|
||||
}
|
||||
|
||||
this.soundsLoaded.push(sounds[i]);
|
||||
promises.push(
|
||||
this.app.sound
|
||||
.loadSound(sounds[i])
|
||||
.catch(err => {
|
||||
logger.warn("Failed to load sound:", sounds[i]);
|
||||
})
|
||||
.then(() => {
|
||||
this.numAssetsLoaded++;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < sprites.length; ++i) {
|
||||
if (this.spritesLoaded.indexOf(sprites[i]) >= 0) {
|
||||
// Already loaded
|
||||
continue;
|
||||
}
|
||||
this.spritesLoaded.push(sprites[i]);
|
||||
promises.push(
|
||||
Loader.preloadCSSSprite(sprites[i])
|
||||
.catch(err => {
|
||||
logger.warn("Failed to load css sprite:", sprites[i]);
|
||||
})
|
||||
.then(() => {
|
||||
this.numAssetsLoaded++;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < atlases.length; ++i) {
|
||||
const atlas = atlases[i];
|
||||
promises.push(
|
||||
Loader.preloadAtlas(atlas)
|
||||
.catch(err => {
|
||||
logger.warn("Failed to load atlas:", atlas.sourceFileName);
|
||||
})
|
||||
.then(() => {
|
||||
this.numAssetsLoaded++;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
Promise.all(promises)
|
||||
|
||||
// // Remove some pressure by waiting a bit
|
||||
// .then(() => {
|
||||
// return new Promise(resolve => {
|
||||
// setTimeout(resolve, 200);
|
||||
// });
|
||||
// })
|
||||
.then(() => {
|
||||
this.numAssetsToLoadTotal = 0;
|
||||
this.numAssetsLoaded = 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,467 +1,465 @@
|
||||
import { createLogger } from "../core/logging";
|
||||
import { Signal } from "../core/signal";
|
||||
import { fastArrayDelete, fastArrayDeleteValueIfContained } from "./utils";
|
||||
import { Vector } from "./vector";
|
||||
import { IS_MOBILE, SUPPORT_TOUCH } from "./config";
|
||||
import { SOUNDS } from "../platform/sound";
|
||||
import { GLOBAL_APP } from "./globals";
|
||||
|
||||
const logger = createLogger("click_detector");
|
||||
|
||||
export const MAX_MOVE_DISTANCE_PX = IS_MOBILE ? 20 : 80;
|
||||
|
||||
// For debugging
|
||||
const registerClickDetectors = G_IS_DEV && true;
|
||||
if (registerClickDetectors) {
|
||||
/** @type {Array<ClickDetector>} */
|
||||
window.activeClickDetectors = [];
|
||||
}
|
||||
|
||||
// Store active click detectors so we can cancel them
|
||||
/** @type {Array<ClickDetector>} */
|
||||
const ongoingClickDetectors = [];
|
||||
|
||||
// Store when the last touch event was registered, to avoid accepting a touch *and* a click event
|
||||
|
||||
export let clickDetectorGlobals = {
|
||||
lastTouchTime: -1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Click detector creation payload typehints
|
||||
* @typedef {{
|
||||
* consumeEvents?: boolean,
|
||||
* preventDefault?: boolean,
|
||||
* applyCssClass?: string,
|
||||
* captureTouchmove?: boolean,
|
||||
* targetOnly?: boolean,
|
||||
* maxDistance?: number,
|
||||
* clickSound?: string,
|
||||
* preventClick?: boolean,
|
||||
* }} ClickDetectorConstructorArgs
|
||||
*/
|
||||
|
||||
// Detects clicks
|
||||
export class ClickDetector {
|
||||
/**
|
||||
*
|
||||
* @param {Element} element
|
||||
* @param {object} param1
|
||||
* @param {boolean=} param1.consumeEvents Whether to call stopPropagation
|
||||
* (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 {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.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 {string=} param1.clickSound Sound key to play on touchdown
|
||||
* @param {boolean=} param1.preventClick Whether to prevent click events
|
||||
*/
|
||||
constructor(
|
||||
element,
|
||||
{
|
||||
consumeEvents = false,
|
||||
preventDefault = true,
|
||||
applyCssClass = "pressed",
|
||||
captureTouchmove = false,
|
||||
targetOnly = false,
|
||||
maxDistance = MAX_MOVE_DISTANCE_PX,
|
||||
clickSound = SOUNDS.uiClick,
|
||||
preventClick = false,
|
||||
}
|
||||
) {
|
||||
assert(element, "No element given!");
|
||||
this.clickDownPosition = null;
|
||||
|
||||
this.consumeEvents = consumeEvents;
|
||||
this.preventDefault = preventDefault;
|
||||
this.applyCssClass = applyCssClass;
|
||||
this.captureTouchmove = captureTouchmove;
|
||||
this.targetOnly = targetOnly;
|
||||
this.clickSound = clickSound;
|
||||
this.maxDistance = maxDistance;
|
||||
this.preventClick = preventClick;
|
||||
|
||||
// Signals
|
||||
this.click = new Signal();
|
||||
this.rightClick = new Signal();
|
||||
this.touchstart = new Signal();
|
||||
this.touchmove = new Signal();
|
||||
this.touchend = new Signal();
|
||||
this.touchcancel = new Signal();
|
||||
|
||||
// Simple signals which just receive the touch position
|
||||
this.touchstartSimple = new Signal();
|
||||
this.touchmoveSimple = new Signal();
|
||||
this.touchendSimple = new Signal();
|
||||
|
||||
// Store time of touch start
|
||||
this.clickStartTime = null;
|
||||
|
||||
// A click can be cancelled if another detector registers a click
|
||||
this.cancelled = false;
|
||||
|
||||
this.internalBindTo(/** @type {HTMLElement} */ (element));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all event listeners of this detector
|
||||
*/
|
||||
cleanup() {
|
||||
if (this.element) {
|
||||
if (registerClickDetectors) {
|
||||
const index = window.activeClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.error("Click detector cleanup but is not active");
|
||||
} else {
|
||||
window.activeClickDetectors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
const options = this.internalGetEventListenerOptions();
|
||||
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchstart", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("touchend", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options);
|
||||
}
|
||||
|
||||
this.element.removeEventListener("mouseup", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("mousedown", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("mouseout", this.handlerTouchCancel, options);
|
||||
|
||||
if (this.captureTouchmove) {
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchmove", this.handlerTouchMove, options);
|
||||
}
|
||||
this.element.removeEventListener("mousemove", this.handlerTouchMove, options);
|
||||
}
|
||||
|
||||
if (this.preventClick) {
|
||||
this.element.removeEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
|
||||
this.click.removeAll();
|
||||
this.touchstart.removeAll();
|
||||
this.touchmove.removeAll();
|
||||
this.touchend.removeAll();
|
||||
this.touchcancel.removeAll();
|
||||
|
||||
// TODO: Remove pointer captures
|
||||
|
||||
this.element = null;
|
||||
}
|
||||
}
|
||||
|
||||
// INTERNAL METHODS
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
internalPreventClick(event) {
|
||||
window.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to get the options to pass to an event listener
|
||||
*/
|
||||
internalGetEventListenerOptions() {
|
||||
return {
|
||||
capture: this.consumeEvents,
|
||||
passive: !this.preventDefault,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the click detector to an element
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
internalBindTo(element) {
|
||||
const options = this.internalGetEventListenerOptions();
|
||||
|
||||
this.handlerTouchStart = this.internalOnPointerDown.bind(this);
|
||||
this.handlerTouchEnd = this.internalOnPointerEnd.bind(this);
|
||||
this.handlerTouchMove = this.internalOnPointerMove.bind(this);
|
||||
this.handlerTouchCancel = this.internalOnTouchCancel.bind(this);
|
||||
|
||||
if (this.preventClick) {
|
||||
this.handlerPreventClick = this.internalPreventClick.bind(this);
|
||||
element.addEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
|
||||
if (SUPPORT_TOUCH) {
|
||||
element.addEventListener("touchstart", this.handlerTouchStart, 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("mouseout", this.handlerTouchCancel, options);
|
||||
|
||||
if (this.captureTouchmove) {
|
||||
if (SUPPORT_TOUCH) {
|
||||
element.addEventListener("touchmove", this.handlerTouchMove, options);
|
||||
}
|
||||
element.addEventListener("mousemove", this.handlerTouchMove, options);
|
||||
}
|
||||
|
||||
if (registerClickDetectors) {
|
||||
window.activeClickDetectors.push(this);
|
||||
}
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the bound element is currently in the DOM.
|
||||
*/
|
||||
internalIsDomElementAttached() {
|
||||
return this.element && document.documentElement.contains(this.element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given event is relevant for this detector
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalEventPreHandler(event, expectedRemainingTouches = 1) {
|
||||
if (!this.element) {
|
||||
// Already cleaned up
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.targetOnly && event.target !== this.element) {
|
||||
// Clicked a child element
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop any propagation and defaults if configured
|
||||
if (this.consumeEvents && event.cancelable) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (this.preventDefault && event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
clickDetectorGlobals.lastTouchTime = performance.now();
|
||||
|
||||
// console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches);
|
||||
if (event.targetTouches.length !== expectedRemainingTouches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the mous position from an event
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
* @returns {Vector} The client space position
|
||||
*/
|
||||
static extractPointerPosition(event) {
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
if (event.changedTouches.length !== 1) {
|
||||
logger.warn(
|
||||
"Got unexpected target touches:",
|
||||
event.targetTouches.length,
|
||||
"->",
|
||||
event.targetTouches
|
||||
);
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
const touch = event.changedTouches[0];
|
||||
return new Vector(touch.clientX, touch.clientY);
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
return new Vector(event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
assertAlways(false, "Got unknown event: " + event);
|
||||
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cacnels all ongoing events on this detector
|
||||
*/
|
||||
cancelOngoingEvents() {
|
||||
if (this.applyCssClass && this.element) {
|
||||
this.element.classList.remove(this.applyCssClass);
|
||||
}
|
||||
this.clickDownPosition = null;
|
||||
this.clickStartTime = null;
|
||||
this.cancelled = true;
|
||||
fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer down handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnPointerDown(event) {
|
||||
window.focus();
|
||||
|
||||
if (!this.internalEventPreHandler(event, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const position = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick = event.button === 2;
|
||||
if (isRightClick) {
|
||||
// Ignore right clicks
|
||||
this.rightClick.dispatch(position, event);
|
||||
this.cancelled = true;
|
||||
this.clickDownPosition = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clickDownPosition) {
|
||||
logger.warn("Ignoring double click");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancelled = false;
|
||||
this.touchstart.dispatch(event);
|
||||
|
||||
// Store where the touch started
|
||||
this.clickDownPosition = position;
|
||||
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) {
|
||||
ongoingClickDetectors.push(this);
|
||||
} else {
|
||||
logger.warn("Click detector got pointer down of active pointer twice");
|
||||
}
|
||||
|
||||
// If we should apply any classes, do this now
|
||||
if (this.applyCssClass) {
|
||||
this.element.classList.add(this.applyCssClass);
|
||||
}
|
||||
|
||||
// If we should play any sound, do this
|
||||
if (this.clickSound) {
|
||||
GLOBAL_APP.sound.playUiSound(this.clickSound);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer move handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnPointerMove(event) {
|
||||
if (!this.internalEventPreHandler(event, 1)) {
|
||||
return false;
|
||||
}
|
||||
this.touchmove.dispatch(event);
|
||||
const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
|
||||
this.touchmoveSimple.dispatch(pos.x, pos.y);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer end handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnPointerEnd(event) {
|
||||
window.focus();
|
||||
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchend on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick = event.button === 2;
|
||||
if (isRightClick) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const index = ongoingClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.warn("Got pointer end but click detector is not in pressed state");
|
||||
} else {
|
||||
fastArrayDelete(ongoingClickDetectors, index);
|
||||
}
|
||||
|
||||
let dispatchClick = false;
|
||||
let dispatchClickPos = null;
|
||||
|
||||
// Check for correct down position, otherwise must have pinched or so
|
||||
if (this.clickDownPosition) {
|
||||
const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
|
||||
const distance = pos.distance(this.clickDownPosition);
|
||||
if (!IS_MOBILE || distance <= this.maxDistance) {
|
||||
dispatchClick = true;
|
||||
dispatchClickPos = pos;
|
||||
} else {
|
||||
console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")");
|
||||
}
|
||||
}
|
||||
|
||||
this.clickDownPosition = null;
|
||||
this.clickStartTime = null;
|
||||
|
||||
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
|
||||
if (this.internalIsDomElementAttached()) {
|
||||
this.touchend.dispatch(event);
|
||||
this.touchendSimple.dispatch();
|
||||
|
||||
if (dispatchClick) {
|
||||
const detectors = ongoingClickDetectors.slice();
|
||||
for (let i = 0; i < detectors.length; ++i) {
|
||||
detectors[i].cancelOngoingEvents();
|
||||
}
|
||||
this.click.dispatch(dispatchClickPos, event);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal touch cancel handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnTouchCancel(event) {
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchcancel on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancelOngoingEvents();
|
||||
this.touchcancel.dispatch(event);
|
||||
this.touchendSimple.dispatch(event);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
import { createLogger } from "../core/logging";
|
||||
import { Signal } from "../core/signal";
|
||||
import { fastArrayDelete, fastArrayDeleteValueIfContained } from "./utils";
|
||||
import { Vector } from "./vector";
|
||||
import { IS_MOBILE, SUPPORT_TOUCH } from "./config";
|
||||
import { SOUNDS } from "../platform/sound";
|
||||
import { GLOBAL_APP } from "./globals";
|
||||
|
||||
const logger = createLogger("click_detector");
|
||||
|
||||
export const MAX_MOVE_DISTANCE_PX = IS_MOBILE ? 20 : 80;
|
||||
|
||||
// For debugging
|
||||
const registerClickDetectors = G_IS_DEV && true;
|
||||
if (registerClickDetectors) {
|
||||
/** @type {Array<ClickDetector>} */
|
||||
window.activeClickDetectors = [];
|
||||
}
|
||||
|
||||
// Store active click detectors so we can cancel them
|
||||
/** @type {Array<ClickDetector>} */
|
||||
const ongoingClickDetectors = [];
|
||||
|
||||
// Store when the last touch event was registered, to avoid accepting a touch *and* a click event
|
||||
|
||||
export let clickDetectorGlobals = {
|
||||
lastTouchTime: -1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Click detector creation payload typehints
|
||||
* @typedef {{
|
||||
* consumeEvents?: boolean,
|
||||
* preventDefault?: boolean,
|
||||
* applyCssClass?: string,
|
||||
* captureTouchmove?: boolean,
|
||||
* targetOnly?: boolean,
|
||||
* maxDistance?: number,
|
||||
* clickSound?: string,
|
||||
* preventClick?: boolean,
|
||||
* }} ClickDetectorConstructorArgs
|
||||
*/
|
||||
|
||||
// Detects clicks
|
||||
export class ClickDetector {
|
||||
/**
|
||||
*
|
||||
* @param {Element} element
|
||||
* @param {object} param1
|
||||
* @param {boolean=} param1.consumeEvents Whether to call stopPropagation
|
||||
* (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 {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.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 {string=} param1.clickSound Sound key to play on touchdown
|
||||
* @param {boolean=} param1.preventClick Whether to prevent click events
|
||||
*/
|
||||
constructor(
|
||||
element,
|
||||
{
|
||||
consumeEvents = false,
|
||||
preventDefault = true,
|
||||
applyCssClass = "pressed",
|
||||
captureTouchmove = false,
|
||||
targetOnly = false,
|
||||
maxDistance = MAX_MOVE_DISTANCE_PX,
|
||||
clickSound = SOUNDS.uiClick,
|
||||
preventClick = false,
|
||||
}
|
||||
) {
|
||||
assert(element, "No element given!");
|
||||
this.clickDownPosition = null;
|
||||
|
||||
this.consumeEvents = consumeEvents;
|
||||
this.preventDefault = preventDefault;
|
||||
this.applyCssClass = applyCssClass;
|
||||
this.captureTouchmove = captureTouchmove;
|
||||
this.targetOnly = targetOnly;
|
||||
this.clickSound = clickSound;
|
||||
this.maxDistance = maxDistance;
|
||||
this.preventClick = preventClick;
|
||||
|
||||
// Signals
|
||||
this.click = new Signal();
|
||||
this.rightClick = new Signal();
|
||||
this.touchstart = new Signal();
|
||||
this.touchmove = new Signal();
|
||||
this.touchend = new Signal();
|
||||
this.touchcancel = new Signal();
|
||||
|
||||
// Simple signals which just receive the touch position
|
||||
this.touchstartSimple = new Signal();
|
||||
this.touchmoveSimple = new Signal();
|
||||
this.touchendSimple = new Signal();
|
||||
|
||||
// Store time of touch start
|
||||
this.clickStartTime = null;
|
||||
|
||||
// A click can be cancelled if another detector registers a click
|
||||
this.cancelled = false;
|
||||
|
||||
this.internalBindTo(/** @type {HTMLElement} */ (element));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all event listeners of this detector
|
||||
*/
|
||||
cleanup() {
|
||||
if (this.element) {
|
||||
if (registerClickDetectors) {
|
||||
const index = window.activeClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.error("Click detector cleanup but is not active");
|
||||
} else {
|
||||
window.activeClickDetectors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
const options = this.internalGetEventListenerOptions();
|
||||
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchstart", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("touchend", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("touchcancel", this.handlerTouchCancel, options);
|
||||
}
|
||||
|
||||
this.element.removeEventListener("mouseup", this.handlerTouchStart, options);
|
||||
this.element.removeEventListener("mousedown", this.handlerTouchEnd, options);
|
||||
this.element.removeEventListener("mouseout", this.handlerTouchCancel, options);
|
||||
|
||||
if (this.captureTouchmove) {
|
||||
if (SUPPORT_TOUCH) {
|
||||
this.element.removeEventListener("touchmove", this.handlerTouchMove, options);
|
||||
}
|
||||
this.element.removeEventListener("mousemove", this.handlerTouchMove, options);
|
||||
}
|
||||
|
||||
if (this.preventClick) {
|
||||
this.element.removeEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
|
||||
this.click.removeAll();
|
||||
this.touchstart.removeAll();
|
||||
this.touchmove.removeAll();
|
||||
this.touchend.removeAll();
|
||||
this.touchcancel.removeAll();
|
||||
|
||||
this.element = null;
|
||||
}
|
||||
}
|
||||
|
||||
// INTERNAL METHODS
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
internalPreventClick(event) {
|
||||
window.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to get the options to pass to an event listener
|
||||
*/
|
||||
internalGetEventListenerOptions() {
|
||||
return {
|
||||
capture: this.consumeEvents,
|
||||
passive: !this.preventDefault,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the click detector to an element
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
internalBindTo(element) {
|
||||
const options = this.internalGetEventListenerOptions();
|
||||
|
||||
this.handlerTouchStart = this.internalOnPointerDown.bind(this);
|
||||
this.handlerTouchEnd = this.internalOnPointerEnd.bind(this);
|
||||
this.handlerTouchMove = this.internalOnPointerMove.bind(this);
|
||||
this.handlerTouchCancel = this.internalOnTouchCancel.bind(this);
|
||||
|
||||
if (this.preventClick) {
|
||||
this.handlerPreventClick = this.internalPreventClick.bind(this);
|
||||
element.addEventListener("click", this.handlerPreventClick, options);
|
||||
}
|
||||
|
||||
if (SUPPORT_TOUCH) {
|
||||
element.addEventListener("touchstart", this.handlerTouchStart, 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("mouseout", this.handlerTouchCancel, options);
|
||||
|
||||
if (this.captureTouchmove) {
|
||||
if (SUPPORT_TOUCH) {
|
||||
element.addEventListener("touchmove", this.handlerTouchMove, options);
|
||||
}
|
||||
element.addEventListener("mousemove", this.handlerTouchMove, options);
|
||||
}
|
||||
|
||||
if (registerClickDetectors) {
|
||||
window.activeClickDetectors.push(this);
|
||||
}
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the bound element is currently in the DOM.
|
||||
*/
|
||||
internalIsDomElementAttached() {
|
||||
return this.element && document.documentElement.contains(this.element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given event is relevant for this detector
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalEventPreHandler(event, expectedRemainingTouches = 1) {
|
||||
if (!this.element) {
|
||||
// Already cleaned up
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.targetOnly && event.target !== this.element) {
|
||||
// Clicked a child element
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop any propagation and defaults if configured
|
||||
if (this.consumeEvents && event.cancelable) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (this.preventDefault && event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
clickDetectorGlobals.lastTouchTime = performance.now();
|
||||
|
||||
// console.log("Got touches", event.targetTouches.length, "vs", expectedRemainingTouches);
|
||||
if (event.targetTouches.length !== expectedRemainingTouches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the mous position from an event
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
* @returns {Vector} The client space position
|
||||
*/
|
||||
static extractPointerPosition(event) {
|
||||
if (window.TouchEvent && event instanceof TouchEvent) {
|
||||
if (event.changedTouches.length !== 1) {
|
||||
logger.warn(
|
||||
"Got unexpected target touches:",
|
||||
event.targetTouches.length,
|
||||
"->",
|
||||
event.targetTouches
|
||||
);
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
const touch = event.changedTouches[0];
|
||||
return new Vector(touch.clientX, touch.clientY);
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
return new Vector(event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
assertAlways(false, "Got unknown event: " + event);
|
||||
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cacnels all ongoing events on this detector
|
||||
*/
|
||||
cancelOngoingEvents() {
|
||||
if (this.applyCssClass && this.element) {
|
||||
this.element.classList.remove(this.applyCssClass);
|
||||
}
|
||||
this.clickDownPosition = null;
|
||||
this.clickStartTime = null;
|
||||
this.cancelled = true;
|
||||
fastArrayDeleteValueIfContained(ongoingClickDetectors, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer down handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnPointerDown(event) {
|
||||
window.focus();
|
||||
|
||||
if (!this.internalEventPreHandler(event, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const position = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick = event.button === 2;
|
||||
if (isRightClick) {
|
||||
// Ignore right clicks
|
||||
this.rightClick.dispatch(position, event);
|
||||
this.cancelled = true;
|
||||
this.clickDownPosition = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clickDownPosition) {
|
||||
logger.warn("Ignoring double click");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancelled = false;
|
||||
this.touchstart.dispatch(event);
|
||||
|
||||
// Store where the touch started
|
||||
this.clickDownPosition = position;
|
||||
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) {
|
||||
ongoingClickDetectors.push(this);
|
||||
} else {
|
||||
logger.warn("Click detector got pointer down of active pointer twice");
|
||||
}
|
||||
|
||||
// If we should apply any classes, do this now
|
||||
if (this.applyCssClass) {
|
||||
this.element.classList.add(this.applyCssClass);
|
||||
}
|
||||
|
||||
// If we should play any sound, do this
|
||||
if (this.clickSound) {
|
||||
GLOBAL_APP.sound.playUiSound(this.clickSound);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer move handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnPointerMove(event) {
|
||||
if (!this.internalEventPreHandler(event, 1)) {
|
||||
return false;
|
||||
}
|
||||
this.touchmove.dispatch(event);
|
||||
const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
|
||||
this.touchmoveSimple.dispatch(pos.x, pos.y);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal pointer end handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnPointerEnd(event) {
|
||||
window.focus();
|
||||
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchend on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
const isRightClick = event.button === 2;
|
||||
if (isRightClick) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const index = ongoingClickDetectors.indexOf(this);
|
||||
if (index < 0) {
|
||||
logger.warn("Got pointer end but click detector is not in pressed state");
|
||||
} else {
|
||||
fastArrayDelete(ongoingClickDetectors, index);
|
||||
}
|
||||
|
||||
let dispatchClick = false;
|
||||
let dispatchClickPos = null;
|
||||
|
||||
// Check for correct down position, otherwise must have pinched or so
|
||||
if (this.clickDownPosition) {
|
||||
const pos = /** @type {typeof ClickDetector} */ (this.constructor).extractPointerPosition(event);
|
||||
const distance = pos.distance(this.clickDownPosition);
|
||||
if (!IS_MOBILE || distance <= this.maxDistance) {
|
||||
dispatchClick = true;
|
||||
dispatchClickPos = pos;
|
||||
} else {
|
||||
console.warn("[ClickDetector] Touch does not count as click:", "(was", distance, ")");
|
||||
}
|
||||
}
|
||||
|
||||
this.clickDownPosition = null;
|
||||
this.clickStartTime = null;
|
||||
|
||||
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
|
||||
if (this.internalIsDomElementAttached()) {
|
||||
this.touchend.dispatch(event);
|
||||
this.touchendSimple.dispatch();
|
||||
|
||||
if (dispatchClick) {
|
||||
const detectors = ongoingClickDetectors.slice();
|
||||
for (let i = 0; i < detectors.length; ++i) {
|
||||
detectors[i].cancelOngoingEvents();
|
||||
}
|
||||
this.click.dispatch(dispatchClickPos, event);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal touch cancel handler
|
||||
* @param {TouchEvent|MouseEvent} event
|
||||
*/
|
||||
internalOnTouchCancel(event) {
|
||||
if (!this.internalEventPreHandler(event, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
// warn(this, "Not dispatching touchcancel on cancelled listener");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancelOngoingEvents();
|
||||
this.touchcancel.dispatch(event);
|
||||
this.touchendSimple.dispatch(event);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { queryParamOptions } from "./query_parameters";
|
||||
|
||||
export const IS_DEBUG =
|
||||
G_IS_DEV &&
|
||||
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.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 IS_MAC = navigator.platform.toLowerCase().indexOf("mac") >= 0;
|
||||
|
||||
const smoothCanvas = true;
|
||||
|
||||
export const THIRDPARTY_URLS = {
|
||||
discord: "https://discord.gg/HN7EVzV",
|
||||
github: "https://github.com/tobspr/shapez.io",
|
||||
reddit: "https://www.reddit.com/r/shapezio",
|
||||
shapeViewer: "https://viewer.shapez.io",
|
||||
|
||||
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
|
||||
};
|
||||
|
||||
export const A_B_TESTING_LINK_TYPE = Math.random() > 0.5 ? "steam_1_pr" : "steam_2_npr";
|
||||
|
||||
export const globalConfig = {
|
||||
// Size of a single tile in Pixels.
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
@ -61,7 +59,7 @@ export const globalConfig = {
|
||||
|
||||
undergroundBeltMaxTilesByTier: [5, 9],
|
||||
|
||||
readerAnalyzeIntervalSeconds: G_IS_DEV ? 3 : 10,
|
||||
readerAnalyzeIntervalSeconds: 10,
|
||||
|
||||
buildingSpeeds: {
|
||||
cutter: 1 / 4,
|
||||
@ -134,3 +132,8 @@ if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
if (globalConfig.debug.fastGameEnter) {
|
||||
globalConfig.debug.noArtificialDelays = true;
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
|
||||
globalConfig.warmupTimeSecondsFast = 0;
|
||||
globalConfig.warmupTimeSecondsRegular = 0;
|
||||
}
|
||||
|
||||
@ -1,26 +1,25 @@
|
||||
import { globalConfig } from "./config";
|
||||
|
||||
/**
|
||||
* @typedef {import("../game/root").GameRoot} GameRoot
|
||||
* @typedef {import("./rectangle").Rectangle} Rectangle
|
||||
*/
|
||||
|
||||
export class DrawParameters {
|
||||
constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) {
|
||||
/** @type {CanvasRenderingContext2D} */
|
||||
this.context = context;
|
||||
|
||||
/** @type {Rectangle} */
|
||||
this.visibleRect = visibleRect;
|
||||
|
||||
/** @type {string} */
|
||||
this.desiredAtlasScale = desiredAtlasScale;
|
||||
|
||||
/** @type {number} */
|
||||
this.zoomLevel = zoomLevel;
|
||||
|
||||
// FIXME: Not really nice
|
||||
/** @type {GameRoot} */
|
||||
this.root = root;
|
||||
}
|
||||
}
|
||||
import { globalConfig } from "./config";
|
||||
|
||||
/**
|
||||
* @typedef {import("../game/root").GameRoot} GameRoot
|
||||
* @typedef {import("./rectangle").Rectangle} Rectangle
|
||||
*/
|
||||
|
||||
export class DrawParameters {
|
||||
constructor({ context, visibleRect, desiredAtlasScale, zoomLevel, root }) {
|
||||
/** @type {CanvasRenderingContext2D} */
|
||||
this.context = context;
|
||||
|
||||
/** @type {Rectangle} */
|
||||
this.visibleRect = visibleRect;
|
||||
|
||||
/** @type {string} */
|
||||
this.desiredAtlasScale = desiredAtlasScale;
|
||||
|
||||
/** @type {number} */
|
||||
this.zoomLevel = zoomLevel;
|
||||
|
||||
/** @type {GameRoot} */
|
||||
this.root = root;
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,17 @@ import { getStringForKeyCode } from "../game/key_action_mapper";
|
||||
import { createLogger } from "./logging";
|
||||
import { T } from "../translations";
|
||||
|
||||
/*
|
||||
* ***************************************************
|
||||
*
|
||||
* LEGACY CODE WARNING
|
||||
*
|
||||
* This is old code from yorg3.io and needs to be refactored
|
||||
* @TODO
|
||||
*
|
||||
* ***************************************************
|
||||
*/
|
||||
|
||||
const kbEnter = 13;
|
||||
const kbCancel = 27;
|
||||
|
||||
@ -60,6 +71,8 @@ export class Dialog {
|
||||
this.buttonSignals[buttonId] = new Signal();
|
||||
}
|
||||
|
||||
this.valueChosen = new Signal();
|
||||
|
||||
this.timeouts = [];
|
||||
this.clickDetectors = [];
|
||||
|
||||
@ -164,7 +177,7 @@ export class Dialog {
|
||||
const timeout = setTimeout(() => {
|
||||
button.classList.remove("timedButton");
|
||||
arrayDeleteValue(this.timeouts, timeout);
|
||||
}, 5000);
|
||||
}, 3000);
|
||||
this.timeouts.push(timeout);
|
||||
}
|
||||
if (isEnter || isEscape) {
|
||||
@ -431,10 +444,12 @@ export class DialogWithForm extends Dialog {
|
||||
for (let i = 0; i < this.formElements.length; ++i) {
|
||||
const elem = this.formElements[i];
|
||||
elem.bindEvents(div, this.clickDetectors);
|
||||
elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
|
||||
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
|
||||
}
|
||||
|
||||
waitNextFrame().then(() => {
|
||||
this.formElements[0].focus();
|
||||
this.formElements[this.formElements.length - 1].focus();
|
||||
});
|
||||
|
||||
return div;
|
||||
|
||||
@ -1,150 +1,232 @@
|
||||
import { ClickDetector } from "./click_detector";
|
||||
|
||||
export class FormElement {
|
||||
constructor(id, label) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
|
||||
getFormElement(parent) {
|
||||
return parent.querySelector("[data-formId='" + this.id + "']");
|
||||
}
|
||||
|
||||
bindEvents(parent, clickTrackers) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
focus() {}
|
||||
|
||||
isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {any} */
|
||||
getValue() {
|
||||
abstract;
|
||||
}
|
||||
}
|
||||
|
||||
export class FormElementInput extends FormElement {
|
||||
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
|
||||
super(id, label);
|
||||
this.placeholder = placeholder;
|
||||
this.defaultValue = defaultValue;
|
||||
this.inputType = inputType;
|
||||
this.validator = validator;
|
||||
|
||||
this.element = null;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
let classes = [];
|
||||
let inputType = "text";
|
||||
let maxlength = 256;
|
||||
switch (this.inputType) {
|
||||
case "text": {
|
||||
classes.push("input-text");
|
||||
break;
|
||||
}
|
||||
|
||||
case "email": {
|
||||
classes.push("input-email");
|
||||
inputType = "email";
|
||||
break;
|
||||
}
|
||||
|
||||
case "token": {
|
||||
classes.push("input-token");
|
||||
inputType = "text";
|
||||
maxlength = 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="formElement input">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<input
|
||||
type="${inputType}"
|
||||
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
|
||||
maxlength="${maxlength}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
class="${classes.join(" ")}"
|
||||
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
|
||||
data-formId="${this.id}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(parent, clickTrackers) {
|
||||
this.element = this.getFormElement(parent);
|
||||
this.element.addEventListener("input", event => this.updateErrorState());
|
||||
this.updateErrorState();
|
||||
}
|
||||
|
||||
updateErrorState() {
|
||||
this.element.classList.toggle("errored", !this.isValid());
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return !this.validator || this.validator(this.element.value);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.element.value;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export class FormElementCheckbox extends FormElement {
|
||||
constructor({ id, label, defaultValue = true }) {
|
||||
super(id, label);
|
||||
this.defaultValue = defaultValue;
|
||||
this.value = this.defaultValue;
|
||||
|
||||
this.element = null;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
return `
|
||||
<div class="formElement checkBoxFormElem">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
|
||||
<span class="knob"></span >
|
||||
</div >
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(parent, clickTrackers) {
|
||||
this.element = this.getFormElement(parent);
|
||||
const detector = new ClickDetector(this.element, {
|
||||
consumeEvents: false,
|
||||
preventDefault: false,
|
||||
});
|
||||
clickTrackers.push(detector);
|
||||
detector.click.add(this.toggle, this);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.value = !this.value;
|
||||
this.element.classList.toggle("checked", this.value);
|
||||
}
|
||||
|
||||
focus(parent) {}
|
||||
}
|
||||
import { BaseItem } from "../game/base_item";
|
||||
import { ClickDetector } from "./click_detector";
|
||||
import { Signal } from "./signal";
|
||||
|
||||
/*
|
||||
* ***************************************************
|
||||
*
|
||||
* LEGACY CODE WARNING
|
||||
*
|
||||
* This is old code from yorg3.io and needs to be refactored
|
||||
* @TODO
|
||||
*
|
||||
* ***************************************************
|
||||
*/
|
||||
|
||||
export class FormElement {
|
||||
constructor(id, label) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
|
||||
this.valueChosen = new Signal();
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
|
||||
getFormElement(parent) {
|
||||
return parent.querySelector("[data-formId='" + this.id + "']");
|
||||
}
|
||||
|
||||
bindEvents(parent, clickTrackers) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
focus() {}
|
||||
|
||||
isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {any} */
|
||||
getValue() {
|
||||
abstract;
|
||||
}
|
||||
}
|
||||
|
||||
export class FormElementInput extends FormElement {
|
||||
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
|
||||
super(id, label);
|
||||
this.placeholder = placeholder;
|
||||
this.defaultValue = defaultValue;
|
||||
this.inputType = inputType;
|
||||
this.validator = validator;
|
||||
|
||||
this.element = null;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
let classes = [];
|
||||
let inputType = "text";
|
||||
let maxlength = 256;
|
||||
switch (this.inputType) {
|
||||
case "text": {
|
||||
classes.push("input-text");
|
||||
break;
|
||||
}
|
||||
|
||||
case "email": {
|
||||
classes.push("input-email");
|
||||
inputType = "email";
|
||||
break;
|
||||
}
|
||||
|
||||
case "token": {
|
||||
classes.push("input-token");
|
||||
inputType = "text";
|
||||
maxlength = 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="formElement input">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<input
|
||||
type="${inputType}"
|
||||
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
|
||||
maxlength="${maxlength}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
class="${classes.join(" ")}"
|
||||
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
|
||||
data-formId="${this.id}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(parent, clickTrackers) {
|
||||
this.element = this.getFormElement(parent);
|
||||
this.element.addEventListener("input", event => this.updateErrorState());
|
||||
this.updateErrorState();
|
||||
}
|
||||
|
||||
updateErrorState() {
|
||||
this.element.classList.toggle("errored", !this.isValid());
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return !this.validator || this.validator(this.element.value);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.element.value;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export class FormElementCheckbox extends FormElement {
|
||||
constructor({ id, label, defaultValue = true }) {
|
||||
super(id, label);
|
||||
this.defaultValue = defaultValue;
|
||||
this.value = this.defaultValue;
|
||||
|
||||
this.element = null;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
return `
|
||||
<div class="formElement checkBoxFormElem">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
|
||||
<span class="knob"></span >
|
||||
</div >
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(parent, clickTrackers) {
|
||||
this.element = this.getFormElement(parent);
|
||||
const detector = new ClickDetector(this.element, {
|
||||
consumeEvents: false,
|
||||
preventDefault: false,
|
||||
});
|
||||
clickTrackers.push(detector);
|
||||
detector.click.add(this.toggle, this);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.value = !this.value;
|
||||
this.element.classList.toggle("checked", this.value);
|
||||
}
|
||||
|
||||
focus(parent) {}
|
||||
}
|
||||
|
||||
export class FormElementItemChooser extends FormElement {
|
||||
/**
|
||||
*
|
||||
* @param {object} param0
|
||||
* @param {string} param0.id
|
||||
* @param {string=} param0.label
|
||||
* @param {Array<BaseItem>} param0.items
|
||||
*/
|
||||
constructor({ id, label, items = [] }) {
|
||||
super(id, label);
|
||||
this.items = items;
|
||||
this.element = null;
|
||||
|
||||
/**
|
||||
* @type {BaseItem}
|
||||
*/
|
||||
this.chosenItem = null;
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
let classes = [];
|
||||
|
||||
return `
|
||||
<div class="formElement">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} parent
|
||||
* @param {Array<ClickDetector>} clickTrackers
|
||||
*/
|
||||
bindEvents(parent, clickTrackers) {
|
||||
this.element = this.getFormElement(parent);
|
||||
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
const item = this.items[i];
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const context = canvas.getContext("2d");
|
||||
item.drawFullSizeOnCanvas(context, 128);
|
||||
this.element.appendChild(canvas);
|
||||
|
||||
const detector = new ClickDetector(canvas, {});
|
||||
clickTrackers.push(detector);
|
||||
detector.click.add(() => {
|
||||
this.chosenItem = item;
|
||||
this.valueChosen.dispatch(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
focus() {}
|
||||
}
|
||||
|
||||
@ -81,10 +81,6 @@ export class ReadWriteProxy {
|
||||
return this.writeAsync();
|
||||
}
|
||||
|
||||
getCurrentData() {
|
||||
return this.currentData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} obj
|
||||
@ -173,7 +169,7 @@ export class ReadWriteProxy {
|
||||
// Check for errors during read
|
||||
.catch(err => {
|
||||
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
|
||||
return Promise.resolve(null);
|
||||
|
||||
154
src/js/core/restriction_manager.js
Normal file
154
src/js/core/restriction_manager.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -108,17 +108,6 @@ export class RandomNumberGenerator {
|
||||
assert(max > min, "rng: max <= 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
|
||||
|
||||
@ -708,3 +708,83 @@ export function dirInterval(key, frames, object, premessage, ...args) {
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,82 +1,100 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { BasicSerializableObject } from "../savegame/serialization";
|
||||
|
||||
/** @type {ItemType[]} **/
|
||||
export const itemTypes = ["shape", "color", "boolean"];
|
||||
|
||||
/**
|
||||
* Class for items on belts etc. Not an entity for performance reasons
|
||||
*/
|
||||
export class BaseItem extends BasicSerializableObject {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static getId() {
|
||||
return "base_item";
|
||||
}
|
||||
|
||||
/** @returns {object} */
|
||||
static getSchema() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @returns {ItemType} **/
|
||||
getItemType() {
|
||||
abstract;
|
||||
return "shape";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the item equals the other itme
|
||||
* @param {BaseItem} other
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
if (this.getItemType() !== other.getItemType()) {
|
||||
return false;
|
||||
}
|
||||
return this.equalsImpl(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override for custom comparison
|
||||
* @abstract
|
||||
* @param {BaseItem} other
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equalsImpl(other) {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the item at the given position
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {number=} diameter
|
||||
*/
|
||||
drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
|
||||
if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
|
||||
this.drawItemCenteredImpl(x, y, parameters, diameter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {number=} diameter
|
||||
*/
|
||||
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
getBackgroundColorAsResource() {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
import { globalConfig } from "../core/config";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { BasicSerializableObject } from "../savegame/serialization";
|
||||
|
||||
/** @type {ItemType[]} **/
|
||||
export const itemTypes = ["shape", "color", "boolean"];
|
||||
|
||||
/**
|
||||
* Class for items on belts etc. Not an entity for performance reasons
|
||||
*/
|
||||
export class BaseItem extends BasicSerializableObject {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static getId() {
|
||||
return "base_item";
|
||||
}
|
||||
|
||||
/** @returns {object} */
|
||||
static getSchema() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @returns {ItemType} **/
|
||||
getItemType() {
|
||||
abstract;
|
||||
return "shape";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string id of the item
|
||||
* @returns {string}
|
||||
*/
|
||||
getAsCopyableKey() {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the item equals the other itme
|
||||
* @param {BaseItem} other
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
if (this.getItemType() !== other.getItemType()) {
|
||||
return false;
|
||||
}
|
||||
return this.equalsImpl(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override for custom comparison
|
||||
* @abstract
|
||||
* @param {BaseItem} other
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equalsImpl(other) {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the item to a canvas
|
||||
* @param {CanvasRenderingContext2D} context
|
||||
* @param {number} size
|
||||
*/
|
||||
drawFullSizeOnCanvas(context, size) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the item at the given position
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {number=} diameter
|
||||
*/
|
||||
drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
|
||||
if (parameters.visibleRect.containsCircle(x, y, diameter / 2)) {
|
||||
this.drawItemCenteredImpl(x, y, parameters, diameter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {number=} diameter
|
||||
*/
|
||||
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
getBackgroundColorAsResource() {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -1112,7 +1112,7 @@ export class BeltPath extends BasicSerializableObject {
|
||||
|
||||
isFirstItemProcessed = false;
|
||||
this.spacingToFirstItem += clampedProgress;
|
||||
if (remainingVelocity < 0.01) {
|
||||
if (remainingVelocity < 1e-7) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { DrawParameters } from "../core/draw_parameters";
|
||||
import { Loader } from "../core/loader";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { findNiceIntegerValue } from "../core/utils";
|
||||
import { Vector } from "../core/vector";
|
||||
import { Entity } from "./entity";
|
||||
import { GameRoot } from "./root";
|
||||
import { findNiceIntegerValue } from "../core/utils";
|
||||
import { blueprintShape } from "./upgrades";
|
||||
import { globalConfig } from "../core/config";
|
||||
|
||||
const logger = createLogger("blueprint");
|
||||
|
||||
export class Blueprint {
|
||||
/**
|
||||
@ -174,7 +169,7 @@ export class Blueprint {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
canAfford(root) {
|
||||
return root.hubGoals.getShapesStoredByKey(blueprintShape) >= this.getCost();
|
||||
return root.hubGoals.getShapesStoredByKey(root.gameMode.getBlueprintShapeKey()) >= this.getCost();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -41,7 +41,7 @@ const variantsCache = new Map();
|
||||
export function registerBuildingVariant(
|
||||
code,
|
||||
meta,
|
||||
variant = "default" /* FIXME: Circular dependency, actually its defaultBuildingVariant */,
|
||||
variant = "default" /* @TODO: Circular dependency, actually its defaultBuildingVariant */,
|
||||
rotationVariant = 0
|
||||
) {
|
||||
assert(!gBuildingVariants[code], "Duplicate id: " + code);
|
||||
|
||||
@ -5,6 +5,7 @@ import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
|
||||
const overlayMatrix = generateMatrixRotations([1, 1, 0, 1, 1, 1, 0, 1, 0]);
|
||||
|
||||
@ -21,8 +22,7 @@ export class MetaAnalyzerBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
// @todo
|
||||
return true;
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
|
||||
}
|
||||
|
||||
/** @returns {"wires"} **/
|
||||
|
||||
@ -7,6 +7,7 @@ import { BeltComponent } from "../components/belt";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { THEME } from "../theme";
|
||||
|
||||
export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right];
|
||||
|
||||
@ -22,7 +23,7 @@ export class MetaBeltBuilding extends MetaBuilding {
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#777";
|
||||
return THEME.map.chunkOverview.beltColor;
|
||||
}
|
||||
|
||||
getPlacementSound() {
|
||||
|
||||
@ -4,6 +4,7 @@ import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
|
||||
export class MetaComparatorBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
@ -18,8 +19,7 @@ export class MetaComparatorBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
// @todo
|
||||
return true;
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
|
||||
}
|
||||
|
||||
/** @returns {"wires"} **/
|
||||
|
||||
@ -23,7 +23,7 @@ export class MetaFilterBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers);
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_filter);
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
|
||||
44
src/js/game/buildings/item_producer.js
Normal file
44
src/js/game/buildings/item_producer.js
Normal 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());
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,7 @@ export class MetaLeverBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers);
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
|
||||
@ -5,6 +5,7 @@ import { MetaBuilding, defaultBuildingVariant } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
|
||||
import { generateMatrixRotations } from "../../core/utils";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumLogicGateVariants = {
|
||||
@ -48,8 +49,7 @@ export class MetaLogicGateBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
// @todo
|
||||
return true;
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
|
||||
}
|
||||
|
||||
/** @returns {"wires"} **/
|
||||
|
||||
@ -71,7 +71,7 @@ export class MetaPainterBuilding extends MetaBuilding {
|
||||
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_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);
|
||||
}
|
||||
return variants;
|
||||
|
||||
@ -5,6 +5,7 @@ import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumTransistorVariants = {
|
||||
@ -29,8 +30,7 @@ export class MetaTransistorBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
// @todo
|
||||
return true;
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_logic_gates);
|
||||
}
|
||||
|
||||
/** @returns {"wires"} **/
|
||||
|
||||
@ -191,7 +191,6 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
|
||||
) {
|
||||
tile = tile.addScalars(searchVector.x, searchVector.y);
|
||||
|
||||
/* WIRES: FIXME */
|
||||
const contents = root.map.getTileContent(tile, "regular");
|
||||
if (contents) {
|
||||
const undergroundComp = contents.components.UndergroundBelt;
|
||||
|
||||
@ -4,6 +4,7 @@ import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
import { MetaCutterBuilding } from "./cutter";
|
||||
import { MetaPainterBuilding } from "./painter";
|
||||
import { MetaRotaterBuilding } from "./rotater";
|
||||
@ -47,8 +48,7 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
// @todo
|
||||
return true;
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_virtual_processing);
|
||||
}
|
||||
|
||||
/** @returns {"wires"} **/
|
||||
|
||||
@ -82,7 +82,7 @@ export class MetaWireBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers);
|
||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -21,7 +21,7 @@ export class MetaWireTunnelBuilding extends MetaBuilding {
|
||||
* @param {GameRoot} 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
|
||||
*/
|
||||
setupEntityComponents(entity) {
|
||||
entity.addComponent(new WireTunnelComponent({}));
|
||||
entity.addComponent(new WireTunnelComponent());
|
||||
}
|
||||
}
|
||||
|
||||
@ -353,7 +353,7 @@ export class Camera extends BasicSerializableObject {
|
||||
.add(() => (this.desiredZoom = this.zoomLevel * 1.2));
|
||||
mapper
|
||||
.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());
|
||||
}
|
||||
@ -502,16 +502,20 @@ export class Camera extends BasicSerializableObject {
|
||||
}
|
||||
const prevZoom = this.zoomLevel;
|
||||
|
||||
const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity();
|
||||
assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY);
|
||||
const scale = 1 + 0.15 * this.root.app.settings.getScrollWheelSensitivity();
|
||||
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);
|
||||
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);
|
||||
|
||||
this.clampZoomLevel();
|
||||
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) {
|
||||
const worldPos = this.root.camera.screenToWorld(mousePosition);
|
||||
const worldDelta = worldPos.sub(this.center);
|
||||
@ -939,6 +943,7 @@ export class Camera extends BasicSerializableObject {
|
||||
this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade);
|
||||
assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel);
|
||||
} else {
|
||||
this.zoomLevel = this.desiredZoom;
|
||||
this.desiredZoom = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import { WireTunnelComponent } from "./components/wire_tunnel";
|
||||
import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
import { FilterComponent } from "./components/filter";
|
||||
import { ItemProducerComponent } from "./components/item_producer";
|
||||
|
||||
export function initComponentRegistry() {
|
||||
gComponentRegistry.register(StaticMapEntityComponent);
|
||||
@ -39,6 +40,7 @@ export function initComponentRegistry() {
|
||||
gComponentRegistry.register(DisplayComponent);
|
||||
gComponentRegistry.register(BeltReaderComponent);
|
||||
gComponentRegistry.register(FilterComponent);
|
||||
gComponentRegistry.register(ItemProducerComponent);
|
||||
|
||||
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ export class ItemEjectorComponent extends Component {
|
||||
static getSchema() {
|
||||
// The cachedDestSlot, cachedTargetEntity fields are not serialized.
|
||||
return {
|
||||
slots: types.array(
|
||||
slots: types.fixedSizeArray(
|
||||
types.structured({
|
||||
item: types.nullable(typeItemSingleton),
|
||||
progress: types.float,
|
||||
|
||||
7
src/js/game/components/item_producer.js
Normal file
7
src/js/game/components/item_producer.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { Component } from "../component";
|
||||
|
||||
export class ItemProducerComponent extends Component {
|
||||
static getId() {
|
||||
return "ItemProducer";
|
||||
}
|
||||
}
|
||||
@ -31,7 +31,7 @@ export class WiredPinsComponent extends Component {
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
slots: types.array(
|
||||
slots: types.fixedSizeArray(
|
||||
types.structured({
|
||||
value: types.nullable(typeItemSingleton),
|
||||
})
|
||||
|
||||
@ -31,6 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
|
||||
import { GameLogic } from "./logic";
|
||||
import { MapView } from "./map_view";
|
||||
import { defaultBuildingVariant } from "./meta_building";
|
||||
import { RegularGameMode } from "./modes/regular";
|
||||
import { ProductionAnalytics } from "./production_analytics";
|
||||
import { GameRoot } from "./root";
|
||||
import { ShapeDefinitionManager } from "./shape_definition_manager";
|
||||
@ -101,6 +102,9 @@ export class GameCore {
|
||||
// Needs to come first
|
||||
root.dynamicTickrate = new DynamicTickrate(root);
|
||||
|
||||
// Init game mode
|
||||
root.gameMode = new RegularGameMode(root);
|
||||
|
||||
// Init classes
|
||||
root.camera = new Camera(root);
|
||||
root.map = new MapView(root);
|
||||
|
||||
@ -18,6 +18,7 @@ import { WireTunnelComponent } from "./components/wire_tunnel";
|
||||
import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
import { FilterComponent } from "./components/filter";
|
||||
import { ItemProducerComponent } from "./components/item_producer";
|
||||
/* typehints:end */
|
||||
|
||||
/**
|
||||
@ -85,6 +86,9 @@ export class EntityComponentStorage {
|
||||
/** @type {FilterComponent} */
|
||||
this.Filter;
|
||||
|
||||
/** @type {ItemProducerComponent} */
|
||||
this.ItemProducer;
|
||||
|
||||
/* typehints:end */
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,26 +188,6 @@ export class EntityManager extends BasicSerializableObject {
|
||||
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
|
||||
* @param {Entity} entity
|
||||
|
||||
71
src/js/game/game_mode.js
Normal file
71
src/js/game/game_mode.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,7 @@ import { DisplaySystem } from "./systems/display";
|
||||
import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays";
|
||||
import { BeltReaderSystem } from "./systems/belt_reader";
|
||||
import { FilterSystem } from "./systems/filter";
|
||||
import { ItemProducerSystem } from "./systems/item_producer";
|
||||
|
||||
const logger = createLogger("game_system_manager");
|
||||
|
||||
@ -96,6 +97,9 @@ export class GameSystemManager {
|
||||
/** @type {FilterSystem} */
|
||||
filter: null,
|
||||
|
||||
/** @type {ItemProducerSystem} */
|
||||
itemProducer: null,
|
||||
|
||||
/* typehints:end */
|
||||
};
|
||||
this.systemUpdateOrder = [];
|
||||
@ -130,6 +134,8 @@ export class GameSystemManager {
|
||||
|
||||
add("filter", FilterSystem);
|
||||
|
||||
add("itemProducer", ItemProducerSystem);
|
||||
|
||||
add("itemEjector", ItemEjectorSystem);
|
||||
|
||||
add("mapResources", MapResourcesSystem);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
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 { enumColors } from "./colors";
|
||||
import { enumItemProcessorTypes } from "./components/item_processor";
|
||||
import { enumAnalyticsDataSource } from "./production_analytics";
|
||||
import { GameRoot } from "./root";
|
||||
import { enumSubShape, ShapeDefinition } from "./shape_definition";
|
||||
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals";
|
||||
import { UPGRADES } from "./upgrades";
|
||||
import { enumHubGoalRewards } from "./tutorial_goals";
|
||||
|
||||
export class HubGoals extends BasicSerializableObject {
|
||||
static getId() {
|
||||
@ -18,32 +19,39 @@ export class HubGoals extends BasicSerializableObject {
|
||||
level: types.uint,
|
||||
storedShapes: types.keyValueMap(types.uint),
|
||||
upgradeLevels: types.keyValueMap(types.uint),
|
||||
|
||||
currentGoal: types.structured({
|
||||
definition: types.knownType(ShapeDefinition),
|
||||
required: types.uint,
|
||||
reward: types.nullable(types.enum(enumHubGoalRewards)),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data) {
|
||||
/**
|
||||
*
|
||||
* @param {*} data
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
deserialize(data, root) {
|
||||
const errorCode = super.deserialize(data);
|
||||
if (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
|
||||
for (let i = 0; i < this.level - 1; ++i) {
|
||||
if (i < tutorialGoals.length) {
|
||||
const reward = tutorialGoals[i].reward;
|
||||
if (i < levels.length) {
|
||||
const reward = levels[i].reward;
|
||||
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute upgrade improvements
|
||||
for (const upgradeId in UPGRADES) {
|
||||
const tiers = UPGRADES[upgradeId];
|
||||
const upgrades = this.root.gameMode.getUpgrades();
|
||||
for (const upgradeId in upgrades) {
|
||||
const tiers = upgrades[upgradeId];
|
||||
const level = this.upgradeLevels[upgradeId] || 0;
|
||||
let totalImprovement = 1;
|
||||
for (let i = 0; i < level; ++i) {
|
||||
@ -53,15 +61,7 @@ export class HubGoals extends BasicSerializableObject {
|
||||
}
|
||||
|
||||
// Compute current goal
|
||||
const goal = tutorialGoals[this.level - 1];
|
||||
if (goal) {
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(goal.shape),
|
||||
required: goal.required,
|
||||
reward: goal.reward,
|
||||
};
|
||||
}
|
||||
this.computeNextGoal();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,11 +97,15 @@ export class HubGoals extends BasicSerializableObject {
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
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.createNextGoal();
|
||||
this.computeNextGoal();
|
||||
|
||||
// Allow quickly switching goals in dev mode
|
||||
if (G_IS_DEV) {
|
||||
@ -109,13 +113,26 @@ export class HubGoals extends BasicSerializableObject {
|
||||
if (ev.key === "b") {
|
||||
// root is not guaranteed to exist within ~0.5s after loading in
|
||||
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
|
||||
* @param {ShapeDefinition} definition
|
||||
@ -150,6 +167,15 @@ export class HubGoals extends BasicSerializableObject {
|
||||
* Returns how much of the current goal was already delivered
|
||||
*/
|
||||
getCurrentGoalDelivered() {
|
||||
if (this.currentGoal.throughputOnly) {
|
||||
return (
|
||||
this.root.productionAnalytics.getCurrentShapeRate(
|
||||
enumAnalyticsDataSource.delivered,
|
||||
this.currentGoal.definition
|
||||
) / globalConfig.analyticsSliceDurationSeconds
|
||||
);
|
||||
}
|
||||
|
||||
return this.getShapesStored(this.currentGoal.definition);
|
||||
}
|
||||
|
||||
@ -184,36 +210,40 @@ export class HubGoals extends BasicSerializableObject {
|
||||
this.root.signals.shapeDelivered.dispatch(definition);
|
||||
|
||||
// Check if we have enough for the next level
|
||||
const targetHash = this.currentGoal.definition.getHash();
|
||||
if (
|
||||
this.storedShapes[targetHash] >= this.currentGoal.required ||
|
||||
this.getCurrentGoalDelivered() >= this.currentGoal.required ||
|
||||
(G_IS_DEV && globalConfig.debug.rewardsInstant)
|
||||
) {
|
||||
this.onGoalCompleted();
|
||||
if (!this.isEndOfDemoReached()) {
|
||||
this.onGoalCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the next goal
|
||||
*/
|
||||
createNextGoal() {
|
||||
computeNextGoal() {
|
||||
const storyIndex = this.level - 1;
|
||||
if (storyIndex < tutorialGoals.length) {
|
||||
const { shape, required, reward } = tutorialGoals[storyIndex];
|
||||
const levels = this.root.gameMode.getLevelDefinitions();
|
||||
if (storyIndex < levels.length) {
|
||||
const { shape, required, reward, throughputOnly } = levels[storyIndex];
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
|
||||
required,
|
||||
reward,
|
||||
throughputOnly,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const required = Math.min(200, 4 + (this.level - 27) * 0.25);
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.createRandomShape(),
|
||||
required: findNiceIntegerValue(1000 + Math.pow(this.level * 2000, 0.8)),
|
||||
definition: this.computeFreeplayShape(this.level),
|
||||
required,
|
||||
reward: enumHubGoalRewards.no_reward_freeplay,
|
||||
throughputOnly: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -226,7 +256,7 @@ export class HubGoals extends BasicSerializableObject {
|
||||
|
||||
this.root.app.gameAnalytics.handleLevelCompleted(this.level);
|
||||
++this.level;
|
||||
this.createNextGoal();
|
||||
this.computeNextGoal();
|
||||
|
||||
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
|
||||
}
|
||||
@ -235,7 +265,7 @@ export class HubGoals extends BasicSerializableObject {
|
||||
* Returns whether we are playing in free-play
|
||||
*/
|
||||
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
|
||||
*/
|
||||
canUnlockUpgrade(upgradeId) {
|
||||
const tiers = UPGRADES[upgradeId];
|
||||
const tiers = this.root.gameMode.getUpgrades()[upgradeId];
|
||||
const currentLevel = this.getUpgradeLevel(upgradeId);
|
||||
|
||||
if (currentLevel >= tiers.length) {
|
||||
@ -272,7 +302,7 @@ export class HubGoals extends BasicSerializableObject {
|
||||
*/
|
||||
getAvailableUpgradeCount() {
|
||||
let count = 0;
|
||||
for (const upgradeId in UPGRADES) {
|
||||
for (const upgradeId in this.root.gameMode.getUpgrades()) {
|
||||
if (this.canUnlockUpgrade(upgradeId)) {
|
||||
++count;
|
||||
}
|
||||
@ -290,7 +320,7 @@ export class HubGoals extends BasicSerializableObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
const upgradeTiers = UPGRADES[upgradeId];
|
||||
const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
|
||||
const currentLevel = this.getUpgradeLevel(upgradeId);
|
||||
|
||||
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}
|
||||
*/
|
||||
createRandomShape() {
|
||||
computeFreeplayShape(level) {
|
||||
const layerCount = clamp(this.level / 25, 2, 4);
|
||||
|
||||
/** @type {Array<import("./shape_definition").ShapeLayer>} */
|
||||
let layers = [];
|
||||
|
||||
const randomColor = () => randomChoice(Object.values(enumColors));
|
||||
const randomShape = () => randomChoice(Object.values(enumSubShape));
|
||||
const rng = new RandomNumberGenerator(this.root.map.seed + "/" + level);
|
||||
|
||||
const colors = this.generateRandomColorSet(rng, level > 35);
|
||||
|
||||
let pickedSymmetry = null; // pairs of quadrants that must be the same
|
||||
let availableShapes = [enumSubShape.rect, enumSubShape.circle, enumSubShape.star];
|
||||
if (rng.next() < 0.5) {
|
||||
pickedSymmetry = [
|
||||
// radial symmetry
|
||||
[0, 2],
|
||||
[1, 3],
|
||||
];
|
||||
availableShapes.push(enumSubShape.windmill); // windmill looks good only in radial symmetry
|
||||
} else {
|
||||
const symmetries = [
|
||||
[
|
||||
// horizontal axis
|
||||
[0, 3],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
// vertical axis
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
],
|
||||
[
|
||||
// diagonal axis
|
||||
[0, 2],
|
||||
[1],
|
||||
[3],
|
||||
],
|
||||
[
|
||||
// other diagonal axis
|
||||
[1, 3],
|
||||
[0],
|
||||
[2],
|
||||
],
|
||||
];
|
||||
pickedSymmetry = rng.choice(symmetries);
|
||||
}
|
||||
|
||||
const randomColor = () => rng.choice(colors);
|
||||
const randomShape = () => rng.choice(Object.values(enumSubShape));
|
||||
|
||||
let anyIsMissingTwo = false;
|
||||
|
||||
@ -336,23 +436,24 @@ export class HubGoals extends BasicSerializableObject {
|
||||
/** @type {import("./shape_definition").ShapeLayer} */
|
||||
const layer = [null, null, null, null];
|
||||
|
||||
for (let quad = 0; quad < 4; ++quad) {
|
||||
layer[quad] = {
|
||||
subShape: randomShape(),
|
||||
color: randomColor(),
|
||||
};
|
||||
}
|
||||
|
||||
// Sometimes shapes are missing
|
||||
if (Math.random() > 0.85) {
|
||||
layer[randomInt(0, 3)] = null;
|
||||
for (let j = 0; j < pickedSymmetry.length; ++j) {
|
||||
const group = pickedSymmetry[j];
|
||||
const shape = randomShape();
|
||||
const color = randomColor();
|
||||
for (let k = 0; k < group.length; ++k) {
|
||||
const quad = group[k];
|
||||
layer[quad] = {
|
||||
subShape: shape,
|
||||
color,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes they actually are missing *two* ones!
|
||||
// Make sure at max only one layer is missing it though, otherwise we could
|
||||
// create an uncreateable shape
|
||||
if (Math.random() > 0.95 && !anyIsMissingTwo) {
|
||||
layer[randomInt(0, 3)] = null;
|
||||
if (level > 75 && rng.next() > 0.95 && !anyIsMissingTwo) {
|
||||
layer[rng.nextIntRange(0, 4)] = null;
|
||||
anyIsMissingTwo = true;
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import { HUDKeybindingOverlay } from "./parts/keybinding_overlay";
|
||||
import { HUDUnlockNotification } from "./parts/unlock_notification";
|
||||
import { HUDGameMenu } from "./parts/game_menu";
|
||||
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 { HUDVignetteOverlay } from "./parts/vignette_overlay";
|
||||
import { HUDStatistics } from "./parts/statistics";
|
||||
@ -45,6 +45,9 @@ import { HUDLeverToggle } from "./parts/lever_toggle";
|
||||
import { HUDLayerPreview } from "./parts/layer_preview";
|
||||
import { HUDMinerHighlight } from "./parts/miner_highlight";
|
||||
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 {
|
||||
/**
|
||||
@ -76,7 +79,6 @@ export class GameHUD {
|
||||
pinnedShapes: new HUDPinnedShapes(this.root),
|
||||
notifications: new HUDNotifications(this.root),
|
||||
settingsMenu: new HUDSettingsMenu(this.root),
|
||||
// betaOverlay: new HUDBetaOverlay(this.root),
|
||||
debugInfo: new HUDDebugInfo(this.root),
|
||||
dialogs: new HUDModalDialogs(this.root),
|
||||
screenshotExporter: new HUDScreenshotExporter(this.root),
|
||||
@ -113,8 +115,10 @@ export class GameHUD {
|
||||
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.standaloneAdvantages = new HUDStandaloneAdvantages(this.root);
|
||||
this.parts.catMemes = new HUDCatMemes(this.root);
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.renderChanges) {
|
||||
@ -138,6 +142,10 @@ export class GameHUD {
|
||||
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();
|
||||
for (const key in this.parts) {
|
||||
this.parts[key].createElements(frag);
|
||||
|
||||
@ -3,7 +3,12 @@ import { makeDiv } from "../../../core/utils";
|
||||
|
||||
export class HUDBetaOverlay extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_BetaOverlay", [], "CLOSED BETA");
|
||||
this.element = makeDiv(
|
||||
parent,
|
||||
"ingame_HUD_BetaOverlay",
|
||||
[],
|
||||
"<h2>UNSTABLE BETA VERSION</h2><span>Unfinalized & potential buggy content!</span>"
|
||||
);
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
@ -6,7 +6,6 @@ import { Vector } from "../../../core/vector";
|
||||
import { T } from "../../../translations";
|
||||
import { enumMouseButton } from "../../camera";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { blueprintShape } from "../../upgrades";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { Blueprint } from "../../blueprint";
|
||||
@ -15,7 +14,9 @@ import { Entity } from "../../entity";
|
||||
|
||||
export class HUDBlueprintPlacer extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(blueprintShape);
|
||||
const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(
|
||||
this.root.gameMode.getBlueprintShapeKey()
|
||||
);
|
||||
const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80);
|
||||
|
||||
this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``);
|
||||
@ -124,7 +125,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart {
|
||||
const tile = worldPos.toTileSpace();
|
||||
if (blueprint.tryPlace(this.root, tile)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -334,7 +334,11 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
|
||||
const tileBelow = this.root.map.getLowerLayerContentXY(tile.x, tile.y);
|
||||
|
||||
// Check if there's a shape or color item below, if so select the miner
|
||||
if (tileBelow && this.root.app.settings.getAllSettings().pickMinerOnPatch) {
|
||||
if (
|
||||
tileBelow &&
|
||||
this.root.app.settings.getAllSettings().pickMinerOnPatch &&
|
||||
this.root.currentLayer === "regular"
|
||||
) {
|
||||
this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding));
|
||||
|
||||
// Select chained miner if available, since that's always desired once unlocked
|
||||
|
||||
@ -14,6 +14,8 @@ import { MetaTrashBuilding } from "../../buildings/trash";
|
||||
import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
|
||||
import { HUDBaseToolbar } from "./base_toolbar";
|
||||
import { MetaStorageBuilding } from "../../buildings/storage";
|
||||
import { MetaItemProducerBuilding } from "../../buildings/item_producer";
|
||||
import { queryParamOptions } from "../../../core/query_parameters";
|
||||
|
||||
export class HUDBuildingsToolbar extends HUDBaseToolbar {
|
||||
constructor(root) {
|
||||
@ -29,6 +31,7 @@ export class HUDBuildingsToolbar extends HUDBaseToolbar {
|
||||
MetaMixerBuilding,
|
||||
MetaPainterBuilding,
|
||||
MetaTrashBuilding,
|
||||
...(queryParamOptions.sandboxMode || G_IS_DEV ? [MetaItemProducerBuilding] : []),
|
||||
],
|
||||
secondaryBuildings: [
|
||||
MetaStorageBuilding,
|
||||
@ -39,7 +42,7 @@ export class HUDBuildingsToolbar extends HUDBaseToolbar {
|
||||
],
|
||||
visibilityCondition: () =>
|
||||
!this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "regular",
|
||||
htmlElementId: "ingame_HUD_buildings_toolbar",
|
||||
htmlElementId: "ingame_HUD_BuildingsToolbar",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
21
src/js/game/hud/parts/cat_memes.js
Normal file
21
src/js/game/hud/parts/cat_memes.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -6,23 +6,25 @@ import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { TrackedState } from "../../../core/tracked_state";
|
||||
import { cachebust } from "../../../core/cachebust";
|
||||
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 = [
|
||||
// Level 1
|
||||
[
|
||||
// 1.1. place an extractor
|
||||
{
|
||||
id: "1_1_extractor",
|
||||
condition: /** @param {GameRoot} root */ root => {
|
||||
return root.entityMgr.getAllWithComponent(MinerComponent).length === 0;
|
||||
},
|
||||
condition: /** @param {GameRoot} root */ root =>
|
||||
root.entityMgr.getAllWithComponent(MinerComponent).length === 0,
|
||||
},
|
||||
// 1.2. connect to hub
|
||||
{
|
||||
id: "1_2_conveyor",
|
||||
condition: /** @param {GameRoot} root */ root => {
|
||||
return root.hubGoals.getCurrentGoalDelivered() === 0;
|
||||
},
|
||||
condition: /** @param {GameRoot} root */ root => root.hubGoals.getCurrentGoalDelivered() === 0,
|
||||
},
|
||||
// 1.3 wait for completion
|
||||
{
|
||||
@ -30,6 +32,108 @@ const tutorialsByLevel = [
|
||||
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 {
|
||||
|
||||
@ -259,7 +259,7 @@ export class HUDKeybindingOverlay extends BaseHUDPart {
|
||||
label: T.ingame.keybindingsOverlay.switchLayers,
|
||||
keys: [k.ingame.switchLayers],
|
||||
condition: () =>
|
||||
this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_filters_and_levers),
|
||||
this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ export class HUDModalDialogs extends BaseHUDPart {
|
||||
|
||||
dialog.buttonSignals.getStandalone.add(() => {
|
||||
this.app.analytics.trackUiClick("demo_dialog_click");
|
||||
window.open(THIRDPARTY_URLS.standaloneStorePage);
|
||||
window.open(THIRDPARTY_URLS.standaloneStorePage + "?ref=ddc");
|
||||
});
|
||||
|
||||
return dialog.buttonSignals;
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
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 { BaseHUDPart } from "../base_hud_part";
|
||||
import { blueprintShape, UPGRADES } from "../../upgrades";
|
||||
import { enumHubGoalRewards } from "../../tutorial_goals";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
|
||||
/**
|
||||
* Manages the pinned shapes on the left side of the screen
|
||||
@ -22,11 +24,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
* convenient. Also allows for cleaning up handles.
|
||||
* @type {Array<{
|
||||
* key: string,
|
||||
* definition: ShapeDefinition,
|
||||
* amountLabel: HTMLElement,
|
||||
* lastRenderedValue: string,
|
||||
* element: HTMLElement,
|
||||
* detector?: ClickDetector,
|
||||
* infoDetector?: ClickDetector
|
||||
* infoDetector?: ClickDetector,
|
||||
* throughputOnly?: boolean
|
||||
* }>}
|
||||
*/
|
||||
this.handles = [];
|
||||
@ -77,7 +81,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
updateShapesAfterUpgrade() {
|
||||
for (let i = 0; i < this.pinnedShapes.length; ++i) {
|
||||
const key = this.pinnedShapes[i];
|
||||
if (key === blueprintShape) {
|
||||
if (key === this.root.gameMode.getBlueprintShapeKey()) {
|
||||
// Ignore blueprint shapes
|
||||
continue;
|
||||
}
|
||||
@ -102,13 +106,14 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
if (key === this.root.hubGoals.currentGoal.definition.getHash()) {
|
||||
return this.root.hubGoals.currentGoal.required;
|
||||
}
|
||||
if (key === blueprintShape) {
|
||||
if (key === this.root.gameMode.getBlueprintShapeKey()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this shape is required for any upgrade
|
||||
for (const upgradeId in UPGRADES) {
|
||||
const upgradeTiers = UPGRADES[upgradeId];
|
||||
const upgrades = this.root.gameMode.getUpgrades();
|
||||
for (const upgradeId in upgrades) {
|
||||
const upgradeTiers = upgrades[upgradeId];
|
||||
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
|
||||
const tierHandle = upgradeTiers[currentTier];
|
||||
|
||||
@ -133,7 +138,10 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
* @param {string} 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
|
||||
return true;
|
||||
}
|
||||
@ -163,29 +171,40 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
this.handles = [];
|
||||
|
||||
// Pin story goal
|
||||
this.internalPinShape(currentKey, false, "goal");
|
||||
this.internalPinShape({
|
||||
key: currentKey,
|
||||
canUnpin: false,
|
||||
className: "goal",
|
||||
throughputOnly: currentGoal.throughputOnly,
|
||||
});
|
||||
|
||||
// Pin blueprint shape as well
|
||||
if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) {
|
||||
this.internalPinShape(blueprintShape, false, "blueprint");
|
||||
this.internalPinShape({
|
||||
key: this.root.gameMode.getBlueprintShapeKey(),
|
||||
canUnpin: false,
|
||||
className: "blueprint",
|
||||
});
|
||||
}
|
||||
|
||||
// Pin manually pinned shapes
|
||||
for (let i = 0; i < this.pinnedShapes.length; ++i) {
|
||||
const key = this.pinnedShapes[i];
|
||||
if (key !== currentKey) {
|
||||
this.internalPinShape(key);
|
||||
this.internalPinShape({ key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins a new shape
|
||||
* @param {string} key
|
||||
* @param {boolean} canUnpin
|
||||
* @param {string=} className
|
||||
* @param {object} param0
|
||||
* @param {string} param0.key
|
||||
* @param {boolean=} param0.canUnpin
|
||||
* @param {string=} param0.className
|
||||
* @param {boolean=} param0.throughputOnly
|
||||
*/
|
||||
internalPinShape(key, canUnpin = true, className = null) {
|
||||
internalPinShape({ key, canUnpin = true, className = null, throughputOnly = false }) {
|
||||
const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key);
|
||||
|
||||
const element = makeDiv(this.element, null, ["shape"]);
|
||||
@ -198,11 +217,11 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
|
||||
let detector = null;
|
||||
if (canUnpin) {
|
||||
element.classList.add("unpinable");
|
||||
element.classList.add("removable");
|
||||
detector = new ClickDetector(element, {
|
||||
consumeEvents: true,
|
||||
preventDefault: true,
|
||||
targetOnly: true,
|
||||
targetOnly: false,
|
||||
});
|
||||
detector.click.add(() => this.unpinShape(key));
|
||||
} else {
|
||||
@ -229,11 +248,13 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
|
||||
this.handles.push({
|
||||
key,
|
||||
definition,
|
||||
element,
|
||||
amountLabel,
|
||||
lastRenderedValue: "",
|
||||
detector,
|
||||
infoDetector,
|
||||
throughputOnly,
|
||||
});
|
||||
}
|
||||
|
||||
@ -244,8 +265,21 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
for (let i = 0; i < this.handles.length; ++i) {
|
||||
const handle = this.handles[i];
|
||||
|
||||
const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key);
|
||||
const currentValueFormatted = formatBigNumber(currentValue);
|
||||
let currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key);
|
||||
let currentValueFormatted = formatBigNumber(currentValue);
|
||||
|
||||
if (handle.throughputOnly) {
|
||||
currentValue =
|
||||
this.root.productionAnalytics.getCurrentShapeRate(
|
||||
enumAnalyticsDataSource.delivered,
|
||||
handle.definition
|
||||
) / globalConfig.analyticsSliceDurationSeconds;
|
||||
currentValueFormatted = T.ingame.statistics.shapesDisplayUnits.second.replace(
|
||||
"<shapes>",
|
||||
String(currentValue)
|
||||
);
|
||||
}
|
||||
|
||||
if (currentValueFormatted !== handle.lastRenderedValue) {
|
||||
handle.lastRenderedValue = currentValueFormatted;
|
||||
handle.amountLabel.innerText = currentValueFormatted;
|
||||
@ -260,6 +294,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
* @param {string} key
|
||||
*/
|
||||
unpinShape(key) {
|
||||
console.log("unpin", key);
|
||||
arrayDeleteValue(this.pinnedShapes, key);
|
||||
this.rerenderFull();
|
||||
}
|
||||
@ -275,7 +310,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === blueprintShape) {
|
||||
if (key === this.root.gameMode.getBlueprintShapeKey()) {
|
||||
// Can not pin the blueprint shape
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { blueprintShape, UPGRADES } from "../../upgrades";
|
||||
import { enumNotificationType } from "./notifications";
|
||||
import { tutorialGoals } from "../../tutorial_goals";
|
||||
|
||||
export class HUDSandboxController extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
@ -75,10 +73,11 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
}
|
||||
|
||||
giveBlueprints() {
|
||||
if (!this.root.hubGoals.storedShapes[blueprintShape]) {
|
||||
this.root.hubGoals.storedShapes[blueprintShape] = 0;
|
||||
const shape = this.root.gameMode.getBlueprintShapeKey();
|
||||
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() {
|
||||
@ -89,7 +88,7 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
}
|
||||
|
||||
modifyUpgrade(id, amount) {
|
||||
const upgradeTiers = UPGRADES[id];
|
||||
const upgradeTiers = this.root.gameMode.getUpgrades()[id];
|
||||
const maxLevel = upgradeTiers.length;
|
||||
|
||||
this.root.hubGoals.upgradeLevels[id] = Math.max(
|
||||
@ -113,7 +112,7 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
modifyLevel(amount) {
|
||||
const hubGoals = this.root.hubGoals;
|
||||
hubGoals.level = Math.max(1, hubGoals.level + amount);
|
||||
hubGoals.createNextGoal();
|
||||
hubGoals.computeNextGoal();
|
||||
|
||||
// Clear all shapes of this level
|
||||
hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0;
|
||||
@ -122,9 +121,10 @@ export class HUDSandboxController extends BaseHUDPart {
|
||||
|
||||
// Compute gained rewards
|
||||
hubGoals.gainedRewards = {};
|
||||
const levels = this.root.gameMode.getLevelDefinitions();
|
||||
for (let i = 0; i < hubGoals.level - 1; ++i) {
|
||||
if (i < tutorialGoals.length) {
|
||||
const reward = tutorialGoals[i].reward;
|
||||
if (i < levels.length) {
|
||||
const reward = levels[i].reward;
|
||||
hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 { globalConfig } from "../../../core/config";
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { createLogger } from "../../../core/logging";
|
||||
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");
|
||||
|
||||
@ -19,7 +19,7 @@ export class HUDScreenshotExporter extends BaseHUDPart {
|
||||
}
|
||||
|
||||
startExport() {
|
||||
if (IS_DEMO) {
|
||||
if (!this.root.app.restrictionMgr.getIsExportingScreenshotsPossible()) {
|
||||
this.root.hud.parts.dialogs.showFeatureRestrictionInfo(T.demo.features.exportingBase);
|
||||
return;
|
||||
}
|
||||
@ -87,7 +87,7 @@ export class HUDScreenshotExporter extends BaseHUDPart {
|
||||
const parameters = new DrawParameters({
|
||||
context,
|
||||
visibleRect,
|
||||
desiredAtlasScale: chunkScale,
|
||||
desiredAtlasScale: 0.25,
|
||||
root: this.root,
|
||||
zoomLevel: chunkScale,
|
||||
});
|
||||
|
||||
@ -43,7 +43,7 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
];
|
||||
|
||||
for (let i = 0; i < buttons.length; ++i) {
|
||||
const { title, action, id } = buttons[i];
|
||||
const { action, id } = buttons[i];
|
||||
|
||||
const element = document.createElement("button");
|
||||
element.classList.add("styledButton");
|
||||
@ -88,13 +88,8 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
this.close();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
}
|
||||
|
||||
show() {
|
||||
this.visible = true;
|
||||
document.body.classList.add("ingameDialogOpen");
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
|
||||
const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60);
|
||||
@ -120,7 +115,6 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
|
||||
close() {
|
||||
this.visible = false;
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -67,7 +67,6 @@ export class HUDShapeViewer extends BaseHUDPart {
|
||||
*/
|
||||
close() {
|
||||
this.visible = false;
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
this.update();
|
||||
}
|
||||
@ -78,7 +77,6 @@ export class HUDShapeViewer extends BaseHUDPart {
|
||||
*/
|
||||
renderForShape(definition) {
|
||||
this.visible = true;
|
||||
document.body.classList.add("ingameDialogOpen");
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
|
||||
removeAllChildren(this.renderArea);
|
||||
@ -124,13 +122,6 @@ export class HUDShapeViewer extends BaseHUDPart {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up everything
|
||||
*/
|
||||
cleanup() {
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
}
|
||||
|
||||
update() {
|
||||
this.domAttach.update(this.visible);
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { ClickDetector } from "../../../core/click_detector";
|
||||
import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { formatBigNumber, makeDiv } from "../../../core/utils";
|
||||
import { formatBigNumber, getRomanNumber, makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { UPGRADES } from "../../upgrades";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
|
||||
@ -21,7 +20,7 @@ export class HUDShop extends BaseHUDPart {
|
||||
this.upgradeToElements = {};
|
||||
|
||||
// Upgrades
|
||||
for (const upgradeId in UPGRADES) {
|
||||
for (const upgradeId in this.root.gameMode.getUpgrades()) {
|
||||
const handle = {};
|
||||
handle.requireIndexToElement = [];
|
||||
|
||||
@ -59,7 +58,7 @@ export class HUDShop extends BaseHUDPart {
|
||||
rerenderFull() {
|
||||
for (const upgradeId in this.upgradeToElements) {
|
||||
const handle = this.upgradeToElements[upgradeId];
|
||||
const upgradeTiers = UPGRADES[upgradeId];
|
||||
const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
|
||||
|
||||
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
|
||||
const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId];
|
||||
@ -68,7 +67,7 @@ export class HUDShop extends BaseHUDPart {
|
||||
// Set tier
|
||||
handle.elemTierLabel.innerText = T.ingame.shop.tier.replace(
|
||||
"<x>",
|
||||
"" + T.ingame.shop.tierLabels[currentTier]
|
||||
getRomanNumber(currentTier + 1)
|
||||
);
|
||||
|
||||
handle.elemTierLabel.setAttribute("data-tier", currentTier);
|
||||
@ -90,17 +89,15 @@ export class HUDShop extends BaseHUDPart {
|
||||
// Max level
|
||||
handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace(
|
||||
"<currentMult>",
|
||||
currentTierMultiplier.toString()
|
||||
formatBigNumber(currentTierMultiplier)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set description
|
||||
handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description
|
||||
.replace("<currentMult>", currentTierMultiplier.toString())
|
||||
.replace("<newMult>", (currentTierMultiplier + tierHandle.improvement).toString())
|
||||
// Backwards compatibility
|
||||
.replace("<gain>", (tierHandle.improvement * 100.0).toString());
|
||||
.replace("<currentMult>", formatBigNumber(currentTierMultiplier))
|
||||
.replace("<newMult>", formatBigNumber(currentTierMultiplier + tierHandle.improvement));
|
||||
|
||||
tierHandle.required.forEach(({ shape, amount }) => {
|
||||
const container = makeDiv(handle.elemRequirements, null, ["requirement"]);
|
||||
@ -207,8 +204,6 @@ export class HUDShop extends BaseHUDPart {
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
|
||||
// Cleanup detectors
|
||||
for (const upgradeId in this.upgradeToElements) {
|
||||
const handle = this.upgradeToElements[upgradeId];
|
||||
@ -224,15 +219,12 @@ export class HUDShop extends BaseHUDPart {
|
||||
|
||||
show() {
|
||||
this.visible = true;
|
||||
document.body.classList.add("ingameDialogOpen");
|
||||
// this.background.classList.add("visible");
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
this.rerenderFull();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.visible = false;
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
this.update();
|
||||
}
|
||||
|
||||
85
src/js/game/hud/parts/standalone_advantages.js
Normal file
85
src/js/game/hud/parts/standalone_advantages.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -151,17 +151,12 @@ export class HUDStatistics extends BaseHUDPart {
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
}
|
||||
|
||||
isBlockingOverlay() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.visible = true;
|
||||
document.body.classList.add("ingameDialogOpen");
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
this.rerenderFull();
|
||||
this.update();
|
||||
@ -169,7 +164,6 @@ export class HUDStatistics extends BaseHUDPart {
|
||||
|
||||
close() {
|
||||
this.visible = false;
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -1,109 +1,106 @@
|
||||
import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { TrackedState } from "../../../core/tracked_state";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { T } from "../../../translations";
|
||||
|
||||
const tutorialVideos = [2, 3, 4, 5, 6, 7, 9, 10, 11];
|
||||
|
||||
export class HUDPartTutorialHints extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(
|
||||
parent,
|
||||
"ingame_HUD_TutorialHints",
|
||||
[],
|
||||
`
|
||||
<div class="header">
|
||||
<span>${T.ingame.tutorialHints.title}</span>
|
||||
<button class="styledButton toggleHint">
|
||||
<span class="show">${T.ingame.tutorialHints.showHint}</span>
|
||||
<span class="hide">${T.ingame.tutorialHints.hideHint}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<video autoplay muted loop class="fullscreenBackgroundVideo">
|
||||
<source type="video/webm">
|
||||
</video>
|
||||
`
|
||||
);
|
||||
|
||||
this.videoElement = this.element.querySelector("video");
|
||||
}
|
||||
|
||||
shouldPauseGame() {
|
||||
return this.enlarged;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.trackClicks(this.element.querySelector(".toggleHint"), this.toggleHintEnlarged);
|
||||
|
||||
this.videoAttach = new DynamicDomAttach(this.root, this.videoElement, {
|
||||
timeToKeepSeconds: 0.3,
|
||||
});
|
||||
|
||||
this.videoAttach.update(false);
|
||||
this.enlarged = false;
|
||||
|
||||
this.inputReciever = new InputReceiver("tutorial_hints");
|
||||
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
|
||||
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
|
||||
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element);
|
||||
|
||||
this.currentShownLevel = new TrackedState(this.updateVideoUrl, this);
|
||||
}
|
||||
|
||||
updateVideoUrl(level) {
|
||||
if (tutorialVideos.indexOf(level) < 0) {
|
||||
this.videoElement.querySelector("source").setAttribute("src", "");
|
||||
this.videoElement.pause();
|
||||
} else {
|
||||
this.videoElement
|
||||
.querySelector("source")
|
||||
.setAttribute("src", "https://static.shapez.io/tutorial_videos/level_" + level + ".webm");
|
||||
this.videoElement.currentTime = 0;
|
||||
this.videoElement.load();
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.enlarged = false;
|
||||
document.body.classList.remove("ingameDialogOpen");
|
||||
this.element.classList.remove("enlarged", "noBlur");
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
this.update();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.root.app.analytics.trackUiClick("tutorial_hint_show");
|
||||
this.root.app.analytics.trackUiClick("tutorial_hint_show_lvl_" + this.root.hubGoals.level);
|
||||
|
||||
document.body.classList.add("ingameDialogOpen");
|
||||
this.element.classList.add("enlarged", "noBlur");
|
||||
this.enlarged = true;
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
this.update();
|
||||
|
||||
this.videoElement.currentTime = 0;
|
||||
this.videoElement.play();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.videoAttach.update(this.enlarged);
|
||||
|
||||
this.currentShownLevel.set(this.root.hubGoals.level);
|
||||
|
||||
const tutorialVisible = tutorialVideos.indexOf(this.root.hubGoals.level) >= 0;
|
||||
this.domAttach.update(tutorialVisible);
|
||||
}
|
||||
|
||||
toggleHintEnlarged() {
|
||||
if (this.enlarged) {
|
||||
this.close();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { TrackedState } from "../../../core/tracked_state";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { T } from "../../../translations";
|
||||
|
||||
const tutorialVideos = [2, 3, 4, 5, 6, 7, 9, 10, 11];
|
||||
|
||||
export class HUDPartTutorialHints extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(
|
||||
parent,
|
||||
"ingame_HUD_TutorialHints",
|
||||
[],
|
||||
`
|
||||
<div class="header">
|
||||
<span>${T.ingame.tutorialHints.title}</span>
|
||||
<button class="styledButton toggleHint">
|
||||
<span class="show">${T.ingame.tutorialHints.showHint}</span>
|
||||
<span class="hide">${T.ingame.tutorialHints.hideHint}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<video autoplay muted loop class="fullscreenBackgroundVideo">
|
||||
<source type="video/webm">
|
||||
</video>
|
||||
`
|
||||
);
|
||||
|
||||
this.videoElement = this.element.querySelector("video");
|
||||
}
|
||||
|
||||
shouldPauseGame() {
|
||||
return this.enlarged;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.trackClicks(this.element.querySelector(".toggleHint"), this.toggleHintEnlarged);
|
||||
|
||||
this.videoAttach = new DynamicDomAttach(this.root, this.videoElement, {
|
||||
timeToKeepSeconds: 0.3,
|
||||
});
|
||||
|
||||
this.videoAttach.update(false);
|
||||
this.enlarged = false;
|
||||
|
||||
this.inputReciever = new InputReceiver("tutorial_hints");
|
||||
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
|
||||
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
|
||||
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element);
|
||||
|
||||
this.currentShownLevel = new TrackedState(this.updateVideoUrl, this);
|
||||
}
|
||||
|
||||
updateVideoUrl(level) {
|
||||
if (tutorialVideos.indexOf(level) < 0) {
|
||||
this.videoElement.querySelector("source").setAttribute("src", "");
|
||||
this.videoElement.pause();
|
||||
} else {
|
||||
this.videoElement
|
||||
.querySelector("source")
|
||||
.setAttribute("src", "https://static.shapez.io/tutorial_videos/level_" + level + ".webm");
|
||||
this.videoElement.currentTime = 0;
|
||||
this.videoElement.load();
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.enlarged = false;
|
||||
this.element.classList.remove("enlarged", "noBlur");
|
||||
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||
this.update();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.root.app.analytics.trackUiClick("tutorial_hint_show");
|
||||
this.root.app.analytics.trackUiClick("tutorial_hint_show_lvl_" + this.root.hubGoals.level);
|
||||
this.element.classList.add("enlarged", "noBlur");
|
||||
this.enlarged = true;
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
this.update();
|
||||
|
||||
this.videoElement.currentTime = 0;
|
||||
this.videoElement.play();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.videoAttach.update(this.enlarged);
|
||||
|
||||
this.currentShownLevel.set(this.root.hubGoals.level);
|
||||
|
||||
const tutorialVisible = tutorialVideos.indexOf(this.root.hubGoals.level) >= 0;
|
||||
this.domAttach.update(tutorialVisible);
|
||||
}
|
||||
|
||||
toggleHintEnlarged() {
|
||||
if (this.enlarged) {
|
||||
this.close();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { globalConfig } from "../../../core/config";
|
||||
import { gMetaBuildingRegistry } from "../../../core/global_registries";
|
||||
import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { SOUNDS } from "../../../platform/sound";
|
||||
import { T } from "../../../translations";
|
||||
import { defaultBuildingVariant } from "../../meta_building";
|
||||
import { enumHubGoalRewards } from "../../tutorial_goals";
|
||||
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
|
||||
import { InputReceiver } from "../../../core/input_receiver";
|
||||
import { enumNotificationType } from "./notifications";
|
||||
|
||||
export class HUDUnlockNotification extends BaseHUDPart {
|
||||
initialize() {
|
||||
@ -50,6 +51,18 @@ export class HUDUnlockNotification extends BaseHUDPart {
|
||||
* @param {enumHubGoalRewards} 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.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace(
|
||||
"<level>",
|
||||
@ -83,7 +96,6 @@ export class HUDUnlockNotification extends BaseHUDPart {
|
||||
|
||||
this.elemContents.innerHTML = html;
|
||||
this.visible = true;
|
||||
this.root.soundProxy.playUi(SOUNDS.levelComplete);
|
||||
|
||||
if (this.buttonShowTimeout) {
|
||||
clearTimeout(this.buttonShowTimeout);
|
||||
|
||||
@ -1,44 +1,72 @@
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { THIRDPARTY_URLS } from "../../../core/config";
|
||||
import { T } from "../../../translations";
|
||||
|
||||
export class HUDWatermark extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_Watermark");
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.trackClicks(this.element, this.onWatermarkClick);
|
||||
}
|
||||
|
||||
onWatermarkClick() {
|
||||
this.root.app.analytics.trackUiClick("watermark_click_2");
|
||||
this.root.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DrawParameters} parameters
|
||||
*/
|
||||
drawOverlays(parameters) {
|
||||
const w = this.root.gameWidth;
|
||||
const x = 280 * this.root.app.getEffectiveUiScale();
|
||||
|
||||
parameters.context.fillStyle = "#f77";
|
||||
parameters.context.font = "bold " + this.root.app.getEffectiveUiScale() * 17 + "px GameFont";
|
||||
// parameters.context.textAlign = "center";
|
||||
parameters.context.fillText(
|
||||
T.demoBanners.title.toUpperCase(),
|
||||
x,
|
||||
this.root.app.getEffectiveUiScale() * 27
|
||||
);
|
||||
|
||||
parameters.context.font = "bold " + this.root.app.getEffectiveUiScale() * 12 + "px GameFont";
|
||||
// parameters.context.textAlign = "center";
|
||||
parameters.context.fillText(T.demoBanners.intro, x, this.root.app.getEffectiveUiScale() * 45);
|
||||
|
||||
// parameters.context.textAlign = "left";
|
||||
}
|
||||
}
|
||||
import { THIRDPARTY_URLS } from "../../../core/config";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
|
||||
const watermarkShowIntervalSeconds = G_IS_DEV ? 120 : 7 * 60;
|
||||
const watermarkShowDuration = 5;
|
||||
|
||||
export class HUDWatermark extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(
|
||||
parent,
|
||||
"ingame_HUD_Watermark",
|
||||
[],
|
||||
`
|
||||
<strong>${T.ingame.watermark.title}</strong>
|
||||
<p>${T.ingame.watermark.desc}</p>
|
||||
`
|
||||
);
|
||||
|
||||
this.linkElement = makeDiv(
|
||||
parent,
|
||||
"ingame_HUD_WatermarkClicker",
|
||||
[],
|
||||
T.ingame.watermark.get_on_steam
|
||||
);
|
||||
this.trackClicks(this.linkElement, () => {
|
||||
this.root.app.analytics.trackUiClick("watermark_click_2_direct");
|
||||
this.root.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage + "?ref=wtmd");
|
||||
});
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.trackClicks(this.element, this.onWatermarkClick);
|
||||
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.element, {
|
||||
attachClass: "visible",
|
||||
timeToKeepSeconds: 0.5,
|
||||
});
|
||||
}
|
||||
|
||||
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
Loading…
Reference in New Issue
Block a user