Merge remote-tracking branch 'template/main'
This commit is contained in:
commit
38e495fe01
40 changed files with 1038 additions and 11867 deletions
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"CurrentProjectSetting": null
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"ExpandedNodes": [
|
||||
"",
|
||||
"\\src",
|
||||
"\\src\\typings",
|
||||
"\\src\\util"
|
||||
],
|
||||
"SelectedNode": "\\src\\App.vue",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
BIN
.vs/slnx.sqlite
BIN
.vs/slnx.sqlite
Binary file not shown.
23
.vscode/launch.json
vendored
Normal file
23
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Debug Current Test File",
|
||||
"autoAttachChildProcesses": true,
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
||||
"args": [
|
||||
"run",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"smartStep": true,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"vitest.commandLine": "npx vitest"
|
||||
}
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.0] - 2022-06-27
|
||||
### Added
|
||||
- Projects now cache for offline play, and show notification when an update is available
|
||||
- Projects can now be "installed" as a Progressive Web App
|
||||
- Conversions can now be given a custom spend function, which defaults to setting the base resource amount to 0
|
||||
- Components for displaying Floor and Square Root symbols
|
||||
### Changed
|
||||
- **BREAKING** Several projInfo properties now default to empty strings, to prevent things like reusing project IDs
|
||||
- **BREAKING** Replaced vue-cli-service with vite (should not break most projects)
|
||||
- Updated dependencies
|
||||
- Made all type-only imports explicit
|
||||
- setupPassiveGeneration now works properly on independent conversions
|
||||
- setupPassiveGeneration now takes an option cap it can't go over
|
||||
- Improved typing for PlayerData.layers
|
||||
- Options Functions have an improved `this` type - it now includes the options themselves
|
||||
- Removed v-show being used in data/common.tsx
|
||||
### Tests
|
||||
- Implement Jest, and running tests automatically on push
|
||||
- Tests written for utils/common.ts
|
||||
|
||||
## [0.4.2] - 2022-05-23
|
||||
### Added
|
||||
- costModifier to conversions
|
||||
|
|
|
@ -13,7 +13,7 @@ npm install
|
|||
|
||||
### Hosts dev server and hot-reloads modules as they're changed
|
||||
```
|
||||
npm starts
|
||||
npm start
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
@ -30,3 +30,8 @@ npm run preview
|
|||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Runs the tests using vite-jest
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
module.exports = {
|
||||
preset: "vite-jest",
|
||||
testEnvironment: "jest-environment-jsdom",
|
||||
moduleNameMapper: {
|
||||
"^./../([^.].*)$": "$1"
|
||||
}
|
||||
};
|
12178
package-lock.json
generated
12178
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"name": "profectus",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vite-jest --no-cache"
|
||||
"test": "vitest run",
|
||||
"testw": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi/particle-emitter": "^5.0.4",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lz-string": "^1.4.4",
|
||||
"nanoevents": "^6.0.2",
|
||||
"pixi.js": "^6.3.0",
|
||||
|
@ -32,25 +32,16 @@
|
|||
"devDependencies": {
|
||||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@types/lz-string": "^1.3.34",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"babel-jest": "^28.1.1",
|
||||
"eslint": "^8.6.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-environment-jsdom": "^27.5.1",
|
||||
"jsdom": "^20.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "~4.5.5",
|
||||
"vite-jest": "^0.1.4",
|
||||
"typescript": "^4.7.4",
|
||||
"vitest": "^0.17.1",
|
||||
"vue-tsc": "^0.38.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
],
|
||||
"engines": {
|
||||
"node": "16.x"
|
||||
}
|
||||
|
|
|
@ -9,30 +9,29 @@ import { computed, inject, onUnmounted, ref, toRefs, unref, watch } from "vue";
|
|||
const _props = defineProps<{ id: string }>();
|
||||
const props = toRefs(_props);
|
||||
|
||||
const register = inject(RegisterNodeInjectionKey);
|
||||
const unregister = inject(UnregisterNodeInjectionKey);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const register = inject(RegisterNodeInjectionKey, () => {});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const unregister = inject(UnregisterNodeInjectionKey, () => {});
|
||||
|
||||
const node = ref<HTMLElement | null>(null);
|
||||
const parentNode = computed(() => node.value && node.value.parentElement);
|
||||
|
||||
if (register && unregister) {
|
||||
watch([parentNode, props.id], ([newNode, newID], [prevNode, prevID]) => {
|
||||
if (prevNode) {
|
||||
unregister(unref(prevID));
|
||||
}
|
||||
if (newNode) {
|
||||
register(newID, newNode);
|
||||
}
|
||||
});
|
||||
watch([parentNode, props.id], ([newNode, newID], [prevNode, prevID]) => {
|
||||
if (prevNode) {
|
||||
unregister(unref(prevID));
|
||||
}
|
||||
if (newNode) {
|
||||
register(newID, newNode);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => unregister(unref(props.id)));
|
||||
}
|
||||
onUnmounted(() => unregister(unref(props.id)));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.node {
|
||||
position: absolute;
|
||||
z-index: -10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
|
42
src/components/Notif.vue
Normal file
42
src/components/Notif.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="notif">!</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped>
|
||||
.notif {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
color: var(--accent3);
|
||||
font-size: x-large;
|
||||
animation: 1s linear infinite bounce;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--locked);
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
animation-timing-function: cubic-bezier(0.1361, 0.2514, 0.2175, 0.8786);
|
||||
transform: translate(0, 0px) scaleY(1);
|
||||
}
|
||||
37% {
|
||||
animation-timing-function: cubic-bezier(0.7674, 0.1844, 0.8382, 0.7157);
|
||||
transform: translate(0, -20px) scaleY(1);
|
||||
}
|
||||
72% {
|
||||
animation-timing-function: cubic-bezier(0.1118, 0.2149, 0.2172, 0.941);
|
||||
transform: translate(0, 0px) scaleY(1);
|
||||
}
|
||||
87% {
|
||||
animation-timing-function: cubic-bezier(0.7494, 0.2259, 0.8209, 0.6963);
|
||||
transform: translate(0, 10px) scaleY(0.602);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0px) scaleY(1);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<span style="white-space: nowrap;">
|
||||
<span style="font-size: larger; font-family: initial">√</span><span style="text-decoration: overline"> <slot /> </span>
|
||||
<span style="white-space: nowrap">
|
||||
<span style="font-size: larger; font-family: initial">√</span
|
||||
><span style="text-decoration: overline"><slot /></span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -25,18 +25,44 @@ import type { Ref } from "vue";
|
|||
import { computed, unref } from "vue";
|
||||
import "./common.css";
|
||||
|
||||
/** An object that configures a {@link ResetButton} */
|
||||
export interface ResetButtonOptions extends ClickableOptions {
|
||||
/** The conversion the button uses to calculate how much resources will be gained on click */
|
||||
conversion: GenericConversion;
|
||||
/** The tree this reset button is apart of */
|
||||
tree: GenericTree;
|
||||
/** The specific tree node associated with this reset button */
|
||||
treeNode: GenericTreeNode;
|
||||
/**
|
||||
* Text to display on low conversion amounts, describing what "resetting" is in this context.
|
||||
* Defaults to "Reset for ".
|
||||
*/
|
||||
resetDescription?: Computable<string>;
|
||||
/** Whether or not to show how much currency would be required to make the gain amount increase. */
|
||||
showNextAt?: Computable<boolean>;
|
||||
/**
|
||||
* The content to display on the button.
|
||||
* By default, this includes the reset description, and amount of currency to be gained.
|
||||
*/
|
||||
display?: Computable<CoercableComponent>;
|
||||
/**
|
||||
* Whether or not this button can currently be clicked.
|
||||
* Defaults to checking the current gain amount is greater than {@link minimumGain}
|
||||
*/
|
||||
canClick?: Computable<boolean>;
|
||||
/**
|
||||
* When {@link canClick} is left to its default, minimumGain is used to only enable the reset button when a sufficient amount of currency to gain is available.
|
||||
*/
|
||||
minimumGain?: Computable<DecimalSource>;
|
||||
/** A persistent ref to track how much time has passed since the last time this tree node was reset. */
|
||||
resetTime?: Persistent<DecimalSource>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button that is used to control a conversion.
|
||||
* It will show how much can be converted currently, and can show when that amount will go up, as well as handle only being clickable when a sufficient amount of currency can be gained.
|
||||
* Assumes this button is associated with a specific node on a tree, and triggers that tree's reset propagation.
|
||||
*/
|
||||
export type ResetButton<T extends ResetButtonOptions> = Replace<
|
||||
Clickable<T>,
|
||||
{
|
||||
|
@ -49,6 +75,7 @@ export type ResetButton<T extends ResetButtonOptions> = Replace<
|
|||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link ResetButton} object. */
|
||||
export type GenericResetButton = Replace<
|
||||
GenericClickable & ResetButton<ResetButtonOptions>,
|
||||
{
|
||||
|
@ -60,6 +87,10 @@ export type GenericResetButton = Replace<
|
|||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a reset button with the given options.
|
||||
* @param optionsFunc A function that returns the options object for this reset button.
|
||||
*/
|
||||
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ResetButton<T> {
|
||||
|
@ -136,12 +167,24 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
}) as unknown as ResetButton<T>;
|
||||
}
|
||||
|
||||
/** An object that configures a {@link LayerTreeNode} */
|
||||
export interface LayerTreeNodeOptions extends TreeNodeOptions {
|
||||
/** The ID of the layer this tree node is associated with */
|
||||
layerID: string;
|
||||
/** The color to display this tree node as */
|
||||
color: Computable<string>; // marking as required
|
||||
/**
|
||||
* The content to display in the tree node.
|
||||
* Defaults to the layer's ID
|
||||
*/
|
||||
display?: Computable<CoercableComponent>;
|
||||
/** Whether or not to append the layer to the tabs list.
|
||||
* If set to false, then the tree node will instead always remove all tabs to its right and then add the layer tab.
|
||||
* Defaults to true.
|
||||
*/
|
||||
append?: Computable<boolean>;
|
||||
}
|
||||
/** A tree node that is associated with a given layer, and which opens the layer when clicked. */
|
||||
export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
|
||||
TreeNode<T>,
|
||||
{
|
||||
|
@ -149,6 +192,7 @@ export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
|
|||
append: GetComputableType<T["append"]>;
|
||||
}
|
||||
>;
|
||||
/** A type that matches any valid {@link LayerTreeNode} object. */
|
||||
export type GenericLayerTreeNode = Replace<
|
||||
LayerTreeNode<LayerTreeNodeOptions>,
|
||||
{
|
||||
|
@ -157,6 +201,10 @@ export type GenericLayerTreeNode = Replace<
|
|||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tree node that's associated with a specific layer, with the given options.
|
||||
* @param optionsFunc A function that returns the options object for this tree node.
|
||||
*/
|
||||
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): LayerTreeNode<T> {
|
||||
|
@ -184,6 +232,18 @@ export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
|
|||
}) as unknown as LayerTreeNode<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of modifier "sections", and creates a JSXFunction that can render all those sections, and allow each section to be collapsed.
|
||||
* Also returns a list of persistent refs that are used to control which sections are currently collapsed.
|
||||
* @param sections An array of options objects for each section to display.
|
||||
* @param sections.title The header for this modifier.
|
||||
* @param sections.subtitle A subtitle for this modifier, e.g. to explain the context for the modifier.
|
||||
* @param sections.modifier The modifier to be displaying in this section.
|
||||
* @param sections.base The base value being modified.
|
||||
* @param sections.unit The unit of measurement for the base.
|
||||
* @param sections.baseText The label to call the base amount.
|
||||
* @param sections.visible Whether or not this section should be currently visible to the player.
|
||||
*/
|
||||
export function createCollapsibleModifierSections(
|
||||
sections: {
|
||||
title: string;
|
||||
|
@ -248,3 +308,12 @@ export function createCollapsibleModifierSections(
|
|||
});
|
||||
return [jsxFunc, collapsed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTML string for a span that writes some given text in a given color.
|
||||
* @param textToColor The content to change the color of
|
||||
* @param color The color to change the content to look like. Defaults to the current theme's accent 2 variable.
|
||||
*/
|
||||
export function colorText(textToColor: string, color = "var(--accent2)"): string {
|
||||
return `<span style="color: ${color}">${textToColor}</span>`;
|
||||
}
|
||||
|
|
|
@ -99,15 +99,27 @@ export const main = createLayer("main", () => {
|
|||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Given a player save data object being loaded, return a list of layers that should currently be enabled.
|
||||
* If your project does not use dynamic layers, this should just return all layers.
|
||||
*/
|
||||
export const getInitialLayers = (
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
player: Partial<PlayerData>
|
||||
): Array<GenericLayer> => [main, f, c, a];
|
||||
|
||||
/**
|
||||
* A computed ref whose value is true whenever the game is over.
|
||||
*/
|
||||
export const hasWon = computed(() => {
|
||||
return Decimal.gt(main.points.value, 25);
|
||||
});
|
||||
|
||||
/**
|
||||
* Given a player save data object being loaded with a different version, update the save data object to match the structure of the current version.
|
||||
* @param oldVersion The version of the save being loaded in
|
||||
* @param player The save data being loaded in
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export function fixOldSave(
|
||||
oldVersion: string | undefined,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/** A object of all CSS variables determined by the current theme. */
|
||||
export interface ThemeVars {
|
||||
"--foreground": string;
|
||||
"--background": string;
|
||||
|
@ -19,14 +20,20 @@ export interface ThemeVars {
|
|||
"--feature-margin": string;
|
||||
}
|
||||
|
||||
/** An object representing a theme the player can use to change the look of the game. */
|
||||
export interface Theme {
|
||||
/** The values of the theme's CSS variables. */
|
||||
variables: ThemeVars;
|
||||
/** Whether or not tabs should "float" in the center of their container. */
|
||||
floatingTabs: boolean;
|
||||
/** Whether or not adjacent features should merge together - removing the margin between them, and only applying the border radius to the first and last elements in the row or column. */
|
||||
mergeAdjacent: boolean;
|
||||
/** Whether or not to show a pin icon on pinned tooltips. */
|
||||
showPin: boolean;
|
||||
}
|
||||
|
||||
declare module "@vue/runtime-dom" {
|
||||
/** Make CSS properties accept any CSS variables usually controlled by a theme. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface CSSProperties extends Partial<ThemeVars> {}
|
||||
|
||||
|
@ -62,6 +69,7 @@ const defaultTheme: Theme = {
|
|||
showPin: true
|
||||
};
|
||||
|
||||
/** An enum of all available themes and their internal IDs. The keys are their display names. */
|
||||
export enum Themes {
|
||||
Classic = "classic",
|
||||
Paper = "paper",
|
||||
|
@ -69,6 +77,7 @@ export enum Themes {
|
|||
Aqua = "aqua"
|
||||
}
|
||||
|
||||
/** A dictionary of all available themes. */
|
||||
export default {
|
||||
classic: defaultTheme,
|
||||
paper: {
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
import "game/notifications";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import settings from "game/settings";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
|
@ -99,6 +101,7 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
if (achievement.shouldEarn) {
|
||||
const genericAchievement = achievement as GenericAchievement;
|
||||
watchEffect(() => {
|
||||
if (settings.active !== player.id) return;
|
||||
if (
|
||||
!genericAchievement.earned.value &&
|
||||
unref(genericAchievement.visibility) === Visibility.Visible &&
|
||||
|
|
|
@ -21,12 +21,9 @@
|
|||
unref(borderStyle) ?? {}
|
||||
]"
|
||||
>
|
||||
<component
|
||||
v-if="component"
|
||||
class="overlayText"
|
||||
:style="unref(textStyle)"
|
||||
:is="component"
|
||||
/>
|
||||
<span v-if="component" class="overlayText" :style="unref(textStyle)">
|
||||
<component :is="component" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="border"
|
||||
|
|
|
@ -24,8 +24,9 @@ export type BuyableDisplay =
|
|||
| CoercableComponent
|
||||
| {
|
||||
title?: CoercableComponent;
|
||||
description: CoercableComponent;
|
||||
description?: CoercableComponent;
|
||||
effectDisplay?: CoercableComponent;
|
||||
showAmount?: boolean;
|
||||
};
|
||||
|
||||
export interface BuyableOptions {
|
||||
|
@ -168,17 +169,8 @@ export function createBuyable<T extends BuyableOptions>(
|
|||
if (currDisplay != null && buyable.cost != null && buyable.resource != null) {
|
||||
const genericBuyable = buyable as GenericBuyable;
|
||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description);
|
||||
const Description = coerceComponent(currDisplay.description || "");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||
const amountDisplay =
|
||||
unref(genericBuyable.purchaseLimit) === Decimal.dInf ? (
|
||||
<>Amount: {formatWhole(genericBuyable.amount.value)}</>
|
||||
) : (
|
||||
<>
|
||||
Amount: {formatWhole(genericBuyable.amount.value)} /{" "}
|
||||
{formatWhole(unref(genericBuyable.purchaseLimit))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<span>
|
||||
|
@ -187,11 +179,20 @@ export function createBuyable<T extends BuyableOptions>(
|
|||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
<div>
|
||||
<br />
|
||||
{amountDisplay}
|
||||
</div>
|
||||
{currDisplay.description ? <Description /> : null}
|
||||
{currDisplay.showAmount === false ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{unref(genericBuyable.purchaseLimit) === Decimal.dInf ? (
|
||||
<>Amount: {formatWhole(genericBuyable.amount.value)}</>
|
||||
) : (
|
||||
<>
|
||||
Amount: {formatWhole(genericBuyable.amount.value)} /{" "}
|
||||
{formatWhole(unref(genericBuyable.purchaseLimit))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currDisplay.effectDisplay ? (
|
||||
<div>
|
||||
<br />
|
||||
|
|
|
@ -12,12 +12,10 @@ import { createLazyProxy } from "util/proxies";
|
|||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
|
||||
/**
|
||||
* An object that configures a {@link conversion}.
|
||||
*/
|
||||
/** An object that configures a {@link Conversion}. */
|
||||
export interface ConversionOptions {
|
||||
/**
|
||||
* The scaling function that is used to determine the rate of conversion from one {@link resource} to the other.
|
||||
* The scaling function that is used to determine the rate of conversion from one {@link features/resources/resource.Resource} to the other.
|
||||
*/
|
||||
scaling: ScalingFunction;
|
||||
/**
|
||||
|
@ -43,11 +41,11 @@ export interface ConversionOptions {
|
|||
*/
|
||||
nextAt?: Computable<DecimalSource>;
|
||||
/**
|
||||
* The input {@link resource} for this conversion.
|
||||
* The input {@link features/resources/resource.Resource} for this conversion.
|
||||
*/
|
||||
baseResource: Resource;
|
||||
/**
|
||||
* The output {@link resource} for this conversion. i.e. the resource being generated.
|
||||
* The output {@link features/resources/resource.Resource} for this conversion. i.e. the resource being generated.
|
||||
*/
|
||||
gainResource: Resource;
|
||||
/**
|
||||
|
@ -78,7 +76,7 @@ export interface ConversionOptions {
|
|||
/**
|
||||
* An additional modifier that will be applied to the gain amounts.
|
||||
* Must be reversible in order to correctly calculate {@link nextAt}.
|
||||
* @see {@link createSequentialModifier} if you want to apply multiple modifiers.
|
||||
* @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers.
|
||||
*/
|
||||
gainModifier?: WithRequired<Modifier, "revert">;
|
||||
/**
|
||||
|
@ -86,7 +84,7 @@ export interface ConversionOptions {
|
|||
* That is to say, this modifier will be applied to the amount of baseResource before going into the scaling function.
|
||||
* A cost modifier of x0.5 would give gain amounts equal to the player having half the baseResource they actually have.
|
||||
* Must be reversible in order to correctly calculate {@link nextAt}.
|
||||
* @see {@link createSequentialModifier} if you want to apply multiple modifiers.
|
||||
* @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers.
|
||||
*/
|
||||
costModifier?: WithRequired<Modifier, "revert">;
|
||||
}
|
||||
|
@ -101,9 +99,7 @@ export interface BaseConversion {
|
|||
convert: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that converts one {@link resource} into another at a given rate.
|
||||
*/
|
||||
/** An object that converts one {@link features/resources/resource.Resource} into another at a given rate. */
|
||||
export type Conversion<T extends ConversionOptions> = Replace<
|
||||
T & BaseConversion,
|
||||
{
|
||||
|
@ -117,9 +113,7 @@ export type Conversion<T extends ConversionOptions> = Replace<
|
|||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* A type that matches any {@link conversion} object.
|
||||
*/
|
||||
/** A type that matches any valid {@link Conversion} object. */
|
||||
export type GenericConversion = Replace<
|
||||
Conversion<ConversionOptions>,
|
||||
{
|
||||
|
|
|
@ -5,12 +5,12 @@ import { isRef } from "vue";
|
|||
|
||||
/**
|
||||
* A symbol to use as a key for a vue component a feature can be rendered with
|
||||
* @see {@link VueFeature}
|
||||
* @see {@link util/vue.VueFeature}
|
||||
*/
|
||||
export const Component = Symbol("Component");
|
||||
/**
|
||||
* A symbol to use as a key for a prop gathering function that a feature can use to send to its component
|
||||
* @see {@link VueFeature}
|
||||
* @see {@link util/vue.VueFeature}
|
||||
*/
|
||||
export const GatherProps = Symbol("GatherProps");
|
||||
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import type { Link } from "features/links/links";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { BoundsInjectionKey, NodesInjectionKey } from "game/layers";
|
||||
import { computed, inject, ref, toRef, watch } from "vue";
|
||||
import { computed, inject, onMounted, ref, toRef, watch } from "vue";
|
||||
import LinkVue from "./Link.vue";
|
||||
|
||||
const _props = defineProps<{ links?: Link[] }>();
|
||||
|
@ -23,15 +24,14 @@ const links = toRef(_props, "links");
|
|||
|
||||
const resizeListener = ref<Element | null>(null);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const nodes = inject(NodesInjectionKey)!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const outerBoundingRect = inject(BoundsInjectionKey)!;
|
||||
const boundingRect = ref<DOMRect | undefined>(undefined);
|
||||
const nodes = inject(NodesInjectionKey, ref<Record<string, FeatureNode | undefined>>({}));
|
||||
const outerBoundingRect = inject(BoundsInjectionKey, ref<DOMRect | undefined>(undefined));
|
||||
const boundingRect = ref<DOMRect | undefined>(resizeListener.value?.getBoundingClientRect());
|
||||
watch(
|
||||
[outerBoundingRect],
|
||||
outerBoundingRect,
|
||||
() => (boundingRect.value = resizeListener.value?.getBoundingClientRect())
|
||||
);
|
||||
onMounted(() => (boundingRect.value = resizeListener.value?.getBoundingClientRect()));
|
||||
|
||||
const validLinks = computed(() => {
|
||||
const n = nodes.value;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { globalBus } from "game/events";
|
|||
import "game/notifications";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import settings, { registerSettingField } from "game/settings";
|
||||
import { camelToTitle } from "util/common";
|
||||
import type {
|
||||
|
@ -132,6 +133,7 @@ export function createMilestone<T extends MilestoneOptions>(
|
|||
if (milestone.shouldEarn) {
|
||||
const genericMilestone = milestone as GenericMilestone;
|
||||
watchEffect(() => {
|
||||
if (settings.active !== player.id) return;
|
||||
if (
|
||||
!genericMilestone.earned.value &&
|
||||
unref(genericMilestone.visibility) === Visibility.Visible &&
|
||||
|
|
|
@ -46,7 +46,7 @@ export default defineComponent({
|
|||
backgroundAlpha: 0
|
||||
});
|
||||
resizeListener.value?.appendChild(app.value.view);
|
||||
props.onInit(app.value as Application);
|
||||
props.onInit?.(app.value as Application);
|
||||
}
|
||||
updateBounds();
|
||||
if (props.onHotReload) {
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
<template>
|
||||
<div>
|
||||
<span v-if="showPrefix">You have </span>
|
||||
<ResourceVue :resource="resource" :color="color || 'white'" />
|
||||
{{ resource.displayName
|
||||
}}<!-- remove whitespace -->
|
||||
<span v-if="effectComponent">, <component :is="effectComponent" /></span>
|
||||
<br /><br />
|
||||
</div>
|
||||
<Sticky>
|
||||
<div
|
||||
class="main-display-container"
|
||||
:style="{ height: `${(effectRef?.$el.clientHeight ?? 0) + 50}px` }"
|
||||
>
|
||||
<div class="main-display">
|
||||
<span v-if="showPrefix">You have </span>
|
||||
<ResourceVue :resource="resource" :color="color || 'white'" />
|
||||
{{ resource.displayName
|
||||
}}<!-- remove whitespace -->
|
||||
<span v-if="effectComponent"
|
||||
>, <component :is="effectComponent" ref="effectRef"
|
||||
/></span>
|
||||
</div>
|
||||
</div>
|
||||
</Sticky>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Sticky from "components/layout/Sticky.vue";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import ResourceVue from "features/resources/Resource.vue";
|
||||
import Decimal from "util/bignum";
|
||||
import { computeOptionalComponent } from "util/vue";
|
||||
import type { Ref, StyleValue } from "vue";
|
||||
import { ComponentPublicInstance, ref, Ref, StyleValue } from "vue";
|
||||
import { computed, toRefs } from "vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
|
@ -27,6 +36,8 @@ const _props = defineProps<{
|
|||
}>();
|
||||
const props = toRefs(_props);
|
||||
|
||||
const effectRef = ref<ComponentPublicInstance | null>(null);
|
||||
|
||||
const effectComponent = computeOptionalComponent(
|
||||
props.effectDisplay as Ref<CoercableComponent | undefined>
|
||||
);
|
||||
|
@ -35,3 +46,11 @@ const showPrefix = computed(() => {
|
|||
return Decimal.lt(props.resource.value, "1e1000");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main-display-container {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,11 @@
|
|||
tabStyle ?? []
|
||||
]"
|
||||
>
|
||||
<Sticky class="tab-buttons-container">
|
||||
<Sticky
|
||||
class="tab-buttons-container"
|
||||
:class="unref(buttonContainerClasses)"
|
||||
:style="unref(buttonContainerStyle)"
|
||||
>
|
||||
<div class="tab-buttons" :class="{ floating }">
|
||||
<TabButton
|
||||
v-for="(button, id) in unref(tabs)"
|
||||
|
@ -61,7 +65,9 @@ export default defineComponent({
|
|||
required: true
|
||||
},
|
||||
style: processedPropType<StyleValue>(String, Object, Array),
|
||||
classes: processedPropType<Record<string, boolean>>(Object)
|
||||
classes: processedPropType<Record<string, boolean>>(Object),
|
||||
buttonContainerStyle: processedPropType<StyleValue>(String, Object, Array),
|
||||
buttonContainerClasses: processedPropType<Record<string, boolean>>(Object)
|
||||
},
|
||||
components: {
|
||||
Sticky,
|
||||
|
|
|
@ -56,6 +56,8 @@ export interface TabFamilyOptions {
|
|||
visibility?: Computable<Visibility>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
buttonContainerClasses?: Computable<Record<string, boolean>>;
|
||||
buttonContainerStyle?: Computable<StyleValue>;
|
||||
}
|
||||
|
||||
export interface BaseTabFamily {
|
||||
|
@ -140,10 +142,30 @@ export function createTabFamily<T extends TabFamilyOptions>(
|
|||
setDefault(tabFamily, "visibility", Visibility.Visible);
|
||||
processComputable(tabFamily as T, "classes");
|
||||
processComputable(tabFamily as T, "style");
|
||||
processComputable(tabFamily as T, "buttonContainerClasses");
|
||||
processComputable(tabFamily as T, "buttonContainerStyle");
|
||||
|
||||
tabFamily[GatherProps] = function (this: GenericTabFamily) {
|
||||
const { visibility, activeTab, selected, tabs, style, classes } = this;
|
||||
return { visibility, activeTab, selected, tabs, style: unref(style), classes };
|
||||
const {
|
||||
visibility,
|
||||
activeTab,
|
||||
selected,
|
||||
tabs,
|
||||
style,
|
||||
classes,
|
||||
buttonContainerClasses,
|
||||
buttonContainerStyle
|
||||
} = this;
|
||||
return {
|
||||
visibility,
|
||||
activeTab,
|
||||
selected,
|
||||
tabs,
|
||||
style: unref(style),
|
||||
classes,
|
||||
buttonContainerClasses,
|
||||
buttonContainerStyle
|
||||
};
|
||||
};
|
||||
|
||||
// This is necessary because board.types is different from T and TabFamily
|
||||
|
|
|
@ -34,10 +34,10 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="tsx">
|
||||
import themes from "data/themes";
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { jsx } from "features/feature";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { jsx, StyleValue } from "features/feature";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import settings from "game/settings";
|
||||
import { Direction } from "util/common";
|
||||
|
@ -46,15 +46,15 @@ import {
|
|||
coerceComponent,
|
||||
computeOptionalComponent,
|
||||
processedPropType,
|
||||
render,
|
||||
renderJSX,
|
||||
unwrapRef
|
||||
} from "util/vue";
|
||||
import type { Component, PropType } from "vue";
|
||||
import { computed, defineComponent, ref, shallowRef, toRefs, unref, watchEffect } from "vue";
|
||||
import { computed, defineComponent, ref, shallowRef, toRefs, unref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
element: processedPropType<VueFeature>(Object),
|
||||
element: Object as PropType<VueFeature>,
|
||||
display: {
|
||||
type: processedPropType<CoercableComponent>(Object, String, Function),
|
||||
required: true
|
||||
|
@ -73,14 +73,14 @@ export default defineComponent({
|
|||
const isShown = computed(() => (unwrapRef(pinned) || isHovered.value) && comp.value);
|
||||
const comp = computeOptionalComponent(display);
|
||||
|
||||
const elementComp = shallowRef<Component | "" | null>(null);
|
||||
watchEffect(() => {
|
||||
const currComponent = unwrapRef(element);
|
||||
elementComp.value =
|
||||
currComponent == null
|
||||
? null
|
||||
: coerceComponent(jsx(() => render(currComponent) as JSX.Element));
|
||||
});
|
||||
const elementComp = shallowRef<Component | "" | null>(
|
||||
coerceComponent(
|
||||
jsx(() => {
|
||||
const currComponent = unwrapRef(element);
|
||||
return currComponent == null ? "" : renderJSX(currComponent);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
function togglePinned(e: MouseEvent) {
|
||||
const isPinned = pinned as unknown as Persistent<boolean> | undefined; // Vue typing :/
|
||||
|
|
|
@ -70,28 +70,28 @@ export function addTooltip<T extends TooltipOptions>(
|
|||
processComputable(options as T, "xoffset");
|
||||
processComputable(options as T, "yoffset");
|
||||
|
||||
nextTick(() => {
|
||||
if (options.pinnable) {
|
||||
if ("pinned" in element) {
|
||||
console.error(
|
||||
"Cannot add pinnable tooltip to element that already has a property called 'pinned'"
|
||||
);
|
||||
options.pinnable = false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(element as any).pinned = options.pinned = persistent<boolean>(false);
|
||||
}
|
||||
if (options.pinnable) {
|
||||
if ("pinned" in element) {
|
||||
console.error(
|
||||
"Cannot add pinnable tooltip to element that already has a property called 'pinned'"
|
||||
);
|
||||
options.pinnable = false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(element as any).pinned = options.pinned = persistent<boolean>(false);
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const elementComponent = element[Component];
|
||||
element[Component] = TooltipComponent;
|
||||
const elementGratherProps = element[GatherProps].bind(element);
|
||||
const elementGatherProps = element[GatherProps].bind(element);
|
||||
element[GatherProps] = function gatherTooltipProps(this: GenericTooltip) {
|
||||
const { display, classes, style, direction, xoffset, yoffset, pinned } = this;
|
||||
return {
|
||||
element: {
|
||||
[Component]: elementComponent,
|
||||
[GatherProps]: elementGratherProps
|
||||
[GatherProps]: elementGatherProps
|
||||
},
|
||||
display,
|
||||
classes,
|
||||
|
|
|
@ -24,63 +24,133 @@ import { createLazyProxy } from "util/proxies";
|
|||
import type { InjectionKey, Ref } from "vue";
|
||||
import { ref, shallowReactive, unref } from "vue";
|
||||
|
||||
/** A feature's node in the DOM that has its size tracked. */
|
||||
export interface FeatureNode {
|
||||
rect: DOMRect;
|
||||
observer: MutationObserver;
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* An injection key that a {@link ContextComponent} will use to provide a function that registers a {@link FeatureNode} with the given id and HTML element.
|
||||
*/
|
||||
export const RegisterNodeInjectionKey: InjectionKey<(id: string, element: HTMLElement) => void> =
|
||||
Symbol("RegisterNode");
|
||||
/**
|
||||
* An injection key that a {@link ContextComponent} will use to provide a function that unregisters a {@link FeatureNode} with the given id.
|
||||
*/
|
||||
export const UnregisterNodeInjectionKey: InjectionKey<(id: string) => void> =
|
||||
Symbol("UnregisterNode");
|
||||
/**
|
||||
* An injection key that a {@link ContextComponent} will use to provide a ref to a map of all currently registered {@link FeatureNode}s.
|
||||
*/
|
||||
export const NodesInjectionKey: InjectionKey<Ref<Record<string, FeatureNode | undefined>>> =
|
||||
Symbol("Nodes");
|
||||
/**
|
||||
* An injection key that a {@link ContextComponent} will use to provide a ref to a bounding rect of the Context.
|
||||
*/
|
||||
export const BoundsInjectionKey: InjectionKey<Ref<DOMRect | undefined>> = Symbol("Bounds");
|
||||
|
||||
/** All types of events able to be sent or emitted from a layer's emitter. */
|
||||
export interface LayerEvents {
|
||||
// Generation
|
||||
/** Sent every game tick, before the update event. Intended for "generation" type actions. */
|
||||
preUpdate: (diff: number) => void;
|
||||
// Actions (e.g. automation)
|
||||
/** Sent every game tick. Intended for "automation" type actions. */
|
||||
update: (diff: number) => void;
|
||||
// Effects (e.g. milestones)
|
||||
/** Sent every game tick, after the update event. Intended for checking state. */
|
||||
postUpdate: (diff: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to all the current layers.
|
||||
* It is shallow reactive so it will update when layers are added or removed, but not interfere with the existing refs within each layer.
|
||||
*/
|
||||
export const layers: Record<string, Readonly<GenericLayer> | undefined> = shallowReactive({});
|
||||
|
||||
declare global {
|
||||
/** Augment the window object so the layers can be accessed from the console. */
|
||||
interface Window {
|
||||
layers: Record<string, Readonly<GenericLayer> | undefined>;
|
||||
}
|
||||
}
|
||||
window.layers = layers;
|
||||
|
||||
declare module "@vue/runtime-dom" {
|
||||
/** Augment CSS Properties to allow for setting the layer color CSS variable. */
|
||||
interface CSSProperties {
|
||||
"--layer-color"?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** An object representing the position of some entity. */
|
||||
export interface Position {
|
||||
/** The X component of the entity's position. */
|
||||
x: number;
|
||||
/** The Y component of the entity's position. */
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Layer}.
|
||||
* Even moreso than features, the developer is expected to include extra properties in this object.
|
||||
* All {@link game/persistence.Persistent} refs must be included somewhere within the layer object.
|
||||
*/
|
||||
export interface LayerOptions {
|
||||
/** The color of the layer, used to theme the entire layer's display. */
|
||||
color?: Computable<string>;
|
||||
/**
|
||||
* The layout of this layer's features.
|
||||
* When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is display.
|
||||
*/
|
||||
display: Computable<CoercableComponent>;
|
||||
/** An object of classes that should be applied to the display. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** Styles that should be applied to the display. */
|
||||
style?: Computable<StyleValue>;
|
||||
/**
|
||||
* The name of the layer, used on minimized tabs.
|
||||
* Defaults to {@link BaseLayer.id}.
|
||||
*/
|
||||
name?: Computable<string>;
|
||||
/**
|
||||
* Whether or not the layer can be minimized.
|
||||
* Defaults to true.
|
||||
*/
|
||||
minimizable?: Computable<boolean>;
|
||||
/**
|
||||
* Whether or not to force the go back button to be hidden.
|
||||
* If true, go back will be hidden regardless of {@link data/projInfo.allowGoBack}.
|
||||
*/
|
||||
forceHideGoBack?: Computable<boolean>;
|
||||
/**
|
||||
* A CSS min-width value that is applied to the layer.
|
||||
* Can be a number, in which case the unit is assumed to be px.
|
||||
* Defaults to 600px.
|
||||
*/
|
||||
minWidth?: Computable<number | string>;
|
||||
}
|
||||
|
||||
/** The properties that are added onto a processed {@link LayerOptions} to create a {@link Layer} */
|
||||
export interface BaseLayer {
|
||||
/**
|
||||
* The ID of the layer.
|
||||
* Populated from the {@link createLayer} parameters.
|
||||
* Used for saving and tracking open tabs.
|
||||
*/
|
||||
id: string;
|
||||
/** A persistent ref tracking if the tab is minimized or not. */
|
||||
minimized: Persistent<boolean>;
|
||||
/** An emitter for sending {@link LayerEvents} events for this layer. */
|
||||
emitter: Emitter<LayerEvents>;
|
||||
/** A function to register an event listener on {@link emitter}. */
|
||||
on: OmitThisParameter<Emitter<LayerEvents>["on"]>;
|
||||
emit: <K extends keyof LayerEvents>(event: K, ...args: Parameters<LayerEvents[K]>) => void;
|
||||
/** A function to emit a {@link LayerEvents} event to this layer. */
|
||||
emit: <K extends keyof LayerEvents>(...args: [K, ...Parameters<LayerEvents[K]>]) => void;
|
||||
/** A map of {@link FeatureNode}s present in this layer's {@link ContextComponent} component. */
|
||||
nodes: Ref<Record<string, FeatureNode | undefined>>;
|
||||
}
|
||||
|
||||
/** An unit of game content. Displayed to the user as a tab or modal. */
|
||||
export type Layer<T extends LayerOptions> = Replace<
|
||||
T & BaseLayer,
|
||||
{
|
||||
|
@ -95,6 +165,7 @@ export type Layer<T extends LayerOptions> = Replace<
|
|||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Layer} object. */
|
||||
export type GenericLayer = Replace<
|
||||
Layer<LayerOptions>,
|
||||
{
|
||||
|
@ -104,8 +175,19 @@ export type GenericLayer = Replace<
|
|||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* When creating layers, this object a map of layer ID to a set of any created persistent refs in order to check they're all included in the final layer object.
|
||||
*/
|
||||
export const persistentRefs: Record<string, Set<Persistent>> = {};
|
||||
/**
|
||||
* When creating layers, this array stores the layers currently being created, as a stack.
|
||||
*/
|
||||
export const addingLayers: string[] = [];
|
||||
/**
|
||||
* Lazily creates a layer with the given options.
|
||||
* @param id The ID this layer will have. See {@link BaseLayer.id}.
|
||||
* @param optionsFunc Layer options.
|
||||
*/
|
||||
export function createLayer<T extends LayerOptions>(
|
||||
id: string,
|
||||
optionsFunc: OptionsFunc<T, BaseLayer>
|
||||
|
@ -114,7 +196,9 @@ export function createLayer<T extends LayerOptions>(
|
|||
const layer = {} as T & Partial<BaseLayer>;
|
||||
const emitter = (layer.emitter = createNanoEvents<LayerEvents>());
|
||||
layer.on = emitter.on.bind(emitter);
|
||||
layer.emit = emitter.emit.bind(emitter);
|
||||
layer.emit = emitter.emit.bind(emitter) as <K extends keyof LayerEvents>(
|
||||
...args: [K, ...Parameters<LayerEvents[K]>]
|
||||
) => void;
|
||||
layer.nodes = ref({});
|
||||
layer.id = id;
|
||||
|
||||
|
@ -143,6 +227,14 @@ export function createLayer<T extends LayerOptions>(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a layer object, so it will be updated every tick.
|
||||
* Note that accessing a layer/its properties does NOT require it to be enabled.
|
||||
* For dynamic layers you can call this function and {@link removeLayer} as necessary. Just make sure {@link data/projEntry.getInitialLayers} will provide an accurate list of layers based on the player data object.
|
||||
* For static layers just make {@link data/projEntry.getInitialLayers} return all the layers.
|
||||
* @param layer The layer to add.
|
||||
* @param player The player data object, which will have a data object for this layer.
|
||||
*/
|
||||
export function addLayer(
|
||||
layer: GenericLayer,
|
||||
player: { layers?: Record<string, Record<string, unknown>> }
|
||||
|
@ -166,10 +258,19 @@ export function addLayer(
|
|||
globalBus.emit("addLayer", layer, player.layers[layer.id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting a layer by its ID with correct typing.
|
||||
* @param layerID The ID of the layer to get.
|
||||
*/
|
||||
export function getLayer<T extends GenericLayer>(layerID: string): T {
|
||||
return layers[layerID] as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a layer, so it will no longer be updated every tick.
|
||||
* Note that accessing a layer/its properties does NOT require it to be enabled.
|
||||
* @param layer The layer to remove.
|
||||
*/
|
||||
export function removeLayer(layer: GenericLayer): void {
|
||||
console.info("Removing layer", layer.id);
|
||||
globalBus.emit("removeLayer", layer);
|
||||
|
@ -177,6 +278,11 @@ export function removeLayer(layer: GenericLayer): void {
|
|||
layers[layer.id] = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for removing and immediately re-adding a layer.
|
||||
* This is useful for layers with dynamic content, to ensure persistent refs are correctly configured.
|
||||
* @param layer Layer to remove and then re-add
|
||||
*/
|
||||
export function reloadLayer(layer: GenericLayer): void {
|
||||
removeLayer(layer);
|
||||
|
||||
|
@ -184,6 +290,11 @@ export function reloadLayer(layer: GenericLayer): void {
|
|||
addLayer(layer, player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for creating a modal that display's a {@link LayerOptions.display}.
|
||||
* Returns the modal itself, which can be rendered anywhere you need, as well as a function to open the modal.
|
||||
* @param layer The layer to display in the modal.
|
||||
*/
|
||||
export function setupLayerModal(layer: GenericLayer): {
|
||||
openModal: VoidFunction;
|
||||
modal: JSXFunction;
|
||||
|
|
|
@ -126,6 +126,13 @@ const playerHandler: ProxyHandler<Record<PropertyKey, any>> = {
|
|||
return Object.getOwnPropertyDescriptor(target[ProxyState], key);
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
/** Augment the window object so the player can be accessed from the console. */
|
||||
interface Window {
|
||||
player: Player;
|
||||
}
|
||||
}
|
||||
export default window.player = new Proxy(
|
||||
{ [ProxyState]: state, [ProxyPath]: ["player"] },
|
||||
playerHandler
|
||||
|
|
|
@ -30,7 +30,28 @@ watch(
|
|||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Augment the window object so the settings, and hard resetting the settings, can be accessed from the console.
|
||||
*/
|
||||
interface Window {
|
||||
settings: Settings;
|
||||
hardResetSettings: VoidFunction;
|
||||
}
|
||||
}
|
||||
export default window.settings = state as Settings;
|
||||
export const hardResetSettings = (window.hardResetSettings = () => {
|
||||
const settings = {
|
||||
active: "",
|
||||
saves: [],
|
||||
showTPS: true,
|
||||
theme: Themes.Nordic
|
||||
};
|
||||
globalBus.emit("loadSettings", settings);
|
||||
Object.assign(state, settings);
|
||||
hardReset();
|
||||
});
|
||||
|
||||
export function loadSettings(): void {
|
||||
try {
|
||||
|
@ -59,18 +80,6 @@ export function loadSettings(): void {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
export const hardResetSettings = (window.hardResetSettings = () => {
|
||||
const settings = {
|
||||
active: "",
|
||||
saves: [],
|
||||
showTPS: true,
|
||||
theme: Themes.Nordic
|
||||
};
|
||||
globalBus.emit("loadSettings", settings);
|
||||
Object.assign(state, settings);
|
||||
hardReset();
|
||||
});
|
||||
|
||||
export const settingFields: CoercableComponent[] = reactive([]);
|
||||
export function registerSettingField(component: CoercableComponent) {
|
||||
settingFields.push(component);
|
||||
|
|
|
@ -7,6 +7,12 @@ export interface Transient {
|
|||
NaNReceiver?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
/** Augment the window object so the transient state can be accessed from the console. */
|
||||
interface Window {
|
||||
state: Transient;
|
||||
}
|
||||
}
|
||||
export default window.state = shallowReactive<Transient>({
|
||||
lastTenTicks: [],
|
||||
hasNaN: false,
|
||||
|
|
44
src/main.ts
44
src/main.ts
|
@ -1,45 +1,31 @@
|
|||
import App from "App.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import type { GenericLayer } from "game/layers";
|
||||
import "game/notifications";
|
||||
import type { PlayerData } from "game/player";
|
||||
import type { Settings } from "game/settings";
|
||||
import type { Transient } from "game/state";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import { load } from "util/save";
|
||||
import { useRegisterSW } from "virtual:pwa-register/vue";
|
||||
import type { App as VueApp } from "vue";
|
||||
import { createApp, nextTick } from "vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
|
||||
document.title = projInfo.title;
|
||||
if (projInfo.id === "") {
|
||||
throw "Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json";
|
||||
}
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Augment the window object so the vue app and project info can be accessed from the console.
|
||||
*/
|
||||
interface Window {
|
||||
vue: VueApp;
|
||||
save: VoidFunction;
|
||||
hardReset: VoidFunction;
|
||||
hardResetSettings: VoidFunction;
|
||||
layers: Record<string, Readonly<GenericLayer> | undefined>;
|
||||
player: PlayerData;
|
||||
state: Transient;
|
||||
settings: Settings;
|
||||
Decimal: typeof Decimal;
|
||||
exponentialFormat: (num: DecimalSource, precision: number, mantissa: boolean) => string;
|
||||
commaFormat: (num: DecimalSource, precision: number) => string;
|
||||
regularFormat: (num: DecimalSource, precision: number) => string;
|
||||
format: (num: DecimalSource, precision?: number, small?: boolean) => string;
|
||||
formatWhole: (num: DecimalSource) => string;
|
||||
formatTime: (s: number) => string;
|
||||
toPlaces: (x: DecimalSource, precision: number, maxAccepted: DecimalSource) => string;
|
||||
formatSmall: (x: DecimalSource, precision?: number) => string;
|
||||
invertOOM: (x: DecimalSource) => Decimal;
|
||||
projInfo: typeof projInfo;
|
||||
}
|
||||
|
||||
/** Fix for typedoc treating import functions as taking AssertOptions instead of GlobOptions. */
|
||||
interface AssertOptions {
|
||||
as: string;
|
||||
}
|
||||
}
|
||||
|
||||
document.title = projInfo.title;
|
||||
window.projInfo = projInfo;
|
||||
if (projInfo.id === "") {
|
||||
throw "Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json";
|
||||
}
|
||||
|
||||
requestAnimationFrame(async () => {
|
||||
|
@ -90,5 +76,3 @@ requestAnimationFrame(async () => {
|
|||
|
||||
startGameLoop();
|
||||
});
|
||||
|
||||
window.projInfo = projInfo;
|
||||
|
|
|
@ -17,6 +17,21 @@ export const {
|
|||
|
||||
export type DecimalSource = RawDecimalSource;
|
||||
|
||||
declare global {
|
||||
/** Augment the window object so the big num functions can be access from the console. */
|
||||
interface Window {
|
||||
Decimal: typeof Decimal;
|
||||
exponentialFormat: (num: DecimalSource, precision: number, mantissa: boolean) => string;
|
||||
commaFormat: (num: DecimalSource, precision: number) => string;
|
||||
regularFormat: (num: DecimalSource, precision: number) => string;
|
||||
format: (num: DecimalSource, precision?: number, small?: boolean) => string;
|
||||
formatWhole: (num: DecimalSource) => string;
|
||||
formatTime: (s: number) => string;
|
||||
toPlaces: (x: DecimalSource, precision: number, maxAccepted: DecimalSource) => string;
|
||||
formatSmall: (x: DecimalSource, precision?: number) => string;
|
||||
invertOOM: (x: DecimalSource) => Decimal;
|
||||
}
|
||||
}
|
||||
window.Decimal = Decimal;
|
||||
window.exponentialFormat = exponentialFormat;
|
||||
window.commaFormat = commaFormat;
|
||||
|
|
|
@ -37,6 +37,10 @@ export function processComputable<T, S extends keyof ComputableKeysOf<T>>(
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
obj[key] = computed(computable.bind(obj));
|
||||
} else if (isFunction(computable)) {
|
||||
obj[key] = computable.bind(obj);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(obj[key] as any)[DoNotCache] = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -124,6 +124,16 @@ window.onbeforeunload = () => {
|
|||
save();
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Augment the window object so the save function, and the hard reset function can be access from the console.
|
||||
*/
|
||||
interface Window {
|
||||
save: VoidFunction;
|
||||
hardReset: VoidFunction;
|
||||
}
|
||||
}
|
||||
window.save = save;
|
||||
export const hardReset = (window.hardReset = async () => {
|
||||
await loadSave(newSave());
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { jest, describe, expect, test } from "@jest/globals";
|
||||
import { camelToTitle, isFunction } from "util/common";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
describe("camelToTitle", () => {
|
||||
test("Capitalizes first letter in single word", () =>
|
||||
|
@ -10,7 +10,7 @@ describe("camelToTitle", () => {
|
|||
});
|
||||
|
||||
describe("isFunction", () => {
|
||||
test("Given function returns true", () => expect(isFunction(jest.fn())).toBe(true));
|
||||
test("Given function returns true", () => expect(isFunction(vi.fn())).toBe(true));
|
||||
|
||||
// Go through all primitives and basic types
|
||||
test("Given a string returns false", () => expect(isFunction("test")).toBe(false));
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
"sourceMap": true,
|
||||
"baseUrl": "src",
|
||||
"types": [
|
||||
"jest",
|
||||
"vite/client"
|
||||
"vite/client",
|
||||
"node"
|
||||
],
|
||||
"lib": [
|
||||
"esnext",
|
||||
|
|
|
@ -40,5 +40,8 @@ export default defineConfig({
|
|||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
],
|
||||
test: {
|
||||
environment: "jsdom"
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue