Implemented saves manager

This commit is contained in:
thepaperpilot 2021-06-20 23:29:55 -05:00
parent 69b1fff796
commit f018016477
29 changed files with 923 additions and 141 deletions

129
package-lock.json generated
View file

@ -9,11 +9,13 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0",
"portal-vue": "^2.1.7", "portal-vue": "^2.1.7",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-frag": "^1.1.5", "vue-frag": "^1.1.5",
"vue-reactive-provide": "^0.3.0", "vue-reactive-provide": "^0.3.0",
"vue-select": "^3.11.2", "vue-select": "^3.11.2",
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
"vue-textarea-autosize": "^1.1.1", "vue-textarea-autosize": "^1.1.1",
"vue-transition-expand": "^0.1.0", "vue-transition-expand": "^0.1.0",
"vue2-perfect-scrollbar": "^1.5.0", "vue2-perfect-scrollbar": "^1.5.0",
@ -28,6 +30,7 @@
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"raw-loader": "^4.0.2",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
} }
}, },
@ -8429,6 +8432,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -10812,6 +10820,58 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/raw-loader": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
"dev": true,
"dependencies": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^4.0.0 || ^5.0.0"
}
},
"node_modules/raw-loader/node_modules/loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
},
"engines": {
"node": ">=8.9.0"
}
},
"node_modules/raw-loader/node_modules/schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -11853,6 +11913,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sortablejs": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.13.0.tgz",
"integrity": "sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg=="
},
"node_modules/source-list-map": { "node_modules/source-list-map": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@ -12015,11 +12080,6 @@
"safer-buffer": "^2.0.2", "safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0" "tweetnacl": "~0.14.0"
}, },
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -13506,6 +13566,14 @@
"vue": "2.x" "vue": "2.x"
} }
}, },
"node_modules/vue-sortable": {
"version": "0.1.3",
"resolved": "git+ssh://git@github.com/Netbel/vue-sortable.git#f4d4870ace71ea59bd79252eb2ec1cf6bfb02fe7",
"license": "MIT",
"dependencies": {
"sortablejs": "^1.4.2"
}
},
"node_modules/vue-style-loader": { "node_modules/vue-style-loader": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -21535,6 +21603,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.debounce": { "lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -23516,6 +23589,40 @@
"unpipe": "1.0.0" "unpipe": "1.0.0"
} }
}, },
"raw-loader": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
"dev": true,
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
},
"dependencies": {
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"schema-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.6",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
}
}
},
"read-cache": { "read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -24397,6 +24504,11 @@
} }
} }
}, },
"sortablejs": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.13.0.tgz",
"integrity": "sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg=="
},
"source-list-map": { "source-list-map": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@ -25730,6 +25842,13 @@
"integrity": "sha512-pIOcY8ajWNSwg8Ns4eHVr5ZWwqKCSZeQRymTnlUI8i+3QiQXF6JIM4lylK6mVfbccs4S6vOyxB7zmJBpp7tDUg==", "integrity": "sha512-pIOcY8ajWNSwg8Ns4eHVr5ZWwqKCSZeQRymTnlUI8i+3QiQXF6JIM4lylK6mVfbccs4S6vOyxB7zmJBpp7tDUg==",
"requires": {} "requires": {}
}, },
"vue-sortable": {
"version": "git+ssh://git@github.com/Netbel/vue-sortable.git#f4d4870ace71ea59bd79252eb2ec1cf6bfb02fe7",
"from": "vue-sortable@github:Netbel/vue-sortable#master-fix",
"requires": {
"sortablejs": "^1.4.2"
}
},
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",

View file

