Merge branch 'main' into fix/hotkey-description
All checks were successful
Run Tests / test (pull_request) Successful in 3m13s

This commit is contained in:
thepaperpilot 2024-10-17 16:17:45 +00:00
commit b8bdeb2380
24 changed files with 186 additions and 37 deletions

View file

@ -6,6 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.6.2] - 2024-04-01
### Added
- Export save button in error boundaries
- isRendered utility function
- Automatic galaxy.click cloud saves support
- Support for null and undefined in persistent refs
### Changes
- round, floor, ceil, trunc, and add now invert as no-ops
- "The Paper Pilot Community" renamed to "Profectus & Friends"
- Updated CI etc. to work with Forgejo
- Improved modifier typing
- Rename `printFormula` to `Formula.stringify`
### Fixed
- Hotkeys not working correctly with most combinations of modifiers
- Reset button using `currentAt` when not gaining
- Formulas not using modifiers that are disabled initially
- branchedResetPropagation logic being incorrect
- Fixed default elementsd in the main layer not updating Context when being added or removed
- Board links props not working in camelCase
- Board links absorbing pointer events
- Thrown errors not appearing in console
- Disabled elements would eat mouse events
- Fixed cost requirement without formula counting as being able to afford infinite purchases rather than just one
- Pinnable tooltips causing innocuous console error
- Bars with direction as "Left" wouldn't appear correctly
### Documentation
- Clarified expected progress values for board nodes
- Added CONTRIBUTING.md and enforce eslint on all PRs
### Tests
- Update formula test cases
- Tree reset propagation
Contributors: thepaperpilot, escapee, nif
## [0.6.1] - 2023-05-17 ## [0.6.1] - 2023-05-17
### Added ### Added
- Error boundaries around each layer, and errors now display on the page when in development - Error boundaries around each layer, and errors now display on the page when in development

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "profectus", "name": "profectus",
"version": "0.6.1", "version": "0.6.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "profectus", "name": "profectus",
"version": "0.6.1", "version": "0.6.2",
"dependencies": { "dependencies": {
"@fontsource/material-icons": "^4.5.4", "@fontsource/material-icons": "^4.5.4",
"@fontsource/roboto-mono": "^4.5.8", "@fontsource/roboto-mono": "^4.5.8",

View file

@ -1,6 +1,6 @@
{ {
"name": "profectus", "name": "profectus",
"version": "0.6.1", "version": "0.6.2",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "vite", "start": "vite",

View file

@ -6,6 +6,7 @@
<Nav v-if="useHeader" /> <Nav v-if="useHeader" />
<Game /> <Game />
<TPS v-if="unref(showTPS)" /> <TPS v-if="unref(showTPS)" />
<AddictionWarning />
<GameOverScreen /> <GameOverScreen />
<NaNScreen /> <NaNScreen />
<CloudSaveResolver /> <CloudSaveResolver />
@ -17,15 +18,16 @@
<script setup lang="tsx"> <script setup lang="tsx">
import "@fontsource/roboto-mono"; import "@fontsource/roboto-mono";
import Error from "components/Error.vue"; import Error from "components/Error.vue";
import 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 { jsx } from "features/feature";
import state from "game/state"; import state from "game/state";
import { coerceComponent, render } from "util/vue"; import { coerceComponent, render } from "util/vue";
import type { CSSProperties } from "vue"; import type { CSSProperties } from "vue";
import { computed, toRef, unref } from "vue"; import { computed, toRef, unref } from "vue";
import Game from "./components/Game.vue"; import Game from "./components/Game.vue";
import GameOverScreen from "./components/GameOverScreen.vue";
import NaNScreen from "./components/NaNScreen.vue";
import Nav from "./components/Nav.vue"; import Nav from "./components/Nav.vue";
import TPS from "./components/TPS.vue"; import TPS from "./components/TPS.vue";
import projInfo from "./data/projInfo.json"; import projInfo from "./data/projInfo.json";

View file

@ -103,9 +103,9 @@ import { Direction } from "util/common";
import { galaxy, syncedSaves } from "util/galaxy"; import { galaxy, syncedSaves } from "util/galaxy";
import type { ComponentPublicInstance } from "vue"; import type { ComponentPublicInstance } from "vue";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import Info from "./Info.vue"; import Info from "./modals/Info.vue";
import Options from "./Options.vue"; import Options from "./modals/Options.vue";
import SavesManager from "./saves/SavesManager.vue"; import SavesManager from "./modals/SavesManager.vue";
const info = ref<ComponentPublicInstance<typeof Info> | null>(null); const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null); const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);

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>
<p>
<h4>Resources:</h4>
<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> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import { stringifySave } from "game/player"; import { stringifySave } from "game/player";
import settings from "game/settings"; import settings from "game/settings";
import LZString from "lz-string"; import LZString from "lz-string";
import { conflictingSaves, galaxy } from "util/galaxy"; import { conflictingSaves, galaxy } from "util/galaxy";
import { getUniqueID, save, setupInitialStore } from "util/save"; import { getUniqueID, save, setupInitialStore } from "util/save";
import { ComponentPublicInstance, computed, ref, unref, watch } from "vue"; import { ComponentPublicInstance, computed, ref, unref, watch } from "vue";
import Modal from "./Modal.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
const isOpen = ref(false); const isOpen = ref(false);

View file

@ -37,14 +37,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import { hasWon } from "data/projEntry"; import { hasWon } from "data/projEntry";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import player from "game/player"; import player from "game/player";
import { formatTime } from "util/bignum"; import { formatTime } from "util/bignum";
import { loadSave, newSave } from "util/save"; import { loadSave, newSave } from "util/save";
import { computed, toRef } from "vue"; import { computed, toRef } from "vue";
import Toggle from "./fields/Toggle.vue"; import Toggle from "../fields/Toggle.vue";
import Modal from "./Modal.vue";
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo; const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo;

View file

@ -60,7 +60,6 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import Modal from "components/Modal.vue";
import type Changelog from "data/Changelog.vue"; import type Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
@ -69,6 +68,7 @@ import { infoComponents } from "game/settings";
import { formatTime } from "util/bignum"; import { formatTime } from "util/bignum";
import { coerceComponent, render } from "util/vue"; import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs, unref } from "vue"; import { computed, ref, toRefs, unref } from "vue";
import Modal from "./Modal.vue";
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo; const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;

View file

@ -42,7 +42,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FeatureNode } from "game/layers"; import type { FeatureNode } from "game/layers";
import { computed, ref, toRefs, unref } from "vue"; import { computed, ref, toRefs, unref } from "vue";
import Context from "./Context.vue"; import Context from "../Context.vue";
const _props = defineProps<{ const _props = defineProps<{
modelValue: boolean; modelValue: boolean;

View file

@ -46,7 +46,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import player from "game/player"; import player from "game/player";
import state from "game/state"; import state from "game/state";
@ -54,8 +53,9 @@ import type { DecimalSource } from "util/bignum";
import Decimal, { format } from "util/bignum"; import Decimal, { format } from "util/bignum";
import type { ComponentPublicInstance } from "vue"; import type { ComponentPublicInstance } from "vue";
import { computed, ref, toRef, watch } from "vue"; import { computed, ref, toRef, watch } from "vue";
import Toggle from "./fields/Toggle.vue"; import Toggle from "../fields/Toggle.vue";
import SavesManager from "./saves/SavesManager.vue"; import Modal from "./Modal.vue";
import SavesManager from "./SavesManager.vue";
const { discordName, discordLink } = projInfo; const { discordName, discordLink } = projInfo;
const autosave = ref(true); const autosave = ref(true);

View file

@ -14,6 +14,7 @@
<Toggle :title="unthrottledTitle" v-model="unthrottled" /> <Toggle :title="unthrottledTitle" v-model="unthrottled" />
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" /> <Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
<Toggle :title="offlineProdTitle" v-model="offlineProd" /> <Toggle :title="offlineProdTitle" v-model="offlineProd" />
<Toggle :title="showHealthWarningTitle" v-model="showHealthWarning" v-if="!projInfo.disableHealthWarning" />
<Toggle :title="autosaveTitle" v-model="autosave" /> <Toggle :title="autosaveTitle" v-model="autosave" />
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton> <FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
</div> </div>
@ -28,20 +29,20 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import { save } from "util/save";
import rawThemes from "data/themes"; import rawThemes from "data/themes";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
import Tooltip from "features/tooltips/Tooltip.vue"; import Tooltip from "features/tooltips/Tooltip.vue";
import player from "game/player"; import player from "game/player";
import settings, { settingFields } from "game/settings"; import settings, { settingFields } from "game/settings";
import { camelToTitle, Direction } from "util/common"; import { camelToTitle, Direction } from "util/common";
import { save } from "util/save";
import { coerceComponent, render } from "util/vue"; import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs } from "vue"; import { computed, ref, toRefs } from "vue";
import Select from "./fields/Select.vue"; import FeedbackButton from "../fields/FeedbackButton.vue";
import Toggle from "./fields/Toggle.vue"; import Select from "../fields/Select.vue";
import FeedbackButton from "./fields/FeedbackButton.vue"; import Toggle from "../fields/Toggle.vue";
import Modal from "./Modal.vue";
const isOpen = ref(false); const isOpen = ref(false);
const currentTab = ref("behaviour"); const currentTab = ref("behaviour");
@ -72,7 +73,7 @@ const settingFieldsComponent = computed(() => {
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>))); return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
}); });
const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings); const { showTPS, theme, unthrottled, alignUnits, showHealthWarning } = toRefs(settings);
const { autosave, offlineProd } = toRefs(player); const { autosave, offlineProd } = toRefs(player);
const isPaused = computed({ const isPaused = computed({
get() { get() {
@ -91,10 +92,16 @@ const unthrottledTitle = jsx(() => (
)); ));
const offlineProdTitle = jsx(() => ( const offlineProdTitle = jsx(() => (
<span class="option-title"> <span class="option-title">
Offline Production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip> Offline production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
<desc>Simulate production that occurs while the game is closed.</desc> <desc>Simulate production that occurs while the game is closed.</desc>
</span> </span>
)); ));
const showHealthWarningTitle = jsx(() => (
<span class="option-title">
Show videogame addiction warning
<desc>Show a helpful warning after playing for a long time about video game addiction and encouraging you to take a break.</desc>
</span>
));
const autosaveTitle = jsx(() => ( const autosaveTitle = jsx(() => (
<span class="option-title"> <span class="option-title">
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip> Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>

View file

@ -60,12 +60,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import type { Player } from "game/player"; import type { Player } from "game/player";
import player, { stringifySave } from "game/player"; import player, { stringifySave } from "game/player";
import settings from "game/settings"; import settings from "game/settings";
import LZString from "lz-string"; import LZString from "lz-string";
import { galaxy, syncedSaves } from "util/galaxy";
import { import {
clearCachedSave, clearCachedSave,
clearCachedSaves, clearCachedSaves,
@ -81,8 +81,8 @@ import { computed, nextTick, ref, watch } from "vue";
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import Select from "../fields/Select.vue"; import Select from "../fields/Select.vue";
import Text from "../fields/Text.vue"; import Text from "../fields/Text.vue";
import Modal from "./Modal.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
import { galaxy, syncedSaves } from "util/galaxy";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown }; export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
@ -130,7 +130,7 @@ watch(saveToImport, importedSave => {
} }
}); });
let bankContext = import.meta.globEager("./../../saves/*.txt", { as: "raw" }); let bankContext = import.meta.globEager("./../../../saves/*.txt", { as: "raw" });
let bank = ref( let bank = ref(
Object.keys(bankContext).reduce((acc: Array<{ label: string; value: string }>, curr) => { Object.keys(bankContext).reduce((acc: Array<{ label: string; value: string }>, curr) => {
acc.push({ acc.push({

View file

@ -19,7 +19,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Modal from "components/Modal.vue"; import Modal from "components/modals/Modal.vue";
import { ref } from "vue"; import { ref } from "vue";
const isOpen = ref(false); const isOpen = ref(false);

View file

@ -88,6 +88,10 @@
"type": "string", "type": "string",
"enum": ["base64", "lz", "plain"], "enum": ["base64", "lz", "plain"],
"description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports." "description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports."
},
"disableHealthWarning": {
"type": "boolean",
"description": "Whether or not to disable the health warning that appears to the player after excessive playtime (activity during 6 of the last 8 hours). If left enabled, the player will still be able to individually turn off the health warning in settings or by clicking \"Do not show again\" in the warning itself."
} }
} }
} }

View file

@ -22,5 +22,6 @@
"maxTickLength": 3600, "maxTickLength": 3600,
"offlineLimit": 1, "offlineLimit": 1,
"enablePausing": true, "enablePausing": true,
"exportEncoding": "base64" "exportEncoding": "base64",
"disableHealthWarning": false
} }

View file

@ -2,6 +2,7 @@ import type { Settings } from "game/settings";
import { createNanoEvents } from "nanoevents"; import { createNanoEvents } from "nanoevents";
import type { App } from "vue"; import type { App } from "vue";
import type { GenericLayer } from "./layers"; import type { GenericLayer } from "./layers";
import state from "./state";
/** All types of events able to be sent or emitted from the global event bus. */ /** All types of events able to be sent or emitted from the global event bus. */
export interface GlobalEvents { export interface GlobalEvents {
@ -59,3 +60,7 @@ if ("fonts" in document) {
// JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document // JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded"); document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
} }
document.onmousemove = function () {
state.mouseActivity[state.mouseActivity.length - 1] = true;
};

View file

@ -107,3 +107,7 @@ export async function startGameLoop() {
intervalID = setInterval(update, 50); intervalID = setInterval(update, 50);
} }
} }
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 { import type {
CoercableComponent, CoercableComponent,
JSXFunction, JSXFunction,

View file

@ -20,6 +20,8 @@ export interface Settings {
unthrottled: boolean; unthrottled: boolean;
/** Whether to align modifiers to the unit. */ /** Whether to align modifiers to the unit. */
alignUnits: boolean; alignUnits: boolean;
/** Whether or not to show a video game health warning after playing excessively. */
showHealthWarning: boolean;
} }
const state = reactive<Partial<Settings>>({ const state = reactive<Partial<Settings>>({
@ -28,7 +30,8 @@ const state = reactive<Partial<Settings>>({
showTPS: true, showTPS: true,
theme: Themes.Nordic, theme: Themes.Nordic,
unthrottled: false, unthrottled: false,
alignUnits: false alignUnits: false,
showHealthWarning: true
}); });
watch( watch(
@ -56,12 +59,15 @@ declare global {
export default window.settings = state as Settings; export default window.settings = state as Settings;
/** A function that erases all player settings, including all saves. */ /** A function that erases all player settings, including all saves. */
export const hardResetSettings = (window.hardResetSettings = () => { export const hardResetSettings = (window.hardResetSettings = () => {
const settings = { // Only partial because of any properties that are only added during the loadSettings event.
const settings: Partial<Settings> = {
active: "", active: "",
saves: [], saves: [],
showTPS: true, showTPS: true,
theme: Themes.Nordic, theme: Themes.Nordic,
alignUnits: false unthrottled: false,
alignUnits: false,
showHealthWarning: true
}; };
globalBus.emit("loadSettings", settings); globalBus.emit("loadSettings", settings);
Object.assign(state, settings); Object.assign(state, settings);

View file

@ -6,6 +6,8 @@ import type { Persistent } from "./persistence";
export interface Transient { export interface Transient {
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */ /** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
lastTenTicks: number[]; lastTenTicks: number[];
/** A list of bools represnting which of the last few hours had mouse activity. */
mouseActivity: boolean[];
/** Whether or not a NaN value has been detected and undealt with. */ /** Whether or not a NaN value has been detected and undealt with. */
hasNaN: boolean; hasNaN: boolean;
/** The location within the player save data object of the NaN value. */ /** The location within the player save data object of the NaN value. */
@ -25,6 +27,7 @@ declare global {
/** The global transient state object. */ /** The global transient state object. */
export default window.state = shallowReactive<Transient>({ export default window.state = shallowReactive<Transient>({
lastTenTicks: [], lastTenTicks: [],
mouseActivity: [false],
hasNaN: false, hasNaN: false,
NaNPath: [], NaNPath: [],
errors: reactive([]) errors: reactive([])

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 player, { Player, stringifySave } from "game/player";
import settings from "game/settings"; import settings from "game/settings";
import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
import LZString from "lz-string"; import LZString from "lz-string";
import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
import { ref } from "vue"; import { ref } from "vue";
import { decodeSave, loadSave, save, setupInitialStore } from "./save"; import { decodeSave, loadSave, save, setupInitialStore } from "./save";

View file

@ -1,4 +1,4 @@
import { LoadablePlayerData } from "components/saves/SavesManager.vue"; import { LoadablePlayerData } from "components/modals/SavesManager.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import type { Player } from "game/player"; import type { Player } from "game/player";