1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

[WIP] Add achievements. Start savefile migration

This commit is contained in:
Greg Considine 2021-03-08 19:07:23 -05:00
parent a722c3562d
commit 10b90a8df2
10 changed files with 270 additions and 125 deletions

View File

@ -41,7 +41,7 @@ export const globalConfig = {
shapesSharpness: 1.4,
// Achievements
achievementSliceDuration: 30, // Seconds
achievementSliceDuration: 10, // Seconds
// Production analytics
statisticsGraphDpi: 2.5,

View File

@ -5,21 +5,42 @@ import { GameRoot } from "./root";
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { ACHIEVEMENTS } from "../platform/achievement_provider";
import { BasicSerializableObject } from "../savegame/serialization";
//import { typeAchievementCollection } from "./achievement_resolver";
const logger = createLogger("achievement_proxy");
export class AchievementProxy {
export class AchievementProxy extends BasicSerializableObject {
static getId() {
return "AchievementProxy";
}
static getSchema() {
return {
// collection: typeAchievementCollection
};
}
deserialize(data, root) {
}
/** @param {GameRoot} root */
constructor(root) {
super();
this.root = root;
this.provider = this.root.app.achievementProvider;
this.lastSlice = 0;
this.disabled = true;
if (!this.provider.hasAchievements()) {
return;
}
this.sliceTime = 0;
this.sliceIteration = 0;
this.sliceIterationLimit = 2;
this.root.signals.postLoadHook.add(this.onLoad, this);
}
@ -37,22 +58,49 @@ export class AchievementProxy {
});
}
// Have certain checks every 30 seconds, 10 seconds, etc.
// Re-check relevance every so often
// Consider disabling checks if no longer relevant
startSlice() {
this.lastSlice = this.root.time.now();
this.sliceTime = this.root.time.now();
// Every slice
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.storeShape, this.sliceTime);
// Every other slice
if (this.sliceIteration % 2 === 0) {
this.root.signals.bulkAchievementCheck.dispatch(
ACHIEVEMENTS.play1h, this.lastSlice,
ACHIEVEMENTS.play10h, this.lastSlice,
ACHIEVEMENTS.play20h, this.lastSlice
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
);
}
if (this.sliceIteration === this.sliceIterationLimit) {
this.sliceIteration = 0;
} else {
this.sliceIteration++;
}
}
update() {
if (this.disabled) {
return;
}
if (this.root.time.now() - this.lastSlice > globalConfig.achievementSliceDuration) {
if (this.root.time.now() - this.sliceTime > globalConfig.achievementSliceDuration) {
this.startSlice();
}
}

View File

@ -0,0 +1,4 @@
export function achievementResolver(root, data) {
}

View File

@ -1,5 +1,6 @@
import { generateMatrixRotations } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
@ -37,6 +38,25 @@ export class MetaTrashBuilding extends MetaBuilding {
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
* @param {Entity} entity
@ -57,11 +77,14 @@ export class MetaTrashBuilding extends MetaBuilding {
],
})
);
entity.addComponent(
new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.trash,
})
);
this.addAchievementReceiver(entity);
}
}

View File

@ -251,16 +251,6 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
return this.shapeKeyToDefinition[id];
}
this.shapeKeyToDefinition[id] = definition;
this.root.signals.bulkAchievementCheck.dispatch(
ACHIEVEMENTS.logoBefore18, definition,
ACHIEVEMENTS.oldLevel17, definition,
ACHIEVEMENTS.produceLogo, definition,
ACHIEVEMENTS.produceMsLogo, definition,
ACHIEVEMENTS.produceRocket, definition,
ACHIEVEMENTS.stack4Layers, definition
);
// logger.log("Registered shape with key (2)", id);
return definition;
}

View File

@ -275,9 +275,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
// It's a storage
if (storageComp.canAcceptItem(item)) {
storageComp.takeItem(item);
this.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.storeShape, storageComp);
return true;
}

View File

@ -1,11 +1,14 @@
/* typehints:start */
import { Application } from "../application";
import { BaseItem } from "./base_item";
import { StorageComponent } from "../game/components/storage";
import { Entity } from "../game/entity";
import { GameRoot } from "../game/root";
import { ShapeDefinition } from "../game/shape_definition";
/* typehints:end */
import { enumAnalyticsDataSource } from "../game/production_analytics";
export const ACHIEVEMENTS = {
belt500Tiles: "belt500Tiles",
blueprint100k: "blueprint100k",
@ -32,25 +35,41 @@ export const ACHIEVEMENTS = {
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 WIRE_LAYER = "wires";
const HOUR_1 = 3600; // Seconds
const HOUR_10 = HOUR_1 * 10;
const HOUR_20 = HOUR_1 * 20;
const SHAPE_BLUEPRINT = "CbCbCbRb:CwCwCwCw";
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 SHAPE_BP = "CbCbCbRb:CwCwCwCw";
const SHAPE_LOGO = "RuCw--Cw:----Ru--";
const SHAPE_MS_LOGO = "RgRyRbRr";
const SHAPE_ROCKET = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
const SHAPE_OLD_LEVEL_17 = "WrRgWrRg:CwCrCwCr:SgSgSgSg";
const WIRE_LAYER = "wires";
export class AchievementProviderInterface {
/** @param {Application} app */
@ -167,6 +186,7 @@ export class AchievementCollection {
this.createAndSet(ACHIEVEMENTS.logoBefore18, {
isRelevant: this.isLogoBefore18Relevant,
isValid: this.isLogoBefore18Valid,
signal: "itemProduced"
});
this.createAndSet(ACHIEVEMENTS.mapMarkers15, {
isRelevant: this.isMapMarkers15Relevant,
@ -194,8 +214,12 @@ export class AchievementCollection {
this.createAndSet(ACHIEVEMENTS.produceRocket, this.createShapeOptions(SHAPE_ROCKET));
this.createAndSet(ACHIEVEMENTS.produceMsLogo, this.createShapeOptions(SHAPE_MS_LOGO));
this.createAndSet(ACHIEVEMENTS.rotateShape);
this.createAndSet(ACHIEVEMENTS.speedrunBp30, this.createSpeedOptions(12, MINUTE_30));
this.createAndSet(ACHIEVEMENTS.speedrunBp60, this.createSpeedOptions(12, MINUTE_60));
this.createAndSet(ACHIEVEMENTS.speedrunBp120, this.createSpeedOptions(12, MINUTE_120));
this.createAndSet(ACHIEVEMENTS.stack4Layers, {
isValid: this.isStack4LayersValid,
signal: "itemProduced",
});
this.createAndSet(ACHIEVEMENTS.stackShape);
this.createAndSet(ACHIEVEMENTS.store100Unique, {
@ -205,7 +229,16 @@ export class AchievementCollection {
});
this.createAndSet(ACHIEVEMENTS.storeShape, {
isValid: this.isStoreShapeValid,
signal: "entityGotNewComponent",
});
this.createAndSet(ACHIEVEMENTS.throughputBp25, this.createRateOptions(SHAPE_BP, 25));
this.createAndSet(ACHIEVEMENTS.throughputBp50, this.createRateOptions(SHAPE_BP, 50));
this.createAndSet(ACHIEVEMENTS.throughputLogo25, this.createRateOptions(SHAPE_LOGO, 25));
this.createAndSet(ACHIEVEMENTS.throughputLogo50, this.createRateOptions(SHAPE_LOGO, 50));
this.createAndSet(ACHIEVEMENTS.throughputRocket10, this.createRateOptions(SHAPE_ROCKET, 25));
this.createAndSet(ACHIEVEMENTS.throughputRocket20, this.createRateOptions(SHAPE_ROCKET, 50));
this.createAndSet(ACHIEVEMENTS.trash1000, {
init: this.initTrash1000,
isValid: this.isTrash1000Valid,
});
this.createAndSet(ACHIEVEMENTS.unlockWires, this.createLevelOptions(20));
this.createAndSet(ACHIEVEMENTS.upgradesTier5, this.createUpgradeOptions(5));
@ -219,6 +252,10 @@ export class AchievementCollection {
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;
@ -251,6 +288,10 @@ export class AchievementCollection {
achievement.activate = this.activate;
if (options.init) {
achievement.init = options.init.bind(this, achievement);
}
if (options.isValid) {
achievement.isValid = options.isValid.bind(this);
}
@ -274,16 +315,16 @@ export class AchievementCollection {
/**
* @param {string} key - Maps to an Achievement
* @param {*[]} [arguments] - Additional arguments received from signal dispatches
* @param {?*} data - Data received from signal dispatches for validation
*/
unlock(key) {
unlock(key, data) {
if (!this.map.has(key)) {
return;
}
const achievement = this.map.get(key);
if (!achievement.isValid(...arguments)) {
if (!achievement.isValid(data, achievement.state)) {
return;
}
@ -347,17 +388,46 @@ export class AchievementCollection {
return true;
}
/**
* @param {BaseItem} 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: (key, currentLevel) => currentLevel === level,
isValid: (currentLevel) => currentLevel === level,
signal: "storyGoalCompleted",
};
}
createRateOptions(shape, rate) {
return {
isValid: () => {
return this.root.productionAnalytics.getCurrentShapeRate(
enumAnalyticsDataSource.produced,
this.root.shapeDefinitionMgr.getShapeFromShortKey(shape)
) >= rate;
}
};
}
createShapeOptions(shape) {
return {
isValid: (key, definition) => definition.cachedHash === shape,
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",
};
}
@ -376,62 +446,39 @@ export class AchievementCollection {
};
}
/**
* @param {string} key
* @param {Entity} entity
* @returns {boolean}
*/
isBelt500TilesValid(key, entity) {
/** @param {Entity} entity @returns {boolean} */
isBelt500TilesValid(entity) {
return entity.components.Belt && entity.components.Belt.assignedPath.totalLength >= 500;
}
/**
* @param {string} key
* @param {ShapeDefinition} definition
* @returns {boolean}
*/
isBlueprint100kValid(key, definition) {
/** @param {ShapeDefinition} definition @returns {boolean} */
isBlueprint100kValid(definition) {
return (
definition.cachedHash === SHAPE_BLUEPRINT &&
this.root.hubGoals.storedShapes[SHAPE_BLUEPRINT] >= 100000
definition.cachedHash === SHAPE_BP &&
this.root.hubGoals.storedShapes[SHAPE_BP] >= 100000
);
}
/**
* @param {string} key
* @param {ShapeDefinition} definition
* @returns {boolean}
*/
isBlueprint1mValid(key, definition) {
/** @param {ShapeDefinition} definition @returns {boolean} */
isBlueprint1mValid(definition) {
return (
definition.cachedHash === SHAPE_BLUEPRINT &&
this.root.hubGoals.storedShapes[SHAPE_BLUEPRINT] >= 1000000
definition.cachedHash === SHAPE_BP &&
this.root.hubGoals.storedShapes[SHAPE_BP] >= 1000000
);
}
/**
* @param {string} key
* @returns {boolean}
*/
isDarkModeValid(key) {
/** @returns {boolean} */
isDarkModeValid() {
return this.root.app.settings.currentData.settings.theme === DARK_MODE;
}
/**
* @param {string} key
* @param {number} count - The count of selected entities destroyed
* @returns {boolean}
*/
isDestroy1000Valid(key, count) {
/** @param {number} count @returns {boolean} */
isDestroy1000Valid(count) {
return count >= 1000;
}
/**
* @param {string} key
* @param {ShapeDefinition} definition
* @returns {boolean}
*/
isIrrelevantShapeValid(key, definition) {
/** @param {ShapeDefinition} definition @returns {boolean} */
isIrrelevantShapeValid(definition) {
if (definition.cachedHash === this.root.hubGoals.currentGoal.definition.cachedHash) {
return false;
}
@ -456,13 +503,9 @@ export class AchievementCollection {
return this.root.hubGoals.level < 18;
}
/**
* @param {string} key
* @param {ShapeDefinition} definition
* @returns {boolean}
*/
isLogoBefore18Valid(key, definition) {
return this.root.hubGoals.level < 18 && definition.cachedHash === SHAPE_LOGO;
/** @param {BaseItem} item @returns {boolean} */
isLogoBefore18Valid(item) {
return this.root.hubGoals.level < 18 && this.isShape(item, SHAPE_LOGO);
}
/** @returns {boolean} */
@ -470,30 +513,18 @@ export class AchievementCollection {
return this.root.hud.parts.waypoints.waypoints.length < 16; // 16 - HUB
}
/**
* @param {string} key
* @param {number} count - Count of map markers excluding HUB
* @returns {boolean}
*/
isMapMarkers15Valid(key, count) {
/** @param {number} count @returns {boolean} */
isMapMarkers15Valid(count) {
return count === 15;
}
/**
* @param {string} key
* @param {string} currentLayer
* @returns {boolean}
*/
isOpenWiresValid(key, currentLayer) {
/** @param {string} currentLayer @returns {boolean} */
isOpenWiresValid(currentLayer) {
return currentLayer === WIRE_LAYER;
}
/**
* @param {string} key
* @param {Entity} entity
* @returns {boolean}
*/
isPlace5000WiresValid(key, entity) {
/** @param {Entity} entity @returns {boolean} */
isPlace5000WiresValid(entity) {
return (
entity.components.Wire &&
entity.registered &&
@ -501,31 +532,19 @@ export class AchievementCollection {
);
}
/**
* @param {string} key
* @param {number} count
* @returns {boolean}
*/
isPlaceBlueprintValid(key, count) {
/** @param {number} count @returns {boolean} */
isPlaceBlueprintValid(count) {
return count != 0;
}
/**
* @param {string} key
* @param {number} count
* @returns {boolean}
*/
isPlaceBp1000Valid(key, count) {
/** @param {number} count @returns {boolean} */
isPlaceBp1000Valid(count) {
return count >= 1000;
}
/**
* @param {string} key
* @param {ShapeDefinition} definition
* @returns {boolean}
*/
isStack4LayersValid(key, definition) {
return definition.layers.length === 4;
/** @param {string} key @param {BaseItem} item @returns {boolean} */
isStack4LayersValid(item) {
return item.getItemType() === ITEM_SHAPE && item.definition.layers.length === 4;
}
/** @returns {boolean} */
@ -533,20 +552,43 @@ export class AchievementCollection {
return Object.keys(this.root.hubGoals.storedShapes).length < 100;
}
/**
* @param {string} key
* @returns {boolean}
*/
isStore100UniqueValid(key) {
/** @returns {boolean} */
isStore100UniqueValid() {
return Object.keys(this.root.hubGoals.storedShapes).length === 100;
}
/** @param {StorageComponent} storage @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(achievement) {
// get state from root
console.log(this.root.savegame.currentData.dump);
achievement.state = achievement.state || {
count: 0
};
}
/**
* @param {string} key
* @param {StorageComponent} storage
* @returns {boolean}
*/
isStoreShapeValid(key, storage) {
return storage.storedCount >= 1;
* @params {number} count
* @params {object} state - The achievement's current state
* @returns {boolean} */
isTrash1000Valid(count, state) {
state.count += count;
return state.count >= 1000;
}
}

View File

@ -35,10 +35,20 @@ const ACHIEVEMENT_IDS = {
[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",

View File

@ -0,0 +1,26 @@
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;
}
}
}

View File

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}