@ -9,11 +9,13 @@
}, },
"dependencies": { "dependencies": {
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0",
"portal-vue": "^2.1.7", "portal-vue": "^2.1.7",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-frag": "^1.1.5", "vue-frag": "^1.1.5",
"vue-reactive-provide": "^0.3.0", "vue-reactive-provide": "^0.3.0",
"vue-select": "^3.11.2", "vue-select": "^3.11.2",
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
"vue-textarea-autosize": "^1.1.1", "vue-textarea-autosize": "^1.1.1",
"vue-transition-expand": "^0.1.0", "vue-transition-expand": "^0.1.0",
"vue2-perfect-scrollbar": "^1.5.0", "vue2-perfect-scrollbar": "^1.5.0",
@ -28,6 +30,7 @@
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"raw-loader": "^4.0.2",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
}, },
"eslintConfig": { "eslintConfig": {

View file

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>

0
saves/.placehold Normal file
View file

1
saves/safff.txt Normal file
View file

@ -0,0 +1 @@
eyJpZCI6InRtdC14LTEwNSIsIm5hbWUiOiJEZWZhdWx0IFNhZmZmZiAtIHNvbWV0aGluZyBlbHNlIiwidGFicyI6WyJtYWluIiwiYyJdLCJ0aW1lIjoxNjI0MjQ1MjYxMDg3LCJhdXRvc2F2ZSI6dHJ1ZSwib2ZmbGluZVByb2QiOnRydWUsInRpbWVQbGF5ZWQiOiIzNDQ4LjYxNTc4MTcwOTAxIiwia2VlcEdvaW5nIjpmYWxzZSwibGFzdFRlblRpY2tzIjpbMC4wNTEsMC4wNSwwLjA0OSwwLjA1LDAuMDUsMC4wNTEsMC4wNDksMC4wNSwwLjA1LDAuMDUxXSwic2hvd1RQUyI6dHJ1ZSwibXNEaXNwbGF5IjoiYWxsIiwiaGlkZUNoYWxsZW5nZXMiOmZhbHNlLCJ0aGVtZSI6InBhcGVyIiwic3VidGFicyI6e30sIm1pbmltaXplZCI6e30sIm1vZElEIjoidG10LXgiLCJtb2RWZXJzaW9uIjoiMC4wIiwicG9pbnRzIjoiMzMwMC4zNzc3NzM4NTkwNTUiLCJtYWluIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJmIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImNsaWNrYWJsZXMiOnsiMTEiOiJTdGFydCJ9LCJ1bmxvY2tlZCI6ZmFsc2UsInBvaW50cyI6IjAiLCJib29wIjpmYWxzZX0sImMiOnsidXBncmFkZXMiOlsiMTEiXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImJ1eWFibGVzIjp7IjExIjoiMCJ9LCJjaGFsbGVuZ2VzIjp7IjExIjoiMCJ9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCIsImJlc3QiOiIxIiwidG90YWwiOiIwIiwiYmVlcCI6ZmFsc2UsInRoaW5neSI6InBvaW50eSIsIm90aGVyVGhpbmd5IjoxMCwic3BlbnRPbkJ1eWFibGVzIjoiMCJ9LCJhIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbIjExIl0sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCJ9LCJnIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJoIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJzcG9vayI6eyJ1cGdyYWRlcyI6W10sImFjaGlldmVtZW50cyI6W10sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9fSwib29tcHNNYWciOjAsImxhc3RQb2ludHMiOiIzMzAwLjM3Nzc3Mzg1OTA1NSJ9

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-if="shown" :style="style" <div v-if="challenge.shown" :style="style"
:class="{ :class="{
feature: true, feature: true,
challenge: true, challenge: true,
@ -21,7 +21,6 @@
<script> <script>
import { layers } from '../../store/layers'; import { layers } from '../../store/layers';
import { player } from '../../store/proxies';
import { coerceComponent } from '../../util/vue'; import { coerceComponent } from '../../util/vue';
import './features.css'; import './features.css';
@ -36,9 +35,6 @@ export default {
challenge() { challenge() {
return layers[this.layer || this.tab.layer].challenges[this.id]; return layers[this.layer || this.tab.layer].challenges[this.id];
}, },
shown() {
return this.challenge.unlocked && !(player.hideChallenges && this.challenge.maxes);
},
style() { style() {
return [ return [
layers[this.layer || this.tab.layer].componentStyles?.challenge, layers[this.layer || this.tab.layer].componentStyles?.challenge,

View file

@ -0,0 +1,65 @@
<template>
<span class="container">
<span v-if="confirming">Are you sure?</span>
<button @click.stop="click" class="button danger" :disabled="disabled">
<span v-if="confirming">Yes</span>
<slot v-else />
</button>
<button v-if="confirming" class="button" @click.stop="cancel">No</button>
</span>
</template>
<script>
export default {
name: 'danger-button',
data() {
return {
confirming: false
}
},
props: {
disabled: Boolean
},
watch: {
confirming(newValue) {
this.$emit('confirmingChanged', newValue);
}
},
methods: {
click() {
if (this.confirming) {
this.$emit('click');
}
this.confirming = !this.confirming;
},
cancel() {
this.confirming = false;
}
}
};
</script>
<style scoped>
.container {
display: flex;
align-items: center;
}
.container > * {
margin: 0 4px;
}
.danger {
border: solid 2px var(--danger);
padding-right: 0;
}
.danger::after {
content: "!";
color: white;
background: var(--danger);
padding: 2px;
margin-left: 6px;
height: 100%;
}
</style>

View file

@ -0,0 +1,74 @@
<template>
<button @click.stop="click" class="feedback" :class="{ activated, left }"><slot /></button>
</template>
<script>
export default {
name: 'feedback-button',
data() {
return {
activated: false,
activatedTimeout: null
}
},
props: {
left: Boolean
},
methods: {
click() {
this.$emit('click');
// Give feedback to user
if (this.activatedTimeout) {
clearTimeout(this.activatedTimeout);
}
this.activated = false;
this.$nextTick(() => {
this.activated = true;
this.activatedTimeout = setTimeout(() => this.activated = false, 500);
});
}
}
};
</script>
<style scoped>
.feedback {
position: relative;
}
.feedback::after {
position: absolute;
left: calc(100% + 5px);
top: 50%;
transform: translateY(-50%);
content: '✔';
opacity: 0;
pointer-events: none;
box-shadow: inset 0 0 0 35px rgba(111,148,182,0);
text-shadow: none;
}
.feedback.left::after {
left: unset;
right: calc(100% + 5px);
}
.feedback.activated::after {
animation: feedback .5s ease-out forwards;
}
@keyframes feedback {
0% {
opacity: 1;
transform: scale3d(0.4, 0.4, 1), translateY(-50%);
}
80% {
opacity: 0.1;
}
100% {
opacity: 0;
transform: scale3d(1.2, 1.2, 1), translateY(-50%);
}
}
</style>

View file

@ -30,20 +30,14 @@ export default {
}; };
</script> </script>
<style scoped>
span {
margin: 0;
}
</style>
<style> <style>
.v-select { .v-select {
width: 50%; width: 50%;
margin: 0;
} }
.v-select .vs__dropdown-toggle { .v-select .vs__dropdown-toggle {
border-color: rgba(var(--color), .26); border-color: rgba(var(--color), .26);
margin: -1px 0;
} }
.v-select .vs__selected { .v-select .vs__selected {
@ -64,4 +58,8 @@ span {
.v-select .vs__dropdown-option { .v-select .vs__dropdown-option {
color: var(--color); color: var(--color);
} }
.v-select .vs__open-indicator {
cursor: pointer;
}
</style> </style>

View file

@ -22,11 +22,4 @@ export default {
</script> </script>
<style scoped> <style scoped>
input {
margin-right: 0;
}
.value {
margin-left: 10px;
}
</style> </style>

View file

@ -1,9 +1,13 @@
<template> <template>
<div class="field"> <form @submit.prevent="$emit('submit')">
<span class="field-title" v-if="title">{{ title }}</span> <div class="field">
<textarea-autosize v-if="textarea" :placeholder="placeholder" :value="value" @input="value => $emit('change', value)" /> <span class="field-title" v-if="title">{{ title }}</span>
<input v-else type="text" :value="value" @input="e => $emit('change', e.target.value)" :placeholder="placeholder" /> <textarea-autosize v-if="textarea" :placeholder="placeholder" :value="value" :maxHeight="maxHeight"
</div> @input="value => $emit('change', value)" ref="field" />
<input v-else type="text" :value="value" @input="e => $emit('input', e.target.value)" :placeholder="placeholder" ref="field"
:class="{ fullWidth: !title }" />
</div>
</form>
</template> </template>
<script> <script>
@ -13,12 +17,32 @@ export default {
name: 'TextField', name: 'TextField',
props: { props: {
title: String, title: String,
value: [ String, Object ], value: String,
textarea: Boolean, textarea: Boolean,
placeholder: String placeholder: String,
maxHeight: Number
},
mounted() {
this.$refs.field.focus();
} }
}; };
</script> </script>
<style scoped> <style scoped>
form {
margin: 0;
width: 100%;
}
.field > * {
margin: 0;
}
input {
width: 50%;
}
.fullWidth {
width: 100%;
}
</style> </style>

View file

@ -5,4 +5,9 @@
margin: 10px 0; margin: 10px 0;
user-select: none; user-select: none;
justify-content: space-between; justify-content: space-between;
align-items: center;
}
.field > * {
margin: 0;
} }

View file

@ -30,6 +30,8 @@ import RespecButton from './features/RespecButton';
import Upgrade from './features/Upgrade'; import Upgrade from './features/Upgrade';
import Upgrades from './features/Upgrades'; import Upgrades from './features/Upgrades';
/* fields */ /* fields */
import DangerButton from './fields/DangerButton';
import FeedbackButton from './fields/FeedbackButton';
import Select from './fields/Select'; import Select from './fields/Select';
import Slider from './fields/Slider'; import Slider from './fields/Slider';
import Text from './fields/Text'; import Text from './fields/Text';
@ -48,6 +50,8 @@ import Nav from './system/Nav';
import Options from './system/Options'; import Options from './system/Options';
import Resource from './system/Resource'; import Resource from './system/Resource';
import Row from './system/Row'; import Row from './system/Row';
import Save from './system/Save';
import SavesManager from './system/SavesManager';
import Spacer from './system/Spacer'; import Spacer from './system/Spacer';
import Sticky from './system/Sticky'; import Sticky from './system/Sticky';
import Subtab from './system/Subtab'; import Subtab from './system/Subtab';
@ -70,6 +74,7 @@ import PerfectScrollbar from 'vue2-perfect-scrollbar';
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'; import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
import VueTextareaAutosize from 'vue-textarea-autosize'; import VueTextareaAutosize from 'vue-textarea-autosize';
import PortalVue from 'portal-vue'; import PortalVue from 'portal-vue';
import Sortable from 'vue-sortable';
/* features */ /* features */
Vue.component(Achievement.name, Achievement); Vue.component(Achievement.name, Achievement);
@ -98,6 +103,8 @@ Vue.component(RespecButton.name, RespecButton);
Vue.component(Upgrade.name, Upgrade); Vue.component(Upgrade.name, Upgrade);
Vue.component(Upgrades.name, Upgrades); Vue.component(Upgrades.name, Upgrades);
/* fields */ /* fields */
Vue.component(DangerButton.name, DangerButton);
Vue.component(FeedbackButton.name, FeedbackButton);
Vue.component(Select.name, Select); Vue.component(Select.name, Select);
Vue.component(Slider.name, Slider); Vue.component(Slider.name, Slider);
Vue.component(Text.name, Text); Vue.component(Text.name, Text);
@ -116,6 +123,8 @@ Vue.component(Nav.name, Nav);
Vue.component(Options.name, Options); Vue.component(Options.name, Options);
Vue.component(Resource.name, Resource); Vue.component(Resource.name, Resource);
Vue.component(Row.name, Row); Vue.component(Row.name, Row);
Vue.component(Save.name, Save);
Vue.component(SavesManager.name, SavesManager);
Vue.component(Spacer.name, Spacer); Vue.component(Spacer.name, Spacer);
Vue.component(Sticky.name, Sticky); Vue.component(Sticky.name, Sticky);
Vue.component(Subtab.name, Subtab); Vue.component(Subtab.name, Subtab);
@ -136,3 +145,4 @@ Vue.use(TransitionExpand);
Vue.use(PerfectScrollbar); Vue.use(PerfectScrollbar);
Vue.use(VueTextareaAutosize); Vue.use(VueTextareaAutosize);
Vue.use(PortalVue); Vue.use(PortalVue);
Vue.use(Sortable);

View file

@ -17,6 +17,7 @@
</ul> </ul>
</div> </div>
<div class="info" @click="openDialog('Info')"><br/>i</div> <div class="info" @click="openDialog('Info')"><br/>i</div>
<img class="options" src="images/options_wheel.png" @click="openDialog('Saves')" />
<img class="options" src="images/options_wheel.png" @click="openDialog('Options')" /> <img class="options" src="images/options_wheel.png" @click="openDialog('Options')" />
</div> </div>
<div v-else> <div v-else>
@ -32,10 +33,12 @@
</ul> </ul>
</div> </div>
<div class="info overlay" @click="openDialog('Info')"><br/>i</div> <div class="info overlay" @click="openDialog('Info')"><br/>i</div>
<img class="options overlay" src="images/options_wheel.png" @click="openDialog('Saves')" />
<img class="options overlay" src="images/options_wheel.png" @click="openDialog('Options')" /> <img class="options overlay" src="images/options_wheel.png" @click="openDialog('Options')" />
<div class="version overlay" @click="openDialog('Changelog')">v{{ version }}</div> <div class="version overlay" @click="openDialog('Changelog')">v{{ version }}</div>
</div> </div>
<Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" /> <Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" />
<SavesManager :show="showSaves" @closeDialog="closeDialog" />
<Options :show="showOptions" @closeDialog="closeDialog" /> <Options :show="showOptions" @closeDialog="closeDialog" />
</div> </div>
</template> </template>
@ -54,6 +57,7 @@ export default {
discordLink: modInfo.discordLink, discordLink: modInfo.discordLink,
version: modInfo.versionNumber, version: modInfo.versionNumber,
showInfo: false, showInfo: false,
showSaves: false,
showOptions: false, showOptions: false,
showChangelog: false showChangelog: false
} }
@ -119,6 +123,7 @@ export default {
width: 200px; width: 200px;
transition: right .25s ease; transition: right .25s ease;
background: var(--secondary-background); background: var(--secondary-background);
z-index: 1;
} }
.discord.overlay .discord-links { .discord.overlay .discord-links {

View file

@ -1,15 +1,9 @@
<template> <template>
<Modal :show="show" @close="$emit('closeDialog', 'Options')"> <Modal :show="show" @close="$emit('closeDialog', 'Options')">
<div slot="header"> <div slot="header" class="header">
<h2>Options</h2> <h2>Options</h2>
</div> </div>
<div slot="body"> <div slot="body">
<div class="actions">
<button class="button" @click="save">Manually Save</button>
<button @click="exportSave" class="button">Export</button>
<button @click="importSave" class="button danger">Import</button>
<button @click="hardReset" class="button danger">Hard Reset</button>
</div>
<Select title="Theme" :options="themes" :value="theme" @change="setTheme" default="classic" /> <Select title="Theme" :options="themes" :value="theme" @change="setTheme" default="classic" />
<Select title="Show Milestones" :options="msDisplayOptions" :value="msDisplay" @change="setMSDisplay" default="all" /> <Select title="Show Milestones" :options="msDisplayOptions" :value="msDisplay" @change="setMSDisplay" default="all" />
<Toggle title="Autosave" :value="autosave" @change="toggleOption('autosave')" /> <Toggle title="Autosave" :value="autosave" @change="toggleOption('autosave')" />
@ -56,45 +50,13 @@ export default {
}, },
setMSDisplay(msDisplay) { setMSDisplay(msDisplay) {
player.msDisplay = msDisplay; player.msDisplay = msDisplay;
},
save() {
console.warn("Not yet implemented!");
},
hardReset() {
console.warn("Not yet implemented!");
},
exportSave() {
console.warn("Not yet implemented!");
},
importSave() {
console.warn("Not yet implemented!");
} }
} }
}; };
</script> </script>
<style scoped> <style scoped>
.actions { .header {
display: flex; margin-bottom: -10px;
justify-content: space-between;
padding-bottom: 10px;
}
.actions * {
margin: 0;
}
.danger {
border: solid 2px var(--danger);
padding-right: 0;
}
.danger::after {
content: "!";
color: white;
background: var(--danger);
padding: 2px;
margin-left: 6px;
height: 100%;
} }
</style> </style>

View file

@ -0,0 +1,171 @@
<template>
<div class="save" :class="{ active }">
<div class='handle material-icons'>drag_handle</div>
<div class="actions" v-if="!editing">
<feedback-button @click="$emit('export')" class="button" left v-if="save.error == undefined && !confirming">
<span class="material-icons">content_paste</span>
</feedback-button>
<button @click="$emit('duplicate')" class="button" v-if="save.error == undefined && !confirming">
<span class="material-icons">content_copy</span>
</button>
<button @click="toggleEditing" class="button" v-if="save.error == undefined && !confirming">
<span class="material-icons">edit</span>
</button>
<danger-button :disabled="active" @click="$emit('delete')" @confirmingChanged="confirmingChanged">
<span class="material-icons" style="margin: -2px">delete</span>
</danger-button>
</div>
<div class="actions" v-else>
<button @click="changeName" class="button">
<span class="material-icons">check</span>
</button>
<button @click="toggleEditing" class="button">
<span class="material-icons">close</span>
</button>
</div>
<div class="details" v-if="save.error == undefined && !editing">
<button class="button open" @click="$emit('open')">
<h3>{{ save.name }}</h3>
</button>
<span class="save-version">v{{ save.modVersion }}</span><br>
<div>Last played {{ dateFormat.format(time) }}</div>
</div>
<div class="details" v-else-if="save.error == undefined && editing">
<TextField v-model="newName" class="editname" @submit="changeName" />
</div>
<div v-else class="details error">
Error: Failed to load save with id {{ save.id }}
</div>
</div>
</template>
<script>
import { player } from '../../store/proxies';
export default {
name: 'save',
props: {
save: Object
},
data() {
return {
dateFormat: new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: 'numeric', day: 'numeric',
hour: 'numeric', minute: 'numeric', second: 'numeric',
}),
editing: false,
confirming: false,
newName: ""
};
},
computed: {
active() {
return this.save.id === player.id;
},
time() {
return this.active ? player.time : this.save.time;
}
},
methods: {
confirmingChanged(confirming) {
this.confirming = confirming;
},
toggleEditing() {
this.newName = this.save.name;
this.editing = !this.editing;
},
changeName() {
this.$emit('editSave', this.newName);
this.editing = false;
}
}
};
</script>
<style scoped>
.save {
position: relative;
border: solid 4px var(--separator);
padding: 4px;
background: var(--secondary-background);
margin: var(--feature-margin);
display: flex;
align-items: center;
min-height: 30px;
}
.save.active {
border-color: var(--bought);
}
.open {
display: inline;
margin: 0;
padding-left: 0;
}
.handle {
flex-grow: 0;
margin-right: 8px;
margin-left: 0;
cursor: pointer;
}
.details {
margin: 0;
flex-grow: 1;
margin-right: 80px;
}
.error {
font-size: .8em;
color: var(--danger);
}
.save-version {
margin-left: 4px;
font-size: .7em;
opacity: .7;
}
.actions {
position: absolute;
top: 0;
bottom: 0;
right: 4px;
display: flex;
padding: 4px;
background: inherit;
z-index: 1;
}
.editname {
margin: 0;
}
</style>
<style>
.save button {
transition-duration: 0s;
}
.save .actions button {
display: flex;
font-size: 1.2em;
}
.save .actions button .material-icons {
font-size: unset;
}
.save .button.danger {
display: flex;
align-items: center;
padding: 0;
padding-left: 6px;
}
.save .field {
margin: 0;
}
</style>

View file

@ -0,0 +1,218 @@
<template>
<Modal :show="show" @close="$emit('closeDialog', 'Saves')">
<div slot="header">
<h2>Saves Manager</h2>
</div>
<div slot="body" v-sortable="{ onUpdate, handle: '.handle' }">
<save v-for="(save, index) in saves" :key="index" :save="save" @open="openSave(save.id)" @export="exportSave(save.id)"
@editSave="name => editSave(save.id, name)" @duplicate="duplicateSave(save.id)" @delete="deleteSave(save.id)" />
</div>
<div slot="footer" class="modal-footer">
<TextField :value="saveToImport" @submit="importSave" @input="importSave"
title="Import Save" placeholder="Paste your save here!" :class="{ importingFailed }" />
<div class="field">
<span class="field-title">Create Save</span>
<div class="field-buttons">
<button class="button" @click="newSave">New Game</button>
<Select v-if="Object.keys(bank).length > 0" :value="{ label: 'Select preset' }" :options="bank"
@change="newFromPreset" />
</div>
</div>
<div class="footer">
<div style="flex-grow: 1"></div>
<button class="button modal-default-button" @click="$emit('closeDialog', 'Saves')">
Close
</button>
</div>
</div>
</Modal>
</template>
<script>
import Vue from 'vue';
import { newSave, getUniqueID, loadSave, save } from '../../util/save';
import { player } from '../../store/proxies';
import modInfo from '../../data/modInfo.json';
import '../fields/fields.css';
export default {
name: 'SavesManager',
props: {
show: Boolean
},
data() {
let bankContext = require.context('raw-loader!../../../saves', true, /\.txt$/);
let bank = bankContext.keys().reduce((acc, curr) => {
// .slice(2, -4) strips the leading ./ and the trailing .txt
acc.push({ label: curr.slice(2, -4), value: bankContext(curr).default });
return acc;
}, []);
return {
importingFailed: false,
saves: {}, // Gets populated when the modal is opened
saveToImport: "",
bank
};
},
watch: {
show(newValue) {
if (newValue) {
this.loadSaveData();
}
}
},
methods: {
loadSaveData() {
try {
const { saves } = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
this.saves = saves.reduce((acc, curr) => {
try {
acc[curr] = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(curr)))));
acc[curr].id = curr;
} catch(error) {
console.warn(`Can't load save with id "${curr}"`, error);
acc[curr] = { error, id: curr };
}
return acc;
}, {});
} catch(e) {
this.saves = { [ player.id ]: player };
const modData = { active: player.id, saves: [ player.id ] };
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
}
},
exportSave(id) {
let saveToExport;
if (player.id === id) {
save();
saveToExport = player.saveToExport;
} else {
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id]))));
}
// Put on clipboard. Using the clipboard API asks for permissions and stuff
const el = document.createElement("textarea");
el.value = saveToExport;
document.body.appendChild(el);
el.select();
el.setSelectionRange(0, 99999);
document.execCommand("copy");
document.body.removeChild(el);
},
duplicateSave(id) {
if (player.id === id) {
save();
}
const playerData = { ...this.saves[id], id: getUniqueID() };
localStorage.setItem(playerData.id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
modData.saves.push(playerData.id);
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
Vue.set(this.saves, playerData.id, playerData);
},
deleteSave(id) {
const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
modData.saves = modData.saves.filter(save => save !== id);
localStorage.removeItem(id);
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
Vue.delete(this.saves, id);
},
openSave(id) {
this.saves[player.id].time = player.time;
loadSave(this.saves[id]);
},
async newSave() {
const playerData = await newSave();
Vue.set(this.saves, playerData.id, playerData);
},
newFromPreset(preset) {
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
playerData.id = getUniqueID();
localStorage.setItem(playerData.id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
modData.saves.push(playerData.id);
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
Vue.set(this.saves, playerData.id, playerData);
},
editSave(id, newName) {
this.saves[id].name = newName;
if (player.id === id) {
player.name = newName;
save();
} else {
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id])))));
}
},
importSave(text) {
this.saveToImport = text;
if (text) {
this.$nextTick(() => {
try {
const playerData = JSON.parse(decodeURIComponent(escape(atob(text))));
const id = getUniqueID();
playerData.id = id;
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
Vue.set(this.saves, id, playerData);
this.saveToImport = "";
this.importingFailed = false;
const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
modData.saves.push(id);
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
} catch (e) {
this.importingFailed = true;
}
});
} else {
this.importingFailed = false;
}
},
onUpdate(e) {
this.saves.splice(e.newIndex, 0, this.saves.splive(e.oldIndex, 1)[0]);
const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
modData.saves.splice(e.newIndex, 0, modData.saves.splice(e.oldIndex, 1)[0]);
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
}
}
};
</script>
<style scoped>
.field form,
.field .field-title,
.field .field-buttons {
margin: 0;
}
.field-buttons {
display: flex;
}
.field-buttons .field {
margin: 0;
margin-left: 8px;
}
.modal-footer {
margin-top: -20px;
}
.footer {
display: flex;
margin-top: 20px;
}
</style>
<style>
.importingFailed input {
color: red;
}
.field-buttons .v-select {
width: 220px;
}
</style>

