mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
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
This commit is contained in:
parent
9597f3e89c
commit
835317477d
@ -16,10 +16,12 @@ export class AchievementProxy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.provider.initialize()
|
||||
.then(() => {
|
||||
this.root.signals.achievementUnlocked.add(this.provider.unlock, this.provider);
|
||||
this.root.signals.postLoadHook.add(this.onLoad, this);
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this.provider.initialize(this.root)
|
||||
.then(() => {
|
||||
logger.log("Listening for unlocked achievements");
|
||||
})
|
||||
.catch(err => {
|
||||
|
@ -164,9 +164,7 @@ export class Blueprint {
|
||||
anyPlaced = true;
|
||||
}
|
||||
|
||||
if (anyPlaced) {
|
||||
root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.blueprints);
|
||||
}
|
||||
root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.blueprints, anyPlaced);
|
||||
|
||||
return anyPlaced;
|
||||
});
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { RandomNumberGenerator } from "../core/rng";
|
||||
import { clamp } from "../core/utils";
|
||||
import { ACHIEVEMENTS } from "../platform/achievement_provider";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { enumColors } from "./colors";
|
||||
import { enumItemProcessorTypes } from "./components/item_processor";
|
||||
@ -261,12 +260,6 @@ export class HubGoals extends BasicSerializableObject {
|
||||
this.computeNextGoal();
|
||||
|
||||
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
|
||||
|
||||
if (this.level - 1 === 20) {
|
||||
this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.wires);
|
||||
} else if (this.level - 1 === 26) {
|
||||
this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.freedom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -253,6 +253,7 @@ 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);
|
||||
|
||||
// logger.log("Registered shape with key (2)", id);
|
||||
return definition;
|
||||
|
@ -276,9 +276,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
if (storageComp.canAcceptItem(item)) {
|
||||
storageComp.takeItem(item);
|
||||
|
||||
if (storageComp.storedCount === 1) {
|
||||
this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.storage);
|
||||
}
|
||||
this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.storage, storageComp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -698,11 +698,7 @@ export class WireSystem extends GameSystemWithFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity.components.Wire && entity.registered &&
|
||||
this.root.entityMgr.componentToEntity.Wire.length === 100) {
|
||||
|
||||
this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.networked);
|
||||
}
|
||||
this.root.signals.achievementUnlocked.dispatch(ACHIEVEMENTS.networked, entity);
|
||||
|
||||
// Invalidate affected area
|
||||
const originalRect = staticComp.getTileSpaceBounds();
|
||||
|
@ -1,5 +1,9 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
import { StorageComponent } from "../game/components/storage";
|
||||
import { Entity } from "../game/entity";
|
||||
import { GameRoot } from "../game/root";
|
||||
import { ShapeDefinition } from "../game/shape_definition";
|
||||
/* typehints:end */
|
||||
|
||||
export const ACHIEVEMENTS = {
|
||||
@ -13,8 +17,21 @@ export const ACHIEVEMENTS = {
|
||||
freedom: "freedom",
|
||||
networked: "networked",
|
||||
theLogo: "theLogo",
|
||||
toTheMoon: "toTheMoon",
|
||||
millionBlueprintShapes: "millionBlueprintShapes",
|
||||
|
||||
hundredShapes: "hundredShapes",
|
||||
};
|
||||
|
||||
const ONE_HUNDRED = 100;
|
||||
const ONE_MILLION = 1000000;
|
||||
const BLUEPRINT_SHAPE = "CbCbCbRb:CwCwCwCw";
|
||||
const LOGO_SHAPE = "RuCw--Cw:----Ru--";
|
||||
const ROCKET_SHAPE = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
||||
const FREEDOM_LEVEL = 26;
|
||||
const WIRES_LEVEL = 20;
|
||||
const NETWORKED_WIRE_COUNT = 100;
|
||||
|
||||
export class AchievementProviderInterface {
|
||||
/** @param {Application} app */
|
||||
constructor(app) {
|
||||
@ -30,16 +47,6 @@ export class AchievementProviderInterface {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to unlock an achievement
|
||||
* @param {string} [key] - A property within the ACHIEVEMENTS enum or empty if
|
||||
* bypassing.
|
||||
* @returns {void}
|
||||
*/
|
||||
unlock(key) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if achievements are supported in the current build
|
||||
* @returns {boolean}
|
||||
@ -51,13 +58,26 @@ export class AchievementProviderInterface {
|
||||
}
|
||||
|
||||
export class Achievement {
|
||||
/**
|
||||
* @param {string} key - An ACHIEVEMENTS key
|
||||
*/
|
||||
constructor (key) {
|
||||
/** @param {string} key - An ACHIEVEMENTS key */
|
||||
constructor(key) {
|
||||
this.key = key;
|
||||
this.unlock = null;
|
||||
this.isValid = null;
|
||||
this.unlocked = false;
|
||||
this.signal = null;
|
||||
this.receiver = null;
|
||||
this.activate = null;
|
||||
this.activatePromise = null;
|
||||
}
|
||||
|
||||
isValid () {
|
||||
return true;
|
||||
}
|
||||
|
||||
unlock() {
|
||||
if (!this.activatePromise) {
|
||||
this.activatePromise = this.activate(this.key);
|
||||
}
|
||||
|
||||
return this.activatePromise;
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,73 +86,221 @@ export class AchievementCollection {
|
||||
* @param {string[]} keys - An array of ACHIEVEMENTS keys
|
||||
* @param {function} [activate] - Resolves when provider activation is complete
|
||||
*/
|
||||
constructor (keys, activate) {
|
||||
constructor(keys, activate) {
|
||||
this.map = new Map();
|
||||
this.activate = activate ? activate : () => Promise.resolve();
|
||||
this.activate = activate;
|
||||
|
||||
assert(Object.keys(ACHIEVEMENTS).length === keys.length, "Mismatched achievements");
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
assert(ACHIEVEMENTS[keys[i]], "Achievement does not exist: " + keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const achievement = new Achievement(keys[i]);
|
||||
this.setValidation(achievement);
|
||||
this.map.set(keys[i], achievement);
|
||||
/** @param {GameRoot} root */
|
||||
initialize(root) {
|
||||
this.root = root;
|
||||
this.root.signals.achievementUnlocked.add(this.unlock, this);
|
||||
|
||||
this.createAndSet(ACHIEVEMENTS.painting)
|
||||
this.createAndSet(ACHIEVEMENTS.cutting)
|
||||
this.createAndSet(ACHIEVEMENTS.rotating)
|
||||
this.createAndSet(ACHIEVEMENTS.stacking)
|
||||
this.createAndSet(ACHIEVEMENTS.blueprints, this.isBlueprintsValid);
|
||||
|
||||
if (this.isWiresRelevant()) {
|
||||
this.createAndSet(ACHIEVEMENTS.wires, this.isWiresValid, "storyGoalCompleted");
|
||||
}
|
||||
|
||||
this.createAndSet(ACHIEVEMENTS.storage, this.isStorageValid, "entityGotNewComponent");
|
||||
|
||||
if (this.isFreedomRelevant()) { // ...is it?
|
||||
this.createAndSet(ACHIEVEMENTS.freedom, this.isFreedomValid, "storyGoalCompleted");
|
||||
}
|
||||
|
||||
this.createAndSet(ACHIEVEMENTS.networked, this.isNetworkedValid);
|
||||
this.createAndSet(ACHIEVEMENTS.theLogo, this.isTheLogoValid);
|
||||
this.createAndSet(ACHIEVEMENTS.toTheMoon, this.isToTheMoonValid);
|
||||
this.createAndSet(
|
||||
ACHIEVEMENTS.millionBlueprintShapes,
|
||||
this.isMillionBlueprintShapesValid,
|
||||
"shapeDelivered"
|
||||
);
|
||||
|
||||
if (this.isHundredShapesRelevant()) {
|
||||
this.createAndSet(
|
||||
ACHIEVEMENTS.hundredShapes,
|
||||
this.isHundredShapesValid,
|
||||
"shapeDelivered"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key - Maps to an Achievement
|
||||
* @returns {boolean}
|
||||
* @param {function} [isValid] - Validates achievement when a signal message is received
|
||||
* @param {string} [signal] - Signal name to listen to for unlock attempts
|
||||
*/
|
||||
has(key) {
|
||||
return this.map.has(key);
|
||||
createAndSet(key, isValid, signal) {
|
||||
const achievement = new Achievement(key);
|
||||
|
||||
achievement.activate = this.activate;
|
||||
|
||||
if (isValid) {
|
||||
achievement.isValid = isValid.bind(this);
|
||||
}
|
||||
|
||||
if (signal) {
|
||||
achievement.signal = signal;
|
||||
achievement.receiver = this.unlock.bind(this, key);
|
||||
this.root.signals[achievement.signal].add(achievement.receiver);
|
||||
}
|
||||
|
||||
this.map.set(key, achievement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key - Maps to an Achievement
|
||||
* @param {*} [details] - Additional information as needed to validate
|
||||
* @returns {boolean}
|
||||
* @param {*[]} [arguments] - Additional arguments received from signal dispatches
|
||||
*/
|
||||
isValid(key, details) {
|
||||
return this.map.get(key).isValid(details);
|
||||
}
|
||||
unlock(key) {
|
||||
if (!this.map.has(key)) {
|
||||
console.log("Achievement unlocked or irrelevant:", key);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key - Maps to an Achievement
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
unlock(key) {
|
||||
const achievement = this.map.get(key);
|
||||
|
||||
return achievement.unlock = achievement.unlock || this.activate(achievement)
|
||||
.then(() => {
|
||||
this.map.delete(key);
|
||||
})
|
||||
.catch(err => {
|
||||
achievement.unlock = null;
|
||||
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Achievement} achievement - Achievement receiving a validation function
|
||||
*/
|
||||
setValidation(achievement) {
|
||||
switch (achievement.key) {
|
||||
case ACHIEVEMENTS.theLogo:
|
||||
achievement.isValid = this.isTheLogoValid;
|
||||
break;
|
||||
default:
|
||||
achievement.isValid = () => true;
|
||||
break;
|
||||
if (!achievement.isValid(...arguments)) {
|
||||
console.log("Achievement is invalid:", key);
|
||||
return;
|
||||
}
|
||||
|
||||
return achievement.unlock()
|
||||
.finally(() => {
|
||||
if (achievement.receiver) {
|
||||
this.root.signals[achievement.signal].remove(achievement.receiver);
|
||||
console.log("Achievement receiver removed:", key);
|
||||
}
|
||||
|
||||
this.map.delete(key);
|
||||
|
||||
if (!this.hasDefaultReceivers()) {
|
||||
this.root.signals.achievementUnlocked.remove(this.unlock);
|
||||
console.log("removed achievementUnlocked receiver");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasDefaultReceivers() {
|
||||
if (!this.map.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(let achievement of this.map.values()) {
|
||||
if (!achievement.signal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} shortKey - The shape's shortKey to check
|
||||
* @param {string} key
|
||||
* @param {boolean} anyPlaced
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTheLogoValid(shortKey) {
|
||||
return shortKey === "RuCw--Cw:----Ru--";
|
||||
isBlueprintsValid(key, anyPlaced) {
|
||||
return anyPlaced;
|
||||
}
|
||||
|
||||
/** @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isHundredShapesValid(key) {
|
||||
return Object.keys(this.root.hubGoals.storedShapes).length === ONE_HUNDRED;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,11 @@ const ACHIEVEMENT_IDS = {
|
||||
[ACHIEVEMENTS.storage]: "<id>",
|
||||
[ACHIEVEMENTS.freedom]: "<id>",
|
||||
[ACHIEVEMENTS.networked]: "<id>",
|
||||
[ACHIEVEMENTS.theLogo]: "<id>"
|
||||
[ACHIEVEMENTS.theLogo]: "<id>",
|
||||
[ACHIEVEMENTS.toTheMoon]: "<id>",
|
||||
[ACHIEVEMENTS.millionBlueprintShapes]: "<id>",
|
||||
|
||||
[ACHIEVEMENTS.hundredShapes]: "<id>",
|
||||
};
|
||||
|
||||
export class SteamAchievementProvider extends AchievementProviderInterface {
|
||||
@ -32,87 +36,64 @@ export class SteamAchievementProvider extends AchievementProviderInterface {
|
||||
super(app);
|
||||
|
||||
this.initialized = false;
|
||||
this.collection = new AchievementCollection(
|
||||
Object.keys(ACHIEVEMENT_IDS),
|
||||
this.activate.bind(this)
|
||||
);
|
||||
this.keys = Object.keys(ACHIEVEMENT_IDS);
|
||||
this.collection = new AchievementCollection(this.keys, this.activate.bind(this));
|
||||
|
||||
logger.log("Steam achievement collection created");
|
||||
}
|
||||
|
||||
initialize () {
|
||||
if (!G_IS_STANDALONE) {
|
||||
logger.warn("Steam listener isn't active. Achievements won't sync.");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.ipc = getIPCRenderer();
|
||||
|
||||
return this.ipc.invoke("steam:is-initialized")
|
||||
.then(initialized => {
|
||||
if (!initialized) {
|
||||
logger.warn("Steam failed to intialize. Achievements won't sync.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
logger.log("Steam achievement provider initialized");
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Steam achievement provider error", err);
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key - Maps to an Achievement
|
||||
* @param {*} [details] - Additional information as needed to validate
|
||||
*/
|
||||
unlock (key, details) {
|
||||
if (!this.collection.has(key)) {
|
||||
console.log("Achievement already unlocked", key);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.collection.isValid(key, details)) {
|
||||
console.log("Achievement is invalid", key);
|
||||
return;
|
||||
}
|
||||
|
||||
this.collection.unlock(key)
|
||||
.then(() => {
|
||||
logger.log("Achievement unlocked:", key);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Failed to unlock achievement", err);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key - Maps to an API ID for the achievement
|
||||
* @returns {string}
|
||||
*/
|
||||
getApiId (key) {
|
||||
return ACHIEVEMENT_IDS[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Achievement} achievement
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
activate (achievement) {
|
||||
if (!this.initialized) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.ipc.invoke("steam:activate-achievement", this.getApiId(achievement.key))
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAchievements() {
|
||||
return true;
|
||||
}
|
||||
|
||||
initialize (root) {
|
||||
this.collection.initialize(root);
|
||||
|
||||
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");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Steam achievement provider error", err);
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key - An ACHIEVEMENTS 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 unlocked:", key);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Failed to unlock achievement:", key, err);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user