2022-03-03 21:39:48 -06:00
|
|
|
import Toggle from "components/fields/Toggle.vue";
|
|
|
|
import ChallengeComponent from "features/challenges/Challenge.vue";
|
2022-01-13 22:25:47 -06:00
|
|
|
import {
|
|
|
|
CoercableComponent,
|
|
|
|
Component,
|
2022-02-27 13:49:34 -06:00
|
|
|
GatherProps,
|
2022-01-13 22:25:47 -06:00
|
|
|
getUniqueID,
|
2022-02-27 16:41:39 -06:00
|
|
|
jsx,
|
2022-01-13 22:25:47 -06:00
|
|
|
Replace,
|
|
|
|
setDefault,
|
|
|
|
StyleValue,
|
|
|
|
Visibility
|
2022-03-03 21:39:48 -06:00
|
|
|
} from "features/feature";
|
|
|
|
import { GenericReset } from "features/reset";
|
|
|
|
import { Resource } from "features/resources/resource";
|
|
|
|
import { globalBus } from "game/events";
|
|
|
|
import { persistent, PersistentRef } from "game/persistence";
|
|
|
|
import settings, { registerSettingField } from "game/settings";
|
|
|
|
import Decimal, { DecimalSource } from "util/bignum";
|
2022-01-13 22:25:47 -06:00
|
|
|
import {
|
|
|
|
Computable,
|
|
|
|
GetComputableType,
|
|
|
|
GetComputableTypeWithDefault,
|
|
|
|
processComputable,
|
|
|
|
ProcessedComputable
|
2022-03-03 21:39:48 -06:00
|
|
|
} from "util/computed";
|
|
|
|
import { createLazyProxy } from "util/proxies";
|
2022-01-13 22:25:47 -06:00
|
|
|
import { computed, Ref, unref } from "vue";
|
|
|
|
|
|
|
|
export const ChallengeType = Symbol("ChallengeType");
|
|
|
|
|
|
|
|
export interface ChallengeOptions {
|
|
|
|
visibility?: Computable<Visibility>;
|
|
|
|
canStart?: Computable<boolean>;
|
|
|
|
reset?: GenericReset;
|
|
|
|
canComplete?: Computable<boolean | DecimalSource>;
|
|
|
|
completionLimit?: Computable<DecimalSource>;
|
|
|
|
mark?: Computable<boolean | string>;
|
2022-01-27 22:47:26 -06:00
|
|
|
resource?: Resource;
|
2022-01-13 22:25:47 -06:00
|
|
|
goal?: Computable<DecimalSource>;
|
|
|
|
classes?: Computable<Record<string, boolean>>;
|
|
|
|
style?: Computable<StyleValue>;
|
|
|
|
display?: Computable<
|
|
|
|
| CoercableComponent
|
|
|
|
| {
|
|
|
|
title?: CoercableComponent;
|
|
|
|
description: CoercableComponent;
|
|
|
|
goal?: CoercableComponent;
|
|
|
|
reward?: CoercableComponent;
|
|
|
|
effectDisplay?: CoercableComponent;
|
|
|
|
}
|
|
|
|
>;
|
|
|
|
onComplete?: VoidFunction;
|
|
|
|
onExit?: VoidFunction;
|
|
|
|
onEnter?: VoidFunction;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface BaseChallenge {
|
|
|
|
id: string;
|
|
|
|
completions: PersistentRef<DecimalSource>;
|
|
|
|
completed: Ref<boolean>;
|
|
|
|
maxed: Ref<boolean>;
|
|
|
|
active: PersistentRef<boolean>;
|
|
|
|
toggle: VoidFunction;
|
|
|
|
type: typeof ChallengeType;
|
|
|
|
[Component]: typeof ChallengeComponent;
|
2022-02-27 13:49:34 -06:00
|
|
|
[GatherProps]: () => Record<string, unknown>;
|
2022-01-13 22:25:47 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export type Challenge<T extends ChallengeOptions> = Replace<
|
|
|
|
T & BaseChallenge,
|
|
|
|
{
|
|
|
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
|
|
|
canStart: GetComputableTypeWithDefault<T["canStart"], Ref<boolean>>;
|
|
|
|
canComplete: GetComputableTypeWithDefault<T["canComplete"], Ref<boolean>>;
|
|
|
|
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
|
|
|
|
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
|
|
|
|
goal: GetComputableType<T["goal"]>;
|
|
|
|
classes: GetComputableType<T["classes"]>;
|
|
|
|
style: GetComputableType<T["style"]>;
|
|
|
|
display: GetComputableType<T["display"]>;
|
|
|
|
}
|
|
|
|
>;
|
|
|
|
|
|
|
|
export type GenericChallenge = Replace<
|
|
|
|
Challenge<ChallengeOptions>,
|
|
|
|
{
|
|
|
|
visibility: ProcessedComputable<Visibility>;
|
|
|
|
canStart: ProcessedComputable<boolean>;
|
|
|
|
canComplete: ProcessedComputable<boolean>;
|
|
|
|
completionLimit: ProcessedComputable<DecimalSource>;
|
|
|
|
mark: ProcessedComputable<boolean>;
|
|
|
|
}
|
|
|
|
>;
|
|
|
|
|
|
|
|
export function createActiveChallenge(
|
|
|
|
challenges: GenericChallenge[]
|
|
|
|
): Ref<GenericChallenge | undefined> {
|
|
|
|
return computed(() => challenges.find(challenge => challenge.active.value));
|
|
|
|
}
|
|
|
|
|
|
|
|
export function createChallenge<T extends ChallengeOptions>(
|
2022-02-27 13:49:34 -06:00
|
|
|
optionsFunc: () => T & ThisType<Challenge<T>>
|
2022-01-13 22:25:47 -06:00
|
|
|
): Challenge<T> {
|
2022-02-27 13:49:34 -06:00
|
|
|
return createLazyProxy(() => {
|
|
|
|
const challenge: T & Partial<BaseChallenge> = optionsFunc();
|
2022-01-13 22:25:47 -06:00
|
|
|
|
2022-02-27 13:49:34 -06:00
|
|
|
if (
|
|
|
|
challenge.canComplete == null &&
|
|
|
|
(challenge.resource == null || challenge.goal == null)
|
|
|
|
) {
|
|
|
|
console.warn(
|
|
|
|
"Cannot create challenge without a canComplete property or a resource and goal property",
|
|
|
|
challenge
|
|
|
|
);
|
|
|
|
throw "Cannot create challenge without a canComplete property or a resource and goal property";
|
2022-01-13 22:25:47 -06:00
|
|
|
}
|
2022-02-27 13:49:34 -06:00
|
|
|
|
|
|
|
challenge.id = getUniqueID("challenge-");
|
|
|
|
challenge.type = ChallengeType;
|
|
|
|
challenge[Component] = ChallengeComponent;
|
|
|
|
|
|
|
|
challenge.completions = persistent(0);
|
|
|
|
challenge.active = persistent(false);
|
|
|
|
challenge.completed = computed(() =>
|
|
|
|
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
|
2022-01-13 22:25:47 -06:00
|
|
|
);
|
2022-02-27 13:49:34 -06:00
|
|
|
challenge.maxed = computed(() =>
|
|
|
|
Decimal.gte(
|
|
|
|
(challenge as GenericChallenge).completions.value,
|
|
|
|
unref((challenge as GenericChallenge).completionLimit)
|
|
|
|
)
|
2022-01-13 22:25:47 -06:00
|
|
|
);
|
2022-02-27 13:49:34 -06:00
|
|
|
challenge.toggle = function () {
|
|
|
|
const genericChallenge = challenge as GenericChallenge;
|
|
|
|
if (genericChallenge.active.value) {
|
|
|
|
if (
|
|
|
|
genericChallenge.canComplete &&
|
|
|
|
unref(genericChallenge.canComplete) &&
|
|
|
|
!genericChallenge.maxed.value
|
|
|
|
) {
|
|
|
|
let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
|
|
|
|
if (typeof completions === "boolean") {
|
|
|
|
completions = 1;
|
|
|
|
}
|
|
|
|
genericChallenge.completions.value = Decimal.min(
|
|
|
|
Decimal.add(genericChallenge.completions.value, completions),
|
|
|
|
unref(genericChallenge.completionLimit)
|
|
|
|
);
|
|
|
|
genericChallenge.onComplete?.();
|
|
|
|
}
|
|
|
|
genericChallenge.active.value = false;
|
|
|
|
genericChallenge.onExit?.();
|
|
|
|
genericChallenge.reset?.reset();
|
|
|
|
} else if (unref(genericChallenge.canStart)) {
|
|
|
|
genericChallenge.reset?.reset();
|
|
|
|
genericChallenge.active.value = true;
|
|
|
|
genericChallenge.onEnter?.();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
processComputable(challenge as T, "visibility");
|
|
|
|
setDefault(challenge, "visibility", Visibility.Visible);
|
|
|
|
const visibility = challenge.visibility as ProcessedComputable<Visibility>;
|
|
|
|
challenge.visibility = computed(() => {
|
|
|
|
if (settings.hideChallenges === true && unref(challenge.maxed)) {
|
|
|
|
return Visibility.None;
|
2022-01-13 22:25:47 -06:00
|
|
|
}
|
2022-02-27 13:49:34 -06:00
|
|
|
return unref(visibility);
|
2022-01-13 22:25:47 -06:00
|
|
|
});
|
2022-02-27 13:49:34 -06:00
|
|
|
if (challenge.canStart == null) {
|
|
|
|
challenge.canStart = computed(
|
|
|
|
() =>
|
|
|
|
unref((challenge as GenericChallenge).visibility) === Visibility.Visible &&
|
|
|
|
Decimal.lt(
|
|
|
|
(challenge as GenericChallenge).completions.value,
|
|
|
|
unref((challenge as GenericChallenge).completionLimit)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (challenge.canComplete == null) {
|
|
|
|
challenge.canComplete = computed(() => {
|
|
|
|
const genericChallenge = challenge as GenericChallenge;
|
|
|
|
if (
|
|
|
|
!genericChallenge.active.value ||
|
|
|
|
genericChallenge.resource == null ||
|
|
|
|
genericChallenge.goal == null
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return Decimal.gte(genericChallenge.resource.value, unref(genericChallenge.goal));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (challenge.mark == null) {
|
|
|
|
challenge.mark = computed(
|
|
|
|
() =>
|
|
|
|
Decimal.gt(unref((challenge as GenericChallenge).completionLimit), 1) &&
|
|
|
|
!!unref(challenge.maxed)
|
|
|
|
);
|
|
|
|
}
|
2022-01-13 22:25:47 -06:00
|
|
|
|
2022-02-27 13:49:34 -06:00
|
|
|
processComputable(challenge as T, "canStart");
|
|
|
|
processComputable(challenge as T, "canComplete");
|
|
|
|
processComputable(challenge as T, "completionLimit");
|
|
|
|
setDefault(challenge, "completionLimit", 1);
|
|
|
|
processComputable(challenge as T, "mark");
|
|
|
|
processComputable(challenge as T, "goal");
|
|
|
|
processComputable(challenge as T, "classes");
|
|
|
|
processComputable(challenge as T, "style");
|
|
|
|
processComputable(challenge as T, "display");
|
|
|
|
|
|
|
|
if (challenge.reset != null) {
|
|
|
|
globalBus.on("reset", currentReset => {
|
|
|
|
if (currentReset === challenge.reset && (challenge.active as Ref<boolean>).value) {
|
|
|
|
(challenge.toggle as VoidFunction)();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
challenge[GatherProps] = function (this: GenericChallenge) {
|
|
|
|
const {
|
|
|
|
active,
|
|
|
|
maxed,
|
|
|
|
canComplete,
|
|
|
|
display,
|
|
|
|
visibility,
|
|
|
|
style,
|
|
|
|
classes,
|
|
|
|
completed,
|
|
|
|
canStart,
|
|
|
|
mark,
|
|
|
|
id,
|
|
|
|
toggle
|
|
|
|
} = this;
|
|
|
|
return {
|
|
|
|
active,
|
|
|
|
maxed,
|
|
|
|
canComplete,
|
|
|
|
display,
|
|
|
|
visibility,
|
|
|
|
style,
|
|
|
|
classes,
|
|
|
|
completed,
|
|
|
|
canStart,
|
|
|
|
mark,
|
|
|
|
id,
|
|
|
|
toggle
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
return challenge as unknown as Challenge<T>;
|
|
|
|
});
|
2022-01-13 22:25:47 -06:00
|
|
|
}
|
|
|
|
|
2022-03-03 22:45:25 -06:00
|
|
|
declare module "game/settings" {
|
2022-01-13 22:25:47 -06:00
|
|
|
interface Settings {
|
|
|
|
hideChallenges: boolean;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
globalBus.on("loadSettings", settings => {
|
|
|
|
setDefault(settings, "hideChallenges", false);
|
|
|
|
});
|
2022-02-27 16:41:39 -06:00
|
|
|
|
|
|
|
registerSettingField(
|
|
|
|
jsx(() => (
|
|
|
|
<Toggle
|
|
|
|
title="Hide Maxed Challenges"
|
|
|
|
onUpdate:modelValue={value => (settings.hideChallenges = value)}
|
|
|
|
modelValue={settings.hideChallenges}
|
|
|
|
/>
|
|
|
|
))
|
|
|
|
);
|