mirror of
https://github.com/tobspr/shapez.io.git
synced 2024-10-27 20:34:29 +00:00
Achievements (#1087)
* [WIP] Add boilerplate for achievement implementation * Add config.local.template.js and rm cached copy of config.local.js * [WIP] Implement painting, cutting, rotating achievements (to log only) * [WIP] Refactor achievements, jsdoc fixes, add npm script - Refactor achievements to make use of Signals - Move implemented achievement interfaces to appropriate platform folders (SteamAchievements in currently in use in browser wrapper for testing) - Fix invalid jsdocs - Add dev-standalone script to package.json scripts * Add steam/greenworks IPC calls and optional private-artifact dependency * Include private artifacts in standalone builds * Uncomment appid include * [WIP] Add steam overlay fix, add hash to artifact dependency * Update electron, greenworks. Add task to add local config if not present * Add more achievements, refactor achievement code * Add receiver flexibility and more achievements - Add check to see if necessary to create achievement and add receiver - Add remove receiver functionality when achievement is unlocked * Add achievements and accommodations for switching states - Fix startup code to avoid clobbering achievements on state switch - Add a few more achievements * Add achievements, ids. Update names, keys for consistency * Add play time achievements * [WIP] Add more achievements * Add more achievements. Add bulk achievement check signal * [WIP] Add achievements. Start savefile migration * Add achievements. Add savefile migration * Remove superfluous achievement stat * Update lock files, fix merge conflict
This commit is contained in:
parent
afdce2268e
commit
26b842494f
4
.gitignore
vendored
4
.gitignore
vendored
@ -51,3 +51,7 @@ tmp_standalone_files_china
|
|||||||
# Local config
|
# Local config
|
||||||
config.local.js
|
config.local.js
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Editor artifacts
|
||||||
|
*.*.swp
|
||||||
|
*.*.swo
|
||||||
|
@ -6,6 +6,7 @@ const url = require("url");
|
|||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
const { ipcMain, shell } = require("electron");
|
const { ipcMain, shell } = require("electron");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const steam = require('./steam');
|
||||||
const isDev = process.argv.indexOf("--dev") >= 0;
|
const isDev = process.argv.indexOf("--dev") >= 0;
|
||||||
const isLocal = process.argv.indexOf("--local") >= 0;
|
const isLocal = process.argv.indexOf("--local") >= 0;
|
||||||
|
|
||||||
@ -222,3 +223,6 @@ ipcMain.on("fs-sync-job", (event, arg) => {
|
|||||||
const result = performFsJob(arg);
|
const result = performFsJob(arg);
|
||||||
event.returnValue = result;
|
event.returnValue = result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
steam.init(isDev);
|
||||||
|
steam.listen();
|
||||||
|
1731
electron/package-lock.json
generated
Normal file
1731
electron/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,9 @@
|
|||||||
"start": "electron --disable-direct-composition --in-process-gpu ."
|
"start": "electron --disable-direct-composition --in-process-gpu ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "10.1.3"
|
"electron": "11.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"optionalDependencies": {
|
||||||
|
"shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v85"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
73
electron/steam.js
Normal file
73
electron/steam.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { ipcMain } = require("electron");
|
||||||
|
|
||||||
|
let greenworks = null;
|
||||||
|
let appId = null;
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
greenworks = require("shapez.io-private-artifacts/steam/greenworks");
|
||||||
|
appId = parseInt(fs.readFileSync(path.join(__dirname, "steam_appid.txt"), "utf8"));
|
||||||
|
} catch (err) {
|
||||||
|
// greenworks is not installed
|
||||||
|
// throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init (isDev) {
|
||||||
|
if (!greenworks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDev) {
|
||||||
|
if (greenworks.restartAppIfNecessary(appId)) {
|
||||||
|
console.log("Restarting ...");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!greenworks.init()) {
|
||||||
|
console.log("Failed to initialize greenworks");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listen () {
|
||||||
|
ipcMain.handle("steam:is-initialized", isInitialized);
|
||||||
|
|
||||||
|
if (!greenworks || !initialized) {
|
||||||
|
console.log("Ignoring Steam IPC events");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("steam:get-achievement-names", getAchievementNames);
|
||||||
|
ipcMain.handle("steam:activate-achievement", activateAchievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInitialized(event) {
|
||||||
|
return Promise.resolve(initialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAchievementNames(event) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const achievements = greenworks.getAchievementNames()
|
||||||
|
resolve(achievements);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateAchievement(event, id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
greenworks.activateAchievement(id, () => resolve(), err => reject(err))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
listen
|
||||||
|
};
|
@ -1 +1 @@
|
|||||||
1134480
|
1318690
|
||||||
|
1148
electron/yarn.lock
1148
electron/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -50,6 +50,9 @@ css.gulptasksCSS($, gulp, buildFolder, browserSync);
|
|||||||
const sounds = require("./sounds");
|
const sounds = require("./sounds");
|
||||||
sounds.gulptasksSounds($, gulp, buildFolder);
|
sounds.gulptasksSounds($, gulp, buildFolder);
|
||||||
|
|
||||||
|
const localConfig = require("./local-config");
|
||||||
|
localConfig.gulptasksLocalConfig($, gulp);
|
||||||
|
|
||||||
const js = require("./js");
|
const js = require("./js");
|
||||||
js.gulptasksJS($, gulp, buildFolder, browserSync);
|
js.gulptasksJS($, gulp, buildFolder, browserSync);
|
||||||
|
|
||||||
@ -225,6 +228,7 @@ gulp.task(
|
|||||||
gulp.series(
|
gulp.series(
|
||||||
"utils.cleanup",
|
"utils.cleanup",
|
||||||
"utils.copyAdditionalBuildFiles",
|
"utils.copyAdditionalBuildFiles",
|
||||||
|
"localConfig.findOrCreate",
|
||||||
"imgres.buildAtlas",
|
"imgres.buildAtlas",
|
||||||
"imgres.atlasToJson",
|
"imgres.atlasToJson",
|
||||||
"imgres.atlas",
|
"imgres.atlas",
|
||||||
@ -242,6 +246,7 @@ gulp.task(
|
|||||||
"build.standalone.dev",
|
"build.standalone.dev",
|
||||||
gulp.series(
|
gulp.series(
|
||||||
"utils.cleanup",
|
"utils.cleanup",
|
||||||
|
"localConfig.findOrCreate",
|
||||||
"imgres.buildAtlas",
|
"imgres.buildAtlas",
|
||||||
"imgres.atlasToJson",
|
"imgres.atlasToJson",
|
||||||
"imgres.atlas",
|
"imgres.atlas",
|
||||||
|
18
gulp/local-config.js
Normal file
18
gulp/local-config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const fse = require("fs-extra");
|
||||||
|
|
||||||
|
const configTemplatePath = path.join(__dirname, "../src/js/core/config.local.template.js");
|
||||||
|
const configPath = path.join(__dirname, "../src/js/core/config.local.js");
|
||||||
|
|
||||||
|
function gulptasksLocalConfig($, gulp) {
|
||||||
|
gulp.task("localConfig.findOrCreate", cb => {
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
fse.copySync(configTemplatePath, configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { gulptasksLocalConfig };
|
@ -1,5 +1,6 @@
|
|||||||
require("colors");
|
require("colors");
|
||||||
const packager = require("electron-packager");
|
const packager = require("electron-packager");
|
||||||
|
const pj = require("../electron/package.json");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { getVersion } = require("./buildutils");
|
const { getVersion } = require("./buildutils");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
@ -9,49 +10,53 @@ const execSync = require("child_process").execSync;
|
|||||||
|
|
||||||
function gulptasksStandalone($, gulp) {
|
function gulptasksStandalone($, gulp) {
|
||||||
const electronBaseDir = path.join(__dirname, "..", "electron");
|
const electronBaseDir = path.join(__dirname, "..", "electron");
|
||||||
|
const targets = [
|
||||||
for (const { tempDestDir, suffix, taskPrefix } of [
|
{
|
||||||
{ tempDestDir: path.join(__dirname, "..", "tmp_standalone_files"), suffix: "", taskPrefix: "" },
|
tempDestDir: path.join(__dirname, "..", "tmp_standalone_files"),
|
||||||
|
suffix: "",
|
||||||
|
taskPrefix: ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
tempDestDir: path.join(__dirname, "..", "tmp_standalone_files_china"),
|
tempDestDir: path.join(__dirname, "..", "tmp_standalone_files_china"),
|
||||||
suffix: "china",
|
suffix: "china",
|
||||||
taskPrefix: "china.",
|
taskPrefix: "china.",
|
||||||
},
|
},
|
||||||
]) {
|
];
|
||||||
|
|
||||||
|
for (const { tempDestDir, suffix, taskPrefix } of targets) {
|
||||||
const tempDestBuildDir = path.join(tempDestDir, "built");
|
const tempDestBuildDir = path.join(tempDestDir, "built");
|
||||||
|
|
||||||
gulp.task(taskPrefix + "standalone.prepare.cleanup", () => {
|
gulp.task(taskPrefix + "standalone.prepare.cleanup", () => {
|
||||||
return gulp.src(tempDestDir, { read: false, allowEmpty: true }).pipe($.clean({ force: true }));
|
return gulp.src(tempDestDir, { read: false, allowEmpty: true })
|
||||||
|
.pipe($.clean({ force: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(taskPrefix + "standalone.prepare.copyPrefab", () => {
|
gulp.task(taskPrefix + "standalone.prepare.copyPrefab", () => {
|
||||||
// const requiredFiles = $.glob.sync("../electron/");
|
|
||||||
const requiredFiles = [
|
const requiredFiles = [
|
||||||
path.join(electronBaseDir, "lib", "**", "*.node"),
|
|
||||||
path.join(electronBaseDir, "node_modules", "**", "*.*"),
|
path.join(electronBaseDir, "node_modules", "**", "*.*"),
|
||||||
path.join(electronBaseDir, "node_modules", "**", ".*"),
|
path.join(electronBaseDir, "node_modules", "**", ".*"),
|
||||||
|
path.join(electronBaseDir, "steam_appid.txt"),
|
||||||
path.join(electronBaseDir, "favicon*"),
|
path.join(electronBaseDir, "favicon*"),
|
||||||
|
|
||||||
// fails on platforms which support symlinks
|
// fails on platforms which support symlinks
|
||||||
// https://github.com/gulpjs/gulp/issues/1427
|
// https://github.com/gulpjs/gulp/issues/1427
|
||||||
// path.join(electronBaseDir, "node_modules", "**", "*"),
|
// path.join(electronBaseDir, "node_modules", "**", "*"),
|
||||||
];
|
];
|
||||||
return gulp.src(requiredFiles, { base: electronBaseDir }).pipe(gulp.dest(tempDestBuildDir));
|
return gulp.src(requiredFiles, { base: electronBaseDir })
|
||||||
|
.pipe(gulp.dest(tempDestBuildDir));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(taskPrefix + "standalone.prepare.writePackageJson", cb => {
|
gulp.task(taskPrefix + "standalone.prepare.writePackageJson", cb => {
|
||||||
fs.writeFileSync(
|
const packageJsonString = JSON.stringify({
|
||||||
path.join(tempDestBuildDir, "package.json"),
|
scripts: {
|
||||||
JSON.stringify(
|
start: pj.scripts.start
|
||||||
{
|
},
|
||||||
devDependencies: {
|
devDependencies: pj.devDependencies,
|
||||||
electron: "6.1.12",
|
optionalDependencies: pj.optionalDependencies
|
||||||
},
|
}, null, 4);
|
||||||
},
|
|
||||||
null,
|
fs.writeFileSync(path.join(tempDestBuildDir, "package.json"), packageJsonString);
|
||||||
4
|
|
||||||
)
|
|
||||||
);
|
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,7 +79,8 @@ function gulptasksStandalone($, gulp) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(taskPrefix + "standalone.prepare.copyGamefiles", () => {
|
gulp.task(taskPrefix + "standalone.prepare.copyGamefiles", () => {
|
||||||
return gulp.src("../build/**/*.*", { base: "../build" }).pipe(gulp.dest(tempDestBuildDir));
|
return gulp.src("../build/**/*.*", { base: "../build" })
|
||||||
|
.pipe(gulp.dest(tempDestBuildDir));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(taskPrefix + "standalone.killRunningInstances", cb => {
|
gulp.task(taskPrefix + "standalone.killRunningInstances", cb => {
|
||||||
@ -106,6 +112,14 @@ function gulptasksStandalone($, gulp) {
|
|||||||
*/
|
*/
|
||||||
function packageStandalone(platform, arch, cb) {
|
function packageStandalone(platform, arch, cb) {
|
||||||
const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml"));
|
const tomlFile = fs.readFileSync(path.join(__dirname, ".itch.toml"));
|
||||||
|
const privateArtifactsPath = "node_modules/shapez.io-private-artifacts";
|
||||||
|
|
||||||
|
let asar;
|
||||||
|
if (fs.existsSync(path.join(tempDestBuildDir, privateArtifactsPath))) {
|
||||||
|
asar = { unpackDir: privateArtifactsPath };
|
||||||
|
} else {
|
||||||
|
asar = true;
|
||||||
|
}
|
||||||
|
|
||||||
packager({
|
packager({
|
||||||
dir: tempDestBuildDir,
|
dir: tempDestBuildDir,
|
||||||
@ -114,7 +128,7 @@ function gulptasksStandalone($, gulp) {
|
|||||||
buildVersion: "1.0.0",
|
buildVersion: "1.0.0",
|
||||||
arch,
|
arch,
|
||||||
platform,
|
platform,
|
||||||
asar: true,
|
asar: asar,
|
||||||
executableName: "shapezio",
|
executableName: "shapezio",
|
||||||
icon: path.join(electronBaseDir, "favicon"),
|
icon: path.join(electronBaseDir, "favicon"),
|
||||||
name: "shapez.io-standalone" + suffix,
|
name: "shapez.io-standalone" + suffix,
|
||||||
@ -136,6 +150,11 @@ function gulptasksStandalone($, gulp) {
|
|||||||
fs.readFileSync(path.join(__dirname, "..", "LICENSE"))
|
fs.readFileSync(path.join(__dirname, "..", "LICENSE"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fse.copySync(
|
||||||
|
path.join(tempDestBuildDir, "steam_appid.txt"),
|
||||||
|
path.join(appPath, "steam_appid.txt")
|
||||||
|
);
|
||||||
|
|
||||||
fs.writeFileSync(path.join(appPath, ".itch.toml"), tomlFile);
|
fs.writeFileSync(path.join(appPath, ".itch.toml"), tomlFile);
|
||||||
|
|
||||||
if (platform === "linux") {
|
if (platform === "linux") {
|
||||||
@ -156,7 +175,9 @@ function gulptasksStandalone($, gulp) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
gulp.task(taskPrefix + "standalone.package.prod.win64", cb => packageStandalone("win32", "x64", cb));
|
gulp.task(taskPrefix + "standalone.package.prod.win64", cb =>
|
||||||
|
packageStandalone("win32", "x64", cb)
|
||||||
|
);
|
||||||
gulp.task(taskPrefix + "standalone.package.prod.linux64", cb =>
|
gulp.task(taskPrefix + "standalone.package.prod.linux64", cb =>
|
||||||
packageStandalone("linux", "x64", cb)
|
packageStandalone("linux", "x64", cb)
|
||||||
);
|
);
|
||||||
|
27320
gulp/yarn.lock
27320
gulp/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd gulp && yarn gulp main.serveDev",
|
"dev": "cd gulp && yarn gulp main.serveDev",
|
||||||
|
"devStandalone": "cd gulp && yarn gulp main.serveStandalone",
|
||||||
"tslint": "cd src/js && tsc",
|
"tslint": "cd src/js && tsc",
|
||||||
"lint": "eslint src/js",
|
"lint": "eslint src/js",
|
||||||
"prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*",
|
"prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*",
|
||||||
|
@ -12,6 +12,7 @@ import { getPlatformName, waitNextFrame } from "./core/utils";
|
|||||||
import { Vector } from "./core/vector";
|
import { Vector } from "./core/vector";
|
||||||
import { AdProviderInterface } from "./platform/ad_provider";
|
import { AdProviderInterface } from "./platform/ad_provider";
|
||||||
import { NoAdProvider } from "./platform/ad_providers/no_ad_provider";
|
import { NoAdProvider } from "./platform/ad_providers/no_ad_provider";
|
||||||
|
import { NoAchievementProvider } from "./platform/browser/no_achievement_provider";
|
||||||
import { AnalyticsInterface } from "./platform/analytics";
|
import { AnalyticsInterface } from "./platform/analytics";
|
||||||
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
|
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
|
||||||
import { SoundImplBrowser } from "./platform/browser/sound";
|
import { SoundImplBrowser } from "./platform/browser/sound";
|
||||||
@ -32,6 +33,7 @@ import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
|||||||
import { RestrictionManager } from "./core/restriction_manager";
|
import { RestrictionManager } from "./core/restriction_manager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
||||||
* @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface
|
* @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface
|
||||||
* @typedef {import("./platform/sound").SoundInterface} SoundInterface
|
* @typedef {import("./platform/sound").SoundInterface} SoundInterface
|
||||||
* @typedef {import("./platform/storage").StorageInterface} StorageInterface
|
* @typedef {import("./platform/storage").StorageInterface} StorageInterface
|
||||||
@ -85,6 +87,9 @@ export class Application {
|
|||||||
/** @type {PlatformWrapperInterface} */
|
/** @type {PlatformWrapperInterface} */
|
||||||
this.platformWrapper = null;
|
this.platformWrapper = null;
|
||||||
|
|
||||||
|
/** @type {AchievementProviderInterface} */
|
||||||
|
this.achievementProvider = null;
|
||||||
|
|
||||||
/** @type {AdProviderInterface} */
|
/** @type {AdProviderInterface} */
|
||||||
this.adProvider = null;
|
this.adProvider = null;
|
||||||
|
|
||||||
@ -137,6 +142,7 @@ export class Application {
|
|||||||
this.sound = new SoundImplBrowser(this);
|
this.sound = new SoundImplBrowser(this);
|
||||||
this.analytics = new GoogleAnalyticsImpl(this);
|
this.analytics = new GoogleAnalyticsImpl(this);
|
||||||
this.gameAnalytics = new ShapezGameAnalytics(this);
|
this.gameAnalytics = new ShapezGameAnalytics(this);
|
||||||
|
this.achievementProvider = new NoAchievementProvider(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,6 +40,9 @@ export const globalConfig = {
|
|||||||
assetsSharpness: 1.5,
|
assetsSharpness: 1.5,
|
||||||
shapesSharpness: 1.4,
|
shapesSharpness: 1.4,
|
||||||
|
|
||||||
|
// Achievements
|
||||||
|
achievementSliceDuration: 10, // Seconds
|
||||||
|
|
||||||
// Production analytics
|
// Production analytics
|
||||||
statisticsGraphDpi: 2.5,
|
statisticsGraphDpi: 2.5,
|
||||||
statisticsGraphSlices: 100,
|
statisticsGraphSlices: 100,
|
||||||
|
@ -59,6 +59,9 @@ export default {
|
|||||||
// Enables ads in the local build (normally they are deactivated there)
|
// Enables ads in the local build (normally they are deactivated there)
|
||||||
// testAds: true,
|
// testAds: true,
|
||||||
// -----------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------
|
||||||
|
// Allows unlocked achievements to be logged to console in the local build
|
||||||
|
// testAchievements: true,
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
// Disables the automatic switch to an overview when zooming out
|
// Disables the automatic switch to an overview when zooming out
|
||||||
// disableMapOverview: true,
|
// disableMapOverview: true,
|
||||||
// -----------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------
|
170
src/js/game/achievement_proxy.js
Normal file
170
src/js/game/achievement_proxy.js
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
/* typehints:start */
|
||||||
|
import { Entity } from "./entity";
|
||||||
|
import { GameRoot } from "./root";
|
||||||
|
/* typehints:end */
|
||||||
|
|
||||||
|
import { globalConfig } from "../core/config";
|
||||||
|
import { createLogger } from "../core/logging";
|
||||||
|
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
||||||
|
import { getBuildingDataFromCode } from "./building_codes";
|
||||||
|
|
||||||
|
const logger = createLogger("achievement_proxy");
|
||||||
|
|
||||||
|
const ROTATER = "rotater";
|
||||||
|
const DEFAULT = "default";
|
||||||
|
const BELT = "belt";
|
||||||
|
const LEVEL_26 = 26;
|
||||||
|
|
||||||
|
export class AchievementProxy {
|
||||||
|
/** @param {GameRoot} root */
|
||||||
|
constructor(root) {
|
||||||
|
this.root = root;
|
||||||
|
this.provider = this.root.app.achievementProvider;
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
if (!this.provider.hasAchievements()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sliceTime = 0;
|
||||||
|
this.sliceIteration = 1;
|
||||||
|
this.sliceIterationLimit = 10;
|
||||||
|
|
||||||
|
this.root.signals.postLoadHook.add(this.onLoad, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
this.provider.onLoad(this.root)
|
||||||
|
.then(() => {
|
||||||
|
this.disabled = false;
|
||||||
|
logger.log("Recieving achievement signals");
|
||||||
|
this.initialize();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.disabled = true;
|
||||||
|
logger.error("Ignoring achievement signals", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.darkMode);
|
||||||
|
|
||||||
|
if (this.has(ACHIEVEMENTS.mam)) {
|
||||||
|
this.root.signals.storyGoalCompleted.add(this.onStoryGoalCompleted, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.has(ACHIEVEMENTS.noInverseRotater)) {
|
||||||
|
this.root.signals.entityAdded.add(this.onEntityAdded, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.has(ACHIEVEMENTS.noBeltUpgradesUntilBp)) {
|
||||||
|
this.root.signals.upgradePurchased.add(this.onUpgradePurchased, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
startSlice() {
|
||||||
|
this.sliceTime = this.root.time.now();
|
||||||
|
|
||||||
|
// Every slice
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.storeShape);
|
||||||
|
|
||||||
|
// Every other slice
|
||||||
|
if (this.sliceIteration % 2 === 0) {
|
||||||
|
this.root.signals.bulkAchievementCheck.dispatch(
|
||||||
|
ACHIEVEMENTS.throughputBp25, this.sliceTime,
|
||||||
|
ACHIEVEMENTS.throughputBp50, this.sliceTime,
|
||||||
|
ACHIEVEMENTS.throughputLogo25, this.sliceTime,
|
||||||
|
ACHIEVEMENTS.throughputLogo50, this.sliceTime,
|
||||||
|
ACHIEVEMENTS.throughputRocket10, this.sliceTime,
|
||||||
|
ACHIEVEMENTS.throughputRocket20, this.sliceTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every 3rd slice
|
||||||
|
if (this.sliceIteration % 3 === 0) {
|
||||||
|
this.root.signals.bulkAchievementCheck.dispatch(
|
||||||
|
ACHIEVEMENTS.play1h, this.sliceTime,
|
||||||
|
ACHIEVEMENTS.play10h, this.sliceTime,
|
||||||
|
ACHIEVEMENTS.play20h, this.sliceTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every 10th slice
|
||||||
|
if (this.sliceIteration % 10 === 0) {
|
||||||
|
this.provider.collection.clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sliceIteration === this.sliceIterationLimit) {
|
||||||
|
this.sliceIteration = 1;
|
||||||
|
} else {
|
||||||
|
this.sliceIteration++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.root.time.now() - this.sliceTime > globalConfig.achievementSliceDuration) {
|
||||||
|
this.startSlice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
has(key) {
|
||||||
|
return this.provider.collection.map.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Entity} entity */
|
||||||
|
onEntityAdded(entity) {
|
||||||
|
if (!entity.components.StaticMapEntity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const building = getBuildingDataFromCode(entity.components.StaticMapEntity.code)
|
||||||
|
|
||||||
|
if (building.metaInstance.id !== ROTATER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (building.variant === DEFAULT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.savegame.currentData.stats.usedInverseRotater = true;
|
||||||
|
this.root.signals.entityAdded.remove(this.onEntityAdded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} level */
|
||||||
|
onStoryGoalCompleted(level) {
|
||||||
|
if (level === LEVEL_26) {
|
||||||
|
this.root.signals.entityAdded.add(this.onMamFailure, this);
|
||||||
|
this.root.signals.entityDestroyed.add(this.onMamFailure, this);
|
||||||
|
} else if (level === LEVEL_26 + 1) {
|
||||||
|
this.root.signals.storyGoalCompleted.remove(this.onStoryGoalCompleted, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMamFailure() {
|
||||||
|
this.root.savegame.currentData.stats.failedMam = true;
|
||||||
|
this.root.signals.entityAdded.remove(this.onMamFailure);
|
||||||
|
this.root.signals.entityDestroyed.remove(this.onMamFailure);
|
||||||
|
this.root.signals.storyGoalCompleted.remove(this.onStoryGoalCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} upgrade */
|
||||||
|
onUpgradePurchased(upgrade) {
|
||||||
|
if (upgrade !== BELT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.savegame.currentData.stats.upgradedBelt = true;
|
||||||
|
this.root.signals.upgradePurchased.remove(this.onUpgradePurchased);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { DrawParameters } from "../core/draw_parameters";
|
|||||||
import { findNiceIntegerValue } from "../core/utils";
|
import { findNiceIntegerValue } from "../core/utils";
|
||||||
import { Vector } from "../core/vector";
|
import { Vector } from "../core/vector";
|
||||||
import { Entity } from "./entity";
|
import { Entity } from "./entity";
|
||||||
|
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
||||||
import { GameRoot } from "./root";
|
import { GameRoot } from "./root";
|
||||||
|
|
||||||
export class Blueprint {
|
export class Blueprint {
|
||||||
@ -148,7 +149,7 @@ export class Blueprint {
|
|||||||
*/
|
*/
|
||||||
tryPlace(root, tile) {
|
tryPlace(root, tile) {
|
||||||
return root.logic.performBulkOperation(() => {
|
return root.logic.performBulkOperation(() => {
|
||||||
let anyPlaced = false;
|
let count = 0;
|
||||||
for (let i = 0; i < this.entities.length; ++i) {
|
for (let i = 0; i < this.entities.length; ++i) {
|
||||||
const entity = this.entities[i];
|
const entity = this.entities[i];
|
||||||
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
||||||
@ -160,9 +161,15 @@ export class Blueprint {
|
|||||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||||
root.map.placeStaticEntity(clone);
|
root.map.placeStaticEntity(clone);
|
||||||
root.entityMgr.registerEntity(clone);
|
root.entityMgr.registerEntity(clone);
|
||||||
anyPlaced = true;
|
count++;
|
||||||
}
|
}
|
||||||
return anyPlaced;
|
|
||||||
|
root.signals.bulkAchievementCheck.dispatch(
|
||||||
|
ACHIEVEMENTS.placeBlueprint, count,
|
||||||
|
ACHIEVEMENTS.placeBp1000, count
|
||||||
|
);
|
||||||
|
|
||||||
|
return count !== 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { generateMatrixRotations } from "../../core/utils";
|
import { generateMatrixRotations } from "../../core/utils";
|
||||||
import { enumDirection, Vector } from "../../core/vector";
|
import { enumDirection, Vector } from "../../core/vector";
|
||||||
|
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
|
||||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||||
import { Entity } from "../entity";
|
import { Entity } from "../entity";
|
||||||
@ -37,6 +38,25 @@ export class MetaTrashBuilding extends MetaBuilding {
|
|||||||
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash);
|
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addAchievementReceiver(entity) {
|
||||||
|
if (!entity.root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemProcessor = entity.components.ItemProcessor
|
||||||
|
const tryTakeItem = itemProcessor.tryTakeItem.bind(itemProcessor);
|
||||||
|
|
||||||
|
itemProcessor.tryTakeItem = () => {
|
||||||
|
const taken = tryTakeItem(...arguments);
|
||||||
|
|
||||||
|
if (taken) {
|
||||||
|
entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the entity at the given location
|
* Creates the entity at the given location
|
||||||
* @param {Entity} entity
|
* @param {Entity} entity
|
||||||
@ -57,11 +77,14 @@ export class MetaTrashBuilding extends MetaBuilding {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
entity.addComponent(
|
entity.addComponent(
|
||||||
new ItemProcessorComponent({
|
new ItemProcessorComponent({
|
||||||
inputsPerCharge: 1,
|
inputsPerCharge: 1,
|
||||||
processorType: enumItemProcessorTypes.trash,
|
processorType: enumItemProcessorTypes.trash,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.addAchievementReceiver(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import { RegularGameMode } from "./modes/regular";
|
|||||||
import { ProductionAnalytics } from "./production_analytics";
|
import { ProductionAnalytics } from "./production_analytics";
|
||||||
import { GameRoot } from "./root";
|
import { GameRoot } from "./root";
|
||||||
import { ShapeDefinitionManager } from "./shape_definition_manager";
|
import { ShapeDefinitionManager } from "./shape_definition_manager";
|
||||||
|
import { AchievementProxy } from "./achievement_proxy";
|
||||||
import { SoundProxy } from "./sound_proxy";
|
import { SoundProxy } from "./sound_proxy";
|
||||||
import { GameTime } from "./time/game_time";
|
import { GameTime } from "./time/game_time";
|
||||||
|
|
||||||
@ -111,6 +112,7 @@ export class GameCore {
|
|||||||
root.logic = new GameLogic(root);
|
root.logic = new GameLogic(root);
|
||||||
root.hud = new GameHUD(root);
|
root.hud = new GameHUD(root);
|
||||||
root.time = new GameTime(root);
|
root.time = new GameTime(root);
|
||||||
|
root.achievementProxy = new AchievementProxy(root);
|
||||||
root.automaticSave = new AutomaticSave(root);
|
root.automaticSave = new AutomaticSave(root);
|
||||||
root.soundProxy = new SoundProxy(root);
|
root.soundProxy = new SoundProxy(root);
|
||||||
|
|
||||||
@ -149,6 +151,9 @@ export class GameCore {
|
|||||||
|
|
||||||
// Update analytics
|
// Update analytics
|
||||||
root.productionAnalytics.update();
|
root.productionAnalytics.update();
|
||||||
|
|
||||||
|
// Check achievements
|
||||||
|
root.achievementProxy.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -274,6 +279,9 @@ export class GameCore {
|
|||||||
|
|
||||||
// Update analytics
|
// Update analytics
|
||||||
root.productionAnalytics.update();
|
root.productionAnalytics.update();
|
||||||
|
|
||||||
|
// Check achievements
|
||||||
|
root.achievementProxy.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update automatic save after everything finished
|
// Update automatic save after everything finished
|
||||||
|
@ -8,6 +8,7 @@ import { globalConfig } from "../../../core/config";
|
|||||||
import { makeDiv, formatBigNumber, formatBigNumberFull } from "../../../core/utils";
|
import { makeDiv, formatBigNumber, formatBigNumberFull } from "../../../core/utils";
|
||||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||||
import { createLogger } from "../../../core/logging";
|
import { createLogger } from "../../../core/logging";
|
||||||
|
import { ACHIEVEMENTS } from "../../../platform/achievement_provider";
|
||||||
import { enumMouseButton } from "../../camera";
|
import { enumMouseButton } from "../../camera";
|
||||||
import { T } from "../../../translations";
|
import { T } from "../../../translations";
|
||||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||||
@ -100,6 +101,7 @@ export class HUDMassSelector extends BaseHUDPart {
|
|||||||
*/
|
*/
|
||||||
const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap();
|
const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap();
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
this.root.logic.performBulkOperation(() => {
|
this.root.logic.performBulkOperation(() => {
|
||||||
for (let i = 0; i < entityUids.length; ++i) {
|
for (let i = 0; i < entityUids.length; ++i) {
|
||||||
const uid = entityUids[i];
|
const uid = entityUids[i];
|
||||||
@ -111,8 +113,12 @@ export class HUDMassSelector extends BaseHUDPart {
|
|||||||
|
|
||||||
if (!this.root.logic.tryDeleteBuilding(entity)) {
|
if (!this.root.logic.tryDeleteBuilding(entity)) {
|
||||||
logger.error("Error in mass delete, could not remove building");
|
logger.error("Error in mass delete, could not remove building");
|
||||||
|
} else {
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.destroy1000, count);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear uids later
|
// Clear uids later
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
removeAllChildren,
|
removeAllChildren,
|
||||||
} from "../../../core/utils";
|
} from "../../../core/utils";
|
||||||
import { Vector } from "../../../core/vector";
|
import { Vector } from "../../../core/vector";
|
||||||
|
import { ACHIEVEMENTS } from "../../../platform/achievement_provider";
|
||||||
import { T } from "../../../translations";
|
import { T } from "../../../translations";
|
||||||
import { BaseItem } from "../../base_item";
|
import { BaseItem } from "../../base_item";
|
||||||
import { MetaHubBuilding } from "../../buildings/hub";
|
import { MetaHubBuilding } from "../../buildings/hub";
|
||||||
@ -349,6 +350,10 @@ export class HUDWaypoints extends BaseHUDPart {
|
|||||||
T.ingame.waypoints.creationSuccessNotification,
|
T.ingame.waypoints.creationSuccessNotification,
|
||||||
enumNotificationType.success
|
enumNotificationType.success
|
||||||
);
|
);
|
||||||
|
this.root.signals.achievementCheck.dispatch(
|
||||||
|
ACHIEVEMENTS.mapMarkers15,
|
||||||
|
this.waypoints.length - 1 // Disregard HUB
|
||||||
|
);
|
||||||
|
|
||||||
// Re-render the list and thus add it
|
// Re-render the list and thus add it
|
||||||
this.rerenderWaypointList();
|
this.rerenderWaypointList();
|
||||||
|
@ -8,6 +8,7 @@ import { createLogger } from "../core/logging";
|
|||||||
import { GameTime } from "./time/game_time";
|
import { GameTime } from "./time/game_time";
|
||||||
import { EntityManager } from "./entity_manager";
|
import { EntityManager } from "./entity_manager";
|
||||||
import { GameSystemManager } from "./game_system_manager";
|
import { GameSystemManager } from "./game_system_manager";
|
||||||
|
import { AchievementProxy } from "./achievement_proxy";
|
||||||
import { GameHUD } from "./hud/hud";
|
import { GameHUD } from "./hud/hud";
|
||||||
import { MapView } from "./map_view";
|
import { MapView } from "./map_view";
|
||||||
import { Camera } from "./camera";
|
import { Camera } from "./camera";
|
||||||
@ -119,6 +120,9 @@ export class GameRoot {
|
|||||||
/** @type {SoundProxy} */
|
/** @type {SoundProxy} */
|
||||||
this.soundProxy = null;
|
this.soundProxy = null;
|
||||||
|
|
||||||
|
/** @type {AchievementProxy} */
|
||||||
|
this.achievementProxy = null;
|
||||||
|
|
||||||
/** @type {ShapeDefinitionManager} */
|
/** @type {ShapeDefinitionManager} */
|
||||||
this.shapeDefinitionMgr = null;
|
this.shapeDefinitionMgr = null;
|
||||||
|
|
||||||
@ -175,6 +179,10 @@ export class GameRoot {
|
|||||||
// Called before actually placing an entity, use to perform additional logic
|
// Called before actually placing an entity, use to perform additional logic
|
||||||
// for freeing space before actually placing.
|
// for freeing space before actually placing.
|
||||||
freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
|
freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
|
||||||
|
|
||||||
|
// Called with an achievement key and necessary args to validate it can be unlocked.
|
||||||
|
achievementCheck: /** @type {TypedSignal<[string, *]>} */ (new Signal()),
|
||||||
|
bulkAchievementCheck: /** @type {TypedSignal<[string, any]...>} */ (new Signal()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// RNG's
|
// RNG's
|
||||||
|
@ -4,6 +4,7 @@ import { enumColors } from "./colors";
|
|||||||
import { ShapeItem } from "./items/shape_item";
|
import { ShapeItem } from "./items/shape_item";
|
||||||
import { GameRoot } from "./root";
|
import { GameRoot } from "./root";
|
||||||
import { enumSubShape, ShapeDefinition } from "./shape_definition";
|
import { enumSubShape, ShapeDefinition } from "./shape_definition";
|
||||||
|
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
||||||
|
|
||||||
const logger = createLogger("shape_definition_manager");
|
const logger = createLogger("shape_definition_manager");
|
||||||
|
|
||||||
@ -96,6 +97,8 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
|
|||||||
const rightSide = definition.cloneFilteredByQuadrants([2, 3]);
|
const rightSide = definition.cloneFilteredByQuadrants([2, 3]);
|
||||||
const leftSide = definition.cloneFilteredByQuadrants([0, 1]);
|
const leftSide = definition.cloneFilteredByQuadrants([0, 1]);
|
||||||
|
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.cutShape);
|
||||||
|
|
||||||
return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key] = [
|
return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key] = [
|
||||||
this.registerOrReturnHandle(rightSide),
|
this.registerOrReturnHandle(rightSide),
|
||||||
this.registerOrReturnHandle(leftSide),
|
this.registerOrReturnHandle(leftSide),
|
||||||
@ -137,6 +140,8 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
|
|||||||
|
|
||||||
const rotated = definition.cloneRotateCW();
|
const rotated = definition.cloneRotateCW();
|
||||||
|
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.rotateShape);
|
||||||
|
|
||||||
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
|
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
|
||||||
rotated
|
rotated
|
||||||
));
|
));
|
||||||
@ -189,6 +194,9 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
|
|||||||
if (this.operationCache[key]) {
|
if (this.operationCache[key]) {
|
||||||
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
|
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.stackShape);
|
||||||
|
|
||||||
const stacked = lowerDefinition.cloneAndStackWith(upperDefinition);
|
const stacked = lowerDefinition.cloneAndStackWith(upperDefinition);
|
||||||
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
|
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
|
||||||
stacked
|
stacked
|
||||||
@ -206,6 +214,9 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
|
|||||||
if (this.operationCache[key]) {
|
if (this.operationCache[key]) {
|
||||||
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
|
return /** @type {ShapeDefinition} */ (this.operationCache[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.paintShape);
|
||||||
|
|
||||||
const colorized = definition.cloneAndPaintWith(color);
|
const colorized = definition.cloneAndPaintWith(color);
|
||||||
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
|
return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle(
|
||||||
colorized
|
colorized
|
||||||
|
@ -4,6 +4,7 @@ import { createLogger } from "../../core/logging";
|
|||||||
import { Rectangle } from "../../core/rectangle";
|
import { Rectangle } from "../../core/rectangle";
|
||||||
import { StaleAreaDetector } from "../../core/stale_area_detector";
|
import { StaleAreaDetector } from "../../core/stale_area_detector";
|
||||||
import { enumDirection, enumDirectionToVector } from "../../core/vector";
|
import { enumDirection, enumDirectionToVector } from "../../core/vector";
|
||||||
|
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
|
||||||
import { BaseItem } from "../base_item";
|
import { BaseItem } from "../base_item";
|
||||||
import { BeltComponent } from "../components/belt";
|
import { BeltComponent } from "../components/belt";
|
||||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
enumInvertedDirections,
|
enumInvertedDirections,
|
||||||
Vector,
|
Vector,
|
||||||
} from "../../core/vector";
|
} from "../../core/vector";
|
||||||
|
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
|
||||||
import { BaseItem } from "../base_item";
|
import { BaseItem } from "../base_item";
|
||||||
import { arrayWireRotationVariantToType, MetaWireBuilding } from "../buildings/wire";
|
import { arrayWireRotationVariantToType, MetaWireBuilding } from "../buildings/wire";
|
||||||
import { getCodeFromBuildingData } from "../building_codes";
|
import { getCodeFromBuildingData } from "../building_codes";
|
||||||
@ -697,6 +698,8 @@ export class WireSystem extends GameSystemWithFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.place5000Wires, entity);
|
||||||
|
|
||||||
// Invalidate affected area
|
// Invalidate affected area
|
||||||
const originalRect = staticComp.getTileSpaceBounds();
|
const originalRect = staticComp.getTileSpaceBounds();
|
||||||
const affectedArea = originalRect.expandedInAllDirections(1);
|
const affectedArea = originalRect.expandedInAllDirections(1);
|
||||||
|
713
src/js/platform/achievement_provider.js
Normal file
713
src/js/platform/achievement_provider.js
Normal file
@ -0,0 +1,713 @@
|
|||||||
|
/* typehints:start */
|
||||||
|
import { Application } from "../application";
|
||||||
|
import { StorageComponent } from "../game/components/storage";
|
||||||
|
import { ShapeItem } from "../game/items/shape_item";
|
||||||
|
import { Entity } from "../game/entity";
|
||||||
|
import { GameRoot } from "../game/root";
|
||||||
|
import { ShapeDefinition } from "../game/shape_definition";
|
||||||
|
/* typehints:end */
|
||||||
|
|
||||||
|
export const ACHIEVEMENTS = {
|
||||||
|
belt500Tiles: "belt500Tiles",
|
||||||
|
blueprint100k: "blueprint100k",
|
||||||
|
blueprint1m: "blueprint1m",
|
||||||
|
completeLvl26: "completeLvl26",
|
||||||
|
cutShape: "cutShape",
|
||||||
|
darkMode: "darkMode",
|
||||||
|
destroy1000: "destroy1000",
|
||||||
|
irrelevantShape: "irrelevantShape",
|
||||||
|
level100: "level100",
|
||||||
|
level50: "level50",
|
||||||
|
logoBefore18: "logoBefore18",
|
||||||
|
mam: "mam",
|
||||||
|
mapMarkers15: "mapMarkers15",
|
||||||
|
noBeltUpgradesUntilBp: "noBeltUpgradesUntilBp",
|
||||||
|
noInverseRotater: "noInverseRotater",
|
||||||
|
oldLevel17: "oldLevel17",
|
||||||
|
openWires: "openWires",
|
||||||
|
paintShape: "paintShape",
|
||||||
|
place5000Wires: "place5000Wires",
|
||||||
|
placeBlueprint: "placeBlueprint",
|
||||||
|
placeBp1000: "placeBp1000",
|
||||||
|
play1h: "play1h",
|
||||||
|
play10h: "play10h",
|
||||||
|
play20h: "play20h",
|
||||||
|
produceLogo: "produceLogo",
|
||||||
|
produceMsLogo: "produceMsLogo",
|
||||||
|
produceRocket: "produceRocket",
|
||||||
|
rotateShape: "rotateShape",
|
||||||
|
speedrunBp30: "speedrunBp30",
|
||||||
|
speedrunBp60: "speedrunBp60",
|
||||||
|
speedrunBp120: "speedrunBp120",
|
||||||
|
stack4Layers: "stack4Layers",
|
||||||
|
stackShape: "stackShape",
|
||||||
|
store100Unique: "store100Unique",
|
||||||
|
storeShape: "storeShape",
|
||||||
|
throughputBp25: "throughputBp25",
|
||||||
|
throughputBp50: "throughputBp50",
|
||||||
|
throughputLogo25: "throughputLogo25",
|
||||||
|
throughputLogo50: "throughputLogo50",
|
||||||
|
throughputRocket10: "throughputRocket10",
|
||||||
|
throughputRocket20: "throughputRocket20",
|
||||||
|
trash1000: "trash1000",
|
||||||
|
unlockWires: "unlockWires",
|
||||||
|
upgradesTier5: "upgradesTier5",
|
||||||
|
upgradesTier8: "upgradesTier8",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DARK_MODE = "dark";
|
||||||
|
const HOUR_1 = 3600; // Seconds
|
||||||
|
const HOUR_10 = HOUR_1 * 10;
|
||||||
|
const HOUR_20 = HOUR_1 * 20;
|
||||||
|
const ITEM_SHAPE = "shape";
|
||||||
|
const MINUTE_30 = 1800; // Seconds
|
||||||
|
const MINUTE_60 = MINUTE_30 * 2;
|
||||||
|
const MINUTE_120 = MINUTE_30 * 4;
|
||||||
|
const PRODUCED = "produced";
|
||||||
|
const RATE_SLICE_COUNT = 10;
|
||||||
|
const ROTATER_CCW_CODE = 12;
|
||||||
|
const ROTATER_180_CODE = 13;
|
||||||
|
const SHAPE_BP = "CbCbCbRb:CwCwCwCw";
|
||||||
|
const SHAPE_LOGO = "RuCw--Cw:----Ru--";
|
||||||
|
const SHAPE_MS_LOGO = "RgRyRbRr";
|
||||||
|
const SHAPE_OLD_LEVEL_17 = "WrRgWrRg:CwCrCwCr:SgSgSgSg";
|
||||||
|
const SHAPE_ROCKET = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
||||||
|
const WIRE_LAYER = "wires";
|
||||||
|
|
||||||
|
export class AchievementProviderInterface {
|
||||||
|
/** @param {Application} app */
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the achievement provider.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
abstract;
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opportunity to do additional initialization work with the GameRoot.
|
||||||
|
* @param {GameRoot} root
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
onLoad(root) {
|
||||||
|
abstract;
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
hasLoaded() {
|
||||||
|
abstract;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call to activate an achievement with the provider
|
||||||
|
* @param {string} key - Maps to an Achievement
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
activate(key) {
|
||||||
|
abstract;
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if achievements are supported in the current build
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
hasAchievements() {
|
||||||
|
abstract;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Achievement {
|
||||||
|
/** @param {string} key - An ACHIEVEMENTS key */
|
||||||
|
constructor(key) {
|
||||||
|
this.key = key;
|
||||||
|
this.activate = null;
|
||||||
|
this.activatePromise = null;
|
||||||
|
this.receiver = null;
|
||||||
|
this.signal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRelevant() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock() {
|
||||||
|
if (!this.activatePromise) {
|
||||||
|
this.activatePromise = this.activate(this.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activatePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AchievementCollection {
|
||||||
|
/**
|
||||||
|
* @param {function} activate - Resolves when provider activation is complete
|
||||||
|
*/
|
||||||
|
constructor(activate) {
|
||||||
|
this.map = new Map();
|
||||||
|
this.activate = activate;
|
||||||
|
|
||||||
|
this.add(ACHIEVEMENTS.belt500Tiles, {
|
||||||
|
isValid: this.isBelt500TilesValid,
|
||||||
|
signal: "entityAdded",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.blueprint100k, {
|
||||||
|
isValid: this.isBlueprint100kValid,
|
||||||
|
signal: "shapeDelivered",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.blueprint1m, {
|
||||||
|
isValid: this.isBlueprint1mValid,
|
||||||
|
signal: "shapeDelivered",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.completeLvl26, this.createLevelOptions(26));
|
||||||
|
this.add(ACHIEVEMENTS.cutShape);
|
||||||
|
this.add(ACHIEVEMENTS.darkMode, {
|
||||||
|
isValid: this.isDarkModeValid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.destroy1000, {
|
||||||
|
isValid: this.isDestroy1000Valid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.irrelevantShape, {
|
||||||
|
isValid: this.isIrrelevantShapeValid,
|
||||||
|
signal: "shapeDelivered",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.level100, this.createLevelOptions(100));
|
||||||
|
this.add(ACHIEVEMENTS.level50, this.createLevelOptions(50));
|
||||||
|
this.add(ACHIEVEMENTS.logoBefore18, {
|
||||||
|
isRelevant: this.isLogoBefore18Relevant,
|
||||||
|
isValid: this.isLogoBefore18Valid,
|
||||||
|
signal: "itemProduced"
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.mam, {
|
||||||
|
isRelevant: this.isMamRelevant,
|
||||||
|
isValid: this.isMamValid,
|
||||||
|
signal: "storyGoalCompleted",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.mapMarkers15, {
|
||||||
|
isRelevant: this.isMapMarkers15Relevant,
|
||||||
|
isValid: this.isMapMarkers15Valid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.noBeltUpgradesUntilBp, {
|
||||||
|
init: this.initNoBeltUpgradesUntilBp,
|
||||||
|
isRelevant: this.isNoBeltUpgradesUntilBpRelevant,
|
||||||
|
isValid: this.isNoBeltUpgradesUntilBpValid,
|
||||||
|
signal: "storyGoalCompleted",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.noInverseRotater, {
|
||||||
|
init: this.initNoInverseRotater,
|
||||||
|
isRelevant: this.isNoInverseRotaterRelevant,
|
||||||
|
isValid: this.isNoInverseRotaterValid,
|
||||||
|
signal: "storyGoalCompleted",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.oldLevel17, this.createShapeOptions(SHAPE_OLD_LEVEL_17));
|
||||||
|
this.add(ACHIEVEMENTS.openWires, {
|
||||||
|
isValid: this.isOpenWiresValid,
|
||||||
|
signal: "editModeChanged",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.paintShape);
|
||||||
|
this.add(ACHIEVEMENTS.place5000Wires, {
|
||||||
|
isValid: this.isPlace5000WiresValid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.placeBlueprint, {
|
||||||
|
isValid: this.isPlaceBlueprintValid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.placeBp1000, {
|
||||||
|
isValid: this.isPlaceBp1000Valid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.play1h, this.createTimeOptions(HOUR_1));
|
||||||
|
this.add(ACHIEVEMENTS.play10h, this.createTimeOptions(HOUR_10));
|
||||||
|
this.add(ACHIEVEMENTS.play20h, this.createTimeOptions(HOUR_20));
|
||||||
|
this.add(ACHIEVEMENTS.produceLogo, this.createShapeOptions(SHAPE_LOGO));
|
||||||
|
this.add(ACHIEVEMENTS.produceRocket, this.createShapeOptions(SHAPE_ROCKET));
|
||||||
|
this.add(ACHIEVEMENTS.produceMsLogo, this.createShapeOptions(SHAPE_MS_LOGO));
|
||||||
|
this.add(ACHIEVEMENTS.rotateShape);
|
||||||
|
this.add(ACHIEVEMENTS.speedrunBp30, this.createSpeedOptions(12, MINUTE_30));
|
||||||
|
this.add(ACHIEVEMENTS.speedrunBp60, this.createSpeedOptions(12, MINUTE_60));
|
||||||
|
this.add(ACHIEVEMENTS.speedrunBp120, this.createSpeedOptions(12, MINUTE_120));
|
||||||
|
this.add(ACHIEVEMENTS.stack4Layers, {
|
||||||
|
isValid: this.isStack4LayersValid,
|
||||||
|
signal: "itemProduced",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.stackShape);
|
||||||
|
this.add(ACHIEVEMENTS.store100Unique, {
|
||||||
|
isRelevant: this.isStore100UniqueRelevant,
|
||||||
|
isValid: this.isStore100UniqueValid,
|
||||||
|
signal: "shapeDelivered",
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.storeShape, {
|
||||||
|
isValid: this.isStoreShapeValid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.throughputBp25, this.createRateOptions(SHAPE_BP, 25));
|
||||||
|
this.add(ACHIEVEMENTS.throughputBp50, this.createRateOptions(SHAPE_BP, 50));
|
||||||
|
this.add(ACHIEVEMENTS.throughputLogo25, this.createRateOptions(SHAPE_LOGO, 25));
|
||||||
|
this.add(ACHIEVEMENTS.throughputLogo50, this.createRateOptions(SHAPE_LOGO, 50));
|
||||||
|
this.add(ACHIEVEMENTS.throughputRocket10, this.createRateOptions(SHAPE_ROCKET, 25));
|
||||||
|
this.add(ACHIEVEMENTS.throughputRocket20, this.createRateOptions(SHAPE_ROCKET, 50));
|
||||||
|
this.add(ACHIEVEMENTS.trash1000, {
|
||||||
|
init: this.initTrash1000,
|
||||||
|
isValid: this.isTrash1000Valid,
|
||||||
|
});
|
||||||
|
this.add(ACHIEVEMENTS.unlockWires, this.createLevelOptions(20));
|
||||||
|
this.add(ACHIEVEMENTS.upgradesTier5, this.createUpgradeOptions(5));
|
||||||
|
this.add(ACHIEVEMENTS.upgradesTier8, this.createUpgradeOptions(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {GameRoot} root */
|
||||||
|
initialize(root) {
|
||||||
|
this.root = root;
|
||||||
|
this.root.signals.achievementCheck.add(this.unlock, this);
|
||||||
|
this.root.signals.bulkAchievementCheck.add(this.bulkUnlock, this);
|
||||||
|
|
||||||
|
for (let [key, achievement] of this.map.entries()) {
|
||||||
|
if (achievement.init) {
|
||||||
|
achievement.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!achievement.isRelevant()) {
|
||||||
|
this.remove(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (achievement.signal) {
|
||||||
|
achievement.receiver = this.unlock.bind(this, key);
|
||||||
|
this.root.signals[achievement.signal].add(achievement.receiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasDefaultReceivers()) {
|
||||||
|
this.root.signals.achievementCheck.remove(this.unlock);
|
||||||
|
this.root.signals.bulkAchievementCheck.remove(this.bulkUnlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key - Maps to an Achievement
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {function} [options.init]
|
||||||
|
* @param {function} [options.isRelevant]
|
||||||
|
* @param {function} [options.isValid]
|
||||||
|
* @param {string} [options.signal]
|
||||||
|
*/
|
||||||
|
add(key, options = {}) {
|
||||||
|
if (G_IS_DEV) {
|
||||||
|
assert(ACHIEVEMENTS[key], "Achievement key not found: ", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const achievement = new Achievement(key);
|
||||||
|
|
||||||
|
achievement.activate = this.activate;
|
||||||
|
|
||||||
|
if (options.init) {
|
||||||
|
achievement.init = options.init.bind(this, achievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.isValid) {
|
||||||
|
achievement.isValid = options.isValid.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.isRelevant) {
|
||||||
|
achievement.isRelevant = options.isRelevant.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.signal) {
|
||||||
|
achievement.signal = options.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map.set(key, achievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkUnlock() {
|
||||||
|
for (let i = 0; i < arguments.length; i += 2) {
|
||||||
|
this.unlock(arguments[i], arguments[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key - Maps to an Achievement
|
||||||
|
* @param {?*} data - Data received from signal dispatches for validation
|
||||||
|
*/
|
||||||
|
unlock(key, data) {
|
||||||
|
if (!this.map.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const achievement = this.map.get(key);
|
||||||
|
|
||||||
|
if (!achievement.isValid(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
achievement.unlock()
|
||||||
|
.then(() => {
|
||||||
|
this.onActivate(null, key);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.onActivate(err, key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up after achievement activation attempt with the provider. Could
|
||||||
|
* utilize err to retry some number of times if needed.
|
||||||
|
* @param {?Error} err - Error is null if activation was successful
|
||||||
|
* @param {string} key - Maps to an Achievement
|
||||||
|
*/
|
||||||
|
onActivate(err, key) {
|
||||||
|
this.remove(key);
|
||||||
|
|
||||||
|
if (!this.hasDefaultReceivers()) {
|
||||||
|
this.root.signals.achievementCheck.remove(this.unlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} key - Maps to an Achievement */
|
||||||
|
remove(key) {
|
||||||
|
const achievement = this.map.get(key);
|
||||||
|
|
||||||
|
if (achievement.receiver) {
|
||||||
|
this.root.signals[achievement.signal].remove(achievement.receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intended to be called on occasion to prune achievements that became
|
||||||
|
* irrelevant during a play session.
|
||||||
|
*/
|
||||||
|
clean() {
|
||||||
|
for (let [key, achievement] of this.map.entries()) {
|
||||||
|
if (!achievement.activatePromise && !achievement.isRelevant()) {
|
||||||
|
this.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the collection-level achievementCheck receivers are still
|
||||||
|
* necessary.
|
||||||
|
*/
|
||||||
|
hasDefaultReceivers() {
|
||||||
|
if (!this.map.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let achievement of this.map.values()) {
|
||||||
|
if (!achievement.signal) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remaining methods exist to extend Achievement instances within the
|
||||||
|
* collection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hasAllUpgradesAtTier(tier) {
|
||||||
|
const upgrades = this.root.gameMode.getUpgrades();
|
||||||
|
|
||||||
|
for (let upgradeId in upgrades) {
|
||||||
|
if (this.root.hubGoals.getUpgradeLevel(upgradeId) < tier - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ShapeItem} item
|
||||||
|
* @param {string} shape
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isShape(item, shape) {
|
||||||
|
return item.getItemType() === ITEM_SHAPE && item.definition.getHash() === shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
createLevelOptions(level) {
|
||||||
|
return {
|
||||||
|
isRelevant: () => this.root.hubGoals.level < level,
|
||||||
|
isValid: (currentLevel) => currentLevel === level,
|
||||||
|
signal: "storyGoalCompleted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createRateOptions(shape, rate) {
|
||||||
|
return {
|
||||||
|
isValid: () => {
|
||||||
|
return this.root.productionAnalytics.getCurrentShapeRate(
|
||||||
|
PRODUCED,
|
||||||
|
this.root.shapeDefinitionMgr.getShapeFromShortKey(shape)
|
||||||
|
) >= rate;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createShapeOptions(shape) {
|
||||||
|
return {
|
||||||
|
isValid: (item) => this.isShape(item, shape),
|
||||||
|
signal: "itemProduced",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createSpeedOptions(level, time) {
|
||||||
|
return {
|
||||||
|
isRelevant: () => this.root.hubGoals.level <= level && this.root.time.now() < time,
|
||||||
|
isValid: (currentLevel) => currentLevel === level && this.root.time.now() < time,
|
||||||
|
signal: "storyGoalCompleted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createTimeOptions(duration) {
|
||||||
|
return {
|
||||||
|
isRelevant: () => this.root.time.now() < duration,
|
||||||
|
isValid: () => this.root.time.now() >= duration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createUpgradeOptions(tier) {
|
||||||
|
return {
|
||||||
|
isRelevant: () => !this.hasAllUpgradesAtTier(tier),
|
||||||
|
isValid: () => this.hasAllUpgradesAtTier(tier),
|
||||||
|
signal: "upgradePurchased",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Entity} entity @returns {boolean} */
|
||||||
|
isBelt500TilesValid(entity) {
|
||||||
|
return entity.components.Belt && entity.components.Belt.assignedPath.totalLength >= 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ShapeDefinition} definition @returns {boolean} */
|
||||||
|
isBlueprint100kValid(definition) {
|
||||||
|
return (
|
||||||
|
definition.cachedHash === SHAPE_BP &&
|
||||||
|
this.root.hubGoals.storedShapes[SHAPE_BP] >= 100000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ShapeDefinition} definition @returns {boolean} */
|
||||||
|
isBlueprint1mValid(definition) {
|
||||||
|
return (
|
||||||
|
definition.cachedHash === SHAPE_BP &&
|
||||||
|
this.root.hubGoals.storedShapes[SHAPE_BP] >= 1000000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isDarkModeValid() {
|
||||||
|
return this.root.app.settings.currentData.settings.theme === DARK_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} count @returns {boolean} */
|
||||||
|
isDestroy1000Valid(count) {
|
||||||
|
return count >= 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ShapeDefinition} definition @returns {boolean} */
|
||||||
|
isIrrelevantShapeValid(definition) {
|
||||||
|
if (definition.cachedHash === this.root.hubGoals.currentGoal.definition.cachedHash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const upgrades = this.root.gameMode.getUpgrades();
|
||||||
|
for (let upgradeId in upgrades) {
|
||||||
|
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
|
||||||
|
const requiredShapes = upgrades[upgradeId][currentTier].required;
|
||||||
|
|
||||||
|
for (let i = 0; i < requiredShapes.length; i++) {
|
||||||
|
if (definition.cachedHash === requiredShapes[i].shape) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isLogoBefore18Relevant() {
|
||||||
|
return this.root.hubGoals.level < 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ShapeItem} item @returns {boolean} */
|
||||||
|
isLogoBefore18Valid(item) {
|
||||||
|
return this.root.hubGoals.level < 18 && this.isShape(item, SHAPE_LOGO);
|
||||||
|
}
|
||||||
|
|
||||||
|
initMam() {
|
||||||
|
const stats = this.root.savegame.currentData.stats;
|
||||||
|
|
||||||
|
if (stats.failedMam === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.root.hubGoals.level === 26 && stats.failedMam === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.failedMam = this.root.hubGoals.level < 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isMamRelevant() {
|
||||||
|
return this.root.hubGoals.level <= 26 && !this.root.savegame.currentData.stats.failedMam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @params {number} level @returns {boolean} */
|
||||||
|
isMamValid(level) {
|
||||||
|
return level === 27 && !this.root.savegame.currentData.stats.failedMam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isMapMarkers15Relevant() {
|
||||||
|
return this.root.hud.parts.waypoints.waypoints.length < 16; // 16 - HUB
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} count @returns {boolean} */
|
||||||
|
isMapMarkers15Valid(count) {
|
||||||
|
return count === 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
initNoBeltUpgradesUntilBp() {
|
||||||
|
const stats = this.root.savegame.currentData.stats;
|
||||||
|
|
||||||
|
if (stats.upgradedBelt === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.upgradedBelt = this.root.hubGoals.upgradeLevels.belt > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isNoBeltUpgradesUntilBpRelevant() {
|
||||||
|
return this.root.hubGoals.level <= 12 && this.root.hubGoals.upgradeLevels.belt === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @params {number} level @returns {boolean} */
|
||||||
|
isNoBeltUpgradesUntilBpValid(level) {
|
||||||
|
return level === 12 && this.root.hubGoals.upgradeLevels.belt === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
initNoInverseRotater() {
|
||||||
|
if (this.root.savegame.currentData.stats.usedInverseRotater === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities = this.root.entityMgr.componentToEntity.StaticMapEntity;
|
||||||
|
|
||||||
|
let usedInverseRotater = false;
|
||||||
|
for (var i = 0; i < entities.length; i++) {
|
||||||
|
const entity = entities[i].components.StaticMapEntity;
|
||||||
|
|
||||||
|
if (entity.code === ROTATER_CCW_CODE || entity.code === ROTATER_180_CODE) {
|
||||||
|
usedInverseRotater = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.savegame.currentData.stats.usedInverseRotater = usedInverseRotater;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isNoInverseRotaterRelevant() {
|
||||||
|
return this.root.hubGoals.level < 14 &&
|
||||||
|
!this.root.savegame.currentData.stats.usedInverseRotater;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} level @returns {boolean} */
|
||||||
|
isNoInverseRotaterValid(level) {
|
||||||
|
return level === 14 && !this.root.savegame.currentData.stats.usedInverseRotater;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} currentLayer @returns {boolean} */
|
||||||
|
isOpenWiresValid(currentLayer) {
|
||||||
|
return currentLayer === WIRE_LAYER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Entity} entity @returns {boolean} */
|
||||||
|
isPlace5000WiresValid(entity) {
|
||||||
|
return (
|
||||||
|
entity.components.Wire &&
|
||||||
|
entity.registered &&
|
||||||
|
entity.root.entityMgr.componentToEntity.Wire.length === 5000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} count @returns {boolean} */
|
||||||
|
isPlaceBlueprintValid(count) {
|
||||||
|
return count != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} count @returns {boolean} */
|
||||||
|
isPlaceBp1000Valid(count) {
|
||||||
|
return count >= 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ShapeItem} item @returns {boolean} */
|
||||||
|
isStack4LayersValid(item) {
|
||||||
|
return item.getItemType() === ITEM_SHAPE && item.definition.layers.length === 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isStore100UniqueRelevant() {
|
||||||
|
return Object.keys(this.root.hubGoals.storedShapes).length < 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isStore100UniqueValid() {
|
||||||
|
return Object.keys(this.root.hubGoals.storedShapes).length === 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
isStoreShapeValid() {
|
||||||
|
const entities = this.root.systemMgr.systems.storage.allEntities;
|
||||||
|
|
||||||
|
if (entities.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < entities.length; i++) {
|
||||||
|
if (entities[i].components.Storage.storedCount > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
initTrash1000() {
|
||||||
|
if (Number(this.root.savegame.currentData.stats.trashedCount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.savegame.currentData.stats.trashedCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @params {number} count @returns {boolean} */
|
||||||
|
isTrash1000Valid(count) {
|
||||||
|
this.root.savegame.currentData.stats.trashedCount += count;
|
||||||
|
|
||||||
|
return this.root.savegame.currentData.stats.trashedCount >= 1000;
|
||||||
|
}
|
||||||
|
}
|
23
src/js/platform/browser/no_achievement_provider.js
Normal file
23
src/js/platform/browser/no_achievement_provider.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { AchievementProviderInterface } from "../achievement_provider";
|
||||||
|
|
||||||
|
export class NoAchievementProvider extends AchievementProviderInterface {
|
||||||
|
hasAchievements() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasLoaded() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
return Promise.reject(new Error("No achievements to load"));
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,9 @@ import { queryParamOptions } from "../../core/query_parameters";
|
|||||||
import { clamp } from "../../core/utils";
|
import { clamp } from "../../core/utils";
|
||||||
import { GamedistributionAdProvider } from "../ad_providers/gamedistribution";
|
import { GamedistributionAdProvider } from "../ad_providers/gamedistribution";
|
||||||
import { NoAdProvider } from "../ad_providers/no_ad_provider";
|
import { NoAdProvider } from "../ad_providers/no_ad_provider";
|
||||||
|
import { SteamAchievementProvider } from "../electron/steam_achievement_provider";
|
||||||
import { PlatformWrapperInterface } from "../wrapper";
|
import { PlatformWrapperInterface } from "../wrapper";
|
||||||
|
import { NoAchievementProvider } from "./no_achievement_provider";
|
||||||
import { StorageImplBrowser } from "./storage";
|
import { StorageImplBrowser } from "./storage";
|
||||||
import { StorageImplBrowserIndexedDB } from "./storage_indexed_db";
|
import { StorageImplBrowserIndexedDB } from "./storage_indexed_db";
|
||||||
|
|
||||||
@ -71,6 +73,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
|||||||
|
|
||||||
return this.detectStorageImplementation()
|
return this.detectStorageImplementation()
|
||||||
.then(() => this.initializeAdProvider())
|
.then(() => this.initializeAdProvider())
|
||||||
|
.then(() => this.initializeAchievementProvider())
|
||||||
.then(() => super.initialize());
|
.then(() => super.initialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +199,21 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeAchievementProvider() {
|
||||||
|
if (G_IS_DEV && globalConfig.debug.testAchievements) {
|
||||||
|
this.app.achievementProvider = new SteamAchievementProvider(this.app);
|
||||||
|
|
||||||
|
return this.app.achievementProvider.initialize()
|
||||||
|
.catch(err => {
|
||||||
|
logger.error("Failed to initialize achievement provider, disabling:", err);
|
||||||
|
|
||||||
|
this.app.achievementProvider = new NoAchievementProvider(this.app);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.app.achievementProvider.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
exitApp() {
|
exitApp() {
|
||||||
// Can not exit app
|
// Can not exit app
|
||||||
}
|
}
|
||||||
|
148
src/js/platform/electron/steam_achievement_provider.js
Normal file
148
src/js/platform/electron/steam_achievement_provider.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/* typehints:start */
|
||||||
|
import { Application } from "../../application";
|
||||||
|
import { GameRoot } from "../../game/root";
|
||||||
|
/* typehints:end */
|
||||||
|
|
||||||
|
import { createLogger } from "../../core/logging";
|
||||||
|
import { getIPCRenderer } from "../../core/utils";
|
||||||
|
import {
|
||||||
|
ACHIEVEMENTS,
|
||||||
|
AchievementCollection,
|
||||||
|
AchievementProviderInterface
|
||||||
|
} from "../achievement_provider";
|
||||||
|
|
||||||
|
const logger = createLogger("achievements/steam");
|
||||||
|
|
||||||
|
const ACHIEVEMENT_IDS = {
|
||||||
|
[ACHIEVEMENTS.belt500Tiles]: "belt_500_tiles",
|
||||||
|
[ACHIEVEMENTS.blueprint100k]: "blueprint_100k",
|
||||||
|
[ACHIEVEMENTS.blueprint1m]: "blueprint_1m",
|
||||||
|
[ACHIEVEMENTS.completeLvl26]: "complete_lvl_26",
|
||||||
|
[ACHIEVEMENTS.cutShape]: "cut_shape",
|
||||||
|
[ACHIEVEMENTS.darkMode]: "dark_mode",
|
||||||
|
[ACHIEVEMENTS.destroy1000]: "destroy_1000",
|
||||||
|
[ACHIEVEMENTS.irrelevantShape]: "irrelevant_shape",
|
||||||
|
[ACHIEVEMENTS.level100]: "level_100",
|
||||||
|
[ACHIEVEMENTS.level50]: "level_50",
|
||||||
|
[ACHIEVEMENTS.logoBefore18]: "logo_before_18",
|
||||||
|
[ACHIEVEMENTS.mam]: "mam",
|
||||||
|
[ACHIEVEMENTS.mapMarkers15]: "map_markers_15",
|
||||||
|
[ACHIEVEMENTS.openWires]: "open_wires",
|
||||||
|
[ACHIEVEMENTS.oldLevel17]: "old_level_17",
|
||||||
|
[ACHIEVEMENTS.noBeltUpgradesUntilBp]: "no_belt_upgrades_until_bp",
|
||||||
|
[ACHIEVEMENTS.noInverseRotater]: "no_inverse_rotator", // [sic]
|
||||||
|
[ACHIEVEMENTS.paintShape]: "paint_shape",
|
||||||
|
[ACHIEVEMENTS.place5000Wires]: "place_5000_wires",
|
||||||
|
[ACHIEVEMENTS.placeBlueprint]: "place_blueprint",
|
||||||
|
[ACHIEVEMENTS.placeBp1000]: "place_bp_1000",
|
||||||
|
[ACHIEVEMENTS.play1h]: "play_1h",
|
||||||
|
[ACHIEVEMENTS.play10h]: "play_10h",
|
||||||
|
[ACHIEVEMENTS.play20h]: "play_20h",
|
||||||
|
[ACHIEVEMENTS.produceLogo]: "produce_logo",
|
||||||
|
[ACHIEVEMENTS.produceMsLogo]: "produce_ms_logo",
|
||||||
|
[ACHIEVEMENTS.produceRocket]: "produce_rocket",
|
||||||
|
[ACHIEVEMENTS.rotateShape]: "rotate_shape",
|
||||||
|
[ACHIEVEMENTS.speedrunBp30]: "speedrun_bp_30",
|
||||||
|
[ACHIEVEMENTS.speedrunBp60]: "speedrun_bp_60",
|
||||||
|
[ACHIEVEMENTS.speedrunBp120]: "speedrun_bp_120",
|
||||||
|
[ACHIEVEMENTS.stack4Layers]: "stack_4_layers",
|
||||||
|
[ACHIEVEMENTS.stackShape]: "stack_shape",
|
||||||
|
[ACHIEVEMENTS.store100Unique]: "store_100_unique",
|
||||||
|
[ACHIEVEMENTS.storeShape]: "store_shape",
|
||||||
|
[ACHIEVEMENTS.throughputBp25]: "throughput_bp_25",
|
||||||
|
[ACHIEVEMENTS.throughputBp50]: "throughput_bp_50",
|
||||||
|
[ACHIEVEMENTS.throughputLogo25]: "throughput_logo_25",
|
||||||
|
[ACHIEVEMENTS.throughputLogo50]: "throughput_logo_50",
|
||||||
|
[ACHIEVEMENTS.throughputRocket10]: "throughput_rocket_10",
|
||||||
|
[ACHIEVEMENTS.throughputRocket20]: "throughput_rocket_20",
|
||||||
|
[ACHIEVEMENTS.trash1000]: "trash_1000",
|
||||||
|
[ACHIEVEMENTS.unlockWires]: "unlock_wires",
|
||||||
|
[ACHIEVEMENTS.upgradesTier5]: "upgrades_tier_5",
|
||||||
|
[ACHIEVEMENTS.upgradesTier8]: "upgrades_tier_8",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SteamAchievementProvider extends AchievementProviderInterface {
|
||||||
|
/** @param {Application} app */
|
||||||
|
constructor(app) {
|
||||||
|
super(app);
|
||||||
|
|
||||||
|
this.initialized = false;
|
||||||
|
this.collection = new AchievementCollection(this.activate.bind(this));
|
||||||
|
|
||||||
|
if (G_IS_DEV) {
|
||||||
|
for (let key in ACHIEVEMENT_IDS) {
|
||||||
|
assert(this.collection.map.has(key), "Key not found in collection: " + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("Collection created with", this.collection.map.size, "achievements");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {boolean} */
|
||||||
|
hasAchievements() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GameRoot} root
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
onLoad(root) {
|
||||||
|
this.root = root;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.collection = new AchievementCollection(this.activate.bind(this));
|
||||||
|
this.collection.initialize(root);
|
||||||
|
|
||||||
|
logger.log("Initialized", this.collection.map.size, "relevant achievements");
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to initialize the collection");
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<void>} */
|
||||||
|
initialize() {
|
||||||
|
if (!G_IS_STANDALONE) {
|
||||||
|
logger.warn("Steam unavailable. Achievements won't sync.");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ipc = getIPCRenderer();
|
||||||
|
|
||||||
|
return this.ipc.invoke("steam:is-initialized")
|
||||||
|
.then(initialized => {
|
||||||
|
this.initialized = initialized;
|
||||||
|
|
||||||
|
if (!this.initialized) {
|
||||||
|
logger.warn("Steam failed to intialize. Achievements won't sync.");
|
||||||
|
} else {
|
||||||
|
logger.log("Steam achievement provider initialized");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
activate(key) {
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
if (!this.initialized) {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
} else {
|
||||||
|
promise = this.ipc.invoke("steam:activate-achievement", ACHIEVEMENT_IDS[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
.then(() => {
|
||||||
|
logger.log("Achievement activated:", key);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.error("Failed to activate achievement:", key, err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,34 @@
|
|||||||
|
import { NoAchievementProvider } from "../browser/no_achievement_provider";
|
||||||
import { PlatformWrapperImplBrowser } from "../browser/wrapper";
|
import { PlatformWrapperImplBrowser } from "../browser/wrapper";
|
||||||
import { getIPCRenderer } from "../../core/utils";
|
import { getIPCRenderer } from "../../core/utils";
|
||||||
import { createLogger } from "../../core/logging";
|
import { createLogger } from "../../core/logging";
|
||||||
import { StorageImplElectron } from "./storage";
|
import { StorageImplElectron } from "./storage";
|
||||||
|
import { SteamAchievementProvider } from "./steam_achievement_provider";
|
||||||
import { PlatformWrapperInterface } from "../wrapper";
|
import { PlatformWrapperInterface } from "../wrapper";
|
||||||
|
|
||||||
const logger = createLogger("electron-wrapper");
|
const logger = createLogger("electron-wrapper");
|
||||||
|
|
||||||
export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
|
export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
|
||||||
initialize() {
|
initialize() {
|
||||||
|
this.steamOverlayCanvasFix = document.createElement("canvas");
|
||||||
|
this.steamOverlayCanvasFix.width = 1;
|
||||||
|
this.steamOverlayCanvasFix.height = 1;
|
||||||
|
this.steamOverlayCanvasFix.id = "steamOverlayCanvasFix";
|
||||||
|
|
||||||
|
this.steamOverlayContextFix = this.steamOverlayCanvasFix.getContext("2d");
|
||||||
|
document.documentElement.appendChild(this.steamOverlayCanvasFix);
|
||||||
|
|
||||||
|
this.app.ticker.frameEmitted.add(this.steamOverlayFixRedrawCanvas, this);
|
||||||
|
|
||||||
this.app.storage = new StorageImplElectron(this);
|
this.app.storage = new StorageImplElectron(this);
|
||||||
return PlatformWrapperInterface.prototype.initialize.call(this);
|
this.app.achievementProvider = new SteamAchievementProvider(this.app);
|
||||||
|
|
||||||
|
return this.initializeAchievementProvider()
|
||||||
|
.then(() => PlatformWrapperInterface.prototype.initialize.call(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
steamOverlayFixRedrawCanvas() {
|
||||||
|
this.steamOverlayContextFix.clearRect(0, 0, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() {
|
getId() {
|
||||||
@ -38,6 +57,15 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeAchievementProvider() {
|
||||||
|
return this.app.achievementProvider.initialize()
|
||||||
|
.catch(err => {
|
||||||
|
logger.error("Failed to initialize achievement provider, disabling:", err);
|
||||||
|
|
||||||
|
this.app.achievementProvider = new NoAchievementProvider(this.app);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getSupportsFullscreen() {
|
getSupportsFullscreen() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import { SavegameInterface_V1004 } from "./schemas/1004";
|
|||||||
import { SavegameInterface_V1005 } from "./schemas/1005";
|
import { SavegameInterface_V1005 } from "./schemas/1005";
|
||||||
import { SavegameInterface_V1006 } from "./schemas/1006";
|
import { SavegameInterface_V1006 } from "./schemas/1006";
|
||||||
import { SavegameInterface_V1007 } from "./schemas/1007";
|
import { SavegameInterface_V1007 } from "./schemas/1007";
|
||||||
|
import { SavegameInterface_V1008 } from "./schemas/1008";
|
||||||
|
|
||||||
const logger = createLogger("savegame");
|
const logger = createLogger("savegame");
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ export class Savegame extends ReadWriteProxy {
|
|||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
static getCurrentVersion() {
|
static getCurrentVersion() {
|
||||||
return 1007;
|
return 1008;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,6 +127,11 @@ export class Savegame extends ReadWriteProxy {
|
|||||||
data.version = 1007;
|
data.version = 1007;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.version === 1007) {
|
||||||
|
SavegameInterface_V1008.migrate1007to1008(data);
|
||||||
|
data.version = 1008;
|
||||||
|
}
|
||||||
|
|
||||||
return ExplainedResult.good();
|
return ExplainedResult.good();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { SavegameInterface_V1004 } from "./schemas/1004";
|
|||||||
import { SavegameInterface_V1005 } from "./schemas/1005";
|
import { SavegameInterface_V1005 } from "./schemas/1005";
|
||||||
import { SavegameInterface_V1006 } from "./schemas/1006";
|
import { SavegameInterface_V1006 } from "./schemas/1006";
|
||||||
import { SavegameInterface_V1007 } from "./schemas/1007";
|
import { SavegameInterface_V1007 } from "./schemas/1007";
|
||||||
|
import { SavegameInterface_V1008 } from "./schemas/1008";
|
||||||
|
|
||||||
/** @type {Object.<number, typeof BaseSavegameInterface>} */
|
/** @type {Object.<number, typeof BaseSavegameInterface>} */
|
||||||
export const savegameInterfaces = {
|
export const savegameInterfaces = {
|
||||||
@ -19,6 +20,7 @@ export const savegameInterfaces = {
|
|||||||
1005: SavegameInterface_V1005,
|
1005: SavegameInterface_V1005,
|
||||||
1006: SavegameInterface_V1006,
|
1006: SavegameInterface_V1006,
|
||||||
1007: SavegameInterface_V1007,
|
1007: SavegameInterface_V1007,
|
||||||
|
1008: SavegameInterface_V1008,
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = createLogger("savegame_interface_registry");
|
const logger = createLogger("savegame_interface_registry");
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* @typedef {import("../game/entity").Entity} Entity
|
* @typedef {import("../game/entity").Entity} Entity
|
||||||
*
|
*
|
||||||
* @typedef {{}} SavegameStats
|
* @typedef {{
|
||||||
|
* failedMam: boolean,
|
||||||
|
* trashedCount: number,
|
||||||
|
* usedInverseRotater: boolean
|
||||||
|
* }} SavegameStats
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* camera: any,
|
* camera: any,
|
||||||
|
32
src/js/savegame/schemas/1008.js
Normal file
32
src/js/savegame/schemas/1008.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { createLogger } from "../../core/logging.js";
|
||||||
|
import { SavegameInterface_V1007 } from "./1007.js";
|
||||||
|
|
||||||
|
const schema = require("./1008.json");
|
||||||
|
const logger = createLogger("savegame_interface/1008");
|
||||||
|
|
||||||
|
export class SavegameInterface_V1008 extends SavegameInterface_V1007 {
|
||||||
|
getVersion() {
|
||||||
|
return 1008;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSchemaUncached() {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../savegame_typedefs.js").SavegameData} data
|
||||||
|
*/
|
||||||
|
static migrate1007to1008(data) {
|
||||||
|
logger.log("Migrating 1007 to 1008");
|
||||||
|
const dump = data.dump;
|
||||||
|
if (!dump) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(data.stats, {
|
||||||
|
failedMam: false,
|
||||||
|
trashedCount: 0,
|
||||||
|
usedInverseRotater: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
5
src/js/savegame/schemas/1008.json
Normal file
5
src/js/savegame/schemas/1008.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [],
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
@ -1207,6 +1207,11 @@ demo:
|
|||||||
|
|
||||||
settingNotAvailable: Not available in the demo.
|
settingNotAvailable: Not available in the demo.
|
||||||
|
|
||||||
|
achievements:
|
||||||
|
painting:
|
||||||
|
displayName: Painting
|
||||||
|
description: Paint a shape
|
||||||
|
|
||||||
tips:
|
tips:
|
||||||
- The hub will accept any input, not just the current shape!
|
- The hub will accept any input, not just the current shape!
|
||||||
- Make sure your factories are modular - it will pay out!
|
- Make sure your factories are modular - it will pay out!
|
||||||
|
Loading…
Reference in New Issue
Block a user