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 ## [0.7.0] - 2024-12-31
### Additions ### Additions
- Added modal to take a mental health break (can be disabled via projInfo.json) - 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 ### 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** Rewrote how features are written, simplifying them greatly
- **BREAKING** Replaced decorators with mixins and wrappers - **BREAKING** Replaced decorators with mixins and wrappers
- **BREAKING** Moved modals to `src/components/modals` - **BREAKING** Moved modals to `src/components/modals`
- **BREAKING** Updated a very large amount of dependencies, making any necessary adjustments - **BREAKING** Updated a very large amount of dependencies, making any necessary adjustments
- **BREAKING** Removed Grid component - **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 ### Fixes
- Hotkey descriptions were not being wrapped in `unref` - Hotkey descriptions were not being wrapped in `unref`
- Links wouldn't check if the end node existed when determining valid links - 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 Contributors: thepaperpilot

View file

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

View file

@ -88,7 +88,7 @@
</ul> </ul>
</div> </div>
</div> </div>
<Info ref="info" :changelog="changelog" /> <Info ref="info" @open-changelog="changelog?.open()" />
<SavesManager ref="savesManager" /> <SavesManager ref="savesManager" />
<Options ref="options" /> <Options ref="options" />
<Changelog ref="changelog" /> <Changelog ref="changelog" />
@ -97,22 +97,19 @@
<script setup lang="ts"> <script setup lang="ts">
import Changelog from "data/Changelog.vue"; import Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import Tooltip from "wrappers/tooltips/Tooltip.vue";
import settings from "game/settings"; import settings from "game/settings";
import { Direction } from "util/common"; import { Direction } from "util/common";
import { galaxy, syncedSaves } from "util/galaxy"; import { galaxy, syncedSaves } from "util/galaxy";
import type { ComponentPublicInstance } from "vue";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import Tooltip from "wrappers/tooltips/Tooltip.vue";
import Info from "./modals/Info.vue"; import Info from "./modals/Info.vue";
import Options from "./modals/Options.vue"; import Options from "./modals/Options.vue";
import SavesManager from "./modals/SavesManager.vue"; import SavesManager from "./modals/SavesManager.vue";
const info = ref<ComponentPublicInstance<typeof Info> | null>(null); const info = ref<typeof Info | null>(null);
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null); const savesManager = ref<typeof SavesManager | null>(null);
const options = ref<ComponentPublicInstance<typeof Options> | null>(null); const options = ref<typeof Options | null>(null);
// For some reason Info won't accept the changelog unless I do this: const changelog = ref<typeof Changelog | null>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changelog = ref<ComponentPublicInstance<any> | null>(null);
const { useHeader, banner, title, discordName, discordLink, versionNumber } = projInfo; 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 Made in Profectus, by thepaperpilot with inspiration from Acameada and Jacorb
</div> </div>
<br /> <br />
<div class="link" @click="openChangelog">Changelog</div> <div class="link" @click="emits('openChangelog')">Changelog</div>
<br /> <br />
<div> <div>
<a <a
@ -53,14 +53,13 @@
</div> </div>
<br /> <br />
<div>Time Played: {{ timePlayed }}</div> <div>Time Played: {{ timePlayed }}</div>
<Info /> <InfoComponents />
</div> </div>
</template> </template>
</Modal> </Modal>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import type Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import player from "game/player"; import player from "game/player";
import { infoComponents } from "game/settings"; import { infoComponents } from "game/settings";
@ -71,23 +70,21 @@ import Modal from "./Modal.vue";
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo; 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 isOpen = ref(false);
const timePlayed = computed(() => formatTime(player.timePlayed)); const timePlayed = computed(() => formatTime(player.timePlayed));
const Info = () => infoComponents.map(f => render(f)); const InfoComponents = () => infoComponents.map(f => render(f));
defineExpose({ defineExpose({
open() { open() {
isOpen.value = true; isOpen.value = true;
} }
}); });
function openChangelog() {
props.changelog?.open();
}
</script> </script>
<style scoped> <style scoped>

View file

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

View file

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

View file

