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"> <script setup lang="tsx">
import "components/common/fields.css"; import "components/common/fields.css";
import { MaybeGetter } from "util/computed";
import { render, Renderable } from "util/vue"; 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 VueNextSelect from "vue-next-select";
import "vue-next-select/dist/index.css"; import "vue-next-select/dist/index.css";
export type SelectOption = { label: string; value: unknown }; export type SelectOption = { label: string; value: unknown };
const props = defineProps<{ const props = defineProps<{
title?: MaybeRef<Renderable>; title?: MaybeGetter<Renderable>;
modelValue?: unknown; modelValue?: unknown;
options: SelectOption[]; options: SelectOption[];
placeholder?: string; placeholder?: string;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,35 +29,7 @@ const props = defineProps<{
small: Achievement["small"]; small: Achievement["small"];
}>(); }>();
const Component = () => { const Component = () => props.display == null ? <></> : render(props.display);
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>);
}
};
</script> </script>
<style scoped> <style scoped>

View file

@ -14,7 +14,7 @@ import {
} from "game/requirements"; } from "game/requirements";
import settings, { registerSettingField } from "game/settings"; import settings, { registerSettingField } from "game/settings";
import { camelToTitle } from "util/common"; import { camelToTitle } from "util/common";
import { processGetter } from "util/computed"; import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { import {
isJSXElement, isJSXElement,
@ -24,7 +24,7 @@ import {
vueFeatureMixin, vueFeatureMixin,
VueFeatureOptions VueFeatureOptions
} from "util/vue"; } 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 { useToast } from "vue-toastification";
import Achievement from "./Achievement.vue"; import Achievement from "./Achievement.vue";
@ -50,14 +50,15 @@ export interface AchievementOptions extends VueFeatureOptions {
requirements?: Requirements; requirements?: Requirements;
/** The display to use for this achievement. */ /** The display to use for this achievement. */
display?: 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}. */ /** 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. */ /** 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. */ /** 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. */ /** Toggles a smaller design for the feature. */
small?: MaybeRefOrGetter<boolean>; small?: MaybeRefOrGetter<boolean>;
@ -76,13 +77,7 @@ export interface Achievement extends VueFeature {
/** A function that is called when the achievement is completed. */ /** A function that is called when the achievement is completed. */
onComplete?: VoidFunction; onComplete?: VoidFunction;
/** The display to use for this achievement. */ /** The display to use for this achievement. */
display?: display?: MaybeGetter<Renderable>;
| MaybeRef<Renderable>
| {
requirement?: MaybeRef<Renderable>;
effectDisplay?: MaybeRef<Renderable>;
optionsDisplay?: MaybeRef<Renderable>;
};
/** Toggles a smaller design for the feature. */ /** Toggles a smaller design for the feature. */
small?: MaybeRef<boolean>; small?: MaybeRef<boolean>;
/** An image to display as the background for this achievement. */ /** 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); const earned = persistent<boolean>(false, false);
return createLazyProxy(() => { return createLazyProxy(() => {
const options = optionsFunc?.() ?? ({} as T); 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, () => ( const vueFeature = vueFeatureMixin("achievement", options, () => (
<Achievement <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 = { const achievement = {
type: AchievementType, type: AchievementType,
...(props as Omit<typeof props, keyof VueFeature | keyof AchievementOptions>), ...(props as Omit<typeof props, keyof VueFeature | keyof AchievementOptions>),
...vueFeature, ...vueFeature,
visibility: computed(() => { visibility: computed(() => {
const display = unref((achievement as Achievement).display);
switch (settings.msDisplay) { switch (settings.msDisplay) {
default: default:
case AchievementDisplay.All: case AchievementDisplay.All:
@ -131,9 +157,9 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
if ( if (
unref(earned) && unref(earned) &&
!( !(
display != null && _display != null &&
typeof display === "object" && typeof _display === "object" &&
"optionsDisplay" in display !isJSXElement(_display)
) )
) { ) {
return Visibility.None; return Visibility.None;
@ -153,19 +179,7 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
small: processGetter(small), small: processGetter(small),
image: processGetter(image), image: processGetter(image),
showPopups: processGetter(showPopups) ?? true, showPopups: processGetter(showPopups) ?? true,
display: 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)
},
requirements: requirements:
requirements == null requirements == null
? undefined ? undefined
@ -181,19 +195,18 @@ export function createAchievement<T extends AchievementOptions>(optionsFunc?: ()
earned.value = true; earned.value = true;
achievement.onComplete?.(); achievement.onComplete?.();
if (achievement.display != null && unref(achievement.showPopups) === true) { if (achievement.display != null && unref(achievement.showPopups) === true) {
const display = achievement.display; let display = achievement.display;
let Display; if (typeof _display === "object" && !isJSXElement(_display)) {
if (isRef(display) || typeof display === "string" || isJSXElement(display)) { if (_display.requirement != null) {
Display = () => render(display); display = _display.requirement;
} else if (display.requirement != null) { } else {
Display = () => render(display.requirement!); display = displayRequirements(requirements ?? []);
} else { }
Display = () => displayRequirements(achievement.requirements ?? []);
} }
toast.info( toast.info(
<div> <div>
<h3>Achievement earned!</h3> <h3>Achievement earned!</h3>
<div>{Display()}</div> <div>{render(display)}</div>
</div> </div>
); );
} }

View file

@ -1,7 +1,7 @@
import Bar from "features/bars/Bar.vue"; import Bar from "features/bars/Bar.vue";
import type { DecimalSource } from "util/bignum"; import type { DecimalSource } from "util/bignum";
import { Direction } from "util/common"; import { Direction } from "util/common";
import { processGetter } from "util/computed"; import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue"; import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { CSSProperties, MaybeRef, MaybeRefOrGetter } from "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. */ /** The progress value of the bar, from 0 to 1. */
progress: MaybeRefOrGetter<DecimalSource>; progress: MaybeRefOrGetter<DecimalSource>;
/** The display to use for this bar. */ /** 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. */ /** 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. */ /** The progress value of the bar, from 0 to 1. */
progress: MaybeRef<DecimalSource>; progress: MaybeRef<DecimalSource>;
/** The display to use for this bar. */ /** The display to use for this bar. */
display?: MaybeRef<Renderable>; display?: MaybeGetter<Renderable>;
/** A symbol that helps identify features of the same type. */ /** A symbol that helps identify features of the same type. */
type: typeof BarType; type: typeof BarType;
} }
@ -101,7 +101,7 @@ export function createBar<T extends BarOptions>(optionsFunc: () => T) {
textStyle: processGetter(textStyle), textStyle: processGetter(textStyle),
fillStyle: processGetter(fillStyle), fillStyle: processGetter(fillStyle),
progress: processGetter(progress), progress: processGetter(progress),
display: processGetter(display) display
} satisfies Bar; } satisfies Bar;
return bar; return bar;

