Merge branch 'feat/update-deps' into feat/board-rewrite
Some checks failed
Run Tests / test (pull_request) Failing after 1m43s
Some checks failed
Run Tests / test (pull_request) Failing after 1m43s
This commit is contained in:
commit
99511288c9
90 changed files with 5829 additions and 5075 deletions
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.eslintrc.cjs
|
|
@ -5,15 +5,22 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
node: true
|
node: true
|
||||||
},
|
},
|
||||||
extends: [
|
parser: '@typescript-eslint/parser',
|
||||||
"plugin:vue/vue3-essential",
|
plugins: ["@typescript-eslint"],
|
||||||
"@vue/eslint-config-typescript/recommended",
|
overrides: [
|
||||||
"@vue/eslint-config-prettier"
|
{
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
extends: [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"@vue/eslint-config-typescript/recommended",
|
||||||
|
"@vue/eslint-config-prettier"
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
project: "./tsconfig.json"
|
||||||
|
},
|
||||||
|
}
|
||||||
],
|
],
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
project: "tsconfig.json"
|
|
||||||
},
|
|
||||||
ignorePatterns: ["src/lib"],
|
ignorePatterns: ["src/lib"],
|
||||||
rules: {
|
rules: {
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
|
@ -27,6 +34,13 @@ module.exports = {
|
||||||
allowNullableObject: true,
|
allowNullableObject: true,
|
||||||
allowNullableBoolean: true
|
allowNullableBoolean: true
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"eqeqeq": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"null": "never"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
|
@ -8,6 +8,8 @@ jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:21-bullseye
|
||||||
steps:
|
steps:
|
||||||
- name: Setup RSync
|
- name: Setup RSync
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -7,15 +7,14 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:21-bullseye
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build --if-present
|
- run: npm run build --if-present
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
- run: npm run lint
|
||||||
|
|
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
|
@ -12,10 +12,11 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Use Node.js 16.x
|
- name: Use Node.js 21.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 21.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build --if-present
|
- run: npm run build --if-present
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
- run: npm run lint
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"vitest.commandLine": "npx vitest",
|
"vitest.commandLine": "npx vitest",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"git.ignoreLimitWarning": true,
|
"git.ignoreLimitWarning": true,
|
||||||
|
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -6,6 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.6.2] - 2024-04-01
|
||||||
|
### Added
|
||||||
|
- Export save button in error boundaries
|
||||||
|
- isRendered utility function
|
||||||
|
- Automatic galaxy.click cloud saves support
|
||||||
|
- Support for null and undefined in persistent refs
|
||||||
|
### Changes
|
||||||
|
- round, floor, ceil, trunc, and add now invert as no-ops
|
||||||
|
- "The Paper Pilot Community" renamed to "Profectus & Friends"
|
||||||
|
- Updated CI etc. to work with Forgejo
|
||||||
|
- Improved modifier typing
|
||||||
|
- Rename `printFormula` to `Formula.stringify`
|
||||||
|
### Fixed
|
||||||
|
- Hotkeys not working correctly with most combinations of modifiers
|
||||||
|
- Reset button using `currentAt` when not gaining
|
||||||
|
- Formulas not using modifiers that are disabled initially
|
||||||
|
- branchedResetPropagation logic being incorrect
|
||||||
|
- Fixed default elementsd in the main layer not updating Context when being added or removed
|
||||||
|
- Board links props not working in camelCase
|
||||||
|
- Board links absorbing pointer events
|
||||||
|
- Thrown errors not appearing in console
|
||||||
|
- Disabled elements would eat mouse events
|
||||||
|
- Fixed cost requirement without formula counting as being able to afford infinite purchases rather than just one
|
||||||
|
- Pinnable tooltips causing innocuous console error
|
||||||
|
- Bars with direction as "Left" wouldn't appear correctly
|
||||||
|
### Documentation
|
||||||
|
- Clarified expected progress values for board nodes
|
||||||
|
- Added CONTRIBUTING.md and enforce eslint on all PRs
|
||||||
|
### Tests
|
||||||
|
- Update formula test cases
|
||||||
|
- Tree reset propagation
|
||||||
|
|
||||||
|
Contributors: thepaperpilot, escapee, nif
|
||||||
|
|
||||||
## [0.6.1] - 2023-05-17
|
## [0.6.1] - 2023-05-17
|
||||||
### Added
|
### Added
|
||||||
- Error boundaries around each layer, and errors now display on the page when in development
|
- Error boundaries around each layer, and errors now display on the page when in development
|
||||||
|
|
31
CONTRIBUTING.md
Normal file
31
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Contributing to Profectus
|
||||||
|
|
||||||
|
Thank you for considering contributing to Profectus! We appreciate your interest in improving our project. Please take a moment to review the following guidelines to streamline the contribution process.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
For detailed instructions on setting up local development environment, please refer to the [Setup Guide](https://moddingtree.com/guide/getting-started/setup).
|
||||||
|
|
||||||
|
## Issue Reporting
|
||||||
|
|
||||||
|
If you encounter a bug or have a suggestion for improvement, please open an issue on Incremental Social. Provide as much detail as possible, including an example repo or steps to reproduce the issue if applicable.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Make sure to open your PR on [Incremental Social](https://code.incremental.social/profectus/Profectus) - the GitHub repo is just a mirror!
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
All PRs must be reviewed and approved by at least one of the project maintainers before merging. Please be patient during the review process and be open to feedback.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Ensure that your changes pass all existing tests and, if applicable, add new tests to cover the changes you've made. Run `npm run test` to run all the tests.
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
We use ESLint and Prettier to enforce consistent code style throughout the project. Before submitting a PR, run `npm run lint:fix` to automatically fix any linting issues.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing to Profectus, you agree that your contributions will be licensed under the project's [LICENSE](./LICENSE).
|
|
@ -8,6 +8,7 @@
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<link rel="alternate icon" type="image/png" sizes="48x48" href="/favicon.ico">
|
<link rel="alternate icon" type="image/png" sizes="48x48" href="/favicon.ico">
|
||||||
|
<link rel="mask-icon" href="/favicon.svg" color="#2E3440">
|
||||||
<meta name="theme-color" content="#2E3440">
|
<meta name="theme-color" content="#2E3440">
|
||||||
|
|
||||||
<title>Profectus</title>
|
<title>Profectus</title>
|
||||||
|
|
6738
package-lock.json
generated
6738
package-lock.json
generated
File diff suppressed because it is too large
Load diff
74
package.json
74
package.json
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "profectus",
|
"name": "profectus",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -9,49 +10,56 @@
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"testw": "vitest",
|
"testw": "vitest",
|
||||||
"serve": "vite preview --host"
|
"serve": "vite preview --host",
|
||||||
|
"lint": "eslint src --max-warnings 0",
|
||||||
|
"lint:fix": "eslint --fix --max-warnings 0 src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "^5.1.0",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^5.1.0",
|
||||||
"@pixi/app": "~6.3.2",
|
"@pixi/app": "^6.5.10",
|
||||||
"@pixi/constants": "~6.3.2",
|
"@pixi/constants": "~6.5.10",
|
||||||
"@pixi/core": "~6.3.2",
|
"@pixi/core": "^6.5.10",
|
||||||
"@pixi/display": "~6.3.2",
|
"@pixi/display": "~6.5.10",
|
||||||
"@pixi/math": "~6.3.2",
|
"@pixi/math": "~6.5.10",
|
||||||
"@pixi/particle-emitter": "^5.0.7",
|
"@pixi/particle-emitter": "^5.0.7",
|
||||||
"@pixi/sprite": "~6.3.2",
|
"@pixi/sprite": "~6.5.10",
|
||||||
"@pixi/ticker": "~6.3.2",
|
"@pixi/ticker": "~6.5.10",
|
||||||
"@vitejs/plugin-vue": "^2.3.3",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.5.0",
|
||||||
"nanoevents": "^6.0.2",
|
"nanoevents": "^9.0.0",
|
||||||
"vite": "^2.9.12",
|
"unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.1",
|
||||||
"vite-plugin-pwa": "^0.12.0",
|
"vite": "^5.1.8",
|
||||||
"vite-tsconfig-paths": "^3.5.0",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vue": "^3.2.26",
|
"vite-tsconfig-paths": "^4.3.0",
|
||||||
"vue-next-select": "^2.10.2",
|
"vue": "^3.5.12",
|
||||||
|
"vue-next-select": "^2.10.5",
|
||||||
"vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git",
|
"vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git",
|
||||||
"vue-textarea-autosize": "^1.1.1",
|
"vue-textarea-autosize": "^1.1.1",
|
||||||
"vue-toastification": "^2.0.0-rc.1",
|
"vue-toastification": "^2.0.0-rc.5",
|
||||||
"vue-transition-expand": "^0.1.0",
|
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||||
"@rushstack/eslint-patch": "^1.1.0",
|
"@rushstack/eslint-patch": "^1.7.2",
|
||||||
"@types/lz-string": "^1.3.34",
|
"@types/lz-string": "^1.5.0",
|
||||||
"@vue/eslint-config-prettier": "^7.0.0",
|
"@types/node": "^22.7.6",
|
||||||
"@vue/eslint-config-typescript": "^10.0.0",
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
"eslint": "^8.6.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"jsdom": "^20.0.0",
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
"prettier": "^2.5.1",
|
"eslint": "^8.57.0",
|
||||||
"typescript": "^5.0.2",
|
"jsdom": "^24.0.0",
|
||||||
"vitest": "^0.29.3",
|
"prettier": "^3.2.5",
|
||||||
"vue-tsc": "^0.38.1"
|
"typescript": "^5.4.2",
|
||||||
|
"vitest": "^1.4.0",
|
||||||
|
"vue-tsc": "^2.0.6"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "^4.24.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.x"
|
"node": "21.x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/App.vue
10
src/App.vue
|
@ -6,8 +6,10 @@
|
||||||
<Nav v-if="useHeader" />
|
<Nav v-if="useHeader" />
|
||||||
<Game />
|
<Game />
|
||||||
<TPS v-if="unref(showTPS)" />
|
<TPS v-if="unref(showTPS)" />
|
||||||
|
<AddictionWarning />
|
||||||
<GameOverScreen />
|
<GameOverScreen />
|
||||||
<NaNScreen />
|
<NaNScreen />
|
||||||
|
<CloudSaveResolver />
|
||||||
<component :is="gameComponent" />
|
<component :is="gameComponent" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -16,14 +18,16 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import "@fontsource/roboto-mono";
|
import "@fontsource/roboto-mono";
|
||||||
import Error from "components/Error.vue";
|
import Error from "components/Error.vue";
|
||||||
|
import AddictionWarning from "components/modals/AddictionWarning.vue";
|
||||||
|
import CloudSaveResolver from "components/modals/CloudSaveResolver.vue";
|
||||||
|
import GameOverScreen from "components/modals/GameOverScreen.vue";
|
||||||
|
import NaNScreen from "components/modals/NaNScreen.vue";
|
||||||
import { jsx } from "features/feature";
|
import { jsx } from "features/feature";
|
||||||
import state from "game/state";
|
import state from "game/state";
|
||||||
import { coerceComponent, render } from "util/vue";
|
import { coerceComponent, render } from "util/vue";
|
||||||
import { CSSProperties, watch } from "vue";
|
import type { CSSProperties } from "vue";
|
||||||
import { computed, toRef, unref } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
import Game from "./components/Game.vue";
|
import Game from "./components/Game.vue";
|
||||||
import GameOverScreen from "./components/GameOverScreen.vue";
|
|
||||||
import NaNScreen from "./components/NaNScreen.vue";
|
|
||||||
import Nav from "./components/Nav.vue";
|
import Nav from "./components/Nav.vue";
|
||||||
import TPS from "./components/TPS.vue";
|
import TPS from "./components/TPS.vue";
|
||||||
import projInfo from "./data/projInfo.json";
|
import projInfo from "./data/projInfo.json";
|
||||||
|
|
|
@ -29,14 +29,23 @@ import player from "game/player";
|
||||||
import { computed, toRef, unref } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
import Layer from "./Layer.vue";
|
import Layer from "./Layer.vue";
|
||||||
import Nav from "./Nav.vue";
|
import Nav from "./Nav.vue";
|
||||||
|
import { deepUnref } from "util/vue";
|
||||||
|
|
||||||
const tabs = toRef(player, "tabs");
|
const tabs = toRef(player, "tabs");
|
||||||
const layerKeys = computed(() => Object.keys(layers));
|
const layerKeys = computed(() => Object.keys(layers));
|
||||||
const useHeader = projInfo.useHeader;
|
const useHeader = projInfo.useHeader;
|
||||||
|
|
||||||
function gatherLayerProps(layer: GenericLayer) {
|
function gatherLayerProps(layer: GenericLayer) {
|
||||||
const { display, minimized, name, color, minimizable, nodes, minimizedDisplay } = layer;
|
const { display, name, color, minimizable, minimizedDisplay } = deepUnref(layer);
|
||||||
return { display, minimized, name, color, minimizable, nodes, minimizedDisplay };
|
return {
|
||||||
|
display,
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
minimizable,
|
||||||
|
minimizedDisplay,
|
||||||
|
minimized: layer.minimized,
|
||||||
|
nodes: layer.nodes
|
||||||
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -23,80 +23,48 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import type { CoercableComponent } from "features/feature";
|
import type { CoercableComponent } from "features/feature";
|
||||||
import type { FeatureNode } from "game/layers";
|
import type { FeatureNode } from "game/layers";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
import { computeComponent, computeOptionalComponent } from "util/vue";
|
||||||
import { PropType, Ref, computed, defineComponent, onErrorCaptured, ref, toRefs, unref } from "vue";
|
import { Ref, computed, onErrorCaptured, ref, toRef, unref } from "vue";
|
||||||
import Context from "./Context.vue";
|
import Context from "./Context.vue";
|
||||||
import ErrorVue from "./Error.vue";
|
import ErrorVue from "./Error.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: { Context, ErrorVue },
|
index: number;
|
||||||
props: {
|
display: CoercableComponent;
|
||||||
index: {
|
minimizedDisplay?: CoercableComponent;
|
||||||
type: Number,
|
minimized: Ref<boolean>;
|
||||||
required: true
|
name: string;
|
||||||
},
|
color?: string;
|
||||||
display: {
|
minimizable?: boolean;
|
||||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
nodes: Ref<Record<string, FeatureNode | undefined>>;
|
||||||
required: true
|
}>();
|
||||||
},
|
|
||||||
minimizedDisplay: processedPropType<CoercableComponent>(Object, String, Function),
|
|
||||||
minimized: {
|
|
||||||
type: Object as PropType<Ref<boolean>>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: processedPropType<string>(String),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
color: processedPropType<string>(String),
|
|
||||||
minimizable: processedPropType<boolean>(Boolean),
|
|
||||||
nodes: {
|
|
||||||
type: Object as PropType<Ref<Record<string, FeatureNode | undefined>>>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ["setMinimized"],
|
|
||||||
setup(props) {
|
|
||||||
const { display, index, minimized, minimizedDisplay } = toRefs(props);
|
|
||||||
|
|
||||||
const component = computeComponent(display);
|
const component = computeComponent(toRef(props, "display"));
|
||||||
const minimizedComponent = computeOptionalComponent(minimizedDisplay);
|
const minimizedComponent = computeOptionalComponent(toRef(props, "minimizedDisplay"));
|
||||||
const showGoBack = computed(
|
const showGoBack = computed(
|
||||||
() => projInfo.allowGoBack && index.value > 0 && !unwrapRef(minimized)
|
() => projInfo.allowGoBack && props.index > 0 && !unref(props.minimized)
|
||||||
);
|
);
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
player.tabs.splice(unref(props.index), Infinity);
|
player.tabs.splice(unref(props.index), Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNodes(nodes: Record<string, FeatureNode | undefined>) {
|
function updateNodes(nodes: Record<string, FeatureNode | undefined>) {
|
||||||
props.nodes.value = nodes;
|
props.nodes.value = nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = ref<Error[]>([]);
|
const errors = ref<Error[]>([]);
|
||||||
onErrorCaptured((err, instance, info) => {
|
onErrorCaptured((err, instance, info) => {
|
||||||
console.warn(`Error caught in "${props.name}" layer`, err, instance, info);
|
console.warn(`Error caught in "${props.name}" layer`, err, instance, info);
|
||||||
errors.value.push(
|
errors.value.push(
|
||||||
err instanceof Error ? (err as Error) : new Error(JSON.stringify(err))
|
err instanceof Error ? (err as Error) : new Error(JSON.stringify(err))
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
component,
|
|
||||||
minimizedComponent,
|
|
||||||
showGoBack,
|
|
||||||
updateNodes,
|
|
||||||
unref,
|
|
||||||
goBack,
|
|
||||||
errors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div @click="savesManager?.open()">
|
<div @click="savesManager?.open()">
|
||||||
<Tooltip display="Saves" :direction="Direction.Down" xoffset="-20px">
|
<Tooltip display="Saves" :direction="Direction.Down" xoffset="-20px">
|
||||||
<span class="material-icons">library_books</span>
|
<span class="material-icons" :class="{ needsSync }">library_books</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="options?.open()">
|
<div @click="options?.open()">
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div @click="savesManager?.open()">
|
<div @click="savesManager?.open()">
|
||||||
<Tooltip display="Saves" :direction="Direction.Right">
|
<Tooltip display="Saves" :direction="Direction.Right">
|
||||||
<span class="material-icons">library_books</span>
|
<span class="material-icons" :class="{ needsSync }">library_books</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="options?.open()">
|
<div @click="options?.open()">
|
||||||
|
@ -98,12 +98,14 @@
|
||||||
import Changelog from "data/Changelog.vue";
|
import Changelog from "data/Changelog.vue";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||||
|
import settings from "game/settings";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
|
import { galaxy, syncedSaves } from "util/galaxy";
|
||||||
import type { ComponentPublicInstance } from "vue";
|
import type { ComponentPublicInstance } from "vue";
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import Info from "./Info.vue";
|
import Info from "./modals/Info.vue";
|
||||||
import Options from "./Options.vue";
|
import Options from "./modals/Options.vue";
|
||||||
import SavesManager from "./SavesManager.vue";
|
import SavesManager from "./modals/SavesManager.vue";
|
||||||
|
|
||||||
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
||||||
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
||||||
|
@ -117,6 +119,10 @@ const { useHeader, banner, title, discordName, discordLink, versionNumber } = pr
|
||||||
function openDiscord() {
|
function openDiscord() {
|
||||||
window.open(discordLink, "mywindow");
|
window.open(discordLink, "mywindow");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const needsSync = computed(
|
||||||
|
() => galaxy.value?.loggedIn === true && !syncedSaves.value.includes(settings.active)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -264,4 +270,32 @@ function openDiscord() {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.needsSync {
|
||||||
|
color: var(--danger);
|
||||||
|
animation: 4s wiggle ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-3deg);
|
||||||
|
box-shadow: 0 2px 2px #0003;
|
||||||
|
}
|
||||||
|
5% {
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
15% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: rotate(-1deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(0);
|
||||||
|
box-shadow: 0 2px 2px #0003;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,10 +4,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RegisterNodeInjectionKey, UnregisterNodeInjectionKey } from "game/layers";
|
import { RegisterNodeInjectionKey, UnregisterNodeInjectionKey } from "game/layers";
|
||||||
import { computed, inject, onUnmounted, shallowRef, toRefs, unref, watch } from "vue";
|
import { computed, inject, onUnmounted, shallowRef, toRef, unref, watch } from "vue";
|
||||||
|
|
||||||
const _props = defineProps<{ id: string }>();
|
const props = defineProps<{ id: string }>();
|
||||||
const props = toRefs(_props);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const register = inject(RegisterNodeInjectionKey, () => {});
|
const register = inject(RegisterNodeInjectionKey, () => {});
|
||||||
|
@ -17,7 +16,7 @@ const unregister = inject(UnregisterNodeInjectionKey, () => {});
|
||||||
const node = shallowRef<HTMLElement | null>(null);
|
const node = shallowRef<HTMLElement | null>(null);
|
||||||
const parentNode = computed(() => node.value && node.value.parentElement);
|
const parentNode = computed(() => node.value && node.value.parentElement);
|
||||||
|
|
||||||
watch([parentNode, props.id], ([newNode, newID], [prevNode, prevID]) => {
|
watch([parentNode, toRef(props, "id")], ([newNode, newID], [prevNode, prevID]) => {
|
||||||
if (prevNode) {
|
if (prevNode) {
|
||||||
unregister(unref(prevID));
|
unregister(unref(prevID));
|
||||||
}
|
}
|
||||||
|
@ -26,7 +25,7 @@ watch([parentNode, props.id], ([newNode, newID], [prevNode, prevID]) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => unregister(unref(props.id)));
|
onUnmounted(() => unregister(props.id));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -10,13 +10,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, toRefs, unref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const props = defineProps<{
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
skipConfirm?: boolean;
|
skipConfirm?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "click"): void;
|
(e: "click"): void;
|
||||||
(e: "confirmingChanged", value: boolean): void;
|
(e: "confirmingChanged", value: boolean): void;
|
||||||
|
@ -29,7 +29,7 @@ watch(isConfirming, isConfirming => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function click() {
|
function click() {
|
||||||
if (unref(props.skipConfirm)) {
|
if (props.skipConfirm) {
|
||||||
emit("click");
|
emit("click");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,13 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activated = ref(false);
|
const activated = ref(false);
|
||||||
const activatedTimeout = ref<NodeJS.Timer | null>(null);
|
const activatedTimeout = ref<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
function click() {
|
function click() {
|
||||||
emit("click");
|
emit("click");
|
||||||
|
|
||||||
// Give feedback to user
|
// Give feedback to user
|
||||||
if (activatedTimeout.value) {
|
if (activatedTimeout.value != null) {
|
||||||
clearTimeout(activatedTimeout.value);
|
clearTimeout(activatedTimeout.value);
|
||||||
}
|
}
|
||||||
activated.value = false;
|
activated.value = false;
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import "components/common/fields.css";
|
import "components/common/fields.css";
|
||||||
import type { CoercableComponent } from "features/feature";
|
import type { CoercableComponent } from "features/feature";
|
||||||
import { computeOptionalComponent, unwrapRef } from "util/vue";
|
import { computeOptionalComponent } from "util/vue";
|
||||||
import { ref, toRef, watch } from "vue";
|
import { ref, toRef, unref, watch } from "vue";
|
||||||
import VueNextSelect from "vue-next-select";
|
import VueNextSelect from "vue-next-select";
|
||||||
import "vue-next-select/dist/index.css";
|
import "vue-next-select/dist/index.css";
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ const value = ref<SelectOption | null>(
|
||||||
props.options.find(option => option.value === props.modelValue) ?? null
|
props.options.find(option => option.value === props.modelValue) ?? null
|
||||||
);
|
);
|
||||||
watch(toRef(props, "modelValue"), modelValue => {
|
watch(toRef(props, "modelValue"), modelValue => {
|
||||||
if (unwrapRef(value) !== modelValue) {
|
if (unref(value) !== modelValue) {
|
||||||
value.value = props.options.find(option => option.value === modelValue) ?? null;
|
value.value = props.options.find(option => option.value === modelValue) ?? null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,22 +11,22 @@
|
||||||
import "components/common/fields.css";
|
import "components/common/fields.css";
|
||||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
import { computed, toRefs, unref } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const props = defineProps<{
|
||||||
title?: string;
|
title?: string;
|
||||||
modelValue?: number;
|
modelValue?: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update:modelValue", value: number): void;
|
(e: "update:modelValue", value: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get() {
|
get() {
|
||||||
return String(unref(props.modelValue) ?? 0);
|
return String(props.modelValue ?? 0);
|
||||||
},
|
},
|
||||||
set(value: string) {
|
set(value: string) {
|
||||||
emit("update:modelValue", Number(value));
|
emit("update:modelValue", Number(value));
|
||||||
|
|
84
src/components/modals/AddictionWarning.vue
Normal file
84
src/components/modals/AddictionWarning.vue
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<Modal v-model="isOpen" v-bind="$attrs">
|
||||||
|
<template v-slot:header>
|
||||||
|
<div class="vga-modal-header">
|
||||||
|
<h2>Kindly consider taking a break.</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body>
|
||||||
|
<p>
|
||||||
|
You've been actively enjoying this game for awhile recently - and it's great that
|
||||||
|
you've been having a good time! That said, there are dangers to games like these that you should be aware of:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
While incremental games can be fun and even healthy in certain contexts, they can
|
||||||
|
exacerbate video game addiction even more than other genres. If you feel like
|
||||||
|
playing incremental games is taking priority over other things in your life, or
|
||||||
|
manipulating your sleep schedule, it may be prudent to seek help.
|
||||||
|
</p>
|
||||||
|
<h4>Resources:</h4>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<a style="display: inline" href="https://www.samhsa.gov/" target="_blank">
|
||||||
|
SAMHSA
|
||||||
|
</a>
|
||||||
|
(<a style="display: inline" href="tel:1-800-662-4357">1-800-662-HELP</a>)
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<a href="https://www.reddit.com/r/StopGaming/">r/StopGaming</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<div class="vga-footer">
|
||||||
|
<button @click="neverShow" class="button">Never show this again</button>
|
||||||
|
<button @click="isOpen = false" class="button">Close</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
<SavesManager ref="savesManager" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import projInfo from "data/projInfo.json";
|
||||||
|
import settings from "game/settings";
|
||||||
|
import state from "game/state";
|
||||||
|
import { ref, watchEffect } from "vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
import SavesManager from "./SavesManager.vue";
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
watchEffect(() => {
|
||||||
|
if (
|
||||||
|
projInfo.disableHealthWarning === false &&
|
||||||
|
settings.showHealthWarning &&
|
||||||
|
state.mouseActivity.filter(i => i).length > 6
|
||||||
|
) {
|
||||||
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function neverShow() {
|
||||||
|
settings.showHealthWarning = false;
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vga-modal-header {
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vga-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vga-footer button {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
228
src/components/modals/CloudSaveResolver.vue
Normal file
228
src/components/modals/CloudSaveResolver.vue
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
<template>
|
||||||
|
<Modal v-model="isOpen" width="960px" ref="modal" :prevent-closing="true">
|
||||||
|
<template v-slot:header>
|
||||||
|
<div class="cloud-saves-modal-header">
|
||||||
|
<h2>Cloud {{ pluralizedSave }} loaded!</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body>
|
||||||
|
<div>
|
||||||
|
Upon loading, your cloud {{ pluralizedSave }}
|
||||||
|
{{ conflictingSaves.length > 1 ? "appear" : "appears" }} to be out of sync with your
|
||||||
|
local {{ pluralizedSave }}. Which
|
||||||
|
{{ pluralizedSave }}
|
||||||
|
do you want to keep?
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
v-for="(conflict, i) in unref(conflictingSaves)"
|
||||||
|
:key="conflict.id"
|
||||||
|
class="conflict-container"
|
||||||
|
>
|
||||||
|
<div @click="selectCloud(i)" :class="{ selected: selectedSaves[i] === 'cloud' }">
|
||||||
|
<h2>
|
||||||
|
Cloud
|
||||||
|
<span
|
||||||
|
v-if="(conflict.cloud.time ?? 0) > (conflict.local.time ?? 0)"
|
||||||
|
class="note"
|
||||||
|
>(more recent)</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
(conflict.cloud.timePlayed ?? 0) > (conflict.local.timePlayed ?? 0)
|
||||||
|
"
|
||||||
|
class="note"
|
||||||
|
>(more playtime)</span
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<Save :save="conflict.cloud" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
<div @click="selectLocal(i)" :class="{ selected: selectedSaves[i] === 'local' }">
|
||||||
|
<h2>
|
||||||
|
Local
|
||||||
|
<span
|
||||||
|
v-if="(conflict.cloud.time ?? 0) <= (conflict.local.time ?? 0)"
|
||||||
|
class="note"
|
||||||
|
>(more recent)</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
(conflict.cloud.timePlayed ?? 0) <= (conflict.local.timePlayed ?? 0)
|
||||||
|
"
|
||||||
|
class="note"
|
||||||
|
>(more playtime)</span
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<Save :save="conflict.local" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
@click="selectBoth(i)"
|
||||||
|
:class="{ selected: selectedSaves[i] === 'both' }"
|
||||||
|
style="flex-basis: 30%"
|
||||||
|
>
|
||||||
|
<h2>Both</h2>
|
||||||
|
<div class="save">Keep Both</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<div class="cloud-saves-footer">
|
||||||
|
<button @click="close" class="button">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { stringifySave } from "game/player";
|
||||||
|
import settings from "game/settings";
|
||||||
|
import LZString from "lz-string";
|
||||||
|
import { conflictingSaves, galaxy } from "util/galaxy";
|
||||||
|
import { getUniqueID, save, setupInitialStore } from "util/save";
|
||||||
|
import { ComponentPublicInstance, computed, ref, unref, watch } from "vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
import Save from "./Save.vue";
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
// True means replacing local save with cloud save
|
||||||
|
const selectedSaves = ref<("cloud" | "local" | "both")[]>([]);
|
||||||
|
|
||||||
|
const pluralizedSave = computed(() => (conflictingSaves.value.length > 1 ? "saves" : "save"));
|
||||||
|
|
||||||
|
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => conflictingSaves.value.length > 0,
|
||||||
|
shouldOpen => {
|
||||||
|
if (shouldOpen) {
|
||||||
|
selectedSaves.value = conflictingSaves.value.map(({ local, cloud }) => {
|
||||||
|
return (local.time ?? 0) < (cloud.time ?? 0) ? "cloud" : "local";
|
||||||
|
});
|
||||||
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modal.value?.isOpen,
|
||||||
|
open => {
|
||||||
|
if (open === false) {
|
||||||
|
conflictingSaves.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function selectLocal(index: number) {
|
||||||
|
selectedSaves.value[index] = "local";
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCloud(index: number) {
|
||||||
|
selectedSaves.value[index] = "cloud";
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBoth(index: number) {
|
||||||
|
selectedSaves.value[index] = "both";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
for (let i = 0; i < selectedSaves.value.length; i++) {
|
||||||
|
const { slot, local, cloud } = conflictingSaves.value[i];
|
||||||
|
switch (selectedSaves.value[i]) {
|
||||||
|
case "local":
|
||||||
|
// Replace cloud save with local
|
||||||
|
galaxy.value
|
||||||
|
?.save(
|
||||||
|
slot,
|
||||||
|
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
||||||
|
cloud.name
|
||||||
|
)
|
||||||
|
.catch(console.error);
|
||||||
|
break;
|
||||||
|
case "cloud":
|
||||||
|
// Replace local save with cloud
|
||||||
|
save(setupInitialStore(cloud));
|
||||||
|
break;
|
||||||
|
case "both":
|
||||||
|
// Get a new save ID for the cloud save, and sync the local one to the cloud
|
||||||
|
const id = getUniqueID();
|
||||||
|
save({ ...setupInitialStore(cloud), id });
|
||||||
|
settings.saves.push(id);
|
||||||
|
galaxy.value
|
||||||
|
?.save(
|
||||||
|
slot,
|
||||||
|
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
||||||
|
cloud.name
|
||||||
|
)
|
||||||
|
.catch(console.error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cloud-saves-modal-header {
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-saves-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-saves-footer button {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container > * {
|
||||||
|
flex-basis: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container + .conflict-container {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container h2 {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
height: 1.5em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-size: x-small;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save {
|
||||||
|
border: solid 4px var(--outline);
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--raised-background);
|
||||||
|
margin: var(--feature-margin);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 30px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.conflict-container .save {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container .selected .save {
|
||||||
|
border-color: var(--bought);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -37,14 +37,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from "components/Modal.vue";
|
|
||||||
import { hasWon } from "data/projEntry";
|
import { hasWon } from "data/projEntry";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import { formatTime } from "util/bignum";
|
import { formatTime } from "util/bignum";
|
||||||
import { loadSave, newSave } from "util/save";
|
import { loadSave, newSave } from "util/save";
|
||||||
import { computed, toRef } from "vue";
|
import { computed, toRef } from "vue";
|
||||||
import Toggle from "./fields/Toggle.vue";
|
import Toggle from "../fields/Toggle.vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
||||||
|
|
|
@ -60,7 +60,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import Modal from "components/Modal.vue";
|
|
||||||
import type Changelog from "data/Changelog.vue";
|
import type Changelog from "data/Changelog.vue";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import { jsx } from "features/feature";
|
import { jsx } from "features/feature";
|
||||||
|
@ -68,12 +67,12 @@ import player from "game/player";
|
||||||
import { infoComponents } from "game/settings";
|
import { infoComponents } from "game/settings";
|
||||||
import { formatTime } from "util/bignum";
|
import { formatTime } from "util/bignum";
|
||||||
import { coerceComponent, render } from "util/vue";
|
import { coerceComponent, render } from "util/vue";
|
||||||
import { computed, ref, toRefs, unref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
||||||
|
|
||||||
const _props = defineProps<{ changelog: typeof Changelog | null }>();
|
const props = defineProps<{ changelog: typeof Changelog | null }>();
|
||||||
const props = toRefs(_props);
|
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
|
|
||||||
|
@ -90,7 +89,7 @@ defineExpose({
|
||||||
});
|
});
|
||||||
|
|
||||||
function openChangelog() {
|
function openChangelog() {
|
||||||
unref(props.changelog)?.open();
|
props.changelog?.open();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
name="modal"
|
name="modal"
|
||||||
@before-enter="isAnimating = true"
|
@before-enter="isAnimating = true"
|
||||||
@after-leave="isAnimating = false"
|
@after-leave="isAnimating = false"
|
||||||
|
appear
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-mask"
|
class="modal-mask"
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<div class="modal-wrapper">
|
<div class="modal-wrapper">
|
||||||
<div class="modal-container">
|
<div class="modal-container" :width="width">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<slot name="header" :shown="isOpen"> default header </slot>
|
<slot name="header" :shown="isOpen"> default header </slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,20 +41,24 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FeatureNode } from "game/layers";
|
import type { FeatureNode } from "game/layers";
|
||||||
import { computed, ref, toRefs, unref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import Context from "./Context.vue";
|
import Context from "../Context.vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
|
preventClosing?: boolean;
|
||||||
|
width?: string;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update:modelValue", value: boolean): void;
|
(e: "update:modelValue", value: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isOpen = computed(() => unref(props.modelValue) || isAnimating.value);
|
const isOpen = computed(() => props.modelValue || isAnimating.value);
|
||||||
function close() {
|
function close() {
|
||||||
emit("update:modelValue", false);
|
if (props.preventClosing !== true) {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAnimating = ref(false);
|
const isAnimating = ref(false);
|
|
@ -46,7 +46,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from "components/Modal.vue";
|
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import state from "game/state";
|
import state from "game/state";
|
||||||
|
@ -54,7 +53,8 @@ import type { DecimalSource } from "util/bignum";
|
||||||
import Decimal, { format } from "util/bignum";
|
import Decimal, { format } from "util/bignum";
|
||||||
import type { ComponentPublicInstance } from "vue";
|
import type { ComponentPublicInstance } from "vue";
|
||||||
import { computed, ref, toRef, watch } from "vue";
|
import { computed, ref, toRef, watch } from "vue";
|
||||||
import Toggle from "./fields/Toggle.vue";
|
import Toggle from "../fields/Toggle.vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
import SavesManager from "./SavesManager.vue";
|
import SavesManager from "./SavesManager.vue";
|
||||||
|
|
||||||
const { discordName, discordLink } = projInfo;
|
const { discordName, discordLink } = projInfo;
|
|
@ -14,6 +14,7 @@
|
||||||
<Toggle :title="unthrottledTitle" v-model="unthrottled" />
|
<Toggle :title="unthrottledTitle" v-model="unthrottled" />
|
||||||
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
|
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
|
||||||
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
|
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
|
||||||
|
<Toggle :title="showHealthWarningTitle" v-model="showHealthWarning" v-if="!projInfo.disableHealthWarning" />
|
||||||
<Toggle :title="autosaveTitle" v-model="autosave" />
|
<Toggle :title="autosaveTitle" v-model="autosave" />
|
||||||
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
|
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,20 +29,20 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import Modal from "components/Modal.vue";
|
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import { save } from "util/save";
|
|
||||||
import rawThemes from "data/themes";
|
import rawThemes from "data/themes";
|
||||||
import { jsx } from "features/feature";
|
import { jsx } from "features/feature";
|
||||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import settings, { settingFields } from "game/settings";
|
import settings, { settingFields } from "game/settings";
|
||||||
import { camelToTitle, Direction } from "util/common";
|
import { camelToTitle, Direction } from "util/common";
|
||||||
|
import { save } from "util/save";
|
||||||
import { coerceComponent, render } from "util/vue";
|
import { coerceComponent, render } from "util/vue";
|
||||||
import { computed, ref, toRefs } from "vue";
|
import { computed, ref, toRefs } from "vue";
|
||||||
import Select from "./fields/Select.vue";
|
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||||
import Toggle from "./fields/Toggle.vue";
|
import Select from "../fields/Select.vue";
|
||||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
import Toggle from "../fields/Toggle.vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
const currentTab = ref("behaviour");
|
const currentTab = ref("behaviour");
|
||||||
|
@ -72,7 +73,7 @@ const settingFieldsComponent = computed(() => {
|
||||||
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
|
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
|
||||||
});
|
});
|
||||||
|
|
||||||
const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings);
|
const { showTPS, theme, unthrottled, alignUnits, showHealthWarning } = toRefs(settings);
|
||||||
const { autosave, offlineProd } = toRefs(player);
|
const { autosave, offlineProd } = toRefs(player);
|
||||||
const isPaused = computed({
|
const isPaused = computed({
|
||||||
get() {
|
get() {
|
||||||
|
@ -91,10 +92,16 @@ const unthrottledTitle = jsx(() => (
|
||||||
));
|
));
|
||||||
const offlineProdTitle = jsx(() => (
|
const offlineProdTitle = jsx(() => (
|
||||||
<span class="option-title">
|
<span class="option-title">
|
||||||
Offline Production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
Offline production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||||
<desc>Simulate production that occurs while the game is closed.</desc>
|
<desc>Simulate production that occurs while the game is closed.</desc>
|
||||||
</span>
|
</span>
|
||||||
));
|
));
|
||||||
|
const showHealthWarningTitle = jsx(() => (
|
||||||
|
<span class="option-title">
|
||||||
|
Show videogame addiction warning
|
||||||
|
<desc>Show a helpful warning after playing for a long time about video game addiction and encouraging you to take a break.</desc>
|
||||||
|
</span>
|
||||||
|
));
|
||||||
const autosaveTitle = jsx(() => (
|
const autosaveTitle = jsx(() => (
|
||||||
<span class="option-title">
|
<span class="option-title">
|
||||||
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="save" :class="{ active: isActive }">
|
<div class="save" :class="{ active: isActive, readonly }">
|
||||||
<div class="handle material-icons">drag_handle</div>
|
<div class="handle material-icons" v-if="readonly !== true">drag_handle</div>
|
||||||
<div class="actions" v-if="!isEditing">
|
<div class="actions" v-if="!isEditing && readonly !== true">
|
||||||
<FeedbackButton
|
<FeedbackButton
|
||||||
@click="emit('export')"
|
@click="emit('export')"
|
||||||
class="button"
|
class="button"
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" v-else>
|
<div class="actions" v-else-if="readonly !== true">
|
||||||
<button @click="changeName" class="button">
|
<button @click="changeName" class="button">
|
||||||
<Tooltip display="Save" :direction="Direction.Left" class="info">
|
<Tooltip display="Save" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons">check</span>
|
<span class="material-icons">check</span>
|
||||||
|
@ -53,12 +53,17 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="details" v-if="save.error == undefined && !isEditing">
|
<div class="details" v-if="save.error == undefined && !isEditing">
|
||||||
<button class="button open" @click="emit('open')">
|
<Tooltip display="Synced!" :direction="Direction.Right" v-if="synced"
|
||||||
|
><span class="material-icons synced">cloud</span></Tooltip
|
||||||
|
>
|
||||||
|
<button class="button open" @click="emit('open')" :disabled="readonly">
|
||||||
<h3>{{ save.name }}</h3>
|
<h3>{{ save.name }}</h3>
|
||||||
</button>
|
</button>
|
||||||
<span class="save-version">v{{ save.modVersion }}</span
|
<span class="save-version">v{{ save.modVersion }}</span
|
||||||
><br />
|
><br />
|
||||||
<div v-if="currentTime">Last played {{ dateFormat.format(currentTime) }}</div>
|
<div v-if="currentTime" class="time">
|
||||||
|
Last played {{ dateFormat.format(currentTime) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="details" v-else-if="save.error == undefined && isEditing">
|
<div class="details" v-else-if="save.error == undefined && isEditing">
|
||||||
<Text v-model="newName" class="editname" @submit="changeName" />
|
<Text v-model="newName" class="editname" @submit="changeName" />
|
||||||
|
@ -73,16 +78,18 @@
|
||||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
import { computed, ref, toRefs, watch } from "vue";
|
import { galaxy, syncedSaves } from "util/galaxy";
|
||||||
import DangerButton from "./fields/DangerButton.vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
import DangerButton from "../fields/DangerButton.vue";
|
||||||
import Text from "./fields/Text.vue";
|
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||||
|
import Text from "../fields/Text.vue";
|
||||||
import type { LoadablePlayerData } from "./SavesManager.vue";
|
import type { LoadablePlayerData } from "./SavesManager.vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const props = defineProps<{
|
||||||
save: LoadablePlayerData;
|
save: LoadablePlayerData;
|
||||||
|
readonly?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const { save } = toRefs(_props);
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "export"): void;
|
(e: "export"): void;
|
||||||
(e: "open"): void;
|
(e: "open"): void;
|
||||||
|
@ -104,11 +111,19 @@ const isEditing = ref(false);
|
||||||
const isConfirming = ref(false);
|
const isConfirming = ref(false);
|
||||||
const newName = ref("");
|
const newName = ref("");
|
||||||
|
|
||||||
watch(isEditing, () => (newName.value = save.value.name ?? ""));
|
watch(isEditing, () => (newName.value = props.save.name ?? ""));
|
||||||
|
|
||||||
const isActive = computed(() => save.value != null && save.value.id === player.id);
|
const isActive = computed(
|
||||||
|
() => props.save != null && props.save.id === player.id && !props.readonly
|
||||||
|
);
|
||||||
const currentTime = computed(() =>
|
const currentTime = computed(() =>
|
||||||
isActive.value ? player.time : (save.value != null && save.value.time) ?? 0
|
isActive.value ? player.time : (props.save != null && props.save.time) ?? 0
|
||||||
|
);
|
||||||
|
const synced = computed(
|
||||||
|
() =>
|
||||||
|
!props.readonly &&
|
||||||
|
galaxy.value?.loggedIn === true &&
|
||||||
|
syncedSaves.value.includes(props.save.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
function changeName() {
|
function changeName() {
|
||||||
|
@ -139,6 +154,13 @@ function changeName() {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.open:disabled {
|
||||||
|
cursor: inherit;
|
||||||
|
color: var(--foreground);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -152,6 +174,10 @@ function changeName() {
|
||||||
margin-right: 80px;
|
margin-right: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save.readonly .details {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
|
@ -176,6 +202,17 @@ function changeName() {
|
||||||
.editname {
|
.editname {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.synced {
|
||||||
|
font-size: 100%;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -201,4 +238,8 @@ function changeName() {
|
||||||
.save .field {
|
.save .field {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details > .tooltip-container {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -4,6 +4,9 @@
|
||||||
<h2>Saves Manager</h2>
|
<h2>Saves Manager</h2>
|
||||||
</template>
|
</template>
|
||||||
<template #body="{ shown }">
|
<template #body="{ shown }">
|
||||||
|
<div v-if="showNotSyncedWarning" style="color: var(--danger)">
|
||||||
|
Not all saves are synced! You may need to delete stale saves.
|
||||||
|
</div>
|
||||||
<Draggable
|
<Draggable
|
||||||
:list="settings.saves"
|
:list="settings.saves"
|
||||||
handle=".handle"
|
handle=".handle"
|
||||||
|
@ -57,18 +60,28 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from "components/Modal.vue";
|
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import type { Player } from "game/player";
|
import type { Player } from "game/player";
|
||||||
import player, { stringifySave } from "game/player";
|
import player, { stringifySave } from "game/player";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
import { getUniqueID, loadSave, newSave, save } from "util/save";
|
import { galaxy, syncedSaves } from "util/galaxy";
|
||||||
|
import {
|
||||||
|
clearCachedSave,
|
||||||
|
clearCachedSaves,
|
||||||
|
decodeSave,
|
||||||
|
getCachedSave,
|
||||||
|
getUniqueID,
|
||||||
|
loadSave,
|
||||||
|
newSave,
|
||||||
|
save
|
||||||
|
} from "util/save";
|
||||||
import type { ComponentPublicInstance } from "vue";
|
import type { ComponentPublicInstance } from "vue";
|
||||||
import { computed, nextTick, ref, shallowReactive, watch } from "vue";
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import Draggable from "vuedraggable";
|
import Draggable from "vuedraggable";
|
||||||
import Select from "./fields/Select.vue";
|
import Select from "../fields/Select.vue";
|
||||||
import Text from "./fields/Text.vue";
|
import Text from "../fields/Text.vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
import Save from "./Save.vue";
|
import Save from "./Save.vue";
|
||||||
|
|
||||||
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
|
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
|
||||||
|
@ -90,16 +103,8 @@ watch(saveToImport, importedSave => {
|
||||||
if (importedSave) {
|
if (importedSave) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
try {
|
try {
|
||||||
if (importedSave[0] === "{") {
|
importedSave = decodeSave(importedSave) ?? "";
|
||||||
// plaintext. No processing needed
|
if (importedSave === "") {
|
||||||
} else if (importedSave[0] === "e") {
|
|
||||||
// Assumed to be base64, which starts with e
|
|
||||||
importedSave = decodeURIComponent(escape(atob(importedSave)));
|
|
||||||
} else if (importedSave[0] === "ᯡ") {
|
|
||||||
// Assumed to be lz, which starts with ᯡ
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
importedSave = LZString.decompressFromUTF16(importedSave)!;
|
|
||||||
} else {
|
|
||||||
console.warn("Unable to determine preset encoding", importedSave);
|
console.warn("Unable to determine preset encoding", importedSave);
|
||||||
importingFailed.value = true;
|
importingFailed.value = true;
|
||||||
return;
|
return;
|
||||||
|
@ -125,7 +130,7 @@ watch(saveToImport, importedSave => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let bankContext = import.meta.globEager("./../../saves/*.txt", { as: "raw" });
|
let bankContext = import.meta.glob("./../../../saves/*.txt", { query: "?raw", eager: true });
|
||||||
let bank = ref(
|
let bank = ref(
|
||||||
Object.keys(bankContext).reduce((acc: Array<{ label: string; value: string }>, curr) => {
|
Object.keys(bankContext).reduce((acc: Array<{ label: string; value: string }>, curr) => {
|
||||||
acc.push({
|
acc.push({
|
||||||
|
@ -139,48 +144,10 @@ let bank = ref(
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
|
|
||||||
function getCachedSave(id: string) {
|
|
||||||
if (cachedSaves[id] == null) {
|
|
||||||
let save = localStorage.getItem(id);
|
|
||||||
if (save == null) {
|
|
||||||
cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
|
|
||||||
} else if (save === "dW5kZWZpbmVk") {
|
|
||||||
cachedSaves[id] = { error: `Save is undefined`, id };
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
if (save[0] === "{") {
|
|
||||||
// plaintext. No processing needed
|
|
||||||
} else if (save[0] === "e") {
|
|
||||||
// Assumed to be base64, which starts with e
|
|
||||||
save = decodeURIComponent(escape(atob(save)));
|
|
||||||
} else if (save[0] === "ᯡ") {
|
|
||||||
// Assumed to be lz, which starts with ᯡ
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
save = LZString.decompressFromUTF16(save)!;
|
|
||||||
} else {
|
|
||||||
console.warn("Unable to determine preset encoding", save);
|
|
||||||
importingFailed.value = true;
|
|
||||||
cachedSaves[id] = { error: "Unable to determine preset encoding", id };
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
return cachedSaves[id]!;
|
|
||||||
}
|
|
||||||
cachedSaves[id] = { ...JSON.parse(save), id };
|
|
||||||
} catch (error) {
|
|
||||||
cachedSaves[id] = { error, id };
|
|
||||||
console.warn(
|
|
||||||
`SavesManager: Failed to load info about save with id ${id}:\n${error}\n${save}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
return cachedSaves[id]!;
|
|
||||||
}
|
|
||||||
// Wipe cache whenever the modal is opened
|
// Wipe cache whenever the modal is opened
|
||||||
watch(isOpen, isOpen => {
|
watch(isOpen, isOpen => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
Object.keys(cachedSaves).forEach(key => delete cachedSaves[key]);
|
clearCachedSaves();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -191,6 +158,10 @@ const saves = computed(() =>
|
||||||
}, {})
|
}, {})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showNotSyncedWarning = computed(
|
||||||
|
() => galaxy.value?.loggedIn === true && settings.saves.length < syncedSaves.value.length
|
||||||
|
);
|
||||||
|
|
||||||
function exportSave(id: string) {
|
function exportSave(id: string) {
|
||||||
let saveToExport;
|
let saveToExport;
|
||||||
if (player.id === id) {
|
if (player.id === id) {
|
||||||
|
@ -233,20 +204,37 @@ function duplicateSave(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSave(id: string) {
|
function deleteSave(id: string) {
|
||||||
|
if (galaxy.value?.loggedIn === true) {
|
||||||
|
galaxy.value.getSaveList().then(list => {
|
||||||
|
const slot = Object.keys(list).find(slot => {
|
||||||
|
const content = list[slot as unknown as number].content;
|
||||||
|
try {
|
||||||
|
if (JSON.parse(content).id === id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (slot != null) {
|
||||||
|
galaxy.value?.save(parseInt(slot), "", "").catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
settings.saves = settings.saves.filter((save: string) => save !== id);
|
settings.saves = settings.saves.filter((save: string) => save !== id);
|
||||||
localStorage.removeItem(id);
|
localStorage.removeItem(id);
|
||||||
cachedSaves[id] = undefined;
|
clearCachedSave(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSave(id: string) {
|
function openSave(id: string) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
saves.value[player.id]!.time = player.time;
|
saves.value[player.id]!.time = player.time;
|
||||||
save();
|
save();
|
||||||
cachedSaves[player.id] = undefined;
|
clearCachedSave(player.id);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
loadSave(saves.value[id]!);
|
loadSave(saves.value[id]!);
|
||||||
// Delete cached version in case of opening it again
|
// Delete cached version in case of opening it again
|
||||||
cachedSaves[id] = undefined;
|
clearCachedSave(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function newFromPreset(preset: string) {
|
function newFromPreset(preset: string) {
|
||||||
|
@ -256,16 +244,8 @@ function newFromPreset(preset: string) {
|
||||||
selectedPreset.value = null;
|
selectedPreset.value = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (preset[0] === "{") {
|
preset = decodeSave(preset) ?? "";
|
||||||
// plaintext. No processing needed
|
if (preset === "") {
|
||||||
} else if (preset[0] === "e") {
|
|
||||||
// Assumed to be base64, which starts with e
|
|
||||||
preset = decodeURIComponent(escape(atob(preset)));
|
|
||||||
} else if (preset[0] === "ᯡ") {
|
|
||||||
// Assumed to be lz, which starts with ᯡ
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
preset = LZString.decompressFromUTF16(preset)!;
|
|
||||||
} else {
|
|
||||||
console.warn("Unable to determine preset encoding", preset);
|
console.warn("Unable to determine preset encoding", preset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -287,7 +267,7 @@ function editSave(id: string, newName: string) {
|
||||||
save();
|
save();
|
||||||
} else {
|
} else {
|
||||||
save(currSave as Player);
|
save(currSave as Player);
|
||||||
cachedSaves[id] = undefined;
|
clearCachedSave(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,7 +19,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Modal from "components/Modal.vue";
|
import Modal from "components/modals/Modal.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
|
|
|
@ -128,7 +128,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
||||||
)}
|
)}
|
||||||
</b>{" "}
|
</b>{" "}
|
||||||
{resetButton.conversion.gainResource.displayName}
|
{resetButton.conversion.gainResource.displayName}
|
||||||
{unref(resetButton.showNextAt) != null ? (
|
{unref(resetButton.showNextAt as ProcessedComputable<boolean>) != null ? (
|
||||||
<div>
|
<div>
|
||||||
<br />
|
<br />
|
||||||
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
||||||
|
|
|
@ -88,6 +88,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["base64", "lz", "plain"],
|
"enum": ["base64", "lz", "plain"],
|
||||||
"description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports."
|
"description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports."
|
||||||
|
},
|
||||||
|
"disableHealthWarning": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether or not to disable the health warning that appears to the player after excessive playtime (activity during 6 of the last 8 hours). If left enabled, the player will still be able to individually turn off the health warning in settings or by clicking \"Do not show again\" in the warning itself."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -22,5 +22,6 @@
|
||||||
"maxTickLength": 3600,
|
"maxTickLength": 3600,
|
||||||
"offlineLimit": 1,
|
"offlineLimit": 1,
|
||||||
"enablePausing": true,
|
"enablePausing": true,
|
||||||
"exportEncoding": "base64"
|
"exportEncoding": "base64",
|
||||||
|
"disableHealthWarning": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,89 +23,62 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script setup lang="tsx">
|
||||||
import "components/common/features.css";
|
import "components/common/features.css";
|
||||||
import MarkNode from "components/MarkNode.vue";
|
|
||||||
import Node from "components/Node.vue";
|
|
||||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||||
import { displayRequirements, Requirements } from "game/requirements";
|
import { displayRequirements, Requirements } from "game/requirements";
|
||||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||||
import { Component, defineComponent, shallowRef, StyleValue, toRefs, unref, UnwrapRef, watchEffect } from "vue";
|
import { Component, shallowRef, StyleValue, unref, UnwrapRef, watchEffect } from "vue";
|
||||||
import { GenericAchievement } from "./achievement";
|
import { GenericAchievement } from "./achievement";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
visibility: Visibility | boolean;
|
||||||
visibility: {
|
display?: UnwrapRef<GenericAchievement["display"]>;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
earned: boolean;
|
||||||
required: true
|
requirements?: Requirements;
|
||||||
},
|
image?: string;
|
||||||
display: processedPropType<UnwrapRef<GenericAchievement["display"]>>(Object, String, Function),
|
style?: StyleValue;
|
||||||
earned: {
|
classes?: Record<string, boolean>;
|
||||||
type: processedPropType<boolean>(Boolean),
|
mark?: boolean | string;
|
||||||
required: true
|
small?: boolean;
|
||||||
},
|
id: string;
|
||||||
requirements: processedPropType<Requirements>(Object, Array),
|
}>();
|
||||||
image: processedPropType<string>(String),
|
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
mark: processedPropType<boolean | string>(Boolean, String),
|
|
||||||
small: processedPropType<boolean>(Boolean),
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Node,
|
|
||||||
MarkNode
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { display, requirements, earned } = toRefs(props);
|
|
||||||
|
|
||||||
const comp = shallowRef<Component | string>("");
|
const comp = shallowRef<Component | string>("");
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const currDisplay = unwrapRef(display);
|
const currDisplay = props.display;
|
||||||
if (currDisplay == null) {
|
if (currDisplay == null) {
|
||||||
comp.value = "";
|
comp.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
if (isCoercableComponent(currDisplay)) {
|
|
||||||
comp.value = coerceComponent(currDisplay);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Requirement = coerceComponent(currDisplay.requirement ? currDisplay.requirement : jsx(() => displayRequirements(unwrapRef(requirements) ?? [])), "h3");
|
|
||||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
|
|
||||||
const OptionsDisplay = unwrapRef(earned) ?
|
|
||||||
coerceComponent(currDisplay.optionsDisplay || "", "span") :
|
|
||||||
"";
|
|
||||||
comp.value = coerceComponent(
|
|
||||||
jsx(() => (
|
|
||||||
<span>
|
|
||||||
<Requirement />
|
|
||||||
{currDisplay.effectDisplay != null ? (
|
|
||||||
<div>
|
|
||||||
<EffectDisplay />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{currDisplay.optionsDisplay != null ? (
|
|
||||||
<div class="equal-spaced">
|
|
||||||
<OptionsDisplay />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
comp,
|
|
||||||
unref,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (isCoercableComponent(currDisplay)) {
|
||||||
|
comp.value = coerceComponent(currDisplay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Requirement = coerceComponent(currDisplay.requirement ? currDisplay.requirement :
|
||||||
|
jsx(() => displayRequirements(props.requirements ?? [])), "h3");
|
||||||
|
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
|
||||||
|
const OptionsDisplay = props.earned ?
|
||||||
|
coerceComponent(currDisplay.optionsDisplay || "", "span") :
|
||||||
|
"";
|
||||||
|
comp.value = coerceComponent(
|
||||||
|
jsx(() => (
|
||||||
|
<span>
|
||||||
|
<Requirement />
|
||||||
|
{currDisplay.effectDisplay != null ? (
|
||||||
|
<div>
|
||||||
|
<EffectDisplay />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{currDisplay.optionsDisplay != null ? (
|
||||||
|
<div class="equal-spaced">
|
||||||
|
<OptionsDisplay />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { computed } from "@vue/reactivity";
|
import { computed } from "vue";
|
||||||
import { isArray } from "@vue/shared";
|
|
||||||
import Select from "components/fields/Select.vue";
|
import Select from "components/fields/Select.vue";
|
||||||
import AchievementComponent from "features/achievements/Achievement.vue";
|
import AchievementComponent from "features/achievements/Achievement.vue";
|
||||||
import { GenericDecorator } from "features/decorators/common";
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
|
@ -208,7 +207,7 @@ export function createAchievement<T extends AchievementOptions>(
|
||||||
unref(achievement.earned) &&
|
unref(achievement.earned) &&
|
||||||
!(
|
!(
|
||||||
display != null &&
|
display != null &&
|
||||||
typeof display == "object" &&
|
typeof display === "object" &&
|
||||||
"optionsDisplay" in (display as Record<string, unknown>)
|
"optionsDisplay" in (display as Record<string, unknown>)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -275,7 +274,7 @@ export function createAchievement<T extends AchievementOptions>(
|
||||||
const requirements = [
|
const requirements = [
|
||||||
createVisibilityRequirement(genericAchievement),
|
createVisibilityRequirement(genericAchievement),
|
||||||
createBooleanRequirement(() => !genericAchievement.earned.value),
|
createBooleanRequirement(() => !genericAchievement.earned.value),
|
||||||
...(isArray(achievement.requirements)
|
...(Array.isArray(achievement.requirements)
|
||||||
? achievement.requirements
|
? achievement.requirements
|
||||||
: [achievement.requirements])
|
: [achievement.requirements])
|
||||||
];
|
];
|
||||||
|
@ -306,18 +305,20 @@ const msDisplayOptions = Object.values(AchievementDisplay).map(option => ({
|
||||||
value: option
|
value: option
|
||||||
}));
|
}));
|
||||||
|
|
||||||
registerSettingField(
|
globalBus.on("setupVue", () =>
|
||||||
jsx(() => (
|
registerSettingField(
|
||||||
<Select
|
jsx(() => (
|
||||||
title={jsx(() => (
|
<Select
|
||||||
<span class="option-title">
|
title={jsx(() => (
|
||||||
Show achievements
|
<span class="option-title">
|
||||||
<desc>Select which achievements to display based on criterias.</desc>
|
Show achievements
|
||||||
</span>
|
<desc>Select which achievements to display based on criterias.</desc>
|
||||||
))}
|
</span>
|
||||||
options={msDisplayOptions}
|
))}
|
||||||
onUpdate:modelValue={value => (settings.msDisplay = value as AchievementDisplay)}
|
options={msDisplayOptions}
|
||||||
modelValue={settings.msDisplay}
|
onUpdate:modelValue={value => (settings.msDisplay = value as AchievementDisplay)}
|
||||||
/>
|
modelValue={settings.msDisplay}
|
||||||
))
|
/>
|
||||||
|
))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { isArray } from "@vue/shared";
|
|
||||||
import ClickableComponent from "features/clickables/Clickable.vue";
|
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
@ -31,7 +30,7 @@ import { coerceComponent, isCoercableComponent, render } from "util/vue";
|
||||||
import { computed, Ref, ref, unref } from "vue";
|
import { computed, Ref, ref, unref } from "vue";
|
||||||
import { BarOptions, createBar, GenericBar } from "./bars/bar";
|
import { BarOptions, createBar, GenericBar } from "./bars/bar";
|
||||||
import { ClickableOptions } from "./clickables/clickable";
|
import { ClickableOptions } from "./clickables/clickable";
|
||||||
import { Decorator, GenericDecorator } from "./decorators/common";
|
import { GenericDecorator } from "./decorators/common";
|
||||||
|
|
||||||
/** A symbol used to identify {@link Action} features. */
|
/** A symbol used to identify {@link Action} features. */
|
||||||
export const ActionType = Symbol("Action");
|
export const ActionType = Symbol("Action");
|
||||||
|
@ -157,7 +156,7 @@ export function createAction<T extends ActionOptions>(
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const originalStyle = unref(style);
|
const originalStyle = unref(style);
|
||||||
if (isArray(originalStyle)) {
|
if (Array.isArray(originalStyle)) {
|
||||||
currStyle.push(...originalStyle);
|
currStyle.push(...originalStyle);
|
||||||
} else if (originalStyle != null) {
|
} else if (originalStyle != null) {
|
||||||
currStyle.push(originalStyle);
|
currStyle.push(originalStyle);
|
||||||
|
@ -219,7 +218,7 @@ export function createAction<T extends ActionOptions>(
|
||||||
|
|
||||||
const onClick = action.onClick.bind(action);
|
const onClick = action.onClick.bind(action);
|
||||||
action.onClick = function () {
|
action.onClick = function () {
|
||||||
if (unref(action.canClick) === false) {
|
if (unref(action.canClick as ProcessedComputable<boolean>) === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const amount = Decimal.div(progress.value, unref(genericAction.duration));
|
const amount = Decimal.div(progress.value, unref(genericAction.duration));
|
||||||
|
|
|
@ -41,107 +41,68 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import MarkNode from "components/MarkNode.vue";
|
|
||||||
import Node from "components/Node.vue";
|
|
||||||
import { CoercableComponent, isHidden, isVisible, Visibility } from "features/feature";
|
import { CoercableComponent, isHidden, isVisible, Visibility } from "features/feature";
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
import Decimal from "util/bignum";
|
import Decimal from "util/bignum";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
import { computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
import { computeOptionalComponent } from "util/vue";
|
||||||
import type { CSSProperties, StyleValue } from "vue";
|
import type { CSSProperties, StyleValue } from "vue";
|
||||||
import { computed, defineComponent, toRefs, unref } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
progress: DecimalSource;
|
||||||
progress: {
|
width: number;
|
||||||
type: processedPropType<DecimalSource>(String, Object, Number),
|
height: number;
|
||||||
required: true
|
direction: Direction;
|
||||||
},
|
display?: CoercableComponent;
|
||||||
width: {
|
visibility: Visibility | boolean;
|
||||||
type: processedPropType<number>(Number),
|
style?: StyleValue;
|
||||||
required: true
|
classes?: Record<string, boolean>;
|
||||||
},
|
borderStyle?: StyleValue;
|
||||||
height: {
|
textStyle?: StyleValue;
|
||||||
type: processedPropType<number>(Number),
|
baseStyle?: StyleValue;
|
||||||
required: true
|
fillStyle?: StyleValue;
|
||||||
},
|
mark?: boolean | string;
|
||||||
direction: {
|
id: string;
|
||||||
type: processedPropType<Direction>(String),
|
}>();
|
||||||
required: true
|
|
||||||
},
|
|
||||||
display: processedPropType<CoercableComponent>(Object, String, Function),
|
|
||||||
visibility: {
|
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
style: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
borderStyle: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
textStyle: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
baseStyle: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
fillStyle: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
mark: processedPropType<boolean | string>(Boolean, String),
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
MarkNode,
|
|
||||||
Node
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { progress, width, height, direction, display } = toRefs(props);
|
|
||||||
|
|
||||||
const normalizedProgress = computed(() => {
|
const normalizedProgress = computed(() => {
|
||||||
let progressNumber =
|
let progressNumber =
|
||||||
progress.value instanceof Decimal
|
props.progress instanceof Decimal
|
||||||
? progress.value.toNumber()
|
? props.progress.toNumber()
|
||||||
: Number(progress.value);
|
: Number(props.progress);
|
||||||
return (1 - Math.min(Math.max(progressNumber, 0), 1)) * 100;
|
return (1 - Math.min(Math.max(progressNumber, 0), 1)) * 100;
|
||||||
});
|
|
||||||
|
|
||||||
const barStyle = computed(() => {
|
|
||||||
const barStyle: Partial<CSSProperties> = {
|
|
||||||
width: unwrapRef(width) + 0.5 + "px",
|
|
||||||
height: unwrapRef(height) + 0.5 + "px"
|
|
||||||
};
|
|
||||||
switch (unref(direction)) {
|
|
||||||
case Direction.Up:
|
|
||||||
barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`;
|
|
||||||
barStyle.width = unwrapRef(width) + 1 + "px";
|
|
||||||
break;
|
|
||||||
case Direction.Down:
|
|
||||||
barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`;
|
|
||||||
barStyle.width = unwrapRef(width) + 1 + "px";
|
|
||||||
break;
|
|
||||||
case Direction.Right:
|
|
||||||
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
|
||||||
break;
|
|
||||||
case Direction.Left:
|
|
||||||
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value}%)`;
|
|
||||||
break;
|
|
||||||
case Direction.Default:
|
|
||||||
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return barStyle;
|
|
||||||
});
|
|
||||||
|
|
||||||
const component = computeOptionalComponent(display);
|
|
||||||
|
|
||||||
return {
|
|
||||||
normalizedProgress,
|
|
||||||
barStyle,
|
|
||||||
component,
|
|
||||||
unref,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const barStyle = computed(() => {
|
||||||
|
const barStyle: Partial<CSSProperties> = {
|
||||||
|
width: props.width + 0.5 + "px",
|
||||||
|
height: props.height + 0.5 + "px"
|
||||||
|
};
|
||||||
|
switch (props.direction) {
|
||||||
|
case Direction.Up:
|
||||||
|
barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`;
|
||||||
|
barStyle.width = props.width + 1 + "px";
|
||||||
|
break;
|
||||||
|
case Direction.Down:
|
||||||
|
barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`;
|
||||||
|
barStyle.width = props.width + 1 + "px";
|
||||||
|
break;
|
||||||
|
case Direction.Right:
|
||||||
|
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
||||||
|
break;
|
||||||
|
case Direction.Left:
|
||||||
|
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value}%)`;
|
||||||
|
break;
|
||||||
|
case Direction.Default:
|
||||||
|
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return barStyle;
|
||||||
|
});
|
||||||
|
|
||||||
|
const component = computeOptionalComponent(toRef(props, "display"));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script setup lang="tsx">
|
||||||
import "components/common/features.css";
|
import "components/common/features.css";
|
||||||
import MarkNode from "components/MarkNode.vue";
|
import MarkNode from "components/MarkNode.vue";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
|
@ -39,139 +39,92 @@ import type { StyleValue } from "features/feature";
|
||||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||||
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
|
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
|
||||||
import { displayRequirements, Requirements } from "game/requirements";
|
import { displayRequirements, Requirements } from "game/requirements";
|
||||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||||
import type { Component, PropType, UnwrapRef } from "vue";
|
import type { Component, UnwrapRef } from "vue";
|
||||||
import { computed, defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
import { computed, shallowRef, unref, watchEffect } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
active: boolean;
|
||||||
active: {
|
maxed: boolean;
|
||||||
type: processedPropType<boolean>(Boolean),
|
canComplete: boolean;
|
||||||
required: true
|
display?: UnwrapRef<GenericChallenge["display"]>;
|
||||||
},
|
requirements?: Requirements;
|
||||||
maxed: {
|
visibility: Visibility | boolean;
|
||||||
type: processedPropType<boolean>(Boolean),
|
style?: StyleValue;
|
||||||
required: true
|
classes?: Record<string, boolean>;
|
||||||
},
|
completed: boolean;
|
||||||
canComplete: {
|
canStart: boolean;
|
||||||
type: processedPropType<boolean>(Boolean),
|
mark?: boolean | string;
|
||||||
required: true
|
id: string;
|
||||||
},
|
toggle: VoidFunction;
|
||||||
display: processedPropType<UnwrapRef<GenericChallenge["display"]>>(
|
}>();
|
||||||
String,
|
|
||||||
Object,
|
|
||||||
Function
|
|
||||||
),
|
|
||||||
requirements: processedPropType<Requirements>(Object, Array),
|
|
||||||
visibility: {
|
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
completed: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
canStart: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
mark: processedPropType<boolean | string>(Boolean, String),
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
toggle: {
|
|
||||||
type: Function as PropType<VoidFunction>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
MarkNode,
|
|
||||||
Node
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { active, maxed, canComplete, display, requirements } = toRefs(props);
|
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
if (active.value) {
|
if (props.active) {
|
||||||
return canComplete.value ? "Finish" : "Exit Early";
|
return props.canComplete ? "Finish" : "Exit Early";
|
||||||
}
|
|
||||||
if (maxed.value) {
|
|
||||||
return "Completed";
|
|
||||||
}
|
|
||||||
return "Start";
|
|
||||||
});
|
|
||||||
|
|
||||||
const comp = shallowRef<Component | string>("");
|
|
||||||
|
|
||||||
const notifyStyle = computed(() => {
|
|
||||||
const currActive = unwrapRef(active);
|
|
||||||
const currCanComplete = unwrapRef(canComplete);
|
|
||||||
if (currActive) {
|
|
||||||
if (currCanComplete) {
|
|
||||||
return getHighNotifyStyle();
|
|
||||||
}
|
|
||||||
return getNotifyStyle();
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const currDisplay = unwrapRef(display);
|
|
||||||
if (currDisplay == null) {
|
|
||||||
comp.value = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isCoercableComponent(currDisplay)) {
|
|
||||||
comp.value = coerceComponent(currDisplay);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
|
||||||
const Description = coerceComponent(currDisplay.description, "div");
|
|
||||||
const Goal = coerceComponent(currDisplay.goal != null ? currDisplay.goal : jsx(() => displayRequirements(unwrapRef(requirements) ?? [])), "h3");
|
|
||||||
const Reward = coerceComponent(currDisplay.reward || "");
|
|
||||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
|
||||||
comp.value = coerceComponent(
|
|
||||||
jsx(() => (
|
|
||||||
<span>
|
|
||||||
{currDisplay.title != null ? (
|
|
||||||
<div>
|
|
||||||
<Title />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<Description />
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
Goal: <Goal />
|
|
||||||
</div>
|
|
||||||
{currDisplay.reward != null ? (
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
Reward: <Reward />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{currDisplay.effectDisplay != null ? (
|
|
||||||
<div>
|
|
||||||
Currently: <EffectDisplay />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
buttonText,
|
|
||||||
notifyStyle,
|
|
||||||
comp,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden,
|
|
||||||
unref
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (props.maxed) {
|
||||||
|
return "Completed";
|
||||||
|
}
|
||||||
|
return "Start";
|
||||||
|
});
|
||||||
|
|
||||||
|
const comp = shallowRef<Component | string>("");
|
||||||
|
|
||||||
|
const notifyStyle = computed(() => {
|
||||||
|
const currActive = props.active;
|
||||||
|
const currCanComplete = props.canComplete;
|
||||||
|
if (currActive) {
|
||||||
|
if (currCanComplete) {
|
||||||
|
return getHighNotifyStyle();
|
||||||
|
}
|
||||||
|
return getNotifyStyle();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const currDisplay = props.display;
|
||||||
|
if (currDisplay == null) {
|
||||||
|
comp.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isCoercableComponent(currDisplay)) {
|
||||||
|
comp.value = coerceComponent(currDisplay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||||
|
const Description = coerceComponent(currDisplay.description, "div");
|
||||||
|
const Goal = coerceComponent(currDisplay.goal != null ? currDisplay.goal : jsx(() => displayRequirements(props.requirements ?? [])), "h3");
|
||||||
|
const Reward = coerceComponent(currDisplay.reward || "");
|
||||||
|
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||||
|
comp.value = coerceComponent(
|
||||||
|
jsx(() => (
|
||||||
|
<span>
|
||||||
|
{currDisplay.title != null ? (
|
||||||
|
<div>
|
||||||
|
<Title />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<Description />
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
Goal: <Goal />
|
||||||
|
</div>
|
||||||
|
{currDisplay.reward != null ? (
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
Reward: <Reward />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{currDisplay.effectDisplay != null ? (
|
||||||
|
<div>
|
||||||
|
Currently: <EffectDisplay />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { isArray } from "@vue/shared";
|
|
||||||
import Toggle from "components/fields/Toggle.vue";
|
import Toggle from "components/fields/Toggle.vue";
|
||||||
import ChallengeComponent from "features/challenges/Challenge.vue";
|
import ChallengeComponent from "features/challenges/Challenge.vue";
|
||||||
import { GenericDecorator } from "features/decorators/common";
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
|
@ -348,7 +347,7 @@ export function createActiveChallenge(
|
||||||
export function isAnyChallengeActive(
|
export function isAnyChallengeActive(
|
||||||
challenges: GenericChallenge[] | Ref<GenericChallenge | null>
|
challenges: GenericChallenge[] | Ref<GenericChallenge | null>
|
||||||
): Ref<boolean> {
|
): Ref<boolean> {
|
||||||
if (isArray(challenges)) {
|
if (Array.isArray(challenges)) {
|
||||||
challenges = createActiveChallenge(challenges);
|
challenges = createActiveChallenge(challenges);
|
||||||
}
|
}
|
||||||
return computed(() => (challenges as Ref<GenericChallenge | null>).value != null);
|
return computed(() => (challenges as Ref<GenericChallenge | null>).value != null);
|
||||||
|
@ -364,17 +363,19 @@ globalBus.on("loadSettings", settings => {
|
||||||
setDefault(settings, "hideChallenges", false);
|
setDefault(settings, "hideChallenges", false);
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSettingField(
|
globalBus.on("setupVue", () =>
|
||||||
jsx(() => (
|
registerSettingField(
|
||||||
<Toggle
|
jsx(() => (
|
||||||
title={jsx(() => (
|
<Toggle
|
||||||
<span class="option-title">
|
title={jsx(() => (
|
||||||
Hide maxed challenges
|
<span class="option-title">
|
||||||
<desc>Hide challenges that have been fully completed.</desc>
|
Hide maxed challenges
|
||||||
</span>
|
<desc>Hide challenges that have been fully completed.</desc>
|
||||||
))}
|
</span>
|
||||||
onUpdate:modelValue={value => (settings.hideChallenges = value)}
|
))}
|
||||||
modelValue={settings.hideChallenges}
|
onUpdate:modelValue={value => (settings.hideChallenges = value)}
|
||||||
/>
|
modelValue={settings.hideChallenges}
|
||||||
))
|
/>
|
||||||
|
))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script setup lang="tsx">
|
||||||
import "components/common/features.css";
|
import "components/common/features.css";
|
||||||
import MarkNode from "components/MarkNode.vue";
|
import MarkNode from "components/MarkNode.vue";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
|
@ -37,90 +37,53 @@ import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||||
import {
|
import {
|
||||||
coerceComponent,
|
coerceComponent,
|
||||||
isCoercableComponent,
|
isCoercableComponent,
|
||||||
processedPropType,
|
setupHoldToClick
|
||||||
setupHoldToClick,
|
|
||||||
unwrapRef
|
|
||||||
} from "util/vue";
|
} from "util/vue";
|
||||||
import type { Component, PropType, UnwrapRef } from "vue";
|
import type { Component, UnwrapRef } from "vue";
|
||||||
import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
import { shallowRef, toRef, unref, watchEffect } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
display: UnwrapRef<GenericClickable["display"]>;
|
||||||
display: {
|
visibility: Visibility | boolean;
|
||||||
type: processedPropType<UnwrapRef<GenericClickable["display"]>>(
|
style?: StyleValue;
|
||||||
Object,
|
classes?: Record<string, boolean>;
|
||||||
String,
|
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||||
Function
|
onHold?: VoidFunction;
|
||||||
),
|
canClick: boolean;
|
||||||
required: true
|
small?: boolean;
|
||||||
},
|
mark?: boolean | string;
|
||||||
visibility: {
|
id: string;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
}>();
|
||||||
required: true
|
|
||||||
},
|
|
||||||
style: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
onClick: Function as PropType<(e?: MouseEvent | TouchEvent) => void>,
|
|
||||||
onHold: Function as PropType<VoidFunction>,
|
|
||||||
canClick: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
small: Boolean,
|
|
||||||
mark: processedPropType<boolean | string>(Boolean, String),
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Node,
|
|
||||||
MarkNode
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { display, onClick, onHold } = toRefs(props);
|
|
||||||
|
|
||||||
const comp = shallowRef<Component | string>("");
|
const comp = shallowRef<Component | string>("");
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const currDisplay = unwrapRef(display);
|
const currDisplay = props.display;
|
||||||
if (currDisplay == null) {
|
if (currDisplay == null) {
|
||||||
comp.value = "";
|
comp.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
if (isCoercableComponent(currDisplay)) {
|
|
||||||
comp.value = coerceComponent(currDisplay);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
|
||||||
const Description = coerceComponent(currDisplay.description, "div");
|
|
||||||
comp.value = coerceComponent(
|
|
||||||
jsx(() => (
|
|
||||||
<span>
|
|
||||||
{currDisplay.title != null ? (
|
|
||||||
<div>
|
|
||||||
<Title />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<Description />
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { start, stop } = setupHoldToClick(onClick, onHold);
|
|
||||||
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
comp,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden,
|
|
||||||
unref
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (isCoercableComponent(currDisplay)) {
|
||||||
|
comp.value = coerceComponent(currDisplay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||||
|
const Description = coerceComponent(currDisplay.description, "div");
|
||||||
|
comp.value = coerceComponent(
|
||||||
|
jsx(() => (
|
||||||
|
<span>
|
||||||
|
{currDisplay.title != null ? (
|
||||||
|
<div>
|
||||||
|
<Title />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<Description />
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -129,7 +129,7 @@ export function createClickable<T extends ClickableOptions>(
|
||||||
if (clickable.onClick) {
|
if (clickable.onClick) {
|
||||||
const onClick = clickable.onClick.bind(clickable);
|
const onClick = clickable.onClick.bind(clickable);
|
||||||
clickable.onClick = function (e) {
|
clickable.onClick = function (e) {
|
||||||
if (unref(clickable.canClick) !== false) {
|
if (unref(clickable.canClick as ProcessedComputable<boolean>) !== false) {
|
||||||
onClick(e);
|
onClick(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -137,7 +137,7 @@ export function createClickable<T extends ClickableOptions>(
|
||||||
if (clickable.onHold) {
|
if (clickable.onHold) {
|
||||||
const onHold = clickable.onHold.bind(clickable);
|
const onHold = clickable.onHold.bind(clickable);
|
||||||
clickable.onHold = function () {
|
clickable.onHold = function () {
|
||||||
if (unref(clickable.canClick) !== false) {
|
if (unref(clickable.canClick as ProcessedComputable<boolean>) !== false) {
|
||||||
onHold();
|
onHold();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -228,7 +228,7 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
||||||
conversion.baseResource.value
|
conversion.baseResource.value
|
||||||
)
|
)
|
||||||
).max(conversion.gainResource.value);
|
).max(conversion.gainResource.value);
|
||||||
if (unref(conversion.buyMax) === false) {
|
if (unref(conversion.buyMax as ProcessedComputable<boolean>) === false) {
|
||||||
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
|
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
|
||||||
}
|
}
|
||||||
return gain;
|
return gain;
|
||||||
|
@ -245,7 +245,7 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
||||||
.floor()
|
.floor()
|
||||||
.max(0);
|
.max(0);
|
||||||
|
|
||||||
if (unref(conversion.buyMax) === false) {
|
if (unref(conversion.buyMax as ProcessedComputable<boolean>) === false) {
|
||||||
gain = gain.min(1);
|
gain = gain.min(1);
|
||||||
}
|
}
|
||||||
return gain;
|
return gain;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Decimal from "util/bignum";
|
||||||
import { DoNotCache, ProcessedComputable } from "util/computed";
|
import { DoNotCache, ProcessedComputable } from "util/computed";
|
||||||
import type { CSSProperties, DefineComponent } from "vue";
|
import type { CSSProperties, DefineComponent } from "vue";
|
||||||
import { isRef, unref } from "vue";
|
import { isRef, unref } from "vue";
|
||||||
|
import { JSX } from "vue/jsx-runtime";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A symbol to use as a key for a vue component a feature can be rendered with
|
* A symbol to use as a key for a vue component a feature can be rendered with
|
||||||
|
@ -92,7 +93,7 @@ export function setDefault<T, K extends keyof T>(
|
||||||
key: K,
|
key: K,
|
||||||
value: T[K]
|
value: T[K]
|
||||||
): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
|
): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
|
||||||
if (object[key] === undefined && value != undefined) {
|
if (object[key] == null && value != null) {
|
||||||
object[key] = value;
|
object[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,7 +136,7 @@ export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]
|
||||||
if (value != null && typeof value === "object") {
|
if (value != null && typeof value === "object") {
|
||||||
if (
|
if (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
typeof (value as Record<string, any>).type == "symbol" &&
|
typeof (value as Record<string, any>).type === "symbol" &&
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
!types.includes((value as Record<string, any>).type)
|
!types.includes((value as Record<string, any>).type)
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
class="table-grid"
|
class="table-grid"
|
||||||
>
|
>
|
||||||
<div v-for="row in unref(rows)" class="row-grid" :class="{ mergeAdjacent }" :key="row">
|
<div v-for="row in unref(rows)" class="row-grid" :class="{ mergeAdjacent }" :key="row">
|
||||||
<GridCell
|
<GridCellVue
|
||||||
v-for="col in unref(cols)"
|
v-for="col in unref(cols)"
|
||||||
:key="col"
|
:key="col"
|
||||||
v-bind="gatherCellProps(unref(cells)[row * 100 + col])"
|
v-bind="gatherCellProps(unref(cells)[row * 100 + col])"
|
||||||
|
@ -16,45 +16,26 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import "components/common/table.css";
|
import "components/common/table.css";
|
||||||
import themes from "data/themes";
|
import themes from "data/themes";
|
||||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||||
import type { GridCell } from "features/grids/grid";
|
import type { GridCell } from "features/grids/grid";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import { processedPropType } from "util/vue";
|
import { computed, unref } from "vue";
|
||||||
import { computed, defineComponent, unref } from "vue";
|
|
||||||
import GridCellVue from "./GridCell.vue";
|
import GridCellVue from "./GridCell.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps<{
|
||||||
props: {
|
visibility: Visibility | boolean;
|
||||||
visibility: {
|
rows: number;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
cols: number;
|
||||||
required: true
|
cells: Record<string, GridCell>;
|
||||||
},
|
}>();
|
||||||
rows: {
|
|
||||||
type: processedPropType<number>(Number),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
cols: {
|
|
||||||
type: processedPropType<number>(Number),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
cells: {
|
|
||||||
type: processedPropType<Record<string, GridCell>>(Object),
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: { GridCell: GridCellVue },
|
|
||||||
setup() {
|
|
||||||
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
|
|
||||||
|
|
||||||
function gatherCellProps(cell: GridCell) {
|
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
|
||||||
const { visibility, onClick, onHold, display, title, style, canClick, id } = cell;
|
|
||||||
return { visibility, onClick, onHold, display, title, style, canClick, id };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { unref, gatherCellProps, Visibility, mergeAdjacent, isVisible, isHidden };
|
function gatherCellProps(cell: GridCell) {
|
||||||
}
|
const { visibility, onClick, onHold, display, title, style, canClick, id } = cell;
|
||||||
});
|
return { visibility, onClick, onHold, display, title, style, canClick, id };
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import "components/common/features.css";
|
import "components/common/features.css";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||||
|
@ -30,58 +30,26 @@ import { isHidden, isVisible, Visibility } from "features/feature";
|
||||||
import {
|
import {
|
||||||
computeComponent,
|
computeComponent,
|
||||||
computeOptionalComponent,
|
computeOptionalComponent,
|
||||||
processedPropType,
|
|
||||||
setupHoldToClick
|
setupHoldToClick
|
||||||
} from "util/vue";
|
} from "util/vue";
|
||||||
import type { PropType } from "vue";
|
import { toRef, unref } from "vue";
|
||||||
import { defineComponent, toRefs, unref } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
visibility: Visibility | boolean;
|
||||||
visibility: {
|
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
onHold?: VoidFunction;
|
||||||
required: true
|
display: CoercableComponent;
|
||||||
},
|
title?: CoercableComponent;
|
||||||
onClick: Function as PropType<(e?: MouseEvent | TouchEvent) => void>,
|
style?: StyleValue;
|
||||||
onHold: Function as PropType<VoidFunction>,
|
canClick: boolean;
|
||||||
display: {
|
id: string;
|
||||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
}>();
|
||||||
required: true
|
|
||||||
},
|
|
||||||
title: processedPropType<CoercableComponent>(Object, String, Function),
|
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
canClick: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Node
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { onClick, onHold, title, display } = toRefs(props);
|
|
||||||
|
|
||||||
const { start, stop } = setupHoldToClick(onClick, onHold);
|
|
||||||
|
|
||||||
const titleComponent = computeOptionalComponent(title);
|
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
|
||||||
const component = computeComponent(display);
|
|
||||||
|
|
||||||
return {
|
const titleComponent = computeOptionalComponent(toRef(props, "title"));
|
||||||
start,
|
const component = computeComponent(toRef(props, "display"));
|
||||||
stop,
|
|
||||||
titleComponent,
|
|
||||||
component,
|
|
||||||
Visibility,
|
|
||||||
unref,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -128,7 +128,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
||||||
if (isFunction(prop)) {
|
if (isFunction(prop)) {
|
||||||
return () => prop.call(receiver, id, target.getState(id));
|
return () => prop.call(receiver, id, target.getState(id));
|
||||||
}
|
}
|
||||||
if (prop != undefined || typeof key === "symbol") {
|
if (prop != null || typeof key === "symbol") {
|
||||||
return prop;
|
return prop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
||||||
cache[key] = computed(() => prop.call(receiver, id, target.getState(id)));
|
cache[key] = computed(() => prop.call(receiver, id, target.getState(id)));
|
||||||
}
|
}
|
||||||
return cache[key].value;
|
return cache[key].value;
|
||||||
} else if (prop != undefined) {
|
} else if (prop != null) {
|
||||||
return unref(prop);
|
return unref(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
||||||
prop = (target as any)[`on${key}`];
|
prop = (target as any)[`on${key}`];
|
||||||
if (isFunction(prop)) {
|
if (isFunction(prop)) {
|
||||||
return () => prop.call(receiver, id, target.getState(id));
|
return () => prop.call(receiver, id, target.getState(id));
|
||||||
} else if (prop != undefined) {
|
} else if (prop != null) {
|
||||||
return prop;
|
return prop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +318,7 @@ export function createGrid<T extends GridOptions>(
|
||||||
return grid.id + "-" + cell;
|
return grid.id + "-" + cell;
|
||||||
};
|
};
|
||||||
grid.getState = function (this: GenericGrid, cell: string | number) {
|
grid.getState = function (this: GenericGrid, cell: string | number) {
|
||||||
if (this.cellState.value[cell] != undefined) {
|
if (this.cellState.value[cell] != null) {
|
||||||
return cellState.value[cell];
|
return cellState.value[cell];
|
||||||
}
|
}
|
||||||
return this.cells[cell].startState;
|
return this.cells[cell].startState;
|
||||||
|
|
|
@ -99,40 +99,57 @@ document.onkeydown = function (e) {
|
||||||
if (hasWon.value && !player.keepGoing) {
|
if (hasWon.value && !player.keepGoing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let key = e.key;
|
const keysToCheck: string[] = [e.key];
|
||||||
if (uppercaseNumbers.includes(key)) {
|
if (e.shiftKey && e.ctrlKey) {
|
||||||
key = "shift+" + uppercaseNumbers.indexOf(key);
|
keysToCheck.splice(0, 1);
|
||||||
|
keysToCheck.push("ctrl+shift+" + e.key.toUpperCase());
|
||||||
|
keysToCheck.push("shift+ctrl+" + e.key.toUpperCase());
|
||||||
|
if (uppercaseNumbers.includes(e.key)) {
|
||||||
|
keysToCheck.push("ctrl+shift+" + uppercaseNumbers.indexOf(e.key));
|
||||||
|
keysToCheck.push("shift+ctrl+" + uppercaseNumbers.indexOf(e.key));
|
||||||
|
} else {
|
||||||
|
keysToCheck.push("ctrl+shift+" + e.key.toLowerCase());
|
||||||
|
keysToCheck.push("shift+ctrl+" + e.key.toLowerCase());
|
||||||
|
}
|
||||||
|
} else if (uppercaseNumbers.includes(e.key)) {
|
||||||
|
keysToCheck.push("shift+" + e.key);
|
||||||
|
keysToCheck.push("shift+" + uppercaseNumbers.indexOf(e.key));
|
||||||
} else if (e.shiftKey) {
|
} else if (e.shiftKey) {
|
||||||
key = "shift+" + key;
|
keysToCheck.push("shift+" + e.key.toUpperCase());
|
||||||
|
keysToCheck.push("shift+" + e.key.toLowerCase());
|
||||||
|
} else if (e.ctrlKey) {
|
||||||
|
// remove e.key since the key doesn't change based on ctrl being held or not
|
||||||
|
keysToCheck.splice(0, 1);
|
||||||
|
keysToCheck.push("ctrl+" + e.key);
|
||||||
}
|
}
|
||||||
if (e.ctrlKey) {
|
const hotkey = hotkeys[keysToCheck.find(key => key in hotkeys) ?? ""];
|
||||||
key = "ctrl+" + key;
|
|
||||||
}
|
|
||||||
const hotkey = hotkeys[key] ?? hotkeys[key.toLowerCase()];
|
|
||||||
if (hotkey && unref(hotkey.enabled)) {
|
if (hotkey && unref(hotkey.enabled)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
hotkey.onPress();
|
hotkey.onPress();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerInfoComponent(
|
globalBus.on("setupVue", () =>
|
||||||
jsx(() => {
|
registerInfoComponent(
|
||||||
const keys = Object.values(hotkeys).filter(hotkey => unref(hotkey?.enabled));
|
jsx(() => {
|
||||||
if (keys.length === 0) {
|
const keys = Object.values(hotkeys).filter(hotkey => unref(hotkey?.enabled));
|
||||||
return "";
|
if (keys.length === 0) {
|
||||||
}
|
return "";
|
||||||
return (
|
}
|
||||||
<div>
|
return (
|
||||||
<br />
|
<div>
|
||||||
<h4>Hotkeys</h4>
|
<br />
|
||||||
<div style="column-count: 2">
|
<h4>Hotkeys</h4>
|
||||||
{keys.map(hotkey => (
|
<div style="column-count: 2">
|
||||||
<div>
|
{keys.map(hotkey => (
|
||||||
<Hotkey hotkey={hotkey as GenericHotkey} /> {hotkey?.description}
|
<div>
|
||||||
</div>
|
<Hotkey hotkey={hotkey as GenericHotkey} />{" "}
|
||||||
))}
|
{unref(hotkey?.description)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,67 +28,33 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
import themes from "data/themes";
|
import themes from "data/themes";
|
||||||
import type { CoercableComponent } from "features/feature";
|
import type { CoercableComponent } from "features/feature";
|
||||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import { computeComponent, processedPropType } from "util/vue";
|
import { computeComponent } from "util/vue";
|
||||||
import type { PropType, Ref, StyleValue } from "vue";
|
import type { Ref, StyleValue } from "vue";
|
||||||
import { computed, defineComponent, toRefs, unref } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
visibility: Visibility | boolean;
|
||||||
visibility: {
|
display: CoercableComponent;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
title: CoercableComponent;
|
||||||
required: true
|
color?: string;
|
||||||
},
|
collapsed: Ref<boolean>;
|
||||||
display: {
|
style?: StyleValue;
|
||||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
titleStyle?: StyleValue;
|
||||||
required: true
|
bodyStyle?: StyleValue;
|
||||||
},
|
classes?: Record<string, boolean>;
|
||||||
title: {
|
id: string;
|
||||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
}>();
|
||||||
required: true
|
|
||||||
},
|
|
||||||
color: processedPropType<string>(String),
|
|
||||||
collapsed: {
|
|
||||||
type: Object as PropType<Ref<boolean>>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
style: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
titleStyle: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
bodyStyle: processedPropType<StyleValue>(Object, String, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Node,
|
|
||||||
CollapseTransition
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { title, display } = toRefs(props);
|
|
||||||
|
|
||||||
const titleComponent = computeComponent(title);
|
const titleComponent = computeComponent(toRef(props, "title"));
|
||||||
const bodyComponent = computeComponent(display);
|
const bodyComponent = computeComponent(toRef(props, "display"));
|
||||||
const stacked = computed(() => themes[settings.theme].mergeAdjacent);
|
const stacked = computed(() => themes[settings.theme].mergeAdjacent);
|
||||||
|
|
||||||
return {
|
|
||||||
titleComponent,
|
|
||||||
bodyComponent,
|
|
||||||
stacked,
|
|
||||||
unref,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -14,47 +14,46 @@
|
||||||
import type { Link } from "features/links/links";
|
import type { Link } from "features/links/links";
|
||||||
import type { FeatureNode } from "game/layers";
|
import type { FeatureNode } from "game/layers";
|
||||||
import { kebabifyObject } from "util/vue";
|
import { kebabifyObject } from "util/vue";
|
||||||
import { computed, toRefs } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const props = defineProps<{
|
||||||
link: Link;
|
link: Link;
|
||||||
startNode: FeatureNode;
|
startNode: FeatureNode;
|
||||||
endNode: FeatureNode;
|
endNode: FeatureNode;
|
||||||
boundingRect: DOMRect | undefined;
|
boundingRect: DOMRect | undefined;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
|
||||||
|
|
||||||
const startPosition = computed(() => {
|
const startPosition = computed(() => {
|
||||||
const rect = props.startNode.value.rect;
|
const rect = props.startNode.rect;
|
||||||
const boundingRect = props.boundingRect.value;
|
const boundingRect = props.boundingRect;
|
||||||
const position = boundingRect
|
const position = boundingRect
|
||||||
? {
|
? {
|
||||||
x: rect.x + rect.width / 2 - boundingRect.x,
|
x: rect.x + rect.width / 2 - boundingRect.x,
|
||||||
y: rect.y + rect.height / 2 - boundingRect.y
|
y: rect.y + rect.height / 2 - boundingRect.y
|
||||||
}
|
}
|
||||||
: { x: 0, y: 0 };
|
: { x: 0, y: 0 };
|
||||||
if (props.link.value.offsetStart) {
|
if (props.link.offsetStart) {
|
||||||
position.x += props.link.value.offsetStart.x;
|
position.x += props.link.offsetStart.x;
|
||||||
position.y += props.link.value.offsetStart.y;
|
position.y += props.link.offsetStart.y;
|
||||||
}
|
}
|
||||||
return position;
|
return position;
|
||||||
});
|
});
|
||||||
|
|
||||||
const endPosition = computed(() => {
|
const endPosition = computed(() => {
|
||||||
const rect = props.endNode.value.rect;
|
const rect = props.endNode.rect;
|
||||||
const boundingRect = props.boundingRect.value;
|
const boundingRect = props.boundingRect;
|
||||||
const position = boundingRect
|
const position = boundingRect
|
||||||
? {
|
? {
|
||||||
x: rect.x + rect.width / 2 - boundingRect.x,
|
x: rect.x + rect.width / 2 - boundingRect.x,
|
||||||
y: rect.y + rect.height / 2 - boundingRect.y
|
y: rect.y + rect.height / 2 - boundingRect.y
|
||||||
}
|
}
|
||||||
: { x: 0, y: 0 };
|
: { x: 0, y: 0 };
|
||||||
if (props.link.value.offsetEnd) {
|
if (props.link.offsetEnd) {
|
||||||
position.x += props.link.value.offsetEnd.x;
|
position.x += props.link.offsetEnd.x;
|
||||||
position.y += props.link.value.offsetEnd.y;
|
position.y += props.link.offsetEnd.y;
|
||||||
}
|
}
|
||||||
return position;
|
return position;
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
const linkProps = computed(() => kebabifyObject(props.link as unknown as Record<string, unknown>));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -16,11 +16,10 @@
|
||||||
import type { Link } from "features/links/links";
|
import type { Link } from "features/links/links";
|
||||||
import type { FeatureNode } from "game/layers";
|
import type { FeatureNode } from "game/layers";
|
||||||
import { BoundsInjectionKey, NodesInjectionKey } from "game/layers";
|
import { BoundsInjectionKey, NodesInjectionKey } from "game/layers";
|
||||||
import { computed, inject, onMounted, ref, toRef, watch } from "vue";
|
import { computed, inject, onMounted, ref, watch } from "vue";
|
||||||
import LinkVue from "./Link.vue";
|
import LinkVue from "./Link.vue";
|
||||||
|
|
||||||
const _props = defineProps<{ links?: Link[] }>();
|
const props = defineProps<{ links?: Link[] }>();
|
||||||
const links = toRef(_props, "links");
|
|
||||||
|
|
||||||
const resizeListener = ref<Element | null>(null);
|
const resizeListener = ref<Element | null>(null);
|
||||||
|
|
||||||
|
@ -36,7 +35,7 @@ onMounted(() => (boundingRect.value = resizeListener.value?.getBoundingClientRec
|
||||||
const validLinks = computed(() => {
|
const validLinks = computed(() => {
|
||||||
const n = nodes.value;
|
const n = nodes.value;
|
||||||
return (
|
return (
|
||||||
links.value?.filter(link => n[link.startNode.id]?.rect && n[link.startNode.id]?.rect) ?? []
|
props.links?.filter(link => n[link.startNode.id]?.rect && n[link.endNode.id]?.rect) ?? []
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,78 +7,61 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { Application } from "@pixi/app";
|
import { Application } from "@pixi/app";
|
||||||
import type { StyleValue } from "features/feature";
|
import type { StyleValue } from "features/feature";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import "lib/pixi";
|
import "lib/pixi";
|
||||||
import { processedPropType } from "util/vue";
|
import { nextTick, onBeforeUnmount, onMounted, shallowRef, unref } from "vue";
|
||||||
import type { PropType } from "vue";
|
|
||||||
import { defineComponent, nextTick, onBeforeUnmount, onMounted, shallowRef, unref } from "vue";
|
|
||||||
|
|
||||||
// TODO get typing support on the Particles component
|
const props = defineProps<{
|
||||||
export default defineComponent({
|
style?: StyleValue;
|
||||||
props: {
|
classes?: Record<string, boolean>;
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
onInit: (app: Application) => void;
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
id: string;
|
||||||
onInit: {
|
onContainerResized?: (rect: DOMRect) => void;
|
||||||
type: Function as PropType<(app: Application) => void>,
|
onHotReload?: VoidFunction;
|
||||||
required: true
|
}>();
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
onContainerResized: Function as PropType<(rect: DOMRect) => void>,
|
|
||||||
onHotReload: Function as PropType<VoidFunction>
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const app = shallowRef<null | Application>(null);
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(updateBounds);
|
const app = shallowRef<null | Application>(null);
|
||||||
const resizeListener = shallowRef<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
const resizeObserver = new ResizeObserver(updateBounds);
|
||||||
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
const resizeListener = shallowRef<HTMLElement | null>(null);
|
||||||
const resListener = resizeListener.value;
|
|
||||||
if (resListener != null) {
|
onMounted(() => {
|
||||||
resizeObserver.observe(resListener);
|
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
||||||
app.value = new Application({
|
const resListener = resizeListener.value;
|
||||||
resizeTo: resListener,
|
if (resListener != null) {
|
||||||
backgroundAlpha: 0
|
resizeObserver.observe(resListener);
|
||||||
});
|
app.value = new Application({
|
||||||
resizeListener.value?.appendChild(app.value.view);
|
resizeTo: resListener,
|
||||||
props.onInit?.(app.value as Application);
|
backgroundAlpha: 0
|
||||||
}
|
|
||||||
updateBounds();
|
|
||||||
if (props.onHotReload) {
|
|
||||||
nextTick(props.onHotReload);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
onBeforeUnmount(() => {
|
resizeListener.value?.appendChild(app.value.view);
|
||||||
app.value?.destroy();
|
props.onInit?.(app.value as Application);
|
||||||
});
|
}
|
||||||
|
updateBounds();
|
||||||
let isDirty = true;
|
if (props.onHotReload) {
|
||||||
function updateBounds() {
|
nextTick(props.onHotReload);
|
||||||
if (isDirty) {
|
|
||||||
isDirty = false;
|
|
||||||
nextTick(() => {
|
|
||||||
if (resizeListener.value != null) {
|
|
||||||
props.onContainerResized?.(resizeListener.value.getBoundingClientRect());
|
|
||||||
}
|
|
||||||
isDirty = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
globalBus.on("fontsLoaded", updateBounds);
|
|
||||||
|
|
||||||
return {
|
|
||||||
unref,
|
|
||||||
resizeListener
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
app.value?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
let isDirty = true;
|
||||||
|
function updateBounds() {
|
||||||
|
if (isDirty) {
|
||||||
|
isDirty = false;
|
||||||
|
nextTick(() => {
|
||||||
|
if (resizeListener.value != null) {
|
||||||
|
props.onContainerResized?.(resizeListener.value.getBoundingClientRect());
|
||||||
|
}
|
||||||
|
isDirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalBus.on("fontsLoaded", updateBounds);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { isArray } from "@vue/shared";
|
|
||||||
import ClickableComponent from "features/clickables/Clickable.vue";
|
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
|
@ -162,7 +161,7 @@ export function createRepeatable<T extends RepeatableOptions>(
|
||||||
canMaximize: true
|
canMaximize: true
|
||||||
} as const;
|
} as const;
|
||||||
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
|
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
|
||||||
if (isArray(repeatable.requirements)) {
|
if (Array.isArray(repeatable.requirements)) {
|
||||||
repeatable.requirements.unshift(visibilityRequirement);
|
repeatable.requirements.unshift(visibilityRequirement);
|
||||||
repeatable.requirements.push(limitRequirement);
|
repeatable.requirements.push(limitRequirement);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -25,23 +25,19 @@ import type { Resource } from "features/resources/resource";
|
||||||
import ResourceVue from "features/resources/Resource.vue";
|
import ResourceVue from "features/resources/Resource.vue";
|
||||||
import Decimal from "util/bignum";
|
import Decimal from "util/bignum";
|
||||||
import { computeOptionalComponent } from "util/vue";
|
import { computeOptionalComponent } from "util/vue";
|
||||||
import { ComponentPublicInstance, ref, Ref, StyleValue } from "vue";
|
import { ComponentPublicInstance, computed, ref, StyleValue, toRef } from "vue";
|
||||||
import { computed, toRefs } from "vue";
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
const props = defineProps<{
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
color?: string;
|
color?: string;
|
||||||
classes?: Record<string, boolean>;
|
classes?: Record<string, boolean>;
|
||||||
style?: StyleValue;
|
style?: StyleValue;
|
||||||
effectDisplay?: CoercableComponent;
|
effectDisplay?: CoercableComponent;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
|
||||||
|
|
||||||
const effectRef = ref<ComponentPublicInstance | null>(null);
|
const effectRef = ref<ComponentPublicInstance | null>(null);
|
||||||
|
|
||||||
const effectComponent = computeOptionalComponent(
|
const effectComponent = computeOptionalComponent(toRef(props, "effectDisplay"));
|
||||||
props.effectDisplay as Ref<CoercableComponent | undefined>
|
|
||||||
);
|
|
||||||
|
|
||||||
const showPrefix = computed(() => {
|
const showPrefix = computed(() => {
|
||||||
return Decimal.lt(props.resource.value, "1e1000");
|
return Decimal.lt(props.resource.value, "1e1000");
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CoercableComponent } from "features/feature";
|
import type { CoercableComponent } from "features/feature";
|
||||||
import { computeComponent } from "util/vue";
|
import { computeComponent } from "util/vue";
|
||||||
import { toRefs } from "vue";
|
import { toRef } from "vue";
|
||||||
|
|
||||||
const _props = defineProps<{ display: CoercableComponent }>();
|
const props = defineProps<{ display: CoercableComponent }>();
|
||||||
const { display } = toRefs(_props);
|
const component = computeComponent(toRef(props, "display"));
|
||||||
const component = computeComponent(display);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -19,61 +19,43 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||||
import { getNotifyStyle } from "game/notifications";
|
import { getNotifyStyle } from "game/notifications";
|
||||||
import { computeComponent, processedPropType, unwrapRef } from "util/vue";
|
import { computeComponent } from "util/vue";
|
||||||
import { computed, defineComponent, toRefs, unref } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
visibility: Visibility | boolean;
|
||||||
visibility: {
|
display: CoercableComponent;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
style?: StyleValue;
|
||||||
required: true
|
classes?: Record<string, boolean>;
|
||||||
},
|
glowColor?: string;
|
||||||
display: {
|
active?: boolean;
|
||||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
floating?: boolean;
|
||||||
required: true
|
}>();
|
||||||
},
|
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
glowColor: processedPropType<string>(String),
|
|
||||||
active: Boolean,
|
|
||||||
floating: Boolean
|
|
||||||
},
|
|
||||||
emits: ["selectTab"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const { display, glowColor, floating } = toRefs(props);
|
|
||||||
|
|
||||||
const component = computeComponent(display);
|
const emit = defineEmits<{
|
||||||
|
selectTab: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const glowColorStyle = computed(() => {
|
const component = computeComponent(toRef(props, "display"));
|
||||||
const color = unwrapRef(glowColor);
|
|
||||||
if (color == null || color === "") {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (unref(floating)) {
|
|
||||||
return getNotifyStyle(color);
|
|
||||||
}
|
|
||||||
return { boxShadow: `0px 9px 5px -6px ${color}` };
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectTab() {
|
const glowColorStyle = computed(() => {
|
||||||
emit("selectTab");
|
const color = props.glowColor;
|
||||||
}
|
if (color == null || color === "") {
|
||||||
|
return {};
|
||||||
return {
|
|
||||||
selectTab,
|
|
||||||
component,
|
|
||||||
glowColorStyle,
|
|
||||||
unref,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (props.floating) {
|
||||||
|
return getNotifyStyle(color);
|
||||||
|
}
|
||||||
|
return { boxShadow: `0px 9px 5px -6px ${color}` };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function selectTab() {
|
||||||
|
emit("selectTab");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import Sticky from "components/layout/Sticky.vue";
|
import Sticky from "components/layout/Sticky.vue";
|
||||||
import themes from "data/themes";
|
import themes from "data/themes";
|
||||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||||
|
@ -42,93 +42,60 @@ import type { GenericTab } from "features/tabs/tab";
|
||||||
import TabButton from "features/tabs/TabButton.vue";
|
import TabButton from "features/tabs/TabButton.vue";
|
||||||
import type { GenericTabButton } from "features/tabs/tabFamily";
|
import type { GenericTabButton } from "features/tabs/tabFamily";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
import { coerceComponent, deepUnref, isCoercableComponent } from "util/vue";
|
||||||
import type { Component, PropType, Ref } from "vue";
|
import type { Component, Ref } from "vue";
|
||||||
import { computed, defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
import { computed, shallowRef, unref, watchEffect } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
visibility: Visibility | boolean;
|
||||||
visibility: {
|
activeTab: GenericTab | CoercableComponent | null;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
selected: Ref<string>;
|
||||||
required: true
|
tabs: Record<string, GenericTabButton>;
|
||||||
},
|
style?: StyleValue;
|
||||||
activeTab: {
|
classes?: Record<string, boolean>;
|
||||||
type: processedPropType<GenericTab | CoercableComponent | null>(Object),
|
buttonContainerStyle?: StyleValue;
|
||||||
required: true
|
buttonContainerClasses?: Record<string, boolean>;
|
||||||
},
|
}>();
|
||||||
selected: {
|
|
||||||
type: Object as PropType<Ref<string>>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
tabs: {
|
|
||||||
type: processedPropType<Record<string, GenericTabButton>>(Object),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
buttonContainerStyle: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
buttonContainerClasses: processedPropType<Record<string, boolean>>(Object)
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Sticky,
|
|
||||||
TabButton
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { activeTab } = toRefs(props);
|
|
||||||
|
|
||||||
const floating = computed(() => {
|
const floating = computed(() => {
|
||||||
return themes[settings.theme].floatingTabs;
|
return themes[settings.theme].floatingTabs;
|
||||||
});
|
|
||||||
|
|
||||||
const component = shallowRef<Component | string>("");
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const currActiveTab = unwrapRef(activeTab);
|
|
||||||
if (currActiveTab == null) {
|
|
||||||
component.value = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isCoercableComponent(currActiveTab)) {
|
|
||||||
component.value = coerceComponent(currActiveTab);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
component.value = coerceComponent(unref(currActiveTab.display));
|
|
||||||
});
|
|
||||||
|
|
||||||
const tabClasses = computed(() => {
|
|
||||||
const currActiveTab = unwrapRef(activeTab);
|
|
||||||
const tabClasses =
|
|
||||||
isCoercableComponent(currActiveTab) || !currActiveTab
|
|
||||||
? undefined
|
|
||||||
: unref(currActiveTab.classes);
|
|
||||||
return tabClasses;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tabStyle = computed(() => {
|
|
||||||
const currActiveTab = unwrapRef(activeTab);
|
|
||||||
return isCoercableComponent(currActiveTab) || !currActiveTab
|
|
||||||
? undefined
|
|
||||||
: unref(currActiveTab.style);
|
|
||||||
});
|
|
||||||
|
|
||||||
function gatherButtonProps(button: GenericTabButton) {
|
|
||||||
const { display, style, classes, glowColor, visibility } = button;
|
|
||||||
return { display, style: unref(style), classes, glowColor, visibility };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
floating,
|
|
||||||
tabClasses,
|
|
||||||
tabStyle,
|
|
||||||
Visibility,
|
|
||||||
component,
|
|
||||||
gatherButtonProps,
|
|
||||||
unref,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const component = shallowRef<Component | string>("");
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const currActiveTab = props.activeTab;
|
||||||
|
if (currActiveTab == null) {
|
||||||
|
component.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isCoercableComponent(currActiveTab)) {
|
||||||
|
component.value = coerceComponent(currActiveTab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
component.value = coerceComponent(unref(currActiveTab.display));
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabClasses = computed(() => {
|
||||||
|
const currActiveTab = props.activeTab;
|
||||||
|
const tabClasses =
|
||||||
|
isCoercableComponent(currActiveTab) || !currActiveTab
|
||||||
|
? undefined
|
||||||
|
: unref(currActiveTab.classes);
|
||||||
|
return tabClasses;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabStyle = computed(() => {
|
||||||
|
const currActiveTab = props.activeTab;
|
||||||
|
return isCoercableComponent(currActiveTab) || !currActiveTab
|
||||||
|
? undefined
|
||||||
|
: unref(currActiveTab.style);
|
||||||
|
});
|
||||||
|
|
||||||
|
function gatherButtonProps(button: GenericTabButton) {
|
||||||
|
const { display, style, classes, glowColor, visibility } = deepUnref(button);
|
||||||
|
return { display, style, classes, glowColor, visibility };
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script setup lang="tsx">
|
||||||
import themes from "data/themes";
|
import themes from "data/themes";
|
||||||
import type { CoercableComponent } from "features/feature";
|
import type { CoercableComponent } from "features/feature";
|
||||||
import { jsx, StyleValue } from "features/feature";
|
import { jsx, StyleValue } from "features/feature";
|
||||||
|
@ -45,66 +45,45 @@ import type { VueFeature } from "util/vue";
|
||||||
import {
|
import {
|
||||||
coerceComponent,
|
coerceComponent,
|
||||||
computeOptionalComponent,
|
computeOptionalComponent,
|
||||||
processedPropType,
|
renderJSX
|
||||||
renderJSX,
|
|
||||||
unwrapRef
|
|
||||||
} from "util/vue";
|
} from "util/vue";
|
||||||
import type { Component, PropType } from "vue";
|
import type { Component } from "vue";
|
||||||
import { computed, defineComponent, ref, shallowRef, toRefs, unref } from "vue";
|
import { computed, ref, shallowRef, toRef, unref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
element?: VueFeature;
|
||||||
element: Object as PropType<VueFeature>,
|
display: CoercableComponent;
|
||||||
display: {
|
style?: StyleValue;
|
||||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
classes?: Record<string, boolean>;
|
||||||
required: true
|
direction?: Direction;
|
||||||
},
|
xoffset?: string;
|
||||||
style: processedPropType<StyleValue>(Object, String, Array),
|
yoffset?: string;
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
pinned?: Persistent<boolean>;
|
||||||
direction: processedPropType<Direction>(String),
|
}>();
|
||||||
xoffset: processedPropType<string>(String),
|
|
||||||
yoffset: processedPropType<string>(String),
|
|
||||||
pinned: Object as PropType<Persistent<boolean>>
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { element, display, pinned } = toRefs(props);
|
|
||||||
|
|
||||||
const isHovered = ref(false);
|
const isHovered = ref(false);
|
||||||
const isShown = computed(() => (unwrapRef(pinned) || isHovered.value) && comp.value);
|
const isShown = computed(() => (props.pinned?.value === true || isHovered.value) && comp.value);
|
||||||
const comp = computeOptionalComponent(display);
|
const comp = computeOptionalComponent(toRef(props, "display"));
|
||||||
|
|
||||||
const elementComp = shallowRef<Component | "" | null>(
|
const elementComp = shallowRef<Component | "" | null>(
|
||||||
coerceComponent(
|
coerceComponent(
|
||||||
jsx(() => {
|
jsx(() => {
|
||||||
const currComponent = unwrapRef(element);
|
const currComponent = props.element;
|
||||||
return currComponent == null ? "" : renderJSX(currComponent);
|
return currComponent == null ? "" : renderJSX(currComponent);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
function togglePinned(e: MouseEvent) {
|
function togglePinned(e: MouseEvent) {
|
||||||
const isPinned = pinned as unknown as Persistent<boolean> | undefined; // Vue typing :/
|
const isPinned = props.pinned;
|
||||||
if (e.shiftKey && isPinned) {
|
if (e.shiftKey && isPinned != null) {
|
||||||
isPinned.value = !isPinned.value;
|
isPinned.value = !isPinned.value;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showPin = computed(() => unwrapRef(pinned) && themes[settings.theme].showPin);
|
|
||||||
|
|
||||||
return {
|
|
||||||
Direction,
|
|
||||||
isHovered,
|
|
||||||
isShown,
|
|
||||||
comp,
|
|
||||||
elementComp,
|
|
||||||
unref,
|
|
||||||
togglePinned,
|
|
||||||
showPin
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const showPin = computed(() => props.pinned?.value === true && themes[settings.theme].showPin);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CoercableComponent, GenericComponent, Replace, StyleValue } from "features/feature";
|
import type { CoercableComponent, GenericComponent, Replace, StyleValue } from "features/feature";
|
||||||
import { Component, GatherProps, setDefault } from "features/feature";
|
import { Component, GatherProps, setDefault } from "features/feature";
|
||||||
import { deletePersistent, Persistent, persistent } from "game/persistence";
|
import { persistent } from "game/persistence";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
import type {
|
import type {
|
||||||
Computable,
|
Computable,
|
||||||
|
|
|
@ -5,74 +5,58 @@
|
||||||
<Links v-if="branches" :links="unref(branches)" />
|
<Links v-if="branches" :links="unref(branches)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script setup lang="tsx">
|
||||||
import "components/common/table.css";
|
import "components/common/table.css";
|
||||||
import { jsx } from "features/feature";
|
import { jsx } from "features/feature";
|
||||||
import Links from "features/links/Links.vue";
|
import Links from "features/links/Links.vue";
|
||||||
import type { GenericTreeNode, TreeBranch } from "features/trees/tree";
|
import type { GenericTreeNode, TreeBranch } from "features/trees/tree";
|
||||||
import { coerceComponent, processedPropType, renderJSX, unwrapRef } from "util/vue";
|
import { coerceComponent, renderJSX } from "util/vue";
|
||||||
import type { Component } from "vue";
|
import type { Component } from "vue";
|
||||||
import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
import { shallowRef, unref, watchEffect } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
nodes: GenericTreeNode[][];
|
||||||
nodes: {
|
leftSideNodes?: GenericTreeNode[];
|
||||||
type: processedPropType<GenericTreeNode[][]>(Array),
|
rightSideNodes?: GenericTreeNode[];
|
||||||
required: true
|
branches?: TreeBranch[];
|
||||||
},
|
}>();
|
||||||
leftSideNodes: processedPropType<GenericTreeNode[]>(Array),
|
|
||||||
rightSideNodes: processedPropType<GenericTreeNode[]>(Array),
|
|
||||||
branches: processedPropType<TreeBranch[]>(Array)
|
|
||||||
},
|
|
||||||
components: { Links },
|
|
||||||
setup(props) {
|
|
||||||
const { nodes, leftSideNodes, rightSideNodes } = toRefs(props);
|
|
||||||
|
|
||||||
const nodesComp = shallowRef<Component | "">();
|
const nodesComp = shallowRef<Component | "">();
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const currNodes = unwrapRef(nodes);
|
const currNodes = props.nodes;
|
||||||
nodesComp.value = coerceComponent(
|
nodesComp.value = coerceComponent(
|
||||||
|
jsx(() => (
|
||||||
|
<>
|
||||||
|
{currNodes.map(row => (
|
||||||
|
<span class="row tree-row" style="margin: 50px auto;">
|
||||||
|
{row.map(renderJSX)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const leftNodesComp = shallowRef<Component | "">();
|
||||||
|
watchEffect(() => {
|
||||||
|
const currNodes = props.leftSideNodes;
|
||||||
|
leftNodesComp.value = currNodes
|
||||||
|
? coerceComponent(
|
||||||
jsx(() => (
|
jsx(() => (
|
||||||
<>
|
<span class="left-side-nodes small">{currNodes.map(renderJSX)}</span>
|
||||||
{currNodes.map(row => (
|
|
||||||
<span class="row tree-row" style="margin: 50px auto;">
|
|
||||||
{row.map(renderJSX)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
))
|
))
|
||||||
);
|
)
|
||||||
});
|
: "";
|
||||||
|
});
|
||||||
|
|
||||||
const leftNodesComp = shallowRef<Component | "">();
|
const rightNodesComp = shallowRef<Component | "">();
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const currNodes = unwrapRef(leftSideNodes);
|
const currNodes = props.rightSideNodes;
|
||||||
leftNodesComp.value = currNodes
|
rightNodesComp.value = currNodes
|
||||||
? coerceComponent(
|
? coerceComponent(
|
||||||
jsx(() => (
|
jsx(() => <span class="side-nodes small">{currNodes.map(renderJSX)}</span>)
|
||||||
<span class="left-side-nodes small">{currNodes.map(renderJSX)}</span>
|
)
|
||||||
))
|
: "";
|
||||||
)
|
|
||||||
: "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const rightNodesComp = shallowRef<Component | "">();
|
|
||||||
watchEffect(() => {
|
|
||||||
const currNodes = unwrapRef(rightSideNodes);
|
|
||||||
rightNodesComp.value = currNodes
|
|
||||||
? coerceComponent(
|
|
||||||
jsx(() => <span class="side-nodes small">{currNodes.map(renderJSX)}</span>)
|
|
||||||
)
|
|
||||||
: "";
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
unref,
|
|
||||||
nodesComp,
|
|
||||||
leftNodesComp,
|
|
||||||
rightNodesComp
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -33,66 +33,34 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="tsx">
|
||||||
import MarkNode from "components/MarkNode.vue";
|
import MarkNode from "components/MarkNode.vue";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
import type { CoercableComponent, StyleValue, Visibility } from "features/feature";
|
||||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
import { isHidden, isVisible } from "features/feature";
|
||||||
import {
|
import {
|
||||||
computeOptionalComponent,
|
computeOptionalComponent,
|
||||||
isCoercableComponent,
|
|
||||||
processedPropType,
|
|
||||||
setupHoldToClick
|
setupHoldToClick
|
||||||
} from "util/vue";
|
} from "util/vue";
|
||||||
import type { PropType } from "vue";
|
import { toRef, unref } from "vue";
|
||||||
import { defineComponent, toRefs, unref } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
visibility: Visibility | boolean;
|
||||||
display: processedPropType<CoercableComponent>(Object, String, Function),
|
canClick: boolean;
|
||||||
visibility: {
|
id: string;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
display?: CoercableComponent;
|
||||||
required: true
|
style?: StyleValue;
|
||||||
},
|
classes?: Record<string, boolean>;
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
onHold?: VoidFunction;
|
||||||
onClick: Function as PropType<(e?: MouseEvent | TouchEvent) => void>,
|
color?: string;
|
||||||
onHold: Function as PropType<VoidFunction>,
|
glowColor?: string;
|
||||||
color: processedPropType<string>(String),
|
mark?: boolean | string;
|
||||||
glowColor: processedPropType<string>(String),
|
}>();
|
||||||
canClick: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
mark: processedPropType<boolean | string>(Boolean, String),
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
MarkNode,
|
|
||||||
Node
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { onClick, onHold, display } = toRefs(props);
|
|
||||||
|
|
||||||
const comp = computeOptionalComponent(display);
|
const comp = computeOptionalComponent(toRef(props, "display"));
|
||||||
|
|
||||||
const { start, stop } = setupHoldToClick(onClick, onHold);
|
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
|
||||||
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
comp,
|
|
||||||
unref,
|
|
||||||
Visibility,
|
|
||||||
isCoercableComponent,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Decorator, GenericDecorator } from "features/decorators/common";
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
|
@ -141,7 +141,9 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
||||||
if (treeNode.onClick) {
|
if (treeNode.onClick) {
|
||||||
const onClick = treeNode.onClick.bind(treeNode);
|
const onClick = treeNode.onClick.bind(treeNode);
|
||||||
treeNode.onClick = function (e) {
|
treeNode.onClick = function (e) {
|
||||||
if (unref(treeNode.canClick) !== false) {
|
if (
|
||||||
|
unref(treeNode.canClick as ProcessedComputable<boolean | undefined>) !== false
|
||||||
|
) {
|
||||||
onClick(e);
|
onClick(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -149,7 +151,9 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
||||||
if (treeNode.onHold) {
|
if (treeNode.onHold) {
|
||||||
const onHold = treeNode.onHold.bind(treeNode);
|
const onHold = treeNode.onHold.bind(treeNode);
|
||||||
treeNode.onHold = function () {
|
treeNode.onHold = function () {
|
||||||
if (unref(treeNode.canClick) !== false) {
|
if (
|
||||||
|
unref(treeNode.canClick as ProcessedComputable<boolean | undefined>) !== false
|
||||||
|
) {
|
||||||
onHold();
|
onHold();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -342,15 +346,15 @@ export const branchedResetPropagation = function (
|
||||||
if (links == null) return;
|
if (links == null) return;
|
||||||
const reset: GenericTreeNode[] = [];
|
const reset: GenericTreeNode[] = [];
|
||||||
let current = [resettingNode];
|
let current = [resettingNode];
|
||||||
while (current.length != 0) {
|
while (current.length !== 0) {
|
||||||
const next: GenericTreeNode[] = [];
|
const next: GenericTreeNode[] = [];
|
||||||
for (const node of current) {
|
for (const node of current) {
|
||||||
for (const link of links.filter(link => link.startNode === node)) {
|
for (const link of links.filter(link => link.startNode === node)) {
|
||||||
if ([...reset, ...current].includes(link.endNode)) continue
|
if ([...reset, ...current].includes(link.endNode)) continue;
|
||||||
next.push(link.endNode);
|
next.push(link.endNode);
|
||||||
link.endNode.reset?.reset();
|
link.endNode.reset?.reset();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
reset.push(...current);
|
reset.push(...current);
|
||||||
current = next;
|
current = next;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script setup lang="tsx">
|
||||||
import "components/common/features.css";
|
import "components/common/features.css";
|
||||||
import MarkNode from "components/MarkNode.vue";
|
import MarkNode from "components/MarkNode.vue";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
|
@ -32,94 +32,56 @@ import type { StyleValue } from "features/feature";
|
||||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||||
import type { GenericUpgrade } from "features/upgrades/upgrade";
|
import type { GenericUpgrade } from "features/upgrades/upgrade";
|
||||||
import { displayRequirements, Requirements } from "game/requirements";
|
import { displayRequirements, Requirements } from "game/requirements";
|
||||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||||
import type { Component, PropType, UnwrapRef } from "vue";
|
import type { Component, UnwrapRef } from "vue";
|
||||||
import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
import { shallowRef, unref, watchEffect } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
display: UnwrapRef<GenericUpgrade["display"]>;
|
||||||
display: {
|
visibility: Visibility | boolean;
|
||||||
type: processedPropType<UnwrapRef<GenericUpgrade["display"]>>(String, Object, Function),
|
style?: StyleValue;
|
||||||
required: true
|
classes?: Record<string, boolean>;
|
||||||
},
|
requirements: Requirements;
|
||||||
visibility: {
|
canPurchase: boolean;
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
bought: boolean;
|
||||||
required: true
|
mark?: boolean | string;
|
||||||
},
|
id: string;
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
purchase?: VoidFunction;
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
}>();
|
||||||
requirements: {
|
|
||||||
type: Object as PropType<Requirements>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
canPurchase: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
bought: {
|
|
||||||
type: processedPropType<boolean>(Boolean),
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
mark: processedPropType<boolean | string>(Boolean, String),
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
purchase: {
|
|
||||||
type: Function as PropType<VoidFunction>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Node,
|
|
||||||
MarkNode
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { display, requirements, bought } = toRefs(props);
|
|
||||||
|
|
||||||
const component = shallowRef<Component | string>("");
|
const component = shallowRef<Component | string>("");
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const currDisplay = unwrapRef(display);
|
const currDisplay = props.display;
|
||||||
if (currDisplay == null) {
|
if (currDisplay == null) {
|
||||||
component.value = "";
|
component.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
if (isCoercableComponent(currDisplay)) {
|
|
||||||
component.value = coerceComponent(currDisplay);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
|
||||||
const Description = coerceComponent(currDisplay.description, "div");
|
|
||||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
|
||||||
component.value = coerceComponent(
|
|
||||||
jsx(() => (
|
|
||||||
<span>
|
|
||||||
{currDisplay.title != null ? (
|
|
||||||
<div>
|
|
||||||
<Title />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<Description />
|
|
||||||
{currDisplay.effectDisplay != null ? (
|
|
||||||
<div>
|
|
||||||
Currently: <EffectDisplay />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{bought.value ? null : <><br />{displayRequirements(requirements.value)}</>}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
component,
|
|
||||||
unref,
|
|
||||||
Visibility,
|
|
||||||
isVisible,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (isCoercableComponent(currDisplay)) {
|
||||||
|
component.value = coerceComponent(currDisplay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||||
|
const Description = coerceComponent(currDisplay.description, "div");
|
||||||
|
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||||
|
component.value = coerceComponent(
|
||||||
|
jsx(() => (
|
||||||
|
<span>
|
||||||
|
{currDisplay.title != null ? (
|
||||||
|
<div>
|
||||||
|
<Title />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<Description />
|
||||||
|
{currDisplay.effectDisplay != null ? (
|
||||||
|
<div>
|
||||||
|
Currently: <EffectDisplay />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{props.bought ? null : <><br />{displayRequirements(props.requirements)}</>}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { isArray } from "@vue/shared";
|
|
||||||
import { GenericDecorator } from "features/decorators/common";
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
|
@ -151,7 +150,7 @@ export function createUpgrade<T extends UpgradeOptions>(
|
||||||
};
|
};
|
||||||
|
|
||||||
const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade);
|
const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade);
|
||||||
if (isArray(upgrade.requirements)) {
|
if (Array.isArray(upgrade.requirements)) {
|
||||||
upgrade.requirements.unshift(visibilityRequirement);
|
upgrade.requirements.unshift(visibilityRequirement);
|
||||||
} else {
|
} else {
|
||||||
upgrade.requirements = [visibilityRequirement, upgrade.requirements];
|
upgrade.requirements = [visibilityRequirement, upgrade.requirements];
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { Settings } from "game/settings";
|
||||||
import { createNanoEvents } from "nanoevents";
|
import { createNanoEvents } from "nanoevents";
|
||||||
import type { App } from "vue";
|
import type { App } from "vue";
|
||||||
import type { GenericLayer } from "./layers";
|
import type { GenericLayer } from "./layers";
|
||||||
|
import state from "./state";
|
||||||
|
|
||||||
/** All types of events able to be sent or emitted from the global event bus. */
|
/** All types of events able to be sent or emitted from the global event bus. */
|
||||||
export interface GlobalEvents {
|
export interface GlobalEvents {
|
||||||
|
@ -59,3 +60,7 @@ if ("fonts" in document) {
|
||||||
// JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document
|
// JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document
|
||||||
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
|
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.onmousemove = function () {
|
||||||
|
state.mouseActivity[state.mouseActivity.length - 1] = true;
|
||||||
|
};
|
||||||
|
|
|
@ -48,6 +48,7 @@ export interface InternalFormula<T extends [FormulaSource] | FormulaSource[]> {
|
||||||
invertIntegral?(value: DecimalSource): DecimalSource;
|
invertIntegral?(value: DecimalSource): DecimalSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||||
export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[]> {
|
export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[]> {
|
||||||
readonly inputs: T;
|
readonly inputs: T;
|
||||||
|
|
||||||
|
@ -56,6 +57,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
||||||
protected readonly internalIntegrate: IntegrateFunction<T> | undefined;
|
protected readonly internalIntegrate: IntegrateFunction<T> | undefined;
|
||||||
protected readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
|
protected readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
|
||||||
protected readonly applySubstitution: SubstitutionFunction<T> | undefined;
|
protected readonly applySubstitution: SubstitutionFunction<T> | undefined;
|
||||||
|
protected readonly description: string | undefined;
|
||||||
protected readonly internalVariables: number;
|
protected readonly internalVariables: number;
|
||||||
|
|
||||||
public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
|
public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
|
||||||
|
@ -85,6 +87,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
||||||
this.internalIntegrate = readonlyProperties.internalIntegrate;
|
this.internalIntegrate = readonlyProperties.internalIntegrate;
|
||||||
this.internalIntegrateInner = readonlyProperties.internalIntegrateInner;
|
this.internalIntegrateInner = readonlyProperties.internalIntegrateInner;
|
||||||
this.applySubstitution = readonlyProperties.applySubstitution;
|
this.applySubstitution = readonlyProperties.applySubstitution;
|
||||||
|
this.description = options.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupVariable({
|
private setupVariable({
|
||||||
|
@ -216,6 +219,25 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
||||||
return new Formula({ variable: value });
|
return new Formula({ variable: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stringifies the formula so it's more easy to read in the console
|
||||||
|
* @param formula The formula source to print, used for mapping inputs
|
||||||
|
*/
|
||||||
|
public static stringify(formula: FormulaSource): string {
|
||||||
|
if (formula instanceof InternalFormula) {
|
||||||
|
if (formula.description != null) {
|
||||||
|
return formula.description;
|
||||||
|
}
|
||||||
|
if (formula.internalEvaluate == null) {
|
||||||
|
return formula.hasVariable() ? "x" : format(formula.inputs[0] ?? 0);
|
||||||
|
}
|
||||||
|
return `${formula.internalEvaluate.name}(${formula.inputs
|
||||||
|
.map(Formula.stringify)
|
||||||
|
.join(", ")})`;
|
||||||
|
}
|
||||||
|
return format(unref(formula));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO add integration support to step-wise functions
|
// TODO add integration support to step-wise functions
|
||||||
/**
|
/**
|
||||||
* Creates a step-wise formula. After {@link start} the formula will have an additional modifier.
|
* Creates a step-wise formula. After {@link start} the formula will have an additional modifier.
|
||||||
|
@ -256,7 +278,9 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
||||||
return new Formula({
|
return new Formula({
|
||||||
inputs: [value],
|
inputs: [value],
|
||||||
evaluate: evalStep,
|
evaluate: evalStep,
|
||||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined
|
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined,
|
||||||
|
// Can't do anything more descriptive, due to formula's input always being a variable
|
||||||
|
description: "indeterminate"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,7 +333,9 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
||||||
return new Formula({
|
return new Formula({
|
||||||
inputs: [value],
|
inputs: [value],
|
||||||
evaluate: evalStep,
|
evaluate: evalStep,
|
||||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined
|
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined,
|
||||||
|
// Can't do anything more descriptive, due to formula's input always being a variable
|
||||||
|
description: "indeterminate"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public static conditional(
|
public static conditional(
|
||||||
|
@ -878,6 +904,10 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public stringify() {
|
||||||
|
return Formula.stringify(this);
|
||||||
|
}
|
||||||
|
|
||||||
public step(
|
public step(
|
||||||
start: Computable<DecimalSource>,
|
start: Computable<DecimalSource>,
|
||||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||||
|
@ -1402,28 +1432,6 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stringifies a formula so it's more easy to read in the console
|
|
||||||
* @param formula The formula to print
|
|
||||||
*/
|
|
||||||
export function printFormula(formula: FormulaSource): string {
|
|
||||||
if (formula instanceof InternalFormula) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
return formula.internalEvaluate == null
|
|
||||||
? formula.hasVariable()
|
|
||||||
? "x"
|
|
||||||
: formula.inputs[0] ?? 0
|
|
||||||
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
formula.internalEvaluate.name +
|
|
||||||
"(" +
|
|
||||||
formula.inputs.map(printFormula).join(", ") +
|
|
||||||
")";
|
|
||||||
}
|
|
||||||
return format(unref(formula));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@link cumulativeCost} is changed to false, the calculation will be much faster with higher numbers.
|
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@link cumulativeCost} is changed to false, the calculation will be much faster with higher numbers.
|
||||||
* @param formula The formula to use for calculating buy max from
|
* @param formula The formula to use for calculating buy max from
|
||||||
|
|
|
@ -552,7 +552,9 @@ export function tetrate(
|
||||||
export function invertTetrate(
|
export function invertTetrate(
|
||||||
value: DecimalSource,
|
value: DecimalSource,
|
||||||
base: FormulaSource,
|
base: FormulaSource,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
height: FormulaSource,
|
height: FormulaSource,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
payload: FormulaSource
|
payload: FormulaSource
|
||||||
) {
|
) {
|
||||||
if (hasVariable(base)) {
|
if (hasVariable(base)) {
|
||||||
|
@ -576,6 +578,7 @@ export function invertIteratedExp(
|
||||||
value: DecimalSource,
|
value: DecimalSource,
|
||||||
lhs: FormulaSource,
|
lhs: FormulaSource,
|
||||||
height: FormulaSource,
|
height: FormulaSource,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
payload: FormulaSource
|
payload: FormulaSource
|
||||||
) {
|
) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
@ -626,6 +629,7 @@ export function invertLayeradd(
|
||||||
value: DecimalSource,
|
value: DecimalSource,
|
||||||
lhs: FormulaSource,
|
lhs: FormulaSource,
|
||||||
diff: FormulaSource,
|
diff: FormulaSource,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
base: FormulaSource
|
base: FormulaSource
|
||||||
) {
|
) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
|
8
src/game/formulas/types.d.ts
vendored
8
src/game/formulas/types.d.ts
vendored
|
@ -37,9 +37,13 @@ type SubstitutionFunction<T> = (
|
||||||
...inputs: T
|
...inputs: T
|
||||||
) => GenericFormula;
|
) => GenericFormula;
|
||||||
|
|
||||||
type VariableFormulaOptions = { variable: ProcessedComputable<DecimalSource> };
|
type VariableFormulaOptions = {
|
||||||
|
variable: ProcessedComputable<DecimalSource>;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
type ConstantFormulaOptions = {
|
type ConstantFormulaOptions = {
|
||||||
inputs: [FormulaSource];
|
inputs: [FormulaSource];
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
||||||
inputs: T;
|
inputs: T;
|
||||||
|
@ -48,6 +52,7 @@ type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
||||||
integrate?: IntegrateFunction<T>;
|
integrate?: IntegrateFunction<T>;
|
||||||
integrateInner?: IntegrateFunction<T>;
|
integrateInner?: IntegrateFunction<T>;
|
||||||
applySubstitution?: SubstitutionFunction<T>;
|
applySubstitution?: SubstitutionFunction<T>;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
|
type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
|
||||||
| VariableFormulaOptions
|
| VariableFormulaOptions
|
||||||
|
@ -63,6 +68,7 @@ type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
|
||||||
internalIntegrateInner?: IntegrateFunction<T>;
|
internalIntegrateInner?: IntegrateFunction<T>;
|
||||||
applySubstitution?: SubstitutionFunction<T>;
|
applySubstitution?: SubstitutionFunction<T>;
|
||||||
innermostVariable?: ProcessedComputable<DecimalSource>;
|
innermostVariable?: ProcessedComputable<DecimalSource>;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SubstitutionStack = ((value: GenericFormula) => GenericFormula)[] | undefined;
|
type SubstitutionStack = ((value: GenericFormula) => GenericFormula)[] | undefined;
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
|
import { hasWon } from "data/projEntry";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import Decimal from "util/bignum";
|
import Decimal from "util/bignum";
|
||||||
import { loadingSave } from "util/save";
|
import { loadingSave } from "util/save";
|
||||||
import type { Ref } from "vue";
|
|
||||||
import { watch } from "vue";
|
import { watch } from "vue";
|
||||||
import player from "./player";
|
import player from "./player";
|
||||||
import state from "./state";
|
import state from "./state";
|
||||||
|
|
||||||
let intervalID: NodeJS.Timer | null = null;
|
let intervalID: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
// Not imported immediately due to dependency cycles
|
|
||||||
// This gets set during startGameLoop(), and will only be used in the update function
|
|
||||||
let hasWon: null | Ref<boolean> = null;
|
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
@ -43,7 +39,7 @@ function update() {
|
||||||
loadingSave.value = false;
|
loadingSave.value = false;
|
||||||
|
|
||||||
// Add offline time if any
|
// Add offline time if any
|
||||||
if (player.offlineTime != undefined) {
|
if (player.offlineTime != null) {
|
||||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +59,7 @@ function update() {
|
||||||
diff = Math.min(diff, projInfo.maxTickLength);
|
diff = Math.min(diff, projInfo.maxTickLength);
|
||||||
|
|
||||||
// Apply dev speed
|
// Apply dev speed
|
||||||
if (player.devSpeed != undefined) {
|
if (player.devSpeed != null) {
|
||||||
diff *= player.devSpeed;
|
diff *= player.devSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,15 +91,22 @@ function update() {
|
||||||
|
|
||||||
/** Starts the game loop for the project, which updates the game in ticks. */
|
/** Starts the game loop for the project, which updates the game in ticks. */
|
||||||
export async function startGameLoop() {
|
export async function startGameLoop() {
|
||||||
hasWon = (await import("data/projEntry")).hasWon;
|
|
||||||
watch(hasWon, hasWon => {
|
|
||||||
if (hasWon) {
|
|
||||||
globalBus.emit("gameWon");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (settings.unthrottled) {
|
if (settings.unthrottled) {
|
||||||
requestAnimationFrame(update);
|
requestAnimationFrame(update);
|
||||||
} else {
|
} else {
|
||||||
intervalID = setInterval(update, 50);
|
intervalID = setInterval(update, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(hasWon, hasWon => {
|
||||||
|
if (hasWon) {
|
||||||
|
globalBus.emit("gameWon");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
state.mouseActivity = [...state.mouseActivity.slice(-7), false];
|
||||||
|
},
|
||||||
|
1000 * 60 * 60
|
||||||
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Modal from "components/Modal.vue";
|
import Modal from "components/modals/Modal.vue";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
JSXFunction,
|
JSXFunction,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { isArray } from "@vue/shared";
|
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import type { GenericLayer } from "game/layers";
|
import type { GenericLayer } from "game/layers";
|
||||||
import { addingLayers, persistentRefs } from "game/layers";
|
import { addingLayers, persistentRefs } from "game/layers";
|
||||||
|
@ -62,6 +61,8 @@ export type State =
|
||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
| DecimalSource
|
| DecimalSource
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
| { [key: string]: State }
|
| { [key: string]: State }
|
||||||
| { [key: number]: State };
|
| { [key: number]: State };
|
||||||
|
|
||||||
|
@ -227,7 +228,7 @@ export function noPersist<T extends Persistent<S>, S extends State>(persistent:
|
||||||
if (key === PersistentState) {
|
if (key === PersistentState) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (key == SkipPersistence) {
|
if (key === SkipPersistence) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return Reflect.has(target, key);
|
return Reflect.has(target, key);
|
||||||
|
@ -279,7 +280,7 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
||||||
// Handle SaveDataPath
|
// Handle SaveDataPath
|
||||||
const newPath = [layer.id, ...path, key];
|
const newPath = [layer.id, ...path, key];
|
||||||
if (
|
if (
|
||||||
value[SaveDataPath] != undefined &&
|
value[SaveDataPath] != null &&
|
||||||
JSON.stringify(newPath) !== JSON.stringify(value[SaveDataPath])
|
JSON.stringify(newPath) !== JSON.stringify(value[SaveDataPath])
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -339,7 +340,7 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
||||||
// Show warning for persistent values inside arrays
|
// Show warning for persistent values inside arrays
|
||||||
// TODO handle arrays better
|
// TODO handle arrays better
|
||||||
if (foundPersistentInChild) {
|
if (foundPersistentInChild) {
|
||||||
if (isArray(value) && !isArray(obj)) {
|
if (Array.isArray(value) && !Array.isArray(obj)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves.",
|
"Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves.",
|
||||||
ProxyState in obj
|
ProxyState in obj
|
||||||
|
|
|
@ -36,12 +36,12 @@ export type LayerData<T> = {
|
||||||
[P in keyof T]?: T[P] extends (infer U)[]
|
[P in keyof T]?: T[P] extends (infer U)[]
|
||||||
? Record<string, LayerData<U>>
|
? Record<string, LayerData<U>>
|
||||||
: T[P] extends Record<string, never>
|
: T[P] extends Record<string, never>
|
||||||
? never
|
? never
|
||||||
: T[P] extends Ref<infer S>
|
: T[P] extends Ref<infer S>
|
||||||
? S
|
? S
|
||||||
: T[P] extends object
|
: T[P] extends object
|
||||||
? LayerData<T[P]>
|
? LayerData<T[P]>
|
||||||
: T[P];
|
: T[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
const player = reactive<Player>({
|
const player = reactive<Player>({
|
||||||
|
@ -64,7 +64,8 @@ export default window.player = player;
|
||||||
|
|
||||||
/** Convert a player save data object into a JSON string. Unwraps refs. */
|
/** Convert a player save data object into a JSON string. Unwraps refs. */
|
||||||
export function stringifySave(player: Player): string {
|
export function stringifySave(player: Player): string {
|
||||||
return JSON.stringify(player, (key, value) => unref(value));
|
// Convert undefineds into nulls for proper parsing
|
||||||
|
return JSON.stringify(player, (key, value) => unref(value) ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { isArray } from "@vue/shared";
|
|
||||||
import {
|
import {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
@ -19,6 +18,7 @@ import {
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { joinJSX, renderJSX } from "util/vue";
|
import { joinJSX, renderJSX } from "util/vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
import { JSX } from "vue/jsx-runtime";
|
||||||
import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
|
import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
|
||||||
import type { GenericFormula } from "./formulas/types";
|
import type { GenericFormula } from "./formulas/types";
|
||||||
import { DefaultValue, Persistent } from "./persistence";
|
import { DefaultValue, Persistent } from "./persistence";
|
||||||
|
@ -179,7 +179,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
||||||
? calculateCost(
|
? calculateCost(
|
||||||
req.cost,
|
req.cost,
|
||||||
amount ?? 1,
|
amount ?? 1,
|
||||||
unref(req.cumulativeCost) as boolean,
|
unref(req.cumulativeCost as ProcessedComputable<boolean>),
|
||||||
unref(req.directSum) as number
|
unref(req.directSum) as number
|
||||||
)
|
)
|
||||||
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
||||||
|
@ -222,7 +222,9 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
||||||
Decimal.gte(
|
Decimal.gte(
|
||||||
req.resource.value,
|
req.resource.value,
|
||||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||||
) ? 1 : 0
|
)
|
||||||
|
? 1
|
||||||
|
: 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,7 +269,7 @@ export function createBooleanRequirement(
|
||||||
* @param requirements The 1+ requirements to check
|
* @param requirements The 1+ requirements to check
|
||||||
*/
|
*/
|
||||||
export function requirementsMet(requirements: Requirements): boolean {
|
export function requirementsMet(requirements: Requirements): boolean {
|
||||||
if (isArray(requirements)) {
|
if (Array.isArray(requirements)) {
|
||||||
return requirements.every(requirementsMet);
|
return requirements.every(requirementsMet);
|
||||||
}
|
}
|
||||||
const reqsMet = unref(requirements.requirementMet);
|
const reqsMet = unref(requirements.requirementMet);
|
||||||
|
@ -279,7 +281,7 @@ export function requirementsMet(requirements: Requirements): boolean {
|
||||||
* @param requirements The 1+ requirements to check
|
* @param requirements The 1+ requirements to check
|
||||||
*/
|
*/
|
||||||
export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
||||||
if (isArray(requirements)) {
|
if (Array.isArray(requirements)) {
|
||||||
return requirements.map(maxRequirementsMet).reduce(Decimal.min);
|
return requirements.map(maxRequirementsMet).reduce(Decimal.min);
|
||||||
}
|
}
|
||||||
const reqsMet = unref(requirements.requirementMet);
|
const reqsMet = unref(requirements.requirementMet);
|
||||||
|
@ -297,13 +299,13 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
||||||
* @param amount The amount of levels earned to be displayed
|
* @param amount The amount of levels earned to be displayed
|
||||||
*/
|
*/
|
||||||
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||||
if (isArray(requirements)) {
|
if (Array.isArray(requirements)) {
|
||||||
requirements = requirements.filter(r => isVisible(r.visibility));
|
requirements = requirements.filter(r => isVisible(r.visibility));
|
||||||
if (requirements.length === 1) {
|
if (requirements.length === 1) {
|
||||||
requirements = requirements[0];
|
requirements = requirements[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isArray(requirements)) {
|
if (Array.isArray(requirements)) {
|
||||||
requirements = requirements.filter(r => "partialDisplay" in r);
|
requirements = requirements.filter(r => "partialDisplay" in r);
|
||||||
const withCosts = requirements.filter(r => unref(r.requiresPay));
|
const withCosts = requirements.filter(r => unref(r.requiresPay));
|
||||||
const withoutCosts = requirements.filter(r => !unref(r.requiresPay));
|
const withoutCosts = requirements.filter(r => !unref(r.requiresPay));
|
||||||
|
@ -341,7 +343,7 @@ export function displayRequirements(requirements: Requirements, amount: DecimalS
|
||||||
* @param amount How many levels to pay for
|
* @param amount How many levels to pay for
|
||||||
*/
|
*/
|
||||||
export function payRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
export function payRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||||
if (isArray(requirements)) {
|
if (Array.isArray(requirements)) {
|
||||||
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
|
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
|
||||||
} else if (unref(requirements.requiresPay)) {
|
} else if (unref(requirements.requiresPay)) {
|
||||||
requirements.pay?.(amount);
|
requirements.pay?.(amount);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Themes } from "data/themes";
|
||||||
import type { CoercableComponent } from "features/feature";
|
import type { CoercableComponent } from "features/feature";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
import { hardReset } from "util/save";
|
import { decodeSave, hardReset } from "util/save";
|
||||||
import { reactive, watch } from "vue";
|
import { reactive, watch } from "vue";
|
||||||
|
|
||||||
/** The player's settings object. */
|
/** The player's settings object. */
|
||||||
|
@ -20,6 +20,8 @@ export interface Settings {
|
||||||
unthrottled: boolean;
|
unthrottled: boolean;
|
||||||
/** Whether to align modifiers to the unit. */
|
/** Whether to align modifiers to the unit. */
|
||||||
alignUnits: boolean;
|
alignUnits: boolean;
|
||||||
|
/** Whether or not to show a video game health warning after playing excessively. */
|
||||||
|
showHealthWarning: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive<Partial<Settings>>({
|
const state = reactive<Partial<Settings>>({
|
||||||
|
@ -28,7 +30,8 @@ const state = reactive<Partial<Settings>>({
|
||||||
showTPS: true,
|
showTPS: true,
|
||||||
theme: Themes.Nordic,
|
theme: Themes.Nordic,
|
||||||
unthrottled: false,
|
unthrottled: false,
|
||||||
alignUnits: false
|
alignUnits: false,
|
||||||
|
showHealthWarning: true
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
@ -56,12 +59,15 @@ declare global {
|
||||||
export default window.settings = state as Settings;
|
export default window.settings = state as Settings;
|
||||||
/** A function that erases all player settings, including all saves. */
|
/** A function that erases all player settings, including all saves. */
|
||||||
export const hardResetSettings = (window.hardResetSettings = () => {
|
export const hardResetSettings = (window.hardResetSettings = () => {
|
||||||
const settings = {
|
// Only partial because of any properties that are only added during the loadSettings event.
|
||||||
|
const settings: Partial<Settings> = {
|
||||||
active: "",
|
active: "",
|
||||||
saves: [],
|
saves: [],
|
||||||
showTPS: true,
|
showTPS: true,
|
||||||
theme: Themes.Nordic,
|
theme: Themes.Nordic,
|
||||||
alignUnits: false
|
unthrottled: false,
|
||||||
|
alignUnits: false,
|
||||||
|
showHealthWarning: true
|
||||||
};
|
};
|
||||||
globalBus.emit("loadSettings", settings);
|
globalBus.emit("loadSettings", settings);
|
||||||
Object.assign(state, settings);
|
Object.assign(state, settings);
|
||||||
|
@ -78,16 +84,8 @@ export function loadSettings(): void {
|
||||||
try {
|
try {
|
||||||
let item: string | null = localStorage.getItem(projInfo.id);
|
let item: string | null = localStorage.getItem(projInfo.id);
|
||||||
if (item != null && item !== "") {
|
if (item != null && item !== "") {
|
||||||
if (item[0] === "{") {
|
item = decodeSave(item);
|
||||||
// plaintext. No processing needed
|
if (item == null) {
|
||||||
} else if (item[0] === "e") {
|
|
||||||
// Assumed to be base64, which starts with e
|
|
||||||
item = decodeURIComponent(escape(atob(item)));
|
|
||||||
} else if (item[0] === "ᯡ") {
|
|
||||||
// Assumed to be lz, which starts with ᯡ
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
item = LZString.decompressFromUTF16(item)!;
|
|
||||||
} else {
|
|
||||||
console.warn("Unable to determine settings encoding", item);
|
console.warn("Unable to determine settings encoding", item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import type { Persistent } from "./persistence";
|
||||||
export interface Transient {
|
export interface Transient {
|
||||||
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
|
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
|
||||||
lastTenTicks: number[];
|
lastTenTicks: number[];
|
||||||
|
/** A list of bools represnting which of the last few hours had mouse activity. */
|
||||||
|
mouseActivity: boolean[];
|
||||||
/** Whether or not a NaN value has been detected and undealt with. */
|
/** Whether or not a NaN value has been detected and undealt with. */
|
||||||
hasNaN: boolean;
|
hasNaN: boolean;
|
||||||
/** The location within the player save data object of the NaN value. */
|
/** The location within the player save data object of the NaN value. */
|
||||||
|
@ -25,6 +27,7 @@ declare global {
|
||||||
/** The global transient state object. */
|
/** The global transient state object. */
|
||||||
export default window.state = shallowReactive<Transient>({
|
export default window.state = shallowReactive<Transient>({
|
||||||
lastTenTicks: [],
|
lastTenTicks: [],
|
||||||
|
mouseActivity: [false],
|
||||||
hasNaN: false,
|
hasNaN: false,
|
||||||
NaNPath: [],
|
NaNPath: [],
|
||||||
errors: reactive([])
|
errors: reactive([])
|
||||||
|
|
18
src/main.css
18
src/main.css
|
@ -66,3 +66,21 @@ ul {
|
||||||
.Vue-Toastification__toast {
|
.Vue-Toastification__toast {
|
||||||
margin: unset;
|
margin: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
36
src/main.ts
36
src/main.ts
|
@ -3,11 +3,14 @@ import App from "App.vue";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import "game/notifications";
|
import "game/notifications";
|
||||||
import state from "game/state";
|
import state from "game/state";
|
||||||
|
import "util/galaxy";
|
||||||
import { load } from "util/save";
|
import { load } from "util/save";
|
||||||
import { useRegisterSW } from "virtual:pwa-register/vue";
|
import { useRegisterSW } from "virtual:pwa-register/vue";
|
||||||
import type { App as VueApp } from "vue";
|
import type { App as VueApp } from "vue";
|
||||||
import { createApp, nextTick } from "vue";
|
import { createApp, nextTick } from "vue";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
|
import { globalBus } from "./game/events";
|
||||||
|
import { startGameLoop } from "./game/gameLoop";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
|
@ -17,11 +20,6 @@ declare global {
|
||||||
vue: VueApp;
|
vue: VueApp;
|
||||||
projInfo: typeof projInfo;
|
projInfo: typeof projInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fix for typedoc treating import functions as taking AssertOptions instead of GlobOptions. */
|
|
||||||
interface AssertOptions {
|
|
||||||
as: string;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = console.error;
|
const error = console.error;
|
||||||
|
@ -60,8 +58,6 @@ requestAnimationFrame(async () => {
|
||||||
"padding: 4px;"
|
"padding: 4px;"
|
||||||
);
|
);
|
||||||
await load();
|
await load();
|
||||||
const { globalBus } = await import("./game/events");
|
|
||||||
const { startGameLoop } = await import("./game/gameLoop");
|
|
||||||
|
|
||||||
// Create Vue
|
// Create Vue
|
||||||
const vue = (window.vue = createApp(App));
|
const vue = (window.vue = createApp(App));
|
||||||
|
@ -74,33 +70,13 @@ requestAnimationFrame(async () => {
|
||||||
// Setup PWA update prompt
|
// Setup PWA update prompt
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { updateServiceWorker } = useRegisterSW({
|
useRegisterSW({
|
||||||
onNeedRefresh() {
|
immediate: true,
|
||||||
toast.info("New content available, click here to update.", {
|
|
||||||
timeout: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
draggable: false,
|
|
||||||
icon: {
|
|
||||||
iconClass: "material-icons",
|
|
||||||
iconChildren: "refresh",
|
|
||||||
iconTag: "i"
|
|
||||||
},
|
|
||||||
rtl: false,
|
|
||||||
onClick() {
|
|
||||||
updateServiceWorker();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onOfflineReady() {
|
onOfflineReady() {
|
||||||
toast.info("App ready to work offline");
|
toast.info("App ready to work offline");
|
||||||
},
|
},
|
||||||
onRegisterError: console.warn,
|
onRegisterError: console.warn,
|
||||||
onRegistered(r) {
|
onRegistered: console.info
|
||||||
if (r) {
|
|
||||||
// https://stackoverflow.com/questions/65500916/typeerror-failed-to-execute-update-on-serviceworkerregistration-illegal-in
|
|
||||||
setInterval(() => r.update(), 60 * 60 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ export function exponentialFormat(num: DecimalSource, precision: number, mantiss
|
||||||
const eString = e.gte(1e9)
|
const eString = e.gte(1e9)
|
||||||
? format(e, Math.max(Math.max(precision, 3), projInfo.defaultDecimalsShown))
|
? format(e, Math.max(Math.max(precision, 3), projInfo.defaultDecimalsShown))
|
||||||
: e.gte(10000)
|
: e.gte(10000)
|
||||||
? commaFormat(e, 0)
|
? commaFormat(e, 0)
|
||||||
: e.toStringWithDecimalPlaces(0);
|
: e.toStringWithDecimalPlaces(0);
|
||||||
if (mantissa) {
|
if (mantissa) {
|
||||||
return m.toStringWithDecimalPlaces(precision) + "e" + eString;
|
return m.toStringWithDecimalPlaces(precision) + "e" + eString;
|
||||||
} else {
|
} else {
|
||||||
|
@ -26,7 +26,7 @@ export function exponentialFormat(num: DecimalSource, precision: number, mantiss
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commaFormat(num: DecimalSource, precision: number): string {
|
export function commaFormat(num: DecimalSource, precision: number): string {
|
||||||
if (num === null || num === undefined) {
|
if (num == null) {
|
||||||
return "NaN";
|
return "NaN";
|
||||||
}
|
}
|
||||||
num = new Decimal(num);
|
num = new Decimal(num);
|
||||||
|
@ -36,12 +36,12 @@ export function commaFormat(num: DecimalSource, precision: number): string {
|
||||||
const init = num.toStringWithDecimalPlaces(precision);
|
const init = num.toStringWithDecimalPlaces(precision);
|
||||||
const portions = init.split(".");
|
const portions = init.split(".");
|
||||||
portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
|
portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
|
||||||
if (portions.length == 1) return portions[0];
|
if (portions.length === 1) return portions[0];
|
||||||
return portions[0] + "." + portions[1];
|
return portions[0] + "." + portions[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function regularFormat(num: DecimalSource, precision: number): string {
|
export function regularFormat(num: DecimalSource, precision: number): string {
|
||||||
if (num === null || num === undefined) {
|
if (num == null) {
|
||||||
return "NaN";
|
return "NaN";
|
||||||
}
|
}
|
||||||
num = new Decimal(num);
|
num = new Decimal(num);
|
||||||
|
|
|
@ -8,9 +8,8 @@ export type OptionalKeys<T> = {
|
||||||
export type OmitOptional<T> = Pick<T, RequiredKeys<T>>;
|
export type OmitOptional<T> = Pick<T, RequiredKeys<T>>;
|
||||||
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||||
|
|
||||||
export type ArrayElements<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer S>
|
export type ArrayElements<T extends ReadonlyArray<unknown>> =
|
||||||
? S
|
T extends ReadonlyArray<infer S> ? S : never;
|
||||||
: never;
|
|
||||||
|
|
||||||
// Reference:
|
// Reference:
|
||||||
// https://stackoverflow.com/questions/7225407/convert-camelcasetext-to-sentence-case-text
|
// https://stackoverflow.com/questions/7225407/convert-camelcasetext-to-sentence-case-text
|
||||||
|
@ -36,5 +35,6 @@ export enum Direction {
|
||||||
Down = "Down",
|
Down = "Down",
|
||||||
Left = "Left",
|
Left = "Left",
|
||||||
Right = "Right",
|
Right = "Right",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
|
||||||
Default = "Up"
|
Default = "Up"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,10 @@ export type ProcessedComputable<T> = T | Ref<T>;
|
||||||
export type GetComputableType<T> = T extends { [DoNotCache]: true }
|
export type GetComputableType<T> = T extends { [DoNotCache]: true }
|
||||||
? T
|
? T
|
||||||
: T extends () => infer S
|
: T extends () => infer S
|
||||||
? Ref<S>
|
? Ref<S>
|
||||||
: undefined extends T
|
: undefined extends T
|
||||||
? undefined
|
? undefined
|
||||||
: T;
|
: T;
|
||||||
export type GetComputableTypeWithDefault<T, S> = undefined extends T
|
export type GetComputableTypeWithDefault<T, S> = undefined extends T
|
||||||
? S
|
? S
|
||||||
: GetComputableType<NonNullable<T>>;
|
: GetComputableType<NonNullable<T>>;
|
||||||
|
|
186
src/util/galaxy.ts
Normal file
186
src/util/galaxy.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
|
||||||
|
import player, { Player, stringifySave } from "game/player";
|
||||||
|
import settings from "game/settings";
|
||||||
|
import LZString from "lz-string";
|
||||||
|
import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { decodeSave, loadSave, save, setupInitialStore } from "./save";
|
||||||
|
|
||||||
|
export const galaxy = ref<GalaxyApi>();
|
||||||
|
export const conflictingSaves = ref<
|
||||||
|
{ id: string; local: LoadablePlayerData; cloud: LoadablePlayerData; slot: number }[]
|
||||||
|
>([]);
|
||||||
|
export const syncedSaves = ref<string[]>([]);
|
||||||
|
|
||||||
|
export function sync() {
|
||||||
|
if (galaxy.value?.loggedIn !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (conflictingSaves.value.length > 0) {
|
||||||
|
// Pause syncing while resolving conflicted saves
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
galaxy.value
|
||||||
|
.getSaveList()
|
||||||
|
.then(syncSaves)
|
||||||
|
.then(list => {
|
||||||
|
syncedSaves.value = list.map(s => s.content.id);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup Galaxy API
|
||||||
|
initGalaxy({
|
||||||
|
supportsSaving: true,
|
||||||
|
supportsSaveManager: true,
|
||||||
|
onLoggedInChanged
|
||||||
|
})
|
||||||
|
.then(g => {
|
||||||
|
galaxy.value = g;
|
||||||
|
onLoggedInChanged(g);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
function onLoggedInChanged(g: GalaxyApi) {
|
||||||
|
if (g.loggedIn !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (conflictingSaves.value.length > 0) {
|
||||||
|
// Pause syncing while resolving conflicted saves
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.getSaveList()
|
||||||
|
.then(list => {
|
||||||
|
const saves = syncSaves(list);
|
||||||
|
syncedSaves.value = saves.map(s => s.content.id);
|
||||||
|
|
||||||
|
// If our current save has under 2 minutes of playtime, load the cloud save with the most recent time.
|
||||||
|
if (player.timePlayed < 120 * 1000 && saves.length > 0) {
|
||||||
|
const longestSave = saves.reduce((acc, curr) =>
|
||||||
|
acc.content.time < curr.content.time ? curr : acc
|
||||||
|
);
|
||||||
|
loadSave(longestSave.content);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
setInterval(sync, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSaves(
|
||||||
|
list: Record<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const savesToUpload = new Set(settings.saves.slice());
|
||||||
|
const availableSlots = new Set(new Array(11).fill(0).map((_, i) => i));
|
||||||
|
const saves = (
|
||||||
|
Object.keys(list)
|
||||||
|
.map(slot => {
|
||||||
|
const { label, content } = list[slot as unknown as number];
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
slot: parseInt(slot),
|
||||||
|
label,
|
||||||
|
content: JSON.parse(decodeSave(content) ?? "")
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
n =>
|
||||||
|
n != null &&
|
||||||
|
typeof n.content.id === "string" &&
|
||||||
|
typeof n.content.time === "number" &&
|
||||||
|
typeof n.content.timePlayed === "number"
|
||||||
|
) as {
|
||||||
|
slot: number;
|
||||||
|
label?: string;
|
||||||
|
content: Partial<Player> & { id: string; time: number; timePlayed: number };
|
||||||
|
}[]
|
||||||
|
).filter(cloudSave => {
|
||||||
|
if (cloudSave.label != null) {
|
||||||
|
cloudSave.content.name = cloudSave.label;
|
||||||
|
}
|
||||||
|
availableSlots.delete(cloudSave.slot);
|
||||||
|
const localSaveId = settings.saves.find(id => id === cloudSave.content.id);
|
||||||
|
if (localSaveId == null) {
|
||||||
|
settings.saves.push(cloudSave.content.id);
|
||||||
|
save(setupInitialStore(cloudSave.content));
|
||||||
|
} else {
|
||||||
|
savesToUpload.delete(localSaveId);
|
||||||
|
try {
|
||||||
|
const localSave = JSON.parse(
|
||||||
|
decodeSave(localStorage.getItem(localSaveId) ?? "") ?? ""
|
||||||
|
) as Partial<Player> | null;
|
||||||
|
if (localSave == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
localSave.id = localSaveId;
|
||||||
|
localSave.time = localSave.time ?? 0;
|
||||||
|
localSave.timePlayed = localSave.timePlayed ?? 0;
|
||||||
|
|
||||||
|
const timePlayedDiff = Math.abs(
|
||||||
|
localSave.timePlayed - cloudSave.content.timePlayed
|
||||||
|
);
|
||||||
|
const timeDiff = Math.abs(localSave.time - cloudSave.content.time);
|
||||||
|
// If their last played time and total time played are both within 2 minutes, just use the newer save (very unlikely to be coincidence)
|
||||||
|
// Otherwise, ask the player
|
||||||
|
if (timePlayedDiff < 120 * 1000 && timeDiff < 120 * 1000) {
|
||||||
|
if (localSave.time < cloudSave.content.time) {
|
||||||
|
save(setupInitialStore(cloudSave.content));
|
||||||
|
if (settings.active === localSaveId) {
|
||||||
|
loadSave(cloudSave.content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
galaxy.value
|
||||||
|
?.save(
|
||||||
|
cloudSave.slot,
|
||||||
|
LZString.compressToUTF16(
|
||||||
|
stringifySave(setupInitialStore(localSave))
|
||||||
|
),
|
||||||
|
localSave.name ?? cloudSave.label
|
||||||
|
)
|
||||||
|
.catch(console.error);
|
||||||
|
// Update cloud save content for the return value
|
||||||
|
cloudSave.content = localSave as Player;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conflictingSaves.value.push({
|
||||||
|
id: localSaveId,
|
||||||
|
cloud: cloudSave.content,
|
||||||
|
local: localSave as LoadablePlayerData,
|
||||||
|
slot: cloudSave.slot
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
savesToUpload.forEach(id => {
|
||||||
|
try {
|
||||||
|
if (availableSlots.size > 0) {
|
||||||
|
const localSave = localStorage.getItem(id) ?? "";
|
||||||
|
const parsedLocalSave = JSON.parse(decodeSave(localSave) ?? "");
|
||||||
|
const slot = availableSlots.values().next().value;
|
||||||
|
if (slot == null) return;
|
||||||
|
galaxy.value
|
||||||
|
?.save(slot, localSave, parsedLocalSave.name)
|
||||||
|
.then(() => syncedSaves.value.push(parsedLocalSave.id))
|
||||||
|
.catch(console.error);
|
||||||
|
availableSlots.delete(slot);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return saves;
|
||||||
|
}
|
|
@ -5,28 +5,30 @@ import Decimal from "util/bignum";
|
||||||
export const ProxyState = Symbol("ProxyState");
|
export const ProxyState = Symbol("ProxyState");
|
||||||
export const ProxyPath = Symbol("ProxyPath");
|
export const ProxyPath = Symbol("ProxyPath");
|
||||||
|
|
||||||
export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, unknown>
|
export type ProxiedWithState<T> =
|
||||||
? NonNullable<T> extends Decimal
|
NonNullable<T> extends Record<PropertyKey, unknown>
|
||||||
? T
|
? NonNullable<T> extends Decimal
|
||||||
: {
|
? T
|
||||||
[K in keyof T]: ProxiedWithState<T[K]>;
|
: {
|
||||||
} & {
|
[K in keyof T]: ProxiedWithState<T[K]>;
|
||||||
[ProxyState]: T;
|
} & {
|
||||||
[ProxyPath]: string[];
|
[ProxyState]: T;
|
||||||
}
|
[ProxyPath]: string[];
|
||||||
: T;
|
}
|
||||||
|
: T;
|
||||||
|
|
||||||
export type Proxied<T> = NonNullable<T> extends Record<PropertyKey, unknown>
|
export type Proxied<T> =
|
||||||
? NonNullable<T> extends Persistent<infer S>
|
NonNullable<T> extends Record<PropertyKey, unknown>
|
||||||
? NonPersistent<S>
|
? NonNullable<T> extends Persistent<infer S>
|
||||||
: NonNullable<T> extends Decimal
|
? NonPersistent<S>
|
||||||
? T
|
: NonNullable<T> extends Decimal
|
||||||
: {
|
? T
|
||||||
[K in keyof T]: Proxied<T[K]>;
|
: {
|
||||||
} & {
|
[K in keyof T]: Proxied<T[K]>;
|
||||||
[ProxyState]: T;
|
} & {
|
||||||
}
|
[ProxyState]: T;
|
||||||
: T;
|
}
|
||||||
|
: T;
|
||||||
|
|
||||||
// Takes a function that returns an object and pretends to be that object
|
// Takes a function that returns an object and pretends to be that object
|
||||||
// Note that the object is lazily calculated
|
// Note that the object is lazily calculated
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
|
||||||
|
import { fixOldSave, getInitialLayers } from "data/projEntry";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
|
import { addLayer, layers, removeLayer } from "game/layers";
|
||||||
import type { Player } from "game/player";
|
import type { Player } from "game/player";
|
||||||
import player, { stringifySave } from "game/player";
|
import player, { stringifySave } from "game/player";
|
||||||
import settings, { loadSettings } from "game/settings";
|
import settings, { loadSettings } from "game/settings";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
import { ref } from "vue";
|
import { ref, shallowReactive } from "vue";
|
||||||
|
|
||||||
export function setupInitialStore(player: Partial<Player> = {}): Player {
|
export function setupInitialStore(player: Partial<Player> = {}): Player {
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
|
@ -42,17 +45,9 @@ export async function load(): Promise<void> {
|
||||||
await loadSave(newSave());
|
await loadSave(newSave());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (save[0] === "{") {
|
save = decodeSave(save);
|
||||||
// plaintext. No processing needed
|
if (save == null) {
|
||||||
} else if (save[0] === "e") {
|
throw "Unable to determine save encoding";
|
||||||
// Assumed to be base64, which starts with e
|
|
||||||
save = decodeURIComponent(escape(atob(save)));
|
|
||||||
} else if (save[0] === "ᯡ") {
|
|
||||||
// Assumed to be lz, which starts with ᯡ
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
save = LZString.decompressFromUTF16(save)!;
|
|
||||||
} else {
|
|
||||||
throw `Unable to determine save encoding`;
|
|
||||||
}
|
}
|
||||||
const player = JSON.parse(save);
|
const player = JSON.parse(save);
|
||||||
if (player.modID !== projInfo.id) {
|
if (player.modID !== projInfo.id) {
|
||||||
|
@ -67,6 +62,23 @@ export async function load(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function decodeSave(save: string) {
|
||||||
|
if (save[0] === "{") {
|
||||||
|
// plaintext. No processing needed
|
||||||
|
} else if (save[0] === "e") {
|
||||||
|
// Assumed to be base64, which starts with e
|
||||||
|
save = decodeURIComponent(escape(atob(save)));
|
||||||
|
} else if (save[0] === "ᯡ") {
|
||||||
|
// Assumed to be lz, which starts with ᯡ
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
save = LZString.decompressFromUTF16(save)!;
|
||||||
|
} else {
|
||||||
|
console.warn("Unable to determine preset encoding", save);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return save;
|
||||||
|
}
|
||||||
|
|
||||||
export function newSave(): Player {
|
export function newSave(): Player {
|
||||||
const id = getUniqueID();
|
const id = getUniqueID();
|
||||||
const player = setupInitialStore({ id });
|
const player = setupInitialStore({ id });
|
||||||
|
@ -91,8 +103,6 @@ export const loadingSave = ref(false);
|
||||||
export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
||||||
console.info("Loading save", playerObj);
|
console.info("Loading save", playerObj);
|
||||||
loadingSave.value = true;
|
loadingSave.value = true;
|
||||||
const { layers, removeLayer, addLayer } = await import("game/layers");
|
|
||||||
const { fixOldSave, getInitialLayers } = await import("data/projEntry");
|
|
||||||
|
|
||||||
for (const layer in layers) {
|
for (const layer in layers) {
|
||||||
const l = layers[layer];
|
const l = layers[layer];
|
||||||
|
@ -109,7 +119,7 @@ export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
||||||
playerObj.time &&
|
playerObj.time &&
|
||||||
playerObj.devSpeed !== 0
|
playerObj.devSpeed !== 0
|
||||||
) {
|
) {
|
||||||
if (playerObj.offlineTime == undefined) playerObj.offlineTime = 0;
|
if (playerObj.offlineTime == null) playerObj.offlineTime = 0;
|
||||||
playerObj.offlineTime += Math.min(
|
playerObj.offlineTime += Math.min(
|
||||||
playerObj.offlineTime + (Date.now() - playerObj.time) / 1000,
|
playerObj.offlineTime + (Date.now() - playerObj.time) / 1000,
|
||||||
projInfo.offlineLimit * 3600
|
projInfo.offlineLimit * 3600
|
||||||
|
@ -127,6 +137,40 @@ export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
||||||
globalBus.emit("onLoad");
|
globalBus.emit("onLoad");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
|
||||||
|
export function getCachedSave(id: string) {
|
||||||
|
if (cachedSaves[id] == null) {
|
||||||
|
let save = localStorage.getItem(id);
|
||||||
|
if (save == null) {
|
||||||
|
cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
|
||||||
|
} else if (save === "dW5kZWZpbmVk") {
|
||||||
|
cachedSaves[id] = { error: `Save is undefined`, id };
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
save = decodeSave(save);
|
||||||
|
if (save == null) {
|
||||||
|
console.warn("Unable to determine preset encoding", save);
|
||||||
|
cachedSaves[id] = { error: "Unable to determine preset encoding", id };
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return cachedSaves[id]!;
|
||||||
|
}
|
||||||
|
cachedSaves[id] = { ...JSON.parse(save), id };
|
||||||
|
} catch (error) {
|
||||||
|
cachedSaves[id] = { error, id };
|
||||||
|
console.warn(`Failed to load info about save with id ${id}:\n${error}\n${save}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return cachedSaves[id]!;
|
||||||
|
}
|
||||||
|
export function clearCachedSaves() {
|
||||||
|
Object.keys(cachedSaves).forEach(key => delete cachedSaves[key]);
|
||||||
|
}
|
||||||
|
export function clearCachedSave(id: string) {
|
||||||
|
cachedSaves[id] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (player.autosave) {
|
if (player.autosave) {
|
||||||
save();
|
save();
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
|
/* eslint-disable vue/multi-word-component-names */
|
||||||
|
// ^ I have no idea why that's necessary; the rule is disabled, and this file isn't a vue component?
|
||||||
|
// I'm _guessing_ it's related to us using DefineComponent, but I figured that eslint rule should
|
||||||
|
// only apply to SFCs
|
||||||
import Col from "components/layout/Column.vue";
|
import Col from "components/layout/Column.vue";
|
||||||
import Row from "components/layout/Row.vue";
|
import Row from "components/layout/Row.vue";
|
||||||
import type { CoercableComponent, GenericComponent, JSXFunction } from "features/feature";
|
import type { CoercableComponent, GenericComponent, JSXFunction } from "features/feature";
|
||||||
import {
|
import {
|
||||||
Component as ComponentKey,
|
Component as ComponentKey,
|
||||||
GatherProps,
|
GatherProps,
|
||||||
|
Visibility,
|
||||||
isVisible,
|
isVisible,
|
||||||
jsx,
|
jsx
|
||||||
Visibility
|
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import type { ProcessedComputable } from "util/computed";
|
import type { ProcessedComputable } from "util/computed";
|
||||||
import { DoNotCache } from "util/computed";
|
import { DoNotCache } from "util/computed";
|
||||||
import type { Component, ComputedRef, DefineComponent, PropType, Ref, ShallowRef } from "vue";
|
import type { Component, DefineComponent, Ref, ShallowRef, UnwrapRef } from "vue";
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
|
@ -21,6 +25,7 @@ import {
|
||||||
unref,
|
unref,
|
||||||
watchEffect
|
watchEffect
|
||||||
} from "vue";
|
} from "vue";
|
||||||
|
import { JSX } from "vue/jsx-runtime";
|
||||||
import { camelToKebab } from "./common";
|
import { camelToKebab } from "./common";
|
||||||
|
|
||||||
export function coerceComponent(
|
export function coerceComponent(
|
||||||
|
@ -125,17 +130,17 @@ export function setupHoldToClick(
|
||||||
stop: VoidFunction;
|
stop: VoidFunction;
|
||||||
handleHolding: VoidFunction;
|
handleHolding: VoidFunction;
|
||||||
} {
|
} {
|
||||||
const interval = ref<NodeJS.Timer | null>(null);
|
const interval = ref<NodeJS.Timeout | null>(null);
|
||||||
const event = ref<MouseEvent | TouchEvent | undefined>(undefined);
|
const event = ref<MouseEvent | TouchEvent | undefined>(undefined);
|
||||||
|
|
||||||
function start(e: MouseEvent | TouchEvent) {
|
function start(e: MouseEvent | TouchEvent) {
|
||||||
if (!interval.value) {
|
if (interval.value == null) {
|
||||||
interval.value = setInterval(handleHolding, 250);
|
interval.value = setInterval(handleHolding, 250);
|
||||||
}
|
}
|
||||||
event.value = e;
|
event.value = e;
|
||||||
}
|
}
|
||||||
function stop() {
|
function stop() {
|
||||||
if (interval.value) {
|
if (interval.value != null) {
|
||||||
clearInterval(interval.value);
|
clearInterval(interval.value);
|
||||||
interval.value = null;
|
interval.value = null;
|
||||||
}
|
}
|
||||||
|
@ -174,36 +179,38 @@ export function getFirstFeature<
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeComponent(
|
export function computeComponent(
|
||||||
component: Ref<ProcessedComputable<CoercableComponent>>,
|
component: Ref<CoercableComponent>,
|
||||||
defaultWrapper = "div"
|
defaultWrapper = "div"
|
||||||
): ShallowRef<Component | ""> {
|
): ShallowRef<Component | ""> {
|
||||||
const comp = shallowRef<Component | "">();
|
const comp = shallowRef<Component | "">();
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
comp.value = coerceComponent(unwrapRef(component), defaultWrapper);
|
comp.value = coerceComponent(unref(component), defaultWrapper);
|
||||||
});
|
});
|
||||||
return comp as ShallowRef<Component | "">;
|
return comp as ShallowRef<Component | "">;
|
||||||
}
|
}
|
||||||
export function computeOptionalComponent(
|
export function computeOptionalComponent(
|
||||||
component: Ref<ProcessedComputable<CoercableComponent | undefined> | undefined>,
|
component: Ref<CoercableComponent | undefined>,
|
||||||
defaultWrapper = "div"
|
defaultWrapper = "div"
|
||||||
): ShallowRef<Component | "" | null> {
|
): ShallowRef<Component | "" | null> {
|
||||||
const comp = shallowRef<Component | "" | null>(null);
|
const comp = shallowRef<Component | "" | null>(null);
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const currComponent = unwrapRef(component);
|
const currComponent = unref(component);
|
||||||
comp.value =
|
comp.value =
|
||||||
currComponent == "" || currComponent == null
|
currComponent === "" || currComponent == null
|
||||||
? null
|
? null
|
||||||
: coerceComponent(currComponent, defaultWrapper);
|
: coerceComponent(currComponent, defaultWrapper);
|
||||||
});
|
});
|
||||||
return comp;
|
return comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapRef<T>(ref: Ref<ProcessedComputable<T>>): ComputedRef<T> {
|
export function deepUnref<T extends object>(refObject: T): { [K in keyof T]: UnwrapRef<T[K]> } {
|
||||||
return computed(() => unwrapRef(ref));
|
return (Object.keys(refObject) as (keyof T)[]).reduce(
|
||||||
}
|
(acc, curr) => {
|
||||||
|
acc[curr] = unref(refObject[curr]) as UnwrapRef<T[keyof T]>;
|
||||||
export function unwrapRef<T>(ref: Ref<ProcessedComputable<T>>): T {
|
return acc;
|
||||||
return unref<T>(unref(ref));
|
},
|
||||||
|
{} as { [K in keyof T]: UnwrapRef<T[K]> }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setRefValue<T>(ref: Ref<T | Ref<T>>, value: T) {
|
export function setRefValue<T>(ref: Ref<T | Ref<T>>, value: T) {
|
||||||
|
@ -221,14 +228,6 @@ export type PropTypes =
|
||||||
| typeof Function
|
| typeof Function
|
||||||
| typeof Object
|
| typeof Object
|
||||||
| typeof Array;
|
| typeof Array;
|
||||||
// TODO Unfortunately, the typescript engine gives up on typing completely when you use this method,
|
|
||||||
// Even though it has the same typing as when doing it manually
|
|
||||||
export function processedPropType<T>(...types: PropTypes[]): PropType<ProcessedComputable<T>> {
|
|
||||||
if (!types.includes(Object)) {
|
|
||||||
types.push(Object);
|
|
||||||
}
|
|
||||||
return types as PropType<ProcessedComputable<T>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function trackHover(element: VueFeature): Ref<boolean> {
|
export function trackHover(element: VueFeature): Ref<boolean> {
|
||||||
const isHovered = ref(false);
|
const isHovered = ref(false);
|
||||||
|
@ -244,8 +243,11 @@ export function trackHover(element: VueFeature): Ref<boolean> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function kebabifyObject(object: Record<string, unknown>) {
|
export function kebabifyObject(object: Record<string, unknown>) {
|
||||||
return Object.keys(object).reduce((acc, curr) => {
|
return Object.keys(object).reduce(
|
||||||
acc[camelToKebab(curr)] = object[curr];
|
(acc, curr) => {
|
||||||
return acc;
|
acc[camelToKebab(curr)] = object[curr];
|
||||||
}, {} as Record<string, unknown>);
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, unknown>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
100
tests/features/hotkey.test.ts
Normal file
100
tests/features/hotkey.test.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { createHotkey, hotkeys } from "features/hotkey";
|
||||||
|
import { afterEach, describe, expect, onTestFailed, test } from "vitest";
|
||||||
|
import { Ref, ref } from "vue";
|
||||||
|
import "../utils";
|
||||||
|
|
||||||
|
function createSuccessHotkey(key: string, triggered: Ref<boolean>) {
|
||||||
|
hotkeys[key] = createHotkey(() => ({
|
||||||
|
description: "",
|
||||||
|
key: key,
|
||||||
|
onPress: () => (triggered.value = true)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFailHotkey(key: string) {
|
||||||
|
hotkeys[key] = createHotkey(() => ({
|
||||||
|
description: "Fail test",
|
||||||
|
key,
|
||||||
|
onPress: () => expect(true).toBe(false)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockKeypress(key: string, shiftKey = false, ctrlKey = false) {
|
||||||
|
const event = new KeyboardEvent("keydown", { key, shiftKey, ctrlKey });
|
||||||
|
expect(document.dispatchEvent(event)).toBe(true);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testHotkey(pass: string, fail: string, key: string, shiftKey = false, ctrlKey = false) {
|
||||||
|
const triggered = ref(false);
|
||||||
|
createSuccessHotkey(pass, triggered);
|
||||||
|
createFailHotkey(fail);
|
||||||
|
mockKeypress(key, shiftKey, ctrlKey);
|
||||||
|
expect(triggered.value).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Hotkeys fire correctly", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
Object.keys(hotkeys).forEach(key => delete hotkeys[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Lower case letters", () => testHotkey("a", "A", "a"));
|
||||||
|
|
||||||
|
test.each([["A"], ["shift+a"], ["shift+A"]])("Upper case letters using %s as key", key => {
|
||||||
|
testHotkey(key, "a", "A", true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
[0, ")"],
|
||||||
|
[1, "!"],
|
||||||
|
[2, "@"],
|
||||||
|
[3, "#"],
|
||||||
|
[4, "$"],
|
||||||
|
[5, "%"],
|
||||||
|
[6, "^"],
|
||||||
|
[7, "&"],
|
||||||
|
[8, "*"],
|
||||||
|
[9, "("]
|
||||||
|
])("Handle number %i and it's 'capital', %s", (number, symbol) => {
|
||||||
|
test("Triggering number", () =>
|
||||||
|
testHotkey(number.toString(), symbol, number.toString(), true));
|
||||||
|
test.each([symbol, `shift+${number}`, `shift+${symbol}`])(
|
||||||
|
"Triggering symbol using %s as key",
|
||||||
|
key => testHotkey(key, number.toString(), symbol, true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Ctrl modifier", () => testHotkey("ctrl+a", "a", "a", false, true));
|
||||||
|
|
||||||
|
test.each(["shift+ctrl+a", "ctrl+shift+a", "shift+ctrl+A", "ctrl+shift+A"])(
|
||||||
|
"Shift and Ctrl modifiers using %s as key",
|
||||||
|
key => {
|
||||||
|
const triggered = ref(false);
|
||||||
|
createSuccessHotkey(key, triggered);
|
||||||
|
createFailHotkey("a");
|
||||||
|
createFailHotkey("A");
|
||||||
|
createFailHotkey("shift+A");
|
||||||
|
createFailHotkey("shift+a");
|
||||||
|
createFailHotkey("ctrl+a");
|
||||||
|
createFailHotkey("ctrl+A");
|
||||||
|
mockKeypress("a", true, true);
|
||||||
|
expect(triggered.value).toBe(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test.each(["shift+ctrl+1", "ctrl+shift+1", "shift+ctrl+!", "ctrl+shift+!"])(
|
||||||
|
"Shift and Ctrl modifiers using %s as key",
|
||||||
|
key => {
|
||||||
|
const triggered = ref(false);
|
||||||
|
createSuccessHotkey(key, triggered);
|
||||||
|
createFailHotkey("1");
|
||||||
|
createFailHotkey("!");
|
||||||
|
createFailHotkey("shift+1");
|
||||||
|
createFailHotkey("shift+!");
|
||||||
|
createFailHotkey("ctrl+1");
|
||||||
|
createFailHotkey("ctrl+!");
|
||||||
|
mockKeypress("!", true, true);
|
||||||
|
expect(triggered.value).toBe(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
File diff suppressed because it is too large
Load diff
|
@ -155,7 +155,7 @@ describe("Formula Equality Checking", () => {
|
||||||
describe("Formula aliases", () => {
|
describe("Formula aliases", () => {
|
||||||
function testAliases<T extends FormulaFunctions>(
|
function testAliases<T extends FormulaFunctions>(
|
||||||
aliases: T[],
|
aliases: T[],
|
||||||
args: Parameters<typeof Formula[T]>
|
args: Parameters<(typeof Formula)[T]>
|
||||||
) {
|
) {
|
||||||
describe(aliases[0], () => {
|
describe(aliases[0], () => {
|
||||||
let formula: GenericFormula;
|
let formula: GenericFormula;
|
||||||
|
@ -250,7 +250,7 @@ describe("Creating Formulas", () => {
|
||||||
|
|
||||||
function checkFormula<T extends FormulaFunctions>(
|
function checkFormula<T extends FormulaFunctions>(
|
||||||
functionName: T,
|
functionName: T,
|
||||||
args: Readonly<Parameters<typeof Formula[T]>>
|
args: Readonly<Parameters<(typeof Formula)[T]>>
|
||||||
) {
|
) {
|
||||||
let formula: GenericFormula;
|
let formula: GenericFormula;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
@ -274,7 +274,7 @@ describe("Creating Formulas", () => {
|
||||||
// It's a lot of tests, but I'd rather be exhaustive
|
// It's a lot of tests, but I'd rather be exhaustive
|
||||||
function testFormulaCall<T extends FormulaFunctions>(
|
function testFormulaCall<T extends FormulaFunctions>(
|
||||||
functionName: T,
|
functionName: T,
|
||||||
args: Readonly<Parameters<typeof Formula[T]>>
|
args: Readonly<Parameters<(typeof Formula)[T]>>
|
||||||
) {
|
) {
|
||||||
if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) {
|
if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) {
|
||||||
// These cases in particular take a long time, so skip them
|
// These cases in particular take a long time, so skip them
|
||||||
|
@ -1275,3 +1275,16 @@ describe("Buy Max", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Stringifies", () => {
|
||||||
|
test("Nested formula", () => {
|
||||||
|
const variable = Formula.variable(ref(0));
|
||||||
|
expect(variable.add(5).pow(Formula.constant(10)).stringify()).toBe(
|
||||||
|
"pow(add(x, 5.00), 10.00)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("Indeterminate", () => {
|
||||||
|
expect(Formula.if(10, true, f => f.add(5)).stringify()).toBe("indeterminate");
|
||||||
|
expect(Formula.step(10, 5, f => f.add(5)).stringify()).toBe("indeterminate");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CoercableComponent, JSXFunction } from "features/feature";
|
import { CoercableComponent, JSXFunction } from "features/feature";
|
||||||
import Formula, { printFormula } from "game/formulas/formulas";
|
import Formula from "game/formulas/formulas";
|
||||||
import {
|
import {
|
||||||
createAdditiveModifier,
|
createAdditiveModifier,
|
||||||
createExponentialModifier,
|
createExponentialModifier,
|
||||||
|
@ -52,7 +52,7 @@ function testModifiers<
|
||||||
expect(modifier.invert(operation(10, 5))).compare_tolerance(10));
|
expect(modifier.invert(operation(10, 5))).compare_tolerance(10));
|
||||||
test("getFormula returns the right formula", () => {
|
test("getFormula returns the right formula", () => {
|
||||||
const value = ref(10);
|
const value = ref(10);
|
||||||
expect(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
expect(modifier.getFormula(Formula.variable(value)).stringify()).toBe(
|
||||||
`${operation.name}(x, 5.00)`
|
`${operation.name}(x, 5.00)`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -156,7 +156,7 @@ describe("Sequential Modifiers", () => {
|
||||||
expect(modifier.invert(Decimal.add(10, 5).times(5).pow(5))).compare_tolerance(10));
|
expect(modifier.invert(Decimal.add(10, 5).times(5).pow(5))).compare_tolerance(10));
|
||||||
test("getFormula returns the right formula", () => {
|
test("getFormula returns the right formula", () => {
|
||||||
const value = ref(10);
|
const value = ref(10);
|
||||||
expect(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
expect(modifier.getFormula(Formula.variable(value)).stringify()).toBe(
|
||||||
`pow(mul(add(x, 5.00), 5.00), 5.00)`
|
`pow(mul(add(x, 5.00), 5.00), 5.00)`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,14 +6,11 @@ interface CustomMatchers<R = unknown> {
|
||||||
toLogError(): R;
|
toLogError(): R;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare module "vitest" {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
namespace Vi {
|
interface Assertion extends CustomMatchers {}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
interface Assertion extends CustomMatchers {}
|
interface AsymmetricMatchersContaining extends CustomMatchers {}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
interface AsymmetricMatchersContaining extends CustomMatchers {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"checkJs": false,
|
"checkJs": false,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "vue",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default defineConfig({
|
||||||
}),
|
}),
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
includeAssets: ["Logo.svg", "favicon.ico", "robots.txt", "apple-touch-icon.png"],
|
registerType: 'autoUpdate',
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue