Compare commits

..

36 commits

Author SHA1 Message Date
bd165da264 Update deps some more 2024-10-18 09:11:58 -05:00
c93418bfec Fix merge 2024-10-18 08:50:05 -05:00
88abd53faf Update eslint and prettier 2024-10-18 07:04:33 -05:00
80d7a743bc Update TS 2024-10-18 07:04:29 -05:00
2dab35f7cf Update vue 2024-10-18 07:04:26 -05:00
dc8d0ecc95 Update pixi (while staying at v6 until particle-emitter supports v8) 2024-10-18 07:04:20 -05:00
707aacc383 Update vitest 2024-10-18 07:04:19 -05:00
1e13da1129 Revert "Add link to docs in setupDraggableNode docstring"
This reverts commit 1c7824b550.
2024-10-18 07:02:34 -05:00
643bfccada Add link to docs in setupDraggableNode docstring 2024-10-18 07:02:34 -05:00
f9c59f7636 Revert changes in /data. Check the board-example branch for them instead 2024-10-18 07:02:34 -05:00
99227a2cb2 Move common board node CSS to class 2024-10-18 07:02:34 -05:00
e0da9588d2 Move board to src/game 2024-10-18 07:02:34 -05:00
46d0a9aa2e Add some tests for boards 2024-10-18 07:02:33 -05:00
c6035f9077 Document boards 2024-10-18 07:02:33 -05:00
8745304631 Fix upgrade purchasing on drag 2024-10-18 07:02:33 -05:00
5b33a0fceb Perf optimization 2024-10-18 07:02:33 -05:00
6a17bbc29c Use z-index to avoid changing render order 2024-10-18 07:02:33 -05:00
a75c8d81f8 Add cnodes 2024-10-18 07:02:32 -05:00
c64ac82a25 Add support for rendering VueFeatures in boards 2024-10-18 07:02:32 -05:00
1cbe97251c WIP on rewriting board 2024-10-18 07:02:32 -05:00
6ba25f9abd Removed dynamic imports 2024-10-18 07:02:32 -05:00
673f7790c7 Switch from CJS to ESM 2024-10-18 07:00:56 -05:00
ae45f9bc2f Bump some minor dependencies 2024-10-18 07:00:55 -05:00
8a9e106157 Update vite dependencies 2024-10-18 07:00:46 -05:00
90300ce848 Update fontsource dependencies 2024-10-18 07:00:08 -05:00
2b861c3fcf Fix Links.vue checking startNode twice instead of both nodes
Thanks escapee for reporting the issue!
2024-10-17 16:50:55 +00:00
9debfe6fb4 Unref hotkey descriptions 2024-10-17 16:28:04 +00:00
9f25d7f58f Fix more modal paths 2024-10-17 16:17:32 +00:00
239ae7c94a Update saves bank path 2024-10-17 16:17:32 +00:00
2d28be84a9 Add modal to take a mental health break 2024-10-17 16:17:32 +00:00
c6389317d0 Version Bump 2024-03-29 00:49:58 -05:00
b98f6db1c4 Move printFormula to Formula.stringify and add tests for it
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 52s
Run Tests / test (push) Successful in 2m2s
2024-03-29 00:24:31 -05:00
563eaa7539 Lint 2024-03-29 00:19:57 -05:00
b88fa68874 Fix extends syntax 2024-03-28 23:40:47 -05:00
90d0307cf0 Add hotkey tests, make them pass
Includes updating vitest and supporting hotkeys with both ctrl+shift
2024-03-28 23:40:46 -05:00
dfb14acc6e Allow null and undefined values in persistent refs 2024-03-29 04:39:56 +00:00
40 changed files with 2200 additions and 2630 deletions

View file

@ -34,6 +34,13 @@ module.exports = {
allowNullableObject: true,
allowNullableBoolean: true
}
],
"eqeqeq": [
"error",
"always",
{
"null": "never"
}
]
},
globals: {

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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."
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import Modal from "components/Modal.vue";
import Modal from "components/modals/Modal.vue";
import type {
CoercableComponent,
JSXFunction,

View file

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

View file

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

View file

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

View file

@ -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([])

View file

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

View file

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

View file

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

View file

@ -192,7 +192,7 @@ export function computeOptionalComponent(
watchEffect(() => {
const currComponent = unwrapRef(component);
comp.value =
currComponent == "" || currComponent == null
currComponent === "" || currComponent == null
? null
: coerceComponent(currComponent, defaultWrapper);
});

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

View file

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

View file

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