import { isArray } from "@vue/shared"; import ClickableComponent from "features/clickables/Clickable.vue"; import { Component, findFeatures, GatherProps, GenericComponent, getUniqueID, jsx, JSXFunction, OptionsFunc, Replace, setDefault, StyleValue, Visibility } from "features/feature"; import { globalBus } from "game/events"; import { persistent } from "game/persistence"; import Decimal, { DecimalSource } from "lib/break_eternity"; import { Unsubscribe } from "nanoevents"; import { Direction } from "util/common"; import type { Computable, GetComputableType, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed"; import { processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { coerceComponent, isCoercableComponent, render } from "util/vue"; import { computed, Ref, ref, unref } from "vue"; import { BarOptions, createBar, GenericBar } from "./bars/bar"; import { ClickableOptions } from "./clickables/clickable"; export const ActionType = Symbol("Action"); export interface ActionOptions extends Omit { duration: Computable; autoStart?: Computable; onClick: (amount: DecimalSource) => void; barOptions?: Partial; } export interface BaseAction { id: string; type: typeof ActionType; isHolding: Ref; progress: Ref; progressBar: GenericBar; update: (diff: number) => void; [Component]: GenericComponent; [GatherProps]: () => Record; } export type Action = Replace< T & BaseAction, { duration: GetComputableType; autoStart: GetComputableTypeWithDefault; visibility: GetComputableTypeWithDefault; canClick: GetComputableTypeWithDefault; classes: GetComputableType; style: GetComputableType; mark: GetComputableType; display: JSXFunction; onClick: VoidFunction; } >; export type GenericAction = Replace< Action, { autoStart: ProcessedComputable; visibility: ProcessedComputable; canClick: ProcessedComputable; } >; export function createAction( optionsFunc?: OptionsFunc ): Action { const progress = persistent(0); return createLazyProxy(() => { const action = optionsFunc?.() ?? ({} as ReturnType>); action.id = getUniqueID("action-"); action.type = ActionType; action[Component] = ClickableComponent as GenericComponent; // Required because of display changing types const genericAction = action as unknown as GenericAction; action.isHolding = ref(false); action.progress = progress; processComputable(action as T, "visibility"); setDefault(action, "visibility", Visibility.Visible); processComputable(action as T, "duration"); processComputable(action as T, "autoStart"); setDefault(action, "autoStart", false); processComputable(action as T, "canClick"); setDefault(action, "canClick", true); processComputable(action as T, "classes"); processComputable(action as T, "style"); processComputable(action as T, "mark"); processComputable(action as T, "display"); const style = action.style as ProcessedComputable; action.style = computed(() => { const currStyle: StyleValue[] = [ { cursor: Decimal.gte( progress.value, unref(action.duration as ProcessedComputable) ) ? "pointer" : "progress", display: "flex", flexDirection: "column" } ]; const originalStyle = unref(style); if (isArray(originalStyle)) { currStyle.push(...originalStyle); } else if (originalStyle != null) { currStyle.push(originalStyle); } return currStyle as StyleValue; }); action.progressBar = createBar(() => ({ direction: Direction.Right, width: 100, height: 10, style: "margin-top: 8px", borderStyle: "border-color: black", baseStyle: "margin-top: -1px", progress: () => Decimal.div(progress.value, unref(genericAction.duration)), ...action.barOptions })); const canClick = action.canClick as ProcessedComputable; action.canClick = computed( () => unref(canClick) && Decimal.gte( progress.value, unref(action.duration as ProcessedComputable) ) ); const display = action.display as GetComputableType; action.display = jsx(() => { const currDisplay = unref(display); let Comp: GenericComponent | undefined; if (isCoercableComponent(currDisplay)) { Comp = coerceComponent(currDisplay); } else if (currDisplay != null) { const Title = coerceComponent(currDisplay.title ?? "", "h3"); const Description = coerceComponent(currDisplay.description, "div"); Comp = coerceComponent( jsx(() => ( {currDisplay.title != null ? (
</div> ) : null} <Description /> </span> )) ); } return ( <> <div style="flex-grow: 1" /> {Comp == null ? null : <Comp />} <div style="flex-grow: 1" /> {render(genericAction.progressBar)} </> ); }); const onClick = action.onClick.bind(action); action.onClick = function () { if (unref(action.canClick) === false) { return; } const amount = Decimal.div(progress.value, unref(genericAction.duration)); onClick?.(amount); progress.value = 0; }; action.update = function (diff) { const duration = unref(genericAction.duration); if (Decimal.gte(progress.value, duration)) { progress.value = duration; } else { progress.value = Decimal.add(progress.value, diff); if (genericAction.isHolding.value || unref(genericAction.autoStart)) { genericAction.onClick(); } } }; action[GatherProps] = function (this: GenericAction) { const { display, visibility, style, classes, onClick, isHolding, canClick, small, mark, id } = this; return { display, visibility, style: unref(style), classes, onClick, isHolding, canClick, small, mark, id }; }; return action as unknown as Action<T>; }); } const listeners: Record<string, Unsubscribe | undefined> = {}; globalBus.on("addLayer", layer => { const actions: GenericAction[] = findFeatures(layer, ActionType) as GenericAction[]; listeners[layer.id] = layer.on("postUpdate", diff => { actions.forEach(action => action.update(diff)); }); }); globalBus.on("removeLayer", layer => { // unsubscribe from postUpdate listeners[layer.id]?.(); listeners[layer.id] = undefined; });