forked from profectus/Profectus
Compare commits
36 commits
e32e4fd2bf
...
bd165da264
Author | SHA1 | Date | |
---|---|---|---|
bd165da264 | |||
c93418bfec | |||
88abd53faf | |||
80d7a743bc | |||
2dab35f7cf | |||
dc8d0ecc95 | |||
707aacc383 | |||
1e13da1129 | |||
643bfccada | |||
f9c59f7636 | |||
99227a2cb2 | |||
e0da9588d2 | |||
46d0a9aa2e | |||
c6035f9077 | |||
8745304631 | |||
5b33a0fceb | |||
6a17bbc29c | |||
a75c8d81f8 | |||
c64ac82a25 | |||
1cbe97251c | |||
6ba25f9abd | |||
673f7790c7 | |||
ae45f9bc2f | |||
8a9e106157 | |||
90300ce848 | |||
2b861c3fcf | |||
9debfe6fb4 | |||
9f25d7f58f | |||
239ae7c94a | |||
2d28be84a9 | |||
c6389317d0 | |||
b98f6db1c4 | |||
563eaa7539 | |||
b88fa68874 | |||
90d0307cf0 | |||
dfb14acc6e |
40 changed files with 2200 additions and 2630 deletions
|
@ -34,6 +34,13 @@ module.exports = {
|
|||
allowNullableObject: true,
|
||||
allowNullableBoolean: true
|
||||
}
|
||||
],
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"null": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
globals: {
|
||||
|
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -6,6 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.2] - 2024-04-01
|
||||
### Added
|
||||
- Export save button in error boundaries
|
||||
- isRendered utility function
|
||||
- Automatic galaxy.click cloud saves support
|
||||
- Support for null and undefined in persistent refs
|
||||
### Changes
|
||||
- round, floor, ceil, trunc, and add now invert as no-ops
|
||||
- "The Paper Pilot Community" renamed to "Profectus & Friends"
|
||||
- Updated CI etc. to work with Forgejo
|
||||
- Improved modifier typing
|
||||
- Rename `printFormula` to `Formula.stringify`
|
||||
### Fixed
|
||||
- Hotkeys not working correctly with most combinations of modifiers
|
||||
- Reset button using `currentAt` when not gaining
|
||||
- Formulas not using modifiers that are disabled initially
|
||||
- branchedResetPropagation logic being incorrect
|
||||
- Fixed default elementsd in the main layer not updating Context when being added or removed
|
||||
- Board links props not working in camelCase
|
||||
- Board links absorbing pointer events
|
||||
- Thrown errors not appearing in console
|
||||
- Disabled elements would eat mouse events
|
||||
- Fixed cost requirement without formula counting as being able to afford infinite purchases rather than just one
|
||||
- Pinnable tooltips causing innocuous console error
|
||||
- Bars with direction as "Left" wouldn't appear correctly
|
||||
### Documentation
|
||||
- Clarified expected progress values for board nodes
|
||||
- Added CONTRIBUTING.md and enforce eslint on all PRs
|
||||
### Tests
|
||||
- Update formula test cases
|
||||
- Tree reset propagation
|
||||
|
||||
Contributors: thepaperpilot, escapee, nif
|
||||
|
||||
## [0.6.1] - 2023-05-17
|
||||
### Added
|
||||
- Error boundaries around each layer, and errors now display on the page when in development
|
||||
|
|
4326
package-lock.json
generated
4326
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "profectus",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@ -15,8 +15,8 @@
|
|||
"lint:fix": "eslint --fix --max-warnings 0 src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/material-icons": "^5.0.16",
|
||||
"@fontsource/roboto-mono": "^5.0.17",
|
||||
"@fontsource/material-icons": "^5.1.0",
|
||||
"@fontsource/roboto-mono": "^5.1.0",
|
||||
"@pixi/app": "^6.5.10",
|
||||
"@pixi/constants": "~6.5.10",
|
||||
"@pixi/core": "^6.5.10",
|
||||
|
@ -25,16 +25,16 @@
|
|||
"@pixi/particle-emitter": "^5.0.7",
|
||||
"@pixi/sprite": "~6.5.10",
|
||||
"@pixi/ticker": "~6.5.10",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lz-string": "^1.5.0",
|
||||
"nanoevents": "^9.0.0",
|
||||
"unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.1",
|
||||
"vite": "^5.1.6",
|
||||
"vite-plugin-pwa": "^0.19.4",
|
||||
"vite": "^5.1.8",
|
||||
"vite-plugin-pwa": "^0.20.5",
|
||||
"vite-tsconfig-paths": "^4.3.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue": "^3.5.12",
|
||||
"vue-next-select": "^2.10.5",
|
||||
"vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<Nav v-if="useHeader" />
|
||||
<Game />
|
||||
<TPS v-if="unref(showTPS)" />
|
||||
<AddictionWarning />
|
||||
<GameOverScreen />
|
||||
<NaNScreen />
|
||||
<CloudSaveResolver />
|
||||
|
@ -17,15 +18,16 @@
|
|||
<script setup lang="tsx">
|
||||
import "@fontsource/roboto-mono";
|
||||
import Error from "components/Error.vue";
|
||||
import CloudSaveResolver from "components/saves/CloudSaveResolver.vue";
|
||||
import AddictionWarning from "components/modals/AddictionWarning.vue";
|
||||
import CloudSaveResolver from "components/modals/CloudSaveResolver.vue";
|
||||
import GameOverScreen from "components/modals/GameOverScreen.vue";
|
||||
import NaNScreen from "components/modals/NaNScreen.vue";
|
||||
import { jsx } from "features/feature";
|
||||
import state from "game/state";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { computed, toRef, unref } from "vue";
|
||||
import Game from "./components/Game.vue";
|
||||
import GameOverScreen from "./components/GameOverScreen.vue";
|
||||
import NaNScreen from "./components/NaNScreen.vue";
|
||||
import Nav from "./components/Nav.vue";
|
||||
import TPS from "./components/TPS.vue";
|
||||
import projInfo from "./data/projInfo.json";
|
||||
|
|
|
@ -103,9 +103,9 @@ import { Direction } from "util/common";
|
|||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import Info from "./Info.vue";
|
||||
import Options from "./Options.vue";
|
||||
import SavesManager from "./saves/SavesManager.vue";
|
||||
import Info from "./modals/Info.vue";
|
||||
import Options from "./modals/Options.vue";
|
||||
import SavesManager from "./modals/SavesManager.vue";
|
||||
|
||||
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
||||
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
||||
|
|
83
src/components/modals/AddictionWarning.vue
Normal file
83
src/components/modals/AddictionWarning.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<Modal v-model="isOpen" v-bind="$attrs">
|
||||
<template v-slot:header>
|
||||
<div class="vga-modal-header">
|
||||
<h2>Kindly consider taking a break.</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<p>
|
||||
You've been actively enjoying this game for awhile recently - and it's great that
|
||||
you've been having a good time! That said, there are dangers to games like these that you should be aware of:
|
||||
</p>
|
||||
<p>
|
||||
While incremental games can be fun and even healthy in certain contexts, they can
|
||||
exacerbate video game addiction even more than other genres. If you feel like
|
||||
playing incremental games is taking priority over other things in your life, or
|
||||
manipulating your sleep schedule, it may be prudent to seek help.
|
||||
</p>
|
||||
<h4>Resources:</h4>
|
||||
<p>
|
||||
<span>
|
||||
<a style="display: inline" href="https://www.samhsa.gov/" target="_blank">
|
||||
SAMHSA
|
||||
</a>
|
||||
(<a style="display: inline" href="tel:1-800-662-4357">1-800-662-HELP</a>)
|
||||
</span>
|
||||
<br />
|
||||
<a href="https://www.reddit.com/r/StopGaming/">r/StopGaming</a>
|
||||
</p>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="vga-footer">
|
||||
<button @click="neverShow" class="button">Never show this again</button>
|
||||
<button @click="isOpen = false" class="button">Close</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
<SavesManager ref="savesManager" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import projInfo from "data/projInfo.json";
|
||||
import settings from "game/settings";
|
||||
import state from "game/state";
|
||||
import { ref, watchEffect } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
watchEffect(() => {
|
||||
if (
|
||||
projInfo.disableHealthWarning === false &&
|
||||
settings.showHealthWarning &&
|
||||
state.mouseActivity.filter(i => i).length > 6
|
||||
) {
|
||||
isOpen.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
function neverShow() {
|
||||
settings.showHealthWarning = false;
|
||||
isOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vga-modal-header {
|
||||
padding-top: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.vga-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.vga-footer button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
|
@ -74,13 +74,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import { stringifySave } from "game/player";
|
||||
import settings from "game/settings";
|
||||
import LZString from "lz-string";
|
||||
import { conflictingSaves, galaxy } from "util/galaxy";
|
||||
import { getUniqueID, save, setupInitialStore } from "util/save";
|
||||
import { ComponentPublicInstance, computed, ref, unref, watch } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import Save from "./Save.vue";
|
||||
|
||||
const isOpen = ref(false);
|
|
@ -37,14 +37,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import { hasWon } from "data/projEntry";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import player from "game/player";
|
||||
import { formatTime } from "util/bignum";
|
||||
import { loadSave, newSave } from "util/save";
|
||||
import { computed, toRef } from "vue";
|
||||
import Toggle from "./fields/Toggle.vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
||||
|
|
@ -60,7 +60,6 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import Modal from "components/Modal.vue";
|
||||
import type Changelog from "data/Changelog.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import { jsx } from "features/feature";
|
||||
|
@ -69,6 +68,7 @@ import { infoComponents } from "game/settings";
|
|||
import { formatTime } from "util/bignum";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import { computed, ref, toRefs, unref } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { computed, ref, toRefs, unref } from "vue";
|
||||
import Context from "./Context.vue";
|
||||
import Context from "../Context.vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
modelValue: boolean;
|
|
@ -46,7 +46,6 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import player from "game/player";
|
||||
import state from "game/state";
|
||||
|
@ -54,8 +53,9 @@ import type { DecimalSource } from "util/bignum";
|
|||
import Decimal, { format } from "util/bignum";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { computed, ref, toRef, watch } from "vue";
|
||||
import Toggle from "./fields/Toggle.vue";
|
||||
import SavesManager from "./saves/SavesManager.vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import SavesManager from "./SavesManager.vue";
|
||||
|
||||
const { discordName, discordLink } = projInfo;
|
||||
const autosave = ref(true);
|
|
@ -14,6 +14,7 @@
|
|||
<Toggle :title="unthrottledTitle" v-model="unthrottled" />
|
||||
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
|
||||
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
|
||||
<Toggle :title="showHealthWarningTitle" v-model="showHealthWarning" v-if="!projInfo.disableHealthWarning" />
|
||||
<Toggle :title="autosaveTitle" v-model="autosave" />
|
||||
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
|
||||
</div>
|
||||
|
@ -28,20 +29,20 @@
|
|||
</template>
|
||||
|
||||
<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, Direction } from "util/common";
|
||||
import { save } from "util/save";
|
||||
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";
|
||||
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||
import Select from "../fields/Select.vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
const currentTab = ref("behaviour");
|
||||
|
@ -72,7 +73,7 @@ const settingFieldsComponent = computed(() => {
|
|||
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
|
||||
});
|
||||
|
||||
const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings);
|
||||
const { showTPS, theme, unthrottled, alignUnits, showHealthWarning } = toRefs(settings);
|
||||
const { autosave, offlineProd } = toRefs(player);
|
||||
const isPaused = computed({
|
||||
get() {
|
||||
|
@ -91,10 +92,16 @@ const unthrottledTitle = jsx(() => (
|
|||
));
|
||||
const offlineProdTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Offline Production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
Offline production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Simulate production that occurs while the game is closed.</desc>
|
||||
</span>
|
||||
));
|
||||
const showHealthWarningTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Show videogame addiction warning
|
||||
<desc>Show a helpful warning after playing for a long time about video game addiction and encouraging you to take a break.</desc>
|
||||
</span>
|
||||
));
|
||||
const autosaveTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
|
@ -60,12 +60,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import type { Player } from "game/player";
|
||||
import player, { stringifySave } from "game/player";
|
||||
import settings from "game/settings";
|
||||
import LZString from "lz-string";
|
||||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
import {
|
||||
clearCachedSave,
|
||||
clearCachedSaves,
|
||||
|
@ -81,8 +81,8 @@ import { computed, nextTick, ref, watch } from "vue";
|
|||
import Draggable from "vuedraggable";
|
||||
import Select from "../fields/Select.vue";
|
||||
import Text from "../fields/Text.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import Save from "./Save.vue";
|
||||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
|
||||
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
|
||||
|
||||
|
@ -130,7 +130,7 @@ watch(saveToImport, importedSave => {
|
|||
}
|
||||
});
|
||||
|
||||
let bankContext = import.meta.glob("./../../saves/*.txt", { query: "?raw", eager: true });
|
||||
let bankContext = import.meta.glob("./../../../saves/*.txt", { query: "?raw", eager: true });
|
||||
let bank = ref(
|
||||
Object.keys(bankContext).reduce((acc: Array<{ label: string; value: string }>, curr) => {
|
||||
acc.push({
|
|
@ -19,7 +19,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import Modal from "components/modals/Modal.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
|
|
@ -88,6 +88,10 @@
|
|||
"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."
|
||||
},
|
||||
"disableHealthWarning": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to disable the health warning that appears to the player after excessive playtime (activity during 6 of the last 8 hours). If left enabled, the player will still be able to individually turn off the health warning in settings or by clicking \"Do not show again\" in the warning itself."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
"title": "Profectus",
|
||||
"description": "A project made in Profectus",
|
||||
"id": "321",
|
||||
"id": "",
|
||||
"author": "",
|
||||
"discordName": "",
|
||||
"discordLink": "",
|
||||
|
@ -22,5 +22,6 @@
|
|||
"maxTickLength": 3600,
|
||||
"offlineLimit": 1,
|
||||
"enablePausing": true,
|
||||
"exportEncoding": "base64"
|
||||
"exportEncoding": "base64",
|
||||
"disableHealthWarning": false
|
||||
}
|
||||
|
|
|
@ -207,7 +207,7 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
unref(achievement.earned) &&
|
||||
!(
|
||||
display != null &&
|
||||
typeof display == "object" &&
|
||||
typeof display === "object" &&
|
||||
"optionsDisplay" in (display as Record<string, unknown>)
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -93,7 +93,7 @@ export function setDefault<T, K extends keyof T>(
|
|||
key: K,
|
||||
value: T[K]
|
||||
): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
|
||||
if (object[key] === undefined && value != undefined) {
|
||||
if (object[key] == null && value != null) {
|
||||
object[key] = value;
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]
|
|||
if (value != null && typeof value === "object") {
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
typeof (value as Record<string, any>).type == "symbol" &&
|
||||
typeof (value as Record<string, any>).type === "symbol" &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
!types.includes((value as Record<string, any>).type)
|
||||
) {
|
||||
|
|
|
@ -128,7 +128,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
}
|
||||
if (prop != undefined || typeof key === "symbol") {
|
||||
if (prop != null || typeof key === "symbol") {
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
cache[key] = computed(() => prop.call(receiver, id, target.getState(id)));
|
||||
}
|
||||
return cache[key].value;
|
||||
} else if (prop != undefined) {
|
||||
} else if (prop != null) {
|
||||
return unref(prop);
|
||||
}
|
||||
|
||||
|
@ -153,7 +153,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
prop = (target as any)[`on${key}`];
|
||||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
} else if (prop != undefined) {
|
||||
} else if (prop != null) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
@ -318,7 +318,7 @@ export function createGrid<T extends GridOptions>(
|
|||
return grid.id + "-" + cell;
|
||||
};
|
||||
grid.getState = function (this: GenericGrid, cell: string | number) {
|
||||
if (this.cellState.value[cell] != undefined) {
|
||||
if (this.cellState.value[cell] != null) {
|
||||
return cellState.value[cell];
|
||||
}
|
||||
return this.cells[cell].startState;
|
||||
|
|
|
@ -99,16 +99,30 @@ document.onkeydown = function (e) {
|
|||
if (hasWon.value && !player.keepGoing) {
|
||||
return;
|
||||
}
|
||||
let key = e.key;
|
||||
if (uppercaseNumbers.includes(key)) {
|
||||
key = "shift+" + uppercaseNumbers.indexOf(key);
|
||||
const keysToCheck: string[] = [e.key];
|
||||
if (e.shiftKey && e.ctrlKey) {
|
||||
keysToCheck.splice(0, 1);
|
||||
keysToCheck.push("ctrl+shift+" + e.key.toUpperCase());
|
||||
keysToCheck.push("shift+ctrl+" + e.key.toUpperCase());
|
||||
if (uppercaseNumbers.includes(e.key)) {
|
||||
keysToCheck.push("ctrl+shift+" + uppercaseNumbers.indexOf(e.key));
|
||||
keysToCheck.push("shift+ctrl+" + uppercaseNumbers.indexOf(e.key));
|
||||
} else {
|
||||
keysToCheck.push("ctrl+shift+" + e.key.toLowerCase());
|
||||
keysToCheck.push("shift+ctrl+" + e.key.toLowerCase());
|
||||
}
|
||||
} else if (uppercaseNumbers.includes(e.key)) {
|
||||
keysToCheck.push("shift+" + e.key);
|
||||
keysToCheck.push("shift+" + uppercaseNumbers.indexOf(e.key));
|
||||
} else if (e.shiftKey) {
|
||||
key = "shift+" + key;
|
||||
keysToCheck.push("shift+" + e.key.toUpperCase());
|
||||
keysToCheck.push("shift+" + e.key.toLowerCase());
|
||||
} else if (e.ctrlKey) {
|
||||
// remove e.key since the key doesn't change based on ctrl being held or not
|
||||
keysToCheck.splice(0, 1);
|
||||
keysToCheck.push("ctrl+" + e.key);
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
key = "ctrl+" + key;
|
||||
}
|
||||
const hotkey = hotkeys[key] ?? hotkeys[key.toLowerCase()];
|
||||
const hotkey = hotkeys[keysToCheck.find(key => key in hotkeys) ?? ""];
|
||||
if (hotkey && unref(hotkey.enabled)) {
|
||||
e.preventDefault();
|
||||
hotkey.onPress();
|
||||
|
@ -129,7 +143,8 @@ globalBus.on("setupVue", () =>
|
|||
<div style="column-count: 2">
|
||||
{keys.map(hotkey => (
|
||||
<div>
|
||||
<Hotkey hotkey={hotkey as GenericHotkey} /> {hotkey?.description}
|
||||
<Hotkey hotkey={hotkey as GenericHotkey} />{" "}
|
||||
{unref(hotkey?.description)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@ onMounted(() => (boundingRect.value = resizeListener.value?.getBoundingClientRec
|
|||
const validLinks = computed(() => {
|
||||
const n = nodes.value;
|
||||
return (
|
||||
links.value?.filter(link => n[link.startNode.id]?.rect && n[link.startNode.id]?.rect) ?? []
|
||||
links.value?.filter(link => n[link.startNode.id]?.rect && n[link.endNode.id]?.rect) ?? []
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -346,7 +346,7 @@ export const branchedResetPropagation = function (
|
|||
if (links == null) return;
|
||||
const reset: GenericTreeNode[] = [];
|
||||
let current = [resettingNode];
|
||||
while (current.length != 0) {
|
||||
while (current.length !== 0) {
|
||||
const next: GenericTreeNode[] = [];
|
||||
for (const node of current) {
|
||||
for (const link of links.filter(link => link.startNode === node)) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { Settings } from "game/settings";
|
|||
import { createNanoEvents } from "nanoevents";
|
||||
import type { App } from "vue";
|
||||
import type { GenericLayer } from "./layers";
|
||||
import state from "./state";
|
||||
|
||||
/** All types of events able to be sent or emitted from the global event bus. */
|
||||
export interface GlobalEvents {
|
||||
|
@ -59,3 +60,7 @@ if ("fonts" in document) {
|
|||
// JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document
|
||||
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
|
||||
}
|
||||
|
||||
document.onmousemove = function () {
|
||||
state.mouseActivity[state.mouseActivity.length - 1] = true;
|
||||
};
|
||||
|
|
|
@ -57,6 +57,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
protected readonly internalIntegrate: IntegrateFunction<T> | undefined;
|
||||
protected readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
|
||||
protected readonly applySubstitution: SubstitutionFunction<T> | undefined;
|
||||
protected readonly description: string | undefined;
|
||||
protected readonly internalVariables: number;
|
||||
|
||||
public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
|
||||
|
@ -86,6 +87,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
this.internalIntegrate = readonlyProperties.internalIntegrate;
|
||||
this.internalIntegrateInner = readonlyProperties.internalIntegrateInner;
|
||||
this.applySubstitution = readonlyProperties.applySubstitution;
|
||||
this.description = options.description;
|
||||
}
|
||||
|
||||
private setupVariable({
|
||||
|
@ -217,6 +219,25 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
return new Formula({ variable: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringifies the formula so it's more easy to read in the console
|
||||
* @param formula The formula source to print, used for mapping inputs
|
||||
*/
|
||||
public static stringify(formula: FormulaSource): string {
|
||||
if (formula instanceof InternalFormula) {
|
||||
if (formula.description != null) {
|
||||
return formula.description;
|
||||
}
|
||||
if (formula.internalEvaluate == null) {
|
||||
return formula.hasVariable() ? "x" : format(formula.inputs[0] ?? 0);
|
||||
}
|
||||
return `${formula.internalEvaluate.name}(${formula.inputs
|
||||
.map(Formula.stringify)
|
||||
.join(", ")})`;
|
||||
}
|
||||
return format(unref(formula));
|
||||
}
|
||||
|
||||
// TODO add integration support to step-wise functions
|
||||
/**
|
||||
* Creates a step-wise formula. After {@link start} the formula will have an additional modifier.
|
||||
|
@ -257,7 +278,9 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: evalStep,
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined,
|
||||
// Can't do anything more descriptive, due to formula's input always being a variable
|
||||
description: "indeterminate"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -310,7 +333,9 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: evalStep,
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined,
|
||||
// Can't do anything more descriptive, due to formula's input always being a variable
|
||||
description: "indeterminate"
|
||||
});
|
||||
}
|
||||
public static conditional(
|
||||
|
@ -879,6 +904,10 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
});
|
||||
}
|
||||
|
||||
public stringify() {
|
||||
return Formula.stringify(this);
|
||||
}
|
||||
|
||||
public step(
|
||||
start: Computable<DecimalSource>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
|
@ -1403,28 +1432,6 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringifies a formula so it's more easy to read in the console
|
||||
* @param formula The formula to print
|
||||
*/
|
||||
export function printFormula(formula: FormulaSource): string {
|
||||
if (formula instanceof InternalFormula) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return formula.internalEvaluate == null
|
||||
? formula.hasVariable()
|
||||
? "x"
|
||||
: formula.inputs[0] ?? 0
|
||||
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
formula.internalEvaluate.name +
|
||||
"(" +
|
||||
formula.inputs.map(printFormula).join(", ") +
|
||||
")";
|
||||
}
|
||||
return format(unref(formula));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@link cumulativeCost} is changed to false, the calculation will be much faster with higher numbers.
|
||||
* @param formula The formula to use for calculating buy max from
|
||||
|
|
8
src/game/formulas/types.d.ts
vendored
8
src/game/formulas/types.d.ts
vendored
|
@ -37,9 +37,13 @@ type SubstitutionFunction<T> = (
|
|||
...inputs: T
|
||||
) => GenericFormula;
|
||||
|
||||
type VariableFormulaOptions = { variable: ProcessedComputable<DecimalSource> };
|
||||
type VariableFormulaOptions = {
|
||||
variable: ProcessedComputable<DecimalSource>;
|
||||
description?: string;
|
||||
};
|
||||
type ConstantFormulaOptions = {
|
||||
inputs: [FormulaSource];
|
||||
description?: string;
|
||||
};
|
||||
type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
||||
inputs: T;
|
||||
|
@ -48,6 +52,7 @@ type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
|||
integrate?: IntegrateFunction<T>;
|
||||
integrateInner?: IntegrateFunction<T>;
|
||||
applySubstitution?: SubstitutionFunction<T>;
|
||||
description?: string;
|
||||
};
|
||||
type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
|
||||
| VariableFormulaOptions
|
||||
|
@ -63,6 +68,7 @@ type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
|
|||
internalIntegrateInner?: IntegrateFunction<T>;
|
||||
applySubstitution?: SubstitutionFunction<T>;
|
||||
innermostVariable?: ProcessedComputable<DecimalSource>;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type SubstitutionStack = ((value: GenericFormula) => GenericFormula)[] | undefined;
|
||||
|
|
|
@ -39,7 +39,7 @@ function update() {
|
|||
loadingSave.value = false;
|
||||
|
||||
// Add offline time if any
|
||||
if (player.offlineTime != undefined) {
|
||||
if (player.offlineTime != null) {
|
||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ function update() {
|
|||
diff = Math.min(diff, projInfo.maxTickLength);
|
||||
|
||||
// Apply dev speed
|
||||
if (player.devSpeed != undefined) {
|
||||
if (player.devSpeed != null) {
|
||||
diff *= player.devSpeed;
|
||||
}
|
||||
|
||||
|
@ -103,3 +103,7 @@ watch(hasWon, hasWon => {
|
|||
globalBus.emit("gameWon");
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
state.mouseActivity = [...state.mouseActivity.slice(-7), false];
|
||||
}, 1000 * 60 * 60);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Modal from "components/Modal.vue";
|
||||
import Modal from "components/modals/Modal.vue";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
JSXFunction,
|
||||
|
|
|
@ -61,6 +61,8 @@ export type State =
|
|||
| number
|
||||
| boolean
|
||||
| DecimalSource
|
||||
| null
|
||||
| undefined
|
||||
| { [key: string]: State }
|
||||
| { [key: number]: State };
|
||||
|
||||
|
@ -226,7 +228,7 @@ export function noPersist<T extends Persistent<S>, S extends State>(persistent:
|
|||
if (key === PersistentState) {
|
||||
return false;
|
||||
}
|
||||
if (key == SkipPersistence) {
|
||||
if (key === SkipPersistence) {
|
||||
return true;
|
||||
}
|
||||
return Reflect.has(target, key);
|
||||
|
@ -278,7 +280,7 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
// Handle SaveDataPath
|
||||
const newPath = [layer.id, ...path, key];
|
||||
if (
|
||||
value[SaveDataPath] != undefined &&
|
||||
value[SaveDataPath] != null &&
|
||||
JSON.stringify(newPath) !== JSON.stringify(value[SaveDataPath])
|
||||
) {
|
||||
console.error(
|
||||
|
|
|
@ -64,7 +64,8 @@ export default window.player = player;
|
|||
|
||||
/** Convert a player save data object into a JSON string. Unwraps refs. */
|
||||
export function stringifySave(player: Player): string {
|
||||
return JSON.stringify(player, (key, value) => unref(value));
|
||||
// Convert undefineds into nulls for proper parsing
|
||||
return JSON.stringify(player, (key, value) => unref(value) ?? null);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -20,6 +20,8 @@ export interface Settings {
|
|||
unthrottled: boolean;
|
||||
/** Whether to align modifiers to the unit. */
|
||||
alignUnits: boolean;
|
||||
/** Whether or not to show a video game health warning after playing excessively. */
|
||||
showHealthWarning: boolean;
|
||||
}
|
||||
|
||||
const state = reactive<Partial<Settings>>({
|
||||
|
@ -28,7 +30,8 @@ const state = reactive<Partial<Settings>>({
|
|||
showTPS: true,
|
||||
theme: Themes.Nordic,
|
||||
unthrottled: false,
|
||||
alignUnits: false
|
||||
alignUnits: false,
|
||||
showHealthWarning: true
|
||||
});
|
||||
|
||||
watch(
|
||||
|
@ -56,12 +59,15 @@ declare global {
|
|||
export default window.settings = state as Settings;
|
||||
/** A function that erases all player settings, including all saves. */
|
||||
export const hardResetSettings = (window.hardResetSettings = () => {
|
||||
const settings = {
|
||||
// Only partial because of any properties that are only added during the loadSettings event.
|
||||
const settings: Partial<Settings> = {
|
||||
active: "",
|
||||
saves: [],
|
||||
showTPS: true,
|
||||
theme: Themes.Nordic,
|
||||
alignUnits: false
|
||||
unthrottled: false,
|
||||
alignUnits: false,
|
||||
showHealthWarning: true
|
||||
};
|
||||
globalBus.emit("loadSettings", settings);
|
||||
Object.assign(state, settings);
|
||||
|
|
|
@ -6,6 +6,8 @@ import type { Persistent } from "./persistence";
|
|||
export interface Transient {
|
||||
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
|
||||
lastTenTicks: number[];
|
||||
/** A list of bools represnting which of the last few hours had mouse activity. */
|
||||
mouseActivity: boolean[];
|
||||
/** Whether or not a NaN value has been detected and undealt with. */
|
||||
hasNaN: boolean;
|
||||
/** The location within the player save data object of the NaN value. */
|
||||
|
@ -25,6 +27,7 @@ declare global {
|
|||
/** The global transient state object. */
|
||||
export default window.state = shallowReactive<Transient>({
|
||||
lastTenTicks: [],
|
||||
mouseActivity: [false],
|
||||
hasNaN: false,
|
||||
NaNPath: [],
|
||||
errors: reactive([])
|
||||
|
|
|
@ -26,7 +26,7 @@ export function exponentialFormat(num: DecimalSource, precision: number, mantiss
|
|||
}
|
||||
|
||||
export function commaFormat(num: DecimalSource, precision: number): string {
|
||||
if (num === null || num === undefined) {
|
||||
if (num == null) {
|
||||
return "NaN";
|
||||
}
|
||||
num = new Decimal(num);
|
||||
|
@ -36,12 +36,12 @@ export function commaFormat(num: DecimalSource, precision: number): string {
|
|||
const init = num.toStringWithDecimalPlaces(precision);
|
||||
const portions = init.split(".");
|
||||
portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
|
||||
if (portions.length == 1) return portions[0];
|
||||
if (portions.length === 1) return portions[0];
|
||||
return portions[0] + "." + portions[1];
|
||||
}
|
||||
|
||||
export function regularFormat(num: DecimalSource, precision: number): string {
|
||||
if (num === null || num === undefined) {
|
||||
if (num == null) {
|
||||
return "NaN";
|
||||
}
|
||||
num = new Decimal(num);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { LoadablePlayerData } from "components/saves/SavesManager.vue";
|
||||
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
|
||||
import player, { Player, stringifySave } from "game/player";
|
||||
import settings from "game/settings";
|
||||
import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
|
||||
import LZString from "lz-string";
|
||||
import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
|
||||
import { ref } from "vue";
|
||||
import { decodeSave, loadSave, save, setupInitialStore } from "./save";
|
||||
|
||||
|
@ -110,7 +110,7 @@ function syncSaves(
|
|||
}
|
||||
availableSlots.delete(cloudSave.slot);
|
||||
const localSaveId = settings.saves.find(id => id === cloudSave.content.id);
|
||||
if (localSaveId == undefined) {
|
||||
if (localSaveId == null) {
|
||||
settings.saves.push(cloudSave.content.id);
|
||||
save(setupInitialStore(cloudSave.content));
|
||||
} else {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LoadablePlayerData } from "components/saves/SavesManager.vue";
|
||||
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
|
||||
import { fixOldSave, getInitialLayers } from "data/projEntry";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import { globalBus } from "game/events";
|
||||
|
@ -119,7 +119,7 @@ export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
|||
playerObj.time &&
|
||||
playerObj.devSpeed !== 0
|
||||
) {
|
||||
if (playerObj.offlineTime == undefined) playerObj.offlineTime = 0;
|
||||
if (playerObj.offlineTime == null) playerObj.offlineTime = 0;
|
||||
playerObj.offlineTime += Math.min(
|
||||
playerObj.offlineTime + (Date.now() - playerObj.time) / 1000,
|
||||
projInfo.offlineLimit * 3600
|
||||
|
|
|
@ -192,7 +192,7 @@ export function computeOptionalComponent(
|
|||
watchEffect(() => {
|
||||
const currComponent = unwrapRef(component);
|
||||
comp.value =
|
||||
currComponent == "" || currComponent == null
|
||||
currComponent === "" || currComponent == null
|
||||
? null
|
||||
: coerceComponent(currComponent, defaultWrapper);
|
||||
});
|
||||
|
|
100
tests/features/hotkey.test.ts
Normal file
100
tests/features/hotkey.test.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { createHotkey, hotkeys } from "features/hotkey";
|
||||
import { afterEach, describe, expect, onTestFailed, test } from "vitest";
|
||||
import { Ref, ref } from "vue";
|
||||
import "../utils";
|
||||
|
||||
function createSuccessHotkey(key: string, triggered: Ref<boolean>) {
|
||||
hotkeys[key] = createHotkey(() => ({
|
||||
description: "",
|
||||
key: key,
|
||||
onPress: () => (triggered.value = true)
|
||||
}));
|
||||
}
|
||||
|
||||
function createFailHotkey(key: string) {
|
||||
hotkeys[key] = createHotkey(() => ({
|
||||
description: "Fail test",
|
||||
key,
|
||||
onPress: () => expect(true).toBe(false)
|
||||
}));
|
||||
}
|
||||
|
||||
function mockKeypress(key: string, shiftKey = false, ctrlKey = false) {
|
||||
const event = new KeyboardEvent("keydown", { key, shiftKey, ctrlKey });
|
||||
expect(document.dispatchEvent(event)).toBe(true);
|
||||
return event;
|
||||
}
|
||||
|
||||
function testHotkey(pass: string, fail: string, key: string, shiftKey = false, ctrlKey = false) {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(pass, triggered);
|
||||
createFailHotkey(fail);
|
||||
mockKeypress(key, shiftKey, ctrlKey);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
|
||||
describe("Hotkeys fire correctly", () => {
|
||||
afterEach(() => {
|
||||
Object.keys(hotkeys).forEach(key => delete hotkeys[key]);
|
||||
});
|
||||
|
||||
test("Lower case letters", () => testHotkey("a", "A", "a"));
|
||||
|
||||
test.each([["A"], ["shift+a"], ["shift+A"]])("Upper case letters using %s as key", key => {
|
||||
testHotkey(key, "a", "A", true);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[0, ")"],
|
||||
[1, "!"],
|
||||
[2, "@"],
|
||||
[3, "#"],
|
||||
[4, "$"],
|
||||
[5, "%"],
|
||||
[6, "^"],
|
||||
[7, "&"],
|
||||
[8, "*"],
|
||||
[9, "("]
|
||||
])("Handle number %i and it's 'capital', %s", (number, symbol) => {
|
||||
test("Triggering number", () =>
|
||||
testHotkey(number.toString(), symbol, number.toString(), true));
|
||||
test.each([symbol, `shift+${number}`, `shift+${symbol}`])(
|
||||
"Triggering symbol using %s as key",
|
||||
key => testHotkey(key, number.toString(), symbol, true)
|
||||
);
|
||||
});
|
||||
|
||||
test("Ctrl modifier", () => testHotkey("ctrl+a", "a", "a", false, true));
|
||||
|
||||
test.each(["shift+ctrl+a", "ctrl+shift+a", "shift+ctrl+A", "ctrl+shift+A"])(
|
||||
"Shift and Ctrl modifiers using %s as key",
|
||||
key => {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(key, triggered);
|
||||
createFailHotkey("a");
|
||||
createFailHotkey("A");
|
||||
createFailHotkey("shift+A");
|
||||
createFailHotkey("shift+a");
|
||||
createFailHotkey("ctrl+a");
|
||||
createFailHotkey("ctrl+A");
|
||||
mockKeypress("a", true, true);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(["shift+ctrl+1", "ctrl+shift+1", "shift+ctrl+!", "ctrl+shift+!"])(
|
||||
"Shift and Ctrl modifiers using %s as key",
|
||||
key => {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(key, triggered);
|
||||
createFailHotkey("1");
|
||||
createFailHotkey("!");
|
||||
createFailHotkey("shift+1");
|
||||
createFailHotkey("shift+!");
|
||||
createFailHotkey("ctrl+1");
|
||||
createFailHotkey("ctrl+!");
|
||||
mockKeypress("!", true, true);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -155,7 +155,7 @@ describe("Formula Equality Checking", () => {
|
|||
describe("Formula aliases", () => {
|
||||
function testAliases<T extends FormulaFunctions>(
|
||||
aliases: T[],
|
||||
args: Parameters<typeof Formula[T]>
|
||||
args: Parameters<(typeof Formula)[T]>
|
||||
) {
|
||||
describe(aliases[0], () => {
|
||||
let formula: GenericFormula;
|
||||
|
@ -250,7 +250,7 @@ describe("Creating Formulas", () => {
|
|||
|
||||
function checkFormula<T extends FormulaFunctions>(
|
||||
functionName: T,
|
||||
args: Readonly<Parameters<typeof Formula[T]>>
|
||||
args: Readonly<Parameters<(typeof Formula)[T]>>
|
||||
) {
|
||||
let formula: GenericFormula;
|
||||
beforeAll(() => {
|
||||
|
@ -274,7 +274,7 @@ describe("Creating Formulas", () => {
|
|||
// It's a lot of tests, but I'd rather be exhaustive
|
||||
function testFormulaCall<T extends FormulaFunctions>(
|
||||
functionName: T,
|
||||
args: Readonly<Parameters<typeof Formula[T]>>
|
||||
args: Readonly<Parameters<(typeof Formula)[T]>>
|
||||
) {
|
||||
if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) {
|
||||
// These cases in particular take a long time, so skip them
|
||||
|
@ -1275,3 +1275,16 @@ describe("Buy Max", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stringifies", () => {
|
||||
test("Nested formula", () => {
|
||||
const variable = Formula.variable(ref(0));
|
||||
expect(variable.add(5).pow(Formula.constant(10)).stringify()).toBe(
|
||||
"pow(add(x, 5.00), 10.00)"
|
||||
);
|
||||
});
|
||||
test("Indeterminate", () => {
|
||||
expect(Formula.if(10, true, f => f.add(5)).stringify()).toBe("indeterminate");
|
||||
expect(Formula.step(10, 5, f => f.add(5)).stringify()).toBe("indeterminate");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CoercableComponent, JSXFunction } from "features/feature";
|
||||
import Formula, { printFormula } from "game/formulas/formulas";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import {
|
||||
createAdditiveModifier,
|
||||
createExponentialModifier,
|
||||
|
@ -52,7 +52,7 @@ function testModifiers<
|
|||
expect(modifier.invert(operation(10, 5))).compare_tolerance(10));
|
||||
test("getFormula returns the right formula", () => {
|
||||
const value = ref(10);
|
||||
expect(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
||||
expect(modifier.getFormula(Formula.variable(value)).stringify()).toBe(
|
||||
`${operation.name}(x, 5.00)`
|
||||
);
|
||||
});
|
||||
|
@ -156,7 +156,7 @@ describe("Sequential Modifiers", () => {
|
|||
expect(modifier.invert(Decimal.add(10, 5).times(5).pow(5))).compare_tolerance(10));
|
||||
test("getFormula returns the right formula", () => {
|
||||
const value = ref(10);
|
||||
expect(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
||||
expect(modifier.getFormula(Formula.variable(value)).stringify()).toBe(
|
||||
`pow(mul(add(x, 5.00), 5.00), 5.00)`
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue