Merge remote-tracking branch 'template/main'
This commit is contained in:
commit
61f0eace25
102 changed files with 10887 additions and 4474 deletions
12
.eslintrc.js
12
.eslintrc.js
|
@ -11,7 +11,8 @@ module.exports = {
|
|||
"@vue/eslint-config-prettier"
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
ecmaVersion: 2020,
|
||||
project: "tsconfig.json"
|
||||
},
|
||||
ignorePatterns: ["src/lib"],
|
||||
rules: {
|
||||
|
@ -19,7 +20,14 @@ module.exports = {
|
|||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"vue/script-setup-uses-vars": "warn",
|
||||
"vue/no-mutating-props": "off",
|
||||
"vue/multi-word-component-names": "off"
|
||||
"vue/multi-word-component-names": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": [
|
||||
"error",
|
||||
{
|
||||
allowNullableObject: true,
|
||||
allowNullableBoolean: true
|
||||
}
|
||||
]
|
||||
},
|
||||
globals: {
|
||||
defineProps: "readonly",
|
||||
|
|
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -10,6 +10,8 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
|
||||
run: |
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -10,6 +10,8 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
|
2
.replit
2
.replit
|
@ -1,4 +1,4 @@
|
|||
run = "npm install; npm run serve"
|
||||
run = "npm install; npm run dev"
|
||||
|
||||
[packager]
|
||||
language = "nodejs"
|
||||
|
|
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
|
@ -1,3 +1,12 @@
|
|||
{
|
||||
"vitest.commandLine": "npx vitest"
|
||||
}
|
||||
"vitest.commandLine": "npx vitest",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"git.ignoreLimitWarning": true,
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
|
85
CHANGELOG.md
85
CHANGELOG.md
|
@ -6,6 +6,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **BREAKING** New requirements system
|
||||
- Replaces many features' existing requirements with new generic form
|
||||
- Formulas, which can be used to calculate buy max for you
|
||||
- Action feature
|
||||
- ETA util
|
||||
- createCollapsibleMilestones util
|
||||
- deleteLowerSaves util
|
||||
- Minimized layers can now display a component
|
||||
- submitOnBlur property to Text fields
|
||||
- showPopups property to Milestones
|
||||
- Mouse/touch events to more onClick listeners
|
||||
- Example hotkey to starting layer
|
||||
- Schema for projInfo.json
|
||||
### Changes
|
||||
- **BREAKING** Buyables renamed to Repeatables
|
||||
- Renamed purchaseLimit to limit
|
||||
- Renamed buyMax to maximize
|
||||
- Added initialAmount property
|
||||
- **BREAKING** Persistent refs no longer have redundancies in save object
|
||||
- Requires referencing persistent refs either through a proxy or by wrapping in `noPersist()`
|
||||
- **BREAKING** Visibility properties can now take booleans
|
||||
- Removed showIf util
|
||||
- Tweaked settings display
|
||||
- setupPassiveGeneration will no longer lower the resource
|
||||
- displayResource now floors resource amounts
|
||||
- Tweaked modifier displays, incl showing negative modifiers in red
|
||||
- Hotkeys now appear on key graphic
|
||||
- Mofifier sections now accept computable strings for title and subtitle
|
||||
- Updated b_e
|
||||
### Fixed
|
||||
- NaN detection stopped working
|
||||
- Now specifically only checks persistent refs
|
||||
- trackTotal would increase the total when loading the save
|
||||
- PWAs wouldn't show updates
|
||||
- Board feature no longer working at all
|
||||
- Some discord links didn't open in new tab
|
||||
- Adjacent grid cells wouldn't merge
|
||||
- When fixing old saves, the modVersion would not be updated
|
||||
- Default layer would display `Dev Speed: 0x` when paused
|
||||
- Fixed hotkeys not working with shift + numbers
|
||||
- Fixed console errors about deleted persistent refs not being included in the layer object
|
||||
- Modifiers wouldn't display small numbers
|
||||
- Conversions' addSoftcap wouldn't affect currentAt or nextAt
|
||||
- MainDisplay not respecting style and classes props
|
||||
- Tabs could sometimes not update correctly
|
||||
- offlineTime not capping properly
|
||||
- Tooltips being user-selectable
|
||||
- Workflows not working with submodules
|
||||
- Various minor typing issues
|
||||
### Documented
|
||||
- requirements.tsx
|
||||
- formulas.tsx
|
||||
- repeatables.tsx
|
||||
### Tests
|
||||
- requirements
|
||||
- formulas
|
||||
|
||||
Contributors: thepaperpilot, escapee, adsaf, ducdat
|
||||
|
||||
## [0.5.2] - 2022-08-22
|
||||
### Added
|
||||
- onLoad event
|
||||
- fontsLoaded event
|
||||
- Dismissable notification you can add to VueFeatures when they're interactable
|
||||
- Option on exponential modifiers to better support numbers less than 1
|
||||
- Utility function to track if a VueFeature is being hovered over
|
||||
- Utility to unwrap Resources that may be in refs
|
||||
- Utility to join JSX elements together with a joiner
|
||||
- Type for converting readonly string arrays into a union of string values
|
||||
### Changed
|
||||
- The main and prestige layers no longer use arrow functions for their options functions
|
||||
- Modifiers are now lazily loaded
|
||||
- Collapsible modifier sections are now lazily loaded
|
||||
- Converted several refs into shallow refs for improved performance
|
||||
- Roboto Mono and Material Icons fonts are now bundled instead of downloaded from the web, so they work with PWAs
|
||||
- Node bounds are now updated whenever that context has a node removed or added, fixing many issues with incorrect bounds
|
||||
### Fixed
|
||||
- trackResetTime not updating
|
||||
- colorText prepending $s
|
||||
- Default .replit config was broken
|
||||
- Pixi.js canvases no longer rendering
|
||||
- Node positions being shifted on initial page load due to fonts loading on firefox
|
||||
- Modifier sections looked wrong if the topmost section wasn't visible
|
||||
|
||||
## [0.5.1] - 2022-07-17
|
||||
### Added
|
||||
- Notif component that displays a jumping exclamation point
|
||||
|
|
|
@ -5,12 +5,6 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Kalam&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="alternate icon" type="image/png" sizes="48x48" href="/favicon.ico">
|
||||
|
|
6247
package-lock.json
generated
6247
package-lock.json
generated
File diff suppressed because it is too large
Load diff
22
package.json
22
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "profectus",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -8,12 +8,20 @@
|
|||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"testw": "vitest"
|
||||
"testw": "vitest",
|
||||
"serve": "vite preview --host"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi/app": "^6.4.2",
|
||||
"@pixi/core": "^6.4.2",
|
||||
"@pixi/particle-emitter": "^5.0.4",
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@pixi/app": "~6.3.2",
|
||||
"@pixi/constants": "~6.3.2",
|
||||
"@pixi/core": "~6.3.2",
|
||||
"@pixi/display": "~6.3.2",
|
||||
"@pixi/math": "~6.3.2",
|
||||
"@pixi/particle-emitter": "^5.0.7",
|
||||
"@pixi/sprite": "~6.3.2",
|
||||
"@pixi/ticker": "~6.3.2",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||
"is-plain-object": "^5.0.0",
|
||||
|
@ -24,7 +32,7 @@
|
|||
"vite-tsconfig-paths": "^3.5.0",
|
||||
"vue": "^3.2.26",
|
||||
"vue-next-select": "^2.10.2",
|
||||
"vue-panzoom": "^1.1.6",
|
||||
"vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
"vue-toastification": "^2.0.0-rc.1",
|
||||
"vue-transition-expand": "^0.1.0",
|
||||
|
@ -40,7 +48,7 @@
|
|||
"jsdom": "^20.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vitest": "^0.17.1",
|
||||
"vitest": "^0.29.3",
|
||||
"vue-tsc": "^0.38.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
@ -23,6 +23,7 @@ import projInfo from "./data/projInfo.json";
|
|||
import themes from "./data/themes";
|
||||
import settings, { gameComponents } from "./game/settings";
|
||||
import "./main.css";
|
||||
import "@fontsource/roboto-mono";
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
const useHeader = projInfo.useHeader;
|
||||
|
@ -30,7 +31,7 @@ const theme = computed(() => themes[settings.theme].variables as CSSProperties);
|
|||
const showTPS = toRef(settings, "showTPS");
|
||||
|
||||
const gameComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => <>{gameComponents.map(render)}</>));
|
||||
return coerceComponent(jsx(() => (<>{gameComponents.map(render)}</>)));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -48,5 +49,6 @@ const gameComponent = computed(() => {
|
|||
position: absolute;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
color: var(--foreground);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "game/layers";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { nextTick, onMounted, provide, ref } from "vue";
|
||||
import { globalBus } from "game/events";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "updateNodes", nodes: Record<string, FeatureNode | undefined>): void;
|
||||
|
@ -31,7 +32,7 @@ onMounted(() => {
|
|||
let isDirty = true;
|
||||
let boundingRect = ref(resizeListener.value?.getBoundingClientRect());
|
||||
function updateBounds() {
|
||||
if (resizeListener.value != null && isDirty) {
|
||||
if (isDirty) {
|
||||
isDirty = false;
|
||||
nextTick(() => {
|
||||
boundingRect.value = resizeListener.value?.getBoundingClientRect();
|
||||
|
@ -43,7 +44,7 @@ function updateBounds() {
|
|||
});
|
||||
}
|
||||
}
|
||||
document.fonts.ready.then(updateBounds);
|
||||
globalBus.on("fontsLoaded", updateBounds);
|
||||
|
||||
const observerOptions = {
|
||||
attributes: false,
|
||||
|
@ -55,13 +56,12 @@ provide(RegisterNodeInjectionKey, (id, element) => {
|
|||
const observer = new MutationObserver(() => updateNode(id));
|
||||
observer.observe(element, observerOptions);
|
||||
nodes.value[id] = { element, observer, rect: element.getBoundingClientRect() };
|
||||
emit("updateNodes", nodes.value);
|
||||
nextTick(() => updateNode(id));
|
||||
updateBounds();
|
||||
});
|
||||
provide(UnregisterNodeInjectionKey, id => {
|
||||
nodes.value[id]?.observer.disconnect();
|
||||
nodes.value[id] = undefined;
|
||||
emit("updateNodes", nodes.value);
|
||||
updateBounds();
|
||||
});
|
||||
provide(NodesInjectionKey, nodes);
|
||||
provide(BoundsInjectionKey, boundingRect);
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
class="tab"
|
||||
:ref="`tab-${index}`"
|
||||
:style="unref(layers[tab]?.style)"
|
||||
:class="unref(layers[tab]?.classes)"
|
||||
>
|
||||
|
@ -14,7 +13,7 @@
|
|||
v-if="layerKeys.includes(tab)"
|
||||
v-bind="gatherLayerProps(layers[tab]!)"
|
||||
:index="index"
|
||||
:tab="() => (($refs[`tab-${index}`] as HTMLElement[] | undefined)?.[0])"
|
||||
@set-minimized="value => (layers[tab]!.minimized.value = value)"
|
||||
/>
|
||||
<component :is="tab" :index="index" v-else />
|
||||
</div>
|
||||
|
@ -36,8 +35,8 @@ const layerKeys = computed(() => Object.keys(layers));
|
|||
const useHeader = projInfo.useHeader;
|
||||
|
||||
function gatherLayerProps(layer: GenericLayer) {
|
||||
const { display, minimized, minWidth, name, color, minimizable, nodes } = layer;
|
||||
return { display, minimized, minWidth, name, color, minimizable, nodes };
|
||||
const { display, minimized, name, color, minimizable, nodes, minimizedDisplay } = layer;
|
||||
return { display, minimized, name, color, minimizable, nodes, minimizedDisplay };
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
70
src/components/Hotkey.vue
Normal file
70
src/components/Hotkey.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<!-- Make eslint not whine about putting spaces before the +'s -->
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<template>
|
||||
<template v-if="isCtrl"
|
||||
><div class="key">Ctrl</div
|
||||
>+</template
|
||||
><template v-if="isShift"
|
||||
><div class="key">Shift</div
|
||||
>+</template
|
||||
>
|
||||
<div class="key">{{ key }}</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GenericHotkey } from "features/hotkey";
|
||||
import { watchEffect } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
hotkey: GenericHotkey;
|
||||
}>();
|
||||
|
||||
let key = "";
|
||||
let isCtrl = false;
|
||||
let isShift = false;
|
||||
let isAlpha = false;
|
||||
watchEffect(() => {
|
||||
key = props.hotkey.key;
|
||||
|
||||
isCtrl = key.startsWith("ctrl+");
|
||||
if (isCtrl) {
|
||||
key = key.slice(5);
|
||||
}
|
||||
|
||||
isShift = key.startsWith("shift+");
|
||||
if (isShift) {
|
||||
key = key.slice(6);
|
||||
}
|
||||
|
||||
isAlpha = key.length == 1 && key.toLowerCase() != key.toUpperCase();
|
||||
if (isAlpha) {
|
||||
key = key.toUpperCase();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.key {
|
||||
display: inline-block;
|
||||
height: 1.4em;
|
||||
min-width: 1em;
|
||||
margin-block: 0.1em;
|
||||
padding-inline: 0.2em;
|
||||
vertical-align: 0.1em;
|
||||
|
||||
background: var(--foreground);
|
||||
color: var(--feature-foreground);
|
||||
border: 1px solid #0007;
|
||||
border-radius: 0.3em;
|
||||
box-shadow: 0 0.1em #0007, 0 0.1em var(--foreground);
|
||||
|
||||
font-size: smaller;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
transition: transform 0s, box-shadow 0s;
|
||||
}
|
||||
.key:active {
|
||||
transform: translateY(0.1em);
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
|
@ -21,19 +21,32 @@
|
|||
<div class="link" @click="openChangelog">Changelog</div>
|
||||
<br />
|
||||
<div>
|
||||
<a :href="discordLink" v-if="discordLink" class="info-modal-discord-link">
|
||||
<a
|
||||
:href="discordLink"
|
||||
v-if="discordLink"
|
||||
class="info-modal-discord-link"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="material-icons info-modal-discord">discord</span>
|
||||
{{ discordName }}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://discord.gg/WzejVAx" class="info-modal-discord-link">
|
||||
<a
|
||||
href="https://discord.gg/WzejVAx"
|
||||
class="info-modal-discord-link"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="material-icons info-modal-discord">discord</span>
|
||||
The Paper Pilot Community
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://discord.gg/F3xveHV" class="info-modal-discord-link">
|
||||
<a
|
||||
href="https://discord.gg/F3xveHV"
|
||||
class="info-modal-discord-link"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="material-icons info-modal-discord">discord</span>
|
||||
The Modding Tree
|
||||
</a>
|
||||
|
@ -67,7 +80,7 @@ const isOpen = ref(false);
|
|||
const timePlayed = computed(() => formatTime(player.timePlayed));
|
||||
|
||||
const infoComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => <>{infoComponents.map(render)}</>));
|
||||
return coerceComponent(jsx(() => (<>{infoComponents.map(render)}</>)));
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
<template>
|
||||
<div class="layer-container" :style="{ '--layer-color': unref(color) }">
|
||||
<button v-if="showGoBack" class="goBack" @click="goBack">←</button>
|
||||
<button class="layer-tab minimized" v-if="minimized.value" @click="minimized.value = false">
|
||||
<div>{{ unref(name) }}</div>
|
||||
<button v-if="showGoBack" class="goBack" @click="goBack">❌</button>
|
||||
|
||||
<button
|
||||
class="layer-tab minimized"
|
||||
v-if="unref(minimized)"
|
||||
@click="$emit('setMinimized', false)"
|
||||
>
|
||||
<component v-if="minimizedComponent" :is="minimizedComponent" />
|
||||
<div v-else>{{ unref(name) }}</div>
|
||||
</button>
|
||||
<div class="layer-tab" :class="{ showGoBack }" v-else>
|
||||
<Context @update-nodes="updateNodes">
|
||||
<component :is="component" />
|
||||
</Context>
|
||||
</div>
|
||||
<button v-if="unref(minimizable)" class="minimize" @click="minimized.value = true">
|
||||
|
||||
<button v-if="unref(minimizable)" class="minimize" @click="$emit('setMinimized', true)">
|
||||
▼
|
||||
</button>
|
||||
</div>
|
||||
|
@ -19,11 +26,10 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import { computeComponent, processedPropType, wrapRef } from "util/vue";
|
||||
import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
||||
import type { PropType, Ref } from "vue";
|
||||
import { computed, defineComponent, nextTick, toRefs, unref, watch } from "vue";
|
||||
import { computed, defineComponent, toRefs, unref } from "vue";
|
||||
import Context from "./Context.vue";
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -33,20 +39,13 @@ export default defineComponent({
|
|||
type: Number,
|
||||
required: true
|
||||
},
|
||||
tab: {
|
||||
type: Function as PropType<() => HTMLElement | undefined>,
|
||||
required: true
|
||||
},
|
||||
display: {
|
||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
||||
required: true
|
||||
},
|
||||
minimizedDisplay: processedPropType<CoercableComponent>(Object, String, Function),
|
||||
minimized: {
|
||||
type: Object as PropType<Persistent<boolean>>,
|
||||
required: true
|
||||
},
|
||||
minWidth: {
|
||||
type: processedPropType<number | string>(Number, String),
|
||||
type: Object as PropType<Ref<boolean>>,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
|
@ -60,52 +59,31 @@ export default defineComponent({
|
|||
required: true
|
||||
}
|
||||
},
|
||||
emits: ["setMinimized"],
|
||||
setup(props) {
|
||||
const { display, index, minimized, minWidth, tab } = toRefs(props);
|
||||
const { display, index, minimized, minimizedDisplay } = toRefs(props);
|
||||
|
||||
const component = computeComponent(display);
|
||||
const minimizedComponent = computeOptionalComponent(minimizedDisplay);
|
||||
const showGoBack = computed(
|
||||
() => projInfo.allowGoBack && index.value > 0 && !minimized.value
|
||||
() => projInfo.allowGoBack && index.value > 0 && !unwrapRef(minimized)
|
||||
);
|
||||
|
||||
function goBack() {
|
||||
player.tabs.splice(unref(props.index), Infinity);
|
||||
}
|
||||
|
||||
nextTick(() => updateTab(minimized.value, unref(minWidth.value)));
|
||||
watch([minimized, wrapRef(minWidth)], ([minimized, minWidth]) =>
|
||||
updateTab(minimized, minWidth)
|
||||
);
|
||||
function setMinimized(min: boolean) {
|
||||
minimized.value = min;
|
||||
}
|
||||
|
||||
function updateNodes(nodes: Record<string, FeatureNode | undefined>) {
|
||||
props.nodes.value = nodes;
|
||||
}
|
||||
|
||||
function updateTab(minimized: boolean, minWidth: number | string) {
|
||||
const width =
|
||||
typeof minWidth === "number" || Number.isNaN(parseInt(minWidth))
|
||||
? minWidth + "px"
|
||||
: minWidth;
|
||||
const tabValue = tab.value();
|
||||
if (tabValue != undefined) {
|
||||
if (minimized) {
|
||||
tabValue.style.flexGrow = "0";
|
||||
tabValue.style.flexShrink = "0";
|
||||
tabValue.style.width = "60px";
|
||||
tabValue.style.minWidth = tabValue.style.flexBasis = "";
|
||||
tabValue.style.margin = "0";
|
||||
} else {
|
||||
tabValue.style.flexGrow = "";
|
||||
tabValue.style.flexShrink = "";
|
||||
tabValue.style.width = "";
|
||||
tabValue.style.minWidth = tabValue.style.flexBasis = width;
|
||||
tabValue.style.margin = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
component,
|
||||
minimizedComponent,
|
||||
showGoBack,
|
||||
updateNodes,
|
||||
unref,
|
||||
|
@ -155,9 +133,10 @@ export default defineComponent({
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
.layer-tab.minimized div {
|
||||
.layer-tab.minimized > * {
|
||||
margin: 0;
|
||||
writing-mode: vertical-rl;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
width: 50px;
|
||||
}
|
||||
|
@ -201,8 +180,8 @@ export default defineComponent({
|
|||
|
||||
.goBack {
|
||||
position: sticky;
|
||||
top: 6px;
|
||||
left: 20px;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
line-height: 30px;
|
||||
margin-top: -50px;
|
||||
margin-left: -35px;
|
||||
|
@ -211,7 +190,7 @@ export default defineComponent({
|
|||
box-shadow: var(--background) 0 2px 3px 5px;
|
||||
border-radius: 50%;
|
||||
color: var(--foreground);
|
||||
font-size: 40px;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 7;
|
||||
}
|
||||
|
@ -221,3 +200,10 @@ export default defineComponent({
|
|||
text-shadow: 0 0 7px var(--foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.layer-tab.minimized > * > .desc {
|
||||
color: var(--accent1);
|
||||
font-size: 30px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { computed, ref, toRefs } from "vue";
|
||||
import { computed, ref, toRefs, unref } from "vue";
|
||||
import Context from "./Context.vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
|
@ -51,7 +51,7 @@ const emit = defineEmits<{
|
|||
(e: "update:modelValue", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const isOpen = computed(() => props.modelValue || isAnimating.value);
|
||||
const isOpen = computed(() => unref(props.modelValue) || isAnimating.value);
|
||||
function close() {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
|
|
|
@ -14,9 +14,12 @@
|
|||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<a :href="discordLink" class="nan-modal-discord-link">
|
||||
<a
|
||||
:href="discordLink || 'https://discord.gg/WzejVAx'"
|
||||
class="nan-modal-discord-link"
|
||||
>
|
||||
<span class="material-icons nan-modal-discord">discord</span>
|
||||
{{ discordName }}
|
||||
{{ discordName || "The Paper Pilot Community" }}
|
||||
</a>
|
||||
</div>
|
||||
<br />
|
||||
|
@ -50,49 +53,51 @@ import state from "game/state";
|
|||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format } from "util/bignum";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { computed, ref, toRef } from "vue";
|
||||
import { computed, ref, toRef, watch } from "vue";
|
||||
import Toggle from "./fields/Toggle.vue";
|
||||
import SavesManager from "./SavesManager.vue";
|
||||
|
||||
const { discordName, discordLink } = projInfo;
|
||||
const autosave = toRef(player, "autosave");
|
||||
const autosave = ref(true);
|
||||
const isPaused = ref(true);
|
||||
const hasNaN = toRef(state, "hasNaN");
|
||||
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
||||
|
||||
const path = computed(() => state.NaNPath?.join("."));
|
||||
const property = computed(() => state.NaNPath?.slice(-1)[0]);
|
||||
const previous = computed<DecimalSource | null>(() => {
|
||||
if (state.NaNReceiver && property.value) {
|
||||
return state.NaNReceiver[property.value] as DecimalSource;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const isPaused = computed({
|
||||
get() {
|
||||
return player.devSpeed === 0;
|
||||
},
|
||||
set(value: boolean) {
|
||||
player.devSpeed = value ? null : 0;
|
||||
watch(hasNaN, hasNaN => {
|
||||
if (hasNaN) {
|
||||
autosave.value = player.autosave;
|
||||
isPaused.value = player.devSpeed === 0;
|
||||
} else {
|
||||
player.autosave = autosave.value;
|
||||
player.devSpeed = isPaused.value ? 0 : null;
|
||||
}
|
||||
});
|
||||
|
||||
const path = computed(() => state.NaNPath?.join("."));
|
||||
const previous = computed<DecimalSource | null>(() => {
|
||||
if (state.NaNPersistent != null) {
|
||||
return state.NaNPersistent.value;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function setZero() {
|
||||
if (state.NaNReceiver && property.value) {
|
||||
state.NaNReceiver[property.value] = new Decimal(0);
|
||||
if (state.NaNPersistent != null) {
|
||||
state.NaNPersistent.value = new Decimal(0);
|
||||
state.hasNaN = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setOne() {
|
||||
if (state.NaNReceiver && property.value) {
|
||||
state.NaNReceiver[property.value] = new Decimal(1);
|
||||
if (state.NaNPersistent) {
|
||||
state.NaNPersistent.value = new Decimal(1);
|
||||
state.hasNaN = false;
|
||||
}
|
||||
}
|
||||
|
||||
function ignore() {
|
||||
if (state.NaNReceiver && property.value) {
|
||||
state.NaNReceiver[property.value] = new Decimal(NaN);
|
||||
if (state.NaNPersistent) {
|
||||
state.NaNPersistent.value = new Decimal(NaN);
|
||||
state.hasNaN = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
</Tooltip>
|
||||
</div>
|
||||
<div @click="options?.open()">
|
||||
<Tooltip display="Options" :direction="Direction.Down" xoffset="-66px">
|
||||
<Tooltip display="Settings" :direction="Direction.Down" xoffset="-66px">
|
||||
<span class="material-icons">settings</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@
|
|||
</Tooltip>
|
||||
</div>
|
||||
<div @click="options?.open()">
|
||||
<Tooltip display="Options" :direction="Direction.Right">
|
||||
<Tooltip display="Settings" :direction="Direction.Right">
|
||||
<span class="material-icons">settings</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { RegisterNodeInjectionKey, UnregisterNodeInjectionKey } from "game/layers";
|
||||
import { computed, inject, onUnmounted, ref, toRefs, unref, watch } from "vue";
|
||||
import { computed, inject, onUnmounted, shallowRef, toRefs, unref, watch } from "vue";
|
||||
|
||||
const _props = defineProps<{ id: string }>();
|
||||
const props = toRefs(_props);
|
||||
|
@ -14,7 +14,7 @@ const register = inject(RegisterNodeInjectionKey, () => {});
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const unregister = inject(UnregisterNodeInjectionKey, () => {});
|
||||
|
||||
const node = ref<HTMLElement | null>(null);
|
||||
const node = shallowRef<HTMLElement | null>(null);
|
||||
const parentNode = computed(() => node.value && node.value.parentElement);
|
||||
|
||||
watch([parentNode, props.id], ([newNode, newID], [prevNode, prevID]) => {
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
left: 5px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: var(--accent3);
|
||||
font-size: x-large;
|
||||
animation: 1s linear infinite bounce;
|
||||
|
|
|
@ -2,18 +2,27 @@
|
|||
<Modal v-model="isOpen">
|
||||
<template v-slot:header>
|
||||
<div class="header">
|
||||
<h2>Options</h2>
|
||||
<h2>Settings</h2>
|
||||
<div class="option-tabs">
|
||||
<button :class="{selected: isTab('behaviour')}" @click="setTab('behaviour')">Behaviour</button>
|
||||
<button :class="{selected: isTab('appearance')}" @click="setTab('appearance')">Appearance</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<Select title="Theme" :options="themes" v-model="theme" />
|
||||
<component :is="settingFieldsComponent" />
|
||||
<Toggle title="Show TPS" v-model="showTPS" />
|
||||
<hr />
|
||||
<Toggle title="Unthrottled" v-model="unthrottled" />
|
||||
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
|
||||
<Toggle :title="autosaveTitle" v-model="autosave" />
|
||||
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
|
||||
<div v-if="isTab('behaviour')">
|
||||
<Toggle :title="unthrottledTitle" v-model="unthrottled" />
|
||||
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
|
||||
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
|
||||
<Toggle :title="autosaveTitle" v-model="autosave" />
|
||||
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
|
||||
</div>
|
||||
<div v-if="isTab('appearance')">
|
||||
<Select :title="themeTitle" :options="themes" v-model="theme" />
|
||||
<component :is="settingFieldsComponent" />
|
||||
<Toggle :title="showTPSTitle" v-model="showTPS" />
|
||||
<Toggle :title="alignModifierUnitsTitle" v-model="alignUnits" />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
@ -21,20 +30,34 @@
|
|||
<script setup lang="tsx">
|
||||
import Modal from "components/Modal.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import { save } from "util/save";
|
||||
import rawThemes from "data/themes";
|
||||
import { jsx } from "features/feature";
|
||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import player from "game/player";
|
||||
import settings, { settingFields } from "game/settings";
|
||||
import { camelToTitle } from "util/common";
|
||||
import { camelToTitle, Direction } from "util/common";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import { computed, ref, toRefs } from "vue";
|
||||
import Select from "./fields/Select.vue";
|
||||
import Toggle from "./fields/Toggle.vue";
|
||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
const currentTab = ref("behaviour");
|
||||
|
||||
function isTab(tab: string): boolean {
|
||||
return tab == currentTab.value;
|
||||
}
|
||||
|
||||
function setTab(tab: string) {
|
||||
currentTab.value = tab;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
isTab,
|
||||
setTab,
|
||||
save,
|
||||
open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
|
@ -46,10 +69,10 @@ const themes = Object.keys(rawThemes).map(theme => ({
|
|||
}));
|
||||
|
||||
const settingFieldsComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => <>{settingFields.map(render)}</>));
|
||||
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
|
||||
});
|
||||
|
||||
const { showTPS, theme, unthrottled } = toRefs(settings);
|
||||
const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings);
|
||||
const { autosave, offlineProd } = toRefs(player);
|
||||
const isPaused = computed({
|
||||
get() {
|
||||
|
@ -60,30 +83,85 @@ const isPaused = computed({
|
|||
}
|
||||
});
|
||||
|
||||
const unthrottledTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Unthrottled
|
||||
<desc>Allow the game to run as fast as possible. Not battery friendly.</desc>
|
||||
</span>
|
||||
));
|
||||
const offlineProdTitle = jsx(() => (
|
||||
<span>
|
||||
Offline Production<Tooltip display="Save-specific">*</Tooltip>
|
||||
<span class="option-title">
|
||||
Offline Production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Simulate production that occurs while the game is closed.</desc>
|
||||
</span>
|
||||
));
|
||||
const autosaveTitle = jsx(() => (
|
||||
<span>
|
||||
Autosave<Tooltip display="Save-specific">*</Tooltip>
|
||||
<span class="option-title">
|
||||
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Automatically save the game every second or when the game is closed.</desc>
|
||||
</span>
|
||||
));
|
||||
const isPausedTitle = jsx(() => (
|
||||
<span>
|
||||
Pause game<Tooltip display="Save-specific">*</Tooltip>
|
||||
<span class="option-title">
|
||||
Pause game<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Stop everything from moving.</desc>
|
||||
</span>
|
||||
));
|
||||
const themeTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Theme
|
||||
<desc>How the game looks.</desc>
|
||||
</span>
|
||||
));
|
||||
const showTPSTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Show TPS
|
||||
<desc>Show TPS meter at the bottom-left corner of the page.</desc>
|
||||
</span>
|
||||
));
|
||||
const alignModifierUnitsTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Align modifier units
|
||||
<desc>Align numbers to the beginning of the unit in modifier view.</desc>
|
||||
</span>
|
||||
));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
<style>
|
||||
.option-tabs {
|
||||
border-bottom: 2px solid var(--outline);
|
||||
margin-top: 10px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
*:deep() .tooltip-container {
|
||||
.option-tabs button {
|
||||
background-color: transparent;
|
||||
color: var(--foreground);
|
||||
margin-bottom: -2px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 5px 20px;
|
||||
border: none;
|
||||
border-bottom: 2px solid var(--foreground);
|
||||
}
|
||||
|
||||
.option-tabs button:not(.selected) {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.option-title .tooltip-container {
|
||||
display: inline;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.option-title desc {
|
||||
display: block;
|
||||
opacity: 0.6;
|
||||
font-size: small;
|
||||
width: 300px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -104,11 +104,11 @@ const isEditing = ref(false);
|
|||
const isConfirming = ref(false);
|
||||
const newName = ref("");
|
||||
|
||||
watch(isEditing, () => (newName.value = save.value.name || ""));
|
||||
watch(isEditing, () => (newName.value = save.value.name ?? ""));
|
||||
|
||||
const isActive = computed(() => save.value && save.value.id === player.id);
|
||||
const isActive = computed(() => save.value != null && save.value.id === player.id);
|
||||
const currentTime = computed(() =>
|
||||
isActive.value ? player.time : (save.value && save.value.time) || 0
|
||||
isActive.value ? player.time : (save.value != null && save.value.time) ?? 0
|
||||
);
|
||||
|
||||
function changeName() {
|
||||
|
|
|
@ -59,11 +59,10 @@
|
|||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import type { PlayerData } from "game/player";
|
||||
import type { Player } from "game/player";
|
||||
import player, { stringifySave } from "game/player";
|
||||
import settings from "game/settings";
|
||||
import LZString from "lz-string";
|
||||
import { ProxyState } from "util/proxies";
|
||||
import { getUniqueID, loadSave, newSave, save } from "util/save";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { computed, nextTick, ref, shallowReactive, watch } from "vue";
|
||||
|
@ -72,7 +71,7 @@ import Select from "./fields/Select.vue";
|
|||
import Text from "./fields/Text.vue";
|
||||
import Save from "./Save.vue";
|
||||
|
||||
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
|
||||
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
|
||||
|
||||
const isOpen = ref(false);
|
||||
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null);
|
||||
|
@ -195,7 +194,7 @@ const saves = computed(() =>
|
|||
function exportSave(id: string) {
|
||||
let saveToExport;
|
||||
if (player.id === id) {
|
||||
saveToExport = stringifySave(player[ProxyState]);
|
||||
saveToExport = stringifySave(player);
|
||||
} else {
|
||||
saveToExport = JSON.stringify(saves.value[id]);
|
||||
}
|
||||
|
@ -228,7 +227,7 @@ function duplicateSave(id: string) {
|
|||
}
|
||||
|
||||
const playerData = { ...saves.value[id], id: getUniqueID() };
|
||||
save(playerData as PlayerData);
|
||||
save(playerData as Player);
|
||||
|
||||
settings.saves.push(playerData.id);
|
||||
}
|
||||
|
@ -272,7 +271,7 @@ function newFromPreset(preset: string) {
|
|||
}
|
||||
const playerData = JSON.parse(preset);
|
||||
playerData.id = getUniqueID();
|
||||
save(playerData as PlayerData);
|
||||
save(playerData as Player);
|
||||
|
||||
settings.saves.push(playerData.id);
|
||||
|
||||
|
@ -281,13 +280,13 @@ function newFromPreset(preset: string) {
|
|||
|
||||
function editSave(id: string, newName: string) {
|
||||
const currSave = saves.value[id];
|
||||
if (currSave) {
|
||||
if (currSave != null) {
|
||||
currSave.name = newName;
|
||||
if (player.id === id) {
|
||||
player.name = newName;
|
||||
save();
|
||||
} else {
|
||||
save(currSave as PlayerData);
|
||||
save(currSave as Player);
|
||||
cachedSaves[id] = undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.modifier-container {
|
||||
display: flex;
|
||||
padding: 1px 8px;
|
||||
}
|
||||
|
||||
.modifier-container:nth-child(2n) {
|
||||
|
@ -7,8 +8,12 @@
|
|||
}
|
||||
|
||||
.modifier-amount {
|
||||
flex-basis: 100px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
:not(:first-of-type, :last-of-type) > .modifier-amount::after {
|
||||
content: var(--unit);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modifier-description {
|
||||
|
|
|
@ -56,6 +56,43 @@
|
|||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.row-grid.mergeAdjacent > .feature:not(.dontMerge),
|
||||
.row-grid.mergeAdjacent > .tooltip-container > .feature:not(.dontMerge) {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.row-grid.mergeAdjacent > .feature:not(.dontMerge):last-child,
|
||||
.row-grid.mergeAdjacent > .tooltip-container:last-child > .feature:not(.dontMerge) {
|
||||
border-radius: 0 0 0 0;
|
||||
}
|
||||
|
||||
|
||||
.row-grid.mergeAdjacent > .feature:not(.dontMerge):first-child,
|
||||
.row-grid.mergeAdjacent > .tooltip-container:first-child > .feature:not(.dontMerge) {
|
||||
border-radius: 0 0 0 0;
|
||||
}
|
||||
|
||||
.table-grid > .row-grid.mergeAdjacent:last-child > .feature:not(.dontMerge):first-child {
|
||||
border-radius: 0 0 0 var(--border-radius);
|
||||
}
|
||||
|
||||
.table-grid > .row-grid.mergeAdjacent:first-child > .feature:not(.dontMerge):last-child {
|
||||
border-radius: 0 var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.table-grid > .row-grid.mergeAdjacent:first-child > .feature:not(.dontMerge):first-child {
|
||||
border-radius: var(--border-radius) 0 0 0;
|
||||
}
|
||||
|
||||
.table-grid > .row-grid.mergeAdjacent:last-child > .feature:not(.dontMerge):last-child {
|
||||
border-radius: 0 0 var(--border-radius) 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
TODO how to implement mergeAdjacent for grids?
|
||||
.row.mergeAdjacent + .row.mergeAdjacent > .feature:not(.dontMerge) {
|
||||
|
|
|
@ -87,6 +87,10 @@ function onUpdate(value: SelectOption) {
|
|||
background-color: var(--bought);
|
||||
}
|
||||
|
||||
.vue-input input {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.vue-input input::placeholder {
|
||||
color: var(--link);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ const emit = defineEmits<{
|
|||
|
||||
const value = computed({
|
||||
get() {
|
||||
return String(unref(props.modelValue) || 0);
|
||||
return String(unref(props.modelValue) ?? 0);
|
||||
},
|
||||
set(value: string) {
|
||||
emit("update:modelValue", Number(value));
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
:maxHeight="maxHeight"
|
||||
@blur="submit"
|
||||
@blur="blur"
|
||||
ref="field"
|
||||
/>
|
||||
<input
|
||||
|
@ -18,7 +18,7 @@
|
|||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
:class="{ fullWidth: !title }"
|
||||
@blur="submit"
|
||||
@blur="blur"
|
||||
ref="field"
|
||||
/>
|
||||
</div>
|
||||
|
@ -28,35 +28,34 @@
|
|||
<script setup lang="ts">
|
||||
import "components/common/fields.css";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { coerceComponent } from "util/vue";
|
||||
import { computed, onMounted, ref, toRefs, unref } from "vue";
|
||||
import { computeOptionalComponent } from "util/vue";
|
||||
import { computed, onMounted, shallowRef, toRef, unref } from "vue";
|
||||
import VueTextareaAutosize from "vue-textarea-autosize";
|
||||
|
||||
const _props = defineProps<{
|
||||
const props = defineProps<{
|
||||
title?: CoercableComponent;
|
||||
modelValue?: string;
|
||||
textArea?: boolean;
|
||||
placeholder?: string;
|
||||
maxHeight?: number;
|
||||
submitOnBlur?: boolean;
|
||||
}>();
|
||||
const props = toRefs(_props);
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
(e: "submit"): void;
|
||||
(e: "cancel"): void;
|
||||
}>();
|
||||
|
||||
const titleComponent = computed(
|
||||
() => props.title?.value && coerceComponent(unref(props.title.value), "span")
|
||||
);
|
||||
const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
|
||||
|
||||
const field = ref<HTMLElement | null>(null);
|
||||
const field = shallowRef<HTMLElement | null>(null);
|
||||
onMounted(() => {
|
||||
field.value?.focus();
|
||||
});
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return unref(props.modelValue) || "";
|
||||
return unref(props.modelValue) ?? "";
|
||||
},
|
||||
set(value: string) {
|
||||
emit("update:modelValue", value);
|
||||
|
@ -66,6 +65,14 @@ const value = computed({
|
|||
function submit() {
|
||||
emit("submit");
|
||||
}
|
||||
|
||||
function blur() {
|
||||
if (props.submitOnBlur !== false) {
|
||||
emit("submit");
|
||||
} else {
|
||||
emit("cancel");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -19,7 +19,7 @@ const emit = defineEmits<{
|
|||
(e: "update:modelValue", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const component = computed(() => coerceComponent(unref(props.title) || "<span></span>", "span"));
|
||||
const component = computed(() => coerceComponent(unref(props.title) ?? "<span></span>", "span"));
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
|
@ -43,14 +43,16 @@ input {
|
|||
|
||||
span {
|
||||
width: 100%;
|
||||
padding-right: 41px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* track */
|
||||
input + span::before {
|
||||
content: "";
|
||||
float: right;
|
||||
margin: 5px 0 5px 10px;
|
||||
position: absolute;
|
||||
top: calc(50% - 7px);
|
||||
right: 0px;
|
||||
border-radius: 7px;
|
||||
width: 36px;
|
||||
height: 14px;
|
||||
|
@ -64,7 +66,7 @@ input + span::before {
|
|||
input + span::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
top: calc(50% - 10px);
|
||||
right: 16px;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
|
|
|
@ -38,6 +38,7 @@ const contentComponent = computeComponent(toRef(props, "content"));
|
|||
padding: var(--feature-margin);
|
||||
color: var(--foreground);
|
||||
cursor: pointer;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.collapsible-toggle:last-child {
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { nextTick, onMounted, ref, shallowRef } from "vue";
|
||||
|
||||
const top = ref("0");
|
||||
const observer = new ResizeObserver(updateTop);
|
||||
const element = ref<HTMLElement | null>(null);
|
||||
const element = shallowRef<HTMLElement | null>(null);
|
||||
|
||||
function updateTop() {
|
||||
let el = element.value;
|
||||
|
|
1
src/components/math/Fraction.vue
Normal file
1
src/components/math/Fraction.vue
Normal file
|
@ -0,0 +1 @@
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<span style="white-space: nowrap">
|
||||
<span style="font-size: larger; font-family: initial">√</span
|
||||
><span style="text-decoration: overline"><slot /></span>
|
||||
<span style="font-size: larger; font-family: initial">√</span>
|
||||
<div style="display: inline-block; border-top: 1px solid; padding-left: 0.2em">
|
||||
<slot />
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import Collapsible from "components/layout/Collapsible.vue";
|
||||
import type { Clickable, ClickableOptions, GenericClickable } from "features/clickables/clickable";
|
||||
import { createClickable } from "features/clickables/clickable";
|
||||
import type { GenericConversion } from "features/conversion";
|
||||
import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature";
|
||||
import { jsx, setDefault } from "features/feature";
|
||||
import { displayResource } from "features/resources/resource";
|
||||
import { GenericMilestone } from "features/milestones/milestone";
|
||||
import { displayResource, Resource } from "features/resources/resource";
|
||||
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
||||
import { createTreeNode } from "features/trees/tree";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import type { FormulaSource, GenericFormula } from "game/formulas/types";
|
||||
import type { Modifier } from "game/modifiers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { DefaultValue, persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import settings from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format } from "util/bignum";
|
||||
import Decimal, { format, formatSmall, formatTime } from "util/bignum";
|
||||
import type { WithRequired } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
|
@ -20,9 +25,8 @@ import type {
|
|||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { convertComputable, processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { renderJSX } from "util/vue";
|
||||
import type { Ref } from "vue";
|
||||
import { getFirstFeature, renderColJSX, renderJSX } from "util/vue";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import "./common.css";
|
||||
|
||||
|
@ -72,7 +76,7 @@ export type ResetButton<T extends ResetButtonOptions> = Replace<
|
|||
display: GetComputableTypeWithDefault<T["display"], Ref<JSX.Element>>;
|
||||
canClick: GetComputableTypeWithDefault<T["canClick"], Ref<boolean>>;
|
||||
minimumGain: GetComputableTypeWithDefault<T["minimumGain"], 1>;
|
||||
onClick: VoidFunction;
|
||||
onClick: (event?: MouseEvent | TouchEvent) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -124,7 +128,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
)}
|
||||
</b>{" "}
|
||||
{resetButton.conversion.gainResource.displayName}
|
||||
{unref(resetButton.showNextAt) ? (
|
||||
{unref(resetButton.showNextAt) != null ? (
|
||||
<div>
|
||||
<br />
|
||||
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
||||
|
@ -152,8 +156,8 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
}
|
||||
|
||||
const onClick = resetButton.onClick;
|
||||
resetButton.onClick = function () {
|
||||
if (!unref(resetButton.canClick)) {
|
||||
resetButton.onClick = function (event?: MouseEvent | TouchEvent) {
|
||||
if (unref(resetButton.canClick) === false) {
|
||||
return;
|
||||
}
|
||||
resetButton.conversion.convert();
|
||||
|
@ -161,7 +165,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
if (resetButton.resetTime) {
|
||||
resetButton.resetTime.value = resetButton.resetTime[DefaultValue];
|
||||
}
|
||||
onClick?.();
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
return resetButton;
|
||||
|
@ -236,9 +240,9 @@ export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
|
|||
/** An option object for a modifier display as a single section. **/
|
||||
export interface Section {
|
||||
/** The header for this modifier. **/
|
||||
title: string;
|
||||
title: Computable<string>;
|
||||
/** A subtitle for this modifier, e.g. to explain the context for the modifier. **/
|
||||
subtitle?: string;
|
||||
subtitle?: Computable<string>;
|
||||
/** The modifier to be displaying in this section. **/
|
||||
modifier: WithRequired<Modifier, "description">;
|
||||
/** The base value being modified. **/
|
||||
|
@ -255,66 +259,119 @@ export interface Section {
|
|||
* Takes an array of modifier "sections", and creates a JSXFunction that can render all those sections, and allow each section to be collapsed.
|
||||
* Also returns a list of persistent refs that are used to control which sections are currently collapsed.
|
||||
* @param sectionsFunc A function that returns the sections to display.
|
||||
* @param smallerIsBetter Determines whether numbers larger or smaller than the base should be displayed as red.
|
||||
*/
|
||||
export function createCollapsibleModifierSections(
|
||||
sectionsFunc: () => Section[]
|
||||
): [JSXFunction, Persistent<boolean>[]] {
|
||||
return createLazyProxy(() => {
|
||||
const sections = sectionsFunc();
|
||||
sectionsFunc: () => Section[],
|
||||
smallerIsBetter = false
|
||||
): [JSXFunction, Persistent<Record<number, boolean>>] {
|
||||
const sections: Section[] = [];
|
||||
const processed:
|
||||
| {
|
||||
base: ProcessedComputable<DecimalSource | undefined>[];
|
||||
baseText: ProcessedComputable<CoercableComponent | undefined>[];
|
||||
visible: ProcessedComputable<boolean | undefined>[];
|
||||
title: ProcessedComputable<string | undefined>[];
|
||||
subtitle: ProcessedComputable<string | undefined>[];
|
||||
}
|
||||
| Record<string, never> = {};
|
||||
let calculated = false;
|
||||
function calculateSections() {
|
||||
if (!calculated) {
|
||||
sections.push(...sectionsFunc());
|
||||
processed.base = sections.map(s => convertComputable(s.base));
|
||||
processed.baseText = sections.map(s => convertComputable(s.baseText));
|
||||
processed.visible = sections.map(s => convertComputable(s.visible));
|
||||
processed.title = sections.map(s => convertComputable(s.title));
|
||||
processed.subtitle = sections.map(s => convertComputable(s.subtitle));
|
||||
calculated = true;
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
const processedBase = sections.map(s => convertComputable(s.base));
|
||||
const processedBaseText = sections.map(s => convertComputable(s.baseText));
|
||||
const processedVisible = sections.map(s => convertComputable(s.visible));
|
||||
const collapsed = sections.map(() => persistent<boolean>(false));
|
||||
const jsxFunc = jsx(() => {
|
||||
const sectionJSX = sections.map((s, i) => {
|
||||
if (unref(processedVisible[i]) === false) return null;
|
||||
const header = (
|
||||
<h3
|
||||
onClick={() => (collapsed[i].value = !collapsed[i].value)}
|
||||
style="cursor: pointer"
|
||||
const collapsed = persistent<Record<number, boolean>>({}, false);
|
||||
const jsxFunc = jsx(() => {
|
||||
const sections = calculateSections();
|
||||
|
||||
let firstVisibleSection = true;
|
||||
const sectionJSX = sections.map((s, i) => {
|
||||
if (unref(processed.visible[i]) === false) return null;
|
||||
const header = (
|
||||
<h3
|
||||
onClick={() => (collapsed.value[i] = !collapsed.value[i])}
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<span
|
||||
class={"modifier-toggle" + (unref(collapsed.value[i]) ? " collapsed" : "")}
|
||||
>
|
||||
<span class={"modifier-toggle" + (unref(collapsed[i]) ? " collapsed" : "")}>
|
||||
▼
|
||||
</span>
|
||||
{s.title}
|
||||
{s.subtitle ? <span class="subtitle"> ({s.subtitle})</span> : null}
|
||||
</h3>
|
||||
);
|
||||
▼
|
||||
</span>
|
||||
{unref(processed.title[i])}
|
||||
{unref(processed.subtitle[i]) != null ? (
|
||||
<span class="subtitle"> ({unref(processed.subtitle[i])})</span>
|
||||
) : null}
|
||||
</h3>
|
||||
);
|
||||
|
||||
const modifiers = unref(collapsed[i]) ? null : (
|
||||
<>
|
||||
const modifiers = unref(collapsed.value[i]) ? null : (
|
||||
<>
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-description">
|
||||
{renderJSX(unref(processed.baseText[i]) ?? "Base")}
|
||||
</span>
|
||||
<span class="modifier-amount">
|
||||
{format(unref(processed.base[i]) ?? 1)}
|
||||
{s.unit}
|
||||
</span>
|
||||
</div>
|
||||
{renderJSX(unref(s.modifier.description))}
|
||||
</>
|
||||
);
|
||||
|
||||
const hasPreviousSection = !firstVisibleSection;
|
||||
firstVisibleSection = false;
|
||||
|
||||
const base = unref(processed.base[i]) ?? 1;
|
||||
const total = s.modifier.apply(base);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasPreviousSection ? <br /> : null}
|
||||
<div
|
||||
style={{
|
||||
"--unit":
|
||||
settings.alignUnits && s.unit != null ? "'" + s.unit + "'" : ""
|
||||
}}
|
||||
>
|
||||
{header}
|
||||
<br />
|
||||
{modifiers}
|
||||
<hr />
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-amount">
|
||||
{format(unref(processedBase[i]) ?? 1)}
|
||||
<span class="modifier-description">Total</span>
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
(
|
||||
smallerIsBetter === true
|
||||
? Decimal.gt(total, base ?? 1)
|
||||
: Decimal.lt(total, base ?? 1)
|
||||
)
|
||||
? "color: var(--danger)"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{formatSmall(total)}
|
||||
{s.unit}
|
||||
</span>
|
||||
<span class="modifier-description">
|
||||
{renderJSX(unref(processedBaseText[i]) ?? "Base")}
|
||||
</span>
|
||||
</div>
|
||||
{renderJSX(unref(s.modifier.description))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{i === 0 ? null : <br />}
|
||||
<div>
|
||||
{header}
|
||||
<br />
|
||||
{modifiers}
|
||||
<hr />
|
||||
Total: {format(s.modifier.apply(unref(processedBase[i]) ?? 1))}
|
||||
{s.unit}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
return <>{sectionJSX}</>;
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
return [jsxFunc, collapsed];
|
||||
return <>{sectionJSX}</>;
|
||||
});
|
||||
return [jsxFunc, collapsed];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -323,5 +380,135 @@ export function createCollapsibleModifierSections(
|
|||
* @param color The color to change the content to look like. Defaults to the current theme's accent 2 variable.
|
||||
*/
|
||||
export function colorText(textToColor: string, color = "var(--accent2)"): JSX.Element {
|
||||
return <span style={{ color }}>${textToColor}</span>;
|
||||
return <span style={{ color }}>{textToColor}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collapsible display of a list of milestones
|
||||
* @param milestones A dictionary of the milestones to display, inserted in the order from easiest to hardest
|
||||
*/
|
||||
export function createCollapsibleMilestones(milestones: Record<string, GenericMilestone>) {
|
||||
// Milestones are typically defined from easiest to hardest, and we want to show hardest first
|
||||
const orderedMilestones = Object.values(milestones).reverse();
|
||||
const collapseMilestones = persistent<boolean>(true, false);
|
||||
const lockedMilestones = computed(() =>
|
||||
orderedMilestones.filter(m => m.earned.value === false)
|
||||
);
|
||||
const { firstFeature, collapsedContent, hasCollapsedContent } = getFirstFeature(
|
||||
orderedMilestones,
|
||||
m => m.earned.value
|
||||
);
|
||||
const display = jsx(() => {
|
||||
const milestonesToDisplay = [...lockedMilestones.value];
|
||||
if (firstFeature.value) {
|
||||
milestonesToDisplay.push(firstFeature.value);
|
||||
}
|
||||
return renderColJSX(
|
||||
...milestonesToDisplay,
|
||||
jsx(() => (
|
||||
<Collapsible
|
||||
collapsed={collapseMilestones}
|
||||
content={collapsedContent}
|
||||
display={
|
||||
collapseMilestones.value
|
||||
? "Show other completed milestones"
|
||||
: "Hide other completed milestones"
|
||||
}
|
||||
v-show={unref(hasCollapsedContent)}
|
||||
/>
|
||||
))
|
||||
);
|
||||
});
|
||||
return {
|
||||
collapseMilestones,
|
||||
display
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for getting an ETA for when a target will be reached by a resource with a known (and assumed consistent) gain.
|
||||
* @param resource The resource that will be increasing over time.
|
||||
* @param rate The rate at which the resource is increasing.
|
||||
* @param target The target amount of the resource to estimate the duration until.
|
||||
*/
|
||||
export function estimateTime(
|
||||
resource: Resource,
|
||||
rate: Computable<DecimalSource>,
|
||||
target: Computable<DecimalSource>
|
||||
) {
|
||||
const processedRate = convertComputable(rate);
|
||||
const processedTarget = convertComputable(target);
|
||||
return computed(() => {
|
||||
const currRate = unref(processedRate);
|
||||
const currTarget = unref(processedTarget);
|
||||
if (Decimal.gte(resource.value, currTarget)) {
|
||||
return "Now";
|
||||
} else if (Decimal.lt(currRate, 0)) {
|
||||
return "Never";
|
||||
}
|
||||
return formatTime(Decimal.sub(currTarget, resource.value).div(currRate));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for displaying the result of a formula such that it will, when told to, preview how the formula's result will change.
|
||||
* Requires a formula with a single variable inside.
|
||||
* @param formula The formula to display the result of.
|
||||
* @param showPreview Whether or not to preview how the formula's result will change.
|
||||
* @param previewAmount The amount to _add_ to the current formula's variable amount to preview the change in result.
|
||||
*/
|
||||
export function createFormulaPreview(
|
||||
formula: GenericFormula,
|
||||
showPreview: Computable<boolean>,
|
||||
previewAmount: Computable<DecimalSource> = 1
|
||||
): ComputedRef<CoercableComponent> {
|
||||
const processedShowPreview = convertComputable(showPreview);
|
||||
const processedPreviewAmount = convertComputable(previewAmount);
|
||||
if (!formula.hasVariable()) {
|
||||
throw new Error("Cannot create formula preview if the formula does not have a variable");
|
||||
}
|
||||
return computed(() => {
|
||||
if (unref(processedShowPreview)) {
|
||||
const curr = formatSmall(formula.evaluate());
|
||||
const preview = formatSmall(
|
||||
formula.evaluate(
|
||||
Decimal.add(
|
||||
unref(formula.innermostVariable ?? 0),
|
||||
unref(processedPreviewAmount)
|
||||
)
|
||||
)
|
||||
);
|
||||
return jsx(() => (
|
||||
<>
|
||||
<b>
|
||||
<i>
|
||||
{curr}→{preview}
|
||||
</i>
|
||||
</b>
|
||||
</>
|
||||
));
|
||||
}
|
||||
return formatSmall(formula.evaluate());
|
||||
});
|
||||
}
|
||||
|
||||
export function modifierToFormula<T extends GenericFormula>(
|
||||
modifier: WithRequired<Modifier, "revert">,
|
||||
base: T
|
||||
): T;
|
||||
export function modifierToFormula(modifier: Modifier, base: FormulaSource): GenericFormula;
|
||||
export function modifierToFormula(modifier: Modifier, base: FormulaSource) {
|
||||
return new Formula({
|
||||
inputs: [base],
|
||||
evaluate: val => modifier.apply(val),
|
||||
invert:
|
||||
"revert" in modifier && modifier.revert != null
|
||||
? (val, lhs) => {
|
||||
if (lhs instanceof Formula && lhs.hasVariable()) {
|
||||
return lhs.invert(modifier.revert!(val));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
}
|
||||
|
|
93
src/data/projInfo-schema.json
Normal file
93
src/data/projInfo-schema.json
Normal file
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The name of the project, which will appear in the info tab and the header, if enabled. The page title will also be set to this value."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A description of the project, which will be used when the project is installed as a Progressive Web Application."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "This is a unique ID used when saving player data. Changing this will effectively erase all save data for all players. This ID MUST be unique to your project, and should not be left as the default value. Otherwise, your project may use the save data from another project and cause issues for both projects.",
|
||||
"minLength": 1
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"description": "The author of the project, which will appear in the info tab."
|
||||
},
|
||||
"discordName": {
|
||||
"type": "string",
|
||||
"description": "The text to display for the discord server to point users to. This will appear when hovering over the discord icon, inside the info tab, the game over screen, as well as the NaN detected screen."
|
||||
},
|
||||
"discordLink": {
|
||||
"type": "string",
|
||||
"description": "The link for the discord server to point users to."
|
||||
},
|
||||
"versionNumber": {
|
||||
"type": "string",
|
||||
"description": "The current version of the project loaded. If the player data was last saved in a different version of the project, fixOldSave will be run, so you can perform any save migrations necessary. This will also appear in the nav, the info tab, and the game over screen.",
|
||||
"markdownDescription": "The current version of the project loaded. If the player data was last saved in a different version of the project, [fixOldSave](https://moddingtree.com/guide/creating-your-project/project-entry.html#fixoldsave) will be run, so you can perform any save migrations necessary. This will also appear in the nav, the info tab, and the game over screen."
|
||||
},
|
||||
"versionTitle": {
|
||||
"type": "string",
|
||||
"description": "The display name for the current version of the project loaded. This will also appear in the nav, the info tab, and the game over screen unless set to an empty string."
|
||||
},
|
||||
"allowGoBack": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to allow tabs (besides the first) to display a \"back\" button to close them (and any other tabs to the right of them)."
|
||||
},
|
||||
"defaultShowSmall": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to allow resources to display small values (<.001). If false they'll just display as 0. Individual resources can also be configured to override this value."
|
||||
},
|
||||
"defaultDecimalsShown": {
|
||||
"type": "number",
|
||||
"description": "Default precision to display numbers at when passed into format. Individual format calls can override this value, and resources can be configured with a custom precision as well.",
|
||||
"markdownDescription": "Default precision to display numbers at when passed into format. Individual format calls can override this value, and resources can be configured with a custom precision as well."
|
||||
},
|
||||
"useHeader": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to display the nav as a header at the top of the screen. If disabled, the nav will appear on the left side of the screen laid over the first tab."
|
||||
},
|
||||
"banner": {
|
||||
"type": ["boolean", "null"],
|
||||
"description": "A path to an image file to display as the logo of the app. If null, the title will be shown instead. This will appear in the nav when useHeader is true.",
|
||||
"markdownDescription": "A path to an image file to display as the logo of the app. If null, the title will be shown instead. This will appear in the nav when `useHeader` is true."
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"description": "A path to an image file to display as the logo of the app within the info tab. If left blank no logo will be shown."
|
||||
},
|
||||
"initialTabs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"description": "The list of initial tabs to display on new saves. This value must have at least one element. Each element should be the ID of the layer to display in that tab."
|
||||
},
|
||||
"maxTickLength": {
|
||||
"type": "number",
|
||||
"description": "The longest duration a single tick can be, in seconds. When calculating things like offline time, a single tick will be forced to be this amount or lower. This will make calculating offline time spread out across many ticks as necessary. The default value is 1 hour."
|
||||
},
|
||||
"offlineLimit": {
|
||||
"type": "number",
|
||||
"description": "The max amount of time that can be stored as offline time, in hours."
|
||||
},
|
||||
"enablePausing": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to allow the player to pause the game. Turning this off disables the toggle from the options menu as well as the NaN screen. Developers can still manually pause by just setting player.devSpeed to 0 in console (or 1 to resume).",
|
||||
"markdownDescription": "Whether or not to allow the player to pause the game. Turning this off disables the toggle from the options menu as well as the NaN screen. Developers can still manually pause by just running `player.devSpeed = 0` in console (or `= 1` to resume)."
|
||||
},
|
||||
"exportEncoding": {
|
||||
"type": "string",
|
||||
"enum": ["base64", "lz", "plain"],
|
||||
"description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"$schema": "./projInfo-schema.json",
|
||||
|
||||
"title": "Profectus Demo",
|
||||
"description": "A demo project made in Profectus",
|
||||
"id": "profectus-demo",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined,
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined,
|
||||
backgroundImage: (earned && image && `url(${image})`) || ''
|
||||
},
|
||||
unref(style) ?? []
|
||||
|
@ -27,7 +27,7 @@ import "components/common/features.css";
|
|||
import MarkNode from "components/MarkNode.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { Visibility, isHidden, isVisible } from "features/feature";
|
||||
import { computeOptionalComponent, processedPropType } from "util/vue";
|
||||
import type { StyleValue } from "vue";
|
||||
import { defineComponent, toRefs, unref } from "vue";
|
||||
|
@ -35,7 +35,7 @@ import { defineComponent, toRefs, unref } from "vue";
|
|||
export default defineComponent({
|
||||
props: {
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
display: processedPropType<CoercableComponent>(Object, String, Function),
|
||||
|
@ -62,7 +62,9 @@ export default defineComponent({
|
|||
return {
|
||||
component: computeOptionalComponent(display),
|
||||
unref,
|
||||
Visibility
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
Component,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
isVisible,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
setDefault,
|
||||
|
@ -32,7 +33,7 @@ const toast = useToast();
|
|||
export const AchievementType = Symbol("Achievement");
|
||||
|
||||
export interface AchievementOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
shouldEarn?: () => boolean;
|
||||
display?: Computable<CoercableComponent>;
|
||||
mark?: Computable<boolean | string>;
|
||||
|
@ -66,14 +67,14 @@ export type Achievement<T extends AchievementOptions> = Replace<
|
|||
export type GenericAchievement = Replace<
|
||||
Achievement<AchievementOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createAchievement<T extends AchievementOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>
|
||||
): Achievement<T> {
|
||||
const earned = persistent<boolean>(false);
|
||||
const earned = persistent<boolean>(false, false);
|
||||
return createLazyProxy(() => {
|
||||
const achievement = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
achievement.id = getUniqueID("achievement-");
|
||||
|
@ -104,12 +105,12 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
if (settings.active !== player.id) return;
|
||||
if (
|
||||
!genericAchievement.earned.value &&
|
||||
unref(genericAchievement.visibility) === Visibility.Visible &&
|
||||
isVisible(genericAchievement.visibility) &&
|
||||
genericAchievement.shouldEarn?.()
|
||||
) {
|
||||
genericAchievement.earned.value = true;
|
||||
genericAchievement.onComplete?.();
|
||||
if (genericAchievement.display) {
|
||||
if (genericAchievement.display != null) {
|
||||
const Display = coerceComponent(unref(genericAchievement.display));
|
||||
toast.info(
|
||||
<div>
|
||||
|
|
247
src/features/action.tsx
Normal file
247
src/features/action.tsx
Normal file
|
@ -0,0 +1,247 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import {
|
||||
Component,
|
||||
findFeatures,
|
||||
GatherProps,
|
||||
GenericComponent,
|
||||
getUniqueID,
|
||||
jsx,
|
||||
JSXFunction,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
setDefault,
|
||||
StyleValue,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import { persistent } from "game/persistence";
|
||||
import Decimal, { DecimalSource } from "lib/break_eternity";
|
||||
import { Unsubscribe } from "nanoevents";
|
||||
import { Direction } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { coerceComponent, isCoercableComponent, render } from "util/vue";
|
||||
import { computed, Ref, ref, unref } from "vue";
|
||||
import { BarOptions, createBar, GenericBar } from "./bars/bar";
|
||||
import { ClickableOptions } from "./clickables/clickable";
|
||||
|
||||
export const ActionType = Symbol("Action");
|
||||
|
||||
export interface ActionOptions extends Omit<ClickableOptions, "onClick" | "onHold"> {
|
||||
duration: Computable<DecimalSource>;
|
||||
autoStart?: Computable<boolean>;
|
||||
onClick: (amount: DecimalSource) => void;
|
||||
barOptions?: Partial<BarOptions>;
|
||||
}
|
||||
|
||||
export interface BaseAction {
|
||||
id: string;
|
||||
type: typeof ActionType;
|
||||
isHolding: Ref<boolean>;
|
||||
progress: Ref<DecimalSource>;
|
||||
progressBar: GenericBar;
|
||||
update: (diff: number) => void;
|
||||
[Component]: GenericComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type Action<T extends ActionOptions> = Replace<
|
||||
T & BaseAction,
|
||||
{
|
||||
duration: GetComputableType<T["duration"]>;
|
||||
autoStart: GetComputableTypeWithDefault<T["autoStart"], false>;
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
canClick: GetComputableTypeWithDefault<T["canClick"], true>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
display: JSXFunction;
|
||||
onClick: VoidFunction;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GenericAction = Replace<
|
||||
Action<ActionOptions>,
|
||||
{
|
||||
autoStart: ProcessedComputable<boolean>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createAction<T extends ActionOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>
|
||||
): Action<T> {
|
||||
const progress = persistent<DecimalSource>(0);
|
||||
return createLazyProxy(() => {
|
||||
const action = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
action.id = getUniqueID("action-");
|
||||
action.type = ActionType;
|
||||
action[Component] = ClickableComponent as GenericComponent;
|
||||
|
||||
// Required because of display changing types
|
||||
const genericAction = action as unknown as GenericAction;
|
||||
|
||||
action.isHolding = ref(false);
|
||||
action.progress = progress;
|
||||
|
||||
processComputable(action as T, "visibility");
|
||||
setDefault(action, "visibility", Visibility.Visible);
|
||||
processComputable(action as T, "duration");
|
||||
processComputable(action as T, "autoStart");
|
||||
setDefault(action, "autoStart", false);
|
||||
processComputable(action as T, "canClick");
|
||||
setDefault(action, "canClick", true);
|
||||
processComputable(action as T, "classes");
|
||||
processComputable(action as T, "style");
|
||||
processComputable(action as T, "mark");
|
||||
processComputable(action as T, "display");
|
||||
|
||||
const style = action.style as ProcessedComputable<StyleValue | undefined>;
|
||||
action.style = computed(() => {
|
||||
const currStyle: StyleValue[] = [
|
||||
{
|
||||
cursor: Decimal.gte(
|
||||
progress.value,
|
||||
unref(action.duration as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
? "pointer"
|
||||
: "progress",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}
|
||||
];
|
||||
const originalStyle = unref(style);
|
||||
if (isArray(originalStyle)) {
|
||||
currStyle.push(...originalStyle);
|
||||
} else if (originalStyle != null) {
|
||||
currStyle.push(originalStyle);
|
||||
}
|
||||
return currStyle as StyleValue;
|
||||
});
|
||||
|
||||
action.progressBar = createBar(() => ({
|
||||
direction: Direction.Right,
|
||||
width: 100,
|
||||
height: 10,
|
||||
style: "margin-top: 8px",
|
||||
borderStyle: "border-color: black",
|
||||
baseStyle: "margin-top: -1px",
|
||||
progress: () => Decimal.div(progress.value, unref(genericAction.duration)),
|
||||
...action.barOptions
|
||||
}));
|
||||
|
||||
const canClick = action.canClick as ProcessedComputable<boolean>;
|
||||
action.canClick = computed(
|
||||
() =>
|
||||
unref(canClick) &&
|
||||
Decimal.gte(
|
||||
progress.value,
|
||||
unref(action.duration as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
);
|
||||
|
||||
const display = action.display as GetComputableType<ClickableOptions["display"]>;
|
||||
action.display = jsx(() => {
|
||||
const currDisplay = unref(display);
|
||||
let Comp: GenericComponent | undefined;
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
Comp = coerceComponent(currDisplay);
|
||||
} else if (currDisplay != null) {
|
||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description, "div");
|
||||
Comp = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
</span>
|
||||
))
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div style="flex-grow: 1" />
|
||||
{Comp == null ? null : <Comp />}
|
||||
<div style="flex-grow: 1" />
|
||||
{render(genericAction.progressBar)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const onClick = action.onClick.bind(action);
|
||||
action.onClick = function () {
|
||||
if (unref(action.canClick) === false) {
|
||||
return;
|
||||
}
|
||||
const amount = Decimal.div(progress.value, unref(genericAction.duration));
|
||||
onClick?.(amount);
|
||||
progress.value = 0;
|
||||
};
|
||||
|
||||
action.update = function (diff) {
|
||||
const duration = unref(genericAction.duration);
|
||||
if (Decimal.gte(progress.value, duration)) {
|
||||
progress.value = duration;
|
||||
} else {
|
||||
progress.value = Decimal.add(progress.value, diff);
|
||||
if (genericAction.isHolding.value || unref(genericAction.autoStart)) {
|
||||
genericAction.onClick();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
action[GatherProps] = function (this: GenericAction) {
|
||||
const {
|
||||
display,
|
||||
visibility,
|
||||
style,
|
||||
classes,
|
||||
onClick,
|
||||
isHolding,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
} = this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onClick,
|
||||
isHolding,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
return action as unknown as Action<T>;
|
||||
});
|
||||
}
|
||||
|
||||
const listeners: Record<string, Unsubscribe | undefined> = {};
|
||||
globalBus.on("addLayer", layer => {
|
||||
const actions: GenericAction[] = findFeatures(layer, ActionType) as GenericAction[];
|
||||
listeners[layer.id] = layer.on("postUpdate", diff => {
|
||||
actions.forEach(action => action.update(diff));
|
||||
});
|
||||
});
|
||||
globalBus.on("removeLayer", layer => {
|
||||
// unsubscribe from postUpdate
|
||||
listeners[layer.id]?.();
|
||||
listeners[layer.id] = undefined;
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
width: unref(width) + 'px',
|
||||
height: unref(height) + 'px',
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
|
@ -44,7 +44,7 @@
|
|||
<script lang="ts">
|
||||
import MarkNode from "components/MarkNode.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import { CoercableComponent, Visibility } from "features/feature";
|
||||
import { CoercableComponent, isHidden, isVisible, Visibility } from "features/feature";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import { Direction } from "util/common";
|
||||
|
@ -72,7 +72,7 @@ export default defineComponent({
|
|||
},
|
||||
display: processedPropType<CoercableComponent>(Object, String, Function),
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
style: processedPropType<StyleValue>(Object, String, Array),
|
||||
|
@ -136,7 +136,9 @@ export default defineComponent({
|
|||
barStyle,
|
||||
component,
|
||||
unref,
|
||||
Visibility
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import BarComponent from "features/bars/Bar.vue";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import { Direction } from "util/common";
|
||||
|
@ -16,7 +22,7 @@ import { unref } from "vue";
|
|||
export const BarType = Symbol("Bar");
|
||||
|
||||
export interface BarOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
width: Computable<number>;
|
||||
height: Computable<number>;
|
||||
direction: Computable<Direction>;
|
||||
|
@ -60,7 +66,7 @@ export type Bar<T extends BarOptions> = Replace<
|
|||
export type GenericBar = Replace<
|
||||
Bar<BarOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<panZoom
|
||||
v-if="visibility !== Visibility.None"
|
||||
v-show="visibility === Visibility.Visible"
|
||||
v-if="isVisible(visibility)"
|
||||
v-show="isHidden(visibility)"
|
||||
:style="[
|
||||
{
|
||||
width,
|
||||
|
@ -60,19 +60,17 @@ import type {
|
|||
} from "features/boards/board";
|
||||
import { getNodeProperty } from "features/boards/board";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { PersistentState } from "game/persistence";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import type { ProcessedComputable } from "util/computed";
|
||||
import { computed, ref, Ref, toRefs, unref } from "vue";
|
||||
import panZoom from "vue-panzoom";
|
||||
import BoardLinkVue from "./BoardLink.vue";
|
||||
import BoardNodeVue from "./BoardNode.vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
nodes: Ref<BoardNode[]>;
|
||||
types: Record<string, GenericNodeType>;
|
||||
[PersistentState]: Ref<BoardData>;
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
state: Ref<BoardData>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
width?: ProcessedComputable<string>;
|
||||
height?: ProcessedComputable<string>;
|
||||
style?: ProcessedComputable<StyleValue>;
|
||||
|
@ -80,6 +78,7 @@ const _props = defineProps<{
|
|||
links: Ref<BoardNodeLink[] | null>;
|
||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||
selectedNode: Ref<BoardNode | null>;
|
||||
mousePosition: Ref<{ x: number; y: number } | null>;
|
||||
}>();
|
||||
const props = toRefs(_props);
|
||||
|
||||
|
@ -170,13 +169,13 @@ function mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, dra
|
|||
}
|
||||
}
|
||||
if (nodeID != null) {
|
||||
props[PersistentState].value.selectedNode = null;
|
||||
props[PersistentState].value.selectedAction = null;
|
||||
props.state.value.selectedNode = null;
|
||||
props.state.value.selectedAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
function drag(e: MouseEvent | TouchEvent) {
|
||||
const zoom = stage.value.$panZoomInstance.getTransform().scale;
|
||||
const { x, y, scale } = stage.value.panZoomInstance.getTransform();
|
||||
|
||||
let clientX, clientY;
|
||||
if ("touches" in e) {
|
||||
|
@ -185,6 +184,7 @@ function drag(e: MouseEvent | TouchEvent) {
|
|||
clientY = e.touches[0].clientY;
|
||||
} else {
|
||||
endDragging(dragging.value);
|
||||
props.mousePosition.value = null;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
@ -192,9 +192,14 @@ function drag(e: MouseEvent | TouchEvent) {
|
|||
clientY = e.clientY;
|
||||
}
|
||||
|
||||
props.mousePosition.value = {
|
||||
x: (clientX - x) / scale,
|
||||
y: (clientY - y) / scale
|
||||
};
|
||||
|
||||
dragged.value = {
|
||||
x: dragged.value.x + (clientX - lastMousePosition.value.x) / zoom,
|
||||
y: dragged.value.y + (clientY - lastMousePosition.value.y) / zoom
|
||||
x: dragged.value.x + (clientX - lastMousePosition.value.x) / scale,
|
||||
y: dragged.value.y + (clientY - lastMousePosition.value.y) / scale
|
||||
};
|
||||
lastMousePosition.value = {
|
||||
x: clientX,
|
||||
|
@ -205,7 +210,7 @@ function drag(e: MouseEvent | TouchEvent) {
|
|||
hasDragged.value = true;
|
||||
}
|
||||
|
||||
if (dragging.value) {
|
||||
if (dragging.value != null) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
@ -229,8 +234,8 @@ function endDragging(nodeID: number | null) {
|
|||
|
||||
dragging.value = null;
|
||||
} else if (!hasDragged.value) {
|
||||
props[PersistentState].value.selectedNode = null;
|
||||
props[PersistentState].value.selectedAction = null;
|
||||
props.state.value.selectedNode = null;
|
||||
props.state.value.selectedAction = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -239,7 +244,11 @@ function endDragging(nodeID: number | null) {
|
|||
.vue-pan-zoom-scene {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.vue-pan-zoom-scene:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.g1 {
|
||||
|
|
|
@ -234,17 +234,17 @@ const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(p
|
|||
const label = computed(() => getNodeProperty(props.nodeType.value.label, 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
|
||||
() => 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)) ||
|
||||
getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ??
|
||||
themes[settings.theme].variables["--outline"]
|
||||
);
|
||||
const fillColor = computed(
|
||||
() =>
|
||||
getNodeProperty(props.nodeType.value.fillColor, unref(props.node)) ||
|
||||
getNodeProperty(props.nodeType.value.fillColor, unref(props.node)) ??
|
||||
themes[settings.theme].variables["--raised-background"]
|
||||
);
|
||||
const progressColor = computed(() =>
|
||||
|
@ -252,7 +252,7 @@ const progressColor = computed(() =>
|
|||
);
|
||||
const titleColor = computed(
|
||||
() =>
|
||||
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ||
|
||||
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ??
|
||||
themes[settings.theme].variables["--foreground"]
|
||||
);
|
||||
const progressDisplay = computed(() =>
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
Visibility
|
||||
} from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import type { Persistent, State } from "game/persistence";
|
||||
import { persistent, PersistentState } from "game/persistence";
|
||||
import { DefaultValue, deletePersistent, Persistent, State } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
import { isFunction } from "util/common";
|
||||
import type {
|
||||
|
@ -21,9 +21,12 @@ import type {
|
|||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { computed, Ref, unref } from "vue";
|
||||
import { computed, ref, Ref, unref } from "vue";
|
||||
import panZoom from "vue-panzoom";
|
||||
import type { Link } from "../links/links";
|
||||
|
||||
globalBus.on("setupVue", app => panZoom.install(app));
|
||||
|
||||
export const BoardType = Symbol("Board");
|
||||
|
||||
export type NodeComputable<T> = Computable<T> | ((node: BoardNode) => T);
|
||||
|
@ -126,7 +129,7 @@ export type GenericNodeType = Replace<
|
|||
|
||||
export interface BoardNodeActionOptions {
|
||||
id: string;
|
||||
visibility?: NodeComputable<Visibility>;
|
||||
visibility?: NodeComputable<Visibility | boolean>;
|
||||
icon: NodeComputable<string>;
|
||||
fillColor?: NodeComputable<string>;
|
||||
tooltip: NodeComputable<string>;
|
||||
|
@ -152,26 +155,28 @@ export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
|
|||
export type GenericBoardNodeAction = Replace<
|
||||
BoardNodeAction<BoardNodeActionOptions>,
|
||||
{
|
||||
visibility: NodeComputable<Visibility>;
|
||||
visibility: NodeComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface BoardOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
height?: Computable<string>;
|
||||
width?: Computable<string>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
startNodes: () => Omit<BoardNode, "id">[];
|
||||
types: Record<string, NodeTypeOptions>;
|
||||
state?: Computable<BoardData>;
|
||||
links?: Computable<BoardNodeLink[] | null>;
|
||||
}
|
||||
|
||||
export interface BaseBoard extends Persistent<BoardData> {
|
||||
export interface BaseBoard {
|
||||
id: string;
|
||||
links: Ref<BoardNodeLink[] | null>;
|
||||
nodes: Ref<BoardNode[]>;
|
||||
selectedNode: Ref<BoardNode | null>;
|
||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||
mousePosition: Ref<{ x: number; y: number } | null>;
|
||||
type: typeof BoardType;
|
||||
[Component]: typeof BoardComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
|
@ -186,48 +191,79 @@ export type Board<T extends BoardOptions> = Replace<
|
|||
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>>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GenericBoard = Replace<
|
||||
Board<BoardOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
state: ProcessedComputable<BoardData>;
|
||||
links: ProcessedComputable<BoardNodeLink[] | null>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createBoard<T extends BoardOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseBoard, GenericBoard>
|
||||
): Board<T> {
|
||||
return createLazyProxy(
|
||||
persistent => {
|
||||
const board = Object.assign(persistent, optionsFunc());
|
||||
board.id = getUniqueID("board-");
|
||||
board.type = BoardType;
|
||||
board[Component] = BoardComponent;
|
||||
const state = persistent<BoardData>(
|
||||
{
|
||||
nodes: [],
|
||||
selectedNode: null,
|
||||
selectedAction: null
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
|
||||
board.selectedNode = computed(
|
||||
() =>
|
||||
processedBoard.nodes.value.find(
|
||||
node => node.id === board[PersistentState].value.selectedNode
|
||||
) || null
|
||||
return createLazyProxy(() => {
|
||||
const board = optionsFunc();
|
||||
board.id = getUniqueID("board-");
|
||||
board.type = BoardType;
|
||||
board[Component] = BoardComponent;
|
||||
|
||||
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(
|
||||
() =>
|
||||
processedBoard.nodes.value.find(
|
||||
node => node.id === unref(processedBoard.state).selectedNode
|
||||
) || null
|
||||
);
|
||||
board.selectedAction = computed(() => {
|
||||
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
|
||||
);
|
||||
board.selectedAction = computed(() => {
|
||||
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 === processedBoard[PersistentState].value.selectedAction
|
||||
) || null
|
||||
);
|
||||
});
|
||||
});
|
||||
board.mousePosition = ref(null);
|
||||
if (board.links) {
|
||||
processComputable(board as T, "links");
|
||||
} else {
|
||||
board.links = computed(() => {
|
||||
if (processedBoard.selectedAction.value == null) {
|
||||
return null;
|
||||
|
@ -243,100 +279,97 @@ export function createBoard<T extends BoardOptions>(
|
|||
}
|
||||
return 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", "400px");
|
||||
processComputable(board as T, "classes");
|
||||
processComputable(board as T, "style");
|
||||
}
|
||||
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", "400px");
|
||||
processComputable(board as T, "classes");
|
||||
processComputable(board as T, "style");
|
||||
|
||||
for (const type in board.types) {
|
||||
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
|
||||
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, "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(() =>
|
||||
board[PersistentState].value.nodes.filter(node => node.type === type)
|
||||
);
|
||||
setDefault(nodeType, "onClick", function (node: BoardNode) {
|
||||
board[PersistentState].value.selectedNode = node.id;
|
||||
});
|
||||
processComputable(nodeType as NodeTypeOptions, "title");
|
||||
processComputable(nodeType as NodeTypeOptions, "label");
|
||||
processComputable(nodeType as NodeTypeOptions, "size");
|
||||
setDefault(nodeType, "size", 50);
|
||||
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, "links");
|
||||
}
|
||||
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, "links");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
board[GatherProps] = function (this: GenericBoard) {
|
||||
const {
|
||||
nodes,
|
||||
types,
|
||||
[PersistentState]: state,
|
||||
visibility,
|
||||
width,
|
||||
height,
|
||||
style,
|
||||
classes,
|
||||
links,
|
||||
selectedAction,
|
||||
selectedNode
|
||||
} = this;
|
||||
return {
|
||||
nodes,
|
||||
types,
|
||||
[PersistentState]: state,
|
||||
visibility,
|
||||
width,
|
||||
height,
|
||||
style: unref(style),
|
||||
classes,
|
||||
links,
|
||||
selectedAction,
|
||||
selectedNode
|
||||
};
|
||||
board[GatherProps] = function (this: GenericBoard) {
|
||||
const {
|
||||
nodes,
|
||||
types,
|
||||
state,
|
||||
visibility,
|
||||
width,
|
||||
height,
|
||||
style,
|
||||
classes,
|
||||
links,
|
||||
selectedAction,
|
||||
selectedNode,
|
||||
mousePosition
|
||||
} = this;
|
||||
return {
|
||||
nodes,
|
||||
types,
|
||||
state,
|
||||
visibility,
|
||||
width,
|
||||
height,
|
||||
style: unref(style),
|
||||
classes,
|
||||
links,
|
||||
selectedAction,
|
||||
selectedNode,
|
||||
mousePosition
|
||||
};
|
||||
};
|
||||
|
||||
// This is necessary because board.types is different from T and Board
|
||||
const processedBoard = board as unknown as Board<T>;
|
||||
return processedBoard;
|
||||
},
|
||||
persistent<BoardData>({
|
||||
nodes: [],
|
||||
selectedNode: null,
|
||||
selectedAction: null
|
||||
})
|
||||
);
|
||||
// This is necessary because board.types is different from T and Board
|
||||
const processedBoard = board as unknown as Board<T>;
|
||||
return processedBoard;
|
||||
});
|
||||
}
|
||||
|
||||
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {
|
||||
return isFunction(property) ? property(node) : unref(property);
|
||||
return isFunction<T, [BoardNode], Computable<T>>(property) ? property(node) : unref(property);
|
||||
}
|
||||
|
||||
export function getUniqueNodeID(board: GenericBoard): number {
|
||||
|
|
|
@ -1,243 +0,0 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, formatWhole } from "util/bignum";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
|
||||
export const BuyableType = Symbol("Buyable");
|
||||
|
||||
export type BuyableDisplay =
|
||||
| CoercableComponent
|
||||
| {
|
||||
title?: CoercableComponent;
|
||||
description?: CoercableComponent;
|
||||
effectDisplay?: CoercableComponent;
|
||||
showAmount?: boolean;
|
||||
};
|
||||
|
||||
export interface BuyableOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
cost?: Computable<DecimalSource>;
|
||||
resource?: Resource;
|
||||
canPurchase?: Computable<boolean>;
|
||||
purchaseLimit?: Computable<DecimalSource>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
mark?: Computable<boolean | string>;
|
||||
small?: Computable<boolean>;
|
||||
display?: Computable<BuyableDisplay>;
|
||||
onPurchase?: (cost: DecimalSource | undefined) => void;
|
||||
}
|
||||
|
||||
export interface BaseBuyable {
|
||||
id: string;
|
||||
amount: Persistent<DecimalSource>;
|
||||
maxed: Ref<boolean>;
|
||||
canAfford: Ref<boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
onClick: VoidFunction;
|
||||
purchase: VoidFunction;
|
||||
type: typeof BuyableType;
|
||||
[Component]: typeof ClickableComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type Buyable<T extends BuyableOptions> = Replace<
|
||||
T & BaseBuyable,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
cost: GetComputableType<T["cost"]>;
|
||||
resource: GetComputableType<T["resource"]>;
|
||||
canPurchase: GetComputableTypeWithDefault<T["canPurchase"], Ref<boolean>>;
|
||||
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
small: GetComputableType<T["small"]>;
|
||||
display: Ref<CoercableComponent>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GenericBuyable = Replace<
|
||||
Buyable<BuyableOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
canPurchase: ProcessedComputable<boolean>;
|
||||
purchaseLimit: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createBuyable<T extends BuyableOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseBuyable, GenericBuyable>
|
||||
): Buyable<T> {
|
||||
const amount = persistent<DecimalSource>(0);
|
||||
return createLazyProxy(() => {
|
||||
const buyable = optionsFunc();
|
||||
|
||||
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
|
||||
console.warn(
|
||||
"Cannot create buyable without a canPurchase property or a resource and cost property",
|
||||
buyable
|
||||
);
|
||||
throw "Cannot create buyable without a canPurchase property or a resource and cost property";
|
||||
}
|
||||
|
||||
buyable.id = getUniqueID("buyable-");
|
||||
buyable.type = BuyableType;
|
||||
buyable[Component] = ClickableComponent;
|
||||
|
||||
buyable.amount = amount;
|
||||
buyable.canAfford = computed(() => {
|
||||
const genericBuyable = buyable as GenericBuyable;
|
||||
const cost = unref(genericBuyable.cost);
|
||||
return (
|
||||
genericBuyable.resource != null &&
|
||||
cost != null &&
|
||||
Decimal.gte(genericBuyable.resource.value, cost)
|
||||
);
|
||||
});
|
||||
if (buyable.canPurchase == null) {
|
||||
buyable.canPurchase = computed(
|
||||
() =>
|
||||
unref((buyable as GenericBuyable).visibility) === Visibility.Visible &&
|
||||
unref((buyable as GenericBuyable).canAfford) &&
|
||||
Decimal.lt(
|
||||
(buyable as GenericBuyable).amount.value,
|
||||
unref((buyable as GenericBuyable).purchaseLimit)
|
||||
)
|
||||
);
|
||||
}
|
||||
buyable.maxed = computed(() =>
|
||||
Decimal.gte(
|
||||
(buyable as GenericBuyable).amount.value,
|
||||
unref((buyable as GenericBuyable).purchaseLimit)
|
||||
)
|
||||
);
|
||||
processComputable(buyable as T, "classes");
|
||||
const classes = buyable.classes as ProcessedComputable<Record<string, boolean>> | undefined;
|
||||
buyable.classes = computed(() => {
|
||||
const currClasses = unref(classes) || {};
|
||||
if ((buyable as GenericBuyable).maxed.value) {
|
||||
currClasses.bought = true;
|
||||
}
|
||||
return currClasses;
|
||||
});
|
||||
processComputable(buyable as T, "canPurchase");
|
||||
buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>;
|
||||
buyable.onClick = buyable.purchase =
|
||||
buyable.onClick ??
|
||||
buyable.purchase ??
|
||||
function (this: GenericBuyable) {
|
||||
const genericBuyable = buyable as GenericBuyable;
|
||||
if (!unref(genericBuyable.canPurchase)) {
|
||||
return;
|
||||
}
|
||||
const cost = unref(genericBuyable.cost);
|
||||
if (genericBuyable.cost != null && genericBuyable.resource != null) {
|
||||
genericBuyable.resource.value = Decimal.sub(
|
||||
genericBuyable.resource.value,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
cost!
|
||||
);
|
||||
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
|
||||
}
|
||||
genericBuyable.onPurchase?.(cost);
|
||||
};
|
||||
processComputable(buyable as T, "display");
|
||||
const display = buyable.display;
|
||||
buyable.display = jsx(() => {
|
||||
// TODO once processComputable types correctly, remove this "as X"
|
||||
const currDisplay = unref(display) as BuyableDisplay;
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
const CurrDisplay = coerceComponent(currDisplay);
|
||||
return <CurrDisplay />;
|
||||
}
|
||||
if (currDisplay != null && buyable.cost != null && buyable.resource != null) {
|
||||
const genericBuyable = buyable as GenericBuyable;
|
||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description || "");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||
|
||||
return (
|
||||
<span>
|
||||
{currDisplay.title ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
{currDisplay.description ? <Description /> : null}
|
||||
{currDisplay.showAmount === false ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{unref(genericBuyable.purchaseLimit) === Decimal.dInf ? (
|
||||
<>Amount: {formatWhole(genericBuyable.amount.value)}</>
|
||||
) : (
|
||||
<>
|
||||
Amount: {formatWhole(genericBuyable.amount.value)} /{" "}
|
||||
{formatWhole(unref(genericBuyable.purchaseLimit))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currDisplay.effectDisplay ? (
|
||||
<div>
|
||||
<br />
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
) : null}
|
||||
{genericBuyable.cost && !genericBuyable.maxed.value ? (
|
||||
<div>
|
||||
<br />
|
||||
Cost: {format(unref(genericBuyable.cost) || 0)}{" "}
|
||||
{buyable.resource.displayName}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
processComputable(buyable as T, "visibility");
|
||||
setDefault(buyable, "visibility", Visibility.Visible);
|
||||
processComputable(buyable as T, "cost");
|
||||
processComputable(buyable as T, "resource");
|
||||
processComputable(buyable as T, "purchaseLimit");
|
||||
setDefault(buyable, "purchaseLimit", Decimal.dInf);
|
||||
processComputable(buyable as T, "style");
|
||||
processComputable(buyable as T, "mark");
|
||||
processComputable(buyable as T, "small");
|
||||
|
||||
buyable[GatherProps] = function (this: GenericBuyable) {
|
||||
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
||||
this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onClick,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
return buyable as unknown as Buyable<T>;
|
||||
});
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
notifyStyle,
|
||||
unref(style) ?? {}
|
||||
|
@ -36,7 +36,7 @@ import MarkNode from "components/MarkNode.vue";
|
|||
import Node from "components/Node.vue";
|
||||
import type { GenericChallenge } from "features/challenges/challenge";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { jsx, Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
|
||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
||||
import type { Component, PropType, UnwrapRef } from "vue";
|
||||
|
@ -62,7 +62,7 @@ export default defineComponent({
|
|||
Function
|
||||
),
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
style: processedPropType<StyleValue>(String, Object, Array),
|
||||
|
@ -134,25 +134,25 @@ export default defineComponent({
|
|||
comp.value = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title ? (
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
{currDisplay.goal ? (
|
||||
{currDisplay.goal != null ? (
|
||||
<div>
|
||||
<br />
|
||||
Goal: <Goal />
|
||||
</div>
|
||||
) : null}
|
||||
{currDisplay.reward ? (
|
||||
{currDisplay.reward != null ? (
|
||||
<div>
|
||||
<br />
|
||||
Reward: <Reward />
|
||||
</div>
|
||||
) : null}
|
||||
{currDisplay.effectDisplay ? (
|
||||
{currDisplay.effectDisplay != null ? (
|
||||
<div>
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
|
@ -167,6 +167,8 @@ export default defineComponent({
|
|||
notifyStyle,
|
||||
comp,
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden,
|
||||
unref
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,12 +2,20 @@ import { isArray } from "@vue/shared";
|
|||
import Toggle from "components/fields/Toggle.vue";
|
||||
import ChallengeComponent from "features/challenges/Challenge.vue";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
isVisible,
|
||||
jsx,
|
||||
setDefault,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import type { GenericReset } from "features/reset";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import { globalBus } from "game/events";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import { maxRequirementsMet, Requirements } from "game/requirements";
|
||||
import settings, { registerSettingField } from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
|
@ -25,14 +33,13 @@ import { computed, unref, watch } from "vue";
|
|||
export const ChallengeType = Symbol("ChallengeType");
|
||||
|
||||
export interface ChallengeOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
canStart?: Computable<boolean>;
|
||||
reset?: GenericReset;
|
||||
canComplete?: Computable<boolean | DecimalSource>;
|
||||
requirements: Requirements;
|
||||
maximize?: Computable<boolean>;
|
||||
completionLimit?: Computable<DecimalSource>;
|
||||
mark?: Computable<boolean | string>;
|
||||
resource?: Resource;
|
||||
goal?: Computable<DecimalSource>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
display?: Computable<
|
||||
|
@ -52,6 +59,7 @@ export interface ChallengeOptions {
|
|||
|
||||
export interface BaseChallenge {
|
||||
id: string;
|
||||
canComplete: Ref<DecimalSource>;
|
||||
completions: Persistent<DecimalSource>;
|
||||
completed: Ref<boolean>;
|
||||
maxed: Ref<boolean>;
|
||||
|
@ -68,10 +76,10 @@ export type Challenge<T extends ChallengeOptions> = Replace<
|
|||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
canStart: GetComputableTypeWithDefault<T["canStart"], true>;
|
||||
canComplete: GetComputableTypeWithDefault<T["canComplete"], Ref<boolean>>;
|
||||
requirements: GetComputableType<T["requirements"]>;
|
||||
maximize: GetComputableType<T["maximize"]>;
|
||||
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
|
||||
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
|
||||
goal: GetComputableType<T["goal"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
|
@ -81,9 +89,8 @@ export type Challenge<T extends ChallengeOptions> = Replace<
|
|||
export type GenericChallenge = Replace<
|
||||
Challenge<ChallengeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canStart: ProcessedComputable<boolean>;
|
||||
canComplete: ProcessedComputable<boolean | DecimalSource>;
|
||||
completionLimit: ProcessedComputable<DecimalSource>;
|
||||
mark: ProcessedComputable<boolean>;
|
||||
}
|
||||
|
@ -93,21 +100,10 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>
|
||||
): Challenge<T> {
|
||||
const completions = persistent(0);
|
||||
const active = persistent(false);
|
||||
const active = persistent(false, false);
|
||||
return createLazyProxy(() => {
|
||||
const challenge = optionsFunc();
|
||||
|
||||
if (
|
||||
challenge.canComplete == null &&
|
||||
(challenge.resource == null || challenge.goal == null)
|
||||
) {
|
||||
console.warn(
|
||||
"Cannot create challenge without a canComplete property or a resource and goal property",
|
||||
challenge
|
||||
);
|
||||
throw "Cannot create challenge without a canComplete property or a resource and goal property";
|
||||
}
|
||||
|
||||
challenge.id = getUniqueID("challenge-");
|
||||
challenge.type = ChallengeType;
|
||||
challenge[Component] = ChallengeComponent;
|
||||
|
@ -126,11 +122,11 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
challenge.toggle = function () {
|
||||
const genericChallenge = challenge as GenericChallenge;
|
||||
if (genericChallenge.active.value) {
|
||||
if (unref(genericChallenge.canComplete) && !genericChallenge.maxed.value) {
|
||||
let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
|
||||
if (typeof completions === "boolean") {
|
||||
completions = 1;
|
||||
}
|
||||
if (
|
||||
Decimal.gt(unref(genericChallenge.canComplete), 0) &&
|
||||
!genericChallenge.maxed.value
|
||||
) {
|
||||
const completions = unref(genericChallenge.canComplete);
|
||||
genericChallenge.completions.value = Decimal.min(
|
||||
Decimal.add(genericChallenge.completions.value, completions),
|
||||
unref(genericChallenge.completionLimit)
|
||||
|
@ -142,7 +138,7 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
genericChallenge.reset?.reset();
|
||||
} else if (
|
||||
unref(genericChallenge.canStart) &&
|
||||
unref(genericChallenge.visibility) === Visibility.Visible &&
|
||||
isVisible(genericChallenge.visibility) &&
|
||||
!genericChallenge.maxed.value
|
||||
) {
|
||||
genericChallenge.reset?.reset();
|
||||
|
@ -150,18 +146,20 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
genericChallenge.onEnter?.();
|
||||
}
|
||||
};
|
||||
challenge.canComplete = computed(() =>
|
||||
Decimal.max(
|
||||
maxRequirementsMet((challenge as GenericChallenge).requirements),
|
||||
unref((challenge as GenericChallenge).maximize) ? Decimal.dInf : 1
|
||||
)
|
||||
);
|
||||
challenge.complete = function (remainInChallenge?: boolean) {
|
||||
const genericChallenge = challenge as GenericChallenge;
|
||||
let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
|
||||
const completions = unref(genericChallenge.canComplete);
|
||||
if (
|
||||
genericChallenge.active.value &&
|
||||
completions !== false &&
|
||||
(completions === true || Decimal.neq(0, completions)) &&
|
||||
Decimal.gt(completions, 0) &&
|
||||
!genericChallenge.maxed.value
|
||||
) {
|
||||
if (typeof completions === "boolean") {
|
||||
completions = 1;
|
||||
}
|
||||
genericChallenge.completions.value = Decimal.min(
|
||||
Decimal.add(genericChallenge.completions.value, completions),
|
||||
unref(genericChallenge.completionLimit)
|
||||
|
@ -176,26 +174,13 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
};
|
||||
processComputable(challenge as T, "visibility");
|
||||
setDefault(challenge, "visibility", Visibility.Visible);
|
||||
const visibility = challenge.visibility as ProcessedComputable<Visibility>;
|
||||
const visibility = challenge.visibility as ProcessedComputable<Visibility | boolean>;
|
||||
challenge.visibility = computed(() => {
|
||||
if (settings.hideChallenges === true && unref(challenge.maxed)) {
|
||||
return Visibility.None;
|
||||
}
|
||||
return unref(visibility);
|
||||
});
|
||||
if (challenge.canComplete == null) {
|
||||
challenge.canComplete = computed(() => {
|
||||
const genericChallenge = challenge as GenericChallenge;
|
||||
if (
|
||||
!genericChallenge.active.value ||
|
||||
genericChallenge.resource == null ||
|
||||
genericChallenge.goal == null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return Decimal.gte(genericChallenge.resource.value, unref(genericChallenge.goal));
|
||||
});
|
||||
}
|
||||
if (challenge.mark == null) {
|
||||
challenge.mark = computed(
|
||||
() =>
|
||||
|
@ -206,11 +191,10 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
|
||||
processComputable(challenge as T, "canStart");
|
||||
setDefault(challenge, "canStart", true);
|
||||
processComputable(challenge as T, "canComplete");
|
||||
processComputable(challenge as T, "maximize");
|
||||
processComputable(challenge as T, "completionLimit");
|
||||
setDefault(challenge, "completionLimit", 1);
|
||||
processComputable(challenge as T, "mark");
|
||||
processComputable(challenge as T, "goal");
|
||||
processComputable(challenge as T, "classes");
|
||||
processComputable(challenge as T, "style");
|
||||
processComputable(challenge as T, "display");
|
||||
|
@ -264,11 +248,14 @@ export function setupAutoComplete(
|
|||
exitOnComplete = true
|
||||
): WatchStopHandle {
|
||||
const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
|
||||
return watch([challenge.canComplete, isActive], ([canComplete, isActive]) => {
|
||||
if (canComplete && isActive) {
|
||||
challenge.complete(!exitOnComplete);
|
||||
return watch(
|
||||
[challenge.canComplete as Ref<DecimalSource>, isActive as Ref<boolean>],
|
||||
([canComplete, isActive]) => {
|
||||
if (Decimal.gt(canComplete, 0) && isActive) {
|
||||
challenge.complete(!exitOnComplete);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export function createActiveChallenge(
|
||||
|
@ -299,7 +286,12 @@ globalBus.on("loadSettings", settings => {
|
|||
registerSettingField(
|
||||
jsx(() => (
|
||||
<Toggle
|
||||
title="Hide Maxed Challenges"
|
||||
title={jsx(() => (
|
||||
<span class="option-title">
|
||||
Hide maxed challenges
|
||||
<desc>Hide challenges that have been fully completed.</desc>
|
||||
</span>
|
||||
))}
|
||||
onUpdate:modelValue={value => (settings.hideChallenges = value)}
|
||||
modelValue={settings.hideChallenges}
|
||||
/>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{ visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined },
|
||||
{ visibility: isHidden(visibility) ? 'hidden' : undefined },
|
||||
unref(style) ?? []
|
||||
]"
|
||||
@click="onClick"
|
||||
|
@ -33,7 +33,7 @@ import MarkNode from "components/MarkNode.vue";
|
|||
import Node from "components/Node.vue";
|
||||
import type { GenericClickable } from "features/clickables/clickable";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { jsx, Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import {
|
||||
coerceComponent,
|
||||
isCoercableComponent,
|
||||
|
@ -55,7 +55,7 @@ export default defineComponent({
|
|||
required: true
|
||||
},
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
style: processedPropType<StyleValue>(Object, String, Array),
|
||||
|
@ -92,12 +92,12 @@ export default defineComponent({
|
|||
comp.value = coerceComponent(currDisplay);
|
||||
return;
|
||||
}
|
||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description, "div");
|
||||
comp.value = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title ? (
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
|
@ -115,6 +115,8 @@ export default defineComponent({
|
|||
stop,
|
||||
comp,
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden,
|
||||
unref
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
|
@ -16,7 +22,7 @@ import { computed, unref } from "vue";
|
|||
export const ClickableType = Symbol("Clickable");
|
||||
|
||||
export interface ClickableOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
canClick?: Computable<boolean>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
|
@ -55,7 +61,7 @@ export type Clickable<T extends ClickableOptions> = Replace<
|
|||
export type GenericClickable = Replace<
|
||||
Clickable<ClickableOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
@ -81,7 +87,7 @@ export function createClickable<T extends ClickableOptions>(
|
|||
if (clickable.onClick) {
|
||||
const onClick = clickable.onClick.bind(clickable);
|
||||
clickable.onClick = function (e) {
|
||||
if (unref(clickable.canClick)) {
|
||||
if (unref(clickable.canClick) !== false) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
|
@ -89,7 +95,7 @@ export function createClickable<T extends ClickableOptions>(
|
|||
if (clickable.onHold) {
|
||||
const onHold = clickable.onHold.bind(clickable);
|
||||
clickable.onHold = function () {
|
||||
if (unref(clickable.canClick)) {
|
||||
if (unref(clickable.canClick) !== false) {
|
||||
onHold();
|
||||
}
|
||||
};
|
||||
|
@ -131,7 +137,8 @@ export function setupAutoClick(
|
|||
clickable: GenericClickable,
|
||||
autoActive: Computable<boolean> = true
|
||||
): Unsubscribe {
|
||||
const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
|
||||
const isActive: ProcessedComputable<boolean> =
|
||||
typeof autoActive === "function" ? computed(autoActive) : autoActive;
|
||||
return layer.on("update", () => {
|
||||
if (unref(isActive) && unref(clickable.canClick)) {
|
||||
clickable.onClick?.();
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { setDefault } from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import { InvertibleFormula } from "game/formulas/types";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import type { Modifier } from "game/modifiers";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import type { WithRequired } from "util/common";
|
||||
import type { Computable, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed";
|
||||
import { convertComputable, processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
|
@ -15,9 +14,10 @@ import { computed, unref } from "vue";
|
|||
/** An object that configures a {@link Conversion}. */
|
||||
export interface ConversionOptions {
|
||||
/**
|
||||
* The scaling function that is used to determine the rate of conversion from one {@link features/resources/resource.Resource} to the other.
|
||||
* The formula used to determine how much {@link gainResource} should be earned by this converting.
|
||||
* When evaluating, the variable will always be overidden to the amount of {@link baseResource}.
|
||||
*/
|
||||
scaling: ScalingFunction;
|
||||
formula: InvertibleFormula;
|
||||
/**
|
||||
* How much of the output resource the conversion can currently convert for.
|
||||
* Typically this will be set for you in a conversion constructor.
|
||||
|
@ -53,10 +53,6 @@ export interface ConversionOptions {
|
|||
* Defaults to true.
|
||||
*/
|
||||
buyMax?: Computable<boolean>;
|
||||
/**
|
||||
* Whether or not to round up the cost to generate a given amount of the output resource.
|
||||
*/
|
||||
roundUpCost?: Computable<boolean>;
|
||||
/**
|
||||
* The function that performs the actual conversion from {@link baseResource} to {@link gainResource}.
|
||||
* Typically this will be set for you in a conversion constructor.
|
||||
|
@ -73,20 +69,6 @@ export interface ConversionOptions {
|
|||
* This will not be called whenever using currentGain without calling convert (e.g. passive generation)
|
||||
*/
|
||||
onConvert?: (amountGained: DecimalSource) => void;
|
||||
/**
|
||||
* An additional modifier that will be applied to the gain amounts.
|
||||
* Must be reversible in order to correctly calculate {@link nextAt}.
|
||||
* @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers.
|
||||
*/
|
||||
gainModifier?: WithRequired<Modifier, "revert">;
|
||||
/**
|
||||
* A modifier that will be applied to the cost amounts.
|
||||
* That is to say, this modifier will be applied to the amount of baseResource before going into the scaling function.
|
||||
* A cost modifier of x0.5 would give gain amounts equal to the player having half the baseResource they actually have.
|
||||
* Must be reversible in order to correctly calculate {@link nextAt}.
|
||||
* @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers.
|
||||
*/
|
||||
costModifier?: WithRequired<Modifier, "revert">;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,7 +91,6 @@ export type Conversion<T extends ConversionOptions> = Replace<
|
|||
nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>;
|
||||
buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
|
||||
spend: undefined extends T["spend"] ? (amountGained: DecimalSource) => void : T["spend"];
|
||||
roundUpCost: GetComputableTypeWithDefault<T["roundUpCost"], true>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -123,7 +104,6 @@ export type GenericConversion = Replace<
|
|||
nextAt: ProcessedComputable<DecimalSource>;
|
||||
buyMax: ProcessedComputable<boolean>;
|
||||
spend: (amountGained: DecimalSource) => void;
|
||||
roundUpCost: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -142,14 +122,10 @@ export function createConversion<T extends ConversionOptions>(
|
|||
|
||||
if (conversion.currentGain == null) {
|
||||
conversion.currentGain = computed(() => {
|
||||
let gain = conversion.gainModifier
|
||||
? conversion.gainModifier.apply(
|
||||
conversion.scaling.currentGain(conversion as GenericConversion)
|
||||
)
|
||||
: conversion.scaling.currentGain(conversion as GenericConversion);
|
||||
let gain = conversion.formula.evaluate(conversion.baseResource.value);
|
||||
gain = Decimal.floor(gain).max(0);
|
||||
|
||||
if (!unref(conversion.buyMax)) {
|
||||
if (unref(conversion.buyMax) === false) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
return gain;
|
||||
|
@ -160,16 +136,16 @@ export function createConversion<T extends ConversionOptions>(
|
|||
}
|
||||
if (conversion.currentAt == null) {
|
||||
conversion.currentAt = computed(() => {
|
||||
let current = conversion.scaling.currentAt(conversion as GenericConversion);
|
||||
if (conversion.roundUpCost) current = Decimal.ceil(current);
|
||||
return current;
|
||||
return conversion.formula.invert(
|
||||
Decimal.floor(unref((conversion as GenericConversion).currentGain))
|
||||
);
|
||||
});
|
||||
}
|
||||
if (conversion.nextAt == null) {
|
||||
conversion.nextAt = computed(() => {
|
||||
let next = conversion.scaling.nextAt(conversion as GenericConversion);
|
||||
if (conversion.roundUpCost) next = Decimal.ceil(next);
|
||||
return next;
|
||||
return conversion.formula.invert(
|
||||
Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -197,177 +173,11 @@ export function createConversion<T extends ConversionOptions>(
|
|||
processComputable(conversion as T, "nextAt");
|
||||
processComputable(conversion as T, "buyMax");
|
||||
setDefault(conversion, "buyMax", true);
|
||||
processComputable(conversion as T, "roundUpCost");
|
||||
setDefault(conversion, "roundUpCost", true);
|
||||
|
||||
return conversion as unknown as Conversion<T>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of functions that allow a conversion to scale the amount of resources gained based on the input resource.
|
||||
* This typically shouldn't be created directly. Instead use one of the scaling function constructors.
|
||||
* @see {@link createLinearScaling}.
|
||||
* @see {@link createPolynomialScaling}.
|
||||
*/
|
||||
export interface ScalingFunction {
|
||||
/**
|
||||
* Calculates the amount of the output resource a conversion should be able to currently produce.
|
||||
* This should be based off of `conversion.baseResource.value`.
|
||||
* The conversion is responsible for applying the gainModifier, so this function should be un-modified.
|
||||
* It does not need to be clamped or rounded.
|
||||
*/
|
||||
currentGain: (conversion: GenericConversion) => DecimalSource;
|
||||
/**
|
||||
* Calculates the amount of the input resource that is required for the current value of `conversion.currentGain`.
|
||||
* Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate.
|
||||
* The conversion is responsible for rounding up the amount as appropriate.
|
||||
* The returned value should not be below 0.
|
||||
*/
|
||||
currentAt: (conversion: GenericConversion) => DecimalSource;
|
||||
/**
|
||||
* Calculates the amount of the input resource that would be required for the current value of `conversion.currentGain` to increase.
|
||||
* Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate.
|
||||
* The conversion is responsible for rounding up the amount as appropriate.
|
||||
* The returned value should not be below 0.
|
||||
*/
|
||||
nextAt: (conversion: GenericConversion) => DecimalSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scaling function based off the formula `(baseResource - base) * coefficient`.
|
||||
* If the baseResource value is less than base then the currentGain will be 0.
|
||||
* @param base The base variable in the scaling formula.
|
||||
* @param coefficient The coefficient variable in the scaling formula.
|
||||
* @example
|
||||
* A scaling function created via `createLinearScaling(10, 0.5)` would produce the following values:
|
||||
* | Base Resource | Current Gain |
|
||||
* | ------------- | ------------ |
|
||||
* | 10 | 1 |
|
||||
* | 12 | 2 |
|
||||
* | 20 | 6 |
|
||||
*/
|
||||
export function createLinearScaling(
|
||||
base: Computable<DecimalSource>,
|
||||
coefficient: Computable<DecimalSource>
|
||||
): ScalingFunction {
|
||||
const processedBase = convertComputable(base);
|
||||
const processedCoefficient = convertComputable(coefficient);
|
||||
return {
|
||||
currentGain(conversion) {
|
||||
let baseAmount: DecimalSource = unref(conversion.baseResource.value);
|
||||
if (conversion.costModifier) {
|
||||
baseAmount = conversion.costModifier.apply(baseAmount);
|
||||
}
|
||||
if (Decimal.lt(baseAmount, unref(processedBase))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Decimal.sub(baseAmount, unref(processedBase))
|
||||
.sub(1)
|
||||
.times(unref(processedCoefficient))
|
||||
.add(1);
|
||||
},
|
||||
currentAt(conversion) {
|
||||
let current: DecimalSource = unref(conversion.currentGain);
|
||||
if (conversion.gainModifier) {
|
||||
current = conversion.gainModifier.revert(current);
|
||||
}
|
||||
current = Decimal.max(0, current)
|
||||
.sub(1)
|
||||
.div(unref(processedCoefficient))
|
||||
.add(unref(processedBase));
|
||||
if (conversion.costModifier) {
|
||||
current = conversion.costModifier.revert(current);
|
||||
}
|
||||
return current;
|
||||
},
|
||||
nextAt(conversion) {
|
||||
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor();
|
||||
if (conversion.gainModifier) {
|
||||
next = conversion.gainModifier.revert(next);
|
||||
}
|
||||
next = Decimal.max(0, next)
|
||||
.sub(1)
|
||||
.div(unref(processedCoefficient))
|
||||
.add(unref(processedBase))
|
||||
.max(unref(processedBase));
|
||||
if (conversion.costModifier) {
|
||||
next = conversion.costModifier.revert(next);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scaling function based off the formula `(baseResource / base) ^ exponent`.
|
||||
* If the baseResource value is less than base then the currentGain will be 0.
|
||||
* @param base The base variable in the scaling formula.
|
||||
* @param exponent The exponent variable in the scaling formula.
|
||||
* @example
|
||||
* A scaling function created via `createPolynomialScaling(10, 0.5)` would produce the following values:
|
||||
* | Base Resource | Current Gain |
|
||||
* | ------------- | ------------ |
|
||||
* | 10 | 1 |
|
||||
* | 40 | 2 |
|
||||
* | 250 | 5 |
|
||||
*/
|
||||
export function createPolynomialScaling(
|
||||
base: Computable<DecimalSource>,
|
||||
exponent: Computable<DecimalSource>
|
||||
): ScalingFunction {
|
||||
const processedBase = convertComputable(base);
|
||||
const processedExponent = convertComputable(exponent);
|
||||
return {
|
||||
currentGain(conversion) {
|
||||
let baseAmount: DecimalSource = unref(conversion.baseResource.value);
|
||||
if (conversion.costModifier) {
|
||||
baseAmount = conversion.costModifier.apply(baseAmount);
|
||||
}
|
||||
if (Decimal.lt(baseAmount, unref(processedBase))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const gain = Decimal.div(baseAmount, unref(processedBase)).pow(
|
||||
unref(processedExponent)
|
||||
);
|
||||
|
||||
if (gain.isNan()) {
|
||||
return new Decimal(0);
|
||||
}
|
||||
return gain;
|
||||
},
|
||||
currentAt(conversion) {
|
||||
let current: DecimalSource = unref(conversion.currentGain);
|
||||
if (conversion.gainModifier) {
|
||||
current = conversion.gainModifier.revert(current);
|
||||
}
|
||||
current = Decimal.max(0, current)
|
||||
.root(unref(processedExponent))
|
||||
.times(unref(processedBase));
|
||||
if (conversion.costModifier) {
|
||||
current = conversion.costModifier.revert(current);
|
||||
}
|
||||
return current;
|
||||
},
|
||||
nextAt(conversion) {
|
||||
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor();
|
||||
if (conversion.gainModifier) {
|
||||
next = conversion.gainModifier.revert(next);
|
||||
}
|
||||
next = Decimal.max(0, next)
|
||||
.root(unref(processedExponent))
|
||||
.times(unref(processedBase))
|
||||
.max(unref(processedBase));
|
||||
if (conversion.costModifier) {
|
||||
next = conversion.costModifier.revert(next);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a conversion that simply adds to the gainResource amount upon converting.
|
||||
* This is similar to the behavior of "normal" layers in The Modding Tree.
|
||||
|
@ -395,14 +205,9 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
|||
|
||||
if (conversion.currentGain == null) {
|
||||
conversion.currentGain = computed(() => {
|
||||
let gain = conversion.gainModifier
|
||||
? conversion.gainModifier.apply(
|
||||
conversion.scaling.currentGain(conversion as GenericConversion)
|
||||
)
|
||||
: conversion.scaling.currentGain(conversion as GenericConversion);
|
||||
let gain = conversion.formula.evaluate(conversion.baseResource.value);
|
||||
gain = Decimal.floor(gain).max(conversion.gainResource.value);
|
||||
|
||||
if (!unref(conversion.buyMax)) {
|
||||
if (unref(conversion.buyMax) === false) {
|
||||
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
|
||||
}
|
||||
return gain;
|
||||
|
@ -411,11 +216,11 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
|||
if (conversion.actualGain == null) {
|
||||
conversion.actualGain = computed(() => {
|
||||
let gain = Decimal.sub(
|
||||
Decimal.floor(conversion.scaling.currentGain(conversion as GenericConversion)),
|
||||
conversion.formula.evaluate(conversion.baseResource.value),
|
||||
conversion.gainResource.value
|
||||
).max(0);
|
||||
|
||||
if (!unref(conversion.buyMax)) {
|
||||
if (unref(conversion.buyMax) === false) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
return gain;
|
||||
|
@ -423,11 +228,7 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
|||
}
|
||||
setDefault(conversion, "convert", function () {
|
||||
const amountGained = unref((conversion as GenericConversion).actualGain);
|
||||
conversion.gainResource.value = conversion.gainModifier
|
||||
? conversion.gainModifier.apply(
|
||||
unref((conversion as GenericConversion).currentGain)
|
||||
)
|
||||
: unref((conversion as GenericConversion).currentGain);
|
||||
conversion.gainResource.value = unref((conversion as GenericConversion).currentGain);
|
||||
(conversion as GenericConversion).spend(amountGained);
|
||||
conversion.onConvert?.(amountGained);
|
||||
});
|
||||
|
@ -459,71 +260,9 @@ export function setupPassiveGeneration(
|
|||
conversion.gainResource.value = Decimal.add(
|
||||
conversion.gainResource.value,
|
||||
Decimal.times(currRate, diff).times(Decimal.ceil(unref(conversion.actualGain)))
|
||||
).min(unref(processedCap) ?? Decimal.dInf);
|
||||
)
|
||||
.min(unref(processedCap) ?? Decimal.dInf)
|
||||
.max(conversion.gainResource.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a value, this function finds the amount above a certain value and raises it to a power.
|
||||
* If the power is <1, this will effectively make the value scale slower after the cap.
|
||||
* @param value The raw value.
|
||||
* @param cap The value after which the softcap should be applied.
|
||||
* @param power The power to raise value above the cap to.
|
||||
* @example
|
||||
* A softcap added via `addSoftcap(scaling, 100, 0.5)` would produce the following values:
|
||||
* | Raw Value | Softcapped Value |
|
||||
* | --------- | ---------------- |
|
||||
* | 1 | 1 |
|
||||
* | 100 | 100 |
|
||||
* | 125 | 105 |
|
||||
* | 200 | 110 |
|
||||
*/
|
||||
export function softcap(
|
||||
value: DecimalSource,
|
||||
cap: DecimalSource,
|
||||
power: DecimalSource = 0.5
|
||||
): DecimalSource {
|
||||
if (Decimal.lte(value, cap)) {
|
||||
return value;
|
||||
} else {
|
||||
return Decimal.pow(value, power).times(Decimal.pow(cap, Decimal.sub(1, power)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scaling function based off an existing scaling function, with a softcap applied to it.
|
||||
* The softcap will take any value above a certain value and raise it to a power.
|
||||
* If the power is <1, this will effectively make the value scale slower after the cap.
|
||||
* @param scaling The raw scaling function.
|
||||
* @param cap The value after which the softcap should be applied.
|
||||
* @param power The power to raise value about the cap to.
|
||||
* @see {@link softcap}.
|
||||
*/
|
||||
export function addSoftcap(
|
||||
scaling: ScalingFunction,
|
||||
cap: ProcessedComputable<DecimalSource>,
|
||||
power: ProcessedComputable<DecimalSource> = 0.5
|
||||
): ScalingFunction {
|
||||
return {
|
||||
...scaling,
|
||||
currentGain: conversion =>
|
||||
softcap(scaling.currentGain(conversion), unref(cap), unref(power))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scaling function off an existing function, with a hardcap applied to it.
|
||||
* The harcap will ensure that the currentGain will stop at a given cap.
|
||||
* @param scaling The raw scaling function.
|
||||
* @param cap The maximum value the scaling function can output.
|
||||
*/
|
||||
export function addHardcap(
|
||||
scaling: ScalingFunction,
|
||||
cap: ProcessedComputable<DecimalSource>
|
||||
): ScalingFunction {
|
||||
return {
|
||||
...scaling,
|
||||
currentGain: conversion => Decimal.min(scaling.currentGain(conversion), unref(cap))
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Decimal from "util/bignum";
|
||||
import { DoNotCache } from "util/computed";
|
||||
import { DoNotCache, ProcessedComputable } from "util/computed";
|
||||
import type { CSSProperties, DefineComponent } from "vue";
|
||||
import { isRef } from "vue";
|
||||
import { isRef, unref } from "vue";
|
||||
|
||||
/**
|
||||
* A symbol to use as a key for a vue component a feature can be rendered with
|
||||
|
@ -67,6 +67,16 @@ export enum Visibility {
|
|||
None
|
||||
}
|
||||
|
||||
export function isVisible(visibility: ProcessedComputable<Visibility | boolean>) {
|
||||
const currVisibility = unref(visibility);
|
||||
return currVisibility !== Visibility.None && currVisibility !== false;
|
||||
}
|
||||
|
||||
export function isHidden(visibility: ProcessedComputable<Visibility | boolean>) {
|
||||
const currVisibility = unref(visibility);
|
||||
return currVisibility === Visibility.Hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a function and marks it as JSX so it won't get auto-wrapped into a ComputedRef.
|
||||
* The function may also return empty string as empty JSX tags cause issues.
|
||||
|
@ -76,11 +86,6 @@ export function jsx(func: () => JSX.Element | ""): JSXFunction {
|
|||
return func as JSXFunction;
|
||||
}
|
||||
|
||||
/** Utility function to convert a boolean value into a Visbility value */
|
||||
export function showIf(condition: boolean, otherwise = Visibility.None): Visibility {
|
||||
return condition ? Visibility.Visible : otherwise;
|
||||
}
|
||||
|
||||
/** Utility function to set a property on an object if and only if it doesn't already exist */
|
||||
export function setDefault<T, K extends keyof T>(
|
||||
object: T,
|
||||
|
@ -102,7 +107,7 @@ export function findFeatures(obj: Record<string, unknown>, ...types: symbol[]):
|
|||
const handleObject = (obj: Record<string, unknown>) => {
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
if (value && typeof value === "object") {
|
||||
if (value != null && typeof value === "object") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (types.includes((value as Record<string, any>).type)) {
|
||||
objects.push(value);
|
||||
|
@ -127,7 +132,7 @@ export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]
|
|||
const handleObject = (obj: Record<string, unknown>) => {
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
if (value && typeof value === "object") {
|
||||
if (value != null && typeof value === "object") {
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
typeof (value as Record<string, any>).type == "symbol" &&
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
}"
|
||||
class="table"
|
||||
class="table-grid"
|
||||
>
|
||||
<div v-for="row in unref(rows)" class="row" :class="{ mergeAdjacent }" :key="row">
|
||||
<div v-for="row in unref(rows)" class="row-grid" :class="{ mergeAdjacent }" :key="row">
|
||||
<GridCell
|
||||
v-for="col in unref(cols)"
|
||||
:key="col"
|
||||
|
@ -19,7 +19,7 @@
|
|||
<script lang="ts">
|
||||
import "components/common/table.css";
|
||||
import themes from "data/themes";
|
||||
import { Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import type { GridCell } from "features/grids/grid";
|
||||
import settings from "game/settings";
|
||||
import { processedPropType } from "util/vue";
|
||||
|
@ -29,7 +29,7 @@ import GridCellVue from "./GridCell.vue";
|
|||
export default defineComponent({
|
||||
props: {
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
rows: {
|
||||
|
@ -54,7 +54,7 @@ export default defineComponent({
|
|||
return { visibility, onClick, onHold, display, title, style, canClick, id };
|
||||
}
|
||||
|
||||
return { unref, gatherCellProps, Visibility, mergeAdjacent };
|
||||
return { unref, gatherCellProps, Visibility, mergeAdjacent, isVisible, isHidden };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:class="{ feature: true, tile: true, can: unref(canClick), locked: !unref(canClick) }"
|
||||
:style="[
|
||||
{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
|
@ -26,7 +26,7 @@
|
|||
import "components/common/features.css";
|
||||
import Node from "components/Node.vue";
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import {
|
||||
computeComponent,
|
||||
computeOptionalComponent,
|
||||
|
@ -39,7 +39,7 @@ import { defineComponent, toRefs, unref } from "vue";
|
|||
export default defineComponent({
|
||||
props: {
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
onClick: Function as PropType<(e?: MouseEvent | TouchEvent) => void>,
|
||||
|
@ -76,7 +76,9 @@ export default defineComponent({
|
|||
titleComponent,
|
||||
component,
|
||||
Visibility,
|
||||
unref
|
||||
unref,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -171,7 +171,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
|
||||
export interface GridCell {
|
||||
id: string;
|
||||
visibility: Visibility;
|
||||
visibility: Visibility | boolean;
|
||||
canClick: boolean;
|
||||
startState: State;
|
||||
state: State;
|
||||
|
@ -184,10 +184,10 @@ export interface GridCell {
|
|||
}
|
||||
|
||||
export interface GridOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
rows: Computable<number>;
|
||||
cols: Computable<number>;
|
||||
getVisibility?: CellComputable<Visibility>;
|
||||
getVisibility?: CellComputable<Visibility | boolean>;
|
||||
getCanClick?: CellComputable<boolean>;
|
||||
getStartState: Computable<State> | ((id: string | number) => State);
|
||||
getStyle?: CellComputable<StyleValue>;
|
||||
|
@ -229,8 +229,8 @@ export type Grid<T extends GridOptions> = Replace<
|
|||
export type GenericGrid = Replace<
|
||||
Grid<GridOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
getVisibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
getVisibility: ProcessedComputable<Visibility | boolean>;
|
||||
getCanClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
@ -238,7 +238,7 @@ export type GenericGrid = Replace<
|
|||
export function createGrid<T extends GridOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseGrid, GenericGrid>
|
||||
): Grid<T> {
|
||||
const cellState = persistent<Record<string | number, State>>({});
|
||||
const cellState = persistent<Record<string | number, State>>({}, false);
|
||||
return createLazyProxy(() => {
|
||||
const grid = optionsFunc();
|
||||
grid.id = getUniqueID("grid-");
|
||||
|
@ -277,9 +277,9 @@ export function createGrid<T extends GridOptions>(
|
|||
|
||||
if (grid.onClick) {
|
||||
const onClick = grid.onClick.bind(grid);
|
||||
grid.onClick = function (id, state) {
|
||||
grid.onClick = function (id, state, e) {
|
||||
if (unref((grid as GenericGrid).cells[id].canClick)) {
|
||||
onClick(id, state);
|
||||
onClick(id, state, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { shallowReactive, unref } from "vue";
|
||||
import Hotkey from "components/Hotkey.vue";
|
||||
|
||||
export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({});
|
||||
export const HotkeyType = Symbol("Hotkey");
|
||||
|
@ -43,6 +44,8 @@ export type GenericHotkey = Replace<
|
|||
}
|
||||
>;
|
||||
|
||||
const uppercaseNumbers = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
|
||||
|
||||
export function createHotkey<T extends HotkeyOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseHotkey, GenericHotkey>
|
||||
): Hotkey<T> {
|
||||
|
@ -78,7 +81,9 @@ document.onkeydown = function (e) {
|
|||
return;
|
||||
}
|
||||
let key = e.key;
|
||||
if (e.shiftKey) {
|
||||
if (uppercaseNumbers.includes(key)) {
|
||||
key = "shift+" + uppercaseNumbers.indexOf(key);
|
||||
} else if (e.shiftKey) {
|
||||
key = "shift+" + key;
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
|
@ -101,11 +106,13 @@ registerInfoComponent(
|
|||
<div>
|
||||
<br />
|
||||
<h4>Hotkeys</h4>
|
||||
{keys.map(hotkey => (
|
||||
<div>
|
||||
{hotkey?.key}: {hotkey?.description}
|
||||
</div>
|
||||
))}
|
||||
<div style="column-count: 2">
|
||||
{keys.map(hotkey => (
|
||||
<div>
|
||||
<Hotkey hotkey={hotkey as GenericHotkey} /> {hotkey?.description}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div
|
||||
class="infobox"
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
borderColor: unref(color),
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
|
@ -33,7 +33,7 @@ import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTrans
|
|||
import Node from "components/Node.vue";
|
||||
import themes from "data/themes";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import settings from "game/settings";
|
||||
import { computeComponent, processedPropType } from "util/vue";
|
||||
import type { PropType, Ref, StyleValue } from "vue";
|
||||
|
@ -42,7 +42,7 @@ import { computed, defineComponent, toRefs, unref } from "vue";
|
|||
export default defineComponent({
|
||||
props: {
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
display: {
|
||||
|
@ -83,7 +83,9 @@ export default defineComponent({
|
|||
bodyComponent,
|
||||
stacked,
|
||||
unref,
|
||||
Visibility
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import { unref } from "vue";
|
|||
export const InfoboxType = Symbol("Infobox");
|
||||
|
||||
export interface InfoboxOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
color?: Computable<string>;
|
||||
style?: Computable<StyleValue>;
|
||||
titleStyle?: Computable<StyleValue>;
|
||||
|
@ -51,14 +51,14 @@ export type Infobox<T extends InfoboxOptions> = Replace<
|
|||
export type GenericInfobox = Replace<
|
||||
Infobox<InfoboxOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createInfobox<T extends InfoboxOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseInfobox, GenericInfobox>
|
||||
): Infobox<T> {
|
||||
const collapsed = persistent<boolean>(false);
|
||||
const collapsed = persistent<boolean>(false, false);
|
||||
return createLazyProxy(() => {
|
||||
const infobox = optionsFunc();
|
||||
infobox.id = getUniqueID("infobox-");
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
|
@ -18,7 +18,7 @@
|
|||
import "components/common/features.css";
|
||||
import Node from "components/Node.vue";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { jsx, Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import type { GenericMilestone } from "features/milestones/milestone";
|
||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
||||
import type { Component, UnwrapRef } from "vue";
|
||||
|
@ -27,7 +27,7 @@ import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
|||
export default defineComponent({
|
||||
props: {
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
display: {
|
||||
|
@ -74,12 +74,12 @@ export default defineComponent({
|
|||
jsx(() => (
|
||||
<span>
|
||||
<Requirement />
|
||||
{currDisplay.effectDisplay ? (
|
||||
{currDisplay.effectDisplay != null ? (
|
||||
<div>
|
||||
<EffectDisplay />
|
||||
</div>
|
||||
) : null}
|
||||
{currDisplay.optionsDisplay ? (
|
||||
{currDisplay.optionsDisplay != null ? (
|
||||
<div class="equal-spaced">
|
||||
<OptionsDisplay />
|
||||
</div>
|
||||
|
@ -92,7 +92,9 @@ export default defineComponent({
|
|||
return {
|
||||
comp,
|
||||
unref,
|
||||
Visibility
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
import Select from "components/fields/Select.vue";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
isVisible,
|
||||
jsx,
|
||||
setDefault,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import MilestoneComponent from "features/milestones/Milestone.vue";
|
||||
import { globalBus } from "game/events";
|
||||
import "game/notifications";
|
||||
|
@ -34,7 +48,7 @@ export enum MilestoneDisplay {
|
|||
}
|
||||
|
||||
export interface MilestoneOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
shouldEarn?: () => boolean;
|
||||
style?: Computable<StyleValue>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
|
@ -46,6 +60,7 @@ export interface MilestoneOptions {
|
|||
optionsDisplay?: CoercableComponent;
|
||||
}
|
||||
>;
|
||||
showPopups?: Computable<boolean>;
|
||||
onComplete?: VoidFunction;
|
||||
}
|
||||
|
||||
|
@ -65,20 +80,21 @@ export type Milestone<T extends MilestoneOptions> = Replace<
|
|||
style: GetComputableType<T["style"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
showPopups: GetComputableType<T["showPopups"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GenericMilestone = Replace<
|
||||
Milestone<MilestoneOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createMilestone<T extends MilestoneOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseMilestone, GenericMilestone>
|
||||
): Milestone<T> {
|
||||
const earned = persistent<boolean>(false);
|
||||
const earned = persistent<boolean>(false, false);
|
||||
return createLazyProxy(() => {
|
||||
const milestone = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
milestone.id = getUniqueID("milestone-");
|
||||
|
@ -87,12 +103,30 @@ export function createMilestone<T extends MilestoneOptions>(
|
|||
|
||||
milestone.earned = earned;
|
||||
milestone.complete = function () {
|
||||
const genericMilestone = milestone as GenericMilestone;
|
||||
earned.value = true;
|
||||
genericMilestone.onComplete?.();
|
||||
if (genericMilestone.display != null && unref(genericMilestone.showPopups) === true) {
|
||||
const display = unref(genericMilestone.display);
|
||||
const Display = coerceComponent(
|
||||
isCoercableComponent(display) ? display : display.requirement
|
||||
);
|
||||
toast(
|
||||
<>
|
||||
<h3>Milestone earned!</h3>
|
||||
<div>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<Display />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
processComputable(milestone as T, "visibility");
|
||||
setDefault(milestone, "visibility", Visibility.Visible);
|
||||
const visibility = milestone.visibility as ProcessedComputable<Visibility>;
|
||||
const visibility = milestone.visibility as ProcessedComputable<Visibility | boolean>;
|
||||
milestone.visibility = computed(() => {
|
||||
const display = unref((milestone as GenericMilestone).display);
|
||||
switch (settings.msDisplay) {
|
||||
|
@ -124,6 +158,7 @@ export function createMilestone<T extends MilestoneOptions>(
|
|||
processComputable(milestone as T, "style");
|
||||
processComputable(milestone as T, "classes");
|
||||
processComputable(milestone as T, "display");
|
||||
processComputable(milestone as T, "showPopups");
|
||||
|
||||
milestone[GatherProps] = function (this: GenericMilestone) {
|
||||
const { visibility, display, style, classes, earned, id } = this;
|
||||
|
@ -136,12 +171,15 @@ export function createMilestone<T extends MilestoneOptions>(
|
|||
if (settings.active !== player.id) return;
|
||||
if (
|
||||
!genericMilestone.earned.value &&
|
||||
unref(genericMilestone.visibility) === Visibility.Visible &&
|
||||
isVisible(genericMilestone.visibility) &&
|
||||
genericMilestone.shouldEarn?.()
|
||||
) {
|
||||
genericMilestone.earned.value = true;
|
||||
genericMilestone.onComplete?.();
|
||||
if (genericMilestone.display) {
|
||||
if (
|
||||
genericMilestone.display != null &&
|
||||
unref(genericMilestone.showPopups) === true
|
||||
) {
|
||||
const display = unref(genericMilestone.display);
|
||||
const Display = coerceComponent(
|
||||
isCoercableComponent(display) ? display : display.requirement
|
||||
|
@ -183,7 +221,12 @@ const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({
|
|||
registerSettingField(
|
||||
jsx(() => (
|
||||
<Select
|
||||
title="Show Milestones"
|
||||
title={jsx(() => (
|
||||
<span class="option-title">
|
||||
Show milestones
|
||||
<desc>Select which milestones to display based on criterias.</desc>
|
||||
</span>
|
||||
))}
|
||||
options={msDisplayOptions}
|
||||
onUpdate:modelValue={value => (settings.msDisplay = value as MilestoneDisplay)}
|
||||
modelValue={settings.msDisplay}
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="tsx">
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { Application } from "@pixi/app";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import "lib/pixi";
|
||||
import { processedPropType } from "util/vue";
|
||||
import type { PropType } from "vue";
|
||||
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref, unref } from "vue";
|
||||
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref } from "vue";
|
||||
|
||||
// TODO get typing support on the Particles component
|
||||
export default defineComponent({
|
||||
|
@ -31,10 +33,10 @@ export default defineComponent({
|
|||
onHotReload: Function as PropType<VoidFunction>
|
||||
},
|
||||
setup(props) {
|
||||
const app = ref<null | Application>(null);
|
||||
const app = shallowRef<null | Application>(null);
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateBounds);
|
||||
const resizeListener = ref<HTMLElement | null>(null);
|
||||
const resizeListener = shallowRef<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
||||
|
@ -62,15 +64,14 @@ export default defineComponent({
|
|||
if (isDirty) {
|
||||
isDirty = false;
|
||||
nextTick(() => {
|
||||
if (resizeListener.value != null && props.onContainerResized) {
|
||||
props.onContainerResized(resizeListener.value.getBoundingClientRect());
|
||||
app.value?.resize();
|
||||
if (resizeListener.value != null) {
|
||||
props.onContainerResized?.(resizeListener.value.getBoundingClientRect());
|
||||
}
|
||||
isDirty = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
document.fonts.ready.then(updateBounds);
|
||||
globalBus.on("fontsLoaded", updateBounds);
|
||||
|
||||
return {
|
||||
unref,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Application } from "@pixi/app";
|
||||
import type { EmitterConfigV3 } from "@pixi/particle-emitter";
|
||||
import { Emitter, upgradeConfig } from "@pixi/particle-emitter";
|
||||
import type { GenericComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID } from "features/feature";
|
||||
import ParticlesComponent from "features/particles/Particles.vue";
|
||||
import { Application } from "@pixi/app";
|
||||
import type { Computable, GetComputableType } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Ref, shallowRef, unref } from "vue";
|
||||
|
@ -62,7 +62,8 @@ export function createParticles<T extends ParticlesOptions>(
|
|||
}[] = [];
|
||||
|
||||
function onInit(app: Application) {
|
||||
(particles as GenericParticles).app.value = app;
|
||||
const genericParticles = particles as GenericParticles;
|
||||
genericParticles.app.value = app;
|
||||
emittersToAdd.forEach(({ resolve, config }) => resolve(new Emitter(app.stage, config)));
|
||||
emittersToAdd = [];
|
||||
}
|
||||
|
|
278
src/features/repeatable.tsx
Normal file
278
src/features/repeatable.tsx
Normal file
|
@ -0,0 +1,278 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
|
||||
import { DefaultValue, Persistent, persistent } from "game/persistence";
|
||||
import {
|
||||
createVisibilityRequirement,
|
||||
displayRequirements,
|
||||
maxRequirementsMet,
|
||||
payRequirements,
|
||||
Requirements,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { formatWhole } from "util/bignum";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Repeatable} features. */
|
||||
export const RepeatableType = Symbol("Repeatable");
|
||||
|
||||
/** A type that can be used to customize the {@link Repeatable} display. */
|
||||
export type RepeatableDisplay =
|
||||
| CoercableComponent
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: CoercableComponent;
|
||||
/** The main text that appears in the display. */
|
||||
description?: CoercableComponent;
|
||||
/** A description of the current effect of this repeatable, bsed off its amount. */
|
||||
effectDisplay?: CoercableComponent;
|
||||
/** Whether or not to show the current amount of this repeatable at the bottom of the display. */
|
||||
showAmount?: boolean;
|
||||
};
|
||||
|
||||
/** An object that configures a {@link Repeatable}. */
|
||||
export interface RepeatableOptions {
|
||||
/** Whether this repeatable should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** The requirement(s) to increase this repeatable. */
|
||||
requirements: Requirements;
|
||||
/** The maximum amount obtainable for this repeatable. */
|
||||
limit?: Computable<DecimalSource>;
|
||||
/** The initial amount this repeatable has on a new save / after reset. */
|
||||
initialAmount?: DecimalSource;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
/** Toggles a smaller design for the feature. */
|
||||
small?: Computable<boolean>;
|
||||
/** Whether or not clicking this repeatable should attempt to maximize amount based on the requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */
|
||||
maximize?: Computable<boolean>;
|
||||
/** The display to use for this repeatable. */
|
||||
display?: Computable<RepeatableDisplay>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}.
|
||||
*/
|
||||
export interface BaseRepeatable {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persistent between refreshes or updates. */
|
||||
id: string;
|
||||
/** The current amount this repeatable has. */
|
||||
amount: Persistent<DecimalSource>;
|
||||
/** Whether or not this repeatable's amount is at it's limit. */
|
||||
maxed: Ref<boolean>;
|
||||
/** Whether or not this repeatable can be clicked. */
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
/**
|
||||
* How much amount can be increased by, or 1 if unclickable.
|
||||
* Capped at 1 if {@link RepeatableOptions.maximize} is false.
|
||||
**/
|
||||
amountToIncrease: Ref<DecimalSource>;
|
||||
/** A function that gets called when this repeatable is clicked. */
|
||||
onClick: (event?: MouseEvent | TouchEvent) => void;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof RepeatableType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: typeof ClickableComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature with multiple "levels" with scaling requirements. */
|
||||
export type Repeatable<T extends RepeatableOptions> = Replace<
|
||||
T & BaseRepeatable,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
requirements: GetComputableType<T["requirements"]>;
|
||||
limit: GetComputableTypeWithDefault<T["limit"], Decimal>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
small: GetComputableType<T["small"]>;
|
||||
maximize: GetComputableType<T["maximize"]>;
|
||||
display: Ref<CoercableComponent>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Repeatable} object. */
|
||||
export type GenericRepeatable = Replace<
|
||||
Repeatable<RepeatableOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
limit: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a repeatable with the given options.
|
||||
* @param optionsFunc Repeatable options.
|
||||
*/
|
||||
export function createRepeatable<T extends RepeatableOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>
|
||||
): Repeatable<T> {
|
||||
const amount = persistent<DecimalSource>(0);
|
||||
return createLazyProxy(() => {
|
||||
const repeatable = optionsFunc();
|
||||
|
||||
repeatable.id = getUniqueID("repeatable-");
|
||||
repeatable.type = RepeatableType;
|
||||
repeatable[Component] = ClickableComponent;
|
||||
|
||||
repeatable.amount = amount;
|
||||
repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
|
||||
|
||||
const limitRequirement = {
|
||||
requirementMet: computed(() =>
|
||||
Decimal.sub(
|
||||
unref((repeatable as GenericRepeatable).limit),
|
||||
(repeatable as GenericRepeatable).amount.value
|
||||
)
|
||||
),
|
||||
requiresPay: false,
|
||||
visibility: Visibility.None
|
||||
} as const;
|
||||
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
|
||||
if (isArray(repeatable.requirements)) {
|
||||
repeatable.requirements.unshift(visibilityRequirement);
|
||||
repeatable.requirements.push(limitRequirement);
|
||||
} else {
|
||||
repeatable.requirements = [
|
||||
visibilityRequirement,
|
||||
repeatable.requirements,
|
||||
limitRequirement
|
||||
];
|
||||
}
|
||||
|
||||
repeatable.maxed = computed(() =>
|
||||
Decimal.gte(
|
||||
(repeatable as GenericRepeatable).amount.value,
|
||||
unref((repeatable as GenericRepeatable).limit)
|
||||
)
|
||||
);
|
||||
processComputable(repeatable as T, "classes");
|
||||
const classes = repeatable.classes as
|
||||
| ProcessedComputable<Record<string, boolean>>
|
||||
| undefined;
|
||||
repeatable.classes = computed(() => {
|
||||
const currClasses = unref(classes) || {};
|
||||
if ((repeatable as GenericRepeatable).maxed.value) {
|
||||
currClasses.bought = true;
|
||||
}
|
||||
return currClasses;
|
||||
});
|
||||
repeatable.amountToIncrease = computed(() =>
|
||||
unref((repeatable as GenericRepeatable).maximize)
|
||||
? maxRequirementsMet(repeatable.requirements)
|
||||
: 1
|
||||
);
|
||||
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
|
||||
const onClick = repeatable.onClick;
|
||||
repeatable.onClick = function (this: GenericRepeatable, event?: MouseEvent | TouchEvent) {
|
||||
const genericRepeatable = repeatable as GenericRepeatable;
|
||||
if (!unref(genericRepeatable.canClick)) {
|
||||
return;
|
||||
}
|
||||
payRequirements(repeatable.requirements, unref(repeatable.amountToIncrease));
|
||||
genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1);
|
||||
onClick?.(event);
|
||||
};
|
||||
processComputable(repeatable as T, "display");
|
||||
const display = repeatable.display;
|
||||
repeatable.display = jsx(() => {
|
||||
// TODO once processComputable types correctly, remove this "as X"
|
||||
const currDisplay = unref(display) as RepeatableDisplay;
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
const CurrDisplay = coerceComponent(currDisplay);
|
||||
return <CurrDisplay />;
|
||||
}
|
||||
if (currDisplay != null) {
|
||||
const genericRepeatable = repeatable as GenericRepeatable;
|
||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description ?? "");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay ?? "");
|
||||
|
||||
return (
|
||||
<span>
|
||||
{currDisplay.title == null ? null : (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
)}
|
||||
{currDisplay.description == null ? null : <Description />}
|
||||
{currDisplay.showAmount === false ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{unref(genericRepeatable.limit) === Decimal.dInf ? (
|
||||
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
|
||||
) : (
|
||||
<>
|
||||
Amount: {formatWhole(genericRepeatable.amount.value)} /{" "}
|
||||
{formatWhole(unref(genericRepeatable.limit))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currDisplay.effectDisplay == null ? null : (
|
||||
<div>
|
||||
<br />
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
)}
|
||||
{genericRepeatable.maxed.value ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{displayRequirements(
|
||||
genericRepeatable.requirements,
|
||||
unref(repeatable.amountToIncrease)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
processComputable(repeatable as T, "visibility");
|
||||
setDefault(repeatable, "visibility", Visibility.Visible);
|
||||
processComputable(repeatable as T, "limit");
|
||||
setDefault(repeatable, "limit", Decimal.dInf);
|
||||
processComputable(repeatable as T, "style");
|
||||
processComputable(repeatable as T, "mark");
|
||||
processComputable(repeatable as T, "small");
|
||||
processComputable(repeatable as T, "maximize");
|
||||
|
||||
repeatable[GatherProps] = function (this: GenericRepeatable) {
|
||||
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
||||
this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onClick,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
return repeatable as unknown as Repeatable<T>;
|
||||
});
|
||||
}
|
|
@ -2,8 +2,8 @@ import type { OptionsFunc, Replace } from "features/feature";
|
|||
import { getUniqueID } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { DefaultValue, persistent, PersistentState } from "game/persistence";
|
||||
import type { NonPersistent, Persistent } from "game/persistence";
|
||||
import { DefaultValue, persistent } from "game/persistence";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
import Decimal from "util/bignum";
|
||||
import type { Computable, GetComputableType } from "util/computed";
|
||||
|
@ -43,11 +43,10 @@ export function createReset<T extends ResetOptions>(
|
|||
|
||||
reset.reset = function () {
|
||||
const handleObject = (obj: unknown) => {
|
||||
if (obj && typeof obj === "object") {
|
||||
if (PersistentState in obj) {
|
||||
(obj as Persistent)[PersistentState].value = (obj as Persistent)[
|
||||
DefaultValue
|
||||
];
|
||||
if (obj != null && typeof obj === "object") {
|
||||
if (DefaultValue in obj) {
|
||||
const persistent = obj as NonPersistent;
|
||||
persistent.value = persistent[DefaultValue];
|
||||
} else if (!(obj instanceof Decimal) && !isRef(obj)) {
|
||||
Object.values(obj).forEach(obj =>
|
||||
handleObject(obj as Record<string, unknown>)
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<Sticky>
|
||||
<div
|
||||
class="main-display-container"
|
||||
:style="{ height: `${(effectRef?.$el.clientHeight ?? 0) + 50}px` }"
|
||||
:class="classes ?? {}"
|
||||
:style="[{ height: `${(effectRef?.$el.clientHeight ?? 0) + 50}px` }, style ?? {}]"
|
||||
>
|
||||
<div class="main-display">
|
||||
<span v-if="showPrefix">You have </span>
|
||||
|
@ -52,5 +53,6 @@ const showPrefix = computed(() => {
|
|||
vertical-align: middle;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { globalBus } from "game/events";
|
||||
import type { State } from "game/persistence";
|
||||
import { NonPersistent, Persistent, State } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import settings from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, formatWhole } from "util/bignum";
|
||||
import type { ProcessedComputable } from "util/computed";
|
||||
import { loadingSave } from "util/save";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { computed, isRef, ref, watch } from "vue";
|
||||
import { computed, isRef, ref, unref, watch } from "vue";
|
||||
|
||||
export interface Resource<T = DecimalSource> extends Ref<T> {
|
||||
displayName: string;
|
||||
|
@ -12,24 +16,47 @@ export interface Resource<T = DecimalSource> extends Ref<T> {
|
|||
small?: boolean;
|
||||
}
|
||||
|
||||
export function createResource<T extends State>(
|
||||
defaultValue: T,
|
||||
displayName?: string,
|
||||
precision?: number,
|
||||
small?: boolean | undefined
|
||||
): Resource<T> & Persistent<T> & { [NonPersistent]: Resource<T> };
|
||||
export function createResource<T extends State>(
|
||||
defaultValue: Ref<T>,
|
||||
displayName?: string,
|
||||
precision?: number,
|
||||
small?: boolean | undefined
|
||||
): Resource<T>;
|
||||
export function createResource<T extends State>(
|
||||
defaultValue: T | Ref<T>,
|
||||
displayName = "points",
|
||||
precision = 0,
|
||||
small = undefined
|
||||
): Resource<T> {
|
||||
small: boolean | undefined = undefined
|
||||
) {
|
||||
const resource: Partial<Resource<T>> = isRef(defaultValue)
|
||||
? defaultValue
|
||||
: persistent(defaultValue);
|
||||
resource.displayName = displayName;
|
||||
resource.precision = precision;
|
||||
resource.small = small;
|
||||
if (!isRef(defaultValue)) {
|
||||
const nonPersistentResource = (resource as Persistent<T>)[
|
||||
NonPersistent
|
||||
] as unknown as Resource<T>;
|
||||
nonPersistentResource.displayName = displayName;
|
||||
nonPersistentResource.precision = precision;
|
||||
nonPersistentResource.small = small;
|
||||
}
|
||||
return resource as Resource<T>;
|
||||
}
|
||||
|
||||
export function trackBest(resource: Resource): Ref<DecimalSource> {
|
||||
const best = persistent(resource.value);
|
||||
watch(resource, amount => {
|
||||
if (loadingSave.value) {
|
||||
return;
|
||||
}
|
||||
if (Decimal.gt(amount, best.value)) {
|
||||
best.value = amount;
|
||||
}
|
||||
|
@ -40,6 +67,9 @@ export function trackBest(resource: Resource): Ref<DecimalSource> {
|
|||
export function trackTotal(resource: Resource): Ref<DecimalSource> {
|
||||
const total = persistent(resource.value);
|
||||
watch(resource, (amount, prevAmount) => {
|
||||
if (loadingSave.value) {
|
||||
return;
|
||||
}
|
||||
if (Decimal.gt(amount, prevAmount)) {
|
||||
total.value = Decimal.add(total.value, Decimal.sub(amount, prevAmount));
|
||||
}
|
||||
|
@ -110,7 +140,14 @@ export function trackOOMPS(
|
|||
export function displayResource(resource: Resource, overrideAmount?: DecimalSource): string {
|
||||
const amount = overrideAmount ?? resource.value;
|
||||
if (Decimal.eq(resource.precision, 0)) {
|
||||
return formatWhole(amount);
|
||||
return formatWhole(resource.small ? amount : Decimal.floor(amount));
|
||||
}
|
||||
return format(amount, resource.precision, resource.small);
|
||||
}
|
||||
|
||||
export function unwrapResource(resource: ProcessedComputable<Resource>): Resource {
|
||||
if ("displayName" in resource) {
|
||||
return resource;
|
||||
}
|
||||
return unref(resource);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
@click="selectTab"
|
||||
class="tabButton"
|
||||
:style="[
|
||||
{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
glowColorStyle,
|
||||
unref(style) ?? {}
|
||||
|
@ -21,7 +21,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import { getNotifyStyle } from "game/notifications";
|
||||
import { computeComponent, processedPropType, unwrapRef } from "util/vue";
|
||||
import { computed, defineComponent, toRefs, unref } from "vue";
|
||||
|
@ -29,7 +29,7 @@ import { computed, defineComponent, toRefs, unref } from "vue";
|
|||
export default defineComponent({
|
||||
props: {
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
display: {
|
||||
|
@ -50,7 +50,7 @@ export default defineComponent({
|
|||
|
||||
const glowColorStyle = computed(() => {
|
||||
const color = unwrapRef(glowColor);
|
||||
if (!color) {
|
||||
if (color == null || color === "") {
|
||||
return {};
|
||||
}
|
||||
if (unref(floating)) {
|
||||
|
@ -68,7 +68,9 @@ export default defineComponent({
|
|||
component,
|
||||
glowColorStyle,
|
||||
unref,
|
||||
Visibility
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
class="tab-family-container"
|
||||
:class="{ ...unref(classes), ...tabClasses }"
|
||||
:style="[
|
||||
{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? [],
|
||||
tabStyle ?? []
|
||||
|
@ -37,7 +37,7 @@
|
|||
import Sticky from "components/layout/Sticky.vue";
|
||||
import themes from "data/themes";
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import type { GenericTab } from "features/tabs/tab";
|
||||
import TabButton from "features/tabs/TabButton.vue";
|
||||
import type { GenericTabButton } from "features/tabs/tabFamily";
|
||||
|
@ -49,7 +49,7 @@ import { computed, defineComponent, shallowRef, toRefs, unref, watchEffect } fro
|
|||
export default defineComponent({
|
||||
props: {
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
activeTab: {
|
||||
|
@ -123,7 +123,9 @@ export default defineComponent({
|
|||
Visibility,
|
||||
component,
|
||||
gatherButtonProps,
|
||||
unref
|
||||
unref,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -220,8 +222,8 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.showGoBack
|
||||
> .tab-family-container
|
||||
> .tab-buttons-container:not(.floating):first-child
|
||||
> .tab-family-container:first-child
|
||||
> .tab-buttons-container:not(.floating)
|
||||
.tab-buttons {
|
||||
padding-left: 70px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
isVisible,
|
||||
setDefault,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import TabButtonComponent from "features/tabs/TabButton.vue";
|
||||
import TabFamilyComponent from "features/tabs/TabFamily.vue";
|
||||
import type { Persistent } from "game/persistence";
|
||||
|
@ -20,7 +27,7 @@ export const TabButtonType = Symbol("TabButton");
|
|||
export const TabFamilyType = Symbol("TabFamily");
|
||||
|
||||
export interface TabButtonOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
tab: Computable<GenericTab | CoercableComponent>;
|
||||
display: Computable<CoercableComponent>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
|
@ -48,12 +55,12 @@ export type TabButton<T extends TabButtonOptions> = Replace<
|
|||
export type GenericTabButton = Replace<
|
||||
TabButton<TabButtonOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface TabFamilyOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
buttonContainerClasses?: Computable<Record<string, boolean>>;
|
||||
|
@ -81,7 +88,7 @@ export type TabFamily<T extends TabFamilyOptions> = Replace<
|
|||
export type GenericTabFamily = Replace<
|
||||
TabFamily<TabFamilyOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -91,10 +98,10 @@ export function createTabFamily<T extends TabFamilyOptions>(
|
|||
): TabFamily<T> {
|
||||
if (Object.keys(tabs).length === 0) {
|
||||
console.warn("Cannot create tab family with 0 tabs");
|
||||
throw "Cannot create tab family with 0 tabs";
|
||||
throw new Error("Cannot create tab family with 0 tabs");
|
||||
}
|
||||
|
||||
const selected = persistent(Object.keys(tabs)[0]);
|
||||
const selected = persistent(Object.keys(tabs)[0], false);
|
||||
return createLazyProxy(() => {
|
||||
const tabFamily = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
|
||||
|
@ -123,15 +130,10 @@ export function createTabFamily<T extends TabFamilyOptions>(
|
|||
tabFamily.selected = selected;
|
||||
tabFamily.activeTab = computed(() => {
|
||||
const tabs = unref(processedTabFamily.tabs);
|
||||
if (
|
||||
selected.value in tabs &&
|
||||
unref(tabs[selected.value].visibility) === Visibility.Visible
|
||||
) {
|
||||
if (selected.value in tabs && isVisible(tabs[selected.value].visibility)) {
|
||||
return unref(tabs[selected.value].tab);
|
||||
}
|
||||
const firstTab = Object.values(tabs).find(
|
||||
tab => unref(tab.visibility) === Visibility.Visible
|
||||
);
|
||||
const firstTab = Object.values(tabs).find(tab => isVisible(tab.visibility));
|
||||
if (firstTab) {
|
||||
return unref(firstTab.tab);
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ export function addTooltip<T extends TooltipOptions>(
|
|||
options.pinnable = false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(element as any).pinned = options.pinned = persistent<boolean>(false);
|
||||
(element as any).pinned = options.pinned = persistent<boolean>(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
:style="{ visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined }"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="{ visibility: isHidden(visibility) ? 'hidden' : undefined }"
|
||||
:class="{
|
||||
treeNode: true,
|
||||
can: unref(canClick),
|
||||
|
@ -37,7 +37,7 @@
|
|||
import MarkNode from "components/MarkNode.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import {
|
||||
computeOptionalComponent,
|
||||
isCoercableComponent,
|
||||
|
@ -51,7 +51,7 @@ export default defineComponent({
|
|||
props: {
|
||||
display: processedPropType<CoercableComponent>(Object, String, Function),
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
style: processedPropType<StyleValue>(String, Object, Array),
|
||||
|
@ -87,7 +87,9 @@ export default defineComponent({
|
|||
comp,
|
||||
unref,
|
||||
Visibility,
|
||||
isCoercableComponent
|
||||
isCoercableComponent,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ export const TreeNodeType = Symbol("TreeNode");
|
|||
export const TreeType = Symbol("Tree");
|
||||
|
||||
export interface TreeNodeOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
canClick?: Computable<boolean>;
|
||||
color?: Computable<string>;
|
||||
display?: Computable<CoercableComponent>;
|
||||
|
@ -60,7 +60,7 @@ export type TreeNode<T extends TreeNodeOptions> = Replace<
|
|||
export type GenericTreeNode = Replace<
|
||||
TreeNode<TreeNodeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
@ -87,16 +87,16 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
|||
|
||||
if (treeNode.onClick) {
|
||||
const onClick = treeNode.onClick.bind(treeNode);
|
||||
treeNode.onClick = function () {
|
||||
if (unref(treeNode.canClick)) {
|
||||
onClick();
|
||||
treeNode.onClick = function (e) {
|
||||
if (unref(treeNode.canClick) !== false) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (treeNode.onHold) {
|
||||
const onHold = treeNode.onHold.bind(treeNode);
|
||||
treeNode.onHold = function () {
|
||||
if (unref(treeNode.canClick)) {
|
||||
if (unref(treeNode.canClick) !== false) {
|
||||
onHold();
|
||||
}
|
||||
};
|
||||
|
@ -141,7 +141,7 @@ export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
|
|||
}
|
||||
|
||||
export interface TreeOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
nodes: Computable<GenericTreeNode[][]>;
|
||||
leftSideNodes?: Computable<GenericTreeNode[]>;
|
||||
rightSideNodes?: Computable<GenericTreeNode[]>;
|
||||
|
@ -175,7 +175,7 @@ export type Tree<T extends TreeOptions> = Replace<
|
|||
export type GenericTree = Replace<
|
||||
Tree<TreeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="unref(visibility) !== Visibility.None"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
|
@ -29,11 +29,9 @@ import "components/common/features.css";
|
|||
import MarkNode from "components/MarkNode.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { jsx, Visibility } from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import { displayResource } from "features/resources/resource";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import type { GenericUpgrade } from "features/upgrades/upgrade";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import { displayRequirements, Requirements } from "game/requirements";
|
||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
||||
import type { Component, PropType, UnwrapRef } from "vue";
|
||||
import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
||||
|
@ -45,13 +43,15 @@ export default defineComponent({
|
|||
required: true
|
||||
},
|
||||
visibility: {
|
||||
type: processedPropType<Visibility>(Number),
|
||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||
required: true
|
||||
},
|
||||
style: processedPropType<StyleValue>(String, Object, Array),
|
||||
classes: processedPropType<Record<string, boolean>>(Object),
|
||||
resource: Object as PropType<Resource>,
|
||||
cost: processedPropType<DecimalSource>(String, Object, Number),
|
||||
requirements: {
|
||||
type: Object as PropType<Requirements>,
|
||||
required: true
|
||||
},
|
||||
canPurchase: {
|
||||
type: processedPropType<boolean>(Boolean),
|
||||
required: true
|
||||
|
@ -75,7 +75,7 @@ export default defineComponent({
|
|||
MarkNode
|
||||
},
|
||||
setup(props) {
|
||||
const { display, cost } = toRefs(props);
|
||||
const { display, requirements, bought } = toRefs(props);
|
||||
|
||||
const component = shallowRef<Component | string>("");
|
||||
|
||||
|
@ -89,32 +89,24 @@ export default defineComponent({
|
|||
component.value = coerceComponent(currDisplay);
|
||||
return;
|
||||
}
|
||||
const currCost = unwrapRef(cost);
|
||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description, "div");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||
component.value = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title ? (
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
{currDisplay.effectDisplay ? (
|
||||
{currDisplay.effectDisplay != null ? (
|
||||
<div>
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
) : null}
|
||||
{props.resource != null ? (
|
||||
<>
|
||||
<br />
|
||||
Cost: {props.resource &&
|
||||
displayResource(props.resource, currCost)}{" "}
|
||||
{props.resource?.displayName}
|
||||
</>
|
||||
) : null}
|
||||
{bought.value ? null : <><br />{displayRequirements(requirements.value)}</>}
|
||||
</span>
|
||||
))
|
||||
);
|
||||
|
@ -123,7 +115,9 @@ export default defineComponent({
|
|||
return {
|
||||
component,
|
||||
unref,
|
||||
Visibility
|
||||
Visibility,
|
||||
isVisible,
|
||||
isHidden
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { isArray } from "@vue/shared";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
findFeatures,
|
||||
|
@ -7,13 +14,16 @@ import {
|
|||
setDefault,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import UpgradeComponent from "features/upgrades/Upgrade.vue";
|
||||
import type { GenericLayer } from "game/layers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import {
|
||||
createVisibilityRequirement,
|
||||
payRequirements,
|
||||
Requirements,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import { isFunction } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
|
@ -29,7 +39,7 @@ import { computed, unref } from "vue";
|
|||
export const UpgradeType = Symbol("Upgrade");
|
||||
|
||||
export interface UpgradeOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
display?: Computable<
|
||||
|
@ -40,10 +50,8 @@ export interface UpgradeOptions {
|
|||
effectDisplay?: CoercableComponent;
|
||||
}
|
||||
>;
|
||||
requirements: Requirements;
|
||||
mark?: Computable<boolean | string>;
|
||||
cost?: Computable<DecimalSource>;
|
||||
resource?: Resource;
|
||||
canAfford?: Computable<boolean>;
|
||||
onPurchase?: VoidFunction;
|
||||
}
|
||||
|
||||
|
@ -64,79 +72,53 @@ export type Upgrade<T extends UpgradeOptions> = Replace<
|
|||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
requirements: GetComputableType<T["requirements"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
cost: GetComputableType<T["cost"]>;
|
||||
canAfford: GetComputableTypeWithDefault<T["canAfford"], Ref<boolean>>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GenericUpgrade = Replace<
|
||||
Upgrade<UpgradeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
canPurchase: ProcessedComputable<boolean>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createUpgrade<T extends UpgradeOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseUpgrade, GenericUpgrade>
|
||||
): Upgrade<T> {
|
||||
const bought = persistent<boolean>(false);
|
||||
const bought = persistent<boolean>(false, false);
|
||||
return createLazyProxy(() => {
|
||||
const upgrade = optionsFunc();
|
||||
upgrade.id = getUniqueID("upgrade-");
|
||||
upgrade.type = UpgradeType;
|
||||
upgrade[Component] = UpgradeComponent;
|
||||
|
||||
if (upgrade.canAfford == null && (upgrade.resource == null || upgrade.cost == null)) {
|
||||
console.warn(
|
||||
"Error: can't create upgrade without a canAfford property or a resource and cost property",
|
||||
upgrade
|
||||
);
|
||||
}
|
||||
|
||||
upgrade.bought = bought;
|
||||
if (upgrade.canAfford == null) {
|
||||
upgrade.canAfford = computed(() => {
|
||||
const genericUpgrade = upgrade as GenericUpgrade;
|
||||
return (
|
||||
genericUpgrade.resource != null &&
|
||||
genericUpgrade.cost != null &&
|
||||
Decimal.gte(genericUpgrade.resource.value, unref(genericUpgrade.cost))
|
||||
);
|
||||
});
|
||||
} else {
|
||||
processComputable(upgrade as T, "canAfford");
|
||||
}
|
||||
upgrade.canPurchase = computed(
|
||||
() =>
|
||||
unref((upgrade as GenericUpgrade).visibility) === Visibility.Visible &&
|
||||
unref((upgrade as GenericUpgrade).canAfford) &&
|
||||
!unref(upgrade.bought)
|
||||
);
|
||||
upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements));
|
||||
upgrade.purchase = function () {
|
||||
const genericUpgrade = upgrade as GenericUpgrade;
|
||||
if (!unref(genericUpgrade.canPurchase)) {
|
||||
return;
|
||||
}
|
||||
if (genericUpgrade.resource != null && genericUpgrade.cost != null) {
|
||||
genericUpgrade.resource.value = Decimal.sub(
|
||||
genericUpgrade.resource.value,
|
||||
unref(genericUpgrade.cost)
|
||||
);
|
||||
}
|
||||
payRequirements(upgrade.requirements);
|
||||
bought.value = true;
|
||||
genericUpgrade.onPurchase?.();
|
||||
};
|
||||
|
||||
const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade);
|
||||
if (isArray(upgrade.requirements)) {
|
||||
upgrade.requirements.unshift(visibilityRequirement);
|
||||
} else {
|
||||
upgrade.requirements = [visibilityRequirement, upgrade.requirements];
|
||||
}
|
||||
|
||||
processComputable(upgrade as T, "visibility");
|
||||
setDefault(upgrade, "visibility", Visibility.Visible);
|
||||
processComputable(upgrade as T, "classes");
|
||||
processComputable(upgrade as T, "style");
|
||||
processComputable(upgrade as T, "display");
|
||||
processComputable(upgrade as T, "mark");
|
||||
processComputable(upgrade as T, "cost");
|
||||
processComputable(upgrade as T, "resource");
|
||||
|
||||
upgrade[GatherProps] = function (this: GenericUpgrade) {
|
||||
const {
|
||||
|
@ -144,8 +126,7 @@ export function createUpgrade<T extends UpgradeOptions>(
|
|||
visibility,
|
||||
style,
|
||||
classes,
|
||||
resource,
|
||||
cost,
|
||||
requirements,
|
||||
canPurchase,
|
||||
bought,
|
||||
mark,
|
||||
|
@ -157,8 +138,7 @@ export function createUpgrade<T extends UpgradeOptions>(
|
|||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
resource,
|
||||
cost,
|
||||
requirements,
|
||||
canPurchase,
|
||||
bought,
|
||||
mark,
|
||||
|
@ -176,8 +156,11 @@ export function setupAutoPurchase(
|
|||
autoActive: Computable<boolean>,
|
||||
upgrades: GenericUpgrade[] = []
|
||||
): void {
|
||||
upgrades = upgrades || findFeatures(layer, UpgradeType);
|
||||
const isAutoActive = isFunction(autoActive) ? computed(autoActive) : autoActive;
|
||||
upgrades =
|
||||
upgrades.length === 0 ? (findFeatures(layer, UpgradeType) as GenericUpgrade[]) : upgrades;
|
||||
const isAutoActive: ProcessedComputable<boolean> = isFunction(autoActive)
|
||||
? computed(autoActive)
|
||||
: autoActive;
|
||||
layer.on("update", () => {
|
||||
if (unref(isAutoActive)) {
|
||||
upgrades.forEach(upgrade => upgrade.purchase());
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import player from "game/player";
|
||||
import type { Settings } from "game/settings";
|
||||
import settings from "game/settings";
|
||||
import state from "game/state";
|
||||
import { createNanoEvents } from "nanoevents";
|
||||
import Decimal from "util/bignum";
|
||||
import type { App, Ref } from "vue";
|
||||
import { watch } from "vue";
|
||||
import type { App } from "vue";
|
||||
import type { GenericLayer } from "./layers";
|
||||
|
||||
/** All types of events able to be sent or emitted from the global event bus. */
|
||||
|
@ -50,105 +44,18 @@ export interface GlobalEvents {
|
|||
* Happens when the page is opened and upon switching saves in the saves manager.
|
||||
*/
|
||||
onLoad: VoidFunction;
|
||||
/**
|
||||
* Using document.fonts.ready returns too early on firefox, so we use document.fonts.onloadingdone instead, which doesn't accept multiple listeners.
|
||||
* This event fires when that callback is called.
|
||||
*/
|
||||
fontsLoaded: VoidFunction;
|
||||
}
|
||||
|
||||
/** A global event bus for hooking into {@link GlobalEvents}. */
|
||||
export const globalBus = createNanoEvents<GlobalEvents>();
|
||||
|
||||
let intervalID: NodeJS.Timer | 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() {
|
||||
const now = Date.now();
|
||||
let diff = (now - player.time) / 1e3;
|
||||
player.time = now;
|
||||
const trueDiff = diff;
|
||||
|
||||
state.lastTenTicks.push(trueDiff);
|
||||
if (state.lastTenTicks.length > 10) {
|
||||
state.lastTenTicks = state.lastTenTicks.slice(1);
|
||||
}
|
||||
|
||||
// Stop here if the game is paused on the win screen
|
||||
if (hasWon?.value && !player.keepGoing) {
|
||||
return;
|
||||
}
|
||||
// Stop here if the player had a NaN value
|
||||
if (state.hasNaN) {
|
||||
return;
|
||||
}
|
||||
|
||||
diff = Math.max(diff, 0);
|
||||
|
||||
if (player.devSpeed === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add offline time if any
|
||||
if (player.offlineTime != undefined) {
|
||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||
}
|
||||
if (Decimal.gt(player.offlineTime, 0) && player.devSpeed !== 0) {
|
||||
const offlineDiff = Math.max(player.offlineTime / 10, diff);
|
||||
player.offlineTime = player.offlineTime - offlineDiff;
|
||||
diff += offlineDiff;
|
||||
} else if (player.devSpeed === 0) {
|
||||
player.offlineTime += diff;
|
||||
}
|
||||
if (!player.offlineProd || Decimal.lt(player.offlineTime, 0)) {
|
||||
player.offlineTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at max tick length
|
||||
diff = Math.min(diff, projInfo.maxTickLength);
|
||||
|
||||
// Apply dev speed
|
||||
if (player.devSpeed != undefined) {
|
||||
diff *= player.devSpeed;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(diff)) {
|
||||
diff = 1e308;
|
||||
}
|
||||
|
||||
// Update
|
||||
if (Decimal.eq(diff, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.timePlayed += diff;
|
||||
if (!Number.isFinite(player.timePlayed)) {
|
||||
player.timePlayed = 1e308;
|
||||
}
|
||||
globalBus.emit("update", diff, trueDiff);
|
||||
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
if (intervalID != null) {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
}
|
||||
} else if (intervalID == null) {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the game loop for the project, which updates the game in ticks. */
|
||||
export async function startGameLoop() {
|
||||
hasWon = (await import("data/projEntry")).hasWon;
|
||||
watch(hasWon, hasWon => {
|
||||
if (hasWon) {
|
||||
globalBus.emit("gameWon");
|
||||
}
|
||||
});
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
} else {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
if ("fonts" in document) {
|
||||
// This line breaks tests
|
||||
// JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document
|
||||
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
|
||||
}
|
||||
|
|
1483
src/game/formulas/formulas.ts
Normal file
1483
src/game/formulas/formulas.ts
Normal file
File diff suppressed because it is too large
Load diff
705
src/game/formulas/operations.ts
Normal file
705
src/game/formulas/operations.ts
Normal file
|
@ -0,0 +1,705 @@
|
|||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
|
||||
import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
|
||||
|
||||
const ln10 = Decimal.ln(10);
|
||||
|
||||
export function passthrough<T extends GenericFormula | DecimalSource>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.neg(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return Formula.neg(lhs.getIntegralFormula(stack));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function applySubstitutionNeg(value: GenericFormula) {
|
||||
return Formula.neg(value);
|
||||
}
|
||||
|
||||
export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs)));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs)));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x);
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.times(lhs, rhs.innermostVariable ?? 0).add(x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateInnerAdd(
|
||||
stack: SubstitutionStack,
|
||||
lhs: FormulaSource,
|
||||
rhs: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.add(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.add(x, lhs);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs)));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0));
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.times(lhs, rhs.innermostVariable ?? 0).sub(x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateInnerSub(
|
||||
stack: SubstitutionStack,
|
||||
lhs: FormulaSource,
|
||||
rhs: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sub(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.sub(x, lhs);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs)));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs)));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.times(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.times(x, lhs);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function applySubstitutionMul(
|
||||
value: GenericFormula,
|
||||
lhs: FormulaSource,
|
||||
rhs: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
return Formula.div(value, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
return Formula.div(value, lhs);
|
||||
}
|
||||
throw new Error("Could not apply substitution due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs)));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.div(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.div(lhs, x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function applySubstitutionDiv(
|
||||
value: GenericFormula,
|
||||
lhs: FormulaSource,
|
||||
rhs: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
return Formula.mul(value, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
return Formula.mul(value, lhs);
|
||||
}
|
||||
throw new Error("Could not apply substitution due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.recip(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.ln(x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.pow10(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLog10(lhs: DecimalSource) {
|
||||
return Decimal.ln(lhs).sub(1).times(lhs).div(ln10);
|
||||
}
|
||||
|
||||
function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const numerator = ln10.times(value);
|
||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack)],
|
||||
evaluate: internalIntegrateLog10,
|
||||
invert: internalInvertIntegralLog10
|
||||
});
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) {
|
||||
return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(rhs));
|
||||
}
|
||||
|
||||
function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
|
||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack), rhs],
|
||||
evaluate: internalIntegrateLog,
|
||||
invert: internalInvertIntegralLog
|
||||
});
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.pow(2, value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLog2(lhs: DecimalSource) {
|
||||
return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(2));
|
||||
}
|
||||
|
||||
function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const numerator = Decimal.ln(2).times(value);
|
||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack)],
|
||||
evaluate: internalIntegrateLog2,
|
||||
invert: internalInvertIntegralLog2
|
||||
});
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLn(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.exp(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLn(lhs: DecimalSource) {
|
||||
return Decimal.ln(lhs).sub(1).times(lhs);
|
||||
}
|
||||
|
||||
function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.div(value, Decimal.div(value, Math.E).lambertw()));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack)],
|
||||
evaluate: internalIntegrateLn,
|
||||
invert: internalInvertIntegralLn
|
||||
});
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs)));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs))));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
const pow = Formula.add(rhs, 1);
|
||||
return Formula.pow(x, pow).div(pow);
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.pow(lhs, x).div(Formula.ln(lhs));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.root(value, 10));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.pow10(x).div(Formula.ln(10));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs)));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.pow(rhs, x).div(Formula.ln(rhs));
|
||||
} else if (hasVariable(rhs)) {
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
const denominator = Formula.add(lhs, 1);
|
||||
return Formula.pow(x, denominator).div(denominator);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs))));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value)));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertExp(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.ln(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.exp(x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function tetrate(
|
||||
value: DecimalSource,
|
||||
height: DecimalSource = 2,
|
||||
payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1)
|
||||
) {
|
||||
const heightNumber = Decimal.minabs(height, 1e308).toNumber();
|
||||
return Decimal.tetrate(value, heightNumber, payload);
|
||||
}
|
||||
|
||||
export function invertTetrate(
|
||||
value: DecimalSource,
|
||||
base: FormulaSource,
|
||||
height: FormulaSource,
|
||||
payload: FormulaSource
|
||||
) {
|
||||
if (hasVariable(base)) {
|
||||
return base.invert(Decimal.ssqrt(value));
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function iteratedexp(
|
||||
value: DecimalSource,
|
||||
height: DecimalSource = 2,
|
||||
payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1)
|
||||
) {
|
||||
const heightNumber = Decimal.minabs(height, 1e308).toNumber();
|
||||
return Decimal.iteratedexp(value, heightNumber, new Decimal(payload));
|
||||
}
|
||||
|
||||
export function invertIteratedExp(
|
||||
value: DecimalSource,
|
||||
lhs: FormulaSource,
|
||||
height: FormulaSource,
|
||||
payload: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(
|
||||
Decimal.iteratedlog(
|
||||
value,
|
||||
Math.E,
|
||||
Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber()
|
||||
)
|
||||
);
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function iteratedLog(
|
||||
value: DecimalSource,
|
||||
lhs: DecimalSource = 10,
|
||||
times: DecimalSource = 2
|
||||
) {
|
||||
const timesNumber = Decimal.minabs(times, 1e308).toNumber();
|
||||
return Decimal.iteratedlog(value, lhs, timesNumber);
|
||||
}
|
||||
|
||||
export function slog(value: DecimalSource, lhs: DecimalSource = 10) {
|
||||
const baseNumber = Decimal.minabs(lhs, 1e308).toNumber();
|
||||
return Decimal.slog(value, baseNumber);
|
||||
}
|
||||
|
||||
export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(
|
||||
Decimal.tetrate(value, Decimal.minabs(1e308, unrefFormulaSource(rhs)).toNumber())
|
||||
);
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) {
|
||||
const diffNumber = Decimal.minabs(diff, 1e308).toNumber();
|
||||
return Decimal.layeradd(value, diffNumber, base);
|
||||
}
|
||||
|
||||
export function invertLayeradd(
|
||||
value: DecimalSource,
|
||||
lhs: FormulaSource,
|
||||
diff: FormulaSource,
|
||||
base: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(
|
||||
Decimal.layeradd(
|
||||
value,
|
||||
Decimal.minabs(1e308, unrefFormulaSource(diff)).negate().toNumber()
|
||||
)
|
||||
);
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLambertw(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.pow(Math.E, value).times(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.tetrate(value, 2));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) {
|
||||
const heightNumber = Decimal.minabs(height, 1e308).toNumber();
|
||||
return Decimal.pentate(value, heightNumber, payload);
|
||||
}
|
||||
|
||||
export function invertSin(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.asin(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cos(x).neg();
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertCos(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.acos(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sin(x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertTan(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.atan(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cos(x).ln().neg();
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.sin(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.asin(x)
|
||||
.times(x)
|
||||
.add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.cos(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.acos(x)
|
||||
.times(x)
|
||||
.sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.tan(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.atan(x)
|
||||
.times(x)
|
||||
.sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.asinh(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cosh(x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.acosh(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sinh(x);
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.atanh(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cosh(x).ln();
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.sinh(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt());
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.cosh(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.acosh(x)
|
||||
.times(x)
|
||||
.sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt()));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.tanh(value));
|
||||
}
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.atanh(x)
|
||||
.times(x)
|
||||
.add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2));
|
||||
}
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function createPassthroughBinaryFormula(
|
||||
operation: (a: DecimalSource, b: DecimalSource) => DecimalSource
|
||||
) {
|
||||
return (value: FormulaSource, other: FormulaSource) =>
|
||||
new Formula({
|
||||
inputs: [value, other],
|
||||
evaluate: operation,
|
||||
invert: passthrough as InvertFunction<[FormulaSource, FormulaSource]>
|
||||
});
|
||||
}
|
70
src/game/formulas/types.d.ts
vendored
Normal file
70
src/game/formulas/types.d.ts
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
import Formula from "game/formulas/formulas";
|
||||
import { DecimalSource } from "util/bignum";
|
||||
import { ProcessedComputable } from "util/computed";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type GenericFormula = Formula<any>;
|
||||
type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula;
|
||||
type InvertibleFormula = GenericFormula & {
|
||||
invert: (value: DecimalSource) => DecimalSource;
|
||||
};
|
||||
type IntegrableFormula = GenericFormula & {
|
||||
evaluateIntegral: (variable?: DecimalSource) => DecimalSource;
|
||||
};
|
||||
type InvertibleIntegralFormula = GenericFormula & {
|
||||
invertIntegral: (value: DecimalSource) => DecimalSource;
|
||||
};
|
||||
|
||||
type EvaluateFunction<T> = (
|
||||
this: Formula<T>,
|
||||
...inputs: GuardedFormulasToDecimals<T>
|
||||
) => DecimalSource;
|
||||
type InvertFunction<T> = (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
|
||||
type IntegrateFunction<T> = (
|
||||
this: Formula<T>,
|
||||
stack: SubstitutionStack | undefined,
|
||||
...inputs: T
|
||||
) => GenericFormula;
|
||||
type SubstitutionFunction<T> = (
|
||||
this: Formula<T>,
|
||||
variable: GenericFormula,
|
||||
...inputs: T
|
||||
) => GenericFormula;
|
||||
|
||||
type VariableFormulaOptions = { variable: ProcessedComputable<DecimalSource> };
|
||||
type ConstantFormulaOptions = {
|
||||
inputs: [FormulaSource];
|
||||
};
|
||||
type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
||||
inputs: T;
|
||||
evaluate: EvaluateFunction<T>;
|
||||
invert?: InvertFunction<T>;
|
||||
integrate?: IntegrateFunction<T>;
|
||||
integrateInner?: IntegrateFunction<T>;
|
||||
applySubstitution?: SubstitutionFunction<T>;
|
||||
};
|
||||
type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
|
||||
| VariableFormulaOptions
|
||||
| ConstantFormulaOptions
|
||||
| GeneralFormulaOptions<T>;
|
||||
|
||||
type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
|
||||
inputs: T;
|
||||
internalVariables: number;
|
||||
internalEvaluate?: EvaluateFunction<T>;
|
||||
internalInvert?: InvertFunction<T>;
|
||||
internalIntegrate?: IntegrateFunction<T>;
|
||||
internalIntegrateInner?: IntegrateFunction<T>;
|
||||
applySubstitution?: SubstitutionFunction<T>;
|
||||
innermostVariable?: ProcessedComputable<DecimalSource>;
|
||||
};
|
||||
|
||||
type SubstitutionStack = ((value: GenericFormula) => GenericFormula)[] | undefined;
|
||||
|
||||
// It's really hard to type mapped tuples, but these classes seem to manage
|
||||
type FormulasToDecimals<T extends FormulaSource[]> = {
|
||||
[K in keyof T]: DecimalSource;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type TupleGuard<T extends any[]> = T extends any[] ? FormulasToDecimals<T> : never;
|
||||
type GuardedFormulasToDecimals<T extends FormulaSource[]> = TupleGuard<T>;
|
109
src/game/gameLoop.ts
Normal file
109
src/game/gameLoop.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import { globalBus } from "game/events";
|
||||
import settings from "game/settings";
|
||||
import Decimal from "util/bignum";
|
||||
import { loadingSave } from "util/save";
|
||||
import type { Ref } from "vue";
|
||||
import { watch } from "vue";
|
||||
import player from "./player";
|
||||
import state from "./state";
|
||||
|
||||
let intervalID: NodeJS.Timer | 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() {
|
||||
const now = Date.now();
|
||||
let diff = (now - player.time) / 1e3;
|
||||
player.time = now;
|
||||
const trueDiff = diff;
|
||||
|
||||
state.lastTenTicks.push(trueDiff);
|
||||
if (state.lastTenTicks.length > 10) {
|
||||
state.lastTenTicks = state.lastTenTicks.slice(1);
|
||||
}
|
||||
|
||||
// Stop here if the game is paused on the win screen
|
||||
if (hasWon?.value && !player.keepGoing) {
|
||||
return;
|
||||
}
|
||||
// Stop here if the player had a NaN value
|
||||
if (state.hasNaN) {
|
||||
return;
|
||||
}
|
||||
|
||||
diff = Math.max(diff, 0);
|
||||
|
||||
if (player.devSpeed === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingSave.value = false;
|
||||
|
||||
// Add offline time if any
|
||||
if (player.offlineTime != undefined) {
|
||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||
}
|
||||
if (Decimal.gt(player.offlineTime, 0) && player.devSpeed !== 0) {
|
||||
const offlineDiff = Math.max(player.offlineTime / 10, diff);
|
||||
player.offlineTime = player.offlineTime - offlineDiff;
|
||||
diff += offlineDiff;
|
||||
} else if (player.devSpeed === 0) {
|
||||
player.offlineTime += diff;
|
||||
}
|
||||
if (!player.offlineProd || Decimal.lt(player.offlineTime, 0)) {
|
||||
player.offlineTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at max tick length
|
||||
diff = Math.min(diff, projInfo.maxTickLength);
|
||||
|
||||
// Apply dev speed
|
||||
if (player.devSpeed != undefined) {
|
||||
diff *= player.devSpeed;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(diff)) {
|
||||
diff = 1e308;
|
||||
}
|
||||
|
||||
// Update
|
||||
if (Decimal.eq(diff, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.timePlayed += diff;
|
||||
if (!Number.isFinite(player.timePlayed)) {
|
||||
player.timePlayed = 1e308;
|
||||
}
|
||||
globalBus.emit("update", diff, trueDiff);
|
||||
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
if (intervalID != null) {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
}
|
||||
} else if (intervalID == null) {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the game loop for the project, which updates the game in ticks. */
|
||||
export async function startGameLoop() {
|
||||
hasWon = (await import("data/projEntry")).hasWon;
|
||||
watch(hasWon, hasWon => {
|
||||
if (hasWon) {
|
||||
globalBus.emit("gameWon");
|
||||
}
|
||||
});
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
} else {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ import type {
|
|||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { InjectionKey, Ref } from "vue";
|
||||
import { computed, InjectionKey, Ref } from "vue";
|
||||
import { ref, shallowReactive, unref } from "vue";
|
||||
|
||||
/** A feature's node in the DOM that has its size tracked. */
|
||||
|
@ -109,7 +109,7 @@ export interface LayerOptions {
|
|||
color?: Computable<string>;
|
||||
/**
|
||||
* The layout of this layer's features.
|
||||
* When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is display.
|
||||
* When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is displayed.
|
||||
*/
|
||||
display: Computable<CoercableComponent>;
|
||||
/** An object of classes that should be applied to the display. */
|
||||
|
@ -126,6 +126,11 @@ export interface LayerOptions {
|
|||
* Defaults to true.
|
||||
*/
|
||||
minimizable?: Computable<boolean>;
|
||||
/**
|
||||
* The layout of this layer's features.
|
||||
* When the layer is open in {@link game/player.PlayerData.tabs}, but the tab is {@link Layer.minimized} this is the content that is displayed.
|
||||
*/
|
||||
minimizedDisplay?: Computable<CoercableComponent>;
|
||||
/**
|
||||
* Whether or not to force the go back button to be hidden.
|
||||
* If true, go back will be hidden regardless of {@link data/projInfo.allowGoBack}.
|
||||
|
@ -170,6 +175,7 @@ export type Layer<T extends LayerOptions> = Replace<
|
|||
name: GetComputableTypeWithDefault<T["name"], string>;
|
||||
minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>;
|
||||
minimizable: GetComputableTypeWithDefault<T["minimizable"], true>;
|
||||
minimizedDisplay: GetComputableType<T["minimizedDisplay"]>;
|
||||
forceHideGoBack: GetComputableType<T["forceHideGoBack"]>;
|
||||
}
|
||||
>;
|
||||
|
@ -213,7 +219,7 @@ export function createLayer<T extends LayerOptions>(
|
|||
|
||||
addingLayers.push(id);
|
||||
persistentRefs[id] = new Set();
|
||||
layer.minimized = persistent(false);
|
||||
layer.minimized = persistent(false, false);
|
||||
Object.assign(layer, optionsFunc.call(layer as BaseLayer));
|
||||
if (
|
||||
addingLayers[addingLayers.length - 1] == null ||
|
||||
|
@ -225,12 +231,43 @@ export function createLayer<T extends LayerOptions>(
|
|||
|
||||
processComputable(layer as T, "color");
|
||||
processComputable(layer as T, "display");
|
||||
processComputable(layer as T, "classes");
|
||||
processComputable(layer as T, "style");
|
||||
processComputable(layer as T, "name");
|
||||
setDefault(layer, "name", layer.id);
|
||||
processComputable(layer as T, "minWidth");
|
||||
setDefault(layer, "minWidth", 600);
|
||||
processComputable(layer as T, "minimizable");
|
||||
setDefault(layer, "minimizable", true);
|
||||
processComputable(layer as T, "minimizedDisplay");
|
||||
|
||||
const style = layer.style as ProcessedComputable<StyleValue> | undefined;
|
||||
layer.style = computed(() => {
|
||||
let width = unref(layer.minWidth as ProcessedComputable<number | string>);
|
||||
if (typeof width === "number" || !Number.isNaN(parseInt(width))) {
|
||||
width = width + "px";
|
||||
}
|
||||
return [
|
||||
unref(style) ?? "",
|
||||
layer.minimized?.value
|
||||
? {
|
||||
flexGrow: "0",
|
||||
flexShrink: "0",
|
||||
width: "60px",
|
||||
minWidth: "",
|
||||
flexBasis: "",
|
||||
margin: "0"
|
||||
}
|
||||
: {
|
||||
flexGrow: "",
|
||||
flexShrink: "",
|
||||
width: "",
|
||||
minWidth: width,
|
||||
flexBasis: width,
|
||||
margin: ""
|
||||
}
|
||||
];
|
||||
}) as Ref<StyleValue>;
|
||||
|
||||
return layer as unknown as Layer<T>;
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import "components/common/modifiers.css";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { jsx } from "features/feature";
|
||||
import settings from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format } from "util/bignum";
|
||||
import Decimal, { formatSmall } from "util/bignum";
|
||||
import type { WithRequired } from "util/common";
|
||||
import type { Computable, ProcessedComputable } from "util/computed";
|
||||
import { convertComputable } from "util/computed";
|
||||
|
@ -14,8 +15,7 @@ import { computed, unref } from "vue";
|
|||
* An object that can be used to apply or unapply some modification to a number.
|
||||
* Being reversible requires the operation being invertible, but some features may rely on that.
|
||||
* Descriptions can be optionally included for displaying them to the player.
|
||||
* The built-in modifier creators are designed to display the modifiers using.
|
||||
* {@link createModifierSection}.
|
||||
* The built-in modifier creators are designed to display the modifiers using {@link createModifierSection}.
|
||||
*/
|
||||
export interface Modifier {
|
||||
/** Applies some operation on the input and returns the result. */
|
||||
|
@ -46,29 +46,26 @@ export type ModifierFromOptionalParams<T, S> = T extends undefined
|
|||
: WithRequired<Modifier, "revert" | "enabled" | "description">;
|
||||
|
||||
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */
|
||||
export interface AdditiveModifierOptions<
|
||||
T extends Computable<CoercableComponent> | undefined,
|
||||
S extends Computable<boolean> | undefined
|
||||
> {
|
||||
export interface AdditiveModifierOptions {
|
||||
/** The amount to add to the input value. */
|
||||
addend: Computable<DecimalSource>;
|
||||
/** Description of what this modifier is doing. */
|
||||
description?: T;
|
||||
description?: Computable<CoercableComponent> | undefined;
|
||||
/** A computable that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: S;
|
||||
enabled?: Computable<boolean> | undefined;
|
||||
/** Determines if numbers larger or smaller than 0 should be displayed as red. */
|
||||
smallerIsBetter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a modifier that adds some value to the input value.
|
||||
* @param optionsFunc Additive modifier options.
|
||||
*/
|
||||
export function createAdditiveModifier<
|
||||
T extends Computable<CoercableComponent> | undefined,
|
||||
S extends Computable<boolean> | undefined,
|
||||
R = ModifierFromOptionalParams<T, S>
|
||||
>(optionsFunc: () => AdditiveModifierOptions<T, S>): R {
|
||||
export function createAdditiveModifier<T extends AdditiveModifierOptions>(
|
||||
optionsFunc: () => T
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
return createLazyProxy(() => {
|
||||
const { addend, description, enabled } = optionsFunc();
|
||||
const { addend, description, enabled, smallerIsBetter } = optionsFunc();
|
||||
|
||||
const processedAddend = convertComputable(addend);
|
||||
const processedDescription = convertComputable(description);
|
||||
|
@ -82,46 +79,54 @@ export function createAdditiveModifier<
|
|||
? undefined
|
||||
: jsx(() => (
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-amount">
|
||||
{Decimal.gte(unref(processedAddend), 0) ? "+" : ""}
|
||||
{format(unref(processedAddend))}
|
||||
</span>
|
||||
{unref(processedDescription) ? (
|
||||
{unref(processedDescription) != null ? (
|
||||
<span class="modifier-description">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{renderJSX(unref(processedDescription)!)}
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
(
|
||||
smallerIsBetter === true
|
||||
? Decimal.gt(unref(processedAddend), 0)
|
||||
: Decimal.lt(unref(processedAddend), 0)
|
||||
)
|
||||
? "color: var(--danger)"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{Decimal.gte(unref(processedAddend), 0) ? "+" : ""}
|
||||
{formatSmall(unref(processedAddend))}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
};
|
||||
}) as unknown as R;
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}
|
||||
|
||||
/** An object that configures an multiplicative modifier via {@link createMultiplicativeModifier}. */
|
||||
export interface MultiplicativeModifierOptions<
|
||||
T extends Computable<CoercableComponent> | undefined,
|
||||
S extends Computable<boolean> | undefined
|
||||
> {
|
||||
export interface MultiplicativeModifierOptions {
|
||||
/** The amount to multiply the input value by. */
|
||||
multiplier: Computable<DecimalSource>;
|
||||
/** Description of what this modifier is doing. */
|
||||
description?: T;
|
||||
description?: Computable<CoercableComponent> | undefined;
|
||||
/** A computable that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: S;
|
||||
enabled?: Computable<boolean> | undefined;
|
||||
/** Determines if numbers larger or smaller than 1 should be displayed as red. */
|
||||
smallerIsBetter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a modifier that multiplies the input value by some value.
|
||||
* @param optionsFunc Multiplicative modifier options.
|
||||
*/
|
||||
export function createMultiplicativeModifier<
|
||||
T extends Computable<CoercableComponent> | undefined,
|
||||
S extends Computable<boolean> | undefined,
|
||||
R = ModifierFromOptionalParams<T, S>
|
||||
>(optionsFunc: () => MultiplicativeModifierOptions<T, S>): R {
|
||||
export function createMultiplicativeModifier<T extends MultiplicativeModifierOptions>(
|
||||
optionsFunc: () => T
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
return createLazyProxy(() => {
|
||||
const { multiplier, description, enabled } = optionsFunc();
|
||||
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc();
|
||||
|
||||
const processedMultiplier = convertComputable(multiplier);
|
||||
const processedDescription = convertComputable(description);
|
||||
|
@ -135,71 +140,114 @@ export function createMultiplicativeModifier<
|
|||
? undefined
|
||||
: jsx(() => (
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-amount">
|
||||
x{format(unref(processedMultiplier))}
|
||||
</span>
|
||||
{unref(processedDescription) ? (
|
||||
{unref(processedDescription) != null ? (
|
||||
<span class="modifier-description">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{renderJSX(unref(processedDescription)!)}
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
(
|
||||
smallerIsBetter === true
|
||||
? Decimal.gt(unref(processedMultiplier), 1)
|
||||
: Decimal.lt(unref(processedMultiplier), 1)
|
||||
)
|
||||
? "color: var(--danger)"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
×{formatSmall(unref(processedMultiplier))}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
};
|
||||
}) as unknown as R;
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}
|
||||
|
||||
/** An object that configures an exponential modifier via {@link createExponentialModifier}. */
|
||||
export interface ExponentialModifierOptions<
|
||||
T extends Computable<CoercableComponent> | undefined,
|
||||
S extends Computable<boolean> | undefined
|
||||
> {
|
||||
export interface ExponentialModifierOptions {
|
||||
/** The amount to raise the input value to the power of. */
|
||||
exponent: Computable<DecimalSource>;
|
||||
/** Description of what this modifier is doing. */
|
||||
description?: T;
|
||||
description?: Computable<CoercableComponent> | undefined;
|
||||
/** A computable that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: S;
|
||||
enabled?: Computable<boolean> | undefined;
|
||||
/** Add 1 before calculating, then remove it afterwards. This prevents low numbers from becoming lower. */
|
||||
supportLowNumbers?: boolean;
|
||||
/** Determines if numbers larger or smaller than 1 should be displayed as red. */
|
||||
smallerIsBetter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a modifier that raises the input value to the power of some value.
|
||||
* @param optionsFunc Exponential modifier options.
|
||||
*/
|
||||
export function createExponentialModifier<
|
||||
T extends Computable<CoercableComponent> | undefined,
|
||||
S extends Computable<boolean> | undefined,
|
||||
R = ModifierFromOptionalParams<T, S>
|
||||
>(optionsFunc: () => ExponentialModifierOptions<T, S>): R {
|
||||
export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
||||
optionsFunc: () => T
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
return createLazyProxy(() => {
|
||||
const { exponent, description, enabled } = optionsFunc();
|
||||
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
|
||||
optionsFunc();
|
||||
|
||||
const processedExponent = convertComputable(exponent);
|
||||
const processedDescription = convertComputable(description);
|
||||
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||
return {
|
||||
apply: (gain: DecimalSource) => Decimal.pow(gain, unref(processedExponent)),
|
||||
revert: (gain: DecimalSource) => Decimal.root(gain, unref(processedExponent)),
|
||||
apply: (gain: DecimalSource) => {
|
||||
let result = gain;
|
||||
if (supportLowNumbers) {
|
||||
result = Decimal.add(result, 1);
|
||||
}
|
||||
result = Decimal.pow(result, unref(processedExponent));
|
||||
if (supportLowNumbers) {
|
||||
result = Decimal.sub(result, 1);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
revert: (gain: DecimalSource) => {
|
||||
let result = gain;
|
||||
if (supportLowNumbers) {
|
||||
result = Decimal.add(result, 1);
|
||||
}
|
||||
result = Decimal.root(result, unref(processedExponent));
|
||||
if (supportLowNumbers) {
|
||||
result = Decimal.sub(result, 1);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
enabled: processedEnabled,
|
||||
description:
|
||||
description == null
|
||||
? undefined
|
||||
: jsx(() => (
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-amount">
|
||||
^{format(unref(processedExponent))}
|
||||
</span>
|
||||
{unref(processedDescription) ? (
|
||||
{unref(processedDescription) != null ? (
|
||||
<span class="modifier-description">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{renderJSX(unref(processedDescription)!)}
|
||||
{supportLowNumbers ? " (+1 effective)" : null}
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
(
|
||||
smallerIsBetter === true
|
||||
? Decimal.gt(unref(processedExponent), 1)
|
||||
: Decimal.lt(unref(processedExponent), 1)
|
||||
)
|
||||
? "color: var(--danger)"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
^{formatSmall(unref(processedExponent))}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
};
|
||||
}) as unknown as R;
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -248,39 +296,52 @@ export function createSequentialModifier<
|
|||
/**
|
||||
* Create a JSX element that displays a modifier.
|
||||
* Intended to be used with the output from {@link createSequentialModifier}.
|
||||
* @param title The header for the section.
|
||||
* @param subtitle Smaller text that appears in the header after the title.
|
||||
* @param modifier The modifier to render.
|
||||
* @param base The base value that'll be passed into the modifier.
|
||||
* @param unit The unit of the value being modified, if any.
|
||||
* @param baseText The label to use for the base value.
|
||||
* @param options Modifier section options.
|
||||
*/
|
||||
export function createModifierSection(
|
||||
title: string,
|
||||
subtitle: string,
|
||||
modifier: WithRequired<Modifier, "description">,
|
||||
base: DecimalSource = 1,
|
||||
unit = "",
|
||||
baseText: CoercableComponent = "Base"
|
||||
) {
|
||||
export function createModifierSection({
|
||||
title,
|
||||
subtitle,
|
||||
modifier,
|
||||
base,
|
||||
unit,
|
||||
baseText,
|
||||
smallerIsBetter
|
||||
}: ModifierSectionOptions) {
|
||||
const total = modifier.apply(base ?? 1);
|
||||
return (
|
||||
<div>
|
||||
<div style={{ "--unit": settings.alignUnits && unit != null ? "'" + unit + "'" : "" }}>
|
||||
<h3>
|
||||
{title}
|
||||
{subtitle ? <span class="subtitle"> ({subtitle})</span> : null}
|
||||
{subtitle == null ? null : <span class="subtitle"> ({subtitle})</span>}
|
||||
</h3>
|
||||
<br />
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-description">{renderJSX(baseText ?? "Base")}</span>
|
||||
<span class="modifier-amount">
|
||||
{format(base)}
|
||||
{formatSmall(base ?? 1)}
|
||||
{unit}
|
||||
</span>
|
||||
<span class="modifier-description">{renderJSX(baseText)}</span>
|
||||
</div>
|
||||
{renderJSX(unref(modifier.description))}
|
||||
<hr />
|
||||
Total: {format(modifier.apply(base))}
|
||||
{unit}
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-description">Total</span>
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
(
|
||||
smallerIsBetter === true
|
||||
? Decimal.gt(total, base ?? 1)
|
||||
: Decimal.lt(total, base ?? 1)
|
||||
)
|
||||
? "color: var(--danger)"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{formatSmall(total)}
|
||||
{unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { globalBus } from "game/events";
|
||||
import { convertComputable } from "util/computed";
|
||||
import { trackHover, VueFeature } from "util/vue";
|
||||
import { nextTick, Ref } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import Toast from "vue-toastification";
|
||||
import "vue-toastification/dist/index.css";
|
||||
|
||||
|
@ -19,7 +23,33 @@ export function getNotifyStyle(color = "white", strength = "8px") {
|
|||
};
|
||||
}
|
||||
|
||||
/** Utility function to call {@link getNotifyStyle} with "high importance" parameters */
|
||||
/** Utility function to call {@link getNotifyStyle} with "high importance" parameters. */
|
||||
export function getHighNotifyStyle() {
|
||||
return getNotifyStyle("red", "20px");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a boolean ref that will automatically be set based on the given condition, but also dismissed when hovering over a given element, typically the element where acting upon the notification would take place.
|
||||
* @param element The element that will dismiss the notification on hover.
|
||||
* @param shouldNotify A function or ref that determines if the notif should be active currently or not.
|
||||
*/
|
||||
export function createDismissableNotify(
|
||||
element: VueFeature,
|
||||
shouldNotify: Ref<boolean> | (() => boolean)
|
||||
): Ref<boolean> {
|
||||
const processedShouldNotify = convertComputable(shouldNotify) as Ref<boolean>;
|
||||
const notifying = ref(false);
|
||||
nextTick(() => {
|
||||
notifying.value = processedShouldNotify.value;
|
||||
|
||||
watch(trackHover(element), hovering => {
|
||||
if (!hovering) {
|
||||
notifying.value = false;
|
||||
}
|
||||
});
|
||||
watch(processedShouldNotify, shouldNotify => {
|
||||
notifying.value = shouldNotify;
|
||||
});
|
||||
});
|
||||
return notifying;
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ import { addingLayers, persistentRefs } from "game/layers";
|
|||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import { ProxyState } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { isReactive, isRef, ref } from "vue";
|
||||
import type { Ref, WritableComputedRef } from "vue";
|
||||
import { computed, isReactive, isRef, ref } from "vue";
|
||||
import player from "./player";
|
||||
import state from "./state";
|
||||
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
|
@ -28,6 +30,21 @@ export const StackTrace = Symbol("StackTrace");
|
|||
* @see {@link Persistent[Deleted]}
|
||||
*/
|
||||
export const Deleted = Symbol("Deleted");
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
* @see {@link Persistent[NonPersistent]}
|
||||
*/
|
||||
export const NonPersistent = Symbol("NonPersistent");
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
* @see {@link Persistent[SaveDataPath]}
|
||||
*/
|
||||
export const SaveDataPath = Symbol("SaveDataPath");
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
* @see {@link Persistent[CheckNaN]}
|
||||
*/
|
||||
export const CheckNaN = Symbol("CheckNaN");
|
||||
|
||||
/**
|
||||
* This is a union of things that should be safely stringifiable without needing special processes or knowing what to load them in as.
|
||||
|
@ -46,6 +63,7 @@ export type State =
|
|||
* A {@link Ref} that has been augmented with properties to allow it to be saved and loaded within the player save data object.
|
||||
*/
|
||||
export type Persistent<T extends State = State> = Ref<T> & {
|
||||
value: T;
|
||||
/** A flag that this is a persistent property. Typically a circular reference. */
|
||||
[PersistentState]: Ref<T>;
|
||||
/** The value the ref should be set to in a fresh save, or when updating an old save to the current version. */
|
||||
|
@ -57,32 +75,103 @@ export type Persistent<T extends State = State> = Ref<T> & {
|
|||
* @see {@link deletePersistent} for marking a persistent ref as deleted.
|
||||
*/
|
||||
[Deleted]: boolean;
|
||||
/**
|
||||
* A non-persistent ref that just reads and writes ot the persistent ref. Used for passing to other features without duplicating the persistent ref in the constructed save data object.
|
||||
*/
|
||||
[NonPersistent]: NonPersistent<T>;
|
||||
/**
|
||||
* The path this persistent appears in within the save data object. Predominantly used to ensure it's only placed in there one time.
|
||||
*/
|
||||
[SaveDataPath]: string[] | undefined;
|
||||
/**
|
||||
* Whether or not to NaN-check this ref. Should only be true on values expected to always be DecimalSources.
|
||||
*/
|
||||
[CheckNaN]: boolean;
|
||||
};
|
||||
|
||||
export type NonPersistent<T extends State = State> = WritableComputedRef<T> & { [DefaultValue]: T };
|
||||
|
||||
function getStackTrace() {
|
||||
return (
|
||||
new Error().stack
|
||||
?.split("\n")
|
||||
.slice(3, 5)
|
||||
.map(line => line.trim())
|
||||
.join("\n") || ""
|
||||
.join("\n") ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
function checkNaNAndWrite<T extends State>(persistent: Persistent<T>, value: T) {
|
||||
// Decimal is smart enough to return false on things that aren't supposed to be numbers
|
||||
if (Decimal.isNaN(value as DecimalSource)) {
|
||||
if (!state.hasNaN) {
|
||||
player.autosave = false;
|
||||
state.hasNaN = true;
|
||||
state.NaNPath = persistent[SaveDataPath];
|
||||
state.NaNPersistent = persistent as Persistent<DecimalSource>;
|
||||
}
|
||||
console.error(
|
||||
`Attempted to save NaN value to`,
|
||||
persistent[SaveDataPath]?.join("."),
|
||||
persistent
|
||||
);
|
||||
throw new Error("Attempted to set NaN value. See above for details");
|
||||
}
|
||||
persistent[PersistentState].value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a persistent ref, which can be saved and loaded.
|
||||
* All (non-deleted) persistent refs must be included somewhere within the layer object returned by that layer's options func.
|
||||
* @param defaultValue The value the persistent ref should start at on fresh saves or when reset.
|
||||
* @param checkNaN Whether or not to check this ref for being NaN on set. Only use on refs that should always be DecimalSources.
|
||||
*/
|
||||
export function persistent<T extends State>(defaultValue: T | Ref<T>): Persistent<T> {
|
||||
const persistent = (
|
||||
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
|
||||
) as Persistent<T>;
|
||||
export function persistent<T extends State>(
|
||||
defaultValue: T | Ref<T>,
|
||||
checkNaN = true
|
||||
): Persistent<T> {
|
||||
const persistentState: Ref<T> = isRef(defaultValue)
|
||||
? defaultValue
|
||||
: (ref<T>(defaultValue) as Ref<T>);
|
||||
|
||||
persistent[PersistentState] = persistent;
|
||||
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
|
||||
persistent[StackTrace] = getStackTrace();
|
||||
persistent[Deleted] = false;
|
||||
if (isRef(defaultValue)) {
|
||||
defaultValue = defaultValue.value;
|
||||
}
|
||||
|
||||
const nonPersistent = computed({
|
||||
get() {
|
||||
return persistentState.value;
|
||||
},
|
||||
set(value) {
|
||||
if (checkNaN) {
|
||||
checkNaNAndWrite(persistent, value);
|
||||
} else {
|
||||
persistent[PersistentState].value = value;
|
||||
}
|
||||
}
|
||||
}) as NonPersistent<T>;
|
||||
nonPersistent[DefaultValue] = defaultValue;
|
||||
|
||||
// We're trying to mock a vue ref, which means the type expects a private [RefSymbol] property that we can't access, but the actual implementation of isRef just checks for `__v_isRef`
|
||||
const persistent = {
|
||||
get value() {
|
||||
return persistentState.value as T;
|
||||
},
|
||||
set value(value: T) {
|
||||
if (checkNaN) {
|
||||
checkNaNAndWrite(persistent, value);
|
||||
} else {
|
||||
persistent[PersistentState].value = value;
|
||||
}
|
||||
},
|
||||
__v_isRef: true,
|
||||
[PersistentState]: persistentState,
|
||||
[DefaultValue]: defaultValue,
|
||||
[StackTrace]: getStackTrace(),
|
||||
[Deleted]: false,
|
||||
[NonPersistent]: nonPersistent,
|
||||
[SaveDataPath]: undefined
|
||||
} as unknown as Persistent<T>;
|
||||
|
||||
if (addingLayers.length === 0) {
|
||||
console.warn(
|
||||
|
@ -94,7 +183,25 @@ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persisten
|
|||
persistentRefs[addingLayers[addingLayers.length - 1]].add(persistent);
|
||||
}
|
||||
|
||||
return persistent as Persistent<T>;
|
||||
return persistent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for whether an arbitrary value is a persistent ref
|
||||
* @param value The value that may or may not be a persistent ref
|
||||
*/
|
||||
export function isPersistent(value: unknown): value is Persistent {
|
||||
return value != null && typeof value === "object" && PersistentState in value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps the non-persistent ref inside of persistent refs, to be passed to other features without duplicating values in the save data object.
|
||||
* @param persistent The persistent ref to unwrap
|
||||
*/
|
||||
export function noPersist<T extends Persistent<S>, S extends State>(
|
||||
persistent: T
|
||||
): T[typeof NonPersistent] {
|
||||
return persistent[NonPersistent];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,24 +224,40 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
const handleObject = (obj: Record<string, unknown>, path: string[] = []): boolean => {
|
||||
let foundPersistent = false;
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
if (value && typeof value === "object") {
|
||||
if (PersistentState in value) {
|
||||
let value = obj[key];
|
||||
if (value != null && typeof value === "object") {
|
||||
if (ProxyState in value) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value = (value as any)[ProxyState] as object;
|
||||
}
|
||||
if (isPersistent(value)) {
|
||||
foundPersistent = true;
|
||||
if ((value as Persistent)[Deleted]) {
|
||||
if (value[Deleted]) {
|
||||
console.warn(
|
||||
"Deleted persistent ref present in returned object. Ignoring...",
|
||||
value,
|
||||
"\nCreated at:\n" + (value as Persistent)[StackTrace]
|
||||
"\nCreated at:\n" + value[StackTrace]
|
||||
);
|
||||
return;
|
||||
}
|
||||
persistentRefs[layer.id].delete(
|
||||
ProxyState in value
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
((value as any)[ProxyState] as Persistent)
|
||||
: (value as Persistent)
|
||||
);
|
||||
persistentRefs[layer.id].delete(value);
|
||||
|
||||
// Handle SaveDataPath
|
||||
const newPath = [layer.id, ...path, key];
|
||||
if (
|
||||
value[SaveDataPath] != undefined &&
|
||||
JSON.stringify(newPath) !== JSON.stringify(value[SaveDataPath])
|
||||
) {
|
||||
console.error(
|
||||
`Persistent ref is being saved to \`${newPath.join(
|
||||
"."
|
||||
)}\` when it's already present at \`${value[SaveDataPath].join(
|
||||
"."
|
||||
)}\`. This can cause unexpected behavior when loading saves between updates.`,
|
||||
value
|
||||
);
|
||||
}
|
||||
value[SaveDataPath] = newPath;
|
||||
|
||||
// Construct save path if it doesn't exist
|
||||
const persistentState = path.reduce<Record<string, unknown>>((acc, curr) => {
|
||||
|
@ -147,21 +270,19 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
// Cache currently saved value
|
||||
const savedValue = persistentState[key];
|
||||
// Add ref to save data
|
||||
persistentState[key] = (value as Persistent)[PersistentState];
|
||||
persistentState[key] = value[PersistentState];
|
||||
// Load previously saved value
|
||||
if (isReactive(persistentState)) {
|
||||
if (savedValue != null) {
|
||||
persistentState[key] = savedValue;
|
||||
} else {
|
||||
persistentState[key] = (value as Persistent)[DefaultValue];
|
||||
persistentState[key] = value[DefaultValue];
|
||||
}
|
||||
} else {
|
||||
if (savedValue != null) {
|
||||
(persistentState[key] as Ref<unknown>).value = savedValue;
|
||||
} else {
|
||||
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
|
||||
DefaultValue
|
||||
];
|
||||
(persistentState[key] as Ref<unknown>).value = value[DefaultValue];
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
@ -200,8 +321,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
});
|
||||
return foundPersistent;
|
||||
};
|
||||
handleObject(layer);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handleObject((layer as any)[ProxyState]);
|
||||
persistentRefs[layer.id].forEach(persistent => {
|
||||
if (persistent[Deleted]) {
|
||||
return;
|
||||
}
|
||||
console.error(
|
||||
`Created persistent ref in ${layer.id} without registering it to the layer! Make sure to include everything persistent in the returned object`,
|
||||
persistent,
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import { isPlainObject } from "is-plain-object";
|
||||
import Decimal from "util/bignum";
|
||||
import type { ProxiedWithState } from "util/proxies";
|
||||
import { ProxyPath, ProxyState } from "util/proxies";
|
||||
import { reactive, unref } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import transientState from "./state";
|
||||
import { reactive, unref } from "vue";
|
||||
|
||||
/** The player save data object. */
|
||||
export interface PlayerData {
|
||||
export interface Player {
|
||||
/** The ID of this save. */
|
||||
id: string;
|
||||
/** A multiplier for time passing. Set to 0 when the game is paused. */
|
||||
|
@ -36,9 +31,6 @@ export interface PlayerData {
|
|||
layers: Record<string, LayerData<unknown>>;
|
||||
}
|
||||
|
||||
/** The proxied player that is used to track NaN values. */
|
||||
export type Player = ProxiedWithState<PlayerData>;
|
||||
|
||||
/** A layer's save data. Automatically unwraps refs. */
|
||||
export type LayerData<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
|
@ -52,7 +44,7 @@ export type LayerData<T> = {
|
|||
: T[P];
|
||||
};
|
||||
|
||||
const state = reactive<PlayerData>({
|
||||
const player = reactive<Player>({
|
||||
id: "",
|
||||
devSpeed: null,
|
||||
name: "",
|
||||
|
@ -68,90 +60,16 @@ const state = reactive<PlayerData>({
|
|||
layers: {}
|
||||
});
|
||||
|
||||
export default window.player = player;
|
||||
|
||||
/** Convert a player save data object into a JSON string. Unwraps refs. */
|
||||
export function stringifySave(player: PlayerData): string {
|
||||
export function stringifySave(player: Player): string {
|
||||
return JSON.stringify(player, (key, value) => unref(value));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const playerHandler: ProxyHandler<Record<PropertyKey, any>> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get(target: Record<PropertyKey, any>, key: PropertyKey): any {
|
||||
if (key === ProxyState || key === ProxyPath) {
|
||||
return target[key];
|
||||
}
|
||||
|
||||
const value = target[ProxyState][key];
|
||||
if (key !== "value" && (isPlainObject(value) || Array.isArray(value))) {
|
||||
if (value !== target[key]?.[ProxyState]) {
|
||||
const path = [...target[ProxyPath], key];
|
||||
target[key] = new Proxy({ [ProxyState]: value, [ProxyPath]: path }, playerHandler);
|
||||
}
|
||||
return target[key];
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
set(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
target: Record<PropertyKey, any>,
|
||||
property: PropertyKey,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any,
|
||||
receiver: ProxyConstructor
|
||||
): boolean {
|
||||
if (
|
||||
!transientState.hasNaN &&
|
||||
((typeof value === "number" && isNaN(value)) ||
|
||||
(value instanceof Decimal &&
|
||||
(isNaN(value.sign) || isNaN(value.layer) || isNaN(value.mag))))
|
||||
) {
|
||||
const currentValue = target[ProxyState][property];
|
||||
if (
|
||||
!(
|
||||
(typeof currentValue === "number" && isNaN(currentValue)) ||
|
||||
(currentValue instanceof Decimal &&
|
||||
(isNaN(currentValue.sign) ||
|
||||
isNaN(currentValue.layer) ||
|
||||
isNaN(currentValue.mag)))
|
||||
)
|
||||
) {
|
||||
state.autosave = false;
|
||||
transientState.hasNaN = true;
|
||||
transientState.NaNPath = [...target[ProxyPath], property];
|
||||
transientState.NaNReceiver = receiver as unknown as Record<string, unknown>;
|
||||
console.error(
|
||||
`Attempted to set NaN value`,
|
||||
[...target[ProxyPath], property],
|
||||
target[ProxyState]
|
||||
);
|
||||
throw "Attempted to set NaN value. See above for details";
|
||||
}
|
||||
}
|
||||
target[ProxyState][property] = value;
|
||||
return true;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ownKeys(target: Record<PropertyKey, any>) {
|
||||
return Reflect.ownKeys(target[ProxyState]);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
has(target: Record<PropertyKey, any>, key: string) {
|
||||
return Reflect.has(target[ProxyState], key);
|
||||
},
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
return Object.getOwnPropertyDescriptor(target[ProxyState], key);
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
/** Augment the window object so the player can be accessed from the console. */
|
||||
interface Window {
|
||||
player: Player;
|
||||
}
|
||||
}
|
||||
/** The player save data object. */
|
||||
export default window.player = new Proxy(
|
||||
{ [ProxyState]: state, [ProxyPath]: ["player"] },
|
||||
playerHandler
|
||||
) as Player;
|
||||
|
|
323
src/game/requirements.tsx
Normal file
323
src/game/requirements.tsx
Normal file
|
@ -0,0 +1,323 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import { CoercableComponent, isVisible, jsx, setDefault, Visibility } from "features/feature";
|
||||
import { displayResource, Resource } from "features/resources/resource";
|
||||
import Decimal, { DecimalSource } from "lib/break_eternity";
|
||||
import {
|
||||
Computable,
|
||||
convertComputable,
|
||||
processComputable,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { joinJSX, renderJSX } from "util/vue";
|
||||
import { computed, unref } from "vue";
|
||||
import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
|
||||
import type { GenericFormula, InvertibleFormula } from "./formulas/types";
|
||||
import { DefaultValue, Persistent } from "./persistence";
|
||||
|
||||
/**
|
||||
* An object that can be used to describe a requirement to perform some purchase or other action.
|
||||
* @see {@link createCostRequirement}
|
||||
*/
|
||||
export interface Requirement {
|
||||
/**
|
||||
* The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}.
|
||||
*/
|
||||
partialDisplay?: (amount?: DecimalSource) => JSX.Element;
|
||||
/**
|
||||
* The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}.
|
||||
*/
|
||||
display?: (amount?: DecimalSource) => JSX.Element;
|
||||
/**
|
||||
* Whether or not this requirement should be displayed in Vue Features. {@link displayRequirements} will respect this property.
|
||||
*/
|
||||
visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
|
||||
/**
|
||||
* Whether or not this requirement has been met.
|
||||
*/
|
||||
requirementMet: ProcessedComputable<DecimalSource | boolean>;
|
||||
/**
|
||||
* Whether or not this requirement will need to affect the game state when whatever is using this requirement gets triggered.
|
||||
*/
|
||||
requiresPay: ProcessedComputable<boolean>;
|
||||
/**
|
||||
* Whether or not this requirement can have multiple levels of requirements that can be met at once. Requirement is assumed to not have multiple levels if this property not present.
|
||||
*/
|
||||
canMaximize?: ProcessedComputable<boolean>;
|
||||
/**
|
||||
* Perform any effects to the game state that should happen when the requirement gets triggered.
|
||||
* @param amount The amount of levels of requirements to pay for.
|
||||
*/
|
||||
pay?: (amount?: DecimalSource) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility type for accepting 1 or more {@link Requirement}s.
|
||||
*/
|
||||
export type Requirements = Requirement | Requirement[];
|
||||
|
||||
/** An object that configures a {@link Requirement} based on a resource cost. */
|
||||
export interface CostRequirementOptions {
|
||||
/**
|
||||
* The resource that will be checked for meeting the {@link cost}.
|
||||
*/
|
||||
resource: Resource;
|
||||
/**
|
||||
* The amount of {@link resource} that must be met for this requirement. You can pass a formula, in which case maximizing will work out of the box (assuming its invertible and, for more accurate calculations, its integral is invertible). If you don't pass a formula then you can still support maximizing by passing a custom {@link pay} function.
|
||||
*/
|
||||
cost: Computable<DecimalSource> | GenericFormula;
|
||||
/**
|
||||
* Pass-through to {@link Requirement.visibility}.
|
||||
*/
|
||||
visibility?: Computable<Visibility.Visible | Visibility.None | boolean>;
|
||||
/**
|
||||
* Pass-through to {@link Requirement.requiresPay}. If not set to false, the default {@link pay} function will remove {@link cost} from {@link resource}.
|
||||
*/
|
||||
requiresPay?: Computable<boolean>;
|
||||
/**
|
||||
* When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions.
|
||||
* @see {Formula}
|
||||
*/
|
||||
spendResources: Computable<boolean>;
|
||||
/**
|
||||
* Pass-through to {@link Requirement.pay}. May be required for maximizing support.
|
||||
* @see {@link cost} for restrictions on maximizing support.
|
||||
*/
|
||||
pay?: (amount?: DecimalSource) => void;
|
||||
}
|
||||
|
||||
export type CostRequirement = Requirement & CostRequirementOptions;
|
||||
|
||||
/**
|
||||
* Lazily creates a requirement with the given options, that is based on meeting an amount of a resource.
|
||||
* @param optionsFunc Cost requirement options.
|
||||
*/
|
||||
export function createCostRequirement<T extends CostRequirementOptions>(
|
||||
optionsFunc: () => T
|
||||
): CostRequirement {
|
||||
return createLazyProxy(() => {
|
||||
const req = optionsFunc() as T & Partial<Requirement>;
|
||||
|
||||
req.partialDisplay = amount => (
|
||||
<span
|
||||
style={
|
||||
unref(req.requirementMet as ProcessedComputable<boolean>)
|
||||
? ""
|
||||
: "color: var(--danger)"
|
||||
}
|
||||
>
|
||||
{displayResource(
|
||||
req.resource,
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(
|
||||
req.spendResources as ProcessedComputable<boolean> | undefined
|
||||
) ?? true
|
||||
)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)}{" "}
|
||||
{req.resource.displayName}
|
||||
</span>
|
||||
);
|
||||
req.display = amount => (
|
||||
<div>
|
||||
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
|
||||
{displayResource(
|
||||
req.resource,
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(
|
||||
req.spendResources as ProcessedComputable<boolean> | undefined
|
||||
) ?? true
|
||||
)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)}{" "}
|
||||
{req.resource.displayName}
|
||||
</div>
|
||||
);
|
||||
|
||||
processComputable(req as T, "visibility");
|
||||
setDefault(req, "visibility", Visibility.Visible);
|
||||
processComputable(req as T, "cost");
|
||||
processComputable(req as T, "requiresPay");
|
||||
setDefault(req, "requiresPay", true);
|
||||
processComputable(req as T, "spendResources");
|
||||
setDefault(req, "spendResources", false);
|
||||
setDefault(req, "pay", function (amount?: DecimalSource) {
|
||||
const cost =
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(req.spendResources as ProcessedComputable<boolean> | undefined) ??
|
||||
true
|
||||
)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
||||
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
|
||||
});
|
||||
|
||||
req.canMaximize = req.cost instanceof Formula && req.cost.isInvertible();
|
||||
|
||||
if (req.canMaximize) {
|
||||
req.requirementMet = calculateMaxAffordable(
|
||||
req.cost as InvertibleFormula,
|
||||
req.resource,
|
||||
unref(req.spendResources as ProcessedComputable<boolean> | undefined) ?? true
|
||||
);
|
||||
} else {
|
||||
req.requirementMet = computed(() => {
|
||||
if (req.cost instanceof Formula) {
|
||||
return Decimal.gte(req.resource.value, req.cost.evaluate());
|
||||
} else {
|
||||
return Decimal.gte(
|
||||
req.resource.value,
|
||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return req as CostRequirement;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for creating a requirement that a specified vue feature is visible
|
||||
* @param feature The feature to check the visibility of
|
||||
*/
|
||||
export function createVisibilityRequirement(feature: {
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}): Requirement {
|
||||
return createLazyProxy(() => ({
|
||||
requirementMet: computed(() => isVisible(feature.visibility)),
|
||||
visibility: Visibility.None,
|
||||
requiresPay: false
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a requirement based on a true/false value
|
||||
* @param requirement The boolean requirement to use
|
||||
* @param display How to display this requirement to the user
|
||||
*/
|
||||
export function createBooleanRequirement(
|
||||
requirement: Computable<boolean>,
|
||||
display?: CoercableComponent
|
||||
): Requirement {
|
||||
return createLazyProxy(() => ({
|
||||
requirementMet: convertComputable(requirement),
|
||||
partialDisplay: display == null ? undefined : jsx(() => renderJSX(display)),
|
||||
display: display == null ? undefined : jsx(() => <>Req: {renderJSX(display)}</>),
|
||||
visibility: display == null ? Visibility.None : Visibility.Visible,
|
||||
requiresPay: false
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for checking if 1+ requirements are all met
|
||||
* @param requirements The 1+ requirements to check
|
||||
*/
|
||||
export function requirementsMet(requirements: Requirements): boolean {
|
||||
if (isArray(requirements)) {
|
||||
return requirements.every(requirementsMet);
|
||||
}
|
||||
const reqsMet = unref(requirements.requirementMet);
|
||||
return typeof reqsMet === "boolean" ? reqsMet : Decimal.gt(reqsMet, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum number of levels that could be acquired with the current requirement states. True/false requirements will be counted as Infinity or 0.
|
||||
* @param requirements The 1+ requirements to check
|
||||
*/
|
||||
export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
||||
if (isArray(requirements)) {
|
||||
return requirements.map(maxRequirementsMet).reduce(Decimal.min);
|
||||
}
|
||||
const reqsMet = unref(requirements.requirementMet);
|
||||
if (typeof reqsMet === "boolean") {
|
||||
return reqsMet ? Decimal.dInf : 0;
|
||||
} else if (Decimal.gt(reqsMet, 1) && unref(requirements.canMaximize) !== true) {
|
||||
return 1;
|
||||
}
|
||||
return reqsMet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for display 1+ requirements compactly.
|
||||
* @param requirements The 1+ requirements to display
|
||||
* @param amount The amount of levels earned to be displayed
|
||||
*/
|
||||
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||
if (isArray(requirements)) {
|
||||
requirements = requirements.filter(r => isVisible(r.visibility));
|
||||
if (requirements.length === 1) {
|
||||
requirements = requirements[0];
|
||||
}
|
||||
}
|
||||
if (isArray(requirements)) {
|
||||
requirements = requirements.filter(r => "partialDisplay" in r);
|
||||
const withCosts = requirements.filter(r => unref(r.requiresPay));
|
||||
const withoutCosts = requirements.filter(r => !unref(r.requiresPay));
|
||||
return (
|
||||
<>
|
||||
{withCosts.length > 0 ? (
|
||||
<div>
|
||||
Costs:{" "}
|
||||
{joinJSX(
|
||||
withCosts.map(r => r.partialDisplay!(amount)),
|
||||
<>, </>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{withoutCosts.length > 0 ? (
|
||||
<div>
|
||||
Requires:{" "}
|
||||
{joinJSX(
|
||||
withoutCosts.map(r => r.partialDisplay!(amount)),
|
||||
<>, </>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return requirements.display?.() ?? <></>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for paying the costs for 1+ requirements
|
||||
* @param requirements The 1+ requirements to pay
|
||||
* @param amount How many levels to pay for
|
||||
*/
|
||||
export function payRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||
if (isArray(requirements)) {
|
||||
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
|
||||
} else if (unref(requirements.requiresPay)) {
|
||||
requirements.pay?.(amount);
|
||||
}
|
||||
}
|
||||
|
||||
export function payByDivision(this: CostRequirement, amount?: DecimalSource) {
|
||||
const cost =
|
||||
this.cost instanceof Formula
|
||||
? calculateCost(
|
||||
this.cost,
|
||||
amount ?? 1,
|
||||
unref(this.spendResources as ProcessedComputable<boolean> | undefined) ?? true
|
||||
)
|
||||
: unref(this.cost as ProcessedComputable<DecimalSource>);
|
||||
this.resource.value = Decimal.div(this.resource.value, cost);
|
||||
}
|
||||
|
||||
export function payByReset(overrideDefaultValue?: DecimalSource) {
|
||||
return function (this: CostRequirement) {
|
||||
this.resource.value =
|
||||
overrideDefaultValue ??
|
||||
(this.resource as Resource & Persistent<DecimalSource>)[DefaultValue] ??
|
||||
0;
|
||||
};
|
||||
}
|
|
@ -18,6 +18,8 @@ export interface Settings {
|
|||
theme: Themes;
|
||||
/** Whether or not to cap the project at 20 ticks per second. */
|
||||
unthrottled: boolean;
|
||||
/** Whether to align modifiers to the unit. */
|
||||
alignUnits: boolean;
|
||||
}
|
||||
|
||||
const state = reactive<Partial<Settings>>({
|
||||
|
@ -25,7 +27,8 @@ const state = reactive<Partial<Settings>>({
|
|||
saves: [],
|
||||
showTPS: true,
|
||||
theme: Themes.Nordic,
|
||||
unthrottled: false
|
||||
unthrottled: false,
|
||||
alignUnits: false
|
||||
});
|
||||
|
||||
watch(
|
||||
|
@ -57,7 +60,8 @@ export const hardResetSettings = (window.hardResetSettings = () => {
|
|||
active: "",
|
||||
saves: [],
|
||||
showTPS: true,
|
||||
theme: Themes.Nordic
|
||||
theme: Themes.Nordic,
|
||||
alignUnits: false
|
||||
};
|
||||
globalBus.emit("loadSettings", settings);
|
||||
Object.assign(state, settings);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import type { DecimalSource } from "util/bignum";
|
||||
import { shallowReactive } from "vue";
|
||||
import type { Persistent } from "./persistence";
|
||||
|
||||
/** An object of global data that is not persistent. */
|
||||
export interface Transient {
|
||||
|
@ -8,8 +10,8 @@ export interface Transient {
|
|||
hasNaN: boolean;
|
||||
/** The location within the player save data object of the NaN value. */
|
||||
NaNPath?: string[];
|
||||
/** The parent object of the NaN value. */
|
||||
NaNReceiver?: Record<string, unknown>;
|
||||
/** The ref that was being set to NaN. */
|
||||
NaNPersistent?: Persistent<DecimalSource>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
File diff suppressed because it is too large
Load diff
139
src/lib/lru-cache.ts
Normal file
139
src/lib/lru-cache.ts
Normal file
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* A LRU cache intended for caching pure functions.
|
||||
*/
|
||||
export class LRUCache<K, V> {
|
||||
private map = new Map<K, ListNode<K, V>>();
|
||||
// Invariant: Exactly one of the below is true before and after calling a
|
||||
// LRUCache method:
|
||||
// - first and last are both undefined, and map.size() is 0.
|
||||
// - first and last are the same object, and map.size() is 1.
|
||||
// - first and last are different objects, and map.size() is greater than 1.
|
||||
private first: ListNode<K, V> | undefined = undefined;
|
||||
private last: ListNode<K, V> | undefined = undefined;
|
||||
maxSize: number;
|
||||
|
||||
/**
|
||||
* @param maxSize The maximum size for this cache. We recommend setting this
|
||||
* to be one less than a power of 2, as most hashtables - including V8's
|
||||
* Object hashtable (https://crsrc.org/c/v8/src/objects/ordered-hash-table.cc)
|
||||
* - uses powers of two for hashtable sizes. It can't exactly be a power of
|
||||
* two, as a .set() call could temporarily set the size of the map to be
|
||||
* maxSize + 1.
|
||||
*/
|
||||
constructor(maxSize: number) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.map.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the specified key from the cache, or undefined if it is not in the
|
||||
* cache.
|
||||
* @param key The key to get.
|
||||
* @returns The cached value, or undefined if key is not in the cache.
|
||||
*/
|
||||
get(key: K): V | undefined {
|
||||
const node = this.map.get(key);
|
||||
if (node === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// It is guaranteed that there is at least one item in the cache.
|
||||
// Therefore, first and last are guaranteed to be a ListNode...
|
||||
// but if there is only one item, they might be the same.
|
||||
|
||||
// Update the order of the list to make this node the first node in the
|
||||
// list.
|
||||
// This isn't needed if this node is already the first node in the list.
|
||||
if (node !== this.first) {
|
||||
// As this node is DIFFERENT from the first node, it is guaranteed that
|
||||
// there are at least two items in the cache.
|
||||
// However, this node could possibly be the last item.
|
||||
if (node === this.last) {
|
||||
// This node IS the last node.
|
||||
this.last = node.prev;
|
||||
// From the invariants, there must be at least two items in the cache,
|
||||
// so node - which is the original "last node" - must have a defined
|
||||
// previous node. Therefore, this.last - set above - must be defined
|
||||
// here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.last!.next = undefined;
|
||||
} else {
|
||||
// This node is somewhere in the middle of the list, so there must be at
|
||||
// least THREE items in the list, and this node's prev and next must be
|
||||
// defined here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
node.prev!.next = node.next;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
node.next!.prev = node.prev;
|
||||
}
|
||||
node.next = this.first;
|
||||
// From the invariants, there must be at least two items in the cache, so
|
||||
// this.first must be a valid ListNode.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.first!.prev = node;
|
||||
this.first = node;
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an entry in the cache.
|
||||
*
|
||||
* @param key The key of the entry.
|
||||
* @param value The value of the entry.
|
||||
* @throws Error, if the map already contains the key.
|
||||
*/
|
||||
set(key: K, value: V): void {
|
||||
// Ensure that this.maxSize >= 1.
|
||||
if (this.maxSize < 1) {
|
||||
return;
|
||||
}
|
||||
if (this.map.has(key)) {
|
||||
throw new Error("Cannot update existing keys in the cache");
|
||||
}
|
||||
const node = new ListNode(key, value);
|
||||
// Move node to the front of the list.
|
||||
if (this.first === undefined) {
|
||||
// If the first is undefined, the last is undefined too.
|
||||
// Therefore, this cache has no items in it.
|
||||
this.first = node;
|
||||
this.last = node;
|
||||
} else {
|
||||
// This cache has at least one item in it.
|
||||
node.next = this.first;
|
||||
this.first.prev = node;
|
||||
this.first = node;
|
||||
}
|
||||
this.map.set(key, node);
|
||||
|
||||
while (this.map.size > this.maxSize) {
|
||||
// We are guaranteed that this.maxSize >= 1,
|
||||
// so this.map.size is guaranteed to be >= 2,
|
||||
// so this.first and this.last must be different valid ListNodes,
|
||||
// and this.last.prev must also be a valid ListNode (possibly this.first).
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const last = this.last!;
|
||||
this.map.delete(last.key);
|
||||
this.last = last.prev;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.last!.next = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A node in a doubly linked list.
|
||||
*/
|
||||
class ListNode<K, V> {
|
||||
key: K;
|
||||
value: V;
|
||||
next: ListNode<K, V> | undefined = undefined;
|
||||
prev: ListNode<K, V> | undefined = undefined;
|
||||
|
||||
constructor(key: K, value: V) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
7
src/lib/pixi.ts
Normal file
7
src/lib/pixi.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Application } from "@pixi/app";
|
||||
import { BatchRenderer, Renderer } from "@pixi/core";
|
||||
import { TickerPlugin } from "@pixi/ticker";
|
||||
|
||||
Application.registerPlugin(TickerPlugin);
|
||||
|
||||
Renderer.registerPlugin("batch", BatchRenderer);
|
13
src/main.ts
13
src/main.ts
|
@ -1,3 +1,4 @@
|
|||
import "@fontsource/material-icons";
|
||||
import App from "App.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import "game/notifications";
|
||||
|
@ -25,7 +26,9 @@ declare global {
|
|||
document.title = projInfo.title;
|
||||
window.projInfo = projInfo;
|
||||
if (projInfo.id === "") {
|
||||
throw "Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json";
|
||||
throw new Error(
|
||||
"Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json"
|
||||
);
|
||||
}
|
||||
|
||||
requestAnimationFrame(async () => {
|
||||
|
@ -35,7 +38,8 @@ requestAnimationFrame(async () => {
|
|||
"padding: 4px;"
|
||||
);
|
||||
await load();
|
||||
const { globalBus, startGameLoop } = await import("./game/events");
|
||||
const { globalBus } = await import("./game/events");
|
||||
const { startGameLoop } = await import("./game/gameLoop");
|
||||
|
||||
// Create Vue
|
||||
const vue = (window.vue = createApp(App));
|
||||
|
@ -47,7 +51,7 @@ requestAnimationFrame(async () => {
|
|||
const toast = useToast();
|
||||
const { updateServiceWorker } = useRegisterSW({
|
||||
onNeedRefresh() {
|
||||
toast.info("New content available, click or reload to update.", {
|
||||
toast.info("New content available, click here to update.", {
|
||||
timeout: false,
|
||||
closeOnClick: false,
|
||||
draggable: false,
|
||||
|
@ -68,7 +72,8 @@ requestAnimationFrame(async () => {
|
|||
onRegisterError: console.warn,
|
||||
onRegistered(r) {
|
||||
if (r) {
|
||||
setInterval(r.update, 60 * 60 * 1000);
|
||||
// https://stackoverflow.com/questions/65500916/typeerror-failed-to-execute-update-on-serviceworkerregistration-illegal-in
|
||||
setInterval(() => r.update(), 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ export const {
|
|||
export type DecimalSource = RawDecimalSource;
|
||||
|
||||
declare global {
|
||||
/** Augment the window object so the big num functions can be access from the console. */
|
||||
/** Augment the window object so the big num functions can be accessed from the console. */
|
||||
interface Window {
|
||||
Decimal: typeof Decimal;
|
||||
exponentialFormat: (num: DecimalSource, precision: number, mantissa: boolean) => string;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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>
|
||||
? S
|
||||
: never;
|
||||
|
||||
// Reference:
|
||||
// https://stackoverflow.com/questions/7225407/convert-camelcasetext-to-sentence-case-text
|
||||
export function camelToTitle(camel: string): string {
|
||||
|
@ -8,9 +12,10 @@ export function camelToTitle(camel: string): string {
|
|||
return title;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export function isFunction(func: unknown): func is Function {
|
||||
return typeof func === "function";
|
||||
export function isFunction<T, S extends ReadonlyArray<unknown>, R>(
|
||||
functionOrValue: ((...args: S) => T) | R
|
||||
): functionOrValue is (...args: S) => T {
|
||||
return typeof functionOrValue === "function";
|
||||
}
|
||||
|
||||
export enum Direction {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { JSXFunction } from "features/feature";
|
||||
import { isFunction } from "util/common";
|
||||
import type { Ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { isFunction } from "util/common";
|
||||
|
||||
export const DoNotCache = Symbol("DoNotCache");
|
||||
|
||||
|
@ -32,21 +33,22 @@ export function processComputable<T, S extends keyof ComputableKeysOf<T>>(
|
|||
key: S
|
||||
): asserts obj is T & { [K in S]: ProcessedComputable<UnwrapComputableType<T[S]>> } {
|
||||
const computable = obj[key];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (isFunction(computable) && computable.length === 0 && !(computable as any)[DoNotCache]) {
|
||||
if (
|
||||
isFunction(computable) &&
|
||||
computable.length === 0 &&
|
||||
!(computable as unknown as JSXFunction)[DoNotCache]
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
obj[key] = computed(computable.bind(obj));
|
||||
} else if (isFunction(computable)) {
|
||||
obj[key] = computable.bind(obj);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(obj[key] as any)[DoNotCache] = true;
|
||||
obj[key] = computable.bind(obj) as unknown as T[S];
|
||||
(obj[key] as unknown as JSXFunction)[DoNotCache] = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function convertComputable<T>(obj: Computable<T>): ProcessedComputable<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (isFunction(obj) && !(obj as any)[DoNotCache]) {
|
||||
if (isFunction(obj) && !(obj as unknown as JSXFunction)[DoNotCache]) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
obj = computed(obj);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import type { Persistent } from "game/persistence";
|
||||
import { NonPersistent } from "game/persistence";
|
||||
import Decimal from "util/bignum";
|
||||
|
||||
export const ProxyState = Symbol("ProxyState");
|
||||
export const ProxyPath = Symbol("ProxyPath");
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any>
|
||||
export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, unknown>
|
||||
? NonNullable<T> extends Decimal
|
||||
? T
|
||||
: {
|
||||
|
@ -15,9 +16,21 @@ export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any
|
|||
}
|
||||
: T;
|
||||
|
||||
export type Proxied<T> = NonNullable<T> extends Record<PropertyKey, unknown>
|
||||
? NonNullable<T> extends Persistent<infer S>
|
||||
? NonPersistent<S>
|
||||
: NonNullable<T> extends Decimal
|
||||
? T
|
||||
: {
|
||||
[K in keyof T]: Proxied<T[K]>;
|
||||
} & {
|
||||
[ProxyState]: T;
|
||||
}
|
||||
: T;
|
||||
|
||||
// Takes a function that returns an object and pretends to be that object
|
||||
// Note that the object is lazily calculated
|
||||
export function createLazyProxy<T extends object, S>(
|
||||
export function createLazyProxy<T extends object, S extends T>(
|
||||
objectFunc: (baseObject: S) => T & S,
|
||||
baseObject: S = {} as S
|
||||
): T {
|
||||
|
@ -37,7 +50,11 @@ export function createLazyProxy<T extends object, S>(
|
|||
return calculateObj();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (calculateObj() as any)[key];
|
||||
const val = (calculateObj() as any)[key];
|
||||
if (val != null && typeof val === "object" && NonPersistent in val) {
|
||||
return val[NonPersistent];
|
||||
}
|
||||
return val;
|
||||
},
|
||||
set(target, key, value) {
|
||||
// TODO give warning about this? It should only be done with caution
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import { globalBus } from "game/events";
|
||||
import type { Player, PlayerData } from "game/player";
|
||||
import type { Player } from "game/player";
|
||||
import player, { stringifySave } from "game/player";
|
||||
import settings, { loadSettings } from "game/settings";
|
||||
import LZString from "lz-string";
|
||||
import { ProxyState } from "util/proxies";
|
||||
import { ref } from "vue";
|
||||
|
||||
export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
|
||||
export function setupInitialStore(player: Partial<Player> = {}): Player {
|
||||
return Object.assign(
|
||||
{
|
||||
id: `${projInfo.id}-0`,
|
||||
|
@ -26,11 +26,9 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
|
|||
) as Player;
|
||||
}
|
||||
|
||||
export function save(playerData?: PlayerData): string {
|
||||
const stringifiedSave = LZString.compressToUTF16(
|
||||
stringifySave(playerData ?? player[ProxyState])
|
||||
);
|
||||
localStorage.setItem((playerData ?? player[ProxyState]).id, stringifiedSave);
|
||||
export function save(playerData?: Player): string {
|
||||
const stringifiedSave = LZString.compressToUTF16(stringifySave(playerData ?? player));
|
||||
localStorage.setItem((playerData ?? player).id, stringifiedSave);
|
||||
return stringifiedSave;
|
||||
}
|
||||
|
||||
|
@ -69,7 +67,7 @@ export async function load(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
export function newSave(): PlayerData {
|
||||
export function newSave(): Player {
|
||||
const id = getUniqueID();
|
||||
const player = setupInitialStore({ id });
|
||||
save(player);
|
||||
|
@ -84,12 +82,15 @@ export function getUniqueID(): string {
|
|||
i = 0;
|
||||
do {
|
||||
id = `${projInfo.id}-${i++}`;
|
||||
} while (localStorage.getItem(id));
|
||||
} while (localStorage.getItem(id) != null);
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function loadSave(playerObj: Partial<PlayerData>): Promise<void> {
|
||||
export const loadingSave = ref(false);
|
||||
|
||||
export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
||||
console.info("Loading save", playerObj);
|
||||
loadingSave.value = true;
|
||||
const { layers, removeLayer, addLayer } = await import("game/layers");
|
||||
const { fixOldSave, getInitialLayers } = await import("data/projEntry");
|
||||
|
||||
|
@ -102,13 +103,22 @@ export async function loadSave(playerObj: Partial<PlayerData>): Promise<void> {
|
|||
getInitialLayers(playerObj).forEach(layer => addLayer(layer, playerObj));
|
||||
|
||||
playerObj = setupInitialStore(playerObj);
|
||||
if (playerObj.offlineProd && playerObj.time) {
|
||||
if (
|
||||
playerObj.offlineProd &&
|
||||
playerObj.time != null &&
|
||||
playerObj.time &&
|
||||
playerObj.devSpeed !== 0
|
||||
) {
|
||||
if (playerObj.offlineTime == undefined) playerObj.offlineTime = 0;
|
||||
playerObj.offlineTime += (Date.now() - playerObj.time) / 1000;
|
||||
playerObj.offlineTime += Math.min(
|
||||
playerObj.offlineTime + (Date.now() - playerObj.time) / 1000,
|
||||
projInfo.offlineLimit * 3600
|
||||
);
|
||||
}
|
||||
playerObj.time = Date.now();
|
||||
if (playerObj.modVersion !== projInfo.versionNumber) {
|
||||
fixOldSave(playerObj.modVersion, playerObj);
|
||||
playerObj.modVersion = projInfo.versionNumber;
|
||||
}
|
||||
|
||||
Object.assign(player, playerObj);
|
||||
|
@ -130,14 +140,22 @@ window.onbeforeunload = () => {
|
|||
|
||||
declare global {
|
||||
/**
|
||||
* Augment the window object so the save function, and the hard reset function can be access from the console.
|
||||
* Augment the window object so the save, hard reset, and deleteLowerSaves functions can be accessed from the console.
|
||||
*/
|
||||
interface Window {
|
||||
save: VoidFunction;
|
||||
hardReset: VoidFunction;
|
||||
deleteLowerSaves: VoidFunction;
|
||||
}
|
||||
}
|
||||
window.save = save;
|
||||
export const hardReset = (window.hardReset = async () => {
|
||||
await loadSave(newSave());
|
||||
});
|
||||
export const deleteLowerSaves = (window.deleteLowerSaves = () => {
|
||||
const index = Object.values(settings.saves).indexOf(player.id) + 1;
|
||||
Object.values(settings.saves)
|
||||
.slice(index)
|
||||
.forEach(id => localStorage.removeItem(id));
|
||||
settings.saves = settings.saves.slice(0, index);
|
||||
});
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import Col from "components/layout/Column.vue";
|
||||
import Row from "components/layout/Row.vue";
|
||||
import type { CoercableComponent, GenericComponent, JSXFunction } from "features/feature";
|
||||
import { Component as ComponentKey, GatherProps, jsx, Visibility } from "features/feature";
|
||||
import {
|
||||
Component as ComponentKey,
|
||||
GatherProps,
|
||||
isVisible,
|
||||
jsx,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import type { ProcessedComputable } from "util/computed";
|
||||
import { DoNotCache } from "util/computed";
|
||||
import type { Component, ComputedRef, DefineComponent, PropType, Ref, ShallowRef } from "vue";
|
||||
|
@ -37,10 +43,10 @@ export function coerceComponent(
|
|||
return component;
|
||||
}
|
||||
|
||||
export type VueFeature = {
|
||||
export interface VueFeature {
|
||||
[ComponentKey]: GenericComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export function render(object: VueFeature | CoercableComponent): JSX.Element | DefineComponent {
|
||||
if (isCoercableComponent(object)) {
|
||||
|
@ -85,6 +91,16 @@ export function renderColJSX(...objects: (VueFeature | CoercableComponent)[]): J
|
|||
return <Col>{objects.map(renderJSX)}</Col>;
|
||||
}
|
||||
|
||||
export function joinJSX(objects: JSX.Element[], joiner: JSX.Element): JSX.Element {
|
||||
return objects.reduce((acc, curr) => (
|
||||
<>
|
||||
{acc}
|
||||
{joiner}
|
||||
{curr}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
export function isCoercableComponent(component: unknown): component is CoercableComponent {
|
||||
if (typeof component === "string") {
|
||||
return true;
|
||||
|
@ -137,7 +153,7 @@ export function setupHoldToClick(
|
|||
}
|
||||
|
||||
export function getFirstFeature<
|
||||
T extends VueFeature & { visibility: ProcessedComputable<Visibility> }
|
||||
T extends VueFeature & { visibility: ProcessedComputable<Visibility | boolean> }
|
||||
>(
|
||||
features: T[],
|
||||
filter: (feature: T) => boolean
|
||||
|
@ -147,9 +163,7 @@ export function getFirstFeature<
|
|||
hasCollapsedContent: Ref<boolean>;
|
||||
} {
|
||||
const filteredFeatures = computed(() =>
|
||||
features.filter(
|
||||
feature => unref(feature.visibility) === Visibility.Visible && filter(feature)
|
||||
)
|
||||
features.filter(feature => isVisible(feature.visibility) && filter(feature))
|
||||
);
|
||||
return {
|
||||
firstFeature: computed(() => filteredFeatures.value[0]),
|
||||
|
@ -175,7 +189,10 @@ export function computeOptionalComponent(
|
|||
const comp = shallowRef<Component | "" | null>(null);
|
||||
watchEffect(() => {
|
||||
const currComponent = unwrapRef(component);
|
||||
comp.value = currComponent == null ? null : coerceComponent(currComponent, defaultWrapper);
|
||||
comp.value =
|
||||
currComponent == "" || currComponent == null
|
||||
? null
|
||||
: coerceComponent(currComponent, defaultWrapper);
|
||||
});
|
||||
return comp;
|
||||
}
|
||||
|
@ -211,3 +228,16 @@ export function processedPropType<T>(...types: PropTypes[]): PropType<ProcessedC
|
|||
}
|
||||
return types as PropType<ProcessedComputable<T>>;
|
||||
}
|
||||
|
||||
export function trackHover(element: VueFeature): Ref<boolean> {
|
||||
const isHovered = ref(false);
|
||||
|
||||
const elementGatherProps = element[GatherProps].bind(element);
|
||||
element[GatherProps] = () => ({
|
||||
...elementGatherProps(),
|
||||
onPointerenter: () => (isHovered.value = true),
|
||||
onPointerleave: () => (isHovered.value = false)
|
||||
});
|
||||
|
||||
return isHovered;
|
||||
}
|
||||
|
|
1170
tests/game/formulas.test.ts
Normal file
1170
tests/game/formulas.test.ts
Normal file
File diff suppressed because it is too large
Load diff
185
tests/game/requirements.test.ts
Normal file
185
tests/game/requirements.test.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
import { Visibility } from "features/feature";
|
||||
import { createResource, Resource } from "features/resources/resource";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import {
|
||||
CostRequirement,
|
||||
createBooleanRequirement,
|
||||
createCostRequirement,
|
||||
createVisibilityRequirement,
|
||||
maxRequirementsMet,
|
||||
payRequirements,
|
||||
Requirement,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
import { isRef, ref, unref } from "vue";
|
||||
import "../utils";
|
||||
|
||||
describe("Creating cost requirement", () => {
|
||||
describe("Minimal requirement", () => {
|
||||
let resource: Resource;
|
||||
let requirement: CostRequirement;
|
||||
beforeAll(() => {
|
||||
resource = createResource(ref(10));
|
||||
requirement = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 10,
|
||||
spendResources: false
|
||||
}));
|
||||
});
|
||||
|
||||
test("resource pass-through", () => expect(requirement.resource).toBe(resource));
|
||||
test("cost pass-through", () => expect(requirement.cost).toBe(10));
|
||||
|
||||
test("partialDisplay exists", () =>
|
||||
expect(typeof requirement.partialDisplay).toBe("function"));
|
||||
test("display exists", () => expect(typeof requirement.display).toBe("function"));
|
||||
test("pay exists", () => expect(typeof requirement.pay).toBe("function"));
|
||||
test("requirementMet exists", () => {
|
||||
expect(requirement.requirementMet).not.toBeNull();
|
||||
expect(isRef(requirement.requirementMet)).toBe(true);
|
||||
});
|
||||
test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible));
|
||||
test("requires pay", () => expect(requirement.requiresPay).toBe(true));
|
||||
test("does not spend resources", () => expect(requirement.spendResources).toBe(false));
|
||||
test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false));
|
||||
});
|
||||
|
||||
describe("Fully customized", () => {
|
||||
let resource: Resource;
|
||||
let requirement: CostRequirement;
|
||||
beforeAll(() => {
|
||||
resource = createResource(ref(10));
|
||||
requirement = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).times(10),
|
||||
visibility: Visibility.None,
|
||||
requiresPay: false,
|
||||
maximize: true,
|
||||
spendResources: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
pay() {}
|
||||
}));
|
||||
});
|
||||
|
||||
test("pay is empty function", () =>
|
||||
requirement.pay != null &&
|
||||
typeof requirement.pay === "function" &&
|
||||
requirement.pay.length === 1);
|
||||
test("is not visible", () => expect(requirement.visibility).toBe(Visibility.None));
|
||||
test("does not require pay", () => expect(requirement.requiresPay).toBe(false));
|
||||
test("spends resources", () => expect(requirement.spendResources).toBe(true));
|
||||
test("can maximize", () => expect(unref(requirement.canMaximize)).toBe(true));
|
||||
});
|
||||
|
||||
test("Requirement met when meeting the cost", () => {
|
||||
const resource = createResource(ref(10));
|
||||
const requirement = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 10,
|
||||
spendResources: false
|
||||
}));
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
});
|
||||
|
||||
test("Requirement not met when not meeting the cost", () => {
|
||||
const resource = createResource(ref(10));
|
||||
const requirement = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 100,
|
||||
spendResources: false
|
||||
}));
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Creating visibility requirement", () => {
|
||||
test("Requirement met when visible", () => {
|
||||
const requirement = createVisibilityRequirement({ visibility: Visibility.Visible });
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
});
|
||||
|
||||
test("Requirement not met when not visible", () => {
|
||||
let requirement = createVisibilityRequirement({ visibility: Visibility.None });
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
requirement = createVisibilityRequirement({ visibility: false });
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Creating boolean requirement", () => {
|
||||
test("Requirement met when true", () => {
|
||||
const requirement = createBooleanRequirement(ref(true));
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
});
|
||||
|
||||
test("Requirement not met when false", () => {
|
||||
const requirement = createBooleanRequirement(ref(false));
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checking all requirements met", () => {
|
||||
let metRequirement: Requirement;
|
||||
let unmetRequirement: Requirement;
|
||||
beforeAll(() => {
|
||||
metRequirement = createBooleanRequirement(true);
|
||||
unmetRequirement = createBooleanRequirement(false);
|
||||
});
|
||||
|
||||
test("Returns true if no requirements", () => {
|
||||
expect(requirementsMet([])).toBe(true);
|
||||
});
|
||||
|
||||
test("Returns true if all requirements met", () => {
|
||||
expect(requirementsMet([metRequirement, metRequirement])).toBe(true);
|
||||
});
|
||||
|
||||
test("Returns false if any requirements unmet", () => {
|
||||
expect(requirementsMet([metRequirement, unmetRequirement])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checking maximum levels of requirements met", () => {
|
||||
test("Returns 0 if any requirement is not met", () => {
|
||||
const requirements = [
|
||||
createBooleanRequirement(false),
|
||||
createBooleanRequirement(true),
|
||||
createCostRequirement(() => ({
|
||||
resource: createResource(ref(10)),
|
||||
cost: Formula.variable(0),
|
||||
spendResources: false
|
||||
}))
|
||||
];
|
||||
expect(maxRequirementsMet(requirements)).compare_tolerance(0);
|
||||
});
|
||||
|
||||
test("Returns correct number of requirements met", () => {
|
||||
const requirements = [
|
||||
createBooleanRequirement(true),
|
||||
createCostRequirement(() => ({
|
||||
resource: createResource(ref(10)),
|
||||
cost: Formula.variable(0),
|
||||
spendResources: false
|
||||
}))
|
||||
];
|
||||
expect(maxRequirementsMet(requirements)).compare_tolerance(10);
|
||||
});
|
||||
});
|
||||
|
||||
test("Paying requirements", () => {
|
||||
const resource = createResource(ref(100));
|
||||
const noPayment = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 10,
|
||||
requiresPay: false,
|
||||
spendResources: false
|
||||
}));
|
||||
const payment = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 10,
|
||||
spendResources: false
|
||||
}));
|
||||
payRequirements([noPayment, payment]);
|
||||
expect(resource.value).compare_tolerance(90);
|
||||
});
|
|
@ -13,13 +13,15 @@ describe("isFunction", () => {
|
|||
test("Given function returns true", () => expect(isFunction(vi.fn())).toBe(true));
|
||||
|
||||
// Go through all primitives and basic types
|
||||
test("Given a string returns false", () => expect(isFunction("test")).toBe(false));
|
||||
test("Given a number returns false", () => expect(isFunction(10)).toBe(false));
|
||||
test("Given a bigint returns false", () => expect(isFunction(BigInt(10))).toBe(false));
|
||||
test("Given a boolean returns false", () => expect(isFunction(true)).toBe(false));
|
||||
test("Given undefined returns false", () => expect(isFunction(undefined)).toBe(false));
|
||||
test("Given a symbol returns false", () => expect(isFunction(Symbol())).toBe(false));
|
||||
test("Given null returns false", () => expect(isFunction(null)).toBe(false));
|
||||
test("Given object returns false", () => expect(isFunction({})).toBe(false));
|
||||
test("Given array returns false", () => expect(isFunction([])).toBe(false));
|
||||
test("Given a non-function returns false", () => {
|
||||
expect(isFunction("test")).toBe(false);
|
||||
expect(isFunction(10)).toBe(false);
|
||||
expect(isFunction(BigInt(10))).toBe(false);
|
||||
expect(isFunction(true)).toBe(false);
|
||||
expect(isFunction(undefined)).toBe(false);
|
||||
expect(isFunction(Symbol())).toBe(false);
|
||||
expect(isFunction(null)).toBe(false);
|
||||
expect(isFunction({})).toBe(false);
|
||||
expect(isFunction([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
40
tests/utils.ts
Normal file
40
tests/utils.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Decimal, { DecimalSource, format } from "util/bignum";
|
||||
import { expect } from "vitest";
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
compare_tolerance(expected: DecimalSource, tolerance?: number): R;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Vi {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Assertion extends CustomMatchers {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface AsymmetricMatchersContaining extends CustomMatchers {}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
compare_tolerance(received: DecimalSource, expected: DecimalSource, tolerance?: number) {
|
||||
const { isNot } = this;
|
||||
let pass = false;
|
||||
if (!Decimal.isFinite(expected)) {
|
||||
pass = !Decimal.isFinite(received);
|
||||
} else if (Decimal.isNaN(expected)) {
|
||||
pass = Decimal.isNaN(received);
|
||||
} else {
|
||||
pass = Decimal.eq_tolerance(received, expected, tolerance);
|
||||
}
|
||||
return {
|
||||
// do not alter your "pass" based on isNot. Vitest does it for you
|
||||
pass,
|
||||
message: () =>
|
||||
`Expected ${received} to${
|
||||
(isNot as boolean) ? " not" : ""
|
||||
} be close to ${expected}`,
|
||||
expected: format(expected),
|
||||
actual: format(received)
|
||||
};
|
||||
}
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue