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";
|
2023-04-03 02:42:17 +00:00
|
|
|
import type {
|
|
|
|
CoercableComponent,
|
|
|
|
GenericComponent,
|
|
|
|
OptionsFunc,
|
|
|
|
Replace,
|
|
|
|
StyleValue
|
|
|
|
} from "features/feature";
|
2023-04-20 02:37:28 +00:00
|
|
|
import { Component, GatherProps, Visibility, getUniqueID, jsx, setDefault } from "features/feature";
|
2022-12-31 20:57:09 +00:00
|
|
|
import { DefaultValue, Persistent, persistent } from "game/persistence";
|
|
|
|
import {
|
2023-04-20 02:37:28 +00:00
|
|
|
Requirements,
|
2022-12-31 20:57:09 +00:00
|
|
|
createVisibilityRequirement,
|
|
|
|
displayRequirements,
|
2023-02-05 08:51:07 +00:00
|
|
|
maxRequirementsMet,
|
2022-12-31 20:57:09 +00:00
|
|
|
payRequirements,
|
|
|
|
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";
|
2023-04-20 02:37:28 +00:00
|
|
|
import { GenericDecorator } from "./decorators/common";
|
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-04-03 13:19:58 +00:00
|
|
|
/** A description of the current effect of this repeatable, based 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. */
|
2022-07-06 15:09:37 +00:00
|
|
|
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. */
|
2023-02-16 02:00:36 +00:00
|
|
|
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. */
|
2023-02-15 03:35:05 +00:00
|
|
|
limit?: Computable<DecimalSource>;
|
2023-02-15 16:17:07 +00:00
|
|
|
/** The initial amount this repeatable has on a new save / after reset. */
|
2023-02-15 03:35:05 +00:00
|
|
|
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
|
|
|
/** 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-04-04 05:02:23 +00:00
|
|
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist 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. */
|
2022-02-27 19:49:34 +00:00
|
|
|
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-04-03 00:22:23 +00:00
|
|
|
/**
|
|
|
|
* How much amount can be increased by, or 1 if unclickable.
|
|
|
|
**/
|
|
|
|
amountToIncrease: Ref<DecimalSource>;
|
2023-02-15 16:17:07 +00:00
|
|
|
/** A function that gets called when this repeatable is clicked. */
|
2023-02-15 20:57:22 +00:00
|
|
|
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. */
|
2023-04-03 02:42:17 +00:00
|
|
|
[Component]: GenericComponent;
|
2023-02-15 16:17:07 +00:00
|
|
|
/** A function to gather the props the vue component requires for this feature. */
|
2022-02-27 19:49:34 +00:00
|
|
|
[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"]>;
|
2023-02-15 03:35:05 +00:00
|
|
|
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"]>;
|
|
|
|
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
|
|
|
{
|
2023-02-16 02:00:36 +00:00
|
|
|
visibility: ProcessedComputable<Visibility | boolean>;
|
2023-02-15 03:35:05 +00:00
|
|
|
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>(
|
2023-02-26 00:48:36 +00:00
|
|
|
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>,
|
2023-04-20 00:49:15 +00:00
|
|
|
...decorators: GenericDecorator[]
|
2023-02-14 19:23:59 +00:00
|
|
|
): Repeatable<T> {
|
2022-04-23 23:20:15 +00:00
|
|
|
const amount = persistent<DecimalSource>(0);
|
2023-04-20 01:39:25 +00:00
|
|
|
const decoratedData = decorators.reduce(
|
|
|
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
|
|
|
{}
|
|
|
|
);
|
2023-04-20 00:49:15 +00:00
|
|
|
return createLazyProxy<Repeatable<T>, Repeatable<T>>(feature => {
|
2023-04-19 01:48:34 +00:00
|
|
|
const repeatable = optionsFunc.call(feature, feature);
|
2022-01-14 04:25:47 +00:00
|
|
|
|
2023-02-14 19:23:59 +00:00
|
|
|
repeatable.id = getUniqueID("repeatable-");
|
|
|
|
repeatable.type = RepeatableType;
|
2023-04-03 02:42:17 +00:00
|
|
|
repeatable[Component] = ClickableComponent as GenericComponent;
|
2022-02-27 19:49:34 +00:00
|
|
|
|
2023-02-26 00:48:36 +00:00
|
|
|
for (const decorator of decorators) {
|
|
|
|
decorator.preConstruct?.(repeatable);
|
|
|
|
}
|
|
|
|
|
2023-02-14 19:23:59 +00:00
|
|
|
repeatable.amount = amount;
|
2023-02-15 03:35:05 +00:00
|
|
|
repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
|
2022-12-31 20:57:09 +00:00
|
|
|
|
2023-02-26 00:48:36 +00:00
|
|
|
Object.assign(repeatable, decoratedData);
|
|
|
|
|
2022-12-31 20:57:09 +00:00
|
|
|
const limitRequirement = {
|
|
|
|
requirementMet: computed(() =>
|
2023-02-05 08:51:07 +00:00
|
|
|
Decimal.sub(
|
2023-02-15 03:35:05 +00:00
|
|
|
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,
|
2023-05-12 22:07:37 +00:00
|
|
|
visibility: Visibility.None,
|
|
|
|
canMaximize: true
|
2022-12-31 20:57:09 +00:00
|
|
|
} 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(() =>
|
2022-02-27 19:49:34 +00:00
|
|
|
Decimal.gte(
|
2023-02-14 19:23:59 +00:00
|
|
|
(repeatable as GenericRepeatable).amount.value,
|
2023-02-15 03:35:05 +00:00
|
|
|
unref((repeatable as GenericRepeatable).limit)
|
2022-02-27 19:49:34 +00:00
|
|
|
)
|
|
|
|
);
|
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(() => {
|
2022-02-27 19:49:34 +00:00
|
|
|
const currClasses = unref(classes) || {};
|
2023-02-14 19:23:59 +00:00
|
|
|
if ((repeatable as GenericRepeatable).maxed.value) {
|
2022-02-27 19:49:34 +00:00
|
|
|
currClasses.bought = true;
|
|
|
|
}
|
|
|
|
return currClasses;
|
|
|
|
});
|
2023-04-03 00:22:23 +00:00
|
|
|
repeatable.amountToIncrease = computed(() =>
|
2023-05-07 16:15:02 +00:00
|
|
|
Decimal.clampMin(maxRequirementsMet(repeatable.requirements), 1)
|
2023-04-03 00:22:23 +00:00
|
|
|
);
|
2023-02-14 19:23:59 +00:00
|
|
|
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
|
2023-02-15 06:22:24 +00:00
|
|
|
const onClick = repeatable.onClick;
|
2023-02-15 20:57:22 +00:00
|
|
|
repeatable.onClick = function (this: GenericRepeatable, event?: MouseEvent | TouchEvent) {
|
2023-02-15 06:22:24 +00:00
|
|
|
const genericRepeatable = repeatable as GenericRepeatable;
|
|
|
|
if (!unref(genericRepeatable.canClick)) {
|
|
|
|
return;
|
|
|
|
}
|
2023-05-12 22:07:37 +00:00
|
|
|
const amountToIncrease = unref(repeatable.amountToIncrease) ?? 1;
|
|
|
|
payRequirements(repeatable.requirements, amountToIncrease);
|
|
|
|
genericRepeatable.amount.value = Decimal.add(
|
|
|
|
genericRepeatable.amount.value,
|
|
|
|
amountToIncrease
|
|
|
|
);
|
2023-02-15 20:57:22 +00:00
|
|
|
onClick?.(event);
|
2023-02-15 06:22:24 +00:00
|
|
|
};
|
2023-02-14 19:23:59 +00:00
|
|
|
processComputable(repeatable as T, "display");
|
|
|
|
const display = repeatable.display;
|
|
|
|
repeatable.display = jsx(() => {
|
2022-02-27 19:49:34 +00:00
|
|
|
// TODO once processComputable types correctly, remove this "as X"
|
2023-02-14 19:23:59 +00:00
|
|
|
const currDisplay = unref(display) as RepeatableDisplay;
|
2022-03-16 16:20:53 +00:00
|
|
|
if (isCoercableComponent(currDisplay)) {
|
2022-03-16 16:24:54 +00:00
|
|
|
const CurrDisplay = coerceComponent(currDisplay);
|
|
|
|
return <CurrDisplay />;
|
2022-03-16 16:20:53 +00:00
|
|
|
}
|
2022-12-31 20:57:09 +00:00
|
|
|
if (currDisplay != null) {
|
2023-02-14 19:23:59 +00:00
|
|
|
const genericRepeatable = repeatable as GenericRepeatable;
|
2022-12-21 03:26:25 +00:00
|
|
|
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
|
|
|
const Description = coerceComponent(currDisplay.description ?? "");
|
|
|
|
const EffectDisplay = coerceComponent(currDisplay.effectDisplay ?? "");
|
2022-03-11 15:16:19 +00:00
|
|
|
|
2022-02-27 19:49:34 +00:00
|
|
|
return (
|
|
|
|
<span>
|
2022-12-21 03:26:25 +00:00
|
|
|
{currDisplay.title == null ? null : (
|
2022-02-27 19:49:34 +00:00
|
|
|
<div>
|
|
|
|
<Title />
|
|
|
|
</div>
|
2022-12-21 03:26:25 +00:00
|
|
|
)}
|
|
|
|
{currDisplay.description == null ? null : <Description />}
|
2022-07-06 15:09:37 +00:00
|
|
|
{currDisplay.showAmount === false ? null : (
|
|
|
|
<div>
|
|
|
|
<br />
|
2023-04-30 16:23:38 +00:00
|
|
|
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
|
|
|
|
{Decimal.isFinite(unref(genericRepeatable.limit)) ? (
|
2023-04-20 01:39:25 +00:00
|
|
|
<> / {formatWhole(unref(genericRepeatable.limit))}</>
|
|
|
|
) : undefined}
|
2022-07-06 15:09:37 +00:00
|
|
|
</div>
|
|
|
|
)}
|
2022-12-21 03:26:25 +00:00
|
|
|
{currDisplay.effectDisplay == null ? null : (
|
2022-02-27 19:49:34 +00:00
|
|
|
<div>
|
|
|
|
<br />
|
|
|
|
Currently: <EffectDisplay />
|
|
|
|
</div>
|
2022-12-21 03:26:25 +00:00
|
|
|
)}
|
2023-02-14 19:23:59 +00:00
|
|
|
{genericRepeatable.maxed.value ? null : (
|
2022-02-27 19:49:34 +00:00
|
|
|
<div>
|
|
|
|
<br />
|
2023-02-05 08:51:07 +00:00
|
|
|
{displayRequirements(
|
2023-02-14 19:23:59 +00:00
|
|
|
genericRepeatable.requirements,
|
2023-04-03 00:22:23 +00:00
|
|
|
unref(repeatable.amountToIncrease)
|
2023-02-05 08:51:07 +00:00
|
|
|
)}
|
2022-02-27 19:49:34 +00:00
|
|
|
</div>
|
2022-12-31 20:57:09 +00:00
|
|
|
)}
|
2022-02-27 19:49:34 +00:00
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
});
|
|
|
|
|
2023-02-14 19:23:59 +00:00
|
|
|
processComputable(repeatable as T, "visibility");
|
|
|
|
setDefault(repeatable, "visibility", Visibility.Visible);
|
2023-02-15 03:35:05 +00:00
|
|
|
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");
|
2022-01-14 04:25:47 +00:00
|
|
|
|
2023-02-26 00:48:36 +00:00
|
|
|
for (const decorator of decorators) {
|
|
|
|
decorator.postConstruct?.(repeatable);
|
|
|
|
}
|
|
|
|
|
2023-04-20 01:39:25 +00:00
|
|
|
const decoratedProps = decorators.reduce(
|
|
|
|
(current, next) => Object.assign(current, next.getGatheredProps?.(repeatable)),
|
|
|
|
{}
|
|
|
|
);
|
2023-02-14 19:23:59 +00:00
|
|
|
repeatable[GatherProps] = function (this: GenericRepeatable) {
|
2022-02-27 19:49:34 +00:00
|
|
|
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
|
|
|
this;
|
2022-03-13 22:09:09 +00:00
|
|
|
return {
|
|
|
|
display,
|
|
|
|
visibility,
|
|
|
|
style: unref(style),
|
|
|
|
classes,
|
|
|
|
onClick,
|
|
|
|
canClick,
|
|
|
|
small,
|
|
|
|
mark,
|
2023-02-26 00:48:36 +00:00
|
|
|
id,
|
|
|
|
...decoratedProps
|
2022-03-13 22:09:09 +00:00
|
|
|
};
|
2022-02-27 19:49:34 +00:00
|
|
|
};
|
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
|
|
|
}
|