Implemented saves manager
This commit is contained in:
parent
69b1fff796
commit
f018016477
29 changed files with 923 additions and 141 deletions
129
package-lock.json
generated
129
package-lock.json
generated
|
@ -9,11 +9,13 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"portal-vue": "^2.1.7",
|
||||
"vue": "^2.6.11",
|
||||
"vue-frag": "^1.1.5",
|
||||
"vue-reactive-provide": "^0.3.0",
|
||||
"vue-select": "^3.11.2",
|
||||
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
"vue-transition-expand": "^0.1.0",
|
||||
"vue2-perfect-scrollbar": "^1.5.0",
|
||||
|
@ -28,6 +30,7 @@
|
|||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
}
|
||||
},
|
||||
|
@ -8429,6 +8432,11 @@
|
|||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"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": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
|
@ -10812,6 +10820,58 @@
|
|||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -11853,6 +11913,11 @@
|
|||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||
|
@ -12015,11 +12080,6 @@
|
|||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"sshpk-conv": "bin/sshpk-conv",
|
||||
"sshpk-sign": "bin/sshpk-sign",
|
||||
"sshpk-verify": "bin/sshpk-verify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
@ -13506,6 +13566,14 @@
|
|||
"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": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
|
||||
|
@ -21535,6 +21603,11 @@
|
|||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"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": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
|
@ -23516,6 +23589,40 @@
|
|||
"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": {
|
||||
"version": "1.0.0",
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||
|
@ -25730,6 +25842,13 @@
|
|||
"integrity": "sha512-pIOcY8ajWNSwg8Ns4eHVr5ZWwqKCSZeQRymTnlUI8i+3QiQXF6JIM4lylK6mVfbccs4S6vOyxB7zmJBpp7tDUg==",
|
||||
"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": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
|
||||
|
|
|
@ -9,11 +9,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"portal-vue": "^2.1.7",
|
||||
"vue": "^2.6.11",
|
||||
"vue-frag": "^1.1.5",
|
||||
"vue-reactive-provide": "^0.3.0",
|
||||
"vue-select": "^3.11.2",
|
||||
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
"vue-transition-expand": "^0.1.0",
|
||||
"vue2-perfect-scrollbar": "^1.5.0",
|
||||
|
@ -28,6 +30,7 @@
|
|||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<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/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
|
||||
|
|
0
saves/.placehold
Normal file
0
saves/.placehold
Normal file
1
saves/safff.txt
Normal file
1
saves/safff.txt
Normal file
|
@ -0,0 +1 @@
|
|||
eyJpZCI6InRtdC14LTEwNSIsIm5hbWUiOiJEZWZhdWx0IFNhZmZmZiAtIHNvbWV0aGluZyBlbHNlIiwidGFicyI6WyJtYWluIiwiYyJdLCJ0aW1lIjoxNjI0MjQ1MjYxMDg3LCJhdXRvc2F2ZSI6dHJ1ZSwib2ZmbGluZVByb2QiOnRydWUsInRpbWVQbGF5ZWQiOiIzNDQ4LjYxNTc4MTcwOTAxIiwia2VlcEdvaW5nIjpmYWxzZSwibGFzdFRlblRpY2tzIjpbMC4wNTEsMC4wNSwwLjA0OSwwLjA1LDAuMDUsMC4wNTEsMC4wNDksMC4wNSwwLjA1LDAuMDUxXSwic2hvd1RQUyI6dHJ1ZSwibXNEaXNwbGF5IjoiYWxsIiwiaGlkZUNoYWxsZW5nZXMiOmZhbHNlLCJ0aGVtZSI6InBhcGVyIiwic3VidGFicyI6e30sIm1pbmltaXplZCI6e30sIm1vZElEIjoidG10LXgiLCJtb2RWZXJzaW9uIjoiMC4wIiwicG9pbnRzIjoiMzMwMC4zNzc3NzM4NTkwNTUiLCJtYWluIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJmIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImNsaWNrYWJsZXMiOnsiMTEiOiJTdGFydCJ9LCJ1bmxvY2tlZCI6ZmFsc2UsInBvaW50cyI6IjAiLCJib29wIjpmYWxzZX0sImMiOnsidXBncmFkZXMiOlsiMTEiXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImJ1eWFibGVzIjp7IjExIjoiMCJ9LCJjaGFsbGVuZ2VzIjp7IjExIjoiMCJ9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCIsImJlc3QiOiIxIiwidG90YWwiOiIwIiwiYmVlcCI6ZmFsc2UsInRoaW5neSI6InBvaW50eSIsIm90aGVyVGhpbmd5IjoxMCwic3BlbnRPbkJ1eWFibGVzIjoiMCJ9LCJhIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbIjExIl0sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCJ9LCJnIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJoIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJzcG9vayI6eyJ1cGdyYWRlcyI6W10sImFjaGlldmVtZW50cyI6W10sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9fSwib29tcHNNYWciOjAsImxhc3RQb2ludHMiOiIzMzAwLjM3Nzc3Mzg1OTA1NSJ9
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="shown" :style="style"
|
||||
<div v-if="challenge.shown" :style="style"
|
||||
:class="{
|
||||
feature: true,
|
||||
challenge: true,
|
||||
|
@ -21,7 +21,6 @@
|
|||
|
||||
<script>
|
||||
import { layers } from '../../store/layers';
|
||||
import { player } from '../../store/proxies';
|
||||
import { coerceComponent } from '../../util/vue';
|
||||
import './features.css';
|
||||
|
||||
|
@ -36,9 +35,6 @@ export default {
|
|||
challenge() {
|
||||
return layers[this.layer || this.tab.layer].challenges[this.id];
|
||||
},
|
||||
shown() {
|
||||
return this.challenge.unlocked && !(player.hideChallenges && this.challenge.maxes);
|
||||
},
|
||||
style() {
|
||||
return [
|
||||
layers[this.layer || this.tab.layer].componentStyles?.challenge,
|
||||
|
|
65
src/components/fields/DangerButton.vue
Normal file
65
src/components/fields/DangerButton.vue
Normal 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>
|
74
src/components/fields/FeedbackButton.vue
Normal file
74
src/components/fields/FeedbackButton.vue
Normal 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>
|
|
@ -30,20 +30,14 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.v-select {
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.v-select .vs__dropdown-toggle {
|
||||
border-color: rgba(var(--color), .26);
|
||||
margin: -1px 0;
|
||||
}
|
||||
|
||||
.v-select .vs__selected {
|
||||
|
@ -64,4 +58,8 @@ span {
|
|||
.v-select .vs__dropdown-option {
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.v-select .vs__open-indicator {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -22,11 +22,4 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<template>
|
||||
<form @submit.prevent="$emit('submit')">
|
||||
<div class="field">
|
||||
<span class="field-title" v-if="title">{{ title }}</span>
|
||||
<textarea-autosize v-if="textarea" :placeholder="placeholder" :value="value" @input="value => $emit('change', value)" />
|
||||
<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"
|
||||
@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>
|
||||
|
||||
<script>
|
||||
|
@ -13,12 +17,32 @@ export default {
|
|||
name: 'TextField',
|
||||
props: {
|
||||
title: String,
|
||||
value: [ String, Object ],
|
||||
value: String,
|
||||
textarea: Boolean,
|
||||
placeholder: String
|
||||
placeholder: String,
|
||||
maxHeight: Number
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.field.focus();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,4 +5,9 @@
|
|||
margin: 10px 0;
|
||||
user-select: none;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ import RespecButton from './features/RespecButton';
|
|||
import Upgrade from './features/Upgrade';
|
||||
import Upgrades from './features/Upgrades';
|
||||
/* fields */
|
||||
import DangerButton from './fields/DangerButton';
|
||||
import FeedbackButton from './fields/FeedbackButton';
|
||||
import Select from './fields/Select';
|
||||
import Slider from './fields/Slider';
|
||||
import Text from './fields/Text';
|
||||
|
@ -48,6 +50,8 @@ import Nav from './system/Nav';
|
|||
import Options from './system/Options';
|
||||
import Resource from './system/Resource';
|
||||
import Row from './system/Row';
|
||||
import Save from './system/Save';
|
||||
import SavesManager from './system/SavesManager';
|
||||
import Spacer from './system/Spacer';
|
||||
import Sticky from './system/Sticky';
|
||||
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 VueTextareaAutosize from 'vue-textarea-autosize';
|
||||
import PortalVue from 'portal-vue';
|
||||
import Sortable from 'vue-sortable';
|
||||
|
||||
/* features */
|
||||
Vue.component(Achievement.name, Achievement);
|
||||
|
@ -98,6 +103,8 @@ Vue.component(RespecButton.name, RespecButton);
|
|||
Vue.component(Upgrade.name, Upgrade);
|
||||
Vue.component(Upgrades.name, Upgrades);
|
||||
/* fields */
|
||||
Vue.component(DangerButton.name, DangerButton);
|
||||
Vue.component(FeedbackButton.name, FeedbackButton);
|
||||
Vue.component(Select.name, Select);
|
||||
Vue.component(Slider.name, Slider);
|
||||
Vue.component(Text.name, Text);
|
||||
|
@ -116,6 +123,8 @@ Vue.component(Nav.name, Nav);
|
|||
Vue.component(Options.name, Options);
|
||||
Vue.component(Resource.name, Resource);
|
||||
Vue.component(Row.name, Row);
|
||||
Vue.component(Save.name, Save);
|
||||
Vue.component(SavesManager.name, SavesManager);
|
||||
Vue.component(Spacer.name, Spacer);
|
||||
Vue.component(Sticky.name, Sticky);
|
||||
Vue.component(Subtab.name, Subtab);
|
||||
|
@ -136,3 +145,4 @@ Vue.use(TransitionExpand);
|
|||
Vue.use(PerfectScrollbar);
|
||||
Vue.use(VueTextareaAutosize);
|
||||
Vue.use(PortalVue);
|
||||
Vue.use(Sortable);
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
</ul>
|
||||
</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')" />
|
||||
</div>
|
||||
<div v-else>
|
||||
|
@ -32,10 +33,12 @@
|
|||
</ul>
|
||||
</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')" />
|
||||
<div class="version overlay" @click="openDialog('Changelog')">v{{ version }}</div>
|
||||
</div>
|
||||
<Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" />
|
||||
<SavesManager :show="showSaves" @closeDialog="closeDialog" />
|
||||
<Options :show="showOptions" @closeDialog="closeDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -54,6 +57,7 @@ export default {
|
|||
discordLink: modInfo.discordLink,
|
||||
version: modInfo.versionNumber,
|
||||
showInfo: false,
|
||||
showSaves: false,
|
||||
showOptions: false,
|
||||
showChangelog: false
|
||||
}
|
||||
|
@ -119,6 +123,7 @@ export default {
|
|||
width: 200px;
|
||||
transition: right .25s ease;
|
||||
background: var(--secondary-background);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.discord.overlay .discord-links {
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
<template>
|
||||
<Modal :show="show" @close="$emit('closeDialog', 'Options')">
|
||||
<div slot="header">
|
||||
<div slot="header" class="header">
|
||||
<h2>Options</h2>
|
||||
</div>
|
||||
<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="Show Milestones" :options="msDisplayOptions" :value="msDisplay" @change="setMSDisplay" default="all" />
|
||||
<Toggle title="Autosave" :value="autosave" @change="toggleOption('autosave')" />
|
||||
|
@ -56,45 +50,13 @@ export default {
|
|||
},
|
||||
setMSDisplay(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>
|
||||
|
||||
<style scoped>
|
||||
.actions {
|
||||
display: flex;
|
||||
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%;
|
||||
.header {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
</style>
|
||||
|
|
171
src/components/system/Save.vue
Normal file
171
src/components/system/Save.vue
Normal 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>
|
218
src/components/system/SavesManager.vue
Normal file
218
src/components/system/SavesManager.vue
Normal 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>
|
|
@ -29,15 +29,9 @@ export default {
|
|||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.resizeListener == undefined) {
|
||||
this.mounted();
|
||||
} 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() {
|
||||
return {
|
||||
|
@ -77,7 +71,7 @@ export default {
|
|||
});
|
||||
},
|
||||
unregisterNode(id) {
|
||||
delete this.nodes[id];
|
||||
Vue.delete(this.nodes, id);
|
||||
},
|
||||
registerBranch(start, options) {
|
||||
const end = typeof options === 'string' ? options : options.target;
|
||||
|
|
|
@ -311,7 +311,7 @@ export default {
|
|||
<spacer height="5px" />
|
||||
<button onclick='console.log("yeet")'>'HI'</button>
|
||||
<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>
|
||||
<hr />
|
||||
<milestones />
|
||||
|
|
|
@ -54,7 +54,7 @@ const main = {
|
|||
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() {
|
||||
return {
|
||||
|
@ -80,7 +80,7 @@ export function update(delta) {
|
|||
}
|
||||
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
export function fixOldSave(oldVersion) {
|
||||
export function fixOldSave(oldVersion, playerData) {
|
||||
}
|
||||
|
||||
document.title = modInfo.title;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"title": "The Modding Tree X",
|
||||
"id": "tmt-x",
|
||||
"author": "thepaperpilot",
|
||||
"discordName": "TMT-X",
|
||||
"discordName": "The Paper Pilot Community",
|
||||
"discordLink": "https://discord.gg/WzejVAx",
|
||||
|
||||
"versionNumber": "0.0",
|
||||
|
|
|
@ -48,6 +48,14 @@ a:hover,
|
|||
-3px 0 12px var(--link);
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.button:disabled:hover {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import App from './App';
|
||||
import store from './store';
|
||||
import { addLayer} from './store/layers';
|
||||
import { load } from './util/save';
|
||||
import { setVue } from './util/vue';
|
||||
import { startGameLoop } from './store/game';
|
||||
import './components/index';
|
||||
|
@ -10,9 +10,7 @@ import './components/index';
|
|||
Vue.config.productionTip = false;
|
||||
|
||||
requestAnimationFrame(async () => {
|
||||
// Add layers on second frame so dependencies can resolve
|
||||
const { initialLayers } = await import('./data/mod');
|
||||
initialLayers.forEach(addLayer);
|
||||
await load();
|
||||
|
||||
// Create Vue
|
||||
const vue = window.vue = new Vue({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { getInitialStore } from '../util/load';
|
||||
import { getInitialStore } from '../util/save';
|
||||
import { getters } from '../data/mod';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import clone from 'lodash.clonedeep';
|
||||
import { isFunction, isPlainObject } from '../util/common';
|
||||
import { createProxy, createGridProxy, player } from './proxies';
|
||||
import Decimal from '../util/bignum';
|
||||
|
@ -21,13 +23,16 @@ export function addLayer(layer) {
|
|||
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
|
||||
layer = Object.assign({}, defaultLayerProperties, layer);
|
||||
layer.layer = layer.id;
|
||||
if (layer.type === "static" && (layer.base == undefined || Decimal.lte(layer.base, 1))) {
|
||||
layer.base = 2;
|
||||
}
|
||||
|
||||
const getters = {};
|
||||
|
||||
|
@ -149,6 +154,9 @@ export function addLayer(layer) {
|
|||
if (layer.challenges[id].onExit != undefined) {
|
||||
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() {
|
||||
return !layer.deactivated && player[layer.id].challenges[id]?.gt(0);
|
||||
}
|
||||
|
@ -411,7 +419,7 @@ export function removeLayer(layer) {
|
|||
// Un-set hotkeys
|
||||
if (layers[layer].hotkeys) {
|
||||
for (let id in layers[layer].hotkeys) {
|
||||
delete hotkeys[id];
|
||||
Vue.delete(hotkeys, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,10 @@ const playerHandler = {
|
|||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
deleteProperty(target, prop) {
|
||||
Vue.delete(target, prop);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
export const player = window.player = new Proxy(store.state, playerHandler);
|
||||
|
|
|
@ -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
169
src/util/save.js
Normal 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();
|
||||
}
|
||||
};
|
|
@ -1,6 +1,4 @@
|
|||
module.exports = {
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? '/The-Modding-Tree-X'
|
||||
: '/',
|
||||
publicPath: process.env.NODE_ENV === 'production' ? '/The-Modding-Tree-X' : '/',
|
||||
runtimeCompiler: true
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue