Flesh out advanced concepts a bit

This commit is contained in:
thepaperpilot 2023-04-08 10:13:21 -05:00
parent fcff672a1a
commit feba41c3ae
4 changed files with 87 additions and 5 deletions

View file

@ -82,7 +82,8 @@ module.exports = {
collapsed: false,
items: [
{ text: "Creating Features", link: "/guide/advanced-concepts/creating-features" },
{ text: "Dynamic Layers", link: "/guide/advanced-concepts/dynamic-layers" }
{ text: "Dynamic Layers", link: "/guide/advanced-concepts/dynamic-layers" },
{ text: "Nodes", link: "/guide/advanced-concepts/nodes" }
]
}
],

View file

@ -1,5 +1,75 @@
# Creating Features
\# TODO
Profectus is designed to encourage the developer to eventually start designing their own features for use in specific games. Features are designed to work where they require minimal (and typically zero) modifications around the code base - you simply write a single file for the feature, and any vue components it needs, and the act of importing that feature will set everything up. This also means you can share these features with others in entire collections, and any they don't use won't be present in the build output, won't be loaded, and won't affect the project in any way.
Because typescript does not emit JS, if a value is supposed to be a function it is impossible to determine if a given function is the intended value or a function that returns the actual value. For this reason it is not recommended for any feature types to include properties that are `Computable<Function>`s, and all functions _will_ be wrapped in `computed`. The notable exception to this is [JSX](../important-concepts/coercable#render-functions-jsx), which uses a utility function to mark that a function should not be wrapped.
## Creating the Feature
Every feature has a couple of types. They have the feature themselves, a generic version for convenience, and any constructor typically has an options type and a type that gets "added" to it to create the feature itself. These typically involve replacing some types to denote how various properties change from, for example, `Computable<X>` to `ProcessedComputable<X>`. You should be able to use any of the existing features as a reference for how these types look and work.
Most significantly, the base type should typically always have a `type` property pointing to a symbol unique to this feature, so they can be easily differentiated at runtime. If it's a feature that should be renderable, then it'll also need `[Component]` and `[GatherProps]` properties, which describe the vue component to use and how to get the props for it from this feature, as well as a unique ID for the feature's [node](./nodes). You cna use the [getUniqueID](/api/modules/features/feature#getuniqueid) utility to help.
The constructor itself should do several things. They should take their options within a function, so that they're not resolved when the object is constructed. It should return a lazy proxy of the feature, which allows features to reference each other and only resolve themselves once every feature is defined. The constructor should create any persistent refs it may require outside of the lazy proxy - it won't have access to the options at this point, so it should make any it _potentially_ may require. Any that turn out not being needed can be [deleted](/api/modules/game/persistence#deletepersistent). Inside the lazy proxy the constructor should create the options object, assign onto it every property from the base type, call [processComputable](/api/modules/util/computed#processcomputable) on every computable type, and [setDefault](/api/modules/features/feature#setdefault) on any property with a default value. Then you should be able to simply return the options object, likely with a type cast, and the constructor will be complete.
Because typescript does not emit JS, if a property is supposed to be a function it is impossible to differentiate between a function that is itself the intended value or a function that returns the actual value. For this reason it is not recommended for any feature types to include properties that are `Computable<Function>`s, and all functions _will_ be wrapped in `computed`. The notable exception to this is [JSX](../important-concepts/coercable#render-functions-jsx), which uses a utility function to mark that a function should not be wrapped.
## Vue Components
Any vue components you write need to do a couple things. Typically they'll need to type any props that come from computed properties appropriately, for which you can use the [processedPropType](/api/modules/util/vue#processedproptype) utility - using it will look something like `style: processedPropType<StyleValue>(String, Object, Array)`. You'll also want to make sure to `unref` any of these props you use in the template. The template should make sure to include a `Node` component with the feature's ID. Also, if there are custom displays this feature may have, you'll want to convert the `CoercableComponent` into a Vue component inside the setup function, typically using the [computeComponent](/api/modules/util/vue#computecomponent) or [computeOptionalComponent](/api/modules/util/vue#computeoptionalcomponent) utilities.
## Custom Settings
To add a setting to the options menu specific to this feature, you'll need to do three things, all inside the feature's file. First, extend the settings type with the name of the new setting. For example, here's how the challenge feature adds a setting to hide completed challenges:
```ts
declare module "game/settings" {
interface Settings {
hideChallenges: boolean;
}
}
```
Next you must set the default value of this setting when the settings is loaded, which happens during the `loadSettings` event emitted on the [global bus](/api/modules/game/events#globalbus). This is how that looks like for the same `hideChallenges` setting from above:
```ts
globalBus.on("loadSettings", settings => {
setDefault(settings, "hideChallenges", false);
});
```
Finally, you'll need to register a Vue component to the settings menu so the player can actually modify this setting. Here's the example for the `hideChallenges` setting:
```ts
registerSettingField(
jsx(() => (
<Toggle
title={jsx(() => (
<span class="option-title">
Hide maxed challenges
<desc>Hide challenges that have been fully completed.</desc>
</span>
))}
onUpdate:modelValue={value => (settings.hideChallenges = value)}
modelValue={settings.hideChallenges}
/>
))
);
```
## Updating Features
If your custom feature requires running some sort of update method every tick, you'll want to search layers when they're added for any features of this type (using the [findFeatures](/api/modules/features/feature#findfeatures) utility), add an event handler for every `update`/`postUpdate`/`preUpdate`, and clean it up when the layer is removed. Here's how this looks like for the `action` feature:
```ts
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;
});
```

View file

@ -1,8 +1,8 @@
# Dynamic Layers
You can dynamically add and remove layers using the `addLayer` and `removeLayer` functions. Note that removing a layer does not change the player save data in any way, so you can safely add and remove the same layer. In fact, there is a `reloadLayer` to do just that, which is used for when the structure of a layer changes - e.g., adding a new feature.
You can dynamically add and remove layers using the [addLayer](/api/modules/game/layers#addlayer) and [removeLayer](/api/modules/game/layers#removelayer) functions. It's important to note that removing a layer does not affect the player's save data. You can safely add and remove the same layer without losing any progress. For instances where the structure of a layer changes, such as when adding a new feature, use the [reloadLayer](/api/modules/game/layers#reloadlayer) function.
If you're going to be procedurally generating layers, all with a similar structure, it might make sense to use a utility function like the following in order to easily access a correctly typed reference to a layer with a given ID:
When procedurally generating layers with similar structures, consider using a utility function like the one below. This function allows you to access a correctly typed reference to a layer with a specified ID easily:
```ts
function getDynLayer(id: string): DynamicLayer {
@ -11,3 +11,7 @@ function getDynLayer(id: string): DynamicLayer {
return layer as DynamicLayer; // you might need an "as unknown" after layer
}
```
This utility function can streamline your code when dealing with multiple dynamic layers and ensure that you're working with the correct layer type.
When working with dynamic layers you'll need to ensure you can determine what layers should exist when loading a save file, by returning an accurate list from your project's [getInitialLayers](/api/modules/data/projEntry#getinitiallayers) function.

View file

@ -0,0 +1,7 @@
# Nodes
Every feature that is rendered in the DOM should have a `Node` component within it, which registers itself to the closest `Context` component (typically within the`Layer`'s component) and tracks the bounding rect (both size and position) of the DOM element. You can then search for a feature's unique `id` property within `layer.nodes` to get access to the DOM element for that feature, if it currently exists.
This can be used for features with more complex displays, such as particle effects positioned relative to another feature, or drawing links between different nodes.
The bounding rect that will typically be kept up to date and react to things like nodes changing size, or moving because of the window resizing or feature's showing or hiding. However, there are ocassional situations where it may become out of sync, so it's recommended to only use the node system for visual effects, where any glitches will be relatively minor.