Update dependencies #83
79 changed files with 5286 additions and 7038 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",
|
|
@ -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,13 @@ 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
|
||||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -12,10 +12,10 @@ 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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
7153
package-lock.json
generated
7153
package-lock.json
generated
File diff suppressed because it is too large
Load diff
67
package.json
67
package.json
|
@ -2,6 +2,7 @@
|
||||||
"name": "profectus",
|
"name": "profectus",
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -14,47 +15,51 @@
|
||||||
"lint:fix": "eslint --fix --max-warnings 0 src"
|
"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",
|
||||||
"unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.1",
|
"unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.1",
|
||||||
"vite": "^2.9.12",
|
"vite": "^5.1.8",
|
||||||
"vite-plugin-pwa": "^0.12.0",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vite-tsconfig-paths": "^3.5.0",
|
"vite-tsconfig-paths": "^4.3.0",
|
||||||
"vue": "^3.2.26",
|
"vue": "^3.5.12",
|
||||||
"vue-next-select": "^2.10.2",
|
"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": "^1.3.1",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
playing incremental games is taking priority over other things in your life, or
|
playing incremental games is taking priority over other things in your life, or
|
||||||
manipulating your sleep schedule, it may be prudent to seek help.
|
manipulating your sleep schedule, it may be prudent to seek help.
|
||||||
</p>
|
</p>
|
||||||
|
<h4>Resources:</h4>
|
||||||
<p>
|
<p>
|
||||||
<h4>Resources:</h4>
|
|
||||||
<span>
|
<span>
|
||||||
<a style="display: inline" href="https://www.samhsa.gov/" target="_blank">
|
<a style="display: inline" href="https://www.samhsa.gov/" target="_blank">
|
||||||
SAMHSA
|
SAMHSA
|
||||||
|
|
|
@ -67,13 +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";
|
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>
|
||||||
|
|
||||||
|
|
|
@ -41,22 +41,22 @@
|
||||||
|
|
||||||
<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;
|
preventClosing?: boolean;
|
||||||
width?: string;
|
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() {
|
||||||
if (unref(props.preventClosing) !== true) {
|
if (props.preventClosing !== true) {
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,18 +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, unref, watch } from "vue";
|
import { galaxy, syncedSaves } from "util/galaxy";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
import DangerButton from "../fields/DangerButton.vue";
|
import DangerButton from "../fields/DangerButton.vue";
|
||||||
import FeedbackButton from "../fields/FeedbackButton.vue";
|
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||||
import Text from "../fields/Text.vue";
|
import Text from "../fields/Text.vue";
|
||||||
import type { LoadablePlayerData } from "./SavesManager.vue";
|
import type { LoadablePlayerData } from "./SavesManager.vue";
|
||||||
import { galaxy, syncedSaves } from "util/galaxy";
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
const props = defineProps<{
|
||||||
save: LoadablePlayerData;
|
save: LoadablePlayerData;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const { save, readonly } = toRefs(_props);
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "export"): void;
|
(e: "export"): void;
|
||||||
(e: "open"): void;
|
(e: "open"): void;
|
||||||
|
@ -111,19 +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(
|
const isActive = computed(
|
||||||
() => save.value != null && save.value.id === player.id && !unref(readonly)
|
() => 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(
|
const synced = computed(
|
||||||
() =>
|
() =>
|
||||||
!unref(readonly) &&
|
!props.readonly &&
|
||||||
galaxy.value?.loggedIn === true &&
|
galaxy.value?.loggedIn === true &&
|
||||||
syncedSaves.value.includes(save.value.id)
|
syncedSaves.value.includes(props.save.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
function changeName() {
|
function changeName() {
|
||||||
|
|
|
@ -130,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({
|
||||||
|
@ -311,4 +311,4 @@ function editSave(id: string, newName: string) {
|
||||||
.presets .vue-select[aria-expanded="true"] vue-dropdown {
|
.presets .vue-select[aria-expanded="true"] vue-dropdown {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,3 +7,12 @@
|
||||||
.modifier-toggle.collapsed {
|
.modifier-toggle.collapsed {
|
||||||
transform: translate(-5px, -5px) rotate(-90deg);
|
transform: translate(-5px, -5px) rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-text {
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: middle;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 200%;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: drop-shadow(3px 3px 2px var(--tooltip-background));
|
||||||
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@ import type {
|
||||||
import { convertComputable, processComputable } from "util/computed";
|
import { convertComputable, processComputable } from "util/computed";
|
||||||
import { getFirstFeature, renderColJSX, renderJSX } from "util/vue";
|
import { getFirstFeature, renderColJSX, renderJSX } from "util/vue";
|
||||||
import type { ComputedRef, Ref } from "vue";
|
import type { ComputedRef, Ref } from "vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, ref, unref } from "vue";
|
||||||
|
import { JSX } from "vue/jsx-runtime";
|
||||||
import "./common.css";
|
import "./common.css";
|
||||||
|
|
||||||
/** An object that configures a {@link ResetButton} */
|
/** An object that configures a {@link ResetButton} */
|
||||||
|
@ -128,7 +129,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:"}{" "}
|
||||||
|
@ -505,3 +506,21 @@ export function isRendered(layer: BaseLayer, idOrFeature: string | { id: string
|
||||||
const id = typeof idOrFeature === "string" ? idOrFeature : idOrFeature.id;
|
const id = typeof idOrFeature === "string" ? idOrFeature : idOrFeature.id;
|
||||||
return computed(() => id in layer.nodes.value);
|
return computed(() => id in layer.nodes.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for setting up a system where one of many things can be selected.
|
||||||
|
* It's recommended to use an ID or index rather than the object itself, so that you can wrap the ref in a persistent without breaking anything.
|
||||||
|
* @returns The ref containing the selection, as well as a select and deselect function
|
||||||
|
*/
|
||||||
|
export function setupSelectable<T>() {
|
||||||
|
const selected = ref<T>();
|
||||||
|
return {
|
||||||
|
select: function (node: T) {
|
||||||
|
selected.value = node;
|
||||||
|
},
|
||||||
|
deselect: function () {
|
||||||
|
selected.value = undefined;
|
||||||
|
},
|
||||||
|
selected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -1,294 +0,0 @@
|
||||||
<template>
|
|
||||||
<panZoom
|
|
||||||
v-if="isVisible(visibility)"
|
|
||||||
:style="[
|
|
||||||
{
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
},
|
|
||||||
style
|
|
||||||
]"
|
|
||||||
:class="classes"
|
|
||||||
selector=".g1"
|
|
||||||
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
|
|
||||||
ref="stage"
|
|
||||||
@init="onInit"
|
|
||||||
@mousemove="drag"
|
|
||||||
@touchmove="drag"
|
|
||||||
@mousedown="(e: MouseEvent) => mouseDown(e)"
|
|
||||||
@touchstart="(e: TouchEvent) => mouseDown(e)"
|
|
||||||
@mouseup="() => endDragging(unref(draggingNode))"
|
|
||||||
@touchend.passive="() => endDragging(unref(draggingNode))"
|
|
||||||
@mouseleave="() => endDragging(unref(draggingNode), true)"
|
|
||||||
>
|
|
||||||
<svg class="stage" width="100%" height="100%">
|
|
||||||
<g class="g1">
|
|
||||||
<transition-group name="link" appear>
|
|
||||||
<g
|
|
||||||
v-for="link in unref(links) || []"
|
|
||||||
:key="`${link.startNode.id}-${link.endNode.id}`"
|
|
||||||
>
|
|
||||||
<BoardLinkVue
|
|
||||||
:link="link"
|
|
||||||
:dragging="unref(draggingNode)"
|
|
||||||
:dragged="
|
|
||||||
link.startNode === unref(draggingNode) ||
|
|
||||||
link.endNode === unref(draggingNode)
|
|
||||||
? dragged
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</transition-group>
|
|
||||||
<transition-group name="grow" :duration="500" appear>
|
|
||||||
<g v-for="node in sortedNodes" :key="node.id" style="transition-duration: 0s">
|
|
||||||
<BoardNodeVue
|
|
||||||
:node="node"
|
|
||||||
:nodeType="types[node.type]"
|
|
||||||
:dragging="unref(draggingNode)"
|
|
||||||
:dragged="unref(draggingNode) === node ? dragged : undefined"
|
|
||||||
:hasDragged="unref(draggingNode) == null ? false : hasDragged"
|
|
||||||
:receivingNode="unref(receivingNode) === node"
|
|
||||||
:isSelected="unref(selectedNode) === node"
|
|
||||||
:selectedAction="
|
|
||||||
unref(selectedNode) === node ? unref(selectedAction) : null
|
|
||||||
"
|
|
||||||
@mouseDown="mouseDown"
|
|
||||||
@endDragging="endDragging"
|
|
||||||
@clickAction="(actionId: string) => clickAction(node, actionId)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</transition-group>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</panZoom>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {
|
|
||||||
BoardData,
|
|
||||||
BoardNode,
|
|
||||||
BoardNodeLink,
|
|
||||||
GenericBoardNodeAction,
|
|
||||||
GenericNodeType
|
|
||||||
} from "features/boards/board";
|
|
||||||
import { getNodeProperty } from "features/boards/board";
|
|
||||||
import type { StyleValue } from "features/feature";
|
|
||||||
import { Visibility, isVisible } from "features/feature";
|
|
||||||
import type { ProcessedComputable } from "util/computed";
|
|
||||||
import { Ref, computed, ref, toRefs, unref, watchEffect } from "vue";
|
|
||||||
import BoardLinkVue from "./BoardLink.vue";
|
|
||||||
import BoardNodeVue from "./BoardNode.vue";
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
|
||||||
nodes: Ref<BoardNode[]>;
|
|
||||||
types: Record<string, GenericNodeType>;
|
|
||||||
state: Ref<BoardData>;
|
|
||||||
visibility: ProcessedComputable<Visibility | boolean>;
|
|
||||||
width?: ProcessedComputable<string>;
|
|
||||||
height?: ProcessedComputable<string>;
|
|
||||||
style?: ProcessedComputable<StyleValue>;
|
|
||||||
classes?: ProcessedComputable<Record<string, boolean>>;
|
|
||||||
links: Ref<BoardNodeLink[] | null>;
|
|
||||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
|
||||||
selectedNode: Ref<BoardNode | null>;
|
|
||||||
draggingNode: Ref<BoardNode | null>;
|
|
||||||
receivingNode: Ref<BoardNode | null>;
|
|
||||||
mousePosition: Ref<{ x: number; y: number } | null>;
|
|
||||||
setReceivingNode: (node: BoardNode | null) => void;
|
|
||||||
setDraggingNode: (node: BoardNode | null) => void;
|
|
||||||
}>();
|
|
||||||
const props = toRefs(_props);
|
|
||||||
|
|
||||||
const lastMousePosition = ref({ x: 0, y: 0 });
|
|
||||||
const dragged = ref({ x: 0, y: 0 });
|
|
||||||
const hasDragged = ref(false);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const stage = ref<any>(null);
|
|
||||||
|
|
||||||
const sortedNodes = computed(() => {
|
|
||||||
const nodes = props.nodes.value.slice();
|
|
||||||
if (props.selectedNode.value) {
|
|
||||||
const node = nodes.splice(nodes.indexOf(props.selectedNode.value), 1)[0];
|
|
||||||
nodes.push(node);
|
|
||||||
}
|
|
||||||
if (props.draggingNode.value) {
|
|
||||||
const node = nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0];
|
|
||||||
nodes.push(node);
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
});
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const node = props.draggingNode.value;
|
|
||||||
if (node == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = {
|
|
||||||
x: node.position.x + dragged.value.x,
|
|
||||||
y: node.position.y + dragged.value.y
|
|
||||||
};
|
|
||||||
let smallestDistance = Number.MAX_VALUE;
|
|
||||||
|
|
||||||
props.setReceivingNode.value(
|
|
||||||
props.nodes.value.reduce((smallest: BoardNode | null, curr: BoardNode) => {
|
|
||||||
if (curr.id === node.id) {
|
|
||||||
return smallest;
|
|
||||||
}
|
|
||||||
const nodeType = props.types.value[curr.type];
|
|
||||||
const canAccept = getNodeProperty(nodeType.canAccept, curr, node);
|
|
||||||
if (!canAccept) {
|
|
||||||
return smallest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const distanceSquared =
|
|
||||||
Math.pow(position.x - curr.position.x, 2) +
|
|
||||||
Math.pow(position.y - curr.position.y, 2);
|
|
||||||
let size = getNodeProperty(nodeType.size, curr);
|
|
||||||
if (distanceSquared > smallestDistance || distanceSquared > size * size) {
|
|
||||||
return smallest;
|
|
||||||
}
|
|
||||||
|
|
||||||
smallestDistance = distanceSquared;
|
|
||||||
return curr;
|
|
||||||
}, null)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function onInit(panzoomInstance: any) {
|
|
||||||
panzoomInstance.setTransformOrigin(null);
|
|
||||||
panzoomInstance.moveTo(stage.value.$el.clientWidth / 2, stage.value.$el.clientHeight / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouseDown(e: MouseEvent | TouchEvent, node: BoardNode | null = null, draggable = false) {
|
|
||||||
if (props.draggingNode.value == null) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
let clientX, clientY;
|
|
||||||
if ("touches" in e) {
|
|
||||||
if (e.touches.length === 1) {
|
|
||||||
clientX = e.touches[0].clientX;
|
|
||||||
clientY = e.touches[0].clientY;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
clientX = e.clientX;
|
|
||||||
clientY = e.clientY;
|
|
||||||
}
|
|
||||||
lastMousePosition.value = {
|
|
||||||
x: clientX,
|
|
||||||
y: clientY
|
|
||||||
};
|
|
||||||
dragged.value = { x: 0, y: 0 };
|
|
||||||
hasDragged.value = false;
|
|
||||||
|
|
||||||
if (draggable) {
|
|
||||||
props.setDraggingNode.value(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node != null) {
|
|
||||||
props.state.value.selectedNode = null;
|
|
||||||
props.state.value.selectedAction = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drag(e: MouseEvent | TouchEvent) {
|
|
||||||
const { x, y, scale } = stage.value.panZoomInstance.getTransform();
|
|
||||||
|
|
||||||
let clientX, clientY;
|
|
||||||
if ("touches" in e) {
|
|
||||||
if (e.touches.length === 1) {
|
|
||||||
clientX = e.touches[0].clientX;
|
|
||||||
clientY = e.touches[0].clientY;
|
|
||||||
} else {
|
|
||||||
endDragging(props.draggingNode.value);
|
|
||||||
props.mousePosition.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
clientX = e.clientX;
|
|
||||||
clientY = e.clientY;
|
|
||||||
}
|
|
||||||
|
|
||||||
props.mousePosition.value = {
|
|
||||||
x: (clientX - x) / scale,
|
|
||||||
y: (clientY - y) / scale
|
|
||||||
};
|
|
||||||
|
|
||||||
dragged.value = {
|
|
||||||
x: dragged.value.x + (clientX - lastMousePosition.value.x) / scale,
|
|
||||||
y: dragged.value.y + (clientY - lastMousePosition.value.y) / scale
|
|
||||||
};
|
|
||||||
lastMousePosition.value = {
|
|
||||||
x: clientX,
|
|
||||||
y: clientY
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Math.abs(dragged.value.x) > 10 || Math.abs(dragged.value.y) > 10) {
|
|
||||||
hasDragged.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.draggingNode.value != null) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function endDragging(node: BoardNode | null, mouseLeave = false) {
|
|
||||||
if (props.draggingNode.value != null && props.draggingNode.value === node) {
|
|
||||||
if (props.receivingNode.value == null) {
|
|
||||||
props.draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25;
|
|
||||||
props.draggingNode.value.position.y += Math.round(dragged.value.y / 25) * 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = props.nodes.value;
|
|
||||||
nodes.push(nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0]);
|
|
||||||
|
|
||||||
if (props.receivingNode.value) {
|
|
||||||
props.types.value[props.receivingNode.value.type].onDrop?.(
|
|
||||||
props.receivingNode.value,
|
|
||||||
props.draggingNode.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
props.setDraggingNode.value(null);
|
|
||||||
} else if (!hasDragged.value && !mouseLeave) {
|
|
||||||
props.state.value.selectedNode = null;
|
|
||||||
props.state.value.selectedAction = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickAction(node: BoardNode, actionId: string) {
|
|
||||||
if (props.state.value.selectedAction === actionId) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
unref(props.selectedAction)!.onClick(unref(props.selectedNode)!);
|
|
||||||
} else {
|
|
||||||
props.state.value = { ...props.state.value, selectedAction: actionId };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.vue-pan-zoom-scene {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-pan-zoom-scene:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g1 {
|
|
||||||
transition-duration: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-enter-from,
|
|
||||||
.link-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,80 +0,0 @@
|
||||||
<template>
|
|
||||||
<line
|
|
||||||
class="link"
|
|
||||||
v-bind="linkProps"
|
|
||||||
:class="{ pulsing: link.pulsing }"
|
|
||||||
:x1="startPosition.x"
|
|
||||||
:y1="startPosition.y"
|
|
||||||
:x2="endPosition.x"
|
|
||||||
:y2="endPosition.y"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { BoardNode, BoardNodeLink } from "features/boards/board";
|
|
||||||
import { kebabifyObject } from "util/vue";
|
|
||||||
import { computed, toRefs, unref } from "vue";
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
|
||||||
link: BoardNodeLink;
|
|
||||||
dragging: BoardNode | null;
|
|
||||||
dragged?: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
}>();
|
|
||||||
const props = toRefs(_props);
|
|
||||||
|
|
||||||
const startPosition = computed(() => {
|
|
||||||
const position = { ...props.link.value.startNode.position };
|
|
||||||
if (props.link.value.offsetStart) {
|
|
||||||
position.x += unref(props.link.value.offsetStart).x;
|
|
||||||
position.y += unref(props.link.value.offsetStart).y;
|
|
||||||
}
|
|
||||||
if (props.dragging?.value === props.link.value.startNode) {
|
|
||||||
position.x += props.dragged?.value?.x ?? 0;
|
|
||||||
position.y += props.dragged?.value?.y ?? 0;
|
|
||||||
}
|
|
||||||
return position;
|
|
||||||
});
|
|
||||||
|
|
||||||
const endPosition = computed(() => {
|
|
||||||
const position = { ...props.link.value.endNode.position };
|
|
||||||
if (props.link.value.offsetEnd) {
|
|
||||||
position.x += unref(props.link.value.offsetEnd).x;
|
|
||||||
position.y += unref(props.link.value.offsetEnd).y;
|
|
||||||
}
|
|
||||||
if (props.dragging?.value === props.link.value.endNode) {
|
|
||||||
position.x += props.dragged?.value?.x ?? 0;
|
|
||||||
position.y += props.dragged?.value?.y ?? 0;
|
|
||||||
}
|
|
||||||
return position;
|
|
||||||
});
|
|
||||||
|
|
||||||
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.link {
|
|
||||||
transition-duration: 0s;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link.pulsing {
|
|
||||||
animation: pulsing 2s ease-in infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulsing {
|
|
||||||
0% {
|
|
||||||
opacity: 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0.25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,339 +0,0 @@
|
||||||
<template>
|
|
||||||
<!-- Ugly casting to prevent TS compiler error about style because vue doesn't think it supports arrays when it does -->
|
|
||||||
<g
|
|
||||||
class="boardnode"
|
|
||||||
:class="{ [node.type]: true, isSelected, isDraggable, ...classes }"
|
|
||||||
:style="[{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }, style ?? []] as unknown as (string | CSSProperties)"
|
|
||||||
:transform="`translate(${position.x},${position.y})${isSelected ? ' scale(1.2)' : ''}`"
|
|
||||||
>
|
|
||||||
<BoardNodeAction
|
|
||||||
:actions="actions ?? []"
|
|
||||||
:is-selected="isSelected"
|
|
||||||
:node="node"
|
|
||||||
:node-type="nodeType"
|
|
||||||
:selected-action="selectedAction"
|
|
||||||
@click-action="(actionId: string) => emit('clickAction', actionId)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<g
|
|
||||||
class="node-container"
|
|
||||||
@mousedown="mouseDown"
|
|
||||||
@touchstart.passive="mouseDown"
|
|
||||||
@mouseup="mouseUp"
|
|
||||||
@touchend.passive="mouseUp"
|
|
||||||
>
|
|
||||||
<g v-if="shape === Shape.Circle">
|
|
||||||
<circle
|
|
||||||
v-if="canAccept"
|
|
||||||
class="receiver"
|
|
||||||
:r="size + 8"
|
|
||||||
:fill="backgroundColor"
|
|
||||||
:stroke="receivingNode ? '#0F0' : '#0F03'"
|
|
||||||
:stroke-width="2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<circle
|
|
||||||
class="body"
|
|
||||||
:r="size"
|
|
||||||
:fill="fillColor"
|
|
||||||
:stroke="outlineColor"
|
|
||||||
:stroke-width="4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<circle
|
|
||||||
class="progress progressFill"
|
|
||||||
v-if="progressDisplay === ProgressDisplay.Fill"
|
|
||||||
:r="Math.max(size * progress - 2, 0)"
|
|
||||||
:fill="progressColor"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
v-else
|
|
||||||
:r="size + 4.5"
|
|
||||||
class="progress progressRing"
|
|
||||||
fill="transparent"
|
|
||||||
:stroke-dasharray="(size + 4.5) * 2 * Math.PI"
|
|
||||||
:stroke-width="5"
|
|
||||||
:stroke-dashoffset="
|
|
||||||
(size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
|
|
||||||
"
|
|
||||||
:stroke="progressColor"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
|
|
||||||
<rect
|
|
||||||
v-if="canAccept"
|
|
||||||
class="receiver"
|
|
||||||
:width="size * sqrtTwo + 16"
|
|
||||||
:height="size * sqrtTwo + 16"
|
|
||||||
:transform="`translate(${-(size * sqrtTwo + 16) / 2}, ${
|
|
||||||
-(size * sqrtTwo + 16) / 2
|
|
||||||
})`"
|
|
||||||
:fill="backgroundColor"
|
|
||||||
:stroke="receivingNode ? '#0F0' : '#0F03'"
|
|
||||||
:stroke-width="2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
|
||||||
class="body"
|
|
||||||
:width="size * sqrtTwo"
|
|
||||||
:height="size * sqrtTwo"
|
|
||||||
:transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
|
|
||||||
:fill="fillColor"
|
|
||||||
:stroke="outlineColor"
|
|
||||||
:stroke-width="4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<rect
|
|
||||||
v-if="progressDisplay === ProgressDisplay.Fill"
|
|
||||||
class="progress progressFill"
|
|
||||||
:width="Math.max(size * sqrtTwo * progress - 2, 0)"
|
|
||||||
:height="Math.max(size * sqrtTwo * progress - 2, 0)"
|
|
||||||
:transform="`translate(${-Math.max(size * sqrtTwo * progress - 2, 0) / 2}, ${
|
|
||||||
-Math.max(size * sqrtTwo * progress - 2, 0) / 2
|
|
||||||
})`"
|
|
||||||
:fill="progressColor"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
v-else
|
|
||||||
class="progress progressDiamond"
|
|
||||||
:width="size * sqrtTwo + 9"
|
|
||||||
:height="size * sqrtTwo + 9"
|
|
||||||
:transform="`translate(${-(size * sqrtTwo + 9) / 2}, ${
|
|
||||||
-(size * sqrtTwo + 9) / 2
|
|
||||||
})`"
|
|
||||||
fill="transparent"
|
|
||||||
:stroke-dasharray="(size * sqrtTwo + 9) * 4"
|
|
||||||
:stroke-width="5"
|
|
||||||
:stroke-dashoffset="
|
|
||||||
(size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4
|
|
||||||
"
|
|
||||||
:stroke="progressColor"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<text :fill="titleColor" class="node-title">{{ title }}</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<transition name="fade" appear>
|
|
||||||
<g v-if="label">
|
|
||||||
<text
|
|
||||||
:fill="label.color ?? titleColor"
|
|
||||||
class="node-title"
|
|
||||||
:class="{ pulsing: label.pulsing }"
|
|
||||||
:y="-size - 20"
|
|
||||||
>{{ label.text }}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<transition name="fade" appear>
|
|
||||||
<text
|
|
||||||
v-if="isSelected && selectedAction"
|
|
||||||
:fill="confirmationLabel.color ?? titleColor"
|
|
||||||
class="node-title"
|
|
||||||
:class="{ pulsing: confirmationLabel.pulsing }"
|
|
||||||
:y="size + 75"
|
|
||||||
>{{ confirmationLabel.text }}</text
|
|
||||||
>
|
|
||||||
</transition>
|
|
||||||
</g>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import themes from "data/themes";
|
|
||||||
import type { BoardNode, GenericBoardNodeAction, GenericNodeType } from "features/boards/board";
|
|
||||||
import { ProgressDisplay, Shape, getNodeProperty } from "features/boards/board";
|
|
||||||
import { isVisible } from "features/feature";
|
|
||||||
import settings from "game/settings";
|
|
||||||
import { CSSProperties, computed, toRefs, unref, watch } from "vue";
|
|
||||||
import BoardNodeAction from "./BoardNodeAction.vue";
|
|
||||||
|
|
||||||
const sqrtTwo = Math.sqrt(2);
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
|
||||||
node: BoardNode;
|
|
||||||
nodeType: GenericNodeType;
|
|
||||||
dragging: BoardNode | null;
|
|
||||||
dragged?: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
hasDragged?: boolean;
|
|
||||||
receivingNode?: boolean;
|
|
||||||
isSelected: boolean;
|
|
||||||
selectedAction: GenericBoardNodeAction | null;
|
|
||||||
}>();
|
|
||||||
const props = toRefs(_props);
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "mouseDown", event: MouseEvent | TouchEvent, node: BoardNode, isDraggable: boolean): void;
|
|
||||||
(e: "endDragging", node: BoardNode): void;
|
|
||||||
(e: "clickAction", actionId: string): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isDraggable = computed(() =>
|
|
||||||
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(isDraggable, value => {
|
|
||||||
const node = unref(props.node);
|
|
||||||
if (unref(props.dragging) === node && !value) {
|
|
||||||
emit("endDragging", node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const actions = computed(() => {
|
|
||||||
const node = unref(props.node);
|
|
||||||
return getNodeProperty(props.nodeType.value.actions, node)?.filter(action =>
|
|
||||||
isVisible(getNodeProperty(action.visibility, node))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const position = computed(() => {
|
|
||||||
const node = unref(props.node);
|
|
||||||
|
|
||||||
if (
|
|
||||||
getNodeProperty(props.nodeType.value.draggable, node) &&
|
|
||||||
unref(props.dragging)?.id === node.id &&
|
|
||||||
unref(props.dragged) != null
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const { x, y } = unref(props.dragged)!;
|
|
||||||
return {
|
|
||||||
x: node.position.x + Math.round(x / 25) * 25,
|
|
||||||
y: node.position.y + Math.round(y / 25) * 25
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return node.position;
|
|
||||||
});
|
|
||||||
|
|
||||||
const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(props.node)));
|
|
||||||
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
|
|
||||||
const label = computed(
|
|
||||||
() =>
|
|
||||||
(props.isSelected.value
|
|
||||||
? unref(props.selectedAction) &&
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
getNodeProperty(unref(props.selectedAction)!.tooltip, unref(props.node))
|
|
||||||
: null) ?? getNodeProperty(props.nodeType.value.label, unref(props.node))
|
|
||||||
);
|
|
||||||
const confirmationLabel = computed(() =>
|
|
||||||
getNodeProperty(
|
|
||||||
unref(props.selectedAction)?.confirmationLabel ?? {
|
|
||||||
text: "Tap again to confirm"
|
|
||||||
},
|
|
||||||
unref(props.node)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
|
|
||||||
const progress = computed(
|
|
||||||
() => getNodeProperty(props.nodeType.value.progress, unref(props.node)) ?? 0
|
|
||||||
);
|
|
||||||
const backgroundColor = computed(() => themes[settings.theme].variables["--background"]);
|
|
||||||
const outlineColor = computed(
|
|
||||||
() =>
|
|
||||||
getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ??
|
|
||||||
themes[settings.theme].variables["--outline"]
|
|
||||||
);
|
|
||||||
const fillColor = computed(
|
|
||||||
() =>
|
|
||||||
getNodeProperty(props.nodeType.value.fillColor, unref(props.node)) ??
|
|
||||||
themes[settings.theme].variables["--raised-background"]
|
|
||||||
);
|
|
||||||
const progressColor = computed(() =>
|
|
||||||
getNodeProperty(props.nodeType.value.progressColor, unref(props.node))
|
|
||||||
);
|
|
||||||
const titleColor = computed(
|
|
||||||
() =>
|
|
||||||
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ??
|
|
||||||
themes[settings.theme].variables["--foreground"]
|
|
||||||
);
|
|
||||||
const progressDisplay = computed(() =>
|
|
||||||
getNodeProperty(props.nodeType.value.progressDisplay, unref(props.node))
|
|
||||||
);
|
|
||||||
const canAccept = computed(
|
|
||||||
() =>
|
|
||||||
props.dragging.value != null &&
|
|
||||||
unref(props.hasDragged) &&
|
|
||||||
getNodeProperty(props.nodeType.value.canAccept, unref(props.node), props.dragging.value)
|
|
||||||
);
|
|
||||||
const style = computed(() => getNodeProperty(props.nodeType.value.style, unref(props.node)));
|
|
||||||
const classes = computed(() => getNodeProperty(props.nodeType.value.classes, unref(props.node)));
|
|
||||||
|
|
||||||
function mouseDown(e: MouseEvent | TouchEvent) {
|
|
||||||
emit("mouseDown", e, props.node.value, isDraggable.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouseUp(e: MouseEvent | TouchEvent) {
|
|
||||||
if (!props.hasDragged?.value) {
|
|
||||||
emit("endDragging", props.node.value);
|
|
||||||
props.nodeType.value.onClick?.(props.node.value);
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.boardnode {
|
|
||||||
cursor: pointer;
|
|
||||||
transition-duration: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardnode:hover .body {
|
|
||||||
fill: var(--highlighted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardnode.isSelected .body {
|
|
||||||
fill: var(--accent1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardnode:not(.isDraggable) .body {
|
|
||||||
fill: var(--locked);
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-title {
|
|
||||||
text-anchor: middle;
|
|
||||||
dominant-baseline: middle;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 200%;
|
|
||||||
pointer-events: none;
|
|
||||||
filter: drop-shadow(3px 3px 2px var(--tooltip-background));
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
transition-duration: 0.05s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressRing {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulsing {
|
|
||||||
animation: pulsing 2s ease-in infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulsing {
|
|
||||||
0% {
|
|
||||||
opacity: 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0.25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.grow-enter-from .node-container,
|
|
||||||
.grow-leave-to .node-container {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,109 +0,0 @@
|
||||||
<template>
|
|
||||||
<transition name="actions" appear>
|
|
||||||
<g v-if="isSelected && actions">
|
|
||||||
<g
|
|
||||||
v-for="(action, index) in actions"
|
|
||||||
:key="action.id"
|
|
||||||
class="action"
|
|
||||||
:class="{ selected: selectedAction?.id === action.id }"
|
|
||||||
:transform="`translate(
|
|
||||||
${
|
|
||||||
(-size - 30) *
|
|
||||||
Math.sin(((actions.length - 1) / 2 - index) * actionDistance)
|
|
||||||
},
|
|
||||||
${
|
|
||||||
(size + 30) *
|
|
||||||
Math.cos(((actions.length - 1) / 2 - index) * actionDistance)
|
|
||||||
}
|
|
||||||
)`"
|
|
||||||
@mousedown="e => performAction(e, action)"
|
|
||||||
@touchstart="e => performAction(e, action)"
|
|
||||||
@mouseup="e => actionMouseUp(e, action)"
|
|
||||||
@touchend.stop="e => actionMouseUp(e, action)"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
:fill="getNodeProperty(action.fillColor, node)"
|
|
||||||
r="20"
|
|
||||||
:stroke-width="selectedAction?.id === action.id ? 4 : 0"
|
|
||||||
:stroke="outlineColor"
|
|
||||||
/>
|
|
||||||
<text :fill="titleColor" class="material-icons">{{
|
|
||||||
getNodeProperty(action.icon, node)
|
|
||||||
}}</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import themes from "data/themes";
|
|
||||||
import type { BoardNode, GenericBoardNodeAction, GenericNodeType } from "features/boards/board";
|
|
||||||
import { getNodeProperty } from "features/boards/board";
|
|
||||||
import settings from "game/settings";
|
|
||||||
import { computed, toRefs, unref } from "vue";
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
|
||||||
node: BoardNode;
|
|
||||||
nodeType: GenericNodeType;
|
|
||||||
actions?: GenericBoardNodeAction[];
|
|
||||||
isSelected: boolean;
|
|
||||||
selectedAction: GenericBoardNodeAction | null;
|
|
||||||
}>();
|
|
||||||
const props = toRefs(_props);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "clickAction", actionId: string): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
|
|
||||||
const outlineColor = computed(
|
|
||||||
() =>
|
|
||||||
getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ??
|
|
||||||
themes[settings.theme].variables["--outline"]
|
|
||||||
);
|
|
||||||
const titleColor = computed(
|
|
||||||
() =>
|
|
||||||
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ??
|
|
||||||
themes[settings.theme].variables["--foreground"]
|
|
||||||
);
|
|
||||||
const actionDistance = computed(() =>
|
|
||||||
getNodeProperty(props.nodeType.value.actionDistance, unref(props.node))
|
|
||||||
);
|
|
||||||
|
|
||||||
function performAction(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
|
||||||
emit("clickAction", action.id);
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
|
||||||
if (unref(props.selectedAction)?.id === action.id) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.action:not(.boardnode):hover circle,
|
|
||||||
.action:not(.boardnode).selected circle {
|
|
||||||
r: 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:not(.boardnode):hover text,
|
|
||||||
.action:not(.boardnode).selected text {
|
|
||||||
font-size: 187.5%; /* 150% * 1.25 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:not(.boardnode) text {
|
|
||||||
text-anchor: middle;
|
|
||||||
dominant-baseline: central;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.actions-enter-from .action,
|
|
||||||
.actions-leave-to .action {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,631 +0,0 @@
|
||||||
import BoardComponent from "features/boards/Board.vue";
|
|
||||||
import type { GenericComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
findFeatures,
|
|
||||||
GatherProps,
|
|
||||||
getUniqueID,
|
|
||||||
setDefault,
|
|
||||||
Visibility
|
|
||||||
} from "features/feature";
|
|
||||||
import { globalBus } from "game/events";
|
|
||||||
import { DefaultValue, deletePersistent, Persistent, State } from "game/persistence";
|
|
||||||
import { persistent } from "game/persistence";
|
|
||||||
import type { Unsubscribe } from "nanoevents";
|
|
||||||
import { Direction, isFunction } from "util/common";
|
|
||||||
import type {
|
|
||||||
Computable,
|
|
||||||
GetComputableType,
|
|
||||||
GetComputableTypeWithDefault,
|
|
||||||
ProcessedComputable
|
|
||||||
} from "util/computed";
|
|
||||||
import { processComputable } from "util/computed";
|
|
||||||
import { createLazyProxy } from "util/proxies";
|
|
||||||
import { computed, isRef, ref, Ref, unref } from "vue";
|
|
||||||
import panZoom from "vue-panzoom";
|
|
||||||
import type { Link } from "../links/links";
|
|
||||||
|
|
||||||
globalBus.on("setupVue", app => panZoom.install(app));
|
|
||||||
|
|
||||||
/** A symbol used to identify {@link Board} features. */
|
|
||||||
export const BoardType = Symbol("Board");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A type representing a computable value for a node on the board. Used for node types to return different values based on the given node and the state of the board.
|
|
||||||
*/
|
|
||||||
export type NodeComputable<T, S extends unknown[] = []> =
|
|
||||||
| Computable<T>
|
|
||||||
| ((node: BoardNode, ...args: S) => T);
|
|
||||||
|
|
||||||
/** Ways to display progress of an action with a duration. */
|
|
||||||
export enum ProgressDisplay {
|
|
||||||
Outline = "Outline",
|
|
||||||
Fill = "Fill"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Node shapes. */
|
|
||||||
export enum Shape {
|
|
||||||
Circle = "Circle",
|
|
||||||
Diamond = "Triangle"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An object representing a node on the board. */
|
|
||||||
export interface BoardNode {
|
|
||||||
id: number;
|
|
||||||
position: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
type: string;
|
|
||||||
state?: State;
|
|
||||||
pinned?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An object representing a link between two nodes on the board. */
|
|
||||||
export interface BoardNodeLink extends Omit<Link, "startNode" | "endNode"> {
|
|
||||||
startNode: BoardNode;
|
|
||||||
endNode: BoardNode;
|
|
||||||
stroke: string;
|
|
||||||
strokeWidth: number;
|
|
||||||
pulsing?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An object representing a label for a node. */
|
|
||||||
export interface NodeLabel {
|
|
||||||
text: string;
|
|
||||||
color?: string;
|
|
||||||
pulsing?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The persistent data for a board. */
|
|
||||||
export type BoardData = {
|
|
||||||
nodes: BoardNode[];
|
|
||||||
selectedNode: number | null;
|
|
||||||
selectedAction: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object that configures a {@link NodeType}.
|
|
||||||
*/
|
|
||||||
export interface NodeTypeOptions {
|
|
||||||
/** The title to display for the node. */
|
|
||||||
title: NodeComputable<string>;
|
|
||||||
/** An optional label for the node. */
|
|
||||||
label?: NodeComputable<NodeLabel | null>;
|
|
||||||
/** The size of the node - diameter for circles, width and height for squares. */
|
|
||||||
size: NodeComputable<number>;
|
|
||||||
/** CSS to apply to this node. */
|
|
||||||
style?: NodeComputable<StyleValue>;
|
|
||||||
/** Dictionary of CSS classes to apply to this node. */
|
|
||||||
classes?: NodeComputable<Record<string, boolean>>;
|
|
||||||
/** Whether the node is draggable or not. */
|
|
||||||
draggable?: NodeComputable<boolean>;
|
|
||||||
/** The shape of the node. */
|
|
||||||
shape: NodeComputable<Shape>;
|
|
||||||
/** Whether the node can accept another node being dropped upon it. */
|
|
||||||
canAccept?: NodeComputable<boolean, [BoardNode]>;
|
|
||||||
/** The progress value of the node, from 0 to 1. */
|
|
||||||
progress?: NodeComputable<number>;
|
|
||||||
/** How the progress should be displayed on the node. */
|
|
||||||
progressDisplay?: NodeComputable<ProgressDisplay>;
|
|
||||||
/** The color of the progress indicator. */
|
|
||||||
progressColor?: NodeComputable<string>;
|
|
||||||
/** The fill color of the node. */
|
|
||||||
fillColor?: NodeComputable<string>;
|
|
||||||
/** The outline color of the node. */
|
|
||||||
outlineColor?: NodeComputable<string>;
|
|
||||||
/** The color of the title text. */
|
|
||||||
titleColor?: NodeComputable<string>;
|
|
||||||
/** The list of action options for the node. */
|
|
||||||
actions?: BoardNodeActionOptions[];
|
|
||||||
/** The arc between each action, in radians. */
|
|
||||||
actionDistance?: NodeComputable<number>;
|
|
||||||
/** A function that is called when the node is clicked. */
|
|
||||||
onClick?: (node: BoardNode) => void;
|
|
||||||
/** A function that is called when a node is dropped onto this node. */
|
|
||||||
onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
|
|
||||||
/** A function that is called for each node of this type every tick. */
|
|
||||||
update?: (node: BoardNode, diff: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The properties that are added onto a processed {@link NodeTypeOptions} to create a {@link NodeType}.
|
|
||||||
*/
|
|
||||||
export interface BaseNodeType {
|
|
||||||
/** The nodes currently on the board of this type. */
|
|
||||||
nodes: Ref<BoardNode[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An object that represents a type of node that can appear on a board. It will handle getting properties and callbacks for every node of that type. */
|
|
||||||
export type NodeType<T extends NodeTypeOptions> = Replace<
|
|
||||||
T & BaseNodeType,
|
|
||||||
{
|
|
||||||
title: GetComputableType<T["title"]>;
|
|
||||||
label: GetComputableType<T["label"]>;
|
|
||||||
size: GetComputableTypeWithDefault<T["size"], 50>;
|
|
||||||
style: GetComputableType<T["style"]>;
|
|
||||||
classes: GetComputableType<T["classes"]>;
|
|
||||||
draggable: GetComputableTypeWithDefault<T["draggable"], false>;
|
|
||||||
shape: GetComputableTypeWithDefault<T["shape"], Shape.Circle>;
|
|
||||||
canAccept: GetComputableTypeWithDefault<T["canAccept"], false>;
|
|
||||||
progress: GetComputableType<T["progress"]>;
|
|
||||||
progressDisplay: GetComputableTypeWithDefault<T["progressDisplay"], ProgressDisplay.Fill>;
|
|
||||||
progressColor: GetComputableTypeWithDefault<T["progressColor"], "none">;
|
|
||||||
fillColor: GetComputableType<T["fillColor"]>;
|
|
||||||
outlineColor: GetComputableType<T["outlineColor"]>;
|
|
||||||
titleColor: GetComputableType<T["titleColor"]>;
|
|
||||||
actions?: GenericBoardNodeAction[];
|
|
||||||
actionDistance: GetComputableTypeWithDefault<T["actionDistance"], number>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
/** A type that matches any valid {@link NodeType} object. */
|
|
||||||
export type GenericNodeType = Replace<
|
|
||||||
NodeType<NodeTypeOptions>,
|
|
||||||
{
|
|
||||||
size: NodeComputable<number>;
|
|
||||||
draggable: NodeComputable<boolean>;
|
|
||||||
shape: NodeComputable<Shape>;
|
|
||||||
canAccept: NodeComputable<boolean, [BoardNode]>;
|
|
||||||
progressDisplay: NodeComputable<ProgressDisplay>;
|
|
||||||
progressColor: NodeComputable<string>;
|
|
||||||
actionDistance: NodeComputable<number>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object that configures a {@link BoardNodeAction}.
|
|
||||||
*/
|
|
||||||
export interface BoardNodeActionOptions {
|
|
||||||
/** A unique identifier for the action. */
|
|
||||||
id: string;
|
|
||||||
/** Whether this action should be visible. */
|
|
||||||
visibility?: NodeComputable<Visibility | boolean>;
|
|
||||||
/** The icon to display for the action. */
|
|
||||||
icon: NodeComputable<string>;
|
|
||||||
/** The fill color of the action. */
|
|
||||||
fillColor?: NodeComputable<string>;
|
|
||||||
/** The tooltip text to display for the action. */
|
|
||||||
tooltip: NodeComputable<NodeLabel>;
|
|
||||||
/** The confirmation label that appears under the action. */
|
|
||||||
confirmationLabel?: NodeComputable<NodeLabel>;
|
|
||||||
/** An array of board node links associated with the action. They appear when the action is focused. */
|
|
||||||
links?: NodeComputable<BoardNodeLink[]>;
|
|
||||||
/** A function that is called when the action is clicked. */
|
|
||||||
onClick: (node: BoardNode) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The properties that are added onto a processed {@link BoardNodeActionOptions} to create an {@link BoardNodeAction}.
|
|
||||||
*/
|
|
||||||
export interface BaseBoardNodeAction {
|
|
||||||
links?: Ref<BoardNodeLink[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An object that represents an action that can be taken upon a node. */
|
|
||||||
export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
|
|
||||||
T & BaseBoardNodeAction,
|
|
||||||
{
|
|
||||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
|
||||||
icon: GetComputableType<T["icon"]>;
|
|
||||||
fillColor: GetComputableType<T["fillColor"]>;
|
|
||||||
tooltip: GetComputableType<T["tooltip"]>;
|
|
||||||
confirmationLabel: GetComputableTypeWithDefault<T["confirmationLabel"], NodeLabel>;
|
|
||||||
links: GetComputableType<T["links"]>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
/** A type that matches any valid {@link BoardNodeAction} object. */
|
|
||||||
export type GenericBoardNodeAction = Replace<
|
|
||||||
BoardNodeAction<BoardNodeActionOptions>,
|
|
||||||
{
|
|
||||||
visibility: NodeComputable<Visibility | boolean>;
|
|
||||||
confirmationLabel: NodeComputable<NodeLabel>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object that configures a {@link Board}.
|
|
||||||
*/
|
|
||||||
export interface BoardOptions {
|
|
||||||
/** Whether this board should be visible. */
|
|
||||||
visibility?: Computable<Visibility | boolean>;
|
|
||||||
/** The height of the board. Defaults to 100% */
|
|
||||||
height?: Computable<string>;
|
|
||||||
/** The width of the board. Defaults to 100% */
|
|
||||||
width?: Computable<string>;
|
|
||||||
/** Dictionary of CSS classes to apply to this feature. */
|
|
||||||
classes?: Computable<Record<string, boolean>>;
|
|
||||||
/** CSS to apply to this feature. */
|
|
||||||
style?: Computable<StyleValue>;
|
|
||||||
/** A function that returns an array of initial board nodes, without IDs. */
|
|
||||||
startNodes: () => Omit<BoardNode, "id">[];
|
|
||||||
/** A dictionary of node types that can appear on the board. */
|
|
||||||
types: Record<string, NodeTypeOptions>;
|
|
||||||
/** The persistent state of the board. */
|
|
||||||
state?: Computable<BoardData>;
|
|
||||||
/** An array of board node links to display. */
|
|
||||||
links?: Computable<BoardNodeLink[] | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
|
|
||||||
*/
|
|
||||||
export interface BaseBoard {
|
|
||||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
|
||||||
id: string;
|
|
||||||
/** All the nodes currently on the board. */
|
|
||||||
nodes: Ref<BoardNode[]>;
|
|
||||||
/** The currently selected node, if any. */
|
|
||||||
selectedNode: Ref<BoardNode | null>;
|
|
||||||
/** The currently selected action, if any. */
|
|
||||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
|
||||||
/** The currently being dragged node, if any. */
|
|
||||||
draggingNode: Ref<BoardNode | null>;
|
|
||||||
/** If dragging a node, the node it's currently being hovered over, if any. */
|
|
||||||
receivingNode: Ref<BoardNode | null>;
|
|
||||||
/** The current mouse position, if over the board. */
|
|
||||||
mousePosition: Ref<{ x: number; y: number } | null>;
|
|
||||||
/** Places a node in the nearest empty space in the given direction with the specified space around it. */
|
|
||||||
placeInAvailableSpace: (node: BoardNode, radius?: number, direction?: Direction) => void;
|
|
||||||
/** A symbol that helps identify features of the same type. */
|
|
||||||
type: typeof BoardType;
|
|
||||||
/** The Vue component used to render this feature. */
|
|
||||||
[Component]: GenericComponent;
|
|
||||||
/** A function to gather the props the vue component requires for this feature. */
|
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An object that represents a feature that is a zoomable, pannable board with various nodes upon it. */
|
|
||||||
export type Board<T extends BoardOptions> = Replace<
|
|
||||||
T & BaseBoard,
|
|
||||||
{
|
|
||||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
|
||||||
types: Record<string, GenericNodeType>;
|
|
||||||
height: GetComputableType<T["height"]>;
|
|
||||||
width: GetComputableType<T["width"]>;
|
|
||||||
classes: GetComputableType<T["classes"]>;
|
|
||||||
style: GetComputableType<T["style"]>;
|
|
||||||
state: GetComputableTypeWithDefault<T["state"], Persistent<BoardData>>;
|
|
||||||
links: GetComputableTypeWithDefault<T["links"], Ref<BoardNodeLink[] | null>>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
/** A type that matches any valid {@link Board} object. */
|
|
||||||
export type GenericBoard = Replace<
|
|
||||||
Board<BoardOptions>,
|
|
||||||
{
|
|
||||||
visibility: ProcessedComputable<Visibility | boolean>;
|
|
||||||
state: ProcessedComputable<BoardData>;
|
|
||||||
links: ProcessedComputable<BoardNodeLink[] | null>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazily creates a board with the given options.
|
|
||||||
* @param optionsFunc Board options.
|
|
||||||
*/
|
|
||||||
export function createBoard<T extends BoardOptions>(
|
|
||||||
optionsFunc: OptionsFunc<T, BaseBoard, GenericBoard>
|
|
||||||
): Board<T> {
|
|
||||||
const state = persistent<BoardData>(
|
|
||||||
{
|
|
||||||
nodes: [],
|
|
||||||
selectedNode: null,
|
|
||||||
selectedAction: null
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
return createLazyProxy(feature => {
|
|
||||||
const board = optionsFunc.call(feature, feature);
|
|
||||||
board.id = getUniqueID("board-");
|
|
||||||
board.type = BoardType;
|
|
||||||
board[Component] = BoardComponent as GenericComponent;
|
|
||||||
|
|
||||||
if (board.state) {
|
|
||||||
deletePersistent(state);
|
|
||||||
processComputable(board as T, "state");
|
|
||||||
} else {
|
|
||||||
state[DefaultValue] = {
|
|
||||||
nodes: board.startNodes().map((n, i) => {
|
|
||||||
(n as BoardNode).id = i;
|
|
||||||
return n as BoardNode;
|
|
||||||
}),
|
|
||||||
selectedNode: null,
|
|
||||||
selectedAction: null
|
|
||||||
};
|
|
||||||
board.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
board.nodes = computed(() => unref(processedBoard.state).nodes);
|
|
||||||
board.selectedNode = computed({
|
|
||||||
get() {
|
|
||||||
return (
|
|
||||||
processedBoard.nodes.value.find(
|
|
||||||
node => node.id === unref(processedBoard.state).selectedNode
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
set(node) {
|
|
||||||
if (isRef(processedBoard.state)) {
|
|
||||||
processedBoard.state.value = {
|
|
||||||
...processedBoard.state.value,
|
|
||||||
selectedNode: node?.id ?? null
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
processedBoard.state.selectedNode = node?.id ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
board.selectedAction = computed({
|
|
||||||
get() {
|
|
||||||
const selectedNode = processedBoard.selectedNode.value;
|
|
||||||
if (selectedNode == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const type = processedBoard.types[selectedNode.type];
|
|
||||||
if (type.actions == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
type.actions.find(
|
|
||||||
action => action.id === unref(processedBoard.state).selectedAction
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
set(action) {
|
|
||||||
if (isRef(processedBoard.state)) {
|
|
||||||
processedBoard.state.value = {
|
|
||||||
...processedBoard.state.value,
|
|
||||||
selectedAction: action?.id ?? null
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
processedBoard.state.selectedAction = action?.id ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
board.mousePosition = ref(null);
|
|
||||||
if (board.links) {
|
|
||||||
processComputable(board as T, "links");
|
|
||||||
} else {
|
|
||||||
board.links = computed(() => {
|
|
||||||
if (processedBoard.selectedAction.value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
processedBoard.selectedAction.value.links &&
|
|
||||||
processedBoard.selectedNode.value
|
|
||||||
) {
|
|
||||||
return getNodeProperty(
|
|
||||||
processedBoard.selectedAction.value.links,
|
|
||||||
processedBoard.selectedNode.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
board.draggingNode = ref(null);
|
|
||||||
board.receivingNode = ref(null);
|
|
||||||
processComputable(board as T, "visibility");
|
|
||||||
setDefault(board, "visibility", Visibility.Visible);
|
|
||||||
processComputable(board as T, "width");
|
|
||||||
setDefault(board, "width", "100%");
|
|
||||||
processComputable(board as T, "height");
|
|
||||||
setDefault(board, "height", "100%");
|
|
||||||
processComputable(board as T, "classes");
|
|
||||||
processComputable(board as T, "style");
|
|
||||||
|
|
||||||
for (const type in board.types) {
|
|
||||||
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
|
|
||||||
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "title");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "label");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "size");
|
|
||||||
setDefault(nodeType, "size", 50);
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "style");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "classes");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "draggable");
|
|
||||||
setDefault(nodeType, "draggable", false);
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "shape");
|
|
||||||
setDefault(nodeType, "shape", Shape.Circle);
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "canAccept");
|
|
||||||
setDefault(nodeType, "canAccept", false);
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "progress");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "progressDisplay");
|
|
||||||
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "progressColor");
|
|
||||||
setDefault(nodeType, "progressColor", "none");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "fillColor");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "outlineColor");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "titleColor");
|
|
||||||
processComputable(nodeType as NodeTypeOptions, "actionDistance");
|
|
||||||
setDefault(nodeType, "actionDistance", Math.PI / 6);
|
|
||||||
nodeType.nodes = computed(() =>
|
|
||||||
unref(processedBoard.state).nodes.filter(node => node.type === type)
|
|
||||||
);
|
|
||||||
setDefault(nodeType, "onClick", function (node: BoardNode) {
|
|
||||||
unref(processedBoard.state).selectedNode = node.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nodeType.actions) {
|
|
||||||
for (const action of nodeType.actions) {
|
|
||||||
processComputable(action, "visibility");
|
|
||||||
setDefault(action, "visibility", Visibility.Visible);
|
|
||||||
processComputable(action, "icon");
|
|
||||||
processComputable(action, "fillColor");
|
|
||||||
processComputable(action, "tooltip");
|
|
||||||
processComputable(action, "confirmationLabel");
|
|
||||||
setDefault(action, "confirmationLabel", { text: "Tap again to confirm" });
|
|
||||||
processComputable(action, "links");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDraggingNode(node: BoardNode | null) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
board.draggingNode!.value = node;
|
|
||||||
}
|
|
||||||
function setReceivingNode(node: BoardNode | null) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
board.receivingNode!.value = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
board.placeInAvailableSpace = function (
|
|
||||||
node: BoardNode,
|
|
||||||
radius = 100,
|
|
||||||
direction = Direction.Right
|
|
||||||
) {
|
|
||||||
const nodes = processedBoard.nodes.value
|
|
||||||
.slice()
|
|
||||||
.filter(n => {
|
|
||||||
// Exclude self
|
|
||||||
if (n === node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exclude nodes that aren't within the corridor we'll be moving within
|
|
||||||
if (
|
|
||||||
(direction === Direction.Down || direction === Direction.Up) &&
|
|
||||||
Math.abs(n.position.x - node.position.x) > radius
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(direction === Direction.Left || direction === Direction.Right) &&
|
|
||||||
Math.abs(n.position.y - node.position.y) > radius
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exclude nodes in the wrong direction
|
|
||||||
return !(
|
|
||||||
(direction === Direction.Right &&
|
|
||||||
n.position.x < node.position.x - radius) ||
|
|
||||||
(direction === Direction.Left && n.position.x > node.position.x + radius) ||
|
|
||||||
(direction === Direction.Up && n.position.y > node.position.y + radius) ||
|
|
||||||
(direction === Direction.Down && n.position.y < node.position.y - radius)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.sort(
|
|
||||||
direction === Direction.Right
|
|
||||||
? (a, b) => a.position.x - b.position.x
|
|
||||||
: direction === Direction.Left
|
|
||||||
? (a, b) => b.position.x - a.position.x
|
|
||||||
: direction === Direction.Up
|
|
||||||
? (a, b) => b.position.y - a.position.y
|
|
||||||
: (a, b) => a.position.y - b.position.y
|
|
||||||
);
|
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
|
||||||
const nodeToCheck = nodes[i];
|
|
||||||
const distance =
|
|
||||||
direction === Direction.Right || direction === Direction.Left
|
|
||||||
? Math.abs(node.position.x - nodeToCheck.position.x)
|
|
||||||
: Math.abs(node.position.y - nodeToCheck.position.y);
|
|
||||||
|
|
||||||
// If we're too close to this node, move further
|
|
||||||
if (distance < radius) {
|
|
||||||
if (direction === Direction.Right) {
|
|
||||||
node.position.x = nodeToCheck.position.x + radius;
|
|
||||||
} else if (direction === Direction.Left) {
|
|
||||||
node.position.x = nodeToCheck.position.x - radius;
|
|
||||||
} else if (direction === Direction.Up) {
|
|
||||||
node.position.y = nodeToCheck.position.y - radius;
|
|
||||||
} else if (direction === Direction.Down) {
|
|
||||||
node.position.y = nodeToCheck.position.y + radius;
|
|
||||||
}
|
|
||||||
} else if (i > 0 && distance > radius) {
|
|
||||||
// If we're further from this node than the radius, then the nodes are past us and we can early exit
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
board[GatherProps] = function (this: GenericBoard) {
|
|
||||||
const {
|
|
||||||
nodes,
|
|
||||||
types,
|
|
||||||
state,
|
|
||||||
visibility,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
style,
|
|
||||||
classes,
|
|
||||||
links,
|
|
||||||
selectedAction,
|
|
||||||
selectedNode,
|
|
||||||
mousePosition,
|
|
||||||
draggingNode,
|
|
||||||
receivingNode
|
|
||||||
} = this;
|
|
||||||
return {
|
|
||||||
nodes,
|
|
||||||
types,
|
|
||||||
state,
|
|
||||||
visibility,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
style: unref(style),
|
|
||||||
classes,
|
|
||||||
links,
|
|
||||||
selectedAction,
|
|
||||||
selectedNode,
|
|
||||||
mousePosition,
|
|
||||||
draggingNode,
|
|
||||||
receivingNode,
|
|
||||||
setDraggingNode,
|
|
||||||
setReceivingNode
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is necessary because board.types is different from T and Board
|
|
||||||
const processedBoard = board as unknown as Board<T>;
|
|
||||||
return processedBoard;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value of a property for a specified node.
|
|
||||||
* @param property The property to find the value of
|
|
||||||
* @param node The node to get the property of
|
|
||||||
*/
|
|
||||||
export function getNodeProperty<T, S extends unknown[]>(
|
|
||||||
property: NodeComputable<T, S>,
|
|
||||||
node: BoardNode,
|
|
||||||
...args: S
|
|
||||||
): T {
|
|
||||||
return isFunction<T, [BoardNode, ...S], Computable<T>>(property)
|
|
||||||
? property(node, ...args)
|
|
||||||
: unref(property);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to get an ID for a node that is guaranteed unique.
|
|
||||||
* @param board The board feature to generate an ID for
|
|
||||||
*/
|
|
||||||
export function getUniqueNodeID(board: GenericBoard): number {
|
|
||||||
let id = 0;
|
|
||||||
board.nodes.value.forEach(node => {
|
|
||||||
if (node.id >= id) {
|
|
||||||
id = node.id + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listeners: Record<string, Unsubscribe | undefined> = {};
|
|
||||||
globalBus.on("addLayer", layer => {
|
|
||||||
const boards: GenericBoard[] = findFeatures(layer, BoardType) as GenericBoard[];
|
|
||||||
listeners[layer.id] = layer.on("postUpdate", diff => {
|
|
||||||
boards.forEach(board => {
|
|
||||||
Object.values(board.types).forEach(type =>
|
|
||||||
type.nodes.value.forEach(node => type.update?.(node, diff))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
globalBus.on("removeLayer", layer => {
|
|
||||||
// unsubscribe from postUpdate
|
|
||||||
listeners[layer.id]?.();
|
|
||||||
listeners[layer.id] = undefined;
|
|
||||||
});
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -129,24 +129,27 @@ document.onkeydown = function (e) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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} /> {unref(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.endNode.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>
|
||||||
|
|
|
@ -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,32 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import MarkNode from "components/MarkNode.vue";
|
import type { CoercableComponent, StyleValue, Visibility } from "features/feature";
|
||||||
import Node from "components/Node.vue";
|
import { isHidden, isVisible } from "features/feature";
|
||||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
|
||||||
import { isHidden, isVisible, Visibility } 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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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];
|
||||||
|
|
101
src/game/boards/Board.vue
Normal file
101
src/game/boards/Board.vue
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<panZoom
|
||||||
|
selector=".stage"
|
||||||
|
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
|
||||||
|
ref="stage"
|
||||||
|
@init="onInit"
|
||||||
|
@mousemove="(e: MouseEvent) => emit('drag', e)"
|
||||||
|
@touchmove="(e: TouchEvent) => emit('drag', e)"
|
||||||
|
@mouseleave="(e: MouseEvent) => emit('mouseLeave', e)"
|
||||||
|
@mouseup="(e: MouseEvent) => emit('mouseUp', e)"
|
||||||
|
@touchend.passive="(e: TouchEvent) => emit('mouseUp', e)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="event-listener"
|
||||||
|
@mousedown="(e: MouseEvent) => emit('mouseDown', e)"
|
||||||
|
@touchstart="(e: TouchEvent) => emit('mouseDown', e)"
|
||||||
|
/>
|
||||||
|
<div class="stage">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</panZoom>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PanZoom } from "panzoom";
|
||||||
|
import type { ComponentPublicInstance } from "vue";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
// Required to make sure panzoom component gets registered:
|
||||||
|
import "./board";
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
panZoomInstance: computed(() => stage.value?.panZoomInstance)
|
||||||
|
});
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "drag", e: MouseEvent | TouchEvent): void;
|
||||||
|
(event: "mouseDown", e: MouseEvent | TouchEvent): void;
|
||||||
|
(event: "mouseUp", e: MouseEvent | TouchEvent): void;
|
||||||
|
(event: "mouseLeave", e: MouseEvent | TouchEvent): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const stage = ref<{ panZoomInstance: PanZoom } & ComponentPublicInstance<HTMLElement>>();
|
||||||
|
|
||||||
|
function onInit(panzoomInstance: PanZoom) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
panzoomInstance.setTransformOrigin(null);
|
||||||
|
panzoomInstance.moveTo(0, stage.value?.$el.clientHeight / 2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.event-listener {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
transition-duration: 0s;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vue-pan-zoom-item {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-pan-zoom-scene {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-pan-zoom-scene:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage > * {
|
||||||
|
pointer-events: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Only" child (excluding resize listener) */
|
||||||
|
.layer-tab > .vue-pan-zoom-item:first-child:nth-last-child(2) {
|
||||||
|
width: calc(100% + 20px);
|
||||||
|
height: calc(100% + 100px);
|
||||||
|
margin: -50px -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-node {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transition-duration: 0s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
game/boards/board
|
29
src/game/boards/CircleProgress.vue
Normal file
29
src/game/boards/CircleProgress.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<circle
|
||||||
|
:r="r"
|
||||||
|
fill="transparent"
|
||||||
|
:stroke-dasharray="r * 2 * Math.PI"
|
||||||
|
:stroke-width="5"
|
||||||
|
:stroke-dashoffset="r * 2 * Math.PI - progress * r * 2 * Math.PI"
|
||||||
|
:stroke="stroke"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SVGAttributes } from "vue";
|
||||||
|
|
||||||
|
interface CircleProgressProps extends SVGAttributes {
|
||||||
|
r: number;
|
||||||
|
progress: number;
|
||||||
|
stroke: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<CircleProgressProps>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
circle {
|
||||||
|
transition-duration: 0.05s;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
</style>
|
29
src/game/boards/Draggable.vue
Normal file
29
src/game/boards/Draggable.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="board-node"
|
||||||
|
:style="`transform: translate(calc(${unref(position).x}px - 50%), ${unref(position).y}px);`"
|
||||||
|
@mousedown="e => mouseDown(e)"
|
||||||
|
@touchstart.passive="e => mouseDown(e)"
|
||||||
|
@mouseup="e => mouseUp(e)"
|
||||||
|
@touchend.passive="e => mouseUp(e)"
|
||||||
|
>
|
||||||
|
<component v-if="comp" :is="comp" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { jsx } from "features/feature";
|
||||||
|
import { VueFeature, coerceComponent, renderJSX } from "util/vue";
|
||||||
|
import { Ref, shallowRef, unref } from "vue";
|
||||||
|
import { NodePosition } from "./board";
|
||||||
|
unref;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
element: VueFeature;
|
||||||
|
mouseDown: (e: MouseEvent | TouchEvent) => void;
|
||||||
|
mouseUp: (e: MouseEvent | TouchEvent) => void;
|
||||||
|
position: Ref<NodePosition>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const comp = shallowRef(coerceComponent(jsx(() => renderJSX(props.element))));
|
||||||
|
</script>
|
28
src/game/boards/SVGNode.vue
Normal file
28
src/game/boards/SVGNode.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
@mousedown="e => emit('mouseDown', e)"
|
||||||
|
@touchstart.passive="e => emit('mouseDown', e)"
|
||||||
|
@mouseup="e => emit('mouseUp', e)"
|
||||||
|
@touchend.passive="e => emit('mouseUp', e)"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "mouseDown", event: MouseEvent | TouchEvent): void;
|
||||||
|
(e: "mouseUp", event: MouseEvent | TouchEvent): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
cursor: pointer;
|
||||||
|
transition-duration: 0s;
|
||||||
|
overflow: visible;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
30
src/game/boards/SquareProgress.vue
Normal file
30
src/game/boards/SquareProgress.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<rect
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
:transform="`translate(${-size / 2}, ${-size / 2})`"
|
||||||
|
fill="transparent"
|
||||||
|
:stroke-dasharray="size * 4"
|
||||||
|
:stroke-width="5"
|
||||||
|
:stroke-dashoffset="size * 4 - progress * size * 4"
|
||||||
|
:stroke="stroke"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SVGAttributes } from "vue";
|
||||||
|
|
||||||
|
interface SquareProgressProps extends SVGAttributes {
|
||||||
|
size: number;
|
||||||
|
progress: number;
|
||||||
|
stroke: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<SquareProgressProps>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
rect {
|
||||||
|
transition-duration: 0.05s;
|
||||||
|
}
|
||||||
|
</style>
|
438
src/game/boards/board.tsx
Normal file
438
src/game/boards/board.tsx
Normal file
|
@ -0,0 +1,438 @@
|
||||||
|
import { Component, GatherProps, GenericComponent, jsx } from "features/feature";
|
||||||
|
import { globalBus } from "game/events";
|
||||||
|
import { Persistent, persistent } from "game/persistence";
|
||||||
|
import type { PanZoom } from "panzoom";
|
||||||
|
import { Direction, isFunction } from "util/common";
|
||||||
|
import type { Computable, ProcessedComputable } from "util/computed";
|
||||||
|
import { convertComputable } from "util/computed";
|
||||||
|
import { VueFeature } from "util/vue";
|
||||||
|
import type { ComponentPublicInstance, Ref } from "vue";
|
||||||
|
import { computed, nextTick, ref, unref, watchEffect } from "vue";
|
||||||
|
import panZoom from "vue-panzoom";
|
||||||
|
import { JSX } from "vue/jsx-runtime";
|
||||||
|
import Board from "./Board.vue";
|
||||||
|
import Draggable from "./Draggable.vue";
|
||||||
|
|
||||||
|
// Register panzoom so it can be used in Board.vue
|
||||||
|
globalBus.on("setupVue", app => panZoom.install(app));
|
||||||
|
|
||||||
|
/** A type representing the position of a node. */
|
||||||
|
export type NodePosition = { x: number; y: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type representing a computable value for a node on the board. Used for node types to return different values based on the given node and the state of the board.
|
||||||
|
*/
|
||||||
|
export type NodeComputable<T, R, S extends unknown[] = []> =
|
||||||
|
| Computable<R>
|
||||||
|
| ((node: T, ...args: S) => R);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of a property for a specified node.
|
||||||
|
* @param property The property to find the value of
|
||||||
|
* @param node The node to get the property of
|
||||||
|
*/
|
||||||
|
export function unwrapNodeRef<T, R, S extends unknown[]>(
|
||||||
|
property: NodeComputable<T, R, S>,
|
||||||
|
node: T,
|
||||||
|
...args: S
|
||||||
|
): R {
|
||||||
|
return isFunction<R, [T, ...S], ProcessedComputable<R>>(property)
|
||||||
|
? property(node, ...args)
|
||||||
|
: unref(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a computed ref that can assist in assigning new nodes an ID unique from all current nodes.
|
||||||
|
* @param nodes The list of current nodes with IDs as properties
|
||||||
|
* @returns A computed ref that will give the value of the next unique ID
|
||||||
|
*/
|
||||||
|
export function setupUniqueIds(nodes: Computable<{ id: number }[]>) {
|
||||||
|
const processedNodes = convertComputable(nodes);
|
||||||
|
return computed(() => Math.max(-1, ...unref(processedNodes).map(node => node.id)) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An object that configures a {@link DraggableNode}. */
|
||||||
|
export interface DraggableNodeOptions<T> {
|
||||||
|
/** A ref to the specific instance of the Board vue component the node will be draggable on. Obtained by passing a suitable ref as the "ref" attribute to the <Board> element. */
|
||||||
|
board: Ref<ComponentPublicInstance<typeof Board> | undefined>;
|
||||||
|
/** Getter function to go from the node (typically ID) to the position of said node. */
|
||||||
|
getPosition: (node: T) => NodePosition;
|
||||||
|
/** Setter function to update the position of a node. */
|
||||||
|
setPosition: (node: T, position: NodePosition) => void;
|
||||||
|
/** A list of nodes that the currently dragged node can be dropped upon. */
|
||||||
|
receivingNodes?: NodeComputable<T, T[]>;
|
||||||
|
/** The maximum distance (in pixels, before zoom) away a node can be and still drop onto a receiving node. */
|
||||||
|
dropAreaRadius?: NodeComputable<T, number>;
|
||||||
|
/** A callback for when a node gets dropped upon a receiving node. */
|
||||||
|
onDrop?: (acceptingNode: T, draggingNode: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An object that represents a system for moving nodes on a board by dragging them. */
|
||||||
|
export interface DraggableNode<T> {
|
||||||
|
/** A ref to the node currently being moved. */
|
||||||
|
nodeBeingDragged: Ref<T | undefined>;
|
||||||
|
/** A ref to the node the node being dragged could be dropped upon if let go, if any. The node closest to the node being dragged if there are more than one within the drop area radius. */
|
||||||
|
receivingNode: Ref<T | undefined>;
|
||||||
|
/** A ref to whether or not the node being dragged has actually been dragged away from its starting position. */
|
||||||
|
hasDragged: Ref<boolean>;
|
||||||
|
/** The position of the node being dragged relative to where it started at the beginning of the drag. */
|
||||||
|
dragDelta: Ref<NodePosition>;
|
||||||
|
/** The nodes that can receive the node currently being dragged. */
|
||||||
|
receivingNodes: Ref<T[]>;
|
||||||
|
/** A function to call whenever a drag should start, that takes the mouse event that triggered it. Typically attached to each node's onMouseDown listener. */
|
||||||
|
startDrag: (e: MouseEvent | TouchEvent, node: T) => void;
|
||||||
|
/** A function to call whenever a drag should end, typically attached to the Board's onMouseUp and onMouseLeave listeners. */
|
||||||
|
endDrag: VoidFunction;
|
||||||
|
/** A function to call when the mouse moves during a drag, typically attached to the Board's onDrag listener. */
|
||||||
|
drag: (e: MouseEvent | TouchEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a system to allow nodes to be moved within a board by dragging and dropping.
|
||||||
|
* Also allows for dropping nodes on other nodes to trigger code.
|
||||||
|
* @param options Draggable node options.
|
||||||
|
* @returns A DraggableNode object.
|
||||||
|
*/
|
||||||
|
export function setupDraggableNode<T>(options: DraggableNodeOptions<T>): DraggableNode<T> {
|
||||||
|
const nodeBeingDragged = ref<T>();
|
||||||
|
const receivingNode = ref<T>();
|
||||||
|
const hasDragged = ref(false);
|
||||||
|
const dragDelta = ref({ x: 0, y: 0 });
|
||||||
|
const receivingNodes = computed(() =>
|
||||||
|
nodeBeingDragged.value == null
|
||||||
|
? []
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
unwrapNodeRef(options.receivingNodes ?? [], nodeBeingDragged.value!)
|
||||||
|
);
|
||||||
|
const dropAreaRadius = options.dropAreaRadius ?? 50;
|
||||||
|
|
||||||
|
const mousePosition = ref<NodePosition>();
|
||||||
|
const lastMousePosition = ref({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const node = nodeBeingDragged.value;
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalPosition = options.getPosition(node);
|
||||||
|
const position = {
|
||||||
|
x: originalPosition.x + dragDelta.value.x,
|
||||||
|
y: originalPosition.y + dragDelta.value.y
|
||||||
|
};
|
||||||
|
let smallestDistance = Number.MAX_VALUE;
|
||||||
|
|
||||||
|
receivingNode.value = unref(receivingNodes).reduce((smallest: T | undefined, curr: T) => {
|
||||||
|
if ((curr as T) === node) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = options.getPosition(curr);
|
||||||
|
const distanceSquared = Math.pow(position.x - x, 2) + Math.pow(position.y - y, 2);
|
||||||
|
const size = unwrapNodeRef(dropAreaRadius, curr);
|
||||||
|
if (distanceSquared > smallestDistance || distanceSquared > size * size) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
|
||||||
|
smallestDistance = distanceSquared;
|
||||||
|
return curr;
|
||||||
|
}, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
nodeBeingDragged,
|
||||||
|
receivingNode,
|
||||||
|
hasDragged,
|
||||||
|
dragDelta,
|
||||||
|
receivingNodes,
|
||||||
|
startDrag: function (e: MouseEvent | TouchEvent, node: T) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if ("touches" in e) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
lastMousePosition.value = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY
|
||||||
|
};
|
||||||
|
dragDelta.value = { x: 0, y: 0 };
|
||||||
|
hasDragged.value = false;
|
||||||
|
|
||||||
|
nodeBeingDragged.value = node;
|
||||||
|
},
|
||||||
|
endDrag: function () {
|
||||||
|
if (nodeBeingDragged.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (receivingNode.value == null) {
|
||||||
|
const { x, y } = options.getPosition(nodeBeingDragged.value);
|
||||||
|
const newX = x + Math.round(dragDelta.value.x / 25) * 25;
|
||||||
|
const newY = y + Math.round(dragDelta.value.y / 25) * 25;
|
||||||
|
options.setPosition(nodeBeingDragged.value, { x: newX, y: newY });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receivingNode.value != null) {
|
||||||
|
options.onDrop?.(receivingNode.value, nodeBeingDragged.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeBeingDragged.value = undefined;
|
||||||
|
},
|
||||||
|
drag: function (e: MouseEvent | TouchEvent) {
|
||||||
|
const panZoomInstance = options.board.value?.panZoomInstance as PanZoom | undefined;
|
||||||
|
if (panZoomInstance == null || nodeBeingDragged.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y, scale } = panZoomInstance.getTransform();
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if ("touches" in e) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
result.endDrag();
|
||||||
|
mousePosition.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
mousePosition.value = {
|
||||||
|
x: (clientX - x) / scale,
|
||||||
|
y: (clientY - y) / scale
|
||||||
|
};
|
||||||
|
|
||||||
|
dragDelta.value = {
|
||||||
|
x: dragDelta.value.x + (clientX - lastMousePosition.value.x) / scale,
|
||||||
|
y: dragDelta.value.y + (clientY - lastMousePosition.value.y) / scale
|
||||||
|
};
|
||||||
|
lastMousePosition.value = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Math.abs(dragDelta.value.x) > 10 || Math.abs(dragDelta.value.y) > 10) {
|
||||||
|
hasDragged.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An object that configures how to make a vue feature draggable using {@link makeDraggable}. */
|
||||||
|
export interface MakeDraggableOptions<T> {
|
||||||
|
/** The node ID to use for the vue feature. */
|
||||||
|
id: T;
|
||||||
|
/** A reference to the current node being dragged, typically from {@link setupDraggableNode}. */
|
||||||
|
nodeBeingDragged: Ref<T | undefined>;
|
||||||
|
/** A reference to whether or not the node being dragged has been moved away from its initial position. Typically from {@link setupDraggableNode}. */
|
||||||
|
hasDragged: Ref<boolean>;
|
||||||
|
/** A reference to how far the node being dragged is from its initial position. Typically from {@link setupDraggableNode}. */
|
||||||
|
dragDelta: Ref<NodePosition>;
|
||||||
|
/** A function to call when a drag is supposed to start. Typically from {@link setupDraggableNode}. */
|
||||||
|
startDrag: (e: MouseEvent | TouchEvent, id: T) => void;
|
||||||
|
/** A function to call when a drag is supposed to end. Typically from {@link setupDraggableNode}. */
|
||||||
|
endDrag: VoidFunction;
|
||||||
|
/** A callback that's called when the element is pressed down. Fires before drag starts, and returning `false` will prevent the drag from happening. */
|
||||||
|
onMouseDown?: (e: MouseEvent | TouchEvent) => boolean | void;
|
||||||
|
/** A callback that's called when the mouse is lifted off the element. */
|
||||||
|
onMouseUp?: (e: MouseEvent | TouchEvent) => boolean | void;
|
||||||
|
/** The initial position of the node on the board. Defaults to (0, 0). */
|
||||||
|
initialPosition?: NodePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a vue feature draggable on a Board.
|
||||||
|
* @param element The vue feature to make draggable.
|
||||||
|
* @param options The options to configure the dragging behavior.
|
||||||
|
*/
|
||||||
|
export function makeDraggable<T extends VueFeature, S>(
|
||||||
|
element: T,
|
||||||
|
options: MakeDraggableOptions<S>
|
||||||
|
): asserts element is T & { position: Persistent<NodePosition> } {
|
||||||
|
const position = persistent(options.initialPosition ?? { x: 0, y: 0 });
|
||||||
|
(element as T & { position: Persistent<NodePosition> }).position = position;
|
||||||
|
const computedPosition = computed(() => {
|
||||||
|
if (options.nodeBeingDragged.value === options.id) {
|
||||||
|
return {
|
||||||
|
x: position.value.x + options.dragDelta.value.x,
|
||||||
|
y: position.value.y + options.dragDelta.value.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return position.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleMouseDown(e: MouseEvent | TouchEvent) {
|
||||||
|
if (options.onMouseDown?.(e) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.nodeBeingDragged.value == null) {
|
||||||
|
options.startDrag(e, options.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(e: MouseEvent | TouchEvent) {
|
||||||
|
options.onMouseUp?.(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const elementComponent = element[Component];
|
||||||
|
const elementGatherProps = element[GatherProps].bind(element);
|
||||||
|
element[Component] = Draggable as GenericComponent;
|
||||||
|
element[GatherProps] = function gatherTooltipProps(this: typeof options) {
|
||||||
|
return {
|
||||||
|
element: {
|
||||||
|
[Component]: elementComponent,
|
||||||
|
[GatherProps]: elementGatherProps
|
||||||
|
},
|
||||||
|
mouseDown: handleMouseDown,
|
||||||
|
mouseUp: handleMouseUp,
|
||||||
|
position: computedPosition
|
||||||
|
};
|
||||||
|
}.bind(options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An object that configures how to setup a list of actions using {@link setupActions}. */
|
||||||
|
export interface SetupActionsOptions<T extends NodePosition> {
|
||||||
|
/** The node to display actions upon, or undefined when the actions should be hidden. */
|
||||||
|
node: Computable<T | undefined>;
|
||||||
|
/** Whether or not to currently display the actions. */
|
||||||
|
shouldShowActions?: NodeComputable<T, boolean>;
|
||||||
|
/** The list of actions to display. Actions are arbitrary JSX elements. */
|
||||||
|
actions: NodeComputable<T, ((position: NodePosition) => JSX.Element)[]>;
|
||||||
|
/** The distance from the node to place the actions. */
|
||||||
|
distance: NodeComputable<T, number>;
|
||||||
|
/** The arc length to place between actions, in radians. */
|
||||||
|
arcLength?: NodeComputable<T, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a system where a list of actions, which are arbitrary JSX elements, will get displayed around a node radially, under given conditions. The actions are radially centered around 3/2 PI (Down).
|
||||||
|
* @param options Setup actions options.
|
||||||
|
* @returns A JSX function to render the actions.
|
||||||
|
*/
|
||||||
|
export function setupActions<T extends NodePosition>(options: SetupActionsOptions<T>) {
|
||||||
|
const node = convertComputable(options.node);
|
||||||
|
return jsx(() => {
|
||||||
|
const currNode = unref(node);
|
||||||
|
if (currNode == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = unwrapNodeRef(options.actions, currNode);
|
||||||
|
const shouldShow = unwrapNodeRef(options.shouldShowActions, currNode) ?? true;
|
||||||
|
if (!shouldShow) {
|
||||||
|
return <>{actions.map(f => f(currNode))}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = unwrapNodeRef(options.distance, currNode);
|
||||||
|
const arcLength = unwrapNodeRef(options.arcLength, currNode) ?? Math.PI / 6;
|
||||||
|
const firstAngle = Math.PI / 2 - ((actions.length - 1) / 2) * arcLength;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{actions.map((f, index) =>
|
||||||
|
f({
|
||||||
|
x: currNode.x + Math.cos(firstAngle + index * arcLength) * distance,
|
||||||
|
y: currNode.y + Math.sin(firstAngle + index * arcLength) * distance
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a node so that it is sufficiently far away from any other nodes, to prevent overlapping.
|
||||||
|
* @param nodeToPlace The node to find a spot for, with it's current/preffered position.
|
||||||
|
* @param nodes The list of nodes to make sure nodeToPlace is far enough away from.
|
||||||
|
* @param radius How far away nodeToPlace must be from any other nodes.
|
||||||
|
* @param direction The direction to push the nodeToPlace until it finds an available spot.
|
||||||
|
*/
|
||||||
|
export function placeInAvailableSpace<T extends NodePosition>(
|
||||||
|
nodeToPlace: T,
|
||||||
|
nodes: T[],
|
||||||
|
radius = 100,
|
||||||
|
direction = Direction.Right
|
||||||
|
) {
|
||||||
|
nodes = nodes
|
||||||
|
.filter(n => {
|
||||||
|
// Exclude self
|
||||||
|
if (n === nodeToPlace) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude nodes that aren't within the corridor we'll be moving within
|
||||||
|
if (
|
||||||
|
(direction === Direction.Down || direction === Direction.Up) &&
|
||||||
|
Math.abs(n.x - nodeToPlace.x) > radius
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(direction === Direction.Left || direction === Direction.Right) &&
|
||||||
|
Math.abs(n.y - nodeToPlace.y) > radius
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude nodes in the wrong direction
|
||||||
|
return !(
|
||||||
|
(direction === Direction.Right && n.x < nodeToPlace.x - radius) ||
|
||||||
|
(direction === Direction.Left && n.x > nodeToPlace.x + radius) ||
|
||||||
|
(direction === Direction.Up && n.y > nodeToPlace.y + radius) ||
|
||||||
|
(direction === Direction.Down && n.y < nodeToPlace.y - radius)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort(
|
||||||
|
direction === Direction.Right
|
||||||
|
? (a, b) => a.x - b.x
|
||||||
|
: direction === Direction.Left
|
||||||
|
? (a, b) => b.x - a.x
|
||||||
|
: direction === Direction.Up
|
||||||
|
? (a, b) => b.y - a.y
|
||||||
|
: (a, b) => a.y - b.y
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const nodeToCheck = nodes[i];
|
||||||
|
const distance =
|
||||||
|
direction === Direction.Right || direction === Direction.Left
|
||||||
|
? Math.abs(nodeToPlace.x - nodeToCheck.x)
|
||||||
|
: Math.abs(nodeToPlace.y - nodeToCheck.y);
|
||||||
|
|
||||||
|
// If we're too close to this node, move further
|
||||||
|
// Keep in mind positions start at top right, so "down" means increasing Y
|
||||||
|
if (distance < radius) {
|
||||||
|
if (direction === Direction.Right) {
|
||||||
|
nodeToPlace.x = nodeToCheck.x + radius;
|
||||||
|
} else if (direction === Direction.Left) {
|
||||||
|
nodeToPlace.x = nodeToCheck.x - radius;
|
||||||
|
} else if (direction === Direction.Up) {
|
||||||
|
nodeToPlace.y = nodeToCheck.y - radius;
|
||||||
|
} else if (direction === Direction.Down) {
|
||||||
|
nodeToPlace.y = nodeToCheck.y + radius;
|
||||||
|
}
|
||||||
|
} else if (i > 0 && distance > radius) {
|
||||||
|
// If we're further from this node than the radius, then the nodes are past us and we can early exit
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
@ -95,12 +91,6 @@ 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 {
|
||||||
|
@ -108,6 +98,15 @@ export async function startGameLoop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
watch(hasWon, hasWon => {
|
||||||
state.mouseActivity = [...state.mouseActivity.slice(-7), false];
|
if (hasWon) {
|
||||||
}, 1000 * 60 * 60);
|
globalBus.emit("gameWon");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
state.mouseActivity = [...state.mouseActivity.slice(-7), false];
|
||||||
|
},
|
||||||
|
1000 * 60 * 60
|
||||||
|
);
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -341,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>({
|
||||||
|
|
|
@ -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>);
|
||||||
|
@ -269,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);
|
||||||
|
@ -281,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);
|
||||||
|
@ -299,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));
|
||||||
|
@ -343,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);
|
||||||
|
|
14
src/main.css
14
src/main.css
|
@ -70,3 +70,17 @@ ul {
|
||||||
:disabled {
|
:disabled {
|
||||||
pointer-events: none;
|
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;
|
||||||
|
}
|
||||||
|
|
37
src/main.ts
37
src/main.ts
|
@ -3,12 +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 "util/galaxy";
|
import { globalBus } from "./game/events";
|
||||||
|
import { startGameLoop } from "./game/gameLoop";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
|
@ -18,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;
|
||||||
|
@ -61,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));
|
||||||
|
@ -75,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 {
|
||||||
|
|
|
@ -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>>;
|
||||||
|
|
|
@ -172,6 +172,7 @@ function syncSaves(
|
||||||
const localSave = localStorage.getItem(id) ?? "";
|
const localSave = localStorage.getItem(id) ?? "";
|
||||||
const parsedLocalSave = JSON.parse(decodeSave(localSave) ?? "");
|
const parsedLocalSave = JSON.parse(decodeSave(localSave) ?? "");
|
||||||
const slot = availableSlots.values().next().value;
|
const slot = availableSlots.values().next().value;
|
||||||
|
if (slot == null) return;
|
||||||
galaxy.value
|
galaxy.value
|
||||||
?.save(slot, localSave, parsedLocalSave.name)
|
?.save(slot, localSave, parsedLocalSave.name)
|
||||||
.then(() => syncedSaves.value.push(parsedLocalSave.id))
|
.then(() => syncedSaves.value.push(parsedLocalSave.id))
|
||||||
|
|
|
@ -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,6 +1,8 @@
|
||||||
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
|
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";
|
||||||
|
@ -101,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];
|
||||||
|
|
|
@ -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,22 +179,22 @@ 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
|
||||||
|
@ -198,12 +203,14 @@ export function computeOptionalComponent(
|
||||||
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
108
tests/features/board.test.ts
Normal file
108
tests/features/board.test.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import {
|
||||||
|
NodePosition,
|
||||||
|
placeInAvailableSpace,
|
||||||
|
setupUniqueIds,
|
||||||
|
unwrapNodeRef
|
||||||
|
} from "game/boards/board";
|
||||||
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
|
import { Ref, ref } from "vue";
|
||||||
|
import "../utils";
|
||||||
|
import { Direction } from "util/common";
|
||||||
|
|
||||||
|
describe("Unwraps node refs", () => {
|
||||||
|
test("Static value", () => expect(unwrapNodeRef(100, {})).toBe(100));
|
||||||
|
test("Ref value", () => expect(unwrapNodeRef(ref(100), {})).toBe(100));
|
||||||
|
test("0 param function value", () => expect(unwrapNodeRef(() => 100, {})).toBe(100));
|
||||||
|
test("1 param function value", () => {
|
||||||
|
const actualNode = { foo: "bar" };
|
||||||
|
expect(
|
||||||
|
unwrapNodeRef(function (node) {
|
||||||
|
if (node === actualNode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, actualNode)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Set up unique IDs", () => {
|
||||||
|
let nodes: Ref<{ id: number }[]>, nextId: Ref<number>;
|
||||||
|
beforeEach(() => {
|
||||||
|
nodes = ref([]);
|
||||||
|
nextId = setupUniqueIds(nodes);
|
||||||
|
});
|
||||||
|
test("Starts at 0", () => expect(nextId?.value).toBe(0));
|
||||||
|
test("Calculates initial value properly", () => {
|
||||||
|
nodes.value = [{ id: 0 }, { id: 1 }, { id: 2 }];
|
||||||
|
expect(nextId.value).toBe(3);
|
||||||
|
});
|
||||||
|
test("Non consecutive IDs", () => {
|
||||||
|
nodes.value = [{ id: -5 }, { id: 0 }, { id: 200 }];
|
||||||
|
expect(nextId.value).toBe(201);
|
||||||
|
});
|
||||||
|
test("After modification", () => {
|
||||||
|
nodes.value = [{ id: 0 }, { id: 1 }, { id: 2 }];
|
||||||
|
nodes.value.push({ id: nextId.value });
|
||||||
|
expect(nextId.value).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Place in available space", () => {
|
||||||
|
let nodes: Ref<NodePosition[]>, node: NodePosition;
|
||||||
|
beforeEach(() => {
|
||||||
|
nodes = ref([]);
|
||||||
|
node = { x: 10, y: 20 };
|
||||||
|
});
|
||||||
|
test("No nodes", () => {
|
||||||
|
placeInAvailableSpace(node, nodes.value);
|
||||||
|
expect(node).toMatchObject({ x: 10, y: 20 });
|
||||||
|
});
|
||||||
|
test("Moves node", () => {
|
||||||
|
nodes.value = [{ x: 10, y: 20 }];
|
||||||
|
placeInAvailableSpace(node, nodes.value);
|
||||||
|
expect(node).not.toMatchObject({ x: 10, y: 20 });
|
||||||
|
});
|
||||||
|
describe("Respects radius", () => {
|
||||||
|
test("Positions radius away", () => {
|
||||||
|
nodes.value = [{ x: 10, y: 20 }];
|
||||||
|
placeInAvailableSpace(node, nodes.value, 32);
|
||||||
|
expect(node).toMatchObject({ x: 42, y: 20 });
|
||||||
|
});
|
||||||
|
test("Ignores node already radius away", () => {
|
||||||
|
nodes.value = [{ x: 42, y: 20 }];
|
||||||
|
placeInAvailableSpace(node, nodes.value, 32);
|
||||||
|
expect(node).toMatchObject({ x: 10, y: 20 });
|
||||||
|
});
|
||||||
|
test("Doesn't ignore node just under radius away", () => {
|
||||||
|
nodes.value = [{ x: 41, y: 20 }];
|
||||||
|
placeInAvailableSpace(node, nodes.value, 32);
|
||||||
|
expect(node).not.toMatchObject({ x: 10, y: 20 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Respects direction", () => {
|
||||||
|
test("Goes left", () => {
|
||||||
|
nodes.value = [{ x: 10, y: 20 }];
|
||||||
|
placeInAvailableSpace(node, nodes.value, 10, Direction.Left);
|
||||||
|
expect(node).toMatchObject({ x: 0, y: 20 });
|
||||||
|
});
|
||||||
|
test("Goes up", () => {
|
||||||
|
nodes.value = [{ x: 10, y: 20 }];
|
||||||
|
placeInAvailableSpace(node, nodes.value, 10, Direction.Up);
|
||||||
|
expect(node).toMatchObject({ x: 10, y: 10 });
|
||||||
|
});
|
||||||
|
test("Goes down", () => {
|
||||||
|
nodes.value = [{ x: 10, y: 20 }];
|
||||||
|
placeInAvailableSpace(node, nodes.value, 10, Direction.Down);
|
||||||
|
expect(node).toMatchObject({ x: 10, y: 30 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("Finds hole", () => {
|
||||||
|
nodes.value = [
|
||||||
|
{ x: 10, y: 20 },
|
||||||
|
{ x: 30, y: 20 }
|
||||||
|
];
|
||||||
|
placeInAvailableSpace(node, nodes.value, 10);
|
||||||
|
expect(node).toMatchObject({ x: 20, y: 20 });
|
||||||
|
});
|
||||||
|
});
|
File diff suppressed because it is too large
Load diff
|
@ -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