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",
"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",

View file

@ -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": {

View file

@ -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
View file

1
saves/safff.txt Normal file
View file

@ -0,0 +1 @@
eyJpZCI6InRtdC14LTEwNSIsIm5hbWUiOiJEZWZhdWx0IFNhZmZmZiAtIHNvbWV0aGluZyBlbHNlIiwidGFicyI6WyJtYWluIiwiYyJdLCJ0aW1lIjoxNjI0MjQ1MjYxMDg3LCJhdXRvc2F2ZSI6dHJ1ZSwib2ZmbGluZVByb2QiOnRydWUsInRpbWVQbGF5ZWQiOiIzNDQ4LjYxNTc4MTcwOTAxIiwia2VlcEdvaW5nIjpmYWxzZSwibGFzdFRlblRpY2tzIjpbMC4wNTEsMC4wNSwwLjA0OSwwLjA1LDAuMDUsMC4wNTEsMC4wNDksMC4wNSwwLjA1LDAuMDUxXSwic2hvd1RQUyI6dHJ1ZSwibXNEaXNwbGF5IjoiYWxsIiwiaGlkZUNoYWxsZW5nZXMiOmZhbHNlLCJ0aGVtZSI6InBhcGVyIiwic3VidGFicyI6e30sIm1pbmltaXplZCI6e30sIm1vZElEIjoidG10LXgiLCJtb2RWZXJzaW9uIjoiMC4wIiwicG9pbnRzIjoiMzMwMC4zNzc3NzM4NTkwNTUiLCJtYWluIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJmIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImNsaWNrYWJsZXMiOnsiMTEiOiJTdGFydCJ9LCJ1bmxvY2tlZCI6ZmFsc2UsInBvaW50cyI6IjAiLCJib29wIjpmYWxzZX0sImMiOnsidXBncmFkZXMiOlsiMTEiXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImJ1eWFibGVzIjp7IjExIjoiMCJ9LCJjaGFsbGVuZ2VzIjp7IjExIjoiMCJ9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCIsImJlc3QiOiIxIiwidG90YWwiOiIwIiwiYmVlcCI6ZmFsc2UsInRoaW5neSI6InBvaW50eSIsIm90aGVyVGhpbmd5IjoxMCwic3BlbnRPbkJ1eWFibGVzIjoiMCJ9LCJhIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbIjExIl0sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCJ9LCJnIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJoIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJzcG9vayI6eyJ1cGdyYWRlcyI6W10sImFjaGlldmVtZW50cyI6W10sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9fSwib29tcHNNYWciOjAsImxhc3RQb2ludHMiOiIzMzAwLjM3Nzc3Mzg1OTA1NSJ9

View file

@ -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,

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>
<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>

View file

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

View file

@ -1,9 +1,13 @@
<template>
<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" />
</div>
<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" :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>

View file

@ -5,4 +5,9 @@
margin: 10px 0;
user-select: none;
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 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);

View file

@ -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 {

View file

@ -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>

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() {
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();
}
});
// 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;

View file

@ -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 />

View file

@ -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;

View file

@ -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",

View file

@ -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;
}

View file

@ -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({

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);

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 = {
publicPath: process.env.NODE_ENV === 'production'
? '/The-Modding-Tree-X'
: '/',
runtimeCompiler: true
publicPath: process.env.NODE_ENV === 'production' ? '/The-Modding-Tree-X' : '/',
runtimeCompiler: true
};