View file

@ -22,7 +22,6 @@
<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 { displayRequirements } from "game/requirements";
import { render } from "util/vue"; import { render } from "util/vue";
import type { Component } from "vue"; import type { Component } from "vue";
import { computed, unref } from "vue"; import { computed, unref } from "vue";
@ -61,34 +60,7 @@ const notifyStyle = computed(() => {
return {}; return {};
}); });
const Component = () => { const Component = () => props.display == null ? <></> : render(props.display);
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);
}
</script> </script>
<style scoped> <style scoped>

View file

@ -4,13 +4,20 @@ import type { Reset } from "features/reset";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import type { Persistent } from "game/persistence"; import type { Persistent } from "game/persistence";
import { 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 settings, { registerSettingField } from "game/settings";
import type { DecimalSource } from "util/bignum"; import type { DecimalSource } from "util/bignum";
import Decimal 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 { 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 type { MaybeRef, MaybeRefOrGetter, Ref, WatchStopHandle } from "vue";
import { computed, unref, watch } from "vue"; import { computed, unref, watch } from "vue";
import Challenge from "./Challenge.vue"; import Challenge from "./Challenge.vue";
@ -32,18 +39,19 @@ export interface ChallengeOptions extends VueFeatureOptions {
completionLimit?: MaybeRefOrGetter<DecimalSource>; completionLimit?: MaybeRefOrGetter<DecimalSource>;
/** The display to use for this challenge. */ /** The display to use for this challenge. */
display?: display?:
| MaybeRefOrGetter<Renderable> | Renderable
| (() => Renderable)
| { | {
/** A header to appear at the top of the display. */ /** A header to appear at the top of the display. */
title?: MaybeRefOrGetter<Renderable>; title?: MaybeGetter<Renderable>;
/** The main text that appears in the display. */ /** 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}. */ /** 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. */ /** 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. */ /** 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. */ /** A function that is called when the challenge is completed. */
onComplete?: VoidFunction; onComplete?: VoidFunction;
@ -70,20 +78,7 @@ export interface Challenge extends VueFeature {
/** The maximum number of times the challenge can be completed. */ /** The maximum number of times the challenge can be completed. */
completionLimit?: MaybeRef<DecimalSource>; completionLimit?: MaybeRef<DecimalSource>;
/** The display to use for this challenge. */ /** The display to use for this challenge. */
display?: display?: MaybeGetter<Renderable>;
| 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>;
};
/** The current amount of times this challenge can be completed. */ /** The current amount of times this challenge can be completed. */
canComplete: Ref<DecimalSource>; canComplete: Ref<DecimalSource>;
/** The current number of times this challenge has been completed. */ /** The current number of times this challenge has been completed. */
@ -118,7 +113,7 @@ export function createChallenge<T extends ChallengeOptions>(optionsFunc: () => T
requirements, requirements,
canStart, canStart,
completionLimit, completionLimit,
display, display: _display,
reset, reset,
onComplete, onComplete,
onEnter, 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 = { const challenge = {
type: ChallengeType, type: ChallengeType,
...(props as Omit<typeof props, keyof VueFeature | keyof ChallengeOptions>), ...(props as Omit<typeof props, keyof VueFeature | keyof ChallengeOptions>),
@ -157,18 +187,7 @@ export function createChallenge<T extends ChallengeOptions>(optionsFunc: () => T
onComplete, onComplete,
onEnter, onEnter,
onExit, onExit,
display: 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),
toggle: function () { toggle: function () {
if (active.value) { if (active.value) {
if ( if (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import Infobox from "features/infoboxes/Infobox.vue"; import Infobox from "features/infoboxes/Infobox.vue";
import type { Persistent } from "game/persistence"; import type { Persistent } from "game/persistence";
import { 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 { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue"; import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { CSSProperties, MaybeRef, MaybeRefOrGetter } from "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. */ /** CSS to apply to the body of the infobox. */
bodyStyle?: MaybeRefOrGetter<CSSProperties>; bodyStyle?: MaybeRefOrGetter<CSSProperties>;
/** A header to appear at the top of the display. */ /** A header to appear at the top of the display. */
title: MaybeRefOrGetter<Renderable>; title: MaybeGetter<Renderable>;
/** The main text that appears in the display. */ /** 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. */ /** 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. */ /** CSS to apply to the body of the infobox. */
bodyStyle?: MaybeRef<CSSProperties>; bodyStyle?: MaybeRef<CSSProperties>;
/** A header to appear at the top of the display. */ /** A header to appear at the top of the display. */
title: MaybeRef<Renderable>; title: MaybeGetter<Renderable>;
/** The main text that appears in the display. */ /** The main text that appears in the display. */
display: MaybeRef<Renderable>; display: MaybeGetter<Renderable>;
/** Whether or not this infobox is collapsed. */ /** Whether or not this infobox is collapsed. */
collapsed: Persistent<boolean>; collapsed: Persistent<boolean>;
/** A symbol that helps identify features of the same type. */ /** 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", color: processGetter(color) ?? "--layer-color",
titleStyle: processGetter(titleStyle), titleStyle: processGetter(titleStyle),
bodyStyle: processGetter(bodyStyle), bodyStyle: processGetter(bodyStyle),
title: processGetter(title), title,
display: processGetter(display) display
} satisfies Infobox; } satisfies Infobox;
return infobox; return infobox;

View file

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

View file

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

View file

@ -19,15 +19,16 @@ import Sticky from "components/layout/Sticky.vue";
import type { Resource } from "features/resources/resource"; import type { Resource } from "features/resources/resource";
import ResourceVue from "features/resources/Resource.vue"; import ResourceVue from "features/resources/Resource.vue";
import Decimal from "util/bignum"; import Decimal from "util/bignum";
import { MaybeGetter } from "util/computed";
import { Renderable } from "util/vue"; import { Renderable } from "util/vue";
import { computed, MaybeRefOrGetter, ref, StyleValue, toValue } from "vue"; import { computed, ref, StyleValue, 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?: StyleValue;
effectDisplay?: MaybeRefOrGetter<Renderable>; effectDisplay?: MaybeGetter<Renderable>;
}>(); }>();
const displayRef = ref<Element | null>(null); 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 { createLazyProxy } from "util/proxies";
import { render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue"; import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { MaybeRef, MaybeRefOrGetter } from "vue";
import { JSX } from "vue/jsx-runtime";
/** A symbol used to identify {@link Tab} features. */ /** A symbol used to identify {@link Tab} features. */
export const TabType = Symbol("Tab"); export const TabType = Symbol("Tab");
@ -12,7 +10,7 @@ export const TabType = Symbol("Tab");
*/ */
export interface TabOptions extends VueFeatureOptions { export interface TabOptions extends VueFeatureOptions {
/** The display to use for this tab. */ /** 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 { export interface Tab extends VueFeature {
/** The display to use for this tab. */ /** The display to use for this tab. */
display: MaybeRef<Renderable>; display: MaybeGetter<Renderable>;
/** A symbol that helps identify features of the same type. */ /** A symbol that helps identify features of the same type. */
type: typeof TabType; type: typeof TabType;
} }
@ -38,8 +36,8 @@ export function createTab<T extends TabOptions>(optionsFunc: () => T) {
const tab = { const tab = {
type: TabType, type: TabType,
...(props as Omit<typeof props, keyof VueFeature | keyof TabOptions>), ...(props as Omit<typeof props, keyof VueFeature | keyof TabOptions>),
...vueFeatureMixin("tab", options, (): JSX.Element => render(tab.display)), ...vueFeatureMixin("tab", options, display),
display: processGetter(display) display
} satisfies Tab; } satisfies Tab;
return tab; return tab;

View file

@ -4,7 +4,7 @@ import TabButton from "features/tabs/TabButton.vue";
import TabFamily from "features/tabs/TabFamily.vue"; import TabFamily from "features/tabs/TabFamily.vue";
import type { Persistent } from "game/persistence"; import type { Persistent } from "game/persistence";
import { 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 { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue"; import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue"; import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
@ -20,9 +20,9 @@ export const TabFamilyType = Symbol("TabFamily");
*/ */
export interface TabButtonOptions extends VueFeatureOptions { export interface TabButtonOptions extends VueFeatureOptions {
/** The tab to display when this button is clicked. */ /** The tab to display when this button is clicked. */
tab: Tab | MaybeRefOrGetter<Renderable>; tab: Tab | MaybeGetter<Renderable>;
/** The label on this button. */ /** The label on this button. */
display: MaybeRefOrGetter<Renderable>; display: MaybeGetter<Renderable>;
/** The color of the glow effect to display when this button is active. */ /** The color of the glow effect to display when this button is active. */
glowColor?: MaybeRefOrGetter<string>; glowColor?: MaybeRefOrGetter<string>;
} }
@ -33,9 +33,9 @@ export interface TabButtonOptions extends VueFeatureOptions {
*/ */
export interface TabButton extends VueFeature { export interface TabButton extends VueFeature {
/** The tab to display when this button is clicked. */ /** The tab to display when this button is clicked. */
tab: Tab | MaybeRef<Renderable>; tab: Tab | MaybeGetter<Renderable>;
/** The label on this button. */ /** The label on this button. */
display: MaybeRef<Renderable>; display: MaybeGetter<Renderable>;
/** The color of the glow effect to display when this button is active. */ /** The color of the glow effect to display when this button is active. */
glowColor?: MaybeRef<string>; glowColor?: MaybeRef<string>;
/** A symbol that helps identify features of the same type. */ /** 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. */ /** All the tabs within this family. */
tabs: Record<string, TabButton>; tabs: Record<string, TabButton>;
/** The currently active tab, if any. */ /** 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. */ /** The name of the tab that is currently active. */
selected: Persistent<string>; selected: Persistent<string>;
/** A symbol that helps identify features of the same type. */ /** A symbol that helps identify features of the same type. */
@ -106,16 +106,17 @@ export function createTabFamily<T extends TabFamilyOptions>(
const tabButton = { const tabButton = {
type: TabButtonType, type: TabButtonType,
...(props as Omit<typeof props, keyof VueFeature | keyof TabButtonOptions>), ...(props as Omit<typeof props, keyof VueFeature | keyof TabButtonOptions>),
...vueFeatureMixin("tabButton", options, () => ...vueFeatureMixin("tabButton", options, () => (
<TabButton <TabButton
display={tabButton.display} display={tabButton.display}
glowColor={tabButton.glowColor} glowColor={tabButton.glowColor}
active={unref(tabButton.tab) === unref(tabFamily.activeTab)} active={unref(tabButton.tab) === unref(tabFamily.activeTab)}
onSelectTab={() => tabFamily.selected.value = tab} onSelectTab={() => (tabFamily.selected.value = tab)}
/>), />
tab: processGetter(buttonTab), )),
tab: buttonTab,
glowColor: processGetter(glowColor), glowColor: processGetter(glowColor),
display: processGetter(display) display
} satisfies TabButton; } satisfies TabButton;
parsedTabs[tab] = tabButton; parsedTabs[tab] = tabButton;
@ -124,7 +125,7 @@ export function createTabFamily<T extends TabFamilyOptions>(
buttonContainerClasses: processGetter(buttonContainerClasses), buttonContainerClasses: processGetter(buttonContainerClasses),
buttonContainerStyle: processGetter(buttonContainerStyle), buttonContainerStyle: processGetter(buttonContainerStyle),
selected, selected,
activeTab: computed((): Tab | MaybeRef<Renderable> | null => { activeTab: computed((): Tab | MaybeGetter<Renderable> | null => {
if ( if (
selected.value in tabFamily.tabs && selected.value in tabFamily.tabs &&
isVisible(tabFamily.tabs[selected.value].visibility ?? true) 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 { noPersist } from "game/persistence";
import type { DecimalSource } from "util/bignum"; import type { DecimalSource } from "util/bignum";
import Decimal, { format, formatWhole } 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 { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue"; import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import type { MaybeRef, MaybeRefOrGetter, Ref } from "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. */ /** A symbol used to identify {@link TreeNode} features. */
export const TreeNodeType = Symbol("TreeNode"); export const TreeNodeType = Symbol("TreeNode");
@ -27,7 +27,7 @@ export interface TreeNodeOptions extends VueFeatureOptions {
/** The background color for this node. */ /** The background color for this node. */
color?: MaybeRefOrGetter<string>; color?: MaybeRefOrGetter<string>;
/** The label to display on this tree node. */ /** 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. */ /** The color of the glow effect shown to notify the user there's something to do with this node. */
glowColor?: MaybeRefOrGetter<string>; glowColor?: MaybeRefOrGetter<string>;
/** A reset object attached to this node, used for propagating resets through the tree. */ /** 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. */ /** The background color for this node. */
color?: MaybeRef<string>; color?: MaybeRef<string>;
/** The label to display on this tree node. */ /** 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. */ /** The color of the glow effect shown to notify the user there's something to do with this node. */
glowColor?: MaybeRef<string>; glowColor?: MaybeRef<string>;
/** A reset object attached to this node, used for propagating resets through the tree. */ /** 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, canClick: processGetter(canClick) ?? true,
color: processGetter(color), color: processGetter(color),
display: processGetter(display), display,
glowColor: processGetter(glowColor), glowColor: processGetter(glowColor),
onClick: onClick:
onClick == null onClick == null
@ -265,9 +265,9 @@ export function createResourceTooltip(
resource: Resource, resource: Resource,
requiredResource: Resource | null = null, requiredResource: Resource | null = null,
requirement: MaybeRefOrGetter<DecimalSource> = 0 requirement: MaybeRefOrGetter<DecimalSource> = 0
): Ref<string> { ): () => string {
const req = processGetter(requirement); const req = processGetter(requirement);
return computed(() => { return () => {
if (requiredResource == null || Decimal.gte(resource.value, unref(req))) { if (requiredResource == null || Decimal.gte(resource.value, unref(req))) {
return displayResource(resource) + " " + resource.displayName; return displayResource(resource) + " " + resource.displayName;
} }
@ -280,5 +280,5 @@ export function createResourceTooltip(
? formatWhole(requiredResource.value) ? formatWhole(requiredResource.value)
: format(requiredResource.value, requiredResource.precision) : 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 position = persistent<NodePosition>({ x: 0, y: 0 });
const draggable = createLazyProxy(() => { const draggable = createLazyProxy(() => {
const options = optionsFunc(); 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]; position[DefaultValue] = initialPosition ?? position[DefaultValue];
@ -323,7 +334,7 @@ export function makeDraggable<T, S extends MakeDraggableOptions<T>>(
runAfterEvaluation(element, el => { runAfterEvaluation(element, el => {
draggable.id; // Ensure draggable gets evaluated draggable.id; // Ensure draggable gets evaluated
(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} mouseDown={draggable.onMouseDown}

View file

@ -5,9 +5,9 @@ import { persistent } from "game/persistence";
import player from "game/player"; import player from "game/player";
import type { Emitter } from "nanoevents"; import type { Emitter } from "nanoevents";
import { createNanoEvents } from "nanoevents"; import { createNanoEvents } from "nanoevents";
import { processGetter } from "util/computed"; import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { render, Renderable } from "util/vue"; import { Renderable } from "util/vue";
import { import {
computed, computed,
type CSSProperties, type CSSProperties,
@ -108,7 +108,7 @@ export interface LayerOptions {
* 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.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. */ /** An object of classes that should be applied to the display. */
classes?: MaybeRefOrGetter<Record<string, boolean>>; classes?: MaybeRefOrGetter<Record<string, boolean>>;
/** Styles that should be applied to the display. */ /** Styles that should be applied to the display. */
@ -127,7 +127,7 @@ export interface LayerOptions {
* 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.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. * 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 {@link data/projInfo.allowGoBack}.
@ -169,7 +169,7 @@ export interface Layer extends BaseLayer {
* 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.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. */ /** An object of classes that should be applied to the display. */
classes?: MaybeRef<Record<string, boolean>>; classes?: MaybeRef<Record<string, boolean>>;
/** Styles that should be applied to the display. */ /** Styles that should be applied to the display. */
@ -188,7 +188,7 @@ export interface Layer extends BaseLayer {
* 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.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. * 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 {@link data/projInfo.allowGoBack}.
@ -261,7 +261,7 @@ export function createLayer<T extends LayerOptions>(
...baseLayer, ...baseLayer,
...(props as Omit<typeof props, keyof LayerOptions>), ...(props as Omit<typeof props, keyof LayerOptions>),
color: processGetter(color), color: processGetter(color),
display: processGetter(display), display,
classes: processGetter(classes), classes: processGetter(classes),
style: computed((): CSSProperties => { style: computed((): CSSProperties => {
let width = unref(layer.minWidth); let width = unref(layer.minWidth);
@ -293,7 +293,7 @@ export function createLayer<T extends LayerOptions>(
forceHideGoBack: processGetter(forceHideGoBack), forceHideGoBack: processGetter(forceHideGoBack),
minWidth: processGetter(minWidth) ?? 600, minWidth: processGetter(minWidth) ?? 600,
minimizable: processGetter(minimizable) ?? true, minimizable: processGetter(minimizable) ?? true,
minimizedDisplay: processGetter(minimizedDisplay) minimizedDisplay
} satisfies Layer; } satisfies Layer;
return layer; return layer;
@ -370,21 +370,21 @@ export function reloadLayer(layer: Layer): void {
*/ */
export function setupLayerModal(layer: Layer): { export function setupLayerModal(layer: Layer): {
openModal: VoidFunction; openModal: VoidFunction;
modal: Ref<JSX.Element>; modal: () => JSX.Element;
} { } {
const showModal = ref(false); const showModal = ref(false);
return { return {
openModal: () => (showModal.value = true), openModal: () => (showModal.value = true),
modal: computed(() => ( modal: () => (
<Modal <Modal
modelValue={showModal.value} modelValue={showModal.value}
onUpdate:modelValue={value => (showModal.value = value)} onUpdate:modelValue={value => (showModal.value = value)}
v-slots={{ v-slots={{
header: () => <h2>{unref(layer.name)}</h2>, 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 type { DecimalSource } from "util/bignum";
import Decimal, { formatSmall } from "util/bignum"; import Decimal, { formatSmall } from "util/bignum";
import type { RequiredKeys, WithRequired } from "util/common"; import type { RequiredKeys, WithRequired } from "util/common";
import { processGetter } from "util/computed"; import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { render, Renderable } from "util/vue"; import { render, Renderable } from "util/vue";
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue"; import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
@ -32,7 +32,7 @@ export interface Modifier {
* A description of this modifier. * A description of this modifier.
* @see {@link createModifierSection}. * @see {@link createModifierSection}.
*/ */
description?: MaybeRef<Renderable>; description?: MaybeGetter<Renderable>;
} }
/** Utility type that represents the output of all modifiers that represent a single operation. */ /** 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. */ /** The amount to add to the input value. */
addend: MaybeRefOrGetter<DecimalSource>; addend: MaybeRefOrGetter<DecimalSource>;
/** Description of what this modifier is doing. */ /** 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. */ /** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
enabled?: MaybeRefOrGetter<boolean>; enabled?: MaybeRefOrGetter<boolean>;
/** Determines if numbers larger or smaller than 0 should be displayed as red. */ /** 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 { addend, description, enabled, smallerIsBetter } = optionsFunc();
const processedAddend = processGetter(addend); const processedAddend = processGetter(addend);
const processedDescription = processGetter(description);
const processedEnabled = enabled == null ? undefined : processGetter(enabled); const processedEnabled = enabled == null ? undefined : processGetter(enabled);
return { return {
apply: (gain: DecimalSource) => Decimal.add(gain, unref(processedAddend)), 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), getFormula: (gain: FormulaSource) => Formula.add(gain, processedAddend),
enabled: processedEnabled, enabled: processedEnabled,
description: description:
processedDescription == null description == null
? undefined ? undefined
: computed(() => ( : () => (
<div class="modifier-container"> <div class="modifier-container">
<span class="modifier-description"> <span class="modifier-description">{render(description)}</span>
{render(processedDescription)}
</span>
<span <span
class="modifier-amount" class="modifier-amount"
style={ style={
@ -95,7 +92,7 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
{formatSmall(unref(processedAddend))} {formatSmall(unref(processedAddend))}
</span> </span>
</div> </div>
)) )
}; };
}) as S; }) as S;
} }
@ -105,7 +102,7 @@ export interface MultiplicativeModifierOptions {
/** The amount to multiply the input value by. */ /** The amount to multiply the input value by. */
multiplier: MaybeRefOrGetter<DecimalSource>; multiplier: MaybeRefOrGetter<DecimalSource>;
/** Description of what this modifier is doing. */ /** 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. */ /** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
enabled?: MaybeRefOrGetter<boolean> | undefined; enabled?: MaybeRefOrGetter<boolean> | undefined;
/** Determines if numbers larger or smaller than 1 should be displayed as red. */ /** 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 { multiplier, description, enabled, smallerIsBetter } = optionsFunc();
const processedMultiplier = processGetter(multiplier); const processedMultiplier = processGetter(multiplier);
const processedDescription = processGetter(description);
const processedEnabled = enabled == null ? undefined : processGetter(enabled); const processedEnabled = enabled == null ? undefined : processGetter(enabled);
return { return {
apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)), apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)),
@ -132,13 +128,11 @@ export function createMultiplicativeModifier<
getFormula: (gain: FormulaSource) => Formula.times(gain, processedMultiplier), getFormula: (gain: FormulaSource) => Formula.times(gain, processedMultiplier),
enabled: processedEnabled, enabled: processedEnabled,
description: description:
processedDescription == null description == null
? undefined ? undefined
: computed(() => ( : () => (
<div class="modifier-container"> <div class="modifier-container">
<span class="modifier-description"> <span class="modifier-description">{render(description)}</span>
{render(processedDescription)}
</span>
<span <span
class="modifier-amount" class="modifier-amount"
style={ style={
@ -154,7 +148,7 @@ export function createMultiplicativeModifier<
×{formatSmall(unref(processedMultiplier))} ×{formatSmall(unref(processedMultiplier))}
</span> </span>
</div> </div>
)) )
}; };
}) as S; }) as S;
} }
@ -164,7 +158,7 @@ export interface ExponentialModifierOptions {
/** The amount to raise the input value to the power of. */ /** The amount to raise the input value to the power of. */
exponent: MaybeRefOrGetter<DecimalSource>; exponent: MaybeRefOrGetter<DecimalSource>;
/** Description of what this modifier is doing. */ /** 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. */ /** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
enabled?: MaybeRefOrGetter<boolean> | undefined; enabled?: MaybeRefOrGetter<boolean> | undefined;
/** Add 1 before calculating, then remove it afterwards. This prevents low numbers from becoming lower. */ /** Add 1 before calculating, then remove it afterwards. This prevents low numbers from becoming lower. */
@ -186,7 +180,6 @@ export function createExponentialModifier<
optionsFunc(); optionsFunc();
const processedExponent = processGetter(exponent); const processedExponent = processGetter(exponent);
const processedDescription = processGetter(description);
const processedEnabled = enabled == null ? undefined : processGetter(enabled); const processedEnabled = enabled == null ? undefined : processGetter(enabled);
return { return {
apply: (gain: DecimalSource) => { apply: (gain: DecimalSource) => {
@ -217,12 +210,12 @@ export function createExponentialModifier<
: Formula.pow(gain, processedExponent), : Formula.pow(gain, processedExponent),
enabled: processedEnabled, enabled: processedEnabled,
description: description:
processedDescription == null description == null
? undefined ? undefined
: computed(() => ( : () => (
<div class="modifier-container"> <div class="modifier-container">
<span class="modifier-description"> <span class="modifier-description">
{render(processedDescription)} {render(description)}
{supportLowNumbers ? " (+1 effective)" : null} {supportLowNumbers ? " (+1 effective)" : null}
</span> </span>
<span <span
@ -240,7 +233,7 @@ export function createExponentialModifier<
^{formatSmall(unref(processedExponent))} ^{formatSmall(unref(processedExponent))}
</span> </span>
</div> </div>
)) )
}; };
}) as S; }) as S;
} }
@ -286,14 +279,13 @@ export function createSequentialModifier<
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0) ? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)
: undefined, : undefined,
description: modifiers.some(m => m.description != null) description: modifiers.some(m => m.description != null)
? computed(() => ? () =>
( (
modifiers modifiers
.filter(m => unref(m.enabled) !== false) .filter(m => unref(m.enabled) !== false)
.map(m => unref(m.description)) .map(m => unref(m.description))
.filter(d => d) as MaybeRef<Renderable>[] .filter(d => d) as MaybeGetter<Renderable>[]
).map(m => render(m)) ).map(m => render(m))
)
: undefined : undefined
}; };
}) as S; }) as S;
@ -312,7 +304,7 @@ export interface ModifierSectionOptions {
/** The unit of the value being modified, if any. */ /** The unit of the value being modified, if any. */
unit?: string; unit?: string;
/** The label to use for the base value. Defaults to "Base". */ /** 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. */ /** Determines if numbers larger or smaller than the base should be displayed as red. */
smallerIsBetter?: boolean; smallerIsBetter?: boolean;
} }
@ -332,7 +324,6 @@ export function createModifierSection({
smallerIsBetter smallerIsBetter
}: ModifierSectionOptions) { }: ModifierSectionOptions) {
const total = modifier.apply(base ?? 1); const total = modifier.apply(base ?? 1);
const processedBaseText = processGetter(baseText);
return ( return (
<div style={{ "--unit": settings.alignUnits && unit != null ? "'" + unit + "'" : "" }}> <div style={{ "--unit": settings.alignUnits && unit != null ? "'" + unit + "'" : "" }}>
<h3> <h3>
@ -341,7 +332,7 @@ export function createModifierSection({
</h3> </h3>
<br /> <br />
<div class="modifier-container"> <div class="modifier-container">
<span class="modifier-description">{render(processedBaseText ?? "Base")}</span> <span class="modifier-description">{render(baseText ?? "Base")}</span>
<span class="modifier-amount"> <span class="modifier-amount">
{formatSmall(base ?? 1)} {formatSmall(base ?? 1)}
{unit} {unit}

View file

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

View file

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

View file

@ -2,6 +2,8 @@ import { isFunction } from "util/common";
import type { ComputedRef } from "vue"; import type { ComputedRef } from "vue";
import { computed } 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 { export function processGetter<T>(obj: T): T extends () => infer S ? ComputedRef<S> : T {
if (isFunction(obj)) { if (isFunction(obj)) {
return computed(obj) as ReturnType<typeof processGetter<T>>; 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 Row from "components/layout/Row.vue";
import { getUniqueID, Visibility } from "features/feature"; import { getUniqueID, Visibility } from "features/feature";
import VueFeatureComponent from "features/VueFeature.vue"; 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 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 { JSX } from "vue/jsx-runtime";
import { camelToKebab } from "./common"; import { camelToKebab } from "./common";
@ -35,7 +35,7 @@ export interface VueFeature {
/** CSS to apply to this feature. */ /** CSS to apply to this feature. */
style?: MaybeRef<CSSProperties>; style?: MaybeRef<CSSProperties>;
/** The components to render inside the vue feature */ /** The components to render inside the vue feature */
components: MaybeRef<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: ((el: () => Renderable) => Renderable)[];
/** Used to identify Vue Features */ /** Used to identify Vue Features */
@ -45,14 +45,14 @@ export interface VueFeature {
export function vueFeatureMixin( export function vueFeatureMixin(
featureName: string, featureName: string,
options: VueFeatureOptions, options: VueFeatureOptions,
component?: MaybeRefOrGetter<Renderable> component?: MaybeGetter<Renderable>
) { ) {
return { return {
id: getUniqueID(featureName), id: getUniqueID(featureName),
visibility: processGetter(options.visibility), visibility: processGetter(options.visibility),
classes: processGetter(options.classes), classes: processGetter(options.classes),
style: processGetter(options.style), style: processGetter(options.style),
components: component == null ? [] : [processGetter(component)], components: component == null ? [] : [component],
wrappers: [] as ((el: () => Renderable) => Renderable)[], wrappers: [] as ((el: () => Renderable) => Renderable)[],
[VueFeature]: true [VueFeature]: true
} satisfies VueFeature; } satisfies VueFeature;
@ -60,15 +60,15 @@ export function vueFeatureMixin(
export function render(object: VueFeature, wrapper?: (el: Renderable) => Renderable): JSX.Element; export function render(object: VueFeature, wrapper?: (el: Renderable) => Renderable): JSX.Element;
export function render<T extends Renderable>( export function render<T extends Renderable>(
object: MaybeRef<Renderable>, object: MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => T wrapper?: (el: Renderable) => T
): T; ): T;
export function render( export function render(
object: VueFeature | MaybeRef<Renderable>, object: VueFeature | MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => Renderable wrapper?: (el: Renderable) => Renderable
): Renderable; ): Renderable;
export function render( export function render(
object: VueFeature | MaybeRef<Renderable>, object: VueFeature | MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => Renderable wrapper?: (el: Renderable) => Renderable
) { ) {
if (typeof object === "object" && VueFeature in object) { if (typeof object === "object" && VueFeature in object) {
@ -85,20 +85,24 @@ export function render(
); );
} }
object = unref(object); object = toValue(object);
return wrapper?.(object) ?? 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>; 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>; return <Col>{objects.map(obj => render(obj))}</Col>;
} }
export function joinJSX( export function joinJSX(
objects: (VueFeature | MaybeRef<Renderable>)[], objects: (VueFeature | MaybeGetter<Renderable>)[],
joiner: JSX.Element joiner: JSX.Element
): JSX.Element { ): JSX.Element {
return objects.reduce<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 { processGetter } from "util/computed";
import { createLazyProxy, runAfterEvaluation } from "util/proxies"; import { createLazyProxy, runAfterEvaluation } from "util/proxies";
import type { VueFeature } from "util/vue"; import type { VueFeature } from "util/vue";
@ -23,9 +22,9 @@ export interface Mark {
* @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 options Mark options.
*/ */
export function addMark<T extends MarkOptions>( export function addMark(
element: VueFeature, element: VueFeature,
optionsFunc: OptionsFunc<T, Mark> optionsFunc: () => MarkOptions
): asserts element is VueFeature & { mark: Mark } { ): asserts element is VueFeature & { mark: Mark } {
const mark = createLazyProxy(() => { const mark = createLazyProxy(() => {
const options = optionsFunc(); 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 { deletePersistent, persistent } from "game/persistence";
import { Direction } from "util/common"; import { Direction } from "util/common";
import { processGetter } from "util/computed"; import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy, runAfterEvaluation } from "util/proxies"; import { createLazyProxy, runAfterEvaluation } from "util/proxies";
import { Renderable, vueFeatureMixin, type VueFeature, type VueFeatureOptions } from "util/vue"; import { Renderable, vueFeatureMixin, type VueFeature, type VueFeatureOptions } from "util/vue";
import { MaybeRef, MaybeRefOrGetter, type Ref } from "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. */ /** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
pinnable?: boolean; pinnable?: boolean;
/** The text to display inside the tooltip. */ /** The text to display inside the tooltip. */
display: MaybeRefOrGetter<Renderable>; display: MaybeGetter<Renderable>;
/** The direction in which to display the tooltip */ /** The direction in which to display the tooltip */
direction?: MaybeRefOrGetter<Direction>; direction?: MaybeRefOrGetter<Direction>;
/** The x offset of the tooltip, in px. */ /** 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. */ /** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
pinnable?: boolean; pinnable?: boolean;
/** The text to display inside the tooltip. */ /** The text to display inside the tooltip. */
display: MaybeRef<Renderable>; display: MaybeGetter<Renderable>;
/** The direction in which to display the tooltip */ /** The direction in which to display the tooltip */
direction?: MaybeRef<Direction>; direction?: MaybeRef<Direction>;
/** The x offset of the tooltip, in px. */ /** 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 element The renderable feature to display the tooltip on.
* @param options Tooltip options. * @param options Tooltip options.
*/ */
export function addTooltip<T extends TooltipOptions>( export function addTooltip(
element: VueFeature, element: VueFeature,
optionsFunc: OptionsFunc<T, Tooltip> optionsFunc: () => TooltipOptions
): asserts element is VueFeature & { tooltip: Tooltip } { ): asserts element is VueFeature & { tooltip: Tooltip } {
const pinned = persistent<boolean>(false, false); const pinned = persistent<boolean>(false, false);
const tooltip = createLazyProxy(() => { const tooltip = createLazyProxy(() => {
@ -69,7 +69,7 @@ export function addTooltip<T extends TooltipOptions>(
...vueFeatureMixin("tooltip", options), ...vueFeatureMixin("tooltip", options),
pinnable: pinnable ?? true, pinnable: pinnable ?? true,
pinned: pinnable === false ? undefined : pinned, pinned: pinnable === false ? undefined : pinned,
display: processGetter(display), display,
direction: processGetter(direction ?? Direction.Up), direction: processGetter(direction ?? Direction.Up),
xoffset: processGetter(xoffset), xoffset: processGetter(xoffset),
yoffset: processGetter(yoffset) yoffset: processGetter(yoffset)

View file

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