Merge remote-tracking branch 'template-fork/main' into feat/board-feature-rewrite
Some checks failed
Run Tests / test (pull_request) Failing after 2m29s

This commit is contained in:
thepaperpilot 2024-12-31 07:20:15 -06:00
commit 05dc162ec1
46 changed files with 268 additions and 212 deletions

View file

@ -9,18 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.7.0] - 2024-12-31
### Additions
- Added modal to take a mental health break (can be disabled via projInfo.json)
- Added `ConversionType` symbol
- Added `isType` function that uses a type symbol to check
- Added `MaybeGetter` utility type for something that may be a getter function or a static value (but not a ref)
### Changes
- **BREAKING** Replaced Board feature with generic Board system
- **BREAKING** Replaced Board feature with generic Board system that works with SVG and DOM elements
- **BREAKING** Rewrote how features are written, simplifying them greatly
- **BREAKING** Replaced decorators with mixins and wrappers
- **BREAKING** Moved modals to `src/components/modals`
- **BREAKING** Updated a very large amount of dependencies, making any necessary adjustments
- **BREAKING** Removed Grid component
- **BREAKING** `dontMerge` is now a property on rows and columns rather than an undocumented css class you'd have to include on every feature within the row or column
- **BREAKING** Moved all features that use the clickable component into the clickable folder
- **BREAKING** Removed small property from clickable, since its a single css rule (min-height: unset)
- **BREAKING** Removed `setDefault`, just use `??=`
- **BREAKING** Made Achievement.vue use a Renderable for the display. The object of components can still be passed to createAchievement
- **BREAKING** Made Challenge.vue use a Renderable for the display. The object of components can still be passed to createChallenge
- Upgrades now use the clickable component
### Fixes
- Hotkey descriptions were not being wrapped in `unref`
- Links wouldn't check if the end node existed when determining valid links
- `forceHideGoBack` was not being respected
- Saves manager not being imported in addiction warning component
Contributors: thepaperpilot

View file

@ -25,22 +25,23 @@
<script setup lang="ts">
import projInfo from "data/projInfo.json";
import { Layer, type FeatureNode } from "game/layers";
import { type FeatureNode } from "game/layers";
import player from "game/player";
import { render } from "util/vue";
import { computed, onErrorCaptured, ref, unref } from "vue";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { computed, MaybeRef, onErrorCaptured, Ref, ref, unref } from "vue";
import Context from "./Context.vue";
import ErrorVue from "./Error.vue";
const props = defineProps<{
display: Layer["display"];
minimizedDisplay: Layer["minimizedDisplay"];
minimized: Layer["minimized"];
name: Layer["name"];
color: Layer["color"];
minimizable: Layer["minimizable"];
nodes: Layer["nodes"];
forceHideGoBack: Layer["forceHideGoBack"];
display: MaybeGetter<Renderable>;
minimizedDisplay?: MaybeGetter<Renderable>;
minimized: Ref<boolean>;
name?: MaybeRef<string>;
color?: MaybeRef<string>;
minimizable?: MaybeRef<boolean>;
nodes: Ref<Record<string, FeatureNode | undefined>>;
forceHideGoBack?: MaybeRef<boolean>;
index: number;
}>();

View file

@ -88,7 +88,7 @@
</ul>
</div>
</div>
<Info ref="info" :changelog="changelog" />
<Info ref="info" @open-changelog="changelog?.open()" />
<SavesManager ref="savesManager" />
<Options ref="options" />
<Changelog ref="changelog" />
@ -97,22 +97,19 @@
<script setup lang="ts">
import Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json";
import Tooltip from "wrappers/tooltips/Tooltip.vue";
import settings from "game/settings";
import { Direction } from "util/common";
import { galaxy, syncedSaves } from "util/galaxy";
import type { ComponentPublicInstance } from "vue";
import { computed, ref } from "vue";
import Tooltip from "wrappers/tooltips/Tooltip.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);
const options = ref<ComponentPublicInstance<typeof Options> | null>(null);
// For some reason Info won't accept the changelog unless I do this:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changelog = ref<ComponentPublicInstance<any> | null>(null);
const info = ref<typeof Info | null>(null);
const savesManager = ref<typeof SavesManager | null>(null);
const options = ref<typeof Options | null>(null);
const changelog = ref<typeof Changelog | null>(null);
const { useHeader, banner, title, discordName, discordLink, versionNumber } = projInfo;

View file

@ -18,7 +18,7 @@
Made in Profectus, by thepaperpilot with inspiration from Acameada and Jacorb
</div>
<br />
<div class="link" @click="openChangelog">Changelog</div>
<div class="link" @click="emits('openChangelog')">Changelog</div>
<br />
<div>
<a
@ -53,14 +53,13 @@
</div>
<br />
<div>Time Played: {{ timePlayed }}</div>
<Info />
<InfoComponents />
</div>
</template>
</Modal>
</template>
<script setup lang="tsx">
import type Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json";
import player from "game/player";
import { infoComponents } from "game/settings";
@ -71,23 +70,21 @@ import Modal from "./Modal.vue";
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;
const props = defineProps<{ changelog: typeof Changelog | null }>();
const emits = defineEmits<{
(e: "openChangelog"): void;
}>();
const isOpen = ref(false);
const timePlayed = computed(() => formatTime(player.timePlayed));
const Info = () => infoComponents.map(f => render(f));
const InfoComponents = () => infoComponents.map(f => render(f));
defineExpose({
open() {
isOpen.value = true;
}
});
function openChangelog() {
props.changelog?.open();
}
</script>
<style scoped>

View file

@ -15,14 +15,26 @@
<div class="modal-wrapper">
<div class="modal-container" :width="width">
<div class="modal-header">
<slot name="header" :shown="isOpen"> default header </slot>
<!--
@slot Modal Header
@binding {boolean} shown Whether the modal is currently open or animating
-->
<slot name="header" :shown="isOpen" />
</div>
<div class="modal-body">
<Context ref="contextRef">
<slot name="body" :shown="isOpen"> default body </slot>
<!--
@slot Modal Body
@binding {boolean} shown Whether the modal is currently open or animating
-->
<slot name="body" :shown="isOpen" />
</Context>
</div>
<div class="modal-footer">
<!--
@slot Modal Footer
@binding {boolean} shown Whether the modal is currently open or animating
-->
<slot name="footer" :shown="isOpen">
<div class="modal-default-footer">
<div class="modal-default-flex-grow"></div>

View file

@ -75,15 +75,15 @@
</template>
<script setup lang="ts">
import Tooltip from "wrappers/tooltips/Tooltip.vue";
import player from "game/player";
import { Direction } from "util/common";
import { galaxy, syncedSaves } from "util/galaxy";
import { LoadablePlayerData } from "util/save";
import { computed, ref, watch } from "vue";
import Tooltip from "wrappers/tooltips/Tooltip.vue";
import DangerButton from "../fields/DangerButton.vue";
import FeedbackButton from "../fields/FeedbackButton.vue";
import Text from "../fields/Text.vue";
import type { LoadablePlayerData } from "./SavesManager.vue";
const props = defineProps<{
save: LoadablePlayerData;

View file

@ -72,6 +72,7 @@ import {
decodeSave,
getCachedSave,
getUniqueID,
LoadablePlayerData,
loadSave,
newSave,
save
@ -84,8 +85,6 @@ import Text from "../fields/Text.vue";
import Modal from "./Modal.vue";
import Save from "./Save.vue";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
const isOpen = ref(false);
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null);

View file

View file

@ -19,13 +19,13 @@ import Node from "components/Node.vue";
import type { Visibility } from "features/feature";
import { isHidden, isVisible } from "features/feature";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { render, Renderable, Wrapper } from "util/vue";
import { MaybeRef, unref, type CSSProperties } from "vue";
const props = withDefaults(defineProps<{
id: string;
components: MaybeGetter<Renderable>[];
wrappers: ((el: () => Renderable) => Renderable)[];
wrappers: Wrapper[];
visibility?: MaybeRef<Visibility | boolean>;
style?: MaybeRef<CSSProperties>;
classes?: MaybeRef<Record<string, boolean>>;

View file

@ -16,17 +16,17 @@
<script setup lang="tsx">
import "components/common/features.css";
import { isJSXElement, render } from "util/vue";
import { Component, isRef, unref } from "vue";
import { Achievement } from "./achievement";
import { displayRequirements } from "game/requirements";
import { Requirements } from "game/requirements";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { Component, MaybeRef, Ref, unref } from "vue";
const props = defineProps<{
display: Achievement["display"];
earned: Achievement["earned"];
requirements: Achievement["requirements"];
image: Achievement["image"];
small: Achievement["small"];
display?: MaybeGetter<Renderable>;
earned: Ref<boolean>;
requirements?: Requirements;
image?: MaybeRef<string>;
small?: MaybeRef<boolean>;
}>();
const Component = () => props.display == null ? <></> : render(props.display);

View file

@ -31,23 +31,23 @@
</template>
<script setup lang="ts">
import Decimal from "util/bignum";
import Decimal, { DecimalSource } from "util/bignum";
import { Direction } from "util/common";
import { render } from "util/vue";
import type { CSSProperties } from "vue";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import type { CSSProperties, MaybeRef } from "vue";
import { computed, unref } from "vue";
import { Bar } from "./bar";
const props = defineProps<{
width: Bar["width"];
height: Bar["height"];
direction: Bar["direction"];
borderStyle: Bar["borderStyle"];
baseStyle: Bar["baseStyle"];
textStyle: Bar["textStyle"];
fillStyle: Bar["fillStyle"];
progress: Bar["progress"];
display: Bar["display"];
width: MaybeRef<number>;
height: MaybeRef<number>;
direction: MaybeRef<Direction>;
borderStyle?: MaybeRef<CSSProperties>;
baseStyle?: MaybeRef<CSSProperties>;
textStyle?: MaybeRef<CSSProperties>;
fillStyle?: MaybeRef<CSSProperties>;
progress: MaybeRef<DecimalSource>;
display?: MaybeGetter<Renderable>;
}>();
const normalizedProgress = computed(() => {

View file

@ -10,7 +10,7 @@
>
<button
class="toggleChallenge"
@click="toggle"
@click="emits('toggle')"
:disabled="!unref(canStart) || unref(maxed)"
>
{{ buttonText }}
@ -22,20 +22,25 @@
<script setup lang="tsx">
import "components/common/features.css";
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
import { render } from "util/vue";
import type { Component } from "vue";
import { Requirements } from "game/requirements";
import { DecimalSource } from "util/bignum";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import type { Component, MaybeRef, Ref } from "vue";
import { computed, unref } from "vue";
import { Challenge } from "./challenge";
const props = defineProps<{
active: Challenge["active"];
maxed: Challenge["maxed"];
canComplete: Challenge["canComplete"];
display: Challenge["display"];
requirements: Challenge["requirements"];
completed: Challenge["completed"];
canStart: Challenge["canStart"];
toggle: Challenge["toggle"];
active: Ref<boolean>;
maxed: Ref<boolean>;
canComplete: Ref<DecimalSource>;
display?: MaybeGetter<Renderable>;
requirements: Requirements;
completed: Ref<boolean>;
canStart?: MaybeRef<boolean>;
}>();
const emits = defineEmits<{
(e: "toggle"): void;
}>();
const buttonText = computed(() => {

View file

@ -129,7 +129,7 @@ export function createChallenge<T extends ChallengeOptions>(optionsFunc: () => T
requirements={challenge.requirements}
completed={challenge.completed}
canStart={challenge.canStart}
toggle={challenge.toggle}
onToggle={challenge.toggle}
/>
));

View file

@ -1,6 +1,6 @@
<template>
<button
@click="onClick"
@click="e => emits('click', e)"
@mousedown="start"
@mouseleave="stop"
@mouseup="stop"
@ -20,24 +20,28 @@
<script setup lang="tsx">
import "components/common/features.css";
import type { Clickable } from "features/clickables/clickable";
import { MaybeGetter } from "util/computed";
import {
render,
Renderable,
setupHoldToClick
} from "util/vue";
import type { Component } from "vue";
import { toRef, unref } from "vue";
import type { Component, MaybeRef } from "vue";
import { unref } from "vue";
const props = defineProps<{
canClick: Clickable["canClick"];
onClick: Clickable["onClick"];
onHold?: Clickable["onHold"];
display: Clickable["display"];
canClick: MaybeRef<boolean>;
display?: MaybeGetter<Renderable>;
}>();
const emits = defineEmits<{
(e: "click", event?: MouseEvent | TouchEvent): void;
(e: "hold"): void;
}>();
const Component = () => props.display == null ? <></> : render(props.display);
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
const { start, stop } = setupHoldToClick(() => emits("hold"));
</script>
<style scoped>

View file

@ -2,8 +2,8 @@ import ClickableVue from "features/clickables/Clickable.vue";
import { findFeatures } from "features/feature";
import { globalBus } from "game/events";
import { persistent } from "game/persistence";
import Decimal, { DecimalSource } from "lib/break_eternity";
import { Unsubscribe } from "nanoevents";
import Decimal, { DecimalSource } from "util/bignum";
import { Direction } from "util/common";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
@ -125,6 +125,7 @@ export function createAction<T extends ActionOptions>(optionsFunc?: () => T) {
<ClickableVue
canClick={action.canClick}
onClick={action.onClick}
onHold={action.onClick}
display={action.display}
/>
)

View file

@ -87,6 +87,7 @@ export function createClickable<T extends ClickableOptions>(optionsFunc?: () =>
<Clickable
canClick={clickable.canClick}
onClick={clickable.onClick}
onHold={clickable.onClick}
display={clickable.display}
/>
)),
@ -95,7 +96,7 @@ export function createClickable<T extends ClickableOptions>(optionsFunc?: () =>
onClick:
onClick == null
? undefined
: function (e) {
: function (e?: MouseEvent | TouchEvent) {
if (unref(clickable.canClick) !== false) {
onClick.call(clickable, e);
}

View file

@ -98,6 +98,7 @@ export function createRepeatable<T extends RepeatableOptions>(optionsFunc: () =>
<Clickable
canClick={repeatable.canClick}
onClick={repeatable.onClick}
onHold={repeatable.onClick}
display={repeatable.display}
/>
));

View file

@ -23,16 +23,35 @@ export enum Visibility {
None
}
/**
* Utility function for determining if a visibility value is anything but Visibility.None.
Booleans are allowed and false will be considered to be Visibility.None.
* @param visibility The ref to either a visibility value or boolean
* @returns True if the visibility is either true, Visibility.Visible, or Visibility.Hidden
*/
export function isVisible(visibility: MaybeRef<Visibility | boolean>) {
const currVisibility = unref(visibility);
return currVisibility !== Visibility.None && currVisibility !== false;
}
/**
* Utility function for determining if a visibility value is Visibility.Hidden.
Booleans are allowed but will never be considered to be Visible.Hidden.
* @param visibility The ref to either a visibility value or boolean
* @returns True if the visibility is Visibility.Hidden
*/
export function isHidden(visibility: MaybeRef<Visibility | boolean>) {
const currVisibility = unref(visibility);
return currVisibility === Visibility.Hidden;
}
/**
* Utility function for narrowing something that may or may not be a specified type of feature.
* Works off the principle that all features have a unique symbol to identify themselves with.
* @param object The object to determine whether or not is of the specified type
* @param type The symbol to look for in the object's "type" property
* @returns Whether or not the object is the specified type
*/
export function isType<T extends symbol>(object: unknown, type: T): object is { type: T } {
return object != null && typeof object === "object" && "type" in object && object.type === type;
}
@ -64,6 +83,12 @@ export function findFeatures(obj: object, ...types: symbol[]): unknown[] {
return objects;
}
/**
* Utility function for taking a list of features and filtering them out, but keeping a reference to the first filtered out feature. Used for having a collapsible of the filtered out content, with the first filtered out item remaining outside the collapsible for easy reference.
* @param features The list of features to search through
* @param filter The filter to use to determine features that shouldn't be collapsible
* @returns An object containing a ref to the first filtered _out_ feature, a render function for the collapsed content, and a ref for whether or not there is any collapsed content to show
*/
export function getFirstFeature<T extends VueFeature>(
features: T[],
filter: (feature: T) => boolean

View file

@ -26,17 +26,17 @@
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
import themes from "data/themes";
import settings from "game/settings";
import { render } from "util/vue";
import { computed, unref } from "vue";
import { Infobox } from "./infobox";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { computed, CSSProperties, MaybeRef, Ref, unref } from "vue";
const props = defineProps<{
color: Infobox["color"];
titleStyle: Infobox["titleStyle"];
bodyStyle: Infobox["bodyStyle"];
collapsed: Infobox["collapsed"];
display: Infobox["display"];
title: Infobox["title"];
color?: MaybeRef<string>;
titleStyle?: MaybeRef<CSSProperties>;
bodyStyle?: MaybeRef<CSSProperties>;
collapsed: Ref<boolean>;
display: MaybeGetter<Renderable>;
title: MaybeGetter<Renderable>;
}>();
const Title = () => render(props.title);

View file

@ -15,11 +15,11 @@
<script setup lang="ts">
import type { FeatureNode } from "game/layers";
import { BoundsInjectionKey, NodesInjectionKey } from "game/layers";
import { computed, inject, onMounted, ref, shallowRef, unref, watch } from "vue";
import { computed, inject, MaybeRef, onMounted, ref, shallowRef, unref, watch } from "vue";
import LinkVue from "./Link.vue";
import { Links } from "./links";
import { Link } from "./links";
const props = defineProps<{ links: Links["links"] }>();
const props = defineProps<{ links: MaybeRef<Link[]> }>();
function updateBounds() {
boundingRect.value = resizeListener.value?.getBoundingClientRect();

View file

@ -9,13 +9,12 @@
import { Application } from "@pixi/app";
import { globalBus } from "game/events";
import "lib/pixi";
import { nextTick, onBeforeUnmount, onMounted, shallowRef, unref } from "vue";
import type { Particles } from "./particles";
import { nextTick, onBeforeUnmount, onMounted, shallowRef } from "vue";
const props = defineProps<{
onContainerResized: Particles["onContainerResized"];
onHotReload: Particles["onHotReload"];
onInit: (app: Application) => void;
const emits = defineEmits<{
(e: "containerResized", boundingRect: DOMRect): void;
(e: "hotReload"): void;
(e: "init", app: Application): void;
}>();
const app = shallowRef<null | Application>(null);
@ -32,12 +31,10 @@ onMounted(() => {
backgroundAlpha: 0
});
resizeListener.value?.appendChild(app.value.view);
props.onInit(app.value);
emits("init", app.value);
}
updateBounds();
if (props.onHotReload) {
nextTick(props.onHotReload);
}
nextTick(() => emits("hotReload"));
});
onBeforeUnmount(() => {
app.value?.destroy();
@ -50,7 +47,7 @@ function updateBounds() {
isDirty = false;
nextTick(() => {
if (resizeListener.value != null) {
props.onContainerResized?.(resizeListener.value.getBoundingClientRect());
emits("containerResized", resizeListener.value.getBoundingClientRect());
}
isDirty = true;
});

View file

@ -18,7 +18,7 @@ import { isRef, MaybeRef, MaybeRefOrGetter, unref } from "vue";
export const ResetType = Symbol("Reset");
/**
* An object that configures a {@link Clickable}.
* An object that configures a {@link features/clickables/clickable.Clickable}.
*/
export interface ResetOptions {
/** List of things to reset. Can include objects which will be recursed over for persistent values. */

View file

@ -21,13 +21,13 @@ import ResourceVue from "features/resources/Resource.vue";
import Decimal from "util/bignum";
import { MaybeGetter } from "util/computed";
import { Renderable } from "util/vue";
import { computed, ref, StyleValue, toValue } from "vue";
import { computed, CSSProperties, ref, toValue } from "vue";
const props = defineProps<{
resource: Resource;
color?: string;
classes?: Record<string, boolean>;
style?: StyleValue;
style?: CSSProperties;
effectDisplay?: MaybeGetter<Renderable>;
}>();

View file

@ -5,16 +5,16 @@
</template>
<script setup lang="ts">
import { getNotifyStyle } from "game/notifications";
import { render } from "util/vue";
import { computed, unref } from "vue";
import { TabButton } from "./tabFamily";
import themes from "data/themes";
import { getNotifyStyle } from "game/notifications";
import settings from "game/settings";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { computed, MaybeRef, unref } from "vue";
const props = defineProps<{
display: TabButton["display"];
glowColor: TabButton["glowColor"];
display: MaybeGetter<Renderable>;
glowColor?: MaybeRef<string>;
active?: boolean;
}>();

View file

@ -15,20 +15,21 @@
<script setup lang="ts">
import Sticky from "components/layout/Sticky.vue";
import { isType } from "features/feature";
import { render } from "util/vue";
import type { Component } from "vue";
import { computed, unref } from "vue";
import { TabType } from "./tab";
import { TabFamily } from "./tabFamily";
import themes from "data/themes";
import { isType } from "features/feature";
import settings from "game/settings";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import type { Component, CSSProperties, MaybeRef, Ref } from "vue";
import { computed, unref } from "vue";
import { Tab, TabType } from "./tab";
import { TabButton } from "./tabFamily";
const props = defineProps<{
activeTab: TabFamily["activeTab"];
tabs: TabFamily["tabs"];
buttonContainerClasses: TabFamily["buttonContainerClasses"];
buttonContainerStyle: TabFamily["buttonContainerStyle"];
activeTab: Ref<MaybeGetter<Renderable> | Tab | null>;
tabs: Record<string, TabButton>;
buttonContainerClasses?: MaybeRef<Record<string, boolean>>;
buttonContainerStyle?: MaybeRef<CSSProperties>;
}>();
const Component = () => {

View file

@ -15,7 +15,7 @@ export interface TabOptions extends VueFeatureOptions {
/**
* An object representing a tab of content in a tabbed interface.
* @see {@link TabFamily}
* @see {@link features/tabs/tabFamily.TabFamily}
*/
export interface Tab extends VueFeature {
/** The display to use for this tab. */

View file

@ -8,15 +8,15 @@
<script setup lang="tsx">
import "components/common/table.css";
import Links from "features/links/Links.vue";
import type { Tree } from "features/trees/tree";
import type { Tree, TreeBranch, TreeNode } from "features/trees/tree";
import { render } from "util/vue";
import { unref } from "vue";
import { MaybeRef, unref } from "vue";
const props = defineProps<{
nodes: Tree["nodes"];
leftSideNodes: Tree["leftSideNodes"];
rightSideNodes: Tree["rightSideNodes"];
branches: Tree["branches"];
nodes: MaybeRef<TreeNode[][]>;
leftSideNodes?: MaybeRef<TreeNode[]>;
rightSideNodes?: MaybeRef<TreeNode[]>;
branches?: MaybeRef<TreeBranch[]>;
}>();
const Nodes = () => unref(props.nodes).map(nodes =>

View file

@ -10,7 +10,7 @@
treeNode: true,
can: unref(canClick)
}"
@click="onClick"
@click="e => emits('click', e)"
@mousedown="start"
@mouseleave="stop"
@mouseup="stop"
@ -23,23 +23,26 @@
</template>
<script setup lang="tsx">
import { render, setupHoldToClick } from "util/vue";
import { toRef, unref } from "vue";
import { TreeNode } from "./tree";
import { MaybeGetter } from "util/computed";
import { render, Renderable, setupHoldToClick } from "util/vue";
import { MaybeRef, toRef, unref } from "vue";
const props = defineProps<{
canClick: TreeNode["canClick"];
display: TreeNode["display"];
onClick: TreeNode["onClick"];
onHold: TreeNode["onHold"];
color: TreeNode["color"];
glowColor: TreeNode["glowColor"];
canClick?: MaybeRef<boolean>;
display?: MaybeGetter<Renderable>;
color?: MaybeRef<string>;
glowColor?: MaybeRef<string>;
}>();
const emits = defineEmits<{
(e: "click", event?: MouseEvent | TouchEvent): void;
(e: "hold"): void;
}>();
const Component = () => props.display == null ? <></> :
render(props.display, el => <div>{el}</div>);
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
const { start, stop } = setupHoldToClick(() => emits("hold"));
</script>
<style scoped>

View file

@ -3,10 +3,10 @@
class="board-node"
:style="`transform: translate(calc(${unref(position).x}px - 50%), ${unref(position).y}px);`"
@click.capture.stop="() => {}"
@mousedown="mouseDown"
@touchstart.passive="mouseDown"
@mouseup.capture="mouseUp"
@touchend.passive="mouseUp"
@mousedown="e => emits('mouseDown', e)"
@touchstart.passive="e => emits('mouseDown', e)"
@mouseup.capture="e => emits('mouseUp', e)"
@touchend.passive="e => emits('mouseUp', e)"
>
<slot />
</div>
@ -17,8 +17,11 @@ import { Ref, unref } from "vue";
import { NodePosition } from "./board";
defineProps<{
mouseDown: (e: MouseEvent | TouchEvent) => void;
mouseUp: (e: MouseEvent | TouchEvent) => void;
position: Ref<NodePosition>;
}>();
const emits = defineEmits<{
(e: "mouseDown", event: MouseEvent | TouchEvent): void;
(e: "mouseUp", event: MouseEvent | TouchEvent): void;
}>();
</script>

View file

@ -51,7 +51,7 @@ export function setupUniqueIds(nodes: MaybeRefOrGetter<{ id: number }[]>) {
/** An object that configures a {@link DraggableNode}. */
export interface DraggableNodeOptions<T> {
/** A ref to the specific instance of the Board vue component the node will be draggable on. Obtained by passing a suitable ref as the "ref" attribute to the <Board> element. */
/** A ref to the specific instance of the Board vue component the node will be draggable on. Obtained by passing a suitable ref as the "ref" attribute to the Board component. */
board: Ref<ComponentPublicInstance<typeof Board> | undefined>;
/** Getter function to go from the node (typically ID) to the position of said node. */
getPosition: (node: T) => NodePosition;
@ -266,7 +266,7 @@ export interface Draggable<T> extends MakeDraggableOptions<T> {
/**
* Makes a vue feature draggable on a Board.
* @param element The vue feature to make draggable.
* @param options The options to configure the dragging behavior.
* @param optionsFunc The options to configure the dragging behavior.
*/
export function makeDraggable<T, S extends MakeDraggableOptions<T>>(
element: VueFeature,
@ -337,8 +337,8 @@ export function makeDraggable<T, S extends MakeDraggableOptions<T>>(
(el as VueFeature & { draggable: Draggable<T> }).draggable = draggable;
element.wrappers.push(el => (
<Draggable
mouseDown={draggable.onMouseDown}
mouseUp={draggable.onMouseUp}
onMouseDown={draggable.onMouseDown}
onMouseUp={draggable.onMouseUp}
position={draggable.computedPosition}
>
{el}

View file

@ -27,7 +27,6 @@ export interface GlobalEvents {
* Sent when constructing the {@link Settings} object.
* Use it to add default values for custom properties to the object.
* @param settings The settings object being constructed.
* @see {@link features/features.setDefault} for setting default values.
*/
loadSettings: (settings: Partial<Settings>) => void;
/**

View file

@ -1290,7 +1290,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
* A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted.
* Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently.
* @see {@link calculateMaxAffordable}
* @see {@link /game/requirements.createCostRequirement}
* @see {@link game/requirements.createCostRequirement}
*/
export default class Formula<
T extends [FormulaSource] | FormulaSource[]

View file

@ -29,22 +29,22 @@ export interface FeatureNode {
}
/**
* 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.
* An injection key that a Context component 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.
* An injection key that a Context component 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.
* An injection key that a Context component 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.
* An injection key that a Context component will use to provide a ref to a bounding rect of the Context.
*/
export const BoundsInjectionKey: InjectionKey<Ref<DOMRect | undefined>> = Symbol("Bounds");
@ -106,7 +106,7 @@ export interface LayerOptions {
color?: MaybeRefOrGetter<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 displayed.
* When the layer is open in {@link game/player.Player.tabs}, this is the content that is displayed.
*/
display: MaybeGetter<Renderable>;
/** An object of classes that should be applied to the display. */
@ -125,12 +125,12 @@ export interface LayerOptions {
minimizable?: MaybeRefOrGetter<boolean>;
/**
* The layout of this layer's features.
* When the layer is open in {@link game/player.PlayerData.tabs}, but the tab is {@link Layer.minimized} this is the content that is displayed.
* When the layer is open in {@link game/player.Player.tabs}, but the tab is {@link Layer.minimized} this is the content that is displayed.
*/
minimizedDisplay?: MaybeGetter<Renderable>;
/**
* 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}.
* If true, go back will be hidden regardless of allowGoBack value in the project settings.
*/
forceHideGoBack?: MaybeRefOrGetter<boolean>;
/**
@ -157,7 +157,7 @@ export interface BaseLayer {
on: OmitThisParameter<Emitter<LayerEvents>["on"]>;
/** 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. */
/** A map of {@link FeatureNode}s present in this layer's Context component. */
nodes: Ref<Record<string, FeatureNode | undefined>>;
}
@ -167,7 +167,7 @@ export interface Layer extends BaseLayer {
color?: MaybeRef<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 displayed.
* When the layer is open in {@link game/player.Player.tabs}, this is the content that is displayed.
*/
display: MaybeGetter<Renderable>;
/** An object of classes that should be applied to the display. */
@ -186,12 +186,12 @@ export interface Layer extends BaseLayer {
minimizable?: MaybeRef<boolean>;
/**
* The layout of this layer's features.
* When the layer is open in {@link game/player.PlayerData.tabs}, but the tab is {@link Layer.minimized} this is the content that is displayed.
* When the layer is open in {@link game/player.Player.tabs}, but the tab is {@link Layer.minimized} this is the content that is displayed.
*/
minimizedDisplay?: MaybeGetter<Renderable>;
/**
* 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}.
* If true, go back will be hidden regardless of allowGoBack value in the project settings.
*/
forceHideGoBack?: MaybeRef<boolean>;
/**

View file

@ -1,15 +1,14 @@
import { globalBus } from "game/events";
import { processGetter } from "util/computed";
import { trackHover, VueFeature } from "util/vue";
import { nextTick, Ref } from "vue";
import { ref, watch } from "vue";
import { CSSProperties, nextTick, Ref, ref, watch } from "vue";
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
globalBus.on("setupVue", vue => vue.use(Toast));
/**
* Gives a {@link CSSProperties} object that makes an object glow, to bring focus to it.
* Gives a [CSSProperties](https://vuejs.org/api/utility-types.html#cssproperties) object that makes an object glow, to bring focus to it.
* Default values are for a "soft" white notif effect.
* @param color The color of the glow effect.
* @param strength The strength of the glow effect - affects its spread.
@ -20,7 +19,7 @@ export function getNotifyStyle(color = "white", strength = "8px") {
borderColor: "rgba(0, 0, 0, 0.125)",
boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 ${strength} ${color}`,
zIndex: 1
};
} satisfies CSSProperties;
}
/** Utility function to call {@link getNotifyStyle} with "high importance" parameters. */

View file

@ -67,7 +67,7 @@ export type State =
| { [key: number]: State };
/**
* A {@link Ref} that has been augmented with properties to allow it to be saved and loaded within the player save data object.
* A [Ref](https://vuejs.org/api/reactivity-core.html#ref) that has been augmented with properties to allow it to be saved and loaded within the player save data object.
*/
export type Persistent<T extends State = State> = Ref<T> & {
value: T;

View file

@ -1,6 +1,6 @@
import { isVisible, Visibility } from "features/feature";
import { displayResource, Resource } from "features/resources/resource";
import Decimal, { DecimalSource } from "lib/break_eternity";
import Decimal, { DecimalSource } from "util/bignum";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { joinJSX, Renderable } from "util/vue";
@ -243,7 +243,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(optionsF
/**
* Utility function for creating a requirement that a specified vue feature is visible
* @param feature The feature to check the visibility of
* @param visibility The visibility ref to check
*/
export function createVisibilityRequirement(
visibility: MaybeRef<Visibility | boolean>

View file

@ -77,7 +77,7 @@ export const hardResetSettings = (window.hardResetSettings = () => {
/**
* Loads the player settings from localStorage.
* Calls the {@link GlobalEvents.loadSettings} event for custom properties to be included.
* Calls the {@link game/events.GlobalEvents.loadSettings} event for custom properties to be included.
* Custom properties should be added by the file they relate to, so they won't be included if the file is tree shaken away.
* Custom properties should also register the field to modify said setting using {@link registerSettingField}.
*/

View file

@ -9,8 +9,8 @@ import { useRegisterSW } from "virtual:pwa-register/vue";
import type { App as VueApp } from "vue";
import { createApp, nextTick } from "vue";
import { useToast } from "vue-toastification";
import { globalBus } from "./game/events";
import { startGameLoop } from "./game/gameLoop";
import { globalBus } from "game/events";
import { startGameLoop } from "game/gameLoop";
declare global {
/**

View file

@ -1,10 +1,9 @@
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
import player, { Player, stringifySave } from "game/player";
import settings from "game/settings";
import LZString from "lz-string";
import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
import { ref } from "vue";
import { decodeSave, loadSave, save, setupInitialStore } from "./save";
import { decodeSave, LoadablePlayerData, loadSave, save, setupInitialStore } from "./save";
export const galaxy = ref<GalaxyApi>();
export const conflictingSaves = ref<

View file

@ -3,8 +3,12 @@ import { NonPersistent } from "game/persistence";
export const ProxyState = Symbol("ProxyState");
export const AfterEvaluation = Symbol("AfterEvaluation");
// Takes a function that returns an object and pretends to be that object
// Note that the object is lazily calculated
/**
* Makes a lazily evaluated object through the use of a Proxy
* @param objectFunc Function that constructs the object to be proxies
* @param baseObject An optional base object to pass to objectFunc, which all return properties will be assigned onto
* @returns A proxy for the object created by objectFunc
*/
export function createLazyProxy<T extends object, S extends T>(
objectFunc: (this: S, baseObject: S) => T,
baseObject: S = {} as S
@ -74,6 +78,11 @@ export function createLazyProxy<T extends object, S extends T>(
}) as S & T;
}
/**
* Registers a callback to be called on a lazily evaluated proxy once its been evaluated.
* @param maybeProxy A value that may be a lazily evaluated proxy
* @param callback The callback to call once the proxy has been evaluated (or immediately, if the object is not a proxy)
*/
export function runAfterEvaluation<T extends object>(maybeProxy: T, callback: (object: T) => void) {
if (AfterEvaluation in maybeProxy) {
(maybeProxy[AfterEvaluation] as (callback: (object: T) => void) => void)(callback);

View file

@ -1,4 +1,3 @@
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
import { fixOldSave, getInitialLayers } from "data/projEntry";
import projInfo from "data/projInfo.json";
import { globalBus } from "game/events";
@ -9,6 +8,8 @@ import settings, { loadSettings } from "game/settings";
import LZString from "lz-string";
import { ref, shallowReactive } from "vue";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
export function setupInitialStore(player: Partial<Player> = {}): Player {
return Object.assign(
{

View file

@ -15,6 +15,7 @@ import { camelToKebab } from "./common";
export const VueFeature = Symbol("VueFeature");
export type Renderable = JSX.Element | string;
export type Wrapper = (el: () => Renderable) => Renderable;
export interface VueFeatureOptions {
/** Whether this feature should be visible. */
@ -37,7 +38,7 @@ export interface VueFeature {
/** The components to render inside the vue feature */
components: MaybeGetter<Renderable>[];
/** The components to render wrapped around the vue feature */
wrappers: ((el: () => Renderable) => Renderable)[];
wrappers: Wrapper[];
/** Used to identify Vue Features */
[VueFeature]: true;
}
@ -53,7 +54,7 @@ export function vueFeatureMixin(
classes: processGetter(options.classes),
style: processGetter(options.style),
components: component == null ? [] : [component],
wrappers: [] as ((el: () => Renderable) => Renderable)[],
wrappers: [] as Wrapper[],
[VueFeature]: true
} satisfies VueFeature;
}
@ -89,15 +90,11 @@ export function render(
return wrapper?.(object) ?? object;
}
export function renderRow(
...objects: (VueFeature | MaybeGetter<Renderable>)[]
): JSX.Element {
export function renderRow(...objects: (VueFeature | MaybeGetter<Renderable>)[]): JSX.Element {
return <Row>{objects.map(obj => render(obj))}</Row>;
}
export function renderCol(
...objects: (VueFeature | MaybeGetter<Renderable>)[]
): JSX.Element {
export function renderCol(...objects: (VueFeature | MaybeGetter<Renderable>)[]): JSX.Element {
return <Col>{objects.map(obj => render(obj))}</Col>;
}
@ -123,10 +120,7 @@ export function isJSXElement(element: unknown): element is JSX.Element {
);
}
export function setupHoldToClick(
onClick?: Ref<((e?: MouseEvent | TouchEvent) => void) | undefined>,
onHold?: Ref<VoidFunction | undefined>
): {
export function setupHoldToClick(callback: (e?: MouseEvent | TouchEvent) => void): {
start: (e: MouseEvent | TouchEvent) => void;
stop: VoidFunction;
handleHolding: VoidFunction;
@ -147,11 +141,7 @@ export function setupHoldToClick(
}
}
function handleHolding() {
if (onHold && onHold.value) {
onHold.value();
} else if (onClick && onClick.value) {
onClick.value(event.value);
}
callback(event.value);
}
onUnmounted(stop);

View file

@ -20,7 +20,7 @@ export interface Mark {
/**
* Creates a mark to the top left of the given element with the given options.
* @param element The renderable feature to display the tooltip on.
* @param options Mark options.
* @param optionsFunc Mark options.
*/
export function addMark(
element: VueFeature,

View file

@ -37,19 +37,19 @@
import themes from "data/themes";
import settings from "game/settings";
import { Direction } from "util/common";
import { render } from "util/vue";
import type { Component } from "vue";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import type { Component, CSSProperties, MaybeRef, Ref } from "vue";
import { computed, ref, unref } from "vue";
import { Tooltip } from "./tooltip";
const props = defineProps<{
pinned?: Tooltip["pinned"];
display: Tooltip["display"];
style?: Tooltip["style"];
classes?: Tooltip["classes"];
direction: Tooltip["direction"];
xoffset?: Tooltip["xoffset"];
yoffset?: Tooltip["yoffset"];
pinned?: Ref<boolean>;
display: MaybeGetter<Renderable>;
style?: MaybeRef<CSSProperties>;
classes?: MaybeRef<Record<string, boolean>>;
direction: MaybeRef<Direction>;
xoffset?: MaybeRef<string>;
yoffset?: MaybeRef<string>;
}>();
const isHovered = ref(false);

View file

@ -49,7 +49,7 @@ export interface Tooltip extends VueFeature {
/**
* Creates a tooltip on the given element with the given options.
* @param element The renderable feature to display the tooltip on.
* @param options Tooltip options.
* @param optionsFunc Tooltip options.
*/
export function addTooltip(
element: VueFeature,

View file

@ -4,10 +4,10 @@ import {
setupUniqueIds,
unwrapNodeRef
} from "game/boards/board";
import { Direction } from "util/common";
import { beforeEach, describe, expect, test } from "vitest";
import { Ref, ref } from "vue";
import "../utils";
import { Direction } from "util/common";
describe("Unwraps node refs", () => {
test("Static value", () => expect(unwrapNodeRef(100, {})).toBe(100));