From 311a62052721fbfe5e153752c578130f7562df88 Mon Sep 17 00:00:00 2001 From: Greg Considine Date: Thu, 4 Mar 2021 15:24:34 -0500 Subject: [PATCH] Add play time achievements --- src/js/core/config.js | 3 ++ src/js/game/achievement_proxy.js | 29 +++++++++++++++++++ src/js/game/core.js | 6 ++++ src/js/platform/achievement_provider.js | 25 ++++++++++++++-- .../browser/no_achievement_provider.js | 4 +++ .../electron/steam_achievement_provider.js | 24 +++++++++++---- 6 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/js/core/config.js b/src/js/core/config.js index 4169a0c2..c9cb75bf 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -40,6 +40,9 @@ export const globalConfig = { assetsSharpness: 1.5, shapesSharpness: 1.4, + // Achievements + achievementSliceDuration: 30, // Seconds + // Production analytics statisticsGraphDpi: 2.5, statisticsGraphSlices: 100, diff --git a/src/js/game/achievement_proxy.js b/src/js/game/achievement_proxy.js index 4216dced..5d918ee6 100644 --- a/src/js/game/achievement_proxy.js +++ b/src/js/game/achievement_proxy.js @@ -2,6 +2,7 @@ import { GameRoot } from "./root"; /* typehints:end */ +import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; import { ACHIEVEMENTS } from "../platform/achievement_provider"; @@ -12,6 +13,8 @@ export class AchievementProxy { constructor(root) { this.root = root; this.provider = this.root.app.achievementProvider; + this.lastSlice = 0; + this.disabled = true; if (!this.provider.hasAchievements()) { return; @@ -21,13 +24,39 @@ export class AchievementProxy { } onLoad() { + if (this.provider.hasLoaded()) { + this.disabled = false; + return; + } + this.provider.onLoad(this.root) .then(() => { logger.log("Listening for unlocked achievements"); this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.darkMode); + this.startSlice(); + this.disabled = false; }) .catch(err => { + this.disabled = true; logger.error("Ignoring achievement signals", err); }) } + + startSlice() { + this.lastSlice = this.root.time.now(); + + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.play1h, this.lastSlice); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.play10h, this.lastSlice); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.play20h, this.lastSlice); + } + + update() { + if (this.disabled) { + return; + } + + if (this.root.time.now() - this.lastSlice > globalConfig.achievementSliceDuration) { + this.startSlice(); + } + } } diff --git a/src/js/game/core.js b/src/js/game/core.js index 4de529bc..f4b3e9ee 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -151,6 +151,9 @@ export class GameCore { // Update analytics root.productionAnalytics.update(); + + // Check achievements + root.achievementProxy.update(); } }); } @@ -276,6 +279,9 @@ export class GameCore { // Update analytics root.productionAnalytics.update(); + + // Check achievements + root.achievementProxy.update(); } // Update automatic save after everything finished diff --git a/src/js/platform/achievement_provider.js b/src/js/platform/achievement_provider.js index 01aec976..17880936 100644 --- a/src/js/platform/achievement_provider.js +++ b/src/js/platform/achievement_provider.js @@ -18,6 +18,9 @@ export const ACHIEVEMENTS = { paintShape: "paintShape", place5000Wires: "place5000Wires", placeBlueprint: "placeBlueprint", + play1h: "play1h", + play10h: "play10h", + play20h: "play20h", produceLogo: "produceLogo", produceMsLogo: "produceMsLogo", produceRocket: "produceRocket", @@ -30,6 +33,9 @@ export const ACHIEVEMENTS = { }; const DARK_MODE = "dark"; +const HOUR_1 = 3600; // Seconds +const HOUR_10 = HOUR_1 * 10; +const HOUR_20 = HOUR_1 * 20; const SHAPE_BLUEPRINT = "CbCbCbRb:CwCwCwCw"; const SHAPE_LOGO = "RuCw--Cw:----Ru--"; const SHAPE_MS_LOGO = "RgRyRbRr"; @@ -60,6 +66,12 @@ export class AchievementProviderInterface { return Promise.reject(); } + /** @returns {boolean} */ + hasLoaded() { + abstract; + return false; + } + /** * Call to activate an achievement with the provider * @param {string} key - Maps to an Achievement @@ -114,7 +126,6 @@ export class AchievementCollection { constructor(activate) { this.map = new Map(); this.activate = activate; - this.initialized = false; this.createAndSet(ACHIEVEMENTS.belt500Tiles, { isValid: this.isBelt500TilesValid, @@ -142,6 +153,9 @@ export class AchievementCollection { this.createAndSet(ACHIEVEMENTS.placeBlueprint, { isValid: this.isPlaceBlueprintValid, }); + this.createAndSet(ACHIEVEMENTS.play1h, this.createTimeOptions(HOUR_1)); + this.createAndSet(ACHIEVEMENTS.play10h, this.createTimeOptions(HOUR_10)); + this.createAndSet(ACHIEVEMENTS.play20h, this.createTimeOptions(HOUR_20)); this.createAndSet(ACHIEVEMENTS.produceLogo, this.createShapeOptions(SHAPE_LOGO)); this.createAndSet(ACHIEVEMENTS.produceRocket, this.createShapeOptions(SHAPE_ROCKET)); this.createAndSet(ACHIEVEMENTS.produceMsLogo, this.createShapeOptions(SHAPE_MS_LOGO)); @@ -182,8 +196,6 @@ export class AchievementCollection { if (!this.hasDefaultReceivers()) { this.root.signals.achievementUnlocked.remove(this.unlock); } - - this.initialized = true; } /** @@ -290,6 +302,13 @@ export class AchievementCollection { } } + createTimeOptions (duration) { + return { + isRelevant: () => this.root.time.now() < duration, + isValid: () => this.root.time.now() >= duration, + } + } + /** * @param {string} key * @param {Entity} entity diff --git a/src/js/platform/browser/no_achievement_provider.js b/src/js/platform/browser/no_achievement_provider.js index 13e573e4..91972bf7 100644 --- a/src/js/platform/browser/no_achievement_provider.js +++ b/src/js/platform/browser/no_achievement_provider.js @@ -5,6 +5,10 @@ export class NoAchievementProvider extends AchievementProviderInterface { return false; } + hasLoaded() { + return false; + } + initialize() { return Promise.resolve(); } diff --git a/src/js/platform/electron/steam_achievement_provider.js b/src/js/platform/electron/steam_achievement_provider.js index 0a7d41f8..428ebc2a 100644 --- a/src/js/platform/electron/steam_achievement_provider.js +++ b/src/js/platform/electron/steam_achievement_provider.js @@ -21,6 +21,9 @@ const ACHIEVEMENT_IDS = { [ACHIEVEMENTS.paintShape]: "paint_shape", [ACHIEVEMENTS.place5000Wires]: "place_5000_wires", [ACHIEVEMENTS.placeBlueprint]: "place_blueprint", + [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", @@ -38,26 +41,34 @@ export class SteamAchievementProvider extends AchievementProviderInterface { super(app); this.initialized = false; + this.loaded = false; this.collection = new AchievementCollection(this.activate.bind(this)); logger.log("Collection created with", this.collection.map.size, "achievements"); } - /** - * @returns {boolean} - */ + /** @returns {boolean} */ hasAchievements() { return true; } - /** @param {GameRoot} root */ + /** @returns {boolean} */ + hasLoaded() { + return this.loaded; + } + + /** + * @param {GameRoot} root + * @returns {Promise} + */ onLoad(root) { - if (this.collection.initialized) { + if (this.loaded) { return Promise.resolve(); } try { this.collection.initialize(root); + this.loaded = true; logger.log(this.collection.map.size, "achievements are relevant and initialized"); return Promise.resolve(); } catch (err) { @@ -66,6 +77,7 @@ export class SteamAchievementProvider extends AchievementProviderInterface { } } + /** @returns {Promise} */ initialize() { if (!G_IS_STANDALONE) { logger.warn("Steam unavailable. Achievements won't sync."); @@ -87,7 +99,7 @@ export class SteamAchievementProvider extends AchievementProviderInterface { } /** - * @param {string} key - An ACHIEVEMENTS key + * @param {string} key * @returns {Promise} */ activate(key) {