diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index aeb22169..66aecd40 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -164,7 +164,7 @@ export class Blueprint { anyPlaced = true; } - root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.blueprints, anyPlaced); + root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.placeBlueprint, anyPlaced); return anyPlaced; }); diff --git a/src/js/game/shape_definition_manager.js b/src/js/game/shape_definition_manager.js index b704deae..ba4cf4a8 100644 --- a/src/js/game/shape_definition_manager.js +++ b/src/js/game/shape_definition_manager.js @@ -97,7 +97,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject { const rightSide = definition.cloneFilteredByQuadrants([2, 3]); const leftSide = definition.cloneFilteredByQuadrants([0, 1]); - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.cutting); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.cutShape); return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key] = [ this.registerOrReturnHandle(rightSide), @@ -140,7 +140,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject { const rotated = definition.cloneRotateCW(); - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.rotating); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.rotateShape); return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( rotated @@ -195,7 +195,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject { return /** @type {ShapeDefinition} */ (this.operationCache[key]); } - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.stacking); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.stackShape); const stacked = lowerDefinition.cloneAndStackWith(upperDefinition); return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( @@ -215,7 +215,7 @@ export class ShapeDefinitionManager extends BasicSerializableObject { return /** @type {ShapeDefinition} */ (this.operationCache[key]); } - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.painting); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.paintShape); const colorized = definition.cloneAndPaintWith(color); return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( @@ -252,9 +252,10 @@ export class ShapeDefinitionManager extends BasicSerializableObject { } this.shapeKeyToDefinition[id] = definition; - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.theLogo, definition); - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.toTheMoon, definition); - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.fourLayers, definition); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.produceLogo, definition); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.produceMsLogo, definition); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.produceRocket, definition); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.stack4Layers, definition); // logger.log("Registered shape with key (2)", id); return definition; diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 1033a0fa..82d10974 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -276,7 +276,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { if (storageComp.canAcceptItem(item)) { storageComp.takeItem(item); - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.storage, storageComp); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.storeShape, storageComp); return true; } diff --git a/src/js/game/systems/wire.js b/src/js/game/systems/wire.js index a36fe30b..d18c9380 100644 --- a/src/js/game/systems/wire.js +++ b/src/js/game/systems/wire.js @@ -698,7 +698,7 @@ export class WireSystem extends GameSystemWithFilter { return; } - this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.networked, entity); + this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.place5000Wires, entity); // Invalidate affected area const originalRect = staticComp.getTileSpaceBounds(); diff --git a/src/js/platform/achievement_provider.js b/src/js/platform/achievement_provider.js index 86b66f1e..01aec976 100644 --- a/src/js/platform/achievement_provider.js +++ b/src/js/platform/achievement_provider.js @@ -7,34 +7,33 @@ import { ShapeDefinition } from "../game/shape_definition"; /* typehints:end */ export const ACHIEVEMENTS = { - blueprints: "blueprints", - cutting: "cutting", + belt500Tiles: "belt500Tiles", + blueprint100k: "blueprint100k", + blueprint1m: "blueprint1m", + completeLvl26: "completeLvl26", + cutShape: "cutShape", darkMode: "darkMode", - fourLayers: "fourLayers", - freedom: "freedom", - hundredShapes: "hundredShapes", - longBelt: "longBelt", - millionBlueprintShapes: "millionBlueprintShapes", - networked: "networked", - painting: "painting", - rotating: "rotating", - stacking: "stacking", - storage: "storage", - theLogo: "theLogo", - toTheMoon: "toTheMoon", - wires: "wires", + level100: "level100", + level50: "level50", + paintShape: "paintShape", + place5000Wires: "place5000Wires", + placeBlueprint: "placeBlueprint", + produceLogo: "produceLogo", + produceMsLogo: "produceMsLogo", + produceRocket: "produceRocket", + rotateShape: "rotateShape", + stack4Layers: "stack4Layers", + stackShape: "stackShape", + store100Unique: "store100Unique", + storeShape: "storeShape", + unlockWires: "unlockWires", }; -const BLUEPRINT_SHAPE = "CbCbCbRb:CwCwCwCw"; const DARK_MODE = "dark"; -const FREEDOM_LEVEL = 26; -const LOGO_SHAPE = "RuCw--Cw:----Ru--"; -const LONG_BELT_COUNT = 200; -const NETWORKED_WIRE_COUNT = 100; -const ONE_HUNDRED = 100; -const ONE_MILLION = 1000000; -const ROCKET_SHAPE = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; -const WIRES_LEVEL = 20; +const SHAPE_BLUEPRINT = "CbCbCbRb:CwCwCwCw"; +const SHAPE_LOGO = "RuCw--Cw:----Ru--"; +const SHAPE_MS_LOGO = "RgRyRbRr"; +const SHAPE_ROCKET = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; export class AchievementProviderInterface { /** @param {Application} app */ @@ -117,55 +116,50 @@ export class AchievementCollection { this.activate = activate; this.initialized = false; - this.createAndSet(ACHIEVEMENTS.blueprints, { - isValid: this.isBlueprintsValid + this.createAndSet(ACHIEVEMENTS.belt500Tiles, { + isValid: this.isBelt500TilesValid, + signal: "entityAdded", }); - this.createAndSet(ACHIEVEMENTS.cutting); + this.createAndSet(ACHIEVEMENTS.blueprint100k, { + isValid: this.isBlueprint100kValid, + signal: "shapeDelivered", + }); + this.createAndSet(ACHIEVEMENTS.blueprint1m, { + isValid: this.isBlueprint1mValid, + signal: "shapeDelivered", + }); + this.createAndSet(ACHIEVEMENTS.completeLvl26, this.createLevelOptions(26)); + this.createAndSet(ACHIEVEMENTS.cutShape); this.createAndSet(ACHIEVEMENTS.darkMode, { - isValid: this.isDarkModeValid + isValid: this.isDarkModeValid, }); - this.createAndSet(ACHIEVEMENTS.fourLayers, { - isValid: this.isFourLayersValid + this.createAndSet(ACHIEVEMENTS.level100, this.createLevelOptions(100)); + this.createAndSet(ACHIEVEMENTS.level50, this.createLevelOptions(50)); + this.createAndSet(ACHIEVEMENTS.paintShape); + this.createAndSet(ACHIEVEMENTS.place5000Wires, { + isValid: this.isPlace5000WiresValid, }); - this.createAndSet(ACHIEVEMENTS.freedom, { - isRelevant: this.isFreedomRelevant, - isValid: this.isFreedomValid, - signal: "storyGoalCompleted" + this.createAndSet(ACHIEVEMENTS.placeBlueprint, { + isValid: this.isPlaceBlueprintValid, }); - this.createAndSet(ACHIEVEMENTS.hundredShapes, { - isRelevant: this.isHundredShapesRelevant, - isValid: this.isHundredShapesValid, - signal: "shapeDelivered" + 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)); + this.createAndSet(ACHIEVEMENTS.rotateShape); + this.createAndSet(ACHIEVEMENTS.stack4Layers, { + isValid: this.isStack4LayersValid, }); - this.createAndSet(ACHIEVEMENTS.longBelt, { - isValid: this.isLongBeltValid, - signal: "entityAdded" + this.createAndSet(ACHIEVEMENTS.stackShape); + this.createAndSet(ACHIEVEMENTS.store100Unique, { + isRelevant: this.isStore100UniqueRelevant, + isValid: this.isStore100UniqueValid, + signal: "shapeDelivered", }); - this.createAndSet(ACHIEVEMENTS.millionBlueprintShapes, { - isValid: this.isMillionBlueprintShapesValid, - signal: "shapeDelivered" - }); - this.createAndSet(ACHIEVEMENTS.networked, { - isValid: this.isNetworkedValid, - }); - this.createAndSet(ACHIEVEMENTS.painting); - this.createAndSet(ACHIEVEMENTS.rotating); - this.createAndSet(ACHIEVEMENTS.stacking); - this.createAndSet(ACHIEVEMENTS.storage, { - isValid: this.isStorageValid, - signal: "entityGotNewComponent" - }); - this.createAndSet(ACHIEVEMENTS.theLogo, { - isValid: this.isTheLogoValid - }); - this.createAndSet(ACHIEVEMENTS.toTheMoon, { - isValid: this.isToTheMoonValid - }); - this.createAndSet(ACHIEVEMENTS.wires, { - isRelevant: this.isWiresRelevant, - isValid: this.isWiresValid, - signal: "storyGoalCompleted" + this.createAndSet(ACHIEVEMENTS.storeShape, { + isValid: this.isStoreShapeValid, + signal: "entityGotNewComponent", }); + this.createAndSet(ACHIEVEMENTS.unlockWires, this.createLevelOptions(20)); } /** @param {GameRoot} root */ @@ -199,23 +193,21 @@ export class AchievementCollection { * @param {function} [options.isRelevant] * @param {string} [options.signal] */ - createAndSet(key, options) { + createAndSet(key, options = {}) { const achievement = new Achievement(key); achievement.activate = this.activate; - if (options) { - if (options.isValid) { - achievement.isValid = options.isValid.bind(this); - } + if (options.isValid) { + achievement.isValid = options.isValid.bind(this); + } - if (options.isRelevant) { - achievement.isRelevant = options.isRelevant.bind(this); - } + if (options.isRelevant) { + achievement.isRelevant = options.isRelevant.bind(this); + } - if (options.signal) { - achievement.signal = options.signal; - } + if (options.signal) { + achievement.signal = options.signal; } this.map.set(key, achievement); @@ -236,16 +228,29 @@ export class AchievementCollection { return; } - return achievement.unlock() - .finally(() => { - this.remove(key); - - if (!this.hasDefaultReceivers()) { - this.root.signals.achievementUnlocked.remove(this.unlock); - } + 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.achievementUnlocked.remove(this.unlock); + } + } + /** @param {string} key - Maps to an Achievement */ remove(key) { const achievement = this.map.get(key); @@ -271,50 +276,18 @@ export class AchievementCollection { return false; } - /** - * @param {string} key - * @param {boolean} anyPlaced - * @returns {boolean} - */ - isBlueprintsValid(key, anyPlaced) { - return anyPlaced; + createLevelOptions (level) { + return { + isRelevant: () => this.root.hubGoals.level < level, + isValid: (key, currentLevel) => currentLevel === level, + signal: "storyGoalCompleted" + } } - /** @returns {boolean} */ - isWiresRelevant() { - return this.root.hubGoals.level < WIRES_LEVEL; - } - - /** - * @param {string} key - * @param {number} level - * @returns {boolean} - */ - isWiresValid(key, level) { - return level === WIRES_LEVEL; - } - - /** - * @param {string} key - * @param {StorageComponent} storage - * @returns {boolean} - */ - isStorageValid(key, storage) { - return storage.storedCount >= 1; - } - - /** @returns {boolean} */ - isFreedomRelevant() { - return this.root.hubGoals.level < FREEDOM_LEVEL; - } - - /** - * @param {string} key - * @param {number} level - * @returns {boolean} - */ - isFreedomValid(key, level) { - return level === FREEDOM_LEVEL; + createShapeOptions (shape) { + return { + isValid: (key, definition) => definition.cachedHash === shape + } } /** @@ -322,10 +295,8 @@ export class AchievementCollection { * @param {Entity} entity * @returns {boolean} */ - isNetworkedValid(key, entity) { - return entity.components.Wire && - entity.registered && - entity.root.entityMgr.componentToEntity.Wire.length === NETWORKED_WIRE_COUNT; + isBelt500TilesValid(key, entity) { + return entity.components.Belt && entity.components.Belt.assignedPath.totalLength >= 500; } /** @@ -333,8 +304,9 @@ export class AchievementCollection { * @param {ShapeDefinition} definition * @returns {boolean} */ - isTheLogoValid(key, definition) { - return definition.layers.length === 2 && definition.cachedHash === LOGO_SHAPE; + isBlueprint100kValid(key, definition) { + return definition.cachedHash === SHAPE_BLUEPRINT && + this.root.hubGoals.storedShapes[SHAPE_BLUEPRINT] >= 100000; } /** @@ -342,50 +314,9 @@ export class AchievementCollection { * @param {ShapeDefinition} definition * @returns {boolean} */ - isToTheMoonValid(key, definition) { - return definition.layers.length === 4 && definition.cachedHash === ROCKET_SHAPE; - } - - /** - * @param {string} key - * @param {ShapeDefinition} definition - * @returns {boolean} - */ - isMillionBlueprintShapesValid(key, definition) { - return definition.cachedHash === BLUEPRINT_SHAPE && - this.root.hubGoals.storedShapes[BLUEPRINT_SHAPE] >= ONE_MILLION; - } - - /** @returns {boolean} */ - isHundredShapesRelevant() { - return Object.keys(this.root.hubGoals.storedShapes).length < ONE_HUNDRED; - } - - /** - * @param {string} key - * @returns {boolean} - */ - isHundredShapesValid(key) { - return Object.keys(this.root.hubGoals.storedShapes).length === ONE_HUNDRED; - } - - /** - * @param {string} key - * @param {ShapeDefinition} definition - * @returns {boolean} - */ - isFourLayersValid(key, definition) { - return definition.layers.length === 4; - } - - /** - * @param {string} key - * @param {Entity} entity - * @returns {boolean} - */ - isLongBeltValid(key, entity) { - return entity.components.Belt && - entity.components.Belt.assignedPath.totalLength >= LONG_BELT_COUNT; + isBlueprint1mValid(key, definition) { + return definition.cachedHash === SHAPE_BLUEPRINT && + this.root.hubGoals.storedShapes[SHAPE_BLUEPRINT] >= 1000000; } /** @@ -396,4 +327,54 @@ export class AchievementCollection { return this.root.app.settings.currentData.settings.theme === DARK_MODE; } + /** + * @param {string} key + * @param {Entity} entity + * @returns {boolean} + */ + isPlace5000WiresValid(key, entity) { + return entity.components.Wire && + entity.registered && + entity.root.entityMgr.componentToEntity.Wire.length === 5000; + } + + /** + * @param {string} key + * @param {boolean} anyPlaced + * @returns {boolean} + */ + isPlaceBlueprintValid(key, anyPlaced) { + return anyPlaced; + } + + /** + * @param {string} key + * @param {ShapeDefinition} definition + * @returns {boolean} + */ + isStack4LayersValid(key, definition) { + return definition.layers.length === 4; + } + + /** @returns {boolean} */ + isStore100UniqueRelevant() { + return Object.keys(this.root.hubGoals.storedShapes).length < 100; + } + + /** + * @param {string} key + * @returns {boolean} + */ + isStore100UniqueValid(key) { + return Object.keys(this.root.hubGoals.storedShapes).length === 100; + } + + /** + * @param {string} key + * @param {StorageComponent} storage + * @returns {boolean} + */ + isStoreShapeValid(key, storage) { + return storage.storedCount >= 1; + } } diff --git a/src/js/platform/electron/steam_achievement_provider.js b/src/js/platform/electron/steam_achievement_provider.js index 08959679..0a7d41f8 100644 --- a/src/js/platform/electron/steam_achievement_provider.js +++ b/src/js/platform/electron/steam_achievement_provider.js @@ -5,31 +5,31 @@ import { GameRoot } from "../../game/root"; import { createLogger } from "../../core/logging"; import { getIPCRenderer } from "../../core/utils"; -import { - ACHIEVEMENTS, - AchievementCollection, - AchievementProviderInterface -} from "../achievement_provider"; +import { ACHIEVEMENTS, AchievementCollection, AchievementProviderInterface } from "../achievement_provider"; const logger = createLogger("achievements/steam"); const ACHIEVEMENT_IDS = { - [ACHIEVEMENTS.blueprints]: "", - [ACHIEVEMENTS.cutting]: "achievement_01", // Test ID - [ACHIEVEMENTS.darkMode]: "", - [ACHIEVEMENTS.fourLayers]: "", - [ACHIEVEMENTS.freedom]: "", - [ACHIEVEMENTS.hundredShapes]: "", - [ACHIEVEMENTS.longBelt]: "", - [ACHIEVEMENTS.millionBlueprintShapes]: "", - [ACHIEVEMENTS.networked]: "", - [ACHIEVEMENTS.painting]: "", - [ACHIEVEMENTS.rotating]: "", - [ACHIEVEMENTS.stacking]: "", - [ACHIEVEMENTS.storage]: "", - [ACHIEVEMENTS.theLogo]: "", - [ACHIEVEMENTS.toTheMoon]: "", - [ACHIEVEMENTS.wires]: "", + [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.level100]: "level_100", + [ACHIEVEMENTS.level50]: "level_50", + [ACHIEVEMENTS.paintShape]: "paint_shape", + [ACHIEVEMENTS.place5000Wires]: "place_5000_wires", + [ACHIEVEMENTS.placeBlueprint]: "place_blueprint", + [ACHIEVEMENTS.produceLogo]: "produce_logo", + [ACHIEVEMENTS.produceMsLogo]: "produce_ms_logo", + [ACHIEVEMENTS.produceRocket]: "produce_rocket", + [ACHIEVEMENTS.rotateShape]: "rotate_shape", + [ACHIEVEMENTS.stack4Layers]: "stack_4_layers", + [ACHIEVEMENTS.stackShape]: "stack_shape", + [ACHIEVEMENTS.store100Unique]: "store_100_unique", + [ACHIEVEMENTS.storeShape]: "store_shape", + [ACHIEVEMENTS.unlockWires]: "unlock_wires", }; export class SteamAchievementProvider extends AchievementProviderInterface { @@ -90,7 +90,7 @@ export class SteamAchievementProvider extends AchievementProviderInterface { * @param {string} key - An ACHIEVEMENTS key * @returns {Promise} */ - activate (key) { + activate(key) { let promise; if (!this.initialized) { @@ -99,12 +99,13 @@ export class SteamAchievementProvider extends AchievementProviderInterface { promise = this.ipc.invoke("steam:activate-achievement", ACHIEVEMENT_IDS[key]); } - return promise + return promise .then(() => { logger.log("Achievement activated:", key); }) .catch(err => { logger.error("Failed to activate achievement:", key, err); - }) + throw err; + }); } }