Profectus-Demo/src/features/action.tsx

273 lines
9.9 KiB
TypeScript
Raw Normal View History

2022-12-27 04:50:11 +00:00
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";
/** A symbol used to identify {@link Action} features. */
2022-12-27 04:50:11 +00:00
export const ActionType = Symbol("Action");
/**
* An object that configures an {@link Action}.
*/
2022-12-27 04:50:11 +00:00
export interface ActionOptions extends Omit<ClickableOptions, "onClick" | "onHold"> {
/** The cooldown during which the action cannot be performed again, in seconds. */
2022-12-27 04:50:11 +00:00
duration: Computable<DecimalSource>;
/** Whether or not the action should perform automatically when the cooldown is finished. */
2022-12-27 04:50:11 +00:00
autoStart?: Computable<boolean>;
/** A function that is called when the action is clicked. */
2022-12-27 04:50:11 +00:00
onClick: (amount: DecimalSource) => void;
/** A pass-through to the {@link Bar} used to display the cooldown progress for the action. */
2022-12-27 04:50:11 +00:00
barOptions?: Partial<BarOptions>;
}
/**
* The properties that are added onto a processed {@link ActionOptions} to create an {@link Action}.
*/
2022-12-27 04:50:11 +00:00
export interface BaseAction {
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
2022-12-27 04:50:11 +00:00
id: string;
/** A symbol that helps identify features of the same type. */
2022-12-27 04:50:11 +00:00
type: typeof ActionType;
/** 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. */
2022-12-27 04:50:11 +00:00
isHolding: Ref<boolean>;
/** The current amount of progress through the cooldown. */
2022-12-27 04:50:11 +00:00
progress: Ref<DecimalSource>;
/** The bar used to display the current cooldown progress. */
2022-12-27 04:50:11 +00:00
progressBar: GenericBar;
/** Update the cooldown the specified number of seconds */
2022-12-27 04:50:11 +00:00
update: (diff: number) => void;
/** The Vue component used to render this feature. */
2022-12-27 04:50:11 +00:00
[Component]: GenericComponent;
/** A function to gather the props the vue component requires for this feature. */
2022-12-27 04:50:11 +00:00
[GatherProps]: () => Record<string, unknown>;
}
/** An object that represents a feature that can be clicked upon, and then has a cooldown before it can be clicked again. */
2022-12-27 04:50:11 +00:00
export type Action<T extends ActionOptions> = Replace<
T & BaseAction,
{
duration: GetComputableType<T["duration"]>;
autoStart: GetComputableTypeWithDefault<T["autoStart"], false>;
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
canClick: GetComputableTypeWithDefault<T["canClick"], true>;
classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>;
mark: GetComputableType<T["mark"]>;
display: JSXFunction;
onClick: VoidFunction;
}
>;
/** A type that matches any valid {@link Action} object. */
2022-12-27 04:50:11 +00:00
export type GenericAction = Replace<
Action<ActionOptions>,
{
autoStart: ProcessedComputable<boolean>;
visibility: ProcessedComputable<Visibility | boolean>;
2022-12-27 04:50:11 +00:00
canClick: ProcessedComputable<boolean>;
}
>;
/**
* Lazily creates an action with the given options.
* @param optionsFunc Action options.
*/
2022-12-27 04:50:11 +00:00
export function createAction<T extends ActionOptions>(
optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>
): Action<T> {
const progress = persistent<DecimalSource>(0);
return createLazyProxy(() => {
const action = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
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<StyleValue | undefined>;
action.style = computed(() => {
const currStyle: StyleValue[] = [
{
cursor: Decimal.gte(
progress.value,
unref(action.duration as ProcessedComputable<DecimalSource>)
)
? "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<boolean>;
action.canClick = computed(
() =>
unref(canClick) &&
Decimal.gte(
progress.value,
unref(action.duration as ProcessedComputable<DecimalSource>)
)
);
const display = action.display as GetComputableType<ClickableOptions["display"]>;
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(() => (
<span>
{currDisplay.title != null ? (
<div>
<Title />
</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;
});