import ClickableVue from "features/clickables/Clickable.vue"; import { findFeatures } from "features/feature"; import { globalBus } from "game/events"; import { persistent } from "game/persistence"; import { Unsubscribe } from "nanoevents"; import Decimal, { DecimalSource } from "util/bignum"; import { Direction } from "util/common"; import { MaybeGetter, processGetter } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin } from "util/vue"; import { computed, MaybeRef, MaybeRefOrGetter, Ref, ref, unref } from "vue"; import { Bar, BarOptions, createBar } from "../bars/bar"; import { type Clickable, ClickableOptions } from "./clickable"; /** A symbol used to identify {@link Action} features. */ export const ActionType = Symbol("Action"); /** * An object that configures an {@link Action}. */ export interface ActionOptions extends Omit { /** The cooldown during which the action cannot be performed again, in seconds. */ duration: MaybeRefOrGetter; /** Whether or not the action should perform automatically when the cooldown is finished. */ autoStart?: MaybeRefOrGetter; /** A function that is called when the action is clicked. */ onClick: (amount: DecimalSource) => void; /** A pass-through to the {@link Bar} used to display the cooldown progress for the action. */ barOptions?: Partial; } /** An object that represents a feature that can be clicked upon, and then has a cooldown before it can be clicked again. */ export interface Action extends VueFeature { /** The cooldown during which the action cannot be performed again, in seconds. */ duration: MaybeRef; /** Whether or not the action should perform automatically when the cooldown is finished. */ autoStart: MaybeRef; /** Whether or not the action may be performed. */ canClick: MaybeRef; /** The display to use for this action. */ display?: MaybeGetter; /** A function that is called when the action is clicked. */ onClick: (amount: DecimalSource) => void; /** Whether or not the player is holding down the action. Actions will be considered clicked as soon as the cooldown completes when being held down. */ isHolding: Ref; /** The current amount of progress through the cooldown. */ progress: Ref; /** The bar used to display the current cooldown progress. */ progressBar: Bar; /** Update the cooldown the specified number of seconds */ update: (diff: number) => void; /** A symbol that helps identify features of the same type. */ type: typeof ActionType; } /** * Lazily creates an action with the given options. * @param optionsFunc Action options. */ export function createAction(optionsFunc?: () => T) { const progress = persistent(0); return createLazyProxy(() => { const options = optionsFunc?.() ?? ({} as T); const { style, duration, canClick, autoStart, display: _display, barOptions, onClick, ...props } = options; const processedCanClick = processGetter(canClick) ?? true; const processedStyle = processGetter(style); const progressBar = createBar(() => ({ direction: Direction.Right, width: 100, height: 10, borderStyle: { borderColor: "black" }, baseStyle: { marginTop: "-1px" }, progress: (): DecimalSource => Decimal.div(progress.value, unref(action.duration)), ...(barOptions as Omit) })); let display: MaybeGetter; if (typeof _display === "object" && !isJSXElement(_display)) { display = () => ( {_display.title != null ? (
{render(_display.title, el => (

{el}

))}
) : null} {render(_display.description, el => (
{el}
))}
); } else if (_display != null) { display = _display; } const action = { type: ActionType, ...(props as Omit), ...vueFeatureMixin( "action", { ...options, style: () => ({ cursor: Decimal.gte(progress.value, unref(action.duration)) ? "pointer" : "progress", display: "flex", flexDirection: "column", ...unref(processedStyle) }) }, () => ( ) ), progress, isHolding: ref(false), duration: processGetter(duration), canClick: computed( (): boolean => unref(processedCanClick) && Decimal.gte(progress.value, unref(action.duration)) ), autoStart: processGetter(autoStart) ?? false, display: () => ( <>
{display == null ? null : render(display)}
{render(progressBar)} ), progressBar, onClick: function () { if (unref(action.canClick) === false) { return; } const amount = Decimal.div(progress.value, unref(action.duration)); onClick?.call(action, amount); progress.value = 0; }, update: function (diff) { const duration = unref(action.duration); if (Decimal.gte(progress.value, duration)) { progress.value = duration; } else { progress.value = Decimal.add(progress.value, diff); if (action.isHolding.value || unref(action.autoStart)) { action.onClick(); } } } } satisfies Action satisfies Omit & { type: typeof ActionType }; return action; }); } const listeners: Record = {}; globalBus.on("addLayer", layer => { const actions: Action[] = findFeatures(layer, ActionType) as Action[]; listeners[layer.id] = layer.on("postUpdate", (diff: number) => { actions.forEach(action => action.update(diff)); }); }); globalBus.on("removeLayer", layer => { // unsubscribe from postUpdate listeners[layer.id]?.(); listeners[layer.id] = undefined; });