Complete the rewrite

Renderables no longer get wrapped in computed refs, because JSX.Elements don't like that (desyncs with the DOM)
Relatedly, a lot of display functions got fairly simplified, removing unnecessary local components
Added `MaybeGetter` utility type for something that may be a getter function or a static value (but not a ref)
Made Achievement.vue use a Renderable for the display. The object of components can still be passed to `createAchievement`
Made Challenge.vue use a Renderable for the display. The object of components can still be passed to `createChallenge`
Fixed some issues introduced by the rewrite that broke particles systems
This commit is contained in:
thepaperpilot 2024-12-11 13:58:14 -06:00
parent ffd9529e9a
commit ff14443102
37 changed files with 504 additions and 483 deletions

View file

@ -15,15 +15,16 @@
<script setup lang="tsx">
import "components/common/fields.css";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { MaybeRef, ref, toRef, unref, watch } from "vue";
import { ref, toRef, unref, watch } from "vue";
import VueNextSelect from "vue-next-select";
import "vue-next-select/dist/index.css";
export type SelectOption = { label: string; value: unknown };
const props = defineProps<{
title?: MaybeRef<Renderable>;
title?: MaybeGetter<Renderable>;
modelValue?: unknown;
options: SelectOption[];
placeholder?: string;

View file

@ -27,12 +27,13 @@
<script setup lang="tsx">
import "components/common/fields.css";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { computed, MaybeRef, onMounted, shallowRef, unref } from "vue";
import { computed, onMounted, shallowRef, unref } from "vue";
import VueTextareaAutosize from "vue-textarea-autosize";
const props = defineProps<{
title?: MaybeRef<Renderable>;
title?: MaybeGetter<Renderable>;
modelValue?: string;
textArea?: boolean;
placeholder?: string;

View file

@ -7,11 +7,12 @@
<script setup lang="tsx">
import "components/common/fields.css";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { computed, MaybeRef } from "vue";
import { computed } from "vue";
const props = defineProps<{
title?: MaybeRef<Renderable>;
title?: MaybeGetter<Renderable>;
modelValue?: boolean;
}>();
const emit = defineEmits<{

View file

@ -8,14 +8,15 @@
</template>
<script setup lang="ts">
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import type { MaybeRef, Ref } from "vue";
import type { Ref } from "vue";
import Col from "./Column.vue";
const props = defineProps<{
collapsed: Ref<boolean>;
display: MaybeRef<Renderable>;
content: MaybeRef<Renderable>;
display: MaybeGetter<Renderable>;
content: MaybeGetter<Renderable>;
}>();
const Display = () => render(props.display);

View file

@ -17,7 +17,7 @@ import settings from "game/settings";
import type { DecimalSource } from "util/bignum";
import Decimal, { format, formatSmall, formatTime } from "util/bignum";
import { WithRequired } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { render, Renderable, renderCol } from "util/vue";
import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from "vue";
import { computed, ref, unref } from "vue";
@ -43,7 +43,7 @@ export interface ResetButtonOptions extends ClickableOptions {
* The content to display on the button.
* By default, this includes the reset description, and amount of currency to be gained.
*/
display?: MaybeRefOrGetter<Renderable>;
display?: MaybeGetter<Renderable>;
/**
* Whether or not this button can currently be clicked.
* Defaults to checking the current gain amount is greater than {@link minimumGain}
@ -126,9 +126,8 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
Decimal.gte(unref(conversion.actualGain), unref(resetButton.minimumGain))
),
display:
processGetter(display) ??
computed(
(): JSX.Element => (
display ??
((): JSX.Element => (
<span>
{unref(resetButton.resetDescription)}
<b>
@ -156,8 +155,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
</div>
) : null}
</span>
)
),
)),
onClick: function (e?: MouseEvent | TouchEvent) {
if (unref(resetButton.canClick) === false) {
return;
@ -211,7 +209,7 @@ export function createLayerTreeNode<T extends LayerTreeNodeOptions>(optionsFunc:
return {
...(props as Omit<typeof props, keyof LayerTreeNodeOptions>),
layerID,
display: processGetter(display) ?? layerID,
display: display ?? layerID,
append: processGetter(append) ?? true,
onClick() {
if (unref<boolean>(layerTreeNode.append)) {
@ -244,7 +242,7 @@ export interface Section {
/** The unit of measurement for the base. **/
unit?: string;
/** The label to call the base amount. Defaults to "Base". **/
baseText?: MaybeRefOrGetter<Renderable>;
baseText?: MaybeGetter<Renderable>;
/** Whether or not this section should be currently visible to the player. **/
visible?: MaybeRefOrGetter<boolean>;
/** Determines if numbers larger or smaller than the base should be displayed as red. */
@ -258,12 +256,12 @@ export interface Section {
*/
export function createCollapsibleModifierSections(
sectionsFunc: () => Section[]
): [MaybeRef<Renderable>, Persistent<Record<number, boolean>>] {
): [() => Renderable, Persistent<Record<number, boolean>>] {
const sections: Section[] = [];
const processed:
| {
base: MaybeRef<DecimalSource | undefined>[];
baseText: (MaybeRef<Renderable> | undefined)[];
baseText: (MaybeGetter<Renderable> | undefined)[];
visible: MaybeRef<boolean | undefined>[];
title: MaybeRef<string | undefined>[];
subtitle: MaybeRef<string | undefined>[];
@ -274,7 +272,7 @@ export function createCollapsibleModifierSections(
if (!calculated) {
sections.push(...sectionsFunc());
processed.base = sections.map(s => processGetter(s.base));
processed.baseText = sections.map(s => processGetter(s.baseText));
processed.baseText = sections.map(s => s.baseText);
processed.visible = sections.map(s => processGetter(s.visible));
processed.title = sections.map(s => processGetter(s.title));
processed.subtitle = sections.map(s => processGetter(s.subtitle));
@ -284,7 +282,7 @@ export function createCollapsibleModifierSections(
}
const collapsed = persistent<Record<number, boolean>>({}, false);
const jsxFunc = computed(() => {
const jsxFunc = () => {
const sections = calculateSections();
let firstVisibleSection = true;
@ -364,7 +362,7 @@ export function createCollapsibleModifierSections(
);
});
return <>{sectionJSX}</>;
});
};
return [jsxFunc, collapsed];
}

View file

@ -48,8 +48,9 @@ import settings from "game/settings";
import { DecimalSource } from "lib/break_eternity";
import Decimal, { format, formatWhole } from "util/bignum";
import { Direction } from "util/common";
import { render, Renderable, renderCol, renderRow } from "util/vue";
import { Renderable, render, renderCol, renderRow } from "util/vue";
import { ComputedRef, Ref, computed, ref, unref } from "vue";
import { addMark } from "wrappers/marks/mark";
import { addTooltip } from "wrappers/tooltips/tooltip";
import f from "./f";
@ -71,7 +72,7 @@ const layer = createLayer(id, () => {
title: "Lore",
titleStyle: { color: "#FE0000" },
display: "DEEP LORE!",
bodyStyle: { "backgroundColor": "#0000EE" },
bodyStyle: { backgroundColor: "#0000EE" },
color: "rgb(75, 220, 19)"
}));
@ -121,11 +122,14 @@ const layer = createLayer(id, () => {
})),
completionLimit: 3,
display: {
description: (): Renderable => <>
Makes the game 0% harder<br/>{formatWhole(funChallenge.completions.value)}/{
funChallenge.completionLimit
} completions
</>,
description: (): Renderable => (
<>
Makes the game 0% harder
<br />
{formatWhole(funChallenge.completions.value)}/{funChallenge.completionLimit}{" "}
completions
</>
),
goal: "Have 20 points I guess",
reward: "Says hi",
effectDisplay: format(funEffect.value) + "x"
@ -147,7 +151,10 @@ const layer = createLayer(id, () => {
height: "400px"
}
}));
const funEffect = computed(() => Decimal.add(points.value, 1).tetrate(0.02));
addMark(funChallenge, () => ({
mark: funChallenge.maxed
}));
const funEffect = computed((): Decimal => Decimal.add(points.value, 1).tetrate(0.02));
const generatorUpgrade = createUpgrade(() => ({
display: {
@ -280,7 +287,7 @@ const layer = createLayer(id, () => {
spentOnBuyables.value = Decimal.sub(spentOnBuyables.value, cost);
}
}));
const buyablesDisplay = computed(() => (
const buyablesDisplay = () => (
<Column>
<Row>
<Toggle
@ -321,7 +328,7 @@ const layer = createLayer(id, () => {
}}
/>
</Column>
));
);
const longBoi = createBar(() => ({
fillStyle: { backgroundColor: "#FFFFFF" },
@ -442,12 +449,11 @@ const layer = createLayer(id, () => {
resetDescription: "Melt your points into "
}));
const resetButtonTooltip = addTooltip(resetButton, () => ({
display: computed(() =>
display: () =>
createModifierSection({
title: "Modifiers",
modifier: conversionModifier
})
),
}),
pinnable: true,
direction: Direction.Down,
style: { width: "400px", textAlign: "left" }

View file

@ -36,7 +36,13 @@ const layer = createLayer(id, () => {
const clickable = createClickable(() => ({
display: {
title: "Clicky clicky!",
description: () => <>Current state:<br/>{clickableState.value}</>
description: () => (
<>
Current state:
<br />
{clickableState.value}
</>
)
},
initialState: "Start",
canClick() {

View file

@ -18,12 +18,13 @@ import "components/common/features.css";
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 { MaybeRef, unref, type CSSProperties } from "vue";
const props = withDefaults(defineProps<{
id: string;
components: MaybeRef<Renderable>[];
components: MaybeGetter<Renderable>[];
wrappers: ((el: () => Renderable) => Renderable)[];
visibility?: MaybeRef<Visibility | boolean>;
style?: MaybeRef<CSSProperties>;

View file

@ -29,35 +29,7 @@ const props = defineProps<{
small: Achievement["small"];
}>();
const Component = () => {
if (props.display == null) {
return null;
} else if (
isRef(props.display) ||
typeof props.display === "string" ||
isJSXElement(props.display)
) {
return render(props.display);
} else {
const { requirement, effectDisplay, optionsDisplay } = props.display;
return (
<span>
{requirement ?
render(requirement, el => <h3>{el}</h3>) :
displayRequirements(props.requirements ?? [])}
{effectDisplay ? (
<div>
{render(effectDisplay, el => <b>{el}</b>)}
</div>
) : null}
{optionsDisplay != null ? (
<div class="equal-spaced">
{render(optionsDisplay)}
</div>
) : null}
</span>);
}
};
const Component = () => props.display == null ? <></> : render(props.display);
</script>
<style scoped>

View file

@ -14,7 +14,7 @@ import {
} from "game/requirements";
import settings, { registerSettingField } from "game/settings";
import { camelToTitle } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import {
isJSXElement,
@ -24,7 +24,7 @@ import {
vueFeatureMixin,
VueFeatureOptions
} from "util/vue";
import { computed, isRef, MaybeRef, MaybeRefOrGetter, unref, watchEffect } from "vue";
import { computed, MaybeRef, MaybeRefOrGetter, unref, watchEffect } from "vue";
import { useToast } from "vue-toastification";
import Achievement from "./Achievement.vue";
@ -50,14 +50,15 @@ export interface AchievementOptions extends VueFeatureOptions {
requirements?: Requirements;
/** The display to use for this achievement. */
display?:
| MaybeRefOrGetter<Renderable>
| Renderable
| (() => Renderable)
| {
/** Description of the requirement(s) for this achievement. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
requirement?: MaybeRefOrGetter<Renderable>;
requirement?: MaybeGetter<Renderable>;
/** Description of what will change (if anything) for achieving this. */
effectDisplay?: MaybeRefOrGetter<Renderable>;
effectDisplay?: MaybeGetter<Renderable>;
/** Any additional things to display on this achievement, such as a toggle for it's effect. */
optionsDisplay?: MaybeRefOrGetter<Renderable>;
optionsDisplay?: MaybeGetter<Renderable>;
};
/** Toggles a smaller design for the feature. */
small?: MaybeRefOrGetter<boolean>;
@ -76,13 +77,7 @@ export interface Achievement extends VueFeature {
/** A function that is called when the achievement is completed. */
onComplete?: VoidFunction;
/** The display to use for this achievement. */
display?:
| MaybeRef<Renderable>
| {
requirement?: MaybeRef<Renderable>;
effectDisplay?: MaybeRef<Renderable>;
optionsDisplay?: MaybeRef<Renderable>;
};
display?: MaybeGetter<Renderable>;
/** Toggles a smaller design for the feature. */
small?: MaybeRef<boolean>;
/** An image to display as the background for this achievement. */
@ -105,7 +100,15 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
const earned = persistent<boolean>(false, false);
return createLazyProxy(() => {
const options = optionsFunc?.() ?? ({} as T);
const { requirements, display, small, image, showPopups, onComplete, ...props } = options;
const {
requirements,
display: _display,
small,
image,
showPopups,
onComplete,
...props
} = options;
const vueFeature = vueFeatureMixin("achievement", options, () => (
<Achievement
@ -117,12 +120,35 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
/>
));
let display: MaybeGetter<Renderable> | undefined = undefined;
if (typeof _display === "object" && !isJSXElement(_display)) {
const { requirement, effectDisplay, optionsDisplay } = _display;
display = () => (
<span>
{requirement == null
? displayRequirements(requirements ?? [])
: render(requirement, el => <h3>{el}</h3>)}
{effectDisplay == null ? null : (
<div>
{render(effectDisplay, el => (
<b>{el}</b>
))}
</div>
)}
{optionsDisplay != null ? (
<div class="equal-spaced">{render(optionsDisplay)}</div>
) : null}
</span>
);
} else if (_display != null) {
display = _display;
}
const achievement = {
type: AchievementType,
...(props as Omit<typeof props, keyof VueFeature | keyof AchievementOptions>),
...vueFeature,
visibility: computed(() => {
const display = unref((achievement as Achievement).display);
switch (settings.msDisplay) {
default:
case AchievementDisplay.All:
@ -131,9 +157,9 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
if (
unref(earned) &&
!(
display != null &&
typeof display === "object" &&
"optionsDisplay" in display
_display != null &&
typeof _display === "object" &&
!isJSXElement(_display)
)
) {
return Visibility.None;
@ -153,19 +179,7 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
small: processGetter(small),
image: processGetter(image),
showPopups: processGetter(showPopups) ?? true,
display:
display == null
? undefined
: isRef(display) ||
typeof display === "string" ||
typeof display === "function" ||
isJSXElement(display)
? processGetter(display)
: {
requirement: processGetter(display.requirement),
effectDisplay: processGetter(display.effectDisplay),
optionsDisplay: processGetter(display.optionsDisplay)
},
display,
requirements:
requirements == null
? undefined
@ -181,19 +195,18 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
earned.value = true;
achievement.onComplete?.();
if (achievement.display != null && unref(achievement.showPopups) === true) {
const display = achievement.display;
let Display;
if (isRef(display) || typeof display === "string" || isJSXElement(display)) {
Display = () => render(display);
} else if (display.requirement != null) {
Display = () => render(display.requirement!);
let display = achievement.display;
if (typeof _display === "object" && !isJSXElement(_display)) {
if (_display.requirement != null) {
display = _display.requirement;
} else {
Display = () => displayRequirements(achievement.requirements ?? []);
display = displayRequirements(requirements ?? []);
}
}
toast.info(
<div>
<h3>Achievement earned!</h3>
<div>{Display()}</div>
<div>{render(display)}</div>
</div>
);
}

View file

@ -1,7 +1,7 @@
import Bar from "features/bars/Bar.vue";
import type { DecimalSource } from "util/bignum";
import { Direction } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { CSSProperties, MaybeRef, MaybeRefOrGetter } from "vue";
@ -30,7 +30,7 @@ export interface BarOptions extends VueFeatureOptions {
/** The progress value of the bar, from 0 to 1. */
progress: MaybeRefOrGetter<DecimalSource>;
/** The display to use for this bar. */
display?: MaybeRefOrGetter<Renderable>;
display?: MaybeGetter<Renderable>;
}
/** An object that represents a feature that displays some sort of progress or completion or resource with a cap. */
@ -52,7 +52,7 @@ export interface Bar extends VueFeature {
/** The progress value of the bar, from 0 to 1. */
progress: MaybeRef<DecimalSource>;
/** The display to use for this bar. */
display?: MaybeRef<Renderable>;
display?: MaybeGetter<Renderable>;
/** A symbol that helps identify features of the same type. */
type: typeof BarType;
}
@ -101,7 +101,7 @@ export function createBar<T extends BarOptions>(optionsFunc: () => T) {
textStyle: processGetter(textStyle),
fillStyle: processGetter(fillStyle),
progress: processGetter(progress),
display: processGetter(display)
display
} satisfies Bar;
return bar;

View file

@ -22,7 +22,6 @@
<script setup lang="tsx">
import "components/common/features.css";
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
import { displayRequirements } from "game/requirements";
import { render } from "util/vue";
import type { Component } from "vue";
import { computed, unref } from "vue";
@ -61,34 +60,7 @@ const notifyStyle = computed(() => {
return {};
});
const Component = () => {
if (props.display == null) {
return null;
}
if (typeof props.display === "object" && "description" in props.display) {
const { title, description, goal, reward, effectDisplay } = props.display;
return <span>
{title != null ? (<div>{render(title, el => <h3>{el}</h3>)}</div>) : null}
{render(description, el => <div>{el}</div>)}
<div>
<br />
Goal: {goal == null ? displayRequirements(props.requirements) : render(goal, el => <h3>{el}</h3>)}
</div>
{reward != null ? (
<div>
<br />
Reward: {render(reward)}
</div>
) : null}
{effectDisplay != null ? (
<div>
Currently: {render(effectDisplay)}
</div>
) : null}
</span>;
}
return render(props.display);
}
const Component = () => props.display == null ? <></> : render(props.display);
</script>
<style scoped>

View file

@ -4,13 +4,20 @@ import type { Reset } from "features/reset";
import { globalBus } from "game/events";
import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence";
import { Requirements, maxRequirementsMet } from "game/requirements";
import { Requirements, displayRequirements, maxRequirementsMet } from "game/requirements";
import settings, { registerSettingField } from "game/settings";
import type { DecimalSource } from "util/bignum";
import Decimal from "util/bignum";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, VueFeatureOptions, vueFeatureMixin } from "util/vue";
import {
Renderable,
VueFeature,
VueFeatureOptions,
isJSXElement,
render,
vueFeatureMixin
} from "util/vue";
import type { MaybeRef, MaybeRefOrGetter, Ref, WatchStopHandle } from "vue";
import { computed, unref, watch } from "vue";
import Challenge from "./Challenge.vue";
@ -32,18 +39,19 @@ export interface ChallengeOptions extends VueFeatureOptions {
completionLimit?: MaybeRefOrGetter<DecimalSource>;
/** The display to use for this challenge. */
display?:
| MaybeRefOrGetter<Renderable>
| Renderable
| (() => Renderable)
| {
/** A header to appear at the top of the display. */
title?: MaybeRefOrGetter<Renderable>;
title?: MaybeGetter<Renderable>;
/** The main text that appears in the display. */
description: MaybeRefOrGetter<Renderable>;
description: MaybeGetter<Renderable>;
/** A description of the current goal for this challenge. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
goal?: MaybeRefOrGetter<Renderable>;
goal?: MaybeGetter<Renderable>;
/** A description of what will change upon completing this challenge. */
reward?: MaybeRefOrGetter<Renderable>;
reward?: MaybeGetter<Renderable>;
/** A description of the current effect of this challenge. */
effectDisplay?: MaybeRefOrGetter<Renderable>;
effectDisplay?: MaybeGetter<Renderable>;
};
/** A function that is called when the challenge is completed. */
onComplete?: VoidFunction;
@ -70,20 +78,7 @@ export interface Challenge extends VueFeature {
/** The maximum number of times the challenge can be completed. */
completionLimit?: MaybeRef<DecimalSource>;
/** The display to use for this challenge. */
display?:
| MaybeRef<Renderable>
| {
/** A header to appear at the top of the display. */
title?: MaybeRef<Renderable>;
/** The main text that appears in the display. */
description: MaybeRef<Renderable>;
/** A description of the current goal for this challenge. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
goal?: MaybeRef<Renderable>;
/** A description of what will change upon completing this challenge. */
reward?: MaybeRef<Renderable>;
/** A description of the current effect of this challenge. */
effectDisplay?: MaybeRef<Renderable>;
};
display?: MaybeGetter<Renderable>;
/** The current amount of times this challenge can be completed. */
canComplete: Ref<DecimalSource>;
/** The current number of times this challenge has been completed. */
@ -118,7 +113,7 @@ export function createChallenge<T extends ChallengeOptions>(optionsFunc: () => T
requirements,
canStart,
completionLimit,
display,
display: _display,
reset,
onComplete,
onEnter,
@ -139,6 +134,41 @@ export function createChallenge<T extends ChallengeOptions>(optionsFunc: () => T
/>
));
let display: MaybeGetter<Renderable> | undefined = undefined;
if (typeof _display === "object" && !isJSXElement(_display)) {
const { title, description, goal, reward, effectDisplay } = _display;
display = () => (
<span>
{title != null ? (
<div>
{render(title, el => (
<h3>{el}</h3>
))}
</div>
) : null}
{render(description, el => (
<div>{el}</div>
))}
<div>
<br />
Goal:{" "}
{goal == null
? displayRequirements(challenge.requirements)
: render(goal, el => <h3>{el}</h3>)}
</div>
{reward != null ? (
<div>
<br />
Reward: {render(reward)}
</div>
) : null}
{effectDisplay != null ? <div>Currently: {render(effectDisplay)}</div> : null}
</span>
);
} else if (_display != null) {
display = _display;
}
const challenge = {
type: ChallengeType,
...(props as Omit<typeof props, keyof VueFeature | keyof ChallengeOptions>),
@ -157,18 +187,7 @@ export function createChallenge<T extends ChallengeOptions>(optionsFunc: () => T
onComplete,
onEnter,
onExit,
display:
display == null
? undefined
: typeof display === "object" && "description" in display
? {
title: processGetter(display.title),
description: processGetter(display.description),
goal: processGetter(display.goal),
reward: processGetter(display.reward),
effectDisplay: processGetter(display.effectDisplay)
}
: processGetter(display),
display,
toggle: function () {
if (active.value) {
if (

View file

@ -5,11 +5,10 @@ import { persistent } from "game/persistence";
import Decimal, { DecimalSource } from "lib/break_eternity";
import { Unsubscribe } from "nanoevents";
import { Direction } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { render, Renderable, VueFeature, vueFeatureMixin } from "util/vue";
import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin } from "util/vue";
import { computed, MaybeRef, MaybeRefOrGetter, Ref, ref, unref } from "vue";
import { JSX } from "vue/jsx-runtime";
import { Bar, BarOptions, createBar } from "../bars/bar";
import { type Clickable, ClickableOptions } from "./clickable";
@ -39,7 +38,7 @@ export interface Action extends VueFeature {
/** Whether or not the action may be performed. */
canClick: MaybeRef<boolean>;
/** The display to use for this action. */
display?: MaybeRef<Renderable>;
display?: MaybeGetter<Renderable>;
/** A function that is called when the action is clicked. */
onClick: (amount: DecimalSource) => void;
/** Whether or not the player is holding down the action. Actions will be considered clicked as soon as the cooldown completes when being held down. */
@ -62,8 +61,16 @@ export function createAction<T extends ActionOptions>(optionsFunc?: () => T) {
const progress = persistent<DecimalSource>(0);
return createLazyProxy(() => {
const options = optionsFunc?.() ?? ({} as T);
const { style, duration, canClick, autoStart, display, barOptions, onClick, ...props } =
options;
const {
style,
duration,
canClick,
autoStart,
display: _display,
barOptions,
onClick,
...props
} = options;
const processedCanClick = processGetter(canClick) ?? true;
const processedStyle = processGetter(style);
@ -78,29 +85,24 @@ export function createAction<T extends ActionOptions>(optionsFunc?: () => T) {
...(barOptions as Omit<typeof barOptions, keyof VueFeature>)
}));
let Component: () => JSX.Element;
if (typeof display === "object" && "description" in display) {
const title = processGetter(display.title);
const description = processGetter(display.description);
const Title = () => (title == null ? <></> : render(title, el => <h3>{el}</h3>));
const Description = () => render(description, el => <div>{el}</div>);
Component = () => {
return (
let display: MaybeGetter<Renderable>;
if (typeof _display === "object" && !isJSXElement(_display)) {
display = () => (
<span>
{title != null ? (
{_display.title != null ? (
<div>
<Title />
{render(_display.title, el => (
<h3>{el}</h3>
))}
</div>
) : null}
<Description />
{render(_display.description, el => (
<div>{el}</div>
))}
</span>
);
};
} else if (display != null) {
const processedDisplay = processGetter(display);
Component = () => render(processedDisplay);
} else if (_display != null) {
display = _display;
}
const action = {
@ -135,14 +137,14 @@ export function createAction<T extends ActionOptions>(optionsFunc?: () => T) {
unref(processedCanClick) && Decimal.gte(progress.value, unref(action.duration))
),
autoStart: processGetter(autoStart) ?? false,
display: computed(() => (
display: () => (
<>
<div style="flex-grow: 1" />
{display == null ? null : <Component />}
{display == null ? null : render(display)}
<div style="flex-grow: 1" />
{render(progressBar)}
</>
)),
),
progressBar,
onClick: function () {
if (unref(action.canClick) === false) {

View file

@ -1,9 +1,16 @@
import Clickable from "features/clickables/Clickable.vue";
import type { BaseLayer } from "game/layers";
import type { Unsubscribe } from "nanoevents";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import {
isJSXElement,
render,
Renderable,
VueFeature,
vueFeatureMixin,
VueFeatureOptions
} from "util/vue";
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
/** A symbol used to identify {@link Clickable} features. */
@ -17,12 +24,13 @@ export interface ClickableOptions extends VueFeatureOptions {
canClick?: MaybeRefOrGetter<boolean>;
/** The display to use for this clickable. */
display?:
| MaybeRefOrGetter<Renderable>
| Renderable
| (() => Renderable)
| {
/** A header to appear at the top of the display. */
title?: MaybeRefOrGetter<Renderable>;
title?: MaybeGetter<Renderable>;
/** The main text that appears in the display. */
description: MaybeRefOrGetter<Renderable>;
description: MaybeGetter<Renderable>;
};
/** A function that is called when the clickable is clicked. */
onClick?: (e?: MouseEvent | TouchEvent) => void;
@ -39,7 +47,7 @@ export interface Clickable extends VueFeature {
/** Whether or not the clickable may be clicked. */
canClick: MaybeRef<boolean>;
/** The display to use for this clickable. */
display?: MaybeRef<Renderable>;
display?: MaybeGetter<Renderable>;
/** A symbol that helps identify features of the same type. */
type: typeof ClickableType;
}
@ -53,26 +61,20 @@ export function createClickable<T extends ClickableOptions>(optionsFunc?: () =>
const options = optionsFunc?.() ?? ({} as T);
const { canClick, display: _display, onClick: onClick, onHold: onHold, ...props } = options;
let display: MaybeRef<Renderable> | undefined = undefined;
if (typeof _display === "object" && "description" in _display) {
const title = processGetter(_display.title);
const description = processGetter(_display.description);
const Title = () => (title == null ? <></> : render(title, el => <h3>{el}</h3>));
const Description = () => render(description, el => <div>{el}</div>);
display = computed(() => (
let display: MaybeGetter<Renderable> | undefined = undefined;
if (typeof _display === "object" && !isJSXElement(_display)) {
display = () => (
<span>
{title != null ? (
{_display.title != null ? (
<div>
<Title />
{render(_display.title, el => <h3>{el}</h3>)}
</div>
) : null}
<Description />
{render(_display.description, el => <div>{el}</div>)}
</span>
));
);
} else if (_display != null) {
display = processGetter(_display);
display = _display;
}
const clickable = {

View file

@ -11,11 +11,11 @@ import {
} from "game/requirements";
import type { DecimalSource } from "util/bignum";
import Decimal, { formatWhole } from "util/bignum";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin } from "util/vue";
import type { MaybeRef, MaybeRefOrGetter, Ref } from "vue";
import { computed, isRef, unref } from "vue";
import { computed, unref } from "vue";
import { ClickableOptions } from "./clickable";
/** A symbol used to identify {@link Repeatable} features. */
@ -31,14 +31,15 @@ export interface RepeatableOptions extends ClickableOptions {
initialAmount?: DecimalSource;
/** The display to use for this repeatable. */
display?:
| MaybeRefOrGetter<Renderable>
| Renderable
| (() => Renderable)
| {
/** A header to appear at the top of the display. */
title?: MaybeRefOrGetter<Renderable>;
title?: MaybeGetter<Renderable>;
/** The main text that appears in the display. */
description: MaybeRefOrGetter<Renderable>;
description: MaybeGetter<Renderable>;
/** A description of the current effect of this repeatable, based off its amount. */
effectDisplay?: MaybeRefOrGetter<Renderable>;
effectDisplay?: MaybeGetter<Renderable>;
/** Whether or not to show the current amount of this repeatable at the bottom of the display. */
showAmount?: boolean;
};
@ -53,7 +54,7 @@ export interface Repeatable extends VueFeature {
/** The initial amount this repeatable has on a new save / after reset. */
initialAmount?: DecimalSource;
/** The display to use for this repeatable. */
display?: MaybeRef<Renderable>;
display?: MaybeGetter<Renderable>;
/** Whether or not the repeatable may be clicked. */
canClick: Ref<boolean>;
/** A function that is called when the repeatable is clicked. */
@ -119,25 +120,19 @@ export function createRepeatable<T extends RepeatableOptions>(optionsFunc: () =>
}
let display;
if (typeof _display === "object" && !isRef(_display) && !isJSXElement(_display)) {
const title = processGetter(_display.title);
const description = processGetter(_display.description);
const effectDisplay = processGetter(_display.effectDisplay);
const showAmount = processGetter(_display.showAmount);
if (typeof _display === "object" && !isJSXElement(_display)) {
const { title, description, effectDisplay, showAmount } = _display;
const Title = title == null ? null : () => render(title, el => <h3>{el}</h3>);
const Description = () => render(description, el => <>{el}</>);
const EffectDisplay =
effectDisplay == null ? null : () => render(effectDisplay, el => <>{el}</>);
display = computed(() => (
display = () => (
<span>
{Title == null ? null : (
{title == null ? null : (
<div>
<Title />
{render(title, el => (
<h3>{el}</h3>
))}
</div>
)}
<Description />
{render(description)}
{showAmount === false ? null : (
<div>
<br />
@ -147,10 +142,10 @@ export function createRepeatable<T extends RepeatableOptions>(optionsFunc: () =>
) : undefined}
</div>
)}
{EffectDisplay == null ? null : (
{effectDisplay == null ? null : (
<div>
<br />
Currently: <EffectDisplay />
Currently: {render(effectDisplay)}
</div>
)}
{unref(repeatable.maxed) ? null : (
@ -160,12 +155,9 @@ export function createRepeatable<T extends RepeatableOptions>(optionsFunc: () =>
</div>
)}
</span>
));
);
} else if (_display != null) {
const processedDisplay = processGetter(_display);
display = computed(() => render(processedDisplay));
} else {
display = undefined;
display = _display;
}
amount[DefaultValue] = initialAmount ?? 0;

View file

@ -10,9 +10,16 @@ import {
requirementsMet
} from "game/requirements";
import { isFunction } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, VueFeatureOptions, render, vueFeatureMixin } from "util/vue";
import {
Renderable,
VueFeature,
VueFeatureOptions,
isJSXElement,
render,
vueFeatureMixin
} from "util/vue";
import type { MaybeRef, MaybeRefOrGetter, Ref } from "vue";
import { computed, unref } from "vue";
import Clickable from "./Clickable.vue";
@ -27,14 +34,15 @@ export const UpgradeType = Symbol("Upgrade");
export interface UpgradeOptions extends VueFeatureOptions, ClickableOptions {
/** The display to use for this upgrade. */
display?:
| MaybeRefOrGetter<Renderable>
| Renderable
| (() => Renderable)
| {
/** A header to appear at the top of the display. */
title?: MaybeRefOrGetter<Renderable>;
title?: MaybeGetter<Renderable>;
/** The main text that appears in the display. */
description: MaybeRefOrGetter<Renderable>;
description: MaybeGetter<Renderable>;
/** A description of the current effect of the achievement. Useful when the effect changes dynamically. */
effectDisplay?: MaybeRefOrGetter<Renderable>;
effectDisplay?: MaybeGetter<Renderable>;
};
/** The requirements to purchase this upgrade. */
requirements: Requirements;
@ -47,7 +55,7 @@ export interface Upgrade extends VueFeature {
/** The requirements to purchase this upgrade. */
requirements: Requirements;
/** The display to use for this upgrade. */
display?: MaybeRef<Renderable>;
display?: MaybeGetter<Renderable>;
/** Whether or not this upgrade has been purchased. */
bought: Persistent<boolean>;
/** Whether or not the upgrade can currently be purchased. */
@ -92,30 +100,23 @@ export function createUpgrade<T extends UpgradeOptions>(optionsFunc: () => T) {
requirements.push(createVisibilityRequirement(vueFeature.visibility));
}
let display: MaybeRef<Renderable> | undefined = undefined;
if (typeof _display === "object" && "description" in _display) {
const title = processGetter(_display.title);
const description = processGetter(_display.description);
const effectDisplay = processGetter(_display.effectDisplay);
let display;
if (typeof _display === "object" && !isJSXElement(_display)) {
const { title, description, effectDisplay } = _display;
const Title = () => (title == null ? <></> : render(title, el => <h3>{el}</h3>));
const Description = () => render(description, el => <div>{el}</div>);
const EffectDisplay = () =>
effectDisplay == null ? <></> : render(effectDisplay, el => <>{el}</>);
display = computed(() => (
display = () => (
<span>
{title != null ? (
<div>
<Title />
</div>
) : null}
<Description />
{effectDisplay != null ? (
<div>
Currently: <EffectDisplay />
{render(title, el => (
<h3>{el}</h3>
))}
</div>
) : null}
{render(description, el => (
<div>{el}</div>
))}
{effectDisplay != null ? <div>Currently: {render(effectDisplay)}</div> : null}
{bought.value ? null : (
<>
<br />
@ -123,9 +124,9 @@ export function createUpgrade<T extends UpgradeOptions>(optionsFunc: () => T) {
</>
)}
</span>
));
);
} else if (_display != null) {
display = processGetter(_display);
display = _display;
}
const upgrade = {

View file

@ -5,7 +5,7 @@ import type { BaseLayer } from "game/layers";
import { createBooleanRequirement } from "game/requirements";
import type { DecimalSource } from "util/bignum";
import Decimal from "util/bignum";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable } from "util/vue";
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
@ -310,7 +310,7 @@ export function setupPassiveGeneration(
export function createCanConvertRequirement(
conversion: Conversion,
minGainAmount: MaybeRefOrGetter<DecimalSource> = 1,
display?: MaybeRefOrGetter<Renderable>
display?: MaybeGetter<Renderable>
) {
const computedMinGainAmount = processGetter(minGainAmount);
return createBooleanRequirement(

View file

@ -47,9 +47,12 @@ export function findFeatures(obj: object, ...types: symbol[]): unknown[] {
const handleObject = (obj: object) => {
Object.keys(obj).forEach(key => {
const value: unknown = obj[key as keyof typeof obj];
if (value != null && typeof value === "object") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (types.includes((value as Record<string, any>).type)) {
if (
value != null &&
typeof value === "object" &&
(value as Record<string, unknown>).__v_isVNode !== true
) {
if (types.includes((value as Record<string, unknown>).type as symbol)) {
objects.push(value);
} else if (!(value instanceof Decimal) && !isRef(value)) {
handleObject(value as Record<string, unknown>);
@ -66,7 +69,7 @@ export function getFirstFeature<T extends VueFeature>(
filter: (feature: T) => boolean
): {
firstFeature: Ref<T | undefined>;
collapsedContent: MaybeRef<Renderable>;
collapsedContent: () => Renderable;
hasCollapsedContent: Ref<boolean>;
} {
const filteredFeatures = computed(() =>
@ -74,7 +77,7 @@ export function getFirstFeature<T extends VueFeature>(
);
return {
firstFeature: computed(() => filteredFeatures.value[0]),
collapsedContent: computed(() => renderCol(...filteredFeatures.value.slice(1))),
collapsedContent: () => renderCol(...filteredFeatures.value.slice(1)),
hasCollapsedContent: computed(() => filteredFeatures.value.length > 1)
};
}
@ -90,13 +93,13 @@ export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]
const handleObject = (obj: Record<string, unknown>) => {
Object.keys(obj).forEach(key => {
const value = obj[key];
if (value != null && typeof value === "object") {
if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typeof (value as Record<string, any>).type === "symbol" &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!types.includes((value as Record<string, any>).type)
value != null &&
typeof value === "object" &&
(value as Record<string, unknown>).__v_isVNode !== true
) {
const type = (value as Record<string, unknown>).type;
if (typeof type === "symbol" && !types.includes(type)) {
objects.push(value);
} else if (!(value instanceof Decimal) && !isRef(value)) {
handleObject(value as Record<string, unknown>);

View file

@ -1,16 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Column from "components/layout/Column.vue";
import Row from "components/layout/Row.vue";
import Clickable from "features/clickables/Clickable.vue";
import { getUniqueID, Visibility } from "features/feature";
import type { Persistent, State } from "game/persistence";
import { persistent } from "game/persistence";
import { isFunction } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import {
isJSXElement,
render,
Renderable,
VueFeature,
vueFeatureMixin,
VueFeatureOptions
} from "util/vue";
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
import { computed, isRef, unref } from "vue";
import Column from "components/layout/Column.vue";
import Row from "components/layout/Row.vue";
import Clickable from "features/clickables/Clickable.vue";
import { computed, unref } from "vue";
/** A symbol used to identify {@link Grid} features. */
export const GridType = Symbol("Grid");
@ -39,7 +46,7 @@ export interface GridCell extends VueFeature {
/** The persistent state of this cell. */
state: State;
/** The main text that appears in the display. */
display: MaybeRef<Renderable>;
display: MaybeGetter<Renderable>;
/** A function that is called when the cell is clicked. */
onClick?: (e?: MouseEvent | TouchEvent) => void;
/** A function that is called when the cell is held down. */
@ -65,9 +72,12 @@ export interface GridOptions extends VueFeatureOptions {
/** A getter for the CSS classes for a cell. */
getClasses?: CellMaybeRefOrGetter<Record<string, boolean>>;
/** A getter for the display component for a cell. */
getDisplay: CellMaybeRefOrGetter<Renderable> | {
getTitle?: CellMaybeRefOrGetter<Renderable>;
getDescription: CellMaybeRefOrGetter<Renderable>
getDisplay:
| Renderable
| ((row: number, col: number, state: State) => Renderable)
| {
getTitle?: Renderable | ((row: number, col: number, state: State) => Renderable);
getDescription: Renderable | ((row: number, col: number, state: State) => Renderable);
};
/** A function that is called when a cell is clicked. */
onClick?: (row: number, col: number, state: State, e?: MouseEvent | TouchEvent) => void;
@ -96,7 +106,7 @@ export interface Grid extends VueFeature {
/** A getter for the CSS classes for a cell. */
getClasses?: ProcessedCellRefOrGetter<Record<string, boolean>>;
/** A getter for the display component for a cell. */
getDisplay: ProcessedCellRefOrGetter<Renderable>;
getDisplay: Renderable | ((row: number, col: number, state: State) => Renderable);
/** Get the auto-generated ID for identifying a specific cell of this grid that appears in the DOM. Will not persist between refreshes or updates. */
getID: (row: number, col: number, state: State) => string;
/** Get the persistent state of the given cell. */
@ -214,7 +224,7 @@ function getCellHandler(grid: Grid, row: number, col: number): GridCell {
return grid.getState(row, col);
}
case "id":
return target.id = target.id ?? getUniqueID("gridcell");
return (target.id = target.id ?? getUniqueID("gridcell"));
case "components":
return [
computed(() => (
@ -264,12 +274,10 @@ function getCellHandler(grid: Grid, row: number, col: number): GridCell {
return (grid as any)[key];
},
set(target, key, value) {
console.log("!!?", key, value)
if (typeof key !== "string") {
return false;
}
key = `set${key.slice(0, 1).toUpperCase() + key.slice(1)}`;
console.log(key, grid[key])
if (key in grid && isFunction((grid as any)[key]) && (grid as any)[key].length <= 3) {
(grid as any)[key].call(grid, row, col, value);
return true;
@ -334,20 +342,23 @@ export function createGrid<T extends GridOptions>(optionsFunc: () => T) {
} = options;
let getDisplay;
if (typeof _getDisplay === "object" && !isRef(_getDisplay) && !isJSXElement(_getDisplay)) {
if (typeof _getDisplay === "object" && !isJSXElement(_getDisplay)) {
const { getTitle, getDescription } = _getDisplay;
const getProcessedTitle = convertCellMaybeRefOrGetter(getTitle);
const getProcessedDescription = convertCellMaybeRefOrGetter(getDescription);
getDisplay = function (row: number, col: number, state: State) {
const title = typeof getProcessedTitle === "function" ? getProcessedTitle(row, col, state) : unref(getProcessedTitle);
const description = typeof getProcessedDescription === "function" ? getProcessedDescription(row, col, state) : unref(getProcessedDescription);
return <>
const title = typeof getTitle === "function" ? getTitle(row, col, state) : getTitle;
const description =
typeof getDescription === "function"
? getDescription(row, col, state)
: getDescription;
return (
<>
{title}
{description}
</>;
}
</>
);
};
} else {
getDisplay = convertCellMaybeRefOrGetter(_getDisplay);
getDisplay = _getDisplay;
}
const grid = {
@ -357,10 +368,13 @@ export function createGrid<T extends GridOptions>(optionsFunc: () => T) {
<Column>
{new Array(unref(grid.rows)).fill(0).map((_, row) => (
<Row>
{new Array(unref(grid.cols)).fill(0).map((_, col) =>
render(grid.cells[row][col]))}
</Row>))}
</Column>)),
{new Array(unref(grid.cols))
.fill(0)
.map((_, col) => render(grid.cells[row][col]))}
</Row>
))}
</Column>
)),
cellState,
cells: new Proxy({} as GridCell[][], {
get(target, key: PropertyKey) {
@ -422,8 +436,10 @@ export function createGrid<T extends GridOptions>(optionsFunc: () => T) {
cols: processGetter(cols),
getVisibility: convertCellMaybeRefOrGetter(getVisibility ?? true),
getCanClick: convertCellMaybeRefOrGetter(getCanClick ?? true),
getStartState: typeof getStartState === "function" && getStartState.length > 0 ?
getStartState : processGetter(getStartState),
getStartState:
typeof getStartState === "function" && getStartState.length > 0
? getStartState
: processGetter(getStartState),
getStyle: convertCellMaybeRefOrGetter(getStyle),
getClasses: convertCellMaybeRefOrGetter(getClasses),
getDisplay,

View file

@ -1,7 +1,7 @@
import Infobox from "features/infoboxes/Infobox.vue";
import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { CSSProperties, MaybeRef, MaybeRefOrGetter } from "vue";
@ -20,9 +20,9 @@ export interface InfoboxOptions extends VueFeatureOptions {
/** CSS to apply to the body of the infobox. */
bodyStyle?: MaybeRefOrGetter<CSSProperties>;
/** A header to appear at the top of the display. */
title: MaybeRefOrGetter<Renderable>;
title: MaybeGetter<Renderable>;
/** The main text that appears in the display. */
display: MaybeRefOrGetter<Renderable>;
display: MaybeGetter<Renderable>;
}
/** An object that represents a feature that displays information in a collapsible way. */
@ -34,9 +34,9 @@ export interface Infobox extends VueFeature {
/** CSS to apply to the body of the infobox. */
bodyStyle?: MaybeRef<CSSProperties>;
/** A header to appear at the top of the display. */
title: MaybeRef<Renderable>;
title: MaybeGetter<Renderable>;
/** The main text that appears in the display. */
display: MaybeRef<Renderable>;
display: MaybeGetter<Renderable>;
/** Whether or not this infobox is collapsed. */
collapsed: Persistent<boolean>;
/** A symbol that helps identify features of the same type. */
@ -70,8 +70,8 @@ export function createInfobox<T extends InfoboxOptions>(optionsFunc: () => T) {
color: processGetter(color) ?? "--layer-color",
titleStyle: processGetter(titleStyle),
bodyStyle: processGetter(bodyStyle),
title: processGetter(title),
display: processGetter(display)
title,
display
} satisfies Infobox;
return infobox;

View file

@ -41,6 +41,7 @@ onMounted(() => {
});
onBeforeUnmount(() => {
app.value?.destroy();
app.value = null;
});
let isDirty = true;

View file

@ -3,8 +3,9 @@ import type { EmitterConfigV3 } from "@pixi/particle-emitter";
import { Emitter, upgradeConfig } from "@pixi/particle-emitter";
import { createLazyProxy } from "util/proxies";
import { VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { Ref, shallowRef } from "vue";
import { Ref, shallowRef, unref } from "vue";
import Particles from "./Particles.vue";
import { processGetter } from "util/computed";
/** A symbol used to identify {@link Particles} features. */
export const ParticlesType = Symbol("Particles");
@ -47,7 +48,10 @@ export interface Particles extends VueFeature {
export function createParticles<T extends ParticlesOptions>(optionsFunc?: () => T) {
return createLazyProxy(() => {
const options = optionsFunc?.() ?? ({} as T);
const { onContainerResized, onHotReload, ...props } = options;
const { onContainerResized, onHotReload, style: _style, ...props } = options;
const style = processGetter(_style);
options.style = () => ({ position: "static", ...(unref(style) ?? {}) });
let emittersToAdd: {
resolve: (value: Emitter | PromiseLike<Emitter>) => void;
@ -57,6 +61,7 @@ export function createParticles<T extends ParticlesOptions>(optionsFunc?: () =>
function onInit(app: Application) {
emittersToAdd.forEach(({ resolve, config }) => resolve(new Emitter(app.stage, config)));
emittersToAdd = [];
particles.app.value = app;
}
const particles = {

View file

@ -19,15 +19,16 @@ import Sticky from "components/layout/Sticky.vue";
import type { Resource } from "features/resources/resource";
import ResourceVue from "features/resources/Resource.vue";
import Decimal from "util/bignum";
import { MaybeGetter } from "util/computed";
import { Renderable } from "util/vue";
import { computed, MaybeRefOrGetter, ref, StyleValue, toValue } from "vue";
import { computed, ref, StyleValue, toValue } from "vue";
const props = defineProps<{
resource: Resource;
color?: string;
classes?: Record<string, boolean>;
style?: StyleValue;
effectDisplay?: MaybeRefOrGetter<Renderable>;
effectDisplay?: MaybeGetter<Renderable>;
}>();
const displayRef = ref<Element | null>(null);

View file

@ -1,8 +1,6 @@
import { processGetter } from "util/computed";
import { MaybeGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { MaybeRef, MaybeRefOrGetter } from "vue";
import { JSX } from "vue/jsx-runtime";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
/** A symbol used to identify {@link Tab} features. */
export const TabType = Symbol("Tab");
@ -12,7 +10,7 @@ export const TabType = Symbol("Tab");
*/
export interface TabOptions extends VueFeatureOptions {
/** The display to use for this tab. */
display: MaybeRefOrGetter<Renderable>;
display: MaybeGetter<Renderable>;
}
/**
@ -21,7 +19,7 @@ export interface TabOptions extends VueFeatureOptions {
*/
export interface Tab extends VueFeature {
/** The display to use for this tab. */
display: MaybeRef<Renderable>;
display: MaybeGetter<Renderable>;
/** A symbol that helps identify features of the same type. */
type: typeof TabType;
}
@ -38,8 +36,8 @@ export function createTab<T extends TabOptions>(optionsFunc: () => T) {
const tab = {
type: TabType,
...(props as Omit<typeof props, keyof VueFeature | keyof TabOptions>),
...vueFeatureMixin("tab", options, (): JSX.Element => render(tab.display)),
display: processGetter(display)
...vueFeatureMixin("tab", options, display),
display
} satisfies Tab;
return tab;

View file

@ -4,7 +4,7 @@ import TabButton from "features/tabs/TabButton.vue";
import TabFamily from "features/tabs/TabFamily.vue";
import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
@ -20,9 +20,9 @@ export const TabFamilyType = Symbol("TabFamily");
*/
export interface TabButtonOptions extends VueFeatureOptions {
/** The tab to display when this button is clicked. */
tab: Tab | MaybeRefOrGetter<Renderable>;
tab: Tab | MaybeGetter<Renderable>;
/** The label on this button. */
display: MaybeRefOrGetter<Renderable>;
display: MaybeGetter<Renderable>;
/** The color of the glow effect to display when this button is active. */
glowColor?: MaybeRefOrGetter<string>;
}
@ -33,9 +33,9 @@ export interface TabButtonOptions extends VueFeatureOptions {
*/
export interface TabButton extends VueFeature {
/** The tab to display when this button is clicked. */
tab: Tab | MaybeRef<Renderable>;
tab: Tab | MaybeGetter<Renderable>;
/** The label on this button. */
display: MaybeRef<Renderable>;
display: MaybeGetter<Renderable>;
/** The color of the glow effect to display when this button is active. */
glowColor?: MaybeRef<string>;
/** A symbol that helps identify features of the same type. */
@ -64,7 +64,7 @@ export interface TabFamily extends VueFeature {
/** All the tabs within this family. */
tabs: Record<string, TabButton>;
/** The currently active tab, if any. */
activeTab: Ref<Tab | MaybeRef<Renderable> | null>;
activeTab: Ref<Tab | MaybeGetter<Renderable> | null>;
/** The name of the tab that is currently active. */
selected: Persistent<string>;
/** A symbol that helps identify features of the same type. */
@ -106,16 +106,17 @@ export function createTabFamily<T extends TabFamilyOptions>(
const tabButton = {
type: TabButtonType,
...(props as Omit<typeof props, keyof VueFeature | keyof TabButtonOptions>),
...vueFeatureMixin("tabButton", options, () =>
...vueFeatureMixin("tabButton", options, () => (
<TabButton
display={tabButton.display}
glowColor={tabButton.glowColor}
active={unref(tabButton.tab) === unref(tabFamily.activeTab)}
onSelectTab={() => tabFamily.selected.value = tab}
/>),
tab: processGetter(buttonTab),
onSelectTab={() => (tabFamily.selected.value = tab)}
/>
)),
tab: buttonTab,
glowColor: processGetter(glowColor),
display: processGetter(display)
display
} satisfies TabButton;
parsedTabs[tab] = tabButton;
@ -124,7 +125,7 @@ export function createTabFamily<T extends TabFamilyOptions>(
buttonContainerClasses: processGetter(buttonContainerClasses),
buttonContainerStyle: processGetter(buttonContainerStyle),
selected,
activeTab: computed((): Tab | MaybeRef<Renderable> | null => {
activeTab: computed((): Tab | MaybeGetter<Renderable> | null => {
if (
selected.value in tabFamily.tabs &&
isVisible(tabFamily.tabs[selected.value].visibility ?? true)

View file

@ -7,11 +7,11 @@ import TreeNode from "features/trees/TreeNode.vue";
import { noPersist } from "game/persistence";
import type { DecimalSource } from "util/bignum";
import Decimal, { format, formatWhole } from "util/bignum";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import type { MaybeRef, MaybeRefOrGetter, Ref } from "vue";
import { computed, ref, shallowRef, unref } from "vue";
import { ref, shallowRef, unref } from "vue";
/** A symbol used to identify {@link TreeNode} features. */
export const TreeNodeType = Symbol("TreeNode");
@ -27,7 +27,7 @@ export interface TreeNodeOptions extends VueFeatureOptions {
/** The background color for this node. */
color?: MaybeRefOrGetter<string>;
/** The label to display on this tree node. */
display?: MaybeRefOrGetter<Renderable>;
display?: MaybeGetter<Renderable>;
/** The color of the glow effect shown to notify the user there's something to do with this node. */
glowColor?: MaybeRefOrGetter<string>;
/** A reset object attached to this node, used for propagating resets through the tree. */
@ -47,7 +47,7 @@ export interface TreeNode extends VueFeature {
/** The background color for this node. */
color?: MaybeRef<string>;
/** The label to display on this tree node. */
display?: MaybeRef<Renderable>;
display?: MaybeGetter<Renderable>;
/** The color of the glow effect shown to notify the user there's something to do with this node. */
glowColor?: MaybeRef<string>;
/** A reset object attached to this node, used for propagating resets through the tree. */
@ -84,7 +84,7 @@ export function createTreeNode<T extends TreeNodeOptions>(optionsFunc?: () => T)
)),
canClick: processGetter(canClick) ?? true,
color: processGetter(color),
display: processGetter(display),
display,
glowColor: processGetter(glowColor),
onClick:
onClick == null
@ -265,9 +265,9 @@ export function createResourceTooltip(
resource: Resource,
requiredResource: Resource | null = null,
requirement: MaybeRefOrGetter<DecimalSource> = 0
): Ref<string> {
): () => string {
const req = processGetter(requirement);
return computed(() => {
return () => {
if (requiredResource == null || Decimal.gte(resource.value, unref(req))) {
return displayResource(resource) + " " + resource.displayName;
}
@ -280,5 +280,5 @@ export function createResourceTooltip(
? formatWhole(requiredResource.value)
: format(requiredResource.value, requiredResource.precision)
})`;
});
};
}

View file

@ -275,7 +275,18 @@ export function makeDraggable<T, S extends MakeDraggableOptions<T>>(
const position = persistent<NodePosition>({ x: 0, y: 0 });
const draggable = createLazyProxy(() => {
const options = optionsFunc();
const { id, nodeBeingDragged, hasDragged, dragDelta, startDrag, endDrag, onMouseDown, onMouseUp, initialPosition, ...props } = options;
const {
id,
nodeBeingDragged,
hasDragged,
dragDelta,
startDrag,
endDrag,
onMouseDown,
onMouseUp,
initialPosition,
...props
} = options;
position[DefaultValue] = initialPosition ?? position[DefaultValue];

View file

@ -5,9 +5,9 @@ import { persistent } from "game/persistence";
import player from "game/player";
import type { Emitter } from "nanoevents";
import { createNanoEvents } from "nanoevents";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { render, Renderable } from "util/vue";
import { Renderable } from "util/vue";
import {
computed,
type CSSProperties,
@ -108,7 +108,7 @@ export interface LayerOptions {
* 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.
*/
display: MaybeRefOrGetter<Renderable>;
display: MaybeGetter<Renderable>;
/** An object of classes that should be applied to the display. */
classes?: MaybeRefOrGetter<Record<string, boolean>>;
/** Styles that should be applied to the display. */
@ -127,7 +127,7 @@ export interface LayerOptions {
* 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.
*/
minimizedDisplay?: MaybeRefOrGetter<Renderable>;
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}.
@ -169,7 +169,7 @@ export interface Layer extends BaseLayer {
* 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.
*/
display: MaybeRef<Renderable>;
display: MaybeGetter<Renderable>;
/** An object of classes that should be applied to the display. */
classes?: MaybeRef<Record<string, boolean>>;
/** Styles that should be applied to the display. */
@ -188,7 +188,7 @@ export interface Layer extends BaseLayer {
* 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.
*/
minimizedDisplay?: MaybeRef<Renderable>;
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}.
@ -261,7 +261,7 @@ export function createLayer<T extends LayerOptions>(
...baseLayer,
...(props as Omit<typeof props, keyof LayerOptions>),
color: processGetter(color),
display: processGetter(display),
display,
classes: processGetter(classes),
style: computed((): CSSProperties => {
let width = unref(layer.minWidth);
@ -293,7 +293,7 @@ export function createLayer<T extends LayerOptions>(
forceHideGoBack: processGetter(forceHideGoBack),
minWidth: processGetter(minWidth) ?? 600,
minimizable: processGetter(minimizable) ?? true,
minimizedDisplay: processGetter(minimizedDisplay)
minimizedDisplay
} satisfies Layer;
return layer;
@ -370,21 +370,21 @@ export function reloadLayer(layer: Layer): void {
*/
export function setupLayerModal(layer: Layer): {
openModal: VoidFunction;
modal: Ref<JSX.Element>;
modal: () => JSX.Element;
} {
const showModal = ref(false);
return {
openModal: () => (showModal.value = true),
modal: computed(() => (
modal: () => (
<Modal
modelValue={showModal.value}
onUpdate:modelValue={value => (showModal.value = value)}
v-slots={{
header: () => <h2>{unref(layer.name)}</h2>,
body: () => render(layer.display)
body: typeof layer.display ? layer.display : () => layer.display
}}
/>
))
)
};
}

View file

@ -3,7 +3,7 @@ import settings from "game/settings";
import type { DecimalSource } from "util/bignum";
import Decimal, { formatSmall } from "util/bignum";
import type { RequiredKeys, WithRequired } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { render, Renderable } from "util/vue";
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
@ -32,7 +32,7 @@ export interface Modifier {
* A description of this modifier.
* @see {@link createModifierSection}.
*/
description?: MaybeRef<Renderable>;
description?: MaybeGetter<Renderable>;
}
/** Utility type that represents the output of all modifiers that represent a single operation. */
@ -46,7 +46,7 @@ export interface AdditiveModifierOptions {
/** The amount to add to the input value. */
addend: MaybeRefOrGetter<DecimalSource>;
/** Description of what this modifier is doing. */
description?: MaybeRefOrGetter<Renderable>;
description?: MaybeGetter<Renderable>;
/** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
enabled?: MaybeRefOrGetter<boolean>;
/** Determines if numbers larger or smaller than 0 should be displayed as red. */
@ -64,7 +64,6 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
const { addend, description, enabled, smallerIsBetter } = optionsFunc();
const processedAddend = processGetter(addend);
const processedDescription = processGetter(description);
const processedEnabled = enabled == null ? undefined : processGetter(enabled);
return {
apply: (gain: DecimalSource) => Decimal.add(gain, unref(processedAddend)),
@ -72,13 +71,11 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
getFormula: (gain: FormulaSource) => Formula.add(gain, processedAddend),
enabled: processedEnabled,
description:
processedDescription == null
description == null
? undefined
: computed(() => (
: () => (
<div class="modifier-container">
<span class="modifier-description">
{render(processedDescription)}
</span>
<span class="modifier-description">{render(description)}</span>
<span
class="modifier-amount"
style={
@ -95,7 +92,7 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
{formatSmall(unref(processedAddend))}
</span>
</div>
))
)
};
}) as S;
}
@ -105,7 +102,7 @@ export interface MultiplicativeModifierOptions {
/** The amount to multiply the input value by. */
multiplier: MaybeRefOrGetter<DecimalSource>;
/** Description of what this modifier is doing. */
description?: MaybeRefOrGetter<Renderable> | undefined;
description?: MaybeGetter<Renderable> | undefined;
/** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
enabled?: MaybeRefOrGetter<boolean> | undefined;
/** Determines if numbers larger or smaller than 1 should be displayed as red. */
@ -124,7 +121,6 @@ export function createMultiplicativeModifier<
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc();
const processedMultiplier = processGetter(multiplier);
const processedDescription = processGetter(description);
const processedEnabled = enabled == null ? undefined : processGetter(enabled);
return {
apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)),
@ -132,13 +128,11 @@ export function createMultiplicativeModifier<
getFormula: (gain: FormulaSource) => Formula.times(gain, processedMultiplier),
enabled: processedEnabled,
description:
processedDescription == null
description == null
? undefined
: computed(() => (
: () => (
<div class="modifier-container">
<span class="modifier-description">
{render(processedDescription)}
</span>
<span class="modifier-description">{render(description)}</span>
<span
class="modifier-amount"
style={
@ -154,7 +148,7 @@ export function createMultiplicativeModifier<
×{formatSmall(unref(processedMultiplier))}
</span>
</div>
))
)
};
}) as S;
}
@ -164,7 +158,7 @@ export interface ExponentialModifierOptions {
/** The amount to raise the input value to the power of. */
exponent: MaybeRefOrGetter<DecimalSource>;
/** Description of what this modifier is doing. */
description?: MaybeRefOrGetter<Renderable> | undefined;
description?: MaybeGetter<Renderable> | undefined;
/** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
enabled?: MaybeRefOrGetter<boolean> | undefined;
/** Add 1 before calculating, then remove it afterwards. This prevents low numbers from becoming lower. */
@ -186,7 +180,6 @@ export function createExponentialModifier<
optionsFunc();
const processedExponent = processGetter(exponent);
const processedDescription = processGetter(description);
const processedEnabled = enabled == null ? undefined : processGetter(enabled);
return {
apply: (gain: DecimalSource) => {
@ -217,12 +210,12 @@ export function createExponentialModifier<
: Formula.pow(gain, processedExponent),
enabled: processedEnabled,
description:
processedDescription == null
description == null
? undefined
: computed(() => (
: () => (
<div class="modifier-container">
<span class="modifier-description">
{render(processedDescription)}
{render(description)}
{supportLowNumbers ? " (+1 effective)" : null}
</span>
<span
@ -240,7 +233,7 @@ export function createExponentialModifier<
^{formatSmall(unref(processedExponent))}
</span>
</div>
))
)
};
}) as S;
}
@ -286,14 +279,13 @@ export function createSequentialModifier<
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)
: undefined,
description: modifiers.some(m => m.description != null)
? computed(() =>
? () =>
(
modifiers
.filter(m => unref(m.enabled) !== false)
.map(m => unref(m.description))
.filter(d => d) as MaybeRef<Renderable>[]
.filter(d => d) as MaybeGetter<Renderable>[]
).map(m => render(m))
)
: undefined
};
}) as S;
@ -312,7 +304,7 @@ export interface ModifierSectionOptions {
/** The unit of the value being modified, if any. */
unit?: string;
/** The label to use for the base value. Defaults to "Base". */
baseText?: MaybeRefOrGetter<Renderable>;
baseText?: MaybeGetter<Renderable>;
/** Determines if numbers larger or smaller than the base should be displayed as red. */
smallerIsBetter?: boolean;
}
@ -332,7 +324,6 @@ export function createModifierSection({
smallerIsBetter
}: ModifierSectionOptions) {
const total = modifier.apply(base ?? 1);
const processedBaseText = processGetter(baseText);
return (
<div style={{ "--unit": settings.alignUnits && unit != null ? "'" + unit + "'" : "" }}>
<h3>
@ -341,7 +332,7 @@ export function createModifierSection({
</h3>
<br />
<div class="modifier-container">
<span class="modifier-description">{render(processedBaseText ?? "Base")}</span>
<span class="modifier-description">{render(baseText ?? "Base")}</span>
<span class="modifier-amount">
{formatSmall(base ?? 1)}
{unit}

View file

@ -1,9 +1,9 @@
import { isVisible, Visibility } from "features/feature";
import { displayResource, Resource } from "features/resources/resource";
import Decimal, { DecimalSource } from "lib/break_eternity";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { joinJSX, render, Renderable } from "util/vue";
import { joinJSX, Renderable } from "util/vue";
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
import type { GenericFormula, InvertibleIntegralFormula } from "./formulas/types";
@ -262,16 +262,16 @@ export function createVisibilityRequirement(
*/
export function createBooleanRequirement(
requirement: MaybeRefOrGetter<boolean>,
display?: MaybeRefOrGetter<Renderable>
display?: MaybeGetter<Renderable>
): Requirement {
return createLazyProxy(() => {
const processedDisplay = processGetter(display);
const partialDisplay =
display == null ? undefined : typeof display === "function" ? display : () => display;
return {
requirementMet: processGetter(requirement),
partialDisplay: processedDisplay == null ? undefined : () => render(processedDisplay),
display:
processedDisplay == null ? undefined : () => <>Req: {render(processedDisplay)}</>,
visibility: processedDisplay == null ? Visibility.None : Visibility.Visible,
partialDisplay,
display: display == null ? undefined : () => <>Req: {partialDisplay}</>,
visibility: display == null ? Visibility.None : Visibility.Visible,
requiresPay: false
};
});

View file

@ -2,10 +2,10 @@ import projInfo from "data/projInfo.json";
import { Themes } from "data/themes";
import { globalBus } from "game/events";
import LZString from "lz-string";
import { processGetter } from "util/computed";
import { MaybeGetter } from "util/computed";
import { decodeSave, hardReset } from "util/save";
import { Renderable } from "util/vue";
import { MaybeRef, MaybeRefOrGetter, reactive, watch } from "vue";
import { reactive, watch } from "vue";
/** The player's settings object. */
export interface Settings {
@ -101,22 +101,22 @@ export function loadSettings(): void {
}
/** A list of fields to append to the settings modal. */
export const settingFields: MaybeRef<Renderable>[] = reactive([]);
export const settingFields: MaybeGetter<Renderable>[] = reactive([]);
/** Register a field to be displayed in the settings modal. */
export function registerSettingField(component: MaybeRefOrGetter<Renderable>) {
settingFields.push(processGetter(component));
export function registerSettingField(component: MaybeGetter<Renderable>) {
settingFields.push(component);
}
/** A list of components to show in the info modal. */
export const infoComponents: MaybeRef<Renderable>[] = reactive([]);
export const infoComponents: MaybeGetter<Renderable>[] = reactive([]);
/** Register a component to be displayed in the info modal. */
export function registerInfoComponent(component: MaybeRefOrGetter<Renderable>) {
infoComponents.push(processGetter(component));
export function registerInfoComponent(component: MaybeGetter<Renderable>) {
infoComponents.push(component);
}
/** A list of components to add to the root of the page. */
export const gameComponents: MaybeRef<Renderable>[] = reactive([]);
export const gameComponents: MaybeGetter<Renderable>[] = reactive([]);
/** Register a component to be displayed at the root of the page. */
export function registerGameComponent(component: MaybeRefOrGetter<Renderable>) {
gameComponents.push(processGetter(component));
export function registerGameComponent(component: MaybeGetter<Renderable>) {
gameComponents.push(component);
}

View file

@ -2,6 +2,8 @@ import { isFunction } from "util/common";
import type { ComputedRef } from "vue";
import { computed } from "vue";
export type MaybeGetter<T> = T | (() => T);
export function processGetter<T>(obj: T): T extends () => infer S ? ComputedRef<S> : T {
if (isFunction(obj)) {
return computed(obj) as ReturnType<typeof processGetter<T>>;

View file

@ -6,9 +6,9 @@ import Col from "components/layout/Column.vue";
import Row from "components/layout/Row.vue";
import { getUniqueID, Visibility } from "features/feature";
import VueFeatureComponent from "features/VueFeature.vue";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
import { isRef, onUnmounted, ref, unref } from "vue";
import { isRef, onUnmounted, ref, toValue } from "vue";
import { JSX } from "vue/jsx-runtime";
import { camelToKebab } from "./common";
@ -35,7 +35,7 @@ export interface VueFeature {
/** CSS to apply to this feature. */
style?: MaybeRef<CSSProperties>;
/** The components to render inside the vue feature */
components: MaybeRef<Renderable>[];
components: MaybeGetter<Renderable>[];
/** The components to render wrapped around the vue feature */
wrappers: ((el: () => Renderable) => Renderable)[];
/** Used to identify Vue Features */
@ -45,14 +45,14 @@ export interface VueFeature {
export function vueFeatureMixin(
featureName: string,
options: VueFeatureOptions,
component?: MaybeRefOrGetter<Renderable>
component?: MaybeGetter<Renderable>
) {
return {
id: getUniqueID(featureName),
visibility: processGetter(options.visibility),
classes: processGetter(options.classes),
style: processGetter(options.style),
components: component == null ? [] : [processGetter(component)],
components: component == null ? [] : [component],
wrappers: [] as ((el: () => Renderable) => Renderable)[],
[VueFeature]: true
} satisfies VueFeature;
@ -60,15 +60,15 @@ export function vueFeatureMixin(
export function render(object: VueFeature, wrapper?: (el: Renderable) => Renderable): JSX.Element;
export function render<T extends Renderable>(
object: MaybeRef<Renderable>,
object: MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => T
): T;
export function render(
object: VueFeature | MaybeRef<Renderable>,
object: VueFeature | MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => Renderable
): Renderable;
export function render(
object: VueFeature | MaybeRef<Renderable>,
object: VueFeature | MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => Renderable
) {
if (typeof object === "object" && VueFeature in object) {
@ -85,20 +85,24 @@ export function render(
);
}
object = unref(object);
object = toValue(object);
return wrapper?.(object) ?? object;
}
export function renderRow(...objects: (VueFeature | MaybeRef<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 | MaybeRef<Renderable>)[]): JSX.Element {
export function renderCol(
...objects: (VueFeature | MaybeGetter<Renderable>)[]
): JSX.Element {
return <Col>{objects.map(obj => render(obj))}</Col>;
}
export function joinJSX(
objects: (VueFeature | MaybeRef<Renderable>)[],
objects: (VueFeature | MaybeGetter<Renderable>)[],
joiner: JSX.Element
): JSX.Element {
return objects.reduce<JSX.Element>(

View file

@ -1,4 +1,3 @@
import { type OptionsFunc } from "features/feature";
import { processGetter } from "util/computed";
import { createLazyProxy, runAfterEvaluation } from "util/proxies";
import type { VueFeature } from "util/vue";
@ -23,9 +22,9 @@ export interface Mark {
* @param element The renderable feature to display the tooltip on.
* @param options Mark options.
*/
export function addMark<T extends MarkOptions>(
export function addMark(
element: VueFeature,
optionsFunc: OptionsFunc<T, Mark>
optionsFunc: () => MarkOptions
): asserts element is VueFeature & { mark: Mark } {
const mark = createLazyProxy(() => {
const options = optionsFunc();

View file

@ -1,7 +1,7 @@
import { isVisible, type OptionsFunc } from "features/feature";
import { isVisible } from "features/feature";
import { deletePersistent, persistent } from "game/persistence";
import { Direction } from "util/common";
import { processGetter } from "util/computed";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy, runAfterEvaluation } from "util/proxies";
import { Renderable, vueFeatureMixin, type VueFeature, type VueFeatureOptions } from "util/vue";
import { MaybeRef, MaybeRefOrGetter, type Ref } from "vue";
@ -21,7 +21,7 @@ export interface TooltipOptions extends VueFeatureOptions {
/** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
pinnable?: boolean;
/** The text to display inside the tooltip. */
display: MaybeRefOrGetter<Renderable>;
display: MaybeGetter<Renderable>;
/** The direction in which to display the tooltip */
direction?: MaybeRefOrGetter<Direction>;
/** The x offset of the tooltip, in px. */
@ -35,7 +35,7 @@ export interface Tooltip extends VueFeature {
/** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
pinnable?: boolean;
/** The text to display inside the tooltip. */
display: MaybeRef<Renderable>;
display: MaybeGetter<Renderable>;
/** The direction in which to display the tooltip */
direction?: MaybeRef<Direction>;
/** The x offset of the tooltip, in px. */
@ -51,9 +51,9 @@ export interface Tooltip extends VueFeature {
* @param element The renderable feature to display the tooltip on.
* @param options Tooltip options.
*/
export function addTooltip<T extends TooltipOptions>(
export function addTooltip(
element: VueFeature,
optionsFunc: OptionsFunc<T, Tooltip>
optionsFunc: () => TooltipOptions
): asserts element is VueFeature & { tooltip: Tooltip } {
const pinned = persistent<boolean>(false, false);
const tooltip = createLazyProxy(() => {
@ -69,7 +69,7 @@ export function addTooltip<T extends TooltipOptions>(
...vueFeatureMixin("tooltip", options),
pinnable: pinnable ?? true,
pinned: pinnable === false ? undefined : pinned,
display: processGetter(display),
display,
direction: processGetter(direction ?? Direction.Up),
xoffset: processGetter(xoffset),
yoffset: processGetter(yoffset)

View file

@ -9,15 +9,16 @@ import {
} from "game/modifiers";
import Decimal, { DecimalSource } from "util/bignum";
import { WithRequired } from "util/common";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue";
import { beforeAll, describe, expect, test } from "vitest";
import { MaybeRefOrGetter, Ref, ref, unref } from "vue";
import "../utils";
import { render, Renderable } from "util/vue";
export type ModifierConstructorOptions = {
[S in "addend" | "multiplier" | "exponent"]: MaybeRefOrGetter<DecimalSource>;
} & {
description?: MaybeRefOrGetter<Renderable>;
description?: MaybeGetter<Renderable>;
enabled?: MaybeRefOrGetter<boolean>;
smallerIsBetter?: boolean;
};