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

Ingame achievements (+ dev ingame)

This commit is contained in:
DJ1TJOO 2021-03-12 12:40:56 +01:00
parent d887439966
commit 571daaa7dc
64 changed files with 8240 additions and 7682 deletions

View File

@ -43,7 +43,9 @@ function listen () {
}
ipcMain.handle("steam:get-achievement-names", getAchievementNames);
ipcMain.handle("steam:get-achievement", getAchievement);
ipcMain.handle("steam:activate-achievement", activateAchievement);
ipcMain.handle("steam:deactivate-achievement", deactivateAchievement);
}
function isInitialized(event) {
@ -61,9 +63,21 @@ function getAchievementNames(event) {
});
}
function getAchievement(event, id) {
return new Promise((resolve, reject) => {
greenworks.getAchievement(id, () => resolve(), err => reject(err))
});
}
function activateAchievement(event, id) {
return new Promise((resolve, reject) => {
greenworks.activateAchievement(id, () => resolve(), err => reject(err))
greenworks.activateAchievement(id, (is_achieved) => resolve(is_achieved), err => reject(err))
});
}
function deactivateAchievement(event, id) {
return new Promise((resolve, reject) => {
greenworks.clearAchievement(id, () => resolve(), err => reject(err))
});
}

View File

@ -13051,16 +13051,11 @@ yaml-loader@^0.6.0:
loader-utils "^1.4.0"
yaml "^1.8.3"
yaml@^1.10.0:
yaml@^1.10.0, yaml@^1.8.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
yaml@^1.8.3:
version "1.10.0"
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz"
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
yargs-parser@5.0.0-security.0:
version "5.0.0-security.0"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz"

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
res/ui/achievements/mam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,94 @@
#ingame_HUD_Achievements {
.content {
@include S(padding-right, 10px);
display: flex;
flex-direction: column;
@include S(width, 500px);
.achievement {
display: grid;
grid-template-columns: auto 1fr auto;
background: #eee;
@include S(border-radius, $globalBorderRadius);
@include S(margin-bottom, 4px);
@include S(padding, 5px, 10px);
@include S(grid-row-gap, 1px);
@include S(height, 85px);
grid-template-rows: #{D(20px)} auto;
&:last-child {
margin-bottom: 0;
}
@include DarkThemeOverride {
background: $darkModeControlsBackground;
}
.icon {
@include S(width, 70px);
@include S(height, 70px);
background: center center / 80% no-repeat;
align-self: center;
justify-self: center;
grid-column: 1;
grid-row: 1 / 4;
}
.info {
grid-column: 2;
grid-row: 2 / 3;
.title {
@include PlainText;
display: flex;
align-items: center;
flex-direction: row-reverse;
justify-content: flex-end;
color: $colorGreenBright;
}
.description {
@include PlainText;
color: lighten($colorGreenBright, 20);
align-self: start;
justify-self: end;
}
}
button.reset {
grid-column: 3;
grid-row: 2;
align-self: center;
justify-self: end;
transition: all 0.2s ease-in-out;
transition-property: background-color, opacity;
background-color: $colorGreenBright;
color: #fff;
&:hover {
background-color: lighten($colorGreenBright, 10);
}
}
&:not(.unlocked) {
button.reset {
background-color: #aaa;
cursor: default;
pointer-events: none;
opacity: 0.3;
}
.info {
.title {
color: black;
@include DarkThemeOverride {
color: #fff;
}
}
.description {
color: #aaa;
}
}
}
}
}
}

View File

@ -37,8 +37,16 @@
@include DarkThemeInvert;
&.shop {
&.achievements {
grid-column: 1;
& {
/* @load-async */
background-image: uiResource("icons/achievements.png");
}
}
&.shop {
grid-column: 2;
& {
/* @load-async */
background-image: uiResource("icons/shop.png");
@ -46,7 +54,7 @@
}
&.stats {
grid-column: 2;
grid-column: 3;
& {
/* @load-async */
background-image: uiResource("icons/statistics.png");
@ -58,7 +66,7 @@
/* @load-async */
background-image: uiResource("icons/save.png");
}
grid-column: 3;
grid-column: 4;
@include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) {
0% {
transform: scale(1, 1);
@ -92,7 +100,7 @@
}
&.settings {
grid-column: 4;
grid-column: 5;
& {
/* @load-async */
background-image: uiResource("icons/settings_menu_settings.png");

View File

@ -55,6 +55,7 @@
@import "ingame_hud/sandbox_controller";
@import "ingame_hud/standalone_advantages";
@import "ingame_hud/cat_memes";
@import "ingame_hud/achievements";
// prettier-ignore
$elements:
@ -89,6 +90,7 @@ ingame_HUD_SandboxController,
ingame_HUD_BetaOverlay,
// Dialogs
ingame_HUD_Achievements,
ingame_HUD_Shop,
ingame_HUD_Statistics,
ingame_HUD_ShapeViewer,

View File

@ -66,6 +66,20 @@ $icons: notification_saved, notification_success, notification_upgrade;
}
}
$achievements: belt500Tiles, blueprint100k, blueprint1m, completeLvl26, cutShape, darkMode, destroy1000,
irrelevantShape, level100, level50, logoBefore18, mam, mapMarkers15, noBeltUpgradesUntilBp,
noInverseRotater, oldLevel17, openWires, paintShape, place5000Wires, placeBlueprint, placeBp1000, play1h,
play10h, play20h, produceLogo, produceMsLogo, produceRocket, rotateShape, speedrunBp30, speedrunBp60,
speedrunBp120, stack4Layers, stackShape, store100Unique, storeShape, throughputBp25, throughputBp50,
throughputLogo25, throughputLogo50, throughputRocket10, throughputRocket20, trash1000, unlockWires,
upgradesTier5, upgradesTier8;
@each $achievement in $achievements {
[data-icon="achievements/#{$achievement}.png"] {
/* @load-async */
background-image: uiResource("res/ui/achievements/#{$achievement}.png") !important;
}
}
$languages: en, de, cs, da, et, es-419, fr, it, pt-BR, sv, tr, el, ru, uk, zh-TW, zh-CN, nb, mt-MT, ar, nl, vi,
th, hu, pl, ja, kor, no, pt-PT;

View File

@ -49,6 +49,7 @@ import { HUDStandaloneAdvantages } from "./parts/standalone_advantages";
import { HUDCatMemes } from "./parts/cat_memes";
import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer";
import { HUDConstantSignalEdit } from "./parts/constant_signal_edit";
import { HUDAchievements } from "./parts/achievements";
export class GameHUD {
/**
@ -84,6 +85,7 @@ export class GameHUD {
massSelector: new HUDMassSelector(this.root),
shop: new HUDShop(this.root),
statistics: new HUDStatistics(this.root),
achievements: new HUDAchievements(this.root),
waypoints: new HUDWaypoints(this.root),
wireInfo: new HUDWireInfo(this.root),
leverToggle: new HUDLeverToggle(this.root),

View File

@ -0,0 +1,118 @@
import { ClickDetector } from "../../../core/click_detector";
import { InputReceiver } from "../../../core/input_receiver";
import { formatBigNumber, getRomanNumber, makeDiv } from "../../../core/utils";
import { ACHIEVEMENTS, enum_achievement_mappings } from "../../../platform/achievement_provider";
import { T } from "../../../translations";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
export class HUDAchievements extends BaseHUDPart {
createElements(parent) {
this.background = makeDiv(parent, "ingame_HUD_Achievements", ["ingameDialog"]);
// DIALOG Inner / Wrapper
this.dialogInner = makeDiv(this.background, null, ["dialogInner"]);
this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.achievements.title);
this.closeButton = makeDiv(this.title, null, ["closeButton"]);
this.trackClicks(this.closeButton, this.close);
this.contentDiv = makeDiv(this.dialogInner, null, ["content"]);
this.achievementToElements = {};
// ACHIEVEMENTS
for (const achievementKey in ACHIEVEMENTS) {
const handle = {};
// Wrapper
handle.elem = makeDiv(this.contentDiv, null, ["achievement"]);
// Icon
handle.icon = makeDiv(handle.elem, null, ["icon"]);
handle.icon.setAttribute("data-icon", "achievements/" + achievementKey + ".png");
// Info
handle.info = makeDiv(handle.elem, null, ["info"]);
// Title
const title = makeDiv(handle.info, null, ["title"], T.achievements[achievementKey].title);
// Description
handle.elemDescription = makeDiv(
handle.info,
null,
["description"],
T.achievements[achievementKey].description
);
// Reset button
handle.resetButton = document.createElement("button");
handle.resetButton.classList.add("reset", "styledButton");
handle.resetButton.innerText = T.ingame.achievements.buttonReset;
handle.elem.appendChild(handle.resetButton);
this.trackClicks(handle.resetButton, () => {
this.root.achievementProxy.provider.collection.lock(
achievementKey,
enum_achievement_mappings[ACHIEVEMENTS[achievementKey]]
);
});
// Assign handle
this.achievementToElements[achievementKey] = handle;
}
}
renderStatus() {
for (const achievementKey in this.achievementToElements) {
const handle = this.achievementToElements[achievementKey];
if (!this.root.achievementProxy.provider.collection.map.get(ACHIEVEMENTS[achievementKey])) {
if (!handle.elem.classList.contains("unlocked")) handle.elem.classList.add("unlocked");
} else if (handle.elem.classList.contains("unlocked")) handle.elem.classList.remove("unlocked");
}
}
initialize() {
this.domAttach = new DynamicDomAttach(this.root, this.background, {
attachClass: "visible",
});
this.inputReciever = new InputReceiver("achievements");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuClose).add(this.close, this);
this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuOpenAchievements).add(this.close, this);
this.close();
}
cleanup() {
// Cleanup detectors
for (const achievementKey in this.achievementToElements) {
const handle = this.achievementToElements[achievementKey];
}
}
show() {
this.visible = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
}
close() {
this.visible = false;
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.update();
}
update() {
this.domAttach.update(this.visible);
if (this.visible) {
this.renderStatus();
}
}
isBlockingOverlay() {
return this.visible;
}
}

View File

@ -33,6 +33,13 @@ export class HUDGameMenu extends BaseHUDPart {
visible: () =>
!this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3,
},
{
id: "achievements",
label: "Achievements",
handler: () => this.root.hud.parts.achievements.show(),
keybinding: KEYMAPPINGS.ingame.menuOpenAchievements,
visible: () => true,
},
];
/** @type {Array<{

View File

@ -25,6 +25,7 @@ export const KEYMAPPINGS = {
ingame: {
menuOpenShop: { keyCode: key("F") },
menuOpenStats: { keyCode: key("G") },
menuOpenAchievements: { keyCode: key("H") },
menuClose: { keyCode: key("Q") },
toggleHud: { keyCode: 113 }, // F2

View File

@ -183,6 +183,7 @@ export class GameRoot {
// Called with an achievement key and necessary args to validate it can be unlocked.
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()),
achievementCompleted: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
};
// RNG's

View File

@ -58,6 +58,214 @@ export const ACHIEVEMENTS = {
upgradesTier8: "upgradesTier8",
};
export const enum_achievement_mappings = {
[ACHIEVEMENTS.belt500Tiles]: function () {
return {
isValid: this.isBelt500TilesValid,
signal: "entityAdded",
};
},
[ACHIEVEMENTS.blueprint100k]: function () {
// @ts-ignore
return this.createBlueprintOptions(100000);
},
[ACHIEVEMENTS.blueprint1m]: function () {
// @ts-ignore
return this.createBlueprintOptions(1000000);
},
[ACHIEVEMENTS.completeLvl26]: function () {
// @ts-ignore
return this.createLevelOptions(26);
},
[ACHIEVEMENTS.cutShape]: function () {
return {};
},
[ACHIEVEMENTS.darkMode]: function () {
return {
isValid: this.isDarkModeValid,
};
},
[ACHIEVEMENTS.destroy1000]: function () {
return {
isValid: this.isDestroy1000Valid,
};
},
[ACHIEVEMENTS.irrelevantShape]: function () {
return {
isValid: this.isIrrelevantShapeValid,
signal: "shapeDelivered",
};
},
[ACHIEVEMENTS.level100]: function () {
// @ts-ignore
return this.createLevelOptions(100);
},
[ACHIEVEMENTS.level50]: function () {
// @ts-ignore
return this.createLevelOptions(50);
},
[ACHIEVEMENTS.logoBefore18]: function () {
return {
isValid: this.isLogoBefore18Valid,
signal: "itemProduced",
};
},
[ACHIEVEMENTS.mam]: function () {
return {
isValid: this.isMamValid,
};
},
[ACHIEVEMENTS.mapMarkers15]: function () {
return {
isValid: this.isMapMarkers15Valid,
};
},
[ACHIEVEMENTS.noBeltUpgradesUntilBp]: function () {
return {
isValid: this.isNoBeltUpgradesUntilBpValid,
signal: "storyGoalCompleted",
};
},
[ACHIEVEMENTS.noInverseRotater]: function () {
return {
init: this.initNoInverseRotater,
isValid: this.isNoInverseRotaterValid,
signal: "storyGoalCompleted",
};
},
[ACHIEVEMENTS.oldLevel17]: function () {
// @ts-ignore
return this.createShapeOptions(SHAPE_OLD_LEVEL_17);
},
[ACHIEVEMENTS.openWires]: function () {
return {
isValid: this.isOpenWiresValid,
signal: "editModeChanged",
};
},
[ACHIEVEMENTS.paintShape]: function () {
return {};
},
[ACHIEVEMENTS.place5000Wires]: function () {
return {
isValid: this.isPlace5000WiresValid,
};
},
[ACHIEVEMENTS.placeBlueprint]: function () {
return {
isValid: this.isPlaceBlueprintValid,
};
},
[ACHIEVEMENTS.placeBp1000]: function () {
return {
isValid: this.isPlaceBp1000Valid,
};
},
[ACHIEVEMENTS.play1h]: function () {
// @ts-ignore
return this.createTimeOptions(HOUR_1);
},
[ACHIEVEMENTS.play10h]: function () {
// @ts-ignore
return this.createTimeOptions(HOUR_10);
},
[ACHIEVEMENTS.play20h]: function () {
// @ts-ignore
return this.createTimeOptions(HOUR_20);
},
[ACHIEVEMENTS.produceLogo]: function () {
// @ts-ignore
return this.createShapeOptions(SHAPE_LOGO);
},
[ACHIEVEMENTS.produceRocket]: function () {
// @ts-ignore
return this.createShapeOptions(SHAPE_ROCKET);
},
[ACHIEVEMENTS.produceMsLogo]: function () {
// @ts-ignore
return this.createShapeOptions(SHAPE_MS_LOGO);
},
[ACHIEVEMENTS.rotateShape]: function () {
return {};
},
[ACHIEVEMENTS.speedrunBp30]: function () {
// @ts-ignore
return this.createSpeedOptions(12, MINUTE_30);
},
[ACHIEVEMENTS.speedrunBp60]: function () {
// @ts-ignore
return this.createSpeedOptions(12, MINUTE_60);
},
[ACHIEVEMENTS.speedrunBp120]: function () {
// @ts-ignore
return this.createSpeedOptions(12, MINUTE_120);
},
[ACHIEVEMENTS.stack4Layers]: function () {
return {
isValid: this.isStack4LayersValid,
signal: "itemProduced",
};
},
[ACHIEVEMENTS.stackShape]: function () {
return {};
},
[ACHIEVEMENTS.store100Unique]: function () {
return {
init: this.initStore100Unique,
isValid: this.isStore100UniqueValid,
signal: "shapeDelivered",
};
},
[ACHIEVEMENTS.storeShape]: function () {
return {
init: this.initStoreShape,
isValid: this.isStoreShapeValid,
};
},
[ACHIEVEMENTS.throughputBp25]: function () {
// @ts-ignore
return this.createRateOptions(SHAPE_BP, 25);
},
[ACHIEVEMENTS.throughputBp50]: function () {
// @ts-ignore
return this.createRateOptions(SHAPE_BP, 50);
},
[ACHIEVEMENTS.throughputLogo25]: function () {
// @ts-ignore
return this.createRateOptions(SHAPE_LOGO, 25);
},
[ACHIEVEMENTS.throughputLogo50]: function () {
// @ts-ignore
return this.createRateOptions(SHAPE_LOGO, 50);
},
[ACHIEVEMENTS.throughputRocket10]: function () {
// @ts-ignore
return this.createRateOptions(SHAPE_ROCKET, 25);
},
[ACHIEVEMENTS.throughputRocket20]: function () {
// @ts-ignore
return this.createRateOptions(SHAPE_ROCKET, 50);
},
[ACHIEVEMENTS.trash1000]: function () {
return {
init: this.initTrash1000,
isValid: this.isTrash1000Valid,
};
},
[ACHIEVEMENTS.unlockWires]: function () {
// @ts-ignore
return this.createLevelOptions(20);
},
[ACHIEVEMENTS.upgradesTier5]: function () {
// @ts-ignore
return this.createUpgradeOptions(5);
},
[ACHIEVEMENTS.upgradesTier8]: function () {
// @ts-ignore
return this.createUpgradeOptions(8);
},
};
/** @type {keyof typeof THEMES} */
const DARK_MODE = "dark";
@ -87,6 +295,7 @@ export class AchievementProviderInterface {
/** @param {Application} app */
constructor(app) {
this.app = app;
this.storage = null;
}
/**
@ -162,102 +371,16 @@ export class Achievement {
export class AchievementCollection {
/**
* @param {function} activate - Resolves when provider activation is complete
* @param {function} deactivate - Resolves when provider deactivation is complete
*/
constructor(activate) {
constructor(activate, deactivate) {
this.map = new Map();
this.activate = activate;
this.deactivate = deactivate;
this.add(ACHIEVEMENTS.belt500Tiles, {
isValid: this.isBelt500TilesValid,
signal: "entityAdded",
});
this.add(ACHIEVEMENTS.blueprint100k, this.createBlueprintOptions(100000));
this.add(ACHIEVEMENTS.blueprint1m, this.createBlueprintOptions(1000000));
this.add(ACHIEVEMENTS.completeLvl26, this.createLevelOptions(26));
this.add(ACHIEVEMENTS.cutShape);
this.add(ACHIEVEMENTS.darkMode, {
isValid: this.isDarkModeValid,
});
this.add(ACHIEVEMENTS.destroy1000, {
isValid: this.isDestroy1000Valid,
});
this.add(ACHIEVEMENTS.irrelevantShape, {
isValid: this.isIrrelevantShapeValid,
signal: "shapeDelivered",
});
this.add(ACHIEVEMENTS.level100, this.createLevelOptions(100));
this.add(ACHIEVEMENTS.level50, this.createLevelOptions(50));
this.add(ACHIEVEMENTS.logoBefore18, {
isValid: this.isLogoBefore18Valid,
signal: "itemProduced",
});
this.add(ACHIEVEMENTS.mam, {
isValid: this.isMamValid,
});
this.add(ACHIEVEMENTS.mapMarkers15, {
isValid: this.isMapMarkers15Valid,
});
this.add(ACHIEVEMENTS.noBeltUpgradesUntilBp, {
isValid: this.isNoBeltUpgradesUntilBpValid,
signal: "storyGoalCompleted",
});
this.add(ACHIEVEMENTS.noInverseRotater, {
init: this.initNoInverseRotater,
isValid: this.isNoInverseRotaterValid,
signal: "storyGoalCompleted",
});
this.add(ACHIEVEMENTS.oldLevel17, this.createShapeOptions(SHAPE_OLD_LEVEL_17));
this.add(ACHIEVEMENTS.openWires, {
isValid: this.isOpenWiresValid,
signal: "editModeChanged",
});
this.add(ACHIEVEMENTS.paintShape);
this.add(ACHIEVEMENTS.place5000Wires, {
isValid: this.isPlace5000WiresValid,
});
this.add(ACHIEVEMENTS.placeBlueprint, {
isValid: this.isPlaceBlueprintValid,
});
this.add(ACHIEVEMENTS.placeBp1000, {
isValid: this.isPlaceBp1000Valid,
});
this.add(ACHIEVEMENTS.play1h, this.createTimeOptions(HOUR_1));
this.add(ACHIEVEMENTS.play10h, this.createTimeOptions(HOUR_10));
this.add(ACHIEVEMENTS.play20h, this.createTimeOptions(HOUR_20));
this.add(ACHIEVEMENTS.produceLogo, this.createShapeOptions(SHAPE_LOGO));
this.add(ACHIEVEMENTS.produceRocket, this.createShapeOptions(SHAPE_ROCKET));
this.add(ACHIEVEMENTS.produceMsLogo, this.createShapeOptions(SHAPE_MS_LOGO));
this.add(ACHIEVEMENTS.rotateShape);
this.add(ACHIEVEMENTS.speedrunBp30, this.createSpeedOptions(12, MINUTE_30));
this.add(ACHIEVEMENTS.speedrunBp60, this.createSpeedOptions(12, MINUTE_60));
this.add(ACHIEVEMENTS.speedrunBp120, this.createSpeedOptions(12, MINUTE_120));
this.add(ACHIEVEMENTS.stack4Layers, {
isValid: this.isStack4LayersValid,
signal: "itemProduced",
});
this.add(ACHIEVEMENTS.stackShape);
this.add(ACHIEVEMENTS.store100Unique, {
init: this.initStore100Unique,
isValid: this.isStore100UniqueValid,
signal: "shapeDelivered",
});
this.add(ACHIEVEMENTS.storeShape, {
init: this.initStoreShape,
isValid: this.isStoreShapeValid,
});
this.add(ACHIEVEMENTS.throughputBp25, this.createRateOptions(SHAPE_BP, 25));
this.add(ACHIEVEMENTS.throughputBp50, this.createRateOptions(SHAPE_BP, 50));
this.add(ACHIEVEMENTS.throughputLogo25, this.createRateOptions(SHAPE_LOGO, 25));
this.add(ACHIEVEMENTS.throughputLogo50, this.createRateOptions(SHAPE_LOGO, 50));
this.add(ACHIEVEMENTS.throughputRocket10, this.createRateOptions(SHAPE_ROCKET, 25));
this.add(ACHIEVEMENTS.throughputRocket20, this.createRateOptions(SHAPE_ROCKET, 50));
this.add(ACHIEVEMENTS.trash1000, {
init: this.initTrash1000,
isValid: this.isTrash1000Valid,
});
this.add(ACHIEVEMENTS.unlockWires, this.createLevelOptions(20));
this.add(ACHIEVEMENTS.upgradesTier5, this.createUpgradeOptions(5));
this.add(ACHIEVEMENTS.upgradesTier8, this.createUpgradeOptions(8));
for (const key in ACHIEVEMENTS) {
this.add(ACHIEVEMENTS[key], enum_achievement_mappings[ACHIEVEMENTS[key]].bind(this)());
}
}
/** @param {GameRoot} root */
@ -324,14 +447,14 @@ export class AchievementCollection {
* @param {string} key - Maps to an Achievement
* @param {any} data - Data received from signal dispatches for validation
*/
unlock(key, data) {
unlock(key, data, force = false) {
if (!this.map.has(key)) {
return;
}
const achievement = this.map.get(key);
if (!achievement.isValid(data)) {
if (!force && !achievement.isValid(data)) {
return;
}
@ -339,12 +462,21 @@ export class AchievementCollection {
.unlock()
.then(() => {
this.onActivate(null, key);
this.root.signals.achievementCompleted.dispatch(key, data);
})
.catch(err => {
this.onActivate(err, key);
});
}
/**
* @param {string} key - Maps to an Achievement
*/
lock(key, options) {
this.add(key, options);
this.deactivate(key);
}
/**
* Cleans up after achievement activation attempt with the provider. Could
* utilize err to retry some number of times if needed.

View File

@ -0,0 +1,168 @@
/* typehints:start */
import { Application } from "../../application";
import { GameRoot } from "../../game/root";
/* typehints:end */
import { createLogger } from "../../core/logging";
import { AchievementCollection, AchievementProviderInterface, ACHIEVEMENTS } from "../achievement_provider";
import { ReadWriteProxy } from "../../core/read_write_proxy";
import { ExplainedResult } from "../../core/explained_result";
const logger = createLogger("achievements/browser");
export class BrowserAchievementStorage extends ReadWriteProxy {
constructor(app) {
super(app, "app_achievements.bin");
}
initialize() {
return this.readAsync().then(() => {
console.log(this.currentData);
return Promise.resolve();
});
}
save() {
return this.writeAsync();
}
/** @returns {ExplainedResult} */
verify(data) {
if (!data.unlocked) {
return ExplainedResult.bad("missing key 'unlocked'");
}
if (!Array.isArray(data.unlocked)) {
return ExplainedResult.bad("Bad array 'unlocked'");
}
for (let i = 0; i < data.unlocked.length; i++) {
const achievement = data.unlocked[i];
let exists = false;
for (const key in ACHIEVEMENTS) {
if (ACHIEVEMENTS[key] === achievement) {
exists = true;
break;
}
}
}
return ExplainedResult.good();
}
// Should return the default data
getDefaultData() {
return {
version: this.getCurrentVersion(),
unlocked: [],
};
}
// Should return the current version as an integer
getCurrentVersion() {
return 0;
}
// Should migrate the data (Modify in place)
/** @returns {ExplainedResult} */
migrate(data) {
return ExplainedResult.good();
}
}
export class BrowserAchievementProvider extends AchievementProviderInterface {
/** @param {Application} app */
constructor(app) {
super(app);
this.initialized = false;
this.collection = new AchievementCollection(this.activate.bind(this), this.deactivate.bind(this));
this.storage = new BrowserAchievementStorage(app);
if (G_IS_DEV) {
for (let key in ACHIEVEMENTS) {
assert(this.collection.map.has(key), "Key not found in collection: " + key);
}
}
logger.log("Collection created with", this.collection.map.size, "achievements");
}
/** @returns {boolean} */
hasAchievements() {
return true;
}
/**
* @param {GameRoot} root
* @returns {Promise<void>}
*/
onLoad(root) {
this.root = root;
try {
this.collection = new AchievementCollection(this.activate.bind(this), this.deactivate.bind(this));
this.collection.initialize(root);
//Unlock already unlocked
for (let i = 0; i < this.storage.currentData.unlocked.length; i++) {
const achievement = this.storage.currentData.unlocked[i];
this.collection.unlock(achievement, null, true);
}
logger.log("Initialized", this.collection.map.size, "relevant achievements");
return Promise.resolve();
} catch (err) {
logger.error("Failed to initialize the collection");
return Promise.reject(err);
}
}
/** @returns {Promise<void>} */
initialize() {
return this.storage.initialize();
}
/**
* @param {string} key
* @returns {Promise<void>}
*/
activate(key) {
return Promise.resolve()
.then(() => {
if (!this.storage.currentData.unlocked.includes(key))
this.storage.currentData.unlocked.push(key);
return Promise.resolve();
})
.then(() => this.storage.save())
.then(() => {
logger.log("Achievement activated:", key);
})
.catch(err => {
logger.error("Failed to activate achievement:", key, err);
throw err;
});
}
/**
* @param {string} key
* @returns {Promise<void>}
*/
deactivate(key) {
return Promise.resolve()
.then(() => {
if (this.storage.currentData.unlocked.includes(key))
this.storage.currentData.unlocked.splice(
this.storage.currentData.unlocked.indexOf(key),
1
);
return Promise.resolve();
})
.then(() => this.storage.save())
.then(() => {
logger.log("Achievement deactivated:", key);
})
.catch(err => {
logger.error("Failed to deactivate achievement:", key, err);
throw err;
});
}
}

View File

@ -20,4 +20,8 @@ export class NoAchievementProvider extends AchievementProviderInterface {
activate() {
return Promise.resolve();
}
deactivate() {
return Promise.resolve();
}
}

View File

@ -4,8 +4,8 @@ import { queryParamOptions } from "../../core/query_parameters";
import { clamp } from "../../core/utils";
import { GamedistributionAdProvider } from "../ad_providers/gamedistribution";
import { NoAdProvider } from "../ad_providers/no_ad_provider";
import { SteamAchievementProvider } from "../electron/steam_achievement_provider";
import { PlatformWrapperInterface } from "../wrapper";
import { BrowserAchievementProvider } from "./browser_achievement_provider";
import { NoAchievementProvider } from "./no_achievement_provider";
import { StorageImplBrowser } from "./storage";
import { StorageImplBrowserIndexedDB } from "./storage_indexed_db";
@ -200,8 +200,8 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
}
initializeAchievementProvider() {
if (G_IS_DEV && globalConfig.debug.testAchievements) {
this.app.achievementProvider = new SteamAchievementProvider(this.app);
if (G_IS_DEV) {
this.app.achievementProvider = new BrowserAchievementProvider(this.app);
return this.app.achievementProvider.initialize().catch(err => {
logger.error("Failed to initialize achievement provider, disabling:", err);

View File

@ -63,7 +63,7 @@ export class SteamAchievementProvider extends AchievementProviderInterface {
super(app);
this.initialized = false;
this.collection = new AchievementCollection(this.activate.bind(this));
this.collection = new AchievementCollection(this.activate.bind(this), this.deactivate.bind(this));
if (G_IS_DEV) {
for (let key in ACHIEVEMENT_IDS) {
@ -87,11 +87,25 @@ export class SteamAchievementProvider extends AchievementProviderInterface {
this.root = root;
try {
this.collection = new AchievementCollection(this.activate.bind(this));
this.collection = new AchievementCollection(this.activate.bind(this), this.deactivate.bind(this));
this.collection.initialize(root);
logger.log("Initialized", this.collection.map.size, "relevant achievements");
return Promise.resolve();
let promise = Promise.resolve();
//Unlock already unlocked
for (const id in ACHIEVEMENT_IDS) {
promise.then(() =>
this.ipc.invoke("steam:get-achievement", ACHIEVEMENT_IDS[id]).then(is_achieved => {
if (is_achieved) this.collection.unlock(id, null, true);
return Promise.resolve();
})
);
}
return promise.then(() => {
logger.log("Initialized", this.collection.map.size, "relevant achievements");
return Promise.resolve();
});
} catch (err) {
logger.error("Failed to initialize the collection");
return Promise.reject(err);
@ -140,4 +154,27 @@ export class SteamAchievementProvider extends AchievementProviderInterface {
throw err;
});
}
/**
* @param {string} key
* @returns {Promise<void>}
*/
deactivate(key) {
let promise;
if (!this.initialized) {
promise = Promise.resolve();
} else {
promise = this.ipc.invoke("steam:deactivate-achievement", ACHIEVEMENT_IDS[key]);
}
return promise
.then(() => {
logger.log("Achievement deactivated:", key);
})
.catch(err => {
logger.error("Failed to deactivate achievement:", key, err);
throw err;
});
}
}

View File

@ -355,6 +355,11 @@ ingame:
minute: <shapes> / m
hour: <shapes> / h
# The "Achievements window"
achievements:
title: Achievements
buttonReset: Reset
# Settings menu, when you press "ESC"
settingsMenu:
playtime: Playtime
@ -492,6 +497,144 @@ shopUpgrades:
name: Mixing & Painting
description: Speed x<currentMult> → x<newMult>
# All achievements
achievements:
belt500Tiles:
title: I need trains
description: Have a belt with a length of 500 tiles
blueprint100k:
title: It's piling up
description: Have 100k blueprint shapes stored in your hub
blueprint1m:
title: I'll use it later
description: Have 1 million blueprint shapes stored in your hub
completeLvl26:
title: Freedom
description: Complete level 26, unlocking the free play mode
cutShape:
title: Cutter
description: Cut a shape
darkMode:
title: My eyes no longer hurt
description:
destroy1000:
title: Perfectionist
description: Destroy more than 1000 objects at once
irrelevantShape:
title: Oops
description: Deliver a shape which is not the goal, nor relevant for any upgrade to the hub
level100:
title: Is this the end?
description: Reach level 100
level50:
title: Can't stop
description: Reach level 50
logoBefore18:
title: A bit early?
description: Create the logo shape before level 18
mam:
title: MAM (Make Anything Machine)
description: Complete any level (after level 26) without modifying your factory
mapMarkers15:
title: GPS
description: Have 15 map markers
noBeltUpgradesUntilBp:
title: It's so slow
description: Reach & Complete level 12 without upgrading your belts
noInverseRotater:
title: King of Inefficiency
description: Use no inverse rotator until level 14
oldLevel17:
title: Memories from the past
description: Deliver WrRgWrRg:CwCrCwCr:SgSgSgSg to the hub (Old level 17 shape)
openWires:
title: The next dimension
description: Open the wires layer
paintShape:
title: Painter
description: Paint a shape
place5000Wires:
title: Computer Guy
description: Have more than 5,000 wires
placeBlueprint:
title: Now it's easy
description: Place a blueprint
placeBp1000:
title: Copy-Pasta
description: Place a blueprint with 1000 or more buildings
play1h:
title: Getting into it
description: Play one savegame for more than 1 hour
play10h:
title: It's been a long time
description: Play one savegame for more than 10 hours
play20h:
title: Addicted
description: Play one savegame for more than 20 hours
produceLogo:
title: The logo!
description: Produce the shapez.io logo
produceMsLogo:
title: I've seen that before ..
description: Deliver RgRyRbRr to the hub
produceRocket:
title: To the moon
description: Produce the rocket shape
rotateShape:
title: Rotater
description: Rotate a shape
speedrunBp30:
title: Speedrun Master
description: Reach & complete level 12 in under 30 minutes
speedrunBp60:
title: Speedrun Novice
description: Reach & complete level 12 in under 60 minutes
speedrunBp120:
title: Not an idle game
description: Reach & complete level 12 in under 120 minutes
stack4Layers:
title: Stack overflow
description: Produce a shape with 4 layers
stackShape:
title: Wait, they stack?
description: Stack a shape
store100Unique:
title: It's a mess
description: Have 100 different shapes stored in your hub
storeShape:
title: Storage
description: Store a shape in the storage
throughputBp25:
title: Efficiency 1
description: Deliver 25 blueprints shapes / second to your hub
throughputBp50:
title: Efficiency 2
description: Deliver 50 blueprints shapes / second to your hub
throughputLogo25:
title: Branding specialist 1
description: Deliver 25 logo shapes / second to your hub
throughputLogo50:
title: Branding specialist 2
description: Deliver 50 logo shapes / second to your hub
throughputRocket10:
title: Preparing to launch
description: Deliver 10 rocket shapes / second to your hub
throughputRocket20:
title: SpaceY
description: Deliver 20 rocket shapes / second to your hub
trash1000:
title: Get rid of them
description: Have 1000 shapes trashed
unlockWires:
title: Wires
description: Complete level 20, unlocking the wires layer
upgradesTier5:
title: Faster
description: Have all upgades on tier 5 or higher
upgradesTier8:
title: Even faster
description: Have all upgrades on tier 8 or higher
# Buildings and their name / description
buildings:
hub:
@ -1096,6 +1239,7 @@ keybindings:
menuOpenShop: Upgrades
menuOpenStats: Statistics
menuOpenAchievements: Achievements
menuClose: Close Menu
toggleHud: Toggle HUD

14957
yarn.lock

File diff suppressed because it is too large Load Diff