View file

@ -29,15 +29,9 @@ export default {
}; };
}, },
mounted() { mounted() {
this.$nextTick(() => { // ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
if (this.$refs.resizeListener == undefined) { this.resizeObserver.observe(this.$refs.resizeListener);
this.mounted(); this.updateNodes();
} else {
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
this.resizeObserver.observe(this.$refs.resizeListener);
this.updateNodes();
}
});
}, },
provide() { provide() {
return { return {
@ -77,7 +71,7 @@ export default {
}); });
}, },
unregisterNode(id) { unregisterNode(id) {
delete this.nodes[id]; Vue.delete(this.nodes, id);
}, },
registerBranch(start, options) { registerBranch(start, options) {
const end = typeof options === 'string' ? options : options.target; const end = typeof options === 'string' ? options : options.target;

View file

@ -311,7 +311,7 @@ export default {
<spacer height="5px" /> <spacer height="5px" />
<button onclick='console.log("yeet")'>'HI'</button> <button onclick='console.log("yeet")'>'HI'</button>
<div>Name your points!</div> <div>Name your points!</div>
<TextField :value="player.c.thingy" @change="value => player.c.thingy = value" /> <TextField :value="player.c.thingy" @input="value => player.c.thingy = value" :field="false" />
<sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;">I have {{ format(player.points) }} {{ player.c.thingy }} points!</sticky> <sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;">I have {{ format(player.points) }} {{ player.c.thingy }} points!</sticky>
<hr /> <hr />
<milestones /> <milestones />

View file

@ -54,7 +54,7 @@ const main = {
name: "Tree" name: "Tree"
}; };
export const initialLayers = [ main, f, c, a, g, h, spook ]; export const getInitialLayers = () => [ main, f, c, a, g, h, spook ];
export function getStartingData() { export function getStartingData() {
return { return {
@ -80,7 +80,7 @@ export function update(delta) {
} }
/* eslint-disable-next-line no-unused-vars */ /* eslint-disable-next-line no-unused-vars */
export function fixOldSave(oldVersion) { export function fixOldSave(oldVersion, playerData) {
} }
document.title = modInfo.title; document.title = modInfo.title;

View file

@ -2,7 +2,7 @@
"title": "The Modding Tree X", "title": "The Modding Tree X",
"id": "tmt-x", "id": "tmt-x",
"author": "thepaperpilot", "author": "thepaperpilot",
"discordName": "TMT-X", "discordName": "The Paper Pilot Community",
"discordLink": "https://discord.gg/WzejVAx", "discordLink": "https://discord.gg/WzejVAx",
"versionNumber": "0.0", "versionNumber": "0.0",

View file

@ -48,6 +48,14 @@ a:hover,
-3px 0 12px var(--link); -3px 0 12px var(--link);
} }
.button:disabled {
opacity: .5;
cursor: not-allowed;
}
.button:disabled:hover {
text-shadow: none;
}
ul { ul {
list-style-type: none; list-style-type: none;
} }

View file

@ -1,7 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import App from './App'; import App from './App';
import store from './store'; import store from './store';
import { addLayer} from './store/layers'; import { load } from './util/save';
import { setVue } from './util/vue'; import { setVue } from './util/vue';
import { startGameLoop } from './store/game'; import { startGameLoop } from './store/game';
import './components/index'; import './components/index';
@ -10,9 +10,7 @@ import './components/index';
Vue.config.productionTip = false; Vue.config.productionTip = false;
requestAnimationFrame(async () => { requestAnimationFrame(async () => {
// Add layers on second frame so dependencies can resolve await load();
const { initialLayers } = await import('./data/mod');
initialLayers.forEach(addLayer);
// Create Vue // Create Vue
const vue = window.vue = new Vue({ const vue = window.vue = new Vue({

View file

@ -1,6 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import { getInitialStore } from '../util/load'; import { getInitialStore } from '../util/save';
import { getters } from '../data/mod'; import { getters } from '../data/mod';
Vue.use(Vuex); Vue.use(Vuex);

View file

@ -1,3 +1,5 @@
import Vue from 'vue';
import clone from 'lodash.clonedeep';
import { isFunction, isPlainObject } from '../util/common'; import { isFunction, isPlainObject } from '../util/common';
import { createProxy, createGridProxy, player } from './proxies'; import { createProxy, createGridProxy, player } from './proxies';
import Decimal from '../util/bignum'; import Decimal from '../util/bignum';
@ -21,13 +23,16 @@ export function addLayer(layer) {
return; return;
} }
} }
if (layer.type === "static" && (layer.base == undefined || Decimal.lte(layer.base, 1))) {
layer.base = 2; // Clone object to prevent modifying the original
} layer = clone(layer);
// Set default property values // Set default property values
layer = Object.assign({}, defaultLayerProperties, layer); layer = Object.assign({}, defaultLayerProperties, layer);
layer.layer = layer.id; layer.layer = layer.id;
if (layer.type === "static" && (layer.base == undefined || Decimal.lte(layer.base, 1))) {
layer.base = 2;
}
const getters = {}; const getters = {};
@ -149,6 +154,9 @@ export function addLayer(layer) {
if (layer.challenges[id].onExit != undefined) { if (layer.challenges[id].onExit != undefined) {
layer.challenges[id].onExit.forceCached = false; layer.challenges[id].onExit.forceCached = false;
} }
layer.challenges[id].shown = function() {
return this.unlocked !== false && (player.hideChallenges === false || !this.maxed);
}
layer.challenges[id].completed = function() { layer.challenges[id].completed = function() {
return !layer.deactivated && player[layer.id].challenges[id]?.gt(0); return !layer.deactivated && player[layer.id].challenges[id]?.gt(0);
} }
@ -411,7 +419,7 @@ export function removeLayer(layer) {
// Un-set hotkeys // Un-set hotkeys
if (layers[layer].hotkeys) { if (layers[layer].hotkeys) {
for (let id in layers[layer].hotkeys) { for (let id in layers[layer].hotkeys) {
delete hotkeys[id]; Vue.delete(hotkeys, id);
} }
} }

View file

@ -57,6 +57,10 @@ const playerHandler = {
} }
} }
return true; return true;
},
deleteProperty(target, prop) {
Vue.delete(target, prop);
return true;
} }
}; };
export const player = window.player = new Proxy(store.state, playerHandler); export const player = window.player = new Proxy(store.state, playerHandler);

View file

@ -1,42 +0,0 @@
import modInfo from '../data/modInfo';
import { getStartingData, initialLayers } from '../data/mod';
import { getStartingBuyables, getStartingClickables, getStartingChallenges } from './layers';
import Decimal from './bignum';
export function getInitialStore() {
return {
tabs: modInfo.initialTabs.slice(),
time: Date.now(),
autosave: true,
offlineProd: true,
timePlayed: new Decimal(0),
keepGoing: false,
lastTenTicks: [],
showTPS: true,
msDisplay: "all",
hideChallenges: false,
theme: "paper",
subtabs: {},
minimized: {},
...getStartingData(),
...initialLayers.reduce((acc, layer) => {
acc[layer.id] = {
upgrades: [],
achievements: [],
milestones: [],
infoboxes: {},
buyables: getStartingBuyables(layer),
clickables: getStartingClickables(layer),
challenges: getStartingChallenges(layer),
...layer.startData?.()
};
return acc;
}, {}),
// Values that don't get saved
hasNaN: false,
NaNProperty: "",
NaNReceiver: null,
NaNPrevious: null
}
}

169
src/util/save.js Normal file
View file

@ -0,0 +1,169 @@
import modInfo from '../data/modInfo';
import { getStartingData, getInitialLayers, fixOldSave } from '../data/mod';
import { getStartingBuyables, getStartingClickables, getStartingChallenges } from './layers';
import { player } from '../store/proxies';
import Decimal from './bignum';
export const NOT_IMPORTING = false;
export const IMPORTING = true;
export const IMPORTING_FAILED = "FAILED";
export const IMPORTING_WRONG_ID = "WRONG_ID";
export const IMPORTING_FORCE = "FORCE";
export function getInitialStore(playerData = {}) {
playerData = applyPlayerData({
id: `${modInfo.id}-0`,
name: "Default Save",
tabs: modInfo.initialTabs.slice(),
time: Date.now(),
autosave: true,
offlineProd: true,
timePlayed: new Decimal(0),
keepGoing: false,
lastTenTicks: [],
showTPS: true,
msDisplay: "all",
hideChallenges: false,
theme: "paper",
subtabs: {},
minimized: {},
modID: modInfo.id,
modVersion: modInfo.versionNumber,
...getStartingData(),
// Values that don't get loaded/saved
hasNaN: false,
NaNProperty: "",
NaNReceiver: null,
NaNPrevious: null,
importing: NOT_IMPORTING,
saveToImport: "",
saveToExport: ""
}, playerData);
Object.assign(playerData, getInitialLayers(playerData).reduce((acc, layer) => {
acc[layer.id] = applyPlayerData({
upgrades: [],
achievements: [],
milestones: [],
infoboxes: {},
buyables: getStartingBuyables(layer),
clickables: getStartingClickables(layer),
challenges: getStartingChallenges(layer),
...layer.startData?.()
}, playerData[layer.id]);
return acc;
}, {}));
return playerData;
}
export function save() {
/* eslint-disable-next-line no-unused-vars */
let { hasNaN, NaNProperty, NaNReceiver, NaNPrevious, importing, saveToImport, saveToExport, ...playerData } = player;
player.saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(playerData))));
localStorage.setItem(player.id, player.saveToExport);
}
export async function load() {
try {
let modData = localStorage.getItem(modInfo.id);
if (modData == null) {
await loadSave(newSave());
return;
}
modData = JSON.parse(decodeURIComponent(escape(atob(modData))));
if (modData?.active == null) {
await loadSave(newSave());
return;
}
const save = localStorage.getItem(modData.active);
const playerData = JSON.parse(decodeURIComponent(escape(atob(save))));
if (playerData.modID !== modInfo.id) {
await loadSave(newSave());
return;
}
await loadSave(playerData);
} catch (e) {
await loadSave(newSave());
}
}
export async function newSave() {
const id = getUniqueID();
const playerData = getInitialStore({ id });
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
if (!localStorage.getItem(modInfo.id)) {
const modData = { active: id, saves: [ id ] };
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
} else {
const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
modData.saves.push(id);
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
}
return playerData;
}
export function getUniqueID() {
let id, i = 0;
do {
id = `${modInfo.id}-${i++}`;
} while (localStorage.getItem(id));
return id;
}
export async function loadSave(playerData) {
const { layers, removeLayer, addLayer } = await import('../store/layers');
for (let layer in layers) {
removeLayer(layer);
}
getInitialLayers(playerData).forEach(addLayer);
playerData = getInitialStore(playerData);
if (playerData.offlineProd) {
if (playerData.offTime === undefined)
playerData.offTime = { remain: 0 };
playerData.offTime.remain += (Date.now() - playerData.time) / 1000;
}
playerData.time = Date.now();
if (playerData.modVersion !== modInfo.versionNumber) {
fixOldSave(playerData.modVersion, playerData);
}
Object.assign(player, playerData);
for (let prop in player) {
if (!(prop in playerData)) {
delete player[prop];
}
}
}
function applyPlayerData(target, source) {
for (let prop in source) {
if (target[prop] == null) {
target[prop] = source[prop];
} else if (target[prop] instanceof Decimal) {
target[prop] = new Decimal(source[prop]);
} else if (Array.isArray(target[prop]) || typeof target[prop] === 'object') {
target[prop] = applyPlayerData(target[prop], source[prop]);
} else {
target[prop] = source[prop];
}
}
return target;
}
setInterval(() => {
if (player.autosave) {
save();
}
}, 1000);
window.onbeforeunload = () => {
if (player.autosave) {
save();
}
};

View file

@ -1,6 +1,4 @@
module.exports = { module.exports = {
publicPath: process.env.NODE_ENV === 'production' publicPath: process.env.NODE_ENV === 'production' ? '/The-Modding-Tree-X' : '/',
? '/The-Modding-Tree-X' runtimeCompiler: true
: '/',
runtimeCompiler: true
}; };