parent
78394b83c4
commit
2c4d95b73d
18 changed files with 537 additions and 2 deletions
package.json
src/features
achievements
bars
challenges
clickables
conversion.tshotkey.tsxinfoboxes
links
particles
reset.tsresources
tabs
trees
|
@ -60,6 +60,6 @@
|
|||
"@rollup/rollup-linux-x64-gnu": "^4.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "21.x"
|
||||
"node": ">=21.x"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,81 @@
|
|||
/**
|
||||
* This is a feature that represents some task the player can achieve a single time. This could be used to show the player their next goal, or be a part of a list of accomplishments to try to achieve. Typically these are things the player is expected to do during normal progression, in which case the achievement should either have no reward or a QoL reward, or something players must go out of their way to accomplish, in which case they'd have a mechanical reward.
|
||||
*
|
||||
* In addition to being renderable on its own, the display will also be used to display a notification when the achievement is earned. If no display is specified, it will generate it from the requirements if they exist.
|
||||
*
|
||||
* There are two ways to handle granting the achievement: the requirements systen, or via manually granting it. The first case would look like this, where the achievement will be earned when a total of 10 points are earned:
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource<DecimalSource>(10);
|
||||
* const total = trackTotal(points);
|
||||
* const totalPoints = createResource(total, "total points");
|
||||
*
|
||||
* const achievement = createAchievement(() => ({
|
||||
* requirements: createCostRequirement(() => ({
|
||||
* cost: 10,
|
||||
* resource: totalPoints,
|
||||
* requiresPay: false
|
||||
* })),
|
||||
* display: {
|
||||
* requirement: "Gain 10 total points",
|
||||
* effectDisplay: "1.1x points gain"
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* The other method of granting achievements is through granting them manually. This is for cases where the achievement happens during an "event" or specific moment in time, where the requirements system ultimately just gets in the way. For example, here's an achievement that gets earned for failing a challenge, which would be hard to implement using the requirements system because no reward is granted:
|
||||
*
|
||||
* ```ts
|
||||
* const achievement = createAchievement(() => ({
|
||||
* display: "Fail a challenge"
|
||||
* }));
|
||||
*
|
||||
* const challenge = createChallenge(() => ({
|
||||
* requirements: ...,
|
||||
* onExit() {
|
||||
* if (challenge.completed.value === false) {
|
||||
* achievement.complete();
|
||||
* }
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* You may be interested in giving achievements some sort of effect to the player. You can make new features visible or enable modifiers based on an achievement's `earned` property like so:
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource<DecimalSource>(10);
|
||||
*
|
||||
* const pointGainModifier = createSequentialModifier(() => [
|
||||
* ...,
|
||||
* createMultiplicativeModifier(() => ({ multiplier: 2, enabled: ach.earned }))
|
||||
* ]);
|
||||
*
|
||||
* const pointGain = computed(() => pointGainModifier.apply(1));
|
||||
* layer.on("update", diff => {
|
||||
* points.value = Decimal.add(points.value, Decimal.times(pointGain.value, diff));
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Another common reward is a multiplier to some resource that increases based on the number of completed achievements. That could look like this:
|
||||
*
|
||||
* ```ts
|
||||
* const numCompletedAchievements = computed(() => [
|
||||
* ach1,
|
||||
* ach2,
|
||||
* ach3
|
||||
* ].filter(ach => ach.earned.value).length);
|
||||
*
|
||||
* const pointGainModifier = createSequentialModifier(() => [
|
||||
* ...,
|
||||
* createMultiplicativeModifier(() => ({
|
||||
* multiplier: () => Decimal.pow(1.1, numCompletedAchievements.value),
|
||||
* enabled: () => numCompletedAchievements.value > 0
|
||||
* }))
|
||||
* ]);
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
|
||||
import Select from "components/fields/Select.vue";
|
||||
import { Visibility } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
|
@ -69,7 +147,7 @@ export interface AchievementOptions extends VueFeatureOptions {
|
|||
onComplete?: VoidFunction;
|
||||
}
|
||||
|
||||
/** An object that represents a feature with requirements that is passively earned upon meeting certain requirements. */
|
||||
/** An object that represents a feature with requirements that is passively earned upon meeting certain requirements. See module for more details on how to use this feature. */
|
||||
export interface Achievement extends VueFeature {
|
||||
/** The requirement(s) to earn this achievement. */
|
||||
requirements?: Requirements;
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
/**
|
||||
* This is a feature that represents some sort of progress. This could be progress towards a goal, a cooldown, a countdown, etc. They are rendered as either horizontal or vertical bars that fill in to represent that progres as a percentage. The direction represents which side the fill will expand _to_. For example, using Direction.Right means at 50% the left half will be filled, and it'll reach the right side only at 100%. That is to say, it fills _to_ the right.
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource("points");
|
||||
*
|
||||
* const bar = createBar(() => ({
|
||||
* direction: Direction.Right,
|
||||
* width: 100,
|
||||
* height: 20,
|
||||
* progress: computed(() => Decimal.div(points.value, 100))
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* This is a highly customizable feature with separate CSS styling for the entire bar, the outline, the fill, and the text (if there's a label over the bar). They're all optional, but a common use case is customizing the colors of the bar:
|
||||
*
|
||||
* ```ts
|
||||
const bar = createBar(() => ({
|
||||
...,
|
||||
fillStyle: {
|
||||
background: "green"
|
||||
}
|
||||
}));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import Bar from "features/bars/Bar.vue";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import { Direction } from "util/common";
|
||||
|
|
|
@ -1,3 +1,80 @@
|
|||
/**
|
||||
* This is a feature that represents a challenge a player can enter into that then has some requirement to complete. It is not passive like {@link Achievement}s but rather must be explicitly started by the player.
|
||||
*
|
||||
* ```ts
|
||||
* const challenge = createChallenge(() => ({
|
||||
* requirements: createCostRequirement(() => ({
|
||||
* requiresPay: false,
|
||||
* cost: 100,
|
||||
* resource: noPersist(points)
|
||||
* }))
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* Sometimes these challenges will involve changing how the game is played, such as by nerfing or disabling certain mechanics. Here's an example of making point gain get square rooted by a challenge:
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource<DecimalSource>(10);
|
||||
*
|
||||
* const pointGainModifier = createSequentialModifier(() => [
|
||||
* ...,
|
||||
* createExponentialModifier(() => ({ exponent: 0.5, enabled: challenge.active }))
|
||||
* ]);
|
||||
*
|
||||
* const pointGain = computed(() => pointGainModifier.apply(1));
|
||||
* layer.on("update", diff => {
|
||||
* points.value = Decimal.add(points.value, Decimal.times(pointGain.value, diff));
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* If a challenge can be completed multiple times with scaling requirements, make sure to reference the amount of completions already performed! For example, this one makes the exponent more severe per completion:
|
||||
*
|
||||
* ```ts
|
||||
* const pointGainModifier = createSequentialModifier(() => [
|
||||
* ...,
|
||||
* createExponentialModifier(() => ({
|
||||
* exponent: () => Decimal.times(0.5, Decimal.pow(0.9, challenge.completions.value)),
|
||||
* enabled: challenge.completed
|
||||
* }))
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* You can give the player a reward based on completing a challenge in much the same way as you implement the effects of the challenge. You can use the `completed` property for whether a challenge has been completed any number of times or the `completions` property for the specific count. For example, something that doubles point gain after each completion:
|
||||
*
|
||||
* ```ts
|
||||
* const pointGainModifier = createSequentialModifier(() => [
|
||||
* ...,
|
||||
* createMultiplicativeModifier(() => ({
|
||||
* multiplier: () => Decimal.pow(2, challenge.completions.value),
|
||||
* enabled: ach.earned
|
||||
* }))
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* If that was a challenge that could only be completed once, the multiplier could have been a flat `2`.
|
||||
*
|
||||
* It's common to have multiple different challenges the player can be in, and sometimes you'll want to make it so the player can only be in one of them at a time. There's a couple utility functions to help with determining what challenge (if any) is the active one. For example, here's a setup of 4 challenges where only one can be active at a time. If there are other similarities between your challenges (e.g. a style object they all share) it might be worth making a wrapper around createChallenge.
|
||||
*
|
||||
* ```ts
|
||||
* const challenge1 = createChallenge(() => ({
|
||||
* canStart: computed(() => !anyChallengeActive.value),
|
||||
* ...
|
||||
* }));
|
||||
* const challenge2 = createChallenge(() => ({
|
||||
* canStart: computed(() => !anyChallengeActive.value),
|
||||
* ...
|
||||
* }));
|
||||
* const challenge3 = createChallenge(() => ({
|
||||
* canStart: computed(() => !anyChallengeActive.value),
|
||||
* ...
|
||||
* }));
|
||||
*
|
||||
* const anyChallengeActive = isAnyChallengeActive([challenge1, challenge2, challenge3]);
|
||||
* ```
|
||||
*
|
||||
* There's also a utility function for "auto-completing" challenges. By default challenges must be manually exited by the player before being considered completed, but this utility can skip that process, and optionally choose to make the player exit or remain in the challenge after completion (useful for challenges that can be completed multiple times, where the only difference between each completion is the goal).
|
||||
* @module
|
||||
*/
|
||||
import Toggle from "components/fields/Toggle.vue";
|
||||
import { isVisible } from "features/feature";
|
||||
import type { Reset } from "features/reset";
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
/**
|
||||
* This is a feature that represents something that can be clicked, but with a cooldown. The remaining cooldown is represented by a {@link Bar}. Holding down the action will also automatically perform the action once the cooldown completes.
|
||||
*
|
||||
* ```ts
|
||||
* const action = createAction(() => ({
|
||||
* duration: 5,
|
||||
* display: "Do something cool",
|
||||
* onClick() {
|
||||
* // Some action
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import ClickableVue from "features/clickables/Clickable.vue";
|
||||
import { findFeatures } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
/**
|
||||
* This is a feature that represents something that can be clicked or held down to perform some action. There can be some requirement required to be able to click as well. This is a pretty generic feature and several other features use this component indirectly.
|
||||
*
|
||||
* ```ts
|
||||
* const numClicks = persistent(0);
|
||||
* const clickable = createClickable(() => ({
|
||||
* display: {
|
||||
* description: "",
|
||||
* title: "Something you can click"
|
||||
* },
|
||||
* onClick: () => numClicks.value++
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import Clickable from "features/clickables/Clickable.vue";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
|
|
|
@ -1,3 +1,57 @@
|
|||
/**
|
||||
* This feature represents an upgrade the player can buy multiple times, typically with scaling requirements.
|
||||
*
|
||||
* The simplest form would be a requirements with a static cost, for something like a consumable:
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource<DecimalSource>(10);
|
||||
*
|
||||
* const repeatable = createRepeatable(() => ({
|
||||
* requirements: createCostRequirement(() => ({ cost: 100, resource: noPersist(points) })),
|
||||
* display: {
|
||||
* title: "Potions",
|
||||
* description: "Each heals you for 10 hp. Max 10."
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* Scaling costs will need to take advantage of a formula:
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource<DecimalSource>(10);
|
||||
* const repeatable = createRepeatable(() => ({
|
||||
* requirements: createCostRequirement((): CostRequirementOptions => ({
|
||||
* cost: Formula.variable(repeatable.amount.value).pow_base(3).times(100),
|
||||
* resource: noPersist(points)
|
||||
* })),
|
||||
* display: {
|
||||
* title: "Point Doubler",
|
||||
* description: "Double your points",
|
||||
* effectDisplay: (): string => formatWhole(Decimal.pow(2, repeatable.amount.value)) + "x"
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* Keep in mind to set {@link CostRequirement.maxBulkAmount} and other properties to handle buying multiple at once, if desired.
|
||||
*
|
||||
* If you want to give the player some benefit based on the number of times they've purchased this upgrade, you can refer to the `amount` property, like so:
|
||||
*
|
||||
* ```ts
|
||||
* const pointGainModifier = createSequentialModifier(() => [
|
||||
* ...,
|
||||
* createMultiplicativeModifier(() => ({
|
||||
* multiplier: () => Decimal.pow(2, repeatable.amount.value),
|
||||
* enabled: () => Decimal.gt(repeatable.amount, 0)
|
||||
* }))
|
||||
* ]);
|
||||
*
|
||||
* const pointGain = computed(() => pointGainModifier.apply(1));
|
||||
* layer.on("update", diff => {
|
||||
* points.value = Decimal.add(points.value, Decimal.times(pointGain.value, diff));
|
||||
* });
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import Clickable from "features/clickables/Clickable.vue";
|
||||
import { Visibility } from "features/feature";
|
||||
import { DefaultValue, Persistent, persistent } from "game/persistence";
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
/**
|
||||
* This feature represents an upgrade the player can purchase.
|
||||
*
|
||||
* ```ts
|
||||
* const upgrade = createUpgrade(() => ({
|
||||
* requirements: createCostRequirement(() => ({
|
||||
* cost: 100,
|
||||
* resource: noPersist(points)
|
||||
* })),
|
||||
* display: {
|
||||
* title: "Do something cool",
|
||||
* description: "You have to buy me to find out what"
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* You can then give this upgrade an effect by referring to its `bought` property, such as by making the next feature visible:
|
||||
*
|
||||
* ```ts
|
||||
* const nextUpgrade = createUpgrade(() => ({
|
||||
* ...,
|
||||
* visible: upgrade.bought
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import { findFeatures } from "features/feature";
|
||||
import { Layer } from "game/layers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
|
|
|
@ -1,3 +1,42 @@
|
|||
/**
|
||||
* This feature represents an exchange of resources based on a formula (any kind, not just a constant exchange rate). This can also handle cases where exchange is continuously produced or without actually spending the input currency. The formula works by including a function that takes the input resource as a Formula variable, and performing any mathematical operations necessary to map that variable to the output amount.
|
||||
*
|
||||
* The default type of conversion is a "cumulative conversion", which means the amount of the output resource gets added onto the current amount of that resource. That is, it accumulates.
|
||||
*
|
||||
* ```ts
|
||||
* const conversion = createConversion(() => ({
|
||||
* baseResource: noPersist(points),
|
||||
* gainResource: noPersist(prestigePoints),
|
||||
* formula: x => x.log10()
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* You can use the `setupPassiveGeneration` utility to auto-convert the resources. A third parameter can be added for the rate (where 1 is the `currentGain` every second) so it can be based off an upgrade, a toggle, how much of a repeatable you've bought, etc.
|
||||
*
|
||||
* ```ts
|
||||
* setupPassiveGeneration(layer, conversion);
|
||||
* ```
|
||||
*
|
||||
* In addition to the default, there is an "independent conversion", which just _sets_ the amount of the output resource irrespective of how much was already there. This is similar to how a leveling system might work, where whenever experience is gained you can "convert" that exp amount into a level amount (without spending the experience, of courese).
|
||||
*
|
||||
* ```ts
|
||||
* const conversion = createIndependentConversion(() => ({
|
||||
* baseResource: noPersist(exp),
|
||||
* gainResource: noPersist(levels),
|
||||
* formula: x => x.log10(),
|
||||
* spend: () => {} // no-op
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* For independent conversions, you can make them "automatic" without the use of the utility by just calling `convert` whenever the base resource changes, like this:
|
||||
*
|
||||
* ```ts
|
||||
* watch(exp, conversion.convert);
|
||||
* ```
|
||||
*
|
||||
* The advantage to doing it this way over just a `computed` property mapping experience to levels, is that conversions will expose information about how much of the base resource is required for the output to go up.
|
||||
* @module
|
||||
*/
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import { InvertibleFormula, InvertibleIntegralFormula } from "game/formulas/types";
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
/**
|
||||
* This feature represents an action the player can do by performing a hotkey. It will appear in the information modal for easy player reference.
|
||||
*
|
||||
* Typically the action should already be performable by pressing a clickable, so that mobile users can perform the action. For this use case, setting up the hotkey should look like this:
|
||||
*
|
||||
* ```ts
|
||||
* const hotkey = createHotkey(() => ({
|
||||
* description: "Clicks some button",
|
||||
* key: "A",
|
||||
* onPress: clickable.onClick!,
|
||||
* enabled: () => isVisible(clickable.visibility ?? true)
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import Hotkey from "components/Hotkey.vue";
|
||||
import { hasWon } from "data/projEntry";
|
||||
import { findFeatures } from "features/feature";
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* This feature represents a collapsible box with information that is optional or referencable. This can be used for adding an optional story to the game, a reference sheet the player might not want visible all the time, etc.
|
||||
*
|
||||
* ```ts
|
||||
* const infobox = createInfobox(() => ({
|
||||
* title: "Some optional lore",
|
||||
* display: "insert a reference to your favorite media here"
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import Infobox from "features/infoboxes/Infobox.vue";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/**
|
||||
* This feature represents lines linking any other features that can be rendered. You can tell it what items to link and it will handle calculating their positions and updating the line between them as needed.
|
||||
*
|
||||
* For {@link Tree}s specifically, you don't need to use links directly as they have a `branches` property that handles rendering lines as well as defines reset propagation.
|
||||
*
|
||||
* ```ts
|
||||
* const links = createLinks(() => ({
|
||||
* links: [
|
||||
* { startNode: ach1, endNode: ach2 },
|
||||
* { startNode: ach3, endNode: ach2 },
|
||||
* { startNode: ach1, endNode: ach3 }
|
||||
* ]
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import type { Position } from "game/layers";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
/**
|
||||
* This feature represents particle effects. They're powered by pixi.js and are highly performant. Great for adding visual flair to your game. You can check out [this recipe](/guide/recipes/particles) for a comprehensive guide to adding particle effects.
|
||||
* @module
|
||||
*/
|
||||
import { Application } from "@pixi/app";
|
||||
import type { EmitterConfigV3 } from "@pixi/particle-emitter";
|
||||
import { Emitter, upgradeConfig } from "@pixi/particle-emitter";
|
||||
|
|
|
@ -1,3 +1,22 @@
|
|||
/**
|
||||
* This feature represents some amount of state that can be reset to their default values, often as part of a prestige mechanic or some resettable minigame.
|
||||
*
|
||||
* ```ts
|
||||
* const reset = createReset(() => ({
|
||||
* thingsToReset: noPersist([upg1, ach2])
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* You can then call `reset` to reset those items, such as on a reset button:
|
||||
*
|
||||
* ```ts
|
||||
* const resetButton = createClickable(() => ({
|
||||
* display: "Reset game",
|
||||
* onClick: reset.reset
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import { globalBus } from "game/events";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
|
|
|
@ -1,3 +1,31 @@
|
|||
/**
|
||||
* This feature represents a resource the player can accumulate, spend, etc.
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource<DecimalSource>(10, "points");
|
||||
* ```
|
||||
*
|
||||
* When passing this resource into _other features_, make sure to wrap it in `noPersist` to ensure there's no issues when saving/loading.
|
||||
*
|
||||
* ```ts
|
||||
* const upgrade = createUpgrade(() => ({
|
||||
* ...,
|
||||
* requirements: createCostRequirement(() => ({
|
||||
* cost: 10,
|
||||
* resource: noPersist(points)
|
||||
* }))
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* You can also turn refs from other features into resources, to still benefit from them having a name and precision:
|
||||
*
|
||||
* ```ts
|
||||
* const repeatable = createRepeatable(() => ({ ... }));
|
||||
*
|
||||
* const repeatableLevels = createResource(repeatable.amount, "levels");
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import { globalBus } from "game/events";
|
||||
import type { Persistent, State } from "game/persistence";
|
||||
import { NonPersistent, persistent } from "game/persistence";
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
/**
|
||||
* This feature represents a single tab inside of a {@link TabFamily}. It's essentially just a wrapper around the tab's display.
|
||||
*
|
||||
* ```ts
|
||||
* const tab = createTab(() => ({
|
||||
* display: () => (<>
|
||||
* <h1>Tab content!</h1>
|
||||
* {render(achievement)}
|
||||
* </>)
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import { MaybeGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
|
|
|
@ -1,3 +1,32 @@
|
|||
/**
|
||||
* This feature represents a group of {@link Tab}s that can be switched between by the player.
|
||||
*
|
||||
* ```ts
|
||||
* const illuminatiTabs = createTabFamily({
|
||||
* first: () => ({
|
||||
* tab: () => (
|
||||
* <>
|
||||
* first tab
|
||||
* {render(upgrade)}
|
||||
* </>
|
||||
* ),
|
||||
* display: "first"
|
||||
* }),
|
||||
* second: () => ({
|
||||
* tab: () => (
|
||||
* <>
|
||||
* second tab
|
||||
* {render(repeatable)}
|
||||
* </>
|
||||
* ),
|
||||
* display: "second"
|
||||
* })
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* If the tab family is the only thing being rendererd in a layer, the tabs will hug the borders of that layer, rather than obey the usual margins.
|
||||
* @module
|
||||
*/
|
||||
import { isVisible } from "features/feature";
|
||||
import { Tab } from "features/tabs/tab";
|
||||
import TabButton from "features/tabs/TabButton.vue";
|
||||
|
|
|
@ -1,3 +1,74 @@
|
|||
/**
|
||||
* This feature represents a tree of nodes, which can be used for skill trees, prestige trees, etc.
|
||||
*
|
||||
* ```ts
|
||||
* const tree = createTree(() => ({
|
||||
* nodes: [
|
||||
* [ aNode ],
|
||||
* [ bNode, cNode ]
|
||||
* ],
|
||||
* branches: [
|
||||
* { startNode: aNode, endNode: bNode },
|
||||
* { startNode: aNode, endNode: cNode }
|
||||
* ]
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* One common use case for this would be including a node for each layer the player can navigate between, and clicking those nodes opens those layers. You can use the `createLayerTreeNode` from within each layer to help with that. Just make that if the nodes themselves are already present on the other layer, when defining the tree you'll need to wrap them in `noPersist`:
|
||||
*
|
||||
* ```ts
|
||||
* // In `prestige` layer:
|
||||
* const treeNode = createLayerTreeNode(() => ({
|
||||
* layerID: id,
|
||||
* color: "#4BDC13"
|
||||
* }));
|
||||
*
|
||||
* // In the main layer:
|
||||
* const tree = createTree(() => ({
|
||||
* nodes: noPersist([
|
||||
* [ prestige.treeNode ]
|
||||
* ])
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* Trees can also handle propagating resets. That is, when certain parts of the game state should reset, and which precise parts are determined by the nodes in the tree. For this to work, you need to include a reset object in each node that represents what content that node controls, and then pick a propagation strategy. The basic ones are to reset everything above or below a certain row, or follow the (directional) branches of the tree:
|
||||
*
|
||||
* ```ts
|
||||
* const treeNode = createLayerTreeNode(() => ({
|
||||
* layerID: id,
|
||||
* color: "#4BDC13",
|
||||
* reset: createReset(() => ({
|
||||
* thingsToReset: (): Record<string, unknown>[] => [layer]
|
||||
* }))
|
||||
* }));
|
||||
*
|
||||
* const tree = createTree(() => ({
|
||||
* nodes: noPersist([
|
||||
* [ prestige.treeNode ]
|
||||
* [ generators.treeNode, boosters.treeNode ]
|
||||
* ]),
|
||||
* branches: noPersist([
|
||||
* { startNode: prestige.treeNode, endNode: generators.treeNode },
|
||||
* { startNode: prestige.treeNode, endNode: boosters.treeNode }
|
||||
* ]),
|
||||
* resetPropagation: defaultResetPropagation
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* You can use the onReset field to reset anything that should _always_ be reset and isn't tied to any specific node. For a game like "The Prestige Tree", this is where you'd reset the "points" currency on the main tab.
|
||||
*
|
||||
* ```ts
|
||||
* const points = createResource<DecimalSource>(0);
|
||||
*
|
||||
* const tree = createTree(() => ({
|
||||
* ...,
|
||||
* onReset() {
|
||||
* points.value = 10;
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
* @module
|
||||
*/
|
||||
import { Link } from "features/links/links";
|
||||
import type { Reset } from "features/reset";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue