diff --git a/src/features/action.tsx b/src/features/action.tsx new file mode 100644 index 0000000..18643b8 --- /dev/null +++ b/src/features/action.tsx @@ -0,0 +1,247 @@ +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; +});