2021-03-01 23:07:21 +00:00
|
|
|
/* typehints:start */
|
|
|
|
|
import { Application } from "../application";
|
2021-03-03 16:59:56 +00:00
|
|
|
import { StorageComponent } from "../game/components/storage";
|
|
|
|
|
import { Entity } from "../game/entity";
|
|
|
|
|
import { GameRoot } from "../game/root";
|
|
|
|
|
import { ShapeDefinition } from "../game/shape_definition";
|
2021-03-01 23:07:21 +00:00
|
|
|
/* typehints:end */
|
|
|
|
|
|
|
|
|
|
export const ACHIEVEMENTS = {
|
2021-03-03 22:30:14 +00:00
|
|
|
blueprints: "blueprints",
|
2021-03-01 23:07:21 +00:00
|
|
|
cutting: "cutting",
|
2021-03-03 22:30:14 +00:00
|
|
|
darkMode: "darkMode",
|
|
|
|
|
fourLayers: "fourLayers",
|
|
|
|
|
freedom: "freedom",
|
|
|
|
|
hundredShapes: "hundredShapes",
|
|
|
|
|
longBelt: "longBelt",
|
|
|
|
|
millionBlueprintShapes: "millionBlueprintShapes",
|
|
|
|
|
networked: "networked",
|
|
|
|
|
painting: "painting",
|
2021-03-01 23:07:21 +00:00
|
|
|
rotating: "rotating",
|
|
|
|
|
stacking: "stacking",
|
|
|
|
|
storage: "storage",
|
|
|
|
|
theLogo: "theLogo",
|
2021-03-03 16:59:56 +00:00
|
|
|
toTheMoon: "toTheMoon",
|
2021-03-03 22:30:14 +00:00
|
|
|
wires: "wires",
|
2021-03-01 23:07:21 +00:00
|
|
|
};
|
|
|
|
|
|
2021-03-03 16:59:56 +00:00
|
|
|
const BLUEPRINT_SHAPE = "CbCbCbRb:CwCwCwCw";
|
2021-03-03 22:30:14 +00:00
|
|
|
const DARK_MODE = "dark";
|
|
|
|
|
const FREEDOM_LEVEL = 26;
|
2021-03-03 16:59:56 +00:00
|
|
|
const LOGO_SHAPE = "RuCw--Cw:----Ru--";
|
2021-03-03 22:30:14 +00:00
|
|
|
const LONG_BELT_COUNT = 200;
|
|
|
|
|
const NETWORKED_WIRE_COUNT = 100;
|
|
|
|
|
const ONE_HUNDRED = 100;
|
|
|
|
|
const ONE_MILLION = 1000000;
|
2021-03-03 16:59:56 +00:00
|
|
|
const ROCKET_SHAPE = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
|
|
|
|
const WIRES_LEVEL = 20;
|
|
|
|
|
|
2021-03-01 23:07:21 +00:00
|
|
|
export class AchievementProviderInterface {
|
|
|
|
|
/** @param {Application} app */
|
|
|
|
|
constructor(app) {
|
|
|
|
|
this.app = app;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes the achievement provider.
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
initialize() {
|
|
|
|
|
abstract;
|
|
|
|
|
return Promise.reject();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
/**
|
|
|
|
|
* Opportunity to do additional initialization work with the GameRoot.
|
|
|
|
|
* @param {GameRoot} root
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
onLoad(root) {
|
|
|
|
|
abstract;
|
|
|
|
|
return Promise.reject();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Call to activate an achievement with the provider
|
|
|
|
|
* @param {string} key - Maps to an Achievement
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
|
|
|
|
activate(key) {
|
|
|
|
|
abstract;
|
|
|
|
|
return Promise.reject();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-01 23:07:21 +00:00
|
|
|
/**
|
|
|
|
|
* Checks if achievements are supported in the current build
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
hasAchievements() {
|
|
|
|
|
abstract;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class Achievement {
|
2021-03-03 16:59:56 +00:00
|
|
|
/** @param {string} key - An ACHIEVEMENTS key */
|
|
|
|
|
constructor(key) {
|
2021-03-01 23:07:21 +00:00
|
|
|
this.key = key;
|
2021-03-03 16:59:56 +00:00
|
|
|
this.activate = null;
|
|
|
|
|
this.activatePromise = null;
|
2021-03-03 22:30:14 +00:00
|
|
|
this.receiver = null;
|
|
|
|
|
this.signal = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isValid() {
|
|
|
|
|
return true;
|
2021-03-03 16:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
isRelevant() {
|
2021-03-03 16:59:56 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unlock() {
|
|
|
|
|
if (!this.activatePromise) {
|
|
|
|
|
this.activatePromise = this.activate(this.key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.activatePromise;
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class AchievementCollection {
|
|
|
|
|
/**
|
2021-03-03 22:30:14 +00:00
|
|
|
* @param {function} activate - Resolves when provider activation is complete
|
2021-03-01 23:07:21 +00:00
|
|
|
*/
|
2021-03-03 22:30:14 +00:00
|
|
|
constructor(activate) {
|
2021-03-01 23:07:21 +00:00
|
|
|
this.map = new Map();
|
2021-03-03 16:59:56 +00:00
|
|
|
this.activate = activate;
|
2021-03-03 22:30:14 +00:00
|
|
|
this.initialized = false;
|
|
|
|
|
|
|
|
|
|
this.createAndSet(ACHIEVEMENTS.blueprints, {
|
|
|
|
|
isValid: this.isBlueprintsValid
|
|
|
|
|
});
|
|
|
|
|
this.createAndSet(ACHIEVEMENTS.cutting);
|
|
|
|
|
this.createAndSet(ACHIEVEMENTS.darkMode, {
|
|
|
|
|
isValid: this.isDarkModeValid
|
|
|
|
|
});
|
|
|
|
|
this.createAndSet(ACHIEVEMENTS.fourLayers, {
|
|
|
|
|
isValid: this.isFourLayersValid
|
|
|
|
|
});
|
|
|
|
|
this.createAndSet(ACHIEVEMENTS.freedom, {
|
|
|
|
|
isRelevant: this.isFreedomRelevant,
|
|
|
|
|
isValid: this.isFreedomValid,
|
|
|
|
|
signal: "storyGoalCompleted"
|
|
|
|
|
});
|
|
|
|
|
this.createAndSet(ACHIEVEMENTS.hundredShapes, {
|
|
|
|
|
isRelevant: this.isHundredShapesRelevant,
|
|
|
|
|
isValid: this.isHundredShapesValid,
|
|
|
|
|
signal: "shapeDelivered"
|
|
|
|
|
});
|
|
|
|
|
this.createAndSet(ACHIEVEMENTS.longBelt, {
|
|
|
|
|
isValid: this.isLongBeltValid,
|
|
|
|
|
signal: "entityAdded"
|
|
|
|
|
});
|
|
|
|
|
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"
|
|
|
|
|
});
|
2021-03-03 16:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {GameRoot} root */
|
|
|
|
|
initialize(root) {
|
|
|
|
|
this.root = root;
|
|
|
|
|
this.root.signals.achievementUnlocked.add(this.unlock, this);
|
|
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
for (let [key, achievement] of this.map.entries()) {
|
|
|
|
|
if (!achievement.isRelevant()) {
|
|
|
|
|
this.remove(key);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2021-03-03 16:59:56 +00:00
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
if (achievement.signal) {
|
|
|
|
|
achievement.receiver = this.unlock.bind(this, key);
|
|
|
|
|
this.root.signals[achievement.signal].add(achievement.receiver);
|
|
|
|
|
}
|
2021-03-03 16:59:56 +00:00
|
|
|
}
|
2021-03-01 23:07:21 +00:00
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
if (!this.hasDefaultReceivers()) {
|
|
|
|
|
this.root.signals.achievementUnlocked.remove(this.unlock);
|
2021-03-03 16:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
this.initialized = true;
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} key - Maps to an Achievement
|
2021-03-03 22:30:14 +00:00
|
|
|
* @param {object} [options]
|
|
|
|
|
* @param {function} [options.isValid]
|
|
|
|
|
* @param {function} [options.isRelevant]
|
|
|
|
|
* @param {string} [options.signal]
|
2021-03-01 23:07:21 +00:00
|
|
|
*/
|
2021-03-03 22:30:14 +00:00
|
|
|
createAndSet(key, options) {
|
2021-03-03 16:59:56 +00:00
|
|
|
const achievement = new Achievement(key);
|
|
|
|
|
|
|
|
|
|
achievement.activate = this.activate;
|
|
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
if (options) {
|
|
|
|
|
if (options.isValid) {
|
|
|
|
|
achievement.isValid = options.isValid.bind(this);
|
|
|
|
|
}
|
2021-03-03 16:59:56 +00:00
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
if (options.isRelevant) {
|
|
|
|
|
achievement.isRelevant = options.isRelevant.bind(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.signal) {
|
|
|
|
|
achievement.signal = options.signal;
|
|
|
|
|
}
|
2021-03-03 16:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.map.set(key, achievement);
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} key - Maps to an Achievement
|
2021-03-03 16:59:56 +00:00
|
|
|
* @param {*[]} [arguments] - Additional arguments received from signal dispatches
|
|
|
|
|
*/
|
|
|
|
|
unlock(key) {
|
|
|
|
|
if (!this.map.has(key)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const achievement = this.map.get(key);
|
|
|
|
|
|
|
|
|
|
if (!achievement.isValid(...arguments)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return achievement.unlock()
|
|
|
|
|
.finally(() => {
|
2021-03-03 22:30:14 +00:00
|
|
|
this.remove(key);
|
2021-03-03 16:59:56 +00:00
|
|
|
|
|
|
|
|
if (!this.hasDefaultReceivers()) {
|
|
|
|
|
this.root.signals.achievementUnlocked.remove(this.unlock);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
/** @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);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-03 16:59:56 +00:00
|
|
|
hasDefaultReceivers() {
|
|
|
|
|
if (!this.map.size) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-03 22:30:14 +00:00
|
|
|
for (let achievement of this.map.values()) {
|
2021-03-03 16:59:56 +00:00
|
|
|
if (!achievement.signal) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} key
|
|
|
|
|
* @param {boolean} anyPlaced
|
2021-03-01 23:07:21 +00:00
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
2021-03-03 16:59:56 +00:00
|
|
|
isBlueprintsValid(key, anyPlaced) {
|
|
|
|
|
return anyPlaced;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @returns {boolean} */
|
|
|
|
|
isWiresRelevant() {
|
|
|
|
|
return this.root.hubGoals.level < WIRES_LEVEL;
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2021-03-03 16:59:56 +00:00
|
|
|
* @param {string} key
|
|
|
|
|
* @param {number} level
|
|
|
|
|
* @returns {boolean}
|
2021-03-01 23:07:21 +00:00
|
|
|
*/
|
2021-03-03 16:59:56 +00:00
|
|
|
isWiresValid(key, level) {
|
|
|
|
|
return level === WIRES_LEVEL;
|
|
|
|
|
}
|
2021-03-01 23:07:21 +00:00
|
|
|
|
2021-03-03 16:59:56 +00:00
|
|
|
/**
|
|
|
|
|
* @param {string} key
|
|
|
|
|
* @param {StorageComponent} storage
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
isStorageValid(key, storage) {
|
|
|
|
|
return storage.storedCount >= 1;
|
|
|
|
|
}
|
2021-03-01 23:07:21 +00:00
|
|
|
|
2021-03-03 16:59:56 +00:00
|
|
|
/** @returns {boolean} */
|
|
|
|
|
isFreedomRelevant() {
|
|
|
|
|
return this.root.hubGoals.level < FREEDOM_LEVEL;
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2021-03-03 16:59:56 +00:00
|
|
|
* @param {string} key
|
|
|
|
|
* @param {number} level
|
|
|
|
|
* @returns {boolean}
|
2021-03-01 23:07:21 +00:00
|
|
|
*/
|
2021-03-03 16:59:56 +00:00
|
|
|
isFreedomValid(key, level) {
|
|
|
|
|
return level === FREEDOM_LEVEL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} key
|
|
|
|
|
* @param {Entity} entity
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
isNetworkedValid(key, entity) {
|
|
|
|
|
return entity.components.Wire &&
|
|
|
|
|
entity.registered &&
|
|
|
|
|
entity.root.entityMgr.componentToEntity.Wire.length === NETWORKED_WIRE_COUNT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} key
|
|
|
|
|
* @param {ShapeDefinition} definition
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
isTheLogoValid(key, definition) {
|
|
|
|
|
return definition.layers.length === 2 && definition.cachedHash === LOGO_SHAPE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} key
|
|
|
|
|
* @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;
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2021-03-03 16:59:56 +00:00
|
|
|
* @param {string} key
|
2021-03-01 23:07:21 +00:00
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
2021-03-03 16:59:56 +00:00
|
|
|
isHundredShapesValid(key) {
|
|
|
|
|
return Object.keys(this.root.hubGoals.storedShapes).length === ONE_HUNDRED;
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|
2021-03-03 22:30:14 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} key
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
isDarkModeValid(key) {
|
|
|
|
|
return this.root.app.settings.currentData.settings.theme === DARK_MODE;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-01 23:07:21 +00:00
|
|
|
}
|