forked from profectus/Profectus
Implement Action feature
This commit is contained in:
parent
2ece8e5a17
commit
345f8c1cd8
1 changed files with 247 additions and 0 deletions
247
src/features/action.tsx
Normal file
247
src/features/action.tsx
Normal file
|
@ -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<ClickableOptions, "onClick" | "onHold"> {
|
||||
duration: Computable<DecimalSource>;
|
||||
autoStart?: Computable<boolean>;
|
||||
onClick: (amount: DecimalSource) => void;
|
||||
barOptions?: Partial<BarOptions>;
|
||||
}
|
||||
|
||||
export interface BaseAction {
|
||||
id: string;
|
||||
type: typeof ActionType;
|
||||
isHolding: Ref<boolean>;
|
||||
progress: Ref<DecimalSource>;
|
||||
progressBar: GenericBar;
|
||||
update: (diff: number) => void;
|
||||
[Component]: GenericComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GenericAction = Replace<
|
||||
Action<ActionOptions>,
|
||||
{
|
||||
autoStart: ProcessedComputable<boolean>;
|
||||
visibility: ProcessedComputable<Visibility>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
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;
|
||||
});
|
Loading…
Reference in a new issue