Profectus-Demo/src/features/repeatable.tsx

276 lines
12 KiB
TypeScript
Raw Normal View History

2022-12-31 20:57:09 +00:00
import { isArray } from "@vue/shared";
2022-03-04 03:39:48 +00:00
import ClickableComponent from "features/clickables/Clickable.vue";
2022-12-31 20:57:09 +00:00
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
2022-06-27 00:17:22 +00:00
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
2022-12-31 20:57:09 +00:00
import { DefaultValue, Persistent, persistent } from "game/persistence";
import {
createVisibilityRequirement,
displayRequirements,
2023-02-05 08:51:07 +00:00
maxRequirementsMet,
2022-12-31 20:57:09 +00:00
payRequirements,
Requirements,
requirementsMet
} from "game/requirements";
2022-06-27 00:17:22 +00:00
import type { DecimalSource } from "util/bignum";
2022-12-31 20:57:09 +00:00
import Decimal, { formatWhole } from "util/bignum";
2022-06-27 00:17:22 +00:00
import type {
2022-01-14 04:25:47 +00:00
Computable,
GetComputableType,
GetComputableTypeWithDefault,
ProcessedComputable
2022-03-04 03:39:48 +00:00
} from "util/computed";
2022-06-27 00:17:22 +00:00
import { processComputable } from "util/computed";
2022-03-04 03:39:48 +00:00
import { createLazyProxy } from "util/proxies";
import { coerceComponent, isCoercableComponent } from "util/vue";
2022-06-27 00:17:22 +00:00
import type { Ref } from "vue";
import { computed, unref } from "vue";
2022-01-14 04:25:47 +00:00
2023-02-15 16:17:07 +00:00
/** A symbol used to identify {@link Repeatable} features. */
2023-02-14 19:23:59 +00:00
export const RepeatableType = Symbol("Repeatable");
2022-01-14 04:25:47 +00:00
2023-02-15 16:17:07 +00:00
/** A type that can be used to customize the {@link Repeatable} display. */
2023-02-14 19:23:59 +00:00
export type RepeatableDisplay =
2022-01-14 04:25:47 +00:00
| CoercableComponent
| {
2023-02-15 16:17:07 +00:00
/** A header to appear at the top of the display. */
2022-01-14 04:25:47 +00:00
title?: CoercableComponent;
2023-02-15 16:17:07 +00:00
/** The main text that appears in the display. */
2022-07-07 15:39:45 +00:00
description?: CoercableComponent;
2023-02-15 16:17:07 +00:00
/** A description of the current effect of this repeatable, bsed off its amount. */
2022-01-14 04:25:47 +00:00
effectDisplay?: CoercableComponent;
2023-02-15 16:17:07 +00:00
/** Whether or not to show the current amount of this repeatable at the bottom of the display. */
showAmount?: boolean;
2022-01-14 04:25:47 +00:00
};
2023-02-15 16:17:07 +00:00
/** An object that configures a {@link Repeatable}. */
2023-02-14 19:23:59 +00:00
export interface RepeatableOptions {
2023-02-15 16:17:07 +00:00
/** Whether this repeatable should be visible. */
visibility?: Computable<Visibility | boolean>;
2023-02-15 16:17:07 +00:00
/** The requirement(s) to increase this repeatable. */
2022-12-31 20:57:09 +00:00
requirements: Requirements;
2023-02-15 16:17:07 +00:00
/** The maximum amount obtainable for this repeatable. */
limit?: Computable<DecimalSource>;
2023-02-15 16:17:07 +00:00
/** The initial amount this repeatable has on a new save / after reset. */
initialAmount?: DecimalSource;
2023-02-15 16:17:07 +00:00
/** Dictionary of CSS classes to apply to this feature. */
2022-01-14 04:25:47 +00:00
classes?: Computable<Record<string, boolean>>;
2023-02-15 16:17:07 +00:00
/** CSS to apply to this feature. */
2022-01-14 04:25:47 +00:00
style?: Computable<StyleValue>;
2023-02-15 16:17:07 +00:00
/** Shows a marker on the corner of the feature. */
2022-01-14 04:25:47 +00:00
mark?: Computable<boolean | string>;
2023-02-15 16:17:07 +00:00
/** Toggles a smaller design for the feature. */
2022-01-14 04:25:47 +00:00
small?: Computable<boolean>;
2023-02-15 16:17:07 +00:00
/** Whether or not clicking this repeatable should attempt to maximize amount based on the requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */
2023-02-15 03:47:18 +00:00
maximize?: Computable<boolean>;
2023-02-15 16:17:07 +00:00
/** The display to use for this repeatable. */
2023-02-14 19:23:59 +00:00
display?: Computable<RepeatableDisplay>;
2022-01-14 04:25:47 +00:00
}
2023-02-15 16:17:07 +00:00
/**
* The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}.
*/
2023-02-14 19:23:59 +00:00
export interface BaseRepeatable {
2023-02-15 16:17:07 +00:00
/** An auto-generated ID for identifying features that appear in the DOM. Will not persistent between refreshes or updates. */
2022-01-14 04:25:47 +00:00
id: string;
2023-02-15 16:17:07 +00:00
/** The current amount this repeatable has. */
2022-04-23 23:20:15 +00:00
amount: Persistent<DecimalSource>;
2023-02-15 16:17:07 +00:00
/** Whether or not this repeatable's amount is at it's limit. */
maxed: Ref<boolean>;
2023-02-15 16:17:07 +00:00
/** Whether or not this repeatable can be clicked. */
2022-01-14 04:25:47 +00:00
canClick: ProcessedComputable<boolean>;
2023-02-15 16:17:07 +00:00
/** A function that gets called when this repeatable is clicked. */
onClick: (event?: MouseEvent | TouchEvent) => void;
2023-02-15 16:17:07 +00:00
/** A symbol that helps identify features of the same type. */
2023-02-14 19:23:59 +00:00
type: typeof RepeatableType;
2023-02-15 16:17:07 +00:00
/** The Vue component used to render this feature. */
2022-01-14 04:25:47 +00:00
[Component]: typeof ClickableComponent;
2023-02-15 16:17:07 +00:00
/** A function to gather the props the vue component requires for this feature. */
[GatherProps]: () => Record<string, unknown>;
2022-01-14 04:25:47 +00:00
}
2023-02-15 16:17:07 +00:00
/** An object that represents a feature with multiple "levels" with scaling requirements. */
2023-02-14 19:23:59 +00:00
export type Repeatable<T extends RepeatableOptions> = Replace<
T & BaseRepeatable,
2022-01-14 04:25:47 +00:00
{
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
2022-12-31 20:57:09 +00:00
requirements: GetComputableType<T["requirements"]>;
limit: GetComputableTypeWithDefault<T["limit"], Decimal>;
2022-01-14 04:25:47 +00:00
classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>;
mark: GetComputableType<T["mark"]>;
small: GetComputableType<T["small"]>;
2023-02-15 03:47:18 +00:00
maximize: GetComputableType<T["maximize"]>;
2022-01-14 04:25:47 +00:00
display: Ref<CoercableComponent>;
}
>;
2023-02-15 16:17:07 +00:00
/** A type that matches any valid {@link Repeatable} object. */
2023-02-14 19:23:59 +00:00
export type GenericRepeatable = Replace<
Repeatable<RepeatableOptions>,
2022-01-14 04:25:47 +00:00
{
visibility: ProcessedComputable<Visibility | boolean>;
limit: ProcessedComputable<DecimalSource>;
2022-01-14 04:25:47 +00:00
}
>;
2023-02-15 16:17:07 +00:00
/**
* Lazily creates a repeatable with the given options.
* @param optionsFunc Repeatable options.
*/
2023-02-14 19:23:59 +00:00
export function createRepeatable<T extends RepeatableOptions>(
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>
): Repeatable<T> {
2022-04-23 23:20:15 +00:00
const amount = persistent<DecimalSource>(0);
return createLazyProxy(() => {
2023-02-14 19:23:59 +00:00
const repeatable = optionsFunc();
2022-01-14 04:25:47 +00:00
2023-02-14 19:23:59 +00:00
repeatable.id = getUniqueID("repeatable-");
repeatable.type = RepeatableType;
repeatable[Component] = ClickableComponent;
2023-02-14 19:23:59 +00:00
repeatable.amount = amount;
repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
2022-12-31 20:57:09 +00:00
const limitRequirement = {
requirementMet: computed(() =>
2023-02-05 08:51:07 +00:00
Decimal.sub(
unref((repeatable as GenericRepeatable).limit),
2023-02-14 19:23:59 +00:00
(repeatable as GenericRepeatable).amount.value
2022-12-31 20:57:09 +00:00
)
),
requiresPay: false,
visibility: Visibility.None
} as const;
2023-02-14 19:23:59 +00:00
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
if (isArray(repeatable.requirements)) {
repeatable.requirements.unshift(visibilityRequirement);
repeatable.requirements.push(limitRequirement);
2022-12-31 20:57:09 +00:00
} else {
2023-02-14 19:23:59 +00:00
repeatable.requirements = [
visibilityRequirement,
repeatable.requirements,
limitRequirement
];
2022-01-14 04:25:47 +00:00
}
2022-12-31 20:57:09 +00:00
2023-02-14 19:23:59 +00:00
repeatable.maxed = computed(() =>
Decimal.gte(
2023-02-14 19:23:59 +00:00
(repeatable as GenericRepeatable).amount.value,
unref((repeatable as GenericRepeatable).limit)
)
);
2023-02-14 19:23:59 +00:00
processComputable(repeatable as T, "classes");
const classes = repeatable.classes as
| ProcessedComputable<Record<string, boolean>>
| undefined;
repeatable.classes = computed(() => {
const currClasses = unref(classes) || {};
2023-02-14 19:23:59 +00:00
if ((repeatable as GenericRepeatable).maxed.value) {
currClasses.bought = true;
}
return currClasses;
});
2023-02-14 19:23:59 +00:00
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
const onClick = repeatable.onClick;
repeatable.onClick = function (this: GenericRepeatable, event?: MouseEvent | TouchEvent) {
const genericRepeatable = repeatable as GenericRepeatable;
if (!unref(genericRepeatable.canClick)) {
return;
}
payRequirements(
repeatable.requirements,
unref(genericRepeatable.maximize)
? maxRequirementsMet(genericRepeatable.requirements)
: 1
);
genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1);
onClick?.(event);
};
2023-02-14 19:23:59 +00:00
processComputable(repeatable as T, "display");
const display = repeatable.display;
repeatable.display = jsx(() => {
// TODO once processComputable types correctly, remove this "as X"
2023-02-14 19:23:59 +00:00
const currDisplay = unref(display) as RepeatableDisplay;
if (isCoercableComponent(currDisplay)) {
2022-03-16 16:24:54 +00:00
const CurrDisplay = coerceComponent(currDisplay);
return <CurrDisplay />;
}
2022-12-31 20:57:09 +00:00
if (currDisplay != null) {
2023-02-14 19:23:59 +00:00
const genericRepeatable = repeatable as GenericRepeatable;
const Title = coerceComponent(currDisplay.title ?? "", "h3");
const Description = coerceComponent(currDisplay.description ?? "");
const EffectDisplay = coerceComponent(currDisplay.effectDisplay ?? "");
return (
<span>
{currDisplay.title == null ? null : (
<div>
<Title />
</div>
)}
{currDisplay.description == null ? null : <Description />}
{currDisplay.showAmount === false ? null : (
<div>
<br />
{unref(genericRepeatable.limit) === Decimal.dInf ? (
2023-02-14 19:23:59 +00:00
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
) : (
<>
2023-02-14 19:23:59 +00:00
Amount: {formatWhole(genericRepeatable.amount.value)} /{" "}
{formatWhole(unref(genericRepeatable.limit))}
</>
)}
</div>
)}
{currDisplay.effectDisplay == null ? null : (
<div>
<br />
Currently: <EffectDisplay />
</div>
)}
2023-02-14 19:23:59 +00:00
{genericRepeatable.maxed.value ? null : (
<div>
<br />
2023-02-05 08:51:07 +00:00
{displayRequirements(
2023-02-14 19:23:59 +00:00
genericRepeatable.requirements,
2023-02-15 03:47:18 +00:00
unref(genericRepeatable.maximize)
2023-02-14 19:23:59 +00:00
? maxRequirementsMet(genericRepeatable.requirements)
2023-02-05 08:51:07 +00:00
: 1
)}
</div>
2022-12-31 20:57:09 +00:00
)}
</span>
);
}
return "";
});
2023-02-14 19:23:59 +00:00
processComputable(repeatable as T, "visibility");
setDefault(repeatable, "visibility", Visibility.Visible);
processComputable(repeatable as T, "limit");
setDefault(repeatable, "limit", Decimal.dInf);
2023-02-14 19:23:59 +00:00
processComputable(repeatable as T, "style");
processComputable(repeatable as T, "mark");
processComputable(repeatable as T, "small");
2023-02-15 03:47:18 +00:00
processComputable(repeatable as T, "maximize");
2022-01-14 04:25:47 +00:00
2023-02-14 19:23:59 +00:00
repeatable[GatherProps] = function (this: GenericRepeatable) {
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
this;
return {
display,
visibility,
style: unref(style),
classes,
onClick,
canClick,
small,
mark,
id
};
};
2022-01-14 04:25:47 +00:00
2023-02-14 19:23:59 +00:00
return repeatable as unknown as Repeatable<T>;
2022-04-23 23:20:15 +00:00
});
2022-01-14 04:25:47 +00:00
}