@ -72,6 +72,7 @@ import {
decodeSave, decodeSave,
getCachedSave, getCachedSave,
getUniqueID, getUniqueID,
LoadablePlayerData,
loadSave, loadSave,
newSave, newSave,
save save
@ -84,8 +85,6 @@ import Text from "../fields/Text.vue";
import Modal from "./Modal.vue"; import Modal from "./Modal.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
const isOpen = ref(false); const isOpen = ref(false);
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null); 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 type { Visibility } from "features/feature";
import { isHidden, isVisible } from "features/feature"; import { isHidden, isVisible } from "features/feature";
import { MaybeGetter } from "util/computed"; 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"; import { MaybeRef, unref, type CSSProperties } from "vue";
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
id: string; id: string;
components: MaybeGetter<Renderable>[]; components: MaybeGetter<Renderable>[];
wrappers: ((el: () => Renderable) => Renderable)[]; wrappers: Wrapper[];
visibility?: MaybeRef<Visibility | boolean>; visibility?: MaybeRef<Visibility | boolean>;
style?: MaybeRef<CSSProperties>; style?: MaybeRef<CSSProperties>;
classes?: MaybeRef<Record<string, boolean>>; classes?: MaybeRef<Record<string, boolean>>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,16 +23,35 @@ export enum Visibility {
None 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>) { export function isVisible(visibility: MaybeRef<Visibility | boolean>) {
const currVisibility = unref(visibility); const currVisibility = unref(visibility);
return currVisibility !== Visibility.None && currVisibility !== false; 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>) { export function isHidden(visibility: MaybeRef<Visibility | boolean>) {
const currVisibility = unref(visibility); const currVisibility = unref(visibility);
return currVisibility === Visibility.Hidden; 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 } { 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; 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; 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>( export function getFirstFeature<T extends VueFeature>(
features: T[], features: T[],
filter: (feature: T) => boolean filter: (feature: T) => boolean

View file

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

View file

@ -15,11 +15,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FeatureNode } from "game/layers"; import type { FeatureNode } from "game/layers";
import { BoundsInjectionKey, NodesInjectionKey } 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 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() { function updateBounds() {
boundingRect.value = resizeListener.value?.getBoundingClientRect(); boundingRect.value = resizeListener.value?.getBoundingClientRect();

View file

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

View file

@ -18,7 +18,7 @@ import { isRef, MaybeRef, MaybeRefOrGetter, unref } from "vue";
export const ResetType = Symbol("Reset"); 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 { export interface ResetOptions {
/** List of things to reset. Can include objects which will be recursed over for persistent values. */ /** 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 Decimal from "util/bignum";
import { MaybeGetter } from "util/computed"; import { MaybeGetter } from "util/computed";
import { Renderable } from "util/vue"; import { Renderable } from "util/vue";
import { computed, ref, StyleValue, toValue } from "vue"; import { computed, CSSProperties, ref, toValue } from "vue";
const props = defineProps<{ const props = defineProps<{
resource: Resource; resource: Resource;
color?: string; color?: string;
classes?: Record<string, boolean>; classes?: Record<string, boolean>;
style?: StyleValue; style?: CSSProperties;
effectDisplay?: MaybeGetter<Renderable>; effectDisplay?: MaybeGetter<Renderable>;
}>(); }>();

View file

@ -5,16 +5,16 @@
</template> </template>
<script setup lang="ts"> <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 themes from "data/themes";
import { getNotifyStyle } from "game/notifications";
import settings from "game/settings"; 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<{ const props = defineProps<{
display: TabButton["display"]; display: MaybeGetter<Renderable>;
glowColor: TabButton["glowColor"]; glowColor?: MaybeRef<string>;
active?: boolean; active?: boolean;
}>(); }>();

View file

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

View file

@ -15,7 +15,7 @@ export interface TabOptions extends VueFeatureOptions {
/** /**
* An object representing a tab of content in a tabbed interface. * 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 { export interface Tab extends VueFeature {
/** The display to use for this tab. */ /** The display to use for this tab. */

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,6 @@ export interface GlobalEvents {
* Sent when constructing the {@link Settings} object. * Sent when constructing the {@link Settings} object.
* Use it to add default values for custom properties to the object. * Use it to add default values for custom properties to the object.
* @param settings The settings object being constructed. * @param settings The settings object being constructed.
* @see {@link features/features.setDefault} for setting default values.
*/ */
loadSettings: (settings: Partial<Settings>) => void; 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. * 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. * 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 calculateMaxAffordable}
* @see {@link /game/requirements.createCostRequirement} * @see {@link game/requirements.createCostRequirement}
*/ */
export default class Formula< export default class Formula<
T extends [FormulaSource] | FormulaSource[] 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> = export const RegisterNodeInjectionKey: InjectionKey<(id: string, element: HTMLElement) => void> =
Symbol("RegisterNode"); 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> = export const UnregisterNodeInjectionKey: InjectionKey<(id: string) => void> =
Symbol("UnregisterNode"); 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>>> = export const NodesInjectionKey: InjectionKey<Ref<Record<string, FeatureNode | undefined>>> =
Symbol("Nodes"); 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"); export const BoundsInjectionKey: InjectionKey<Ref<DOMRect | undefined>> = Symbol("Bounds");
@ -106,7 +106,7 @@ export interface LayerOptions {
color?: MaybeRefOrGetter<string>; color?: MaybeRefOrGetter<string>;
/** /**
* The layout of this layer's features. * 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>; display: MaybeGetter<Renderable>;
/** An object of classes that should be applied to the display. */ /** An object of classes that should be applied to the display. */
@ -125,12 +125,12 @@ export interface LayerOptions {
minimizable?: MaybeRefOrGetter<boolean>; minimizable?: MaybeRefOrGetter<boolean>;
/** /**
* The layout of this layer's features. * 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>; minimizedDisplay?: MaybeGetter<Renderable>;
/** /**
* Whether or not to force the go back button to be hidden. * 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>; forceHideGoBack?: MaybeRefOrGetter<boolean>;
/** /**
@ -157,7 +157,7 @@ export interface BaseLayer {
on: OmitThisParameter<Emitter<LayerEvents>["on"]>; on: OmitThisParameter<Emitter<LayerEvents>["on"]>;
/** A function to emit a {@link LayerEvents} event to this layer. */ /** A function to emit a {@link LayerEvents} event to this layer. */
emit: <K extends keyof LayerEvents>(...args: [K, ...Parameters<LayerEvents[K]>]) => void; 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>>; nodes: Ref<Record<string, FeatureNode | undefined>>;
} }
@ -167,7 +167,7 @@ export interface Layer extends BaseLayer {
color?: MaybeRef<string>; color?: MaybeRef<string>;
/** /**
* The layout of this layer's features. * 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>; display: MaybeGetter<Renderable>;
/** An object of classes that should be applied to the display. */ /** An object of classes that should be applied to the display. */
@ -186,12 +186,12 @@ export interface Layer extends BaseLayer {
minimizable?: MaybeRef<boolean>; minimizable?: MaybeRef<boolean>;
/** /**
* The layout of this layer's features. * 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>; minimizedDisplay?: MaybeGetter<Renderable>;
/** /**
* Whether or not to force the go back button to be hidden. * 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>; forceHideGoBack?: MaybeRef<boolean>;
/** /**

View file

@ -1,15 +1,14 @@
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import { processGetter } from "util/computed"; import { processGetter } from "util/computed";
import { trackHover, VueFeature } from "util/vue"; import { trackHover, VueFeature } from "util/vue";
import { nextTick, Ref } from "vue"; import { CSSProperties, nextTick, Ref, ref, watch } from "vue";
import { ref, watch } from "vue";
import Toast from "vue-toastification"; import Toast from "vue-toastification";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
globalBus.on("setupVue", vue => vue.use(Toast)); 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. * Default values are for a "soft" white notif effect.
* @param color The color of the glow effect. * @param color The color of the glow effect.
* @param strength The strength of the glow effect - affects its spread. * @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)", borderColor: "rgba(0, 0, 0, 0.125)",
boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 ${strength} ${color}`, boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 ${strength} ${color}`,
zIndex: 1 zIndex: 1
}; } satisfies CSSProperties;
} }
/** Utility function to call {@link getNotifyStyle} with "high importance" parameters. */ /** Utility function to call {@link getNotifyStyle} with "high importance" parameters. */

View file

@ -67,7 +67,7 @@ export type State =
| { [key: number]: 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> & { export type Persistent<T extends State = State> = Ref<T> & {
value: T; value: T;

View file

@ -1,6 +1,6 @@
import { isVisible, Visibility } from "features/feature"; import { isVisible, Visibility } from "features/feature";
import { displayResource, Resource } from "features/resources/resource"; 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 { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { joinJSX, Renderable } from "util/vue"; 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 * 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( export function createVisibilityRequirement(
visibility: MaybeRef<Visibility | boolean> visibility: MaybeRef<Visibility | boolean>

View file

@ -77,7 +77,7 @@ export const hardResetSettings = (window.hardResetSettings = () => {
/** /**
* Loads the player settings from localStorage. * 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 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}. * 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 type { App as VueApp } from "vue";
import { createApp, nextTick } from "vue"; import { createApp, nextTick } from "vue";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import { globalBus } from "./game/events"; import { globalBus } from "game/events";
import { startGameLoop } from "./game/gameLoop"; import { startGameLoop } from "game/gameLoop";
declare global { declare global {
/** /**

View file

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

View file

@ -3,8 +3,12 @@ import { NonPersistent } from "game/persistence";
export const ProxyState = Symbol("ProxyState"); export const ProxyState = Symbol("ProxyState");
export const AfterEvaluation = Symbol("AfterEvaluation"); 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>( export function createLazyProxy<T extends object, S extends T>(
objectFunc: (this: S, baseObject: S) => T, objectFunc: (this: S, baseObject: S) => T,
baseObject: S = {} as S baseObject: S = {} as S
@ -74,6 +78,11 @@ export function createLazyProxy<T extends object, S extends T>(
}) as S & 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) { export function runAfterEvaluation<T extends object>(maybeProxy: T, callback: (object: T) => void) {
if (AfterEvaluation in maybeProxy) { if (AfterEvaluation in maybeProxy) {
(maybeProxy[AfterEvaluation] as (callback: (object: T) => void) => void)(callback); (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 { fixOldSave, getInitialLayers } from "data/projEntry";
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
@ -9,6 +8,8 @@ import settings, { loadSettings } from "game/settings";
import LZString from "lz-string"; import LZString from "lz-string";
import { ref, shallowReactive } from "vue"; import { ref, shallowReactive } from "vue";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
export function setupInitialStore(player: Partial<Player> = {}): Player { export function setupInitialStore(player: Partial<Player> = {}): Player {
return Object.assign( return Object.assign(
{ {

View file

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

View file

@ -37,19 +37,19 @@
import themes from "data/themes"; import themes from "data/themes";
import settings from "game/settings"; import settings from "game/settings";
import { Direction } from "util/common"; import { Direction } from "util/common";
import { render } from "util/vue"; import { MaybeGetter } from "util/computed";
import type { Component } from "vue"; import { render, Renderable } from "util/vue";
import type { Component, CSSProperties, MaybeRef, Ref } from "vue";
import { computed, ref, unref } from "vue"; import { computed, ref, unref } from "vue";
import { Tooltip } from "./tooltip";
const props = defineProps<{ const props = defineProps<{
pinned?: Tooltip["pinned"]; pinned?: Ref<boolean>;
display: Tooltip["display"]; display: MaybeGetter<Renderable>;
style?: Tooltip["style"]; style?: MaybeRef<CSSProperties>;
classes?: Tooltip["classes"]; classes?: MaybeRef<Record<string, boolean>>;
direction: Tooltip["direction"]; direction: MaybeRef<Direction>;
xoffset?: Tooltip["xoffset"]; xoffset?: MaybeRef<string>;
yoffset?: Tooltip["yoffset"]; yoffset?: MaybeRef<string>;
}>(); }>();
const isHovered = ref(false); 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. * Creates a tooltip on the given element with the given options.
* @param element The renderable feature to display the tooltip on. * @param element The renderable feature to display the tooltip on.
* @param options Tooltip options. * @param optionsFunc Tooltip options.
*/ */
export function addTooltip( export function addTooltip(
element: VueFeature, element: VueFeature,

View file

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