From 45989b4aa6ef492503b929757979608bbf391a4e Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 10 Mar 2024 09:23:12 -0500 Subject: [PATCH 1/6] Add page in advanced concepts on Boards --- docs/.vitepress/config.js | 1 + docs/guide/advanced-concepts/boards.md | 193 ++++++++++++++++++ .../advanced-concepts/creating-features.md | 2 +- 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 docs/guide/advanced-concepts/boards.md diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 1e91d21f..88394252 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -84,6 +84,7 @@ module.exports = { text: "Advanced Concepts", collapsed: false, items: [ + { text: "Boards", link: "/guide/advanced-concepts/boards" }, { text: "Creating Features", link: "/guide/advanced-concepts/creating-features" }, { text: "Dynamic Layers", link: "/guide/advanced-concepts/dynamic-layers" }, { text: "Nodes", link: "/guide/advanced-concepts/nodes" } diff --git a/docs/guide/advanced-concepts/boards.md b/docs/guide/advanced-concepts/boards.md new file mode 100644 index 00000000..68dfab0e --- /dev/null +++ b/docs/guide/advanced-concepts/boards.md @@ -0,0 +1,193 @@ +# Boards + +The Board component allows you to make a pannable and zoomable "board" of components, called nodes. Instead of laying things out using the DOM, everything inside a board should be absolutely positioned. There are various utilities included in [board.ts](/api/modules/game/boards/board) to assist with implementing common behaviors with boards. Also, most of these code snippets are modified from [this Board example](https://code.incremental.social/thepaperpilot/Profectus/src/branch/board-example), which may make a useful reference while implementing your own boards. + +To get started with a board, with a node that's just an upgrade locked to a specific location, it would look like this: + +```ts +const upgrade = createUpgrade({ + class: "board-node", + style: "transform: translate(100px, 100px)", + /** snip **/ +}); + +/** snip **/ + +// Render function +jsx(() => + {render(upgrade)} +); +``` + +## Selecting Nodes + +There is a common (not Board specific) utility for creating a ref, along with a setter and "clearer" that works perfectly for situations where you'd like to let the player select at most 1 of a group of like things. To use it, you'll want to clear the selection on mouse down on either a node or the Board itself, and set the selection on mouse up on a selectable node. + +Note you'll typically want to store an ID rather than the node itself, so that if you make the selection persistent you can still easily determine which node was chosen, even if all their other properties are identical. The easiest way to get guaranteed unique IDs for every node is to include an `id` property on every node and use the [setupUniqueIds](/api/modules/game/boards/board#setupUniqueIds) utility which will give you a ref with the value of a valid unique ID you can use for any newly created node. + +## Dragging Nodes + +Draggable nodes are substantially more complicated. Ultimately you'll want to use either [setupDraggableNode](/api/modules/game/boards/board#setupDraggableNode) and, if applicable, [makeDraggable](/api/modules/game/boards/board#makeDraggable), but from there hooking everything up is still a fairly manual process. + +Similar to selecting nodes, draggable nodes should also typically be based on IDs. Assuming your nodes have x and y properties for their actual position, your `setupDraggableNode` call should look something like this: + +```ts +const board = ref>(); + +// You can use any other method to map IDs to the actual nodes +const nodesById = computed>(() => + nodes.value.reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {}) +); + +const { startDrag, endDrag, drag, nodeBeingDragged, hasDragged, dragDelta } = + setupDraggableNode({ + board, + getPosition(id) { + return nodesById.value[id]; + }, + setPosition(id, position) { + const node = nodesById.value[id]; + node.x = position.x; + node.y = position.y; + } + }); +``` + +The type hints in your IDE should clarify how to hook up or use each of the returned properties, but we'll go over them here as well. On mouse down on a draggable node, call `startDrag`. Whenever the mouse goes back up or leaves the board, call `endDrag`. And `drag` itself gets called when the mouse is moved over the board. In all, your Board element should look like this, including the ref property used to pass the component to `setupDraggableNode`: + +```ts + +``` + +Note that if you'd like to have nodes be both draggable _and_ selectable, you should also include `onMouseDown={deselect}`. Your mouse up and down listeners on the nodes themselves should look something like this: + +```ts +function mouseDown(e: MouseEvent | TouchEvent, node: MyNodeType) { + if (nodeBeingDragged.value == null) { + startDrag(e, node.id); + } + deselect(); +} +function mouseUp(e: MouseEvent | TouchEvent, node: MyNodeType) { + if (!hasDragged.value) { + endDrag(); + select(node.id); + e.stopPropagation(); + } +} +``` + +When dragging, you'll often want to ensure that the node being dragged is always on top of all the others, and when you stop dragging it doesn't just jump down several layers. Rather than re-ordering the elements, which can break CSS transitions, it's recommended to specify the z index of each node. An easy way to do that is by setting the initial z index of any new node to its ID. Then on mouse down update the z indices so that the current node being dragged shifts all nodes between it and the "top" z index. Keep in mind this assumes the indices are contiguous, which you can check for and ensure when removing nodes. + +```ts +const oldZ = node.z; +nodes.value.forEach(node => { + if (node.z > oldZ) { + node.z--; + } +}); +node.z = nextId.value; +``` + +Now to handle rendering the node being dragged appropriately. If they're being dragged, you'll want to account for that drag when calculating the position to render at. Check if the node is being dragged, and if so add the `dragDelta` components to its position. To that end, you might write a function like this to get the translate component of a CSS transform rule: + +```ts +function translate(node: NodePosition, isDragging: boolean) { + let x = node.x; + let y = node.y; + if (isDragging) { + x += dragDelta.value.x; + y += dragDelta.value.y; + } + return ` translate(${x}px,${y}px)`; +} +``` + +Finally, if you have any existing features that you'd like to make draggable, the `makeDraggable` utility lets you do just that so you don't have to worry about not being able to directly hook onto the feature's mouse down or up listeners and other configuration. It typically just requires passing along several of the properties you got from the `setupDraggableNode` call, plus some additional callbacks to allow for still retaining the original feature's interactivity. Here's an example of making an Upgrade feature draggable: + +```ts +makeDraggable(upgrade, { + id: "my-upgrade-id", + endDrag, + startDrag, + hasDragged, + nodeBeingDragged, + dragDelta, + onMouseUp() { + if (!hasDragged.value) { + upgrade.purchase(); + } + } +}); +``` + +## Dropping Nodes onto Other Nodes + +To support dropping nodes onto other nodes, you'll need to provide up to 3 new fields to `setupDraggableNode`: `receivingNodes`, the list of all nodes that the currently dragged node can be dropped upon; `dropAreaRadius`, the radius of the circular drop zone around each receiving node; and of course `onDrop`, the actual callback for when a node gets dropped on a receiving node. + +You'll then typically want to use the returned `receivingNode` and `receivingNodes` properties to display some indicator that anything in `receivingNodes` can receive the currently dragged node, and that if they let their mouse go right now, it'd drop into `receivingNode` specifically. In all, that might look something like this, as part of the node's display: + +```ts +{receivingNodes.value.includes(node.id) && ( + +)} +``` + +## Common Display Components + +The board system is intended to be very generic, allowing you to make whatever sort of components you like. Of particular note, SVG components are first-class citizens and incredibly easy to design completely custom displays with (and is what this section will be focused on since rendering vue features is straightforward). There's a utility component called SVGNode that will handle the positioning and listening to both touch and mouse events for you. Any SVG elements should go inside one of these. + +Keep in mind, for performance reasons, it may be beneficial to put many elements in one `SVGNode`, particularly if they don't need to use the event handlers. For example, examples like this where you're rendering lines connecting various nodes: + +```ts +const links = jsx(() => ( + <> + {nodes.value + .reduce( + (acc, curr) => [ + ...acc, + // Replace this with your own logic for determining links to draw + ...curr.links.map(l => ({ from: curr, to: nodesById.value[l] })) + ], + [] as { from: NodePosition; to: NodePosition }[] + ) + .map(link => ( + + ))} + +)); + +// And then in the render function: +{links()} +``` + +Beyond that, just use the standard SVG elements like `rect`, `circle`, `text`, etc. to fully design your nodes. There's also a few other built-in utility components like `CircularProgress` that do common but complex display parts. You may also use the global `node-text` CSS class to make the default `text` elements appear larger and centered. diff --git a/docs/guide/advanced-concepts/creating-features.md b/docs/guide/advanced-concepts/creating-features.md index 3aa46be2..dc5cb36e 100644 --- a/docs/guide/advanced-concepts/creating-features.md +++ b/docs/guide/advanced-concepts/creating-features.md @@ -10,7 +10,7 @@ Most significantly, the base type should typically always have a `type` property 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`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. +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`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 From 5b499c58e27df6eab4d684626e5a2bf8eb573101 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 25 Dec 2024 11:21:25 -0600 Subject: [PATCH 2/6] Update other pages for 0.7 --- docs/.vitepress/config.js | 5 +- docs/guide/advanced-concepts/boards.md | 14 ++--- .../advanced-concepts/creating-features.md | 26 +++++---- .../guide/advanced-concepts/dynamic-layers.md | 2 +- .../creating-your-project/project-entry.md | 2 +- .../creating-your-project/project-info.md | 7 +++ docs/guide/creating-your-project/themes.md | 2 - docs/guide/getting-started/examples.md | 42 +++++++------- docs/guide/getting-started/first-layer.md | 18 +++--- docs/guide/important-concepts/coercable.md | 58 ------------------- docs/guide/important-concepts/features.md | 18 +++++- docs/guide/important-concepts/formulas.md | 2 +- docs/guide/important-concepts/persistence.md | 2 + docs/guide/important-concepts/reactivity.md | 4 +- docs/guide/important-concepts/requirements.md | 11 ++-- docs/guide/index.md | 2 +- docs/guide/recipes/particles.md | 10 ++-- docs/guide/recipes/save-progress.md | 28 ++++----- profectus | 2 +- 19 files changed, 108 insertions(+), 147 deletions(-) delete mode 100644 docs/guide/important-concepts/coercable.md diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 88394252..9943cb0e 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -64,7 +64,6 @@ module.exports = { items: [ { text: "Layers", link: "/guide/important-concepts/layers" }, { text: "Features", link: "/guide/important-concepts/features" }, - { text: "Coercable Components", link: "/guide/important-concepts/coercable" }, { text: "Reactivity", link: "/guide/important-concepts/reactivity" }, { text: "Persistence", link: "/guide/important-concepts/persistence" }, { text: "Requirements", link: "/guide/important-concepts/requirements" }, @@ -87,6 +86,7 @@ module.exports = { { text: "Boards", link: "/guide/advanced-concepts/boards" }, { text: "Creating Features", link: "/guide/advanced-concepts/creating-features" }, { text: "Dynamic Layers", link: "/guide/advanced-concepts/dynamic-layers" }, + { text: "Mixins and Wrappers", link: "/guide/advanced-concepts/mixins" }, { text: "Nodes", link: "/guide/advanced-concepts/nodes" } ] }, @@ -94,7 +94,8 @@ module.exports = { text: "Migrations", collapsed: true, items: [ - { text: "0.5.X to 0.6.0", link: "/guide/migrations/0-6" } + { text: "0.5.X to 0.6.0", link: "/guide/migrations/0-6" }, + { text: "0.6.X to 0.7.0", link: "/guide/migrations/0-7" } ] } ], diff --git a/docs/guide/advanced-concepts/boards.md b/docs/guide/advanced-concepts/boards.md index 68dfab0e..ae572658 100644 --- a/docs/guide/advanced-concepts/boards.md +++ b/docs/guide/advanced-concepts/boards.md @@ -7,14 +7,14 @@ To get started with a board, with a node that's just an upgrade locked to a spec ```ts const upgrade = createUpgrade({ class: "board-node", - style: "transform: translate(100px, 100px)", + style: { transform: "translate(100px, 100px)" }, /** snip **/ }); /** snip **/ // Render function -jsx(() => +() => ( {render(upgrade)} ); ``` @@ -113,11 +113,7 @@ makeDraggable(upgrade, { hasDragged, nodeBeingDragged, dragDelta, - onMouseUp() { - if (!hasDragged.value) { - upgrade.purchase(); - } - } + onMouseUp: upgrade.purchase }); ``` @@ -145,7 +141,7 @@ The board system is intended to be very generic, allowing you to make whatever s Keep in mind, for performance reasons, it may be beneficial to put many elements in one `SVGNode`, particularly if they don't need to use the event handlers. For example, examples like this where you're rendering lines connecting various nodes: ```ts -const links = jsx(() => ( +const links = () => ( <> {nodes.value .reduce( @@ -184,7 +180,7 @@ const links = jsx(() => ( /> ))} -)); +); // And then in the render function: {links()} diff --git a/docs/guide/advanced-concepts/creating-features.md b/docs/guide/advanced-concepts/creating-features.md index dc5cb36e..0404dedc 100644 --- a/docs/guide/advanced-concepts/creating-features.md +++ b/docs/guide/advanced-concepts/creating-features.md @@ -4,17 +4,21 @@ Profectus is designed to encourage the developer to eventually start designing t ## 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` to `ProcessedComputable`. You should be able to use any of the existing features as a reference for how these types look and work. +Every feature has an interface for the feature itself as well as an options object for every constructor the feature has (typically just one). The main difference is that the options object will have computable properties typed as `MaybeRefOrGetter` but the feature itself will have replaced those with just `MaybeRef`. 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 can use the [getUniqueID](/api/modules/features/feature#getuniqueid) utility to help. +The feature should typically 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 to make sure the options extends `VueFeatureOptions` and the feature itself `VueFeature`. These will handle allowing it to be rendered using the `render` utility function and handle things like style, classes, visibility, a unique ID, and adding its [node](./nodes) to the layer or modal's context. -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. +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 feature object, including the extra properties in the options object, vue feature mixin, and use [processGetter](/api/modules/util/computed#processgetter) on every computable type. You should ensure the feature object `satisfies` the feature interface, and then return it. -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`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. +The vue feature mixin will require a string unique to the feature as well as a function to get a `JSX.Element` for this feature. Typically that just means returning the vue component made for this feature, passing in props from the feature object. + +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 `MaybeRefOrGetter`s, and all functions _will_ be wrapped in `computed`. ## 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(String, Object, Array)`. You'll also want to make sure to `unref` any of these props you use in the template. `style` is an exception though, where it should actually be unreffed inside the `GatherProps` function or else it won't work with computed refs. 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. +Any vue components you write need to do a couple things. Typically they'll need to type each prop directly, but you can just type it as the property on the feature itself. That is, you can't do `defineProps()` but you can do `defineProps<{ display: MyFeature["display"]; }>()`. If there are custom displays this feature may have, you'll want to create a PascalCase variable that is just a function passing the prop into the [render](/api/modules/util/vue#render) utility, like this: + +`const Title = () => render(props.title);` ## Custom Settings @@ -32,22 +36,22 @@ Next you must set the default value of this setting when the settings is loaded, ```ts globalBus.on("loadSettings", settings => { - setDefault(settings, "hideChallenges", false); + 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(() => ( +globalBus.on("setupVue", () => + registerSettingField(() => ( ( + title={ Hide maxed challenges Hide challenges that have been fully completed. - ))} + } onUpdate:modelValue={value => (settings.hideChallenges = value)} modelValue={settings.hideChallenges} /> @@ -62,7 +66,7 @@ If your custom feature requires running some sort of update method every tick, y ```ts const listeners: Record = {}; globalBus.on("addLayer", layer => { - const actions: GenericAction[] = findFeatures(layer, ActionType) as GenericAction[]; + const actions: Action[] = findFeatures(layer, ActionType) as Action[]; listeners[layer.id] = layer.on("postUpdate", diff => { actions.forEach(action => action.update(diff)); }); diff --git a/docs/guide/advanced-concepts/dynamic-layers.md b/docs/guide/advanced-concepts/dynamic-layers.md index 6a6e00ab..af9d162b 100644 --- a/docs/guide/advanced-concepts/dynamic-layers.md +++ b/docs/guide/advanced-concepts/dynamic-layers.md @@ -8,7 +8,7 @@ When procedurally generating layers with similar structures, consider using a ut function getDynLayer(id: string): DynamicLayer { const layer = layers[id]; if (!layer) throw "Layer does not exist"; - return layer as DynamicLayer; // you might need an "as unknown" after layer + return layer as DynamicLayer; } ``` diff --git a/docs/guide/creating-your-project/project-entry.md b/docs/guide/creating-your-project/project-entry.md index 51147885..c2825cdc 100644 --- a/docs/guide/creating-your-project/project-entry.md +++ b/docs/guide/creating-your-project/project-entry.md @@ -10,7 +10,7 @@ This file has 3 things it must export, but beyond that can export anything the c ### getInitialLayers -- Type: `(player: Partial) => GenericLayer[]` +- Type: `(player: Partial) => Layer[]` A function that is given a player save data object currently being loaded, and returns a list of layers that should be active for that player. If a project does not have dynamic layers, this should always return a list of all layers. diff --git a/docs/guide/creating-your-project/project-info.md b/docs/guide/creating-your-project/project-info.md index eb5beedf..5ac63507 100644 --- a/docs/guide/creating-your-project/project-info.md +++ b/docs/guide/creating-your-project/project-info.md @@ -150,3 +150,10 @@ Whether or not to allow the player to pause the game. Turning this off disables - Default: `base64` The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports. + +### disableHealthWarning + +- Type: `boolean` +- Default: `false` + +Whether or not to disable the health warning that appears to the player after excessive playtime (activity during 6 of the last 8 hours). If left enabled, the player will still be able to individually turn off the health warning in settings or by clicking "Do not show again" in the warning itself. diff --git a/docs/guide/creating-your-project/themes.md b/docs/guide/creating-your-project/themes.md index 9e5d9067..f6996c11 100644 --- a/docs/guide/creating-your-project/themes.md +++ b/docs/guide/creating-your-project/themes.md @@ -25,5 +25,3 @@ Toggles whether to display tab buttons in a tab list, similar to how a browser d - Type: `boolean` If true, elements in a row or column will have their margins removed and border radiuses set to 0 between elements. This will cause the elements to appear as segments in a single object. - -Currently, this can only merge in a single dimension. Rows of columns or columns of rows will not merge into a single rectangular object. diff --git a/docs/guide/getting-started/examples.md b/docs/guide/getting-started/examples.md index 26e14cdb..de7a9e5d 100644 --- a/docs/guide/getting-started/examples.md +++ b/docs/guide/getting-started/examples.md @@ -1,36 +1,36 @@ # Example Projects -## Planar Pioneers - -[View Source](https://github.com/thepaperpilot/planar-pioneers/) | [View Project](https://galaxy.click/play/64) - -An incremental game with procedurally generated content. - -## this crazy idea - -[View Source](https://gitlab.com/yhvr/to-be-named) | [View Project](https://galaxy.click/play/94) - -A "hopeless startup simulator", made for the [Profectus Creation Jam](https://itch.io/jam/profectus-creation-jam) - -## Kronos - -[View Source](https://github.com/thepaperpilot/kronos/) - -This is a project that's still under development but is a good resource for things like implementing custom features. - -## TMT-Demo +## Demo [View Source](https://code.incremental.social/profectus/TMT-Demo) | [View Project](https://profectus.pages.incremental.social//TMT-Demo/) A project loosely based off the Demo project for TMT. Uses most of the different features of Profectus, but doesn't have any real gameplay. -## Advent Incremental +## Planar Pioneers + +[View Source](https://github.com/thepaperpilot/planar-pioneers/) | [View Project](https://galaxy.click/play/64) + +An incremental game with procedurally generated content. + +## this crazy idea + +[View Source](https://gitlab.com/yhvr/to-be-named) | [View Project](https://galaxy.click/play/94) + +A "hopeless startup simulator", made for the [Profectus Creation Jam](https://itch.io/jam/profectus-creation-jam) + +## Kronos + +[View Source](https://github.com/thepaperpilot/kronos/) + +This is a project that's still under development but is a good resource for things like implementing custom features. + +## Advent Incremental [View Source](https://github.com/thepaperpilot/advent-Incremental/) | [View Project](https://www.thepaperpilot.org/advent/) An incremental game with 25 different layers of content. A good example of what a large project looks like. There's also a partial port to 0.6 available [here](https://github.com/thepaperpilot/advent-Incremental/tree/next). -## Primordia +## Primordia [View Source](https://github.com/Jacorb90/Primordial-Tree) | [View Project](https://jacorb90.me/Primordial-Tree/) diff --git a/docs/guide/getting-started/first-layer.md b/docs/guide/getting-started/first-layer.md index 1b996d24..fbdd9cdd 100644 --- a/docs/guide/getting-started/first-layer.md +++ b/docs/guide/getting-started/first-layer.md @@ -14,9 +14,9 @@ The `createLayer` function will need a unique ID for your layer and a function t ```ts const id = "p"; -const layer = createLayer(id, function (this: BaseLayer) { +const layer = createLayer(id, layer => { return { - display: jsx(() => <>My layer) + display: () => <>My layer }; }); ``` @@ -37,16 +37,16 @@ In your IDE you'll be able to see the documentation for each parameter - in this ```ts const id = "p"; -const layer = createLayer(id, function (this: BaseLayer) { +const layer = createLayer(id, layer => { const points = createResource(0, "prestige points"); return { points, - display: jsx(() => ( + display: () => ( <> - )) + ) }; }); ``` @@ -56,12 +56,12 @@ const layer = createLayer(id, function (this: BaseLayer) { Some things happen every tick, such as passive resource generation. You can hook into the update loop using an event bus. There's a global one and one for each layer. For example, within the layer function, you can add this code to our example to increase our points at a rate of 1 per second: ```ts -this.on("update", diff => { +layer.on("update", diff => { points.value = Decimal.add(points.value, diff); }); ``` -Note that within the `createLayer`'s function, `this` refers to the layer you're actively creating, and the `diff` parameter represents the seconds that have passed since the last update - which will typically be around 0.05 unless throttling is disabled in the settings. If we wanted to generate an amount other than 1/s, we could multiply diff by the per-second rate. +Note that the `createLayer`'s function receives a parameter for the base layer, which you may have to add. The `diff` parameter insidde the event callback represents the seconds that have passed since the last update - which will typically be around 0.05 unless throttling is disabled in the settings. If we wanted to generate an amount other than 1/s, we could multiply diff by the per-second rate. ## Adding an upgrade @@ -85,12 +85,12 @@ We'll add this upgrade to our returned object and our display. Upgrades are a re return { points, myUpgrade, - display: jsx(() => ( + display: () => ( <> {render(myUpgrade)} - )) + ) } ``` diff --git a/docs/guide/important-concepts/coercable.md b/docs/guide/important-concepts/coercable.md deleted file mode 100644 index 23a321d0..00000000 --- a/docs/guide/important-concepts/coercable.md +++ /dev/null @@ -1,58 +0,0 @@ -# Coercable Components - -Most times a feature has some sort of dynamic display, it'll allow you to pass a "Coercable Component", or rather, something that can be coerced into a Vue component. This page goes over the different types of values you can use - -## Template Strings - -If you provide a string, it will be wrapped in a component using it as the template. This is the simplest method, although not suitable for complex displays, and realistically cannot use Vue components as none are registered globally (by default). Recommended for static or simple dynamic displays, such as displays on features. - -Template strings need to be wrapped in some HTML element. By default, they'll be wrapped in a `` element, although certain features may wrap things in div or header elements instead, as appropriate. - -## Render Functions (JSX) - -You can provide a render function and it will be wrapped in a component as well. The intended use for this is to write JSX inside a function, which will get automatically converted into a render function. You can read more about that process on the Vue docs on [Render Functions & JSX](https://vuejs.org/guide/extras/render-function.html#render-functions-jsx). Note that JSX must be returned in a function - it does not work "standalone". The CoercableComponent type will enforce this for you. Also of note is that you can use `<>` and `` as wrappers to render multiple elements without a containing element, however keep in mind an empty JSX element such as `jsx(() => <>)` is invalid and will fail to render. - -JSX can use imported components, making this suited for writing the display properties on things like Tabs or Layers. There are also built-in functions to `render` features (either as their own or in a layout via `renderRow` and `renderCol`), so you don't need to import the Vue component for every feature you plan on using. - -Typically a feature will accept a `Computable`, which means functions would (normally) be wrapped in a computed (see [Computable](./reactivity#computable) for more details). This would break render functions, so when passing a render function as a CoercableComponent it must be specially marked that it shouldn't be cached. You can use the built-in `jsx` function to mark a function for you. - -#### Example - -```tsx -{ - display: jsx(() => ( - <> - - {render(resetButton)} - {renderRow(upgrade1, upgrade2, upgrade3)} - - )), -} -``` - -### Slots and Models - -Modals and other features that utilize slots are a bit trickier in JSX, as each slot must _also_ be JSX. Here's an example utility for creating modals that correctly uses slots: - -```tsx -function createModal(title: string, body: JSXFunction, otherData = {}) { - const showModal = persistent(false); - const modal = jsx(() => ( - (showModal.value = value)} - v-slots={{ - header: () =>

{title}

, - body - }} - /> - )); - return { modal, showModal, ...otherData }; -} -``` - -That example also shows how to use models in JSX, which are a concept in vue for allowing a component to read and write a value. It requires specifying both the model value as well as a function to update it's value. - -## Components - -This one might be the most obvious, but you can also just give it a Vue component to display outright. Keep in mind it will not be passed any props, so it should not depend on any. You can read more about creating Vue components on [Components Basics](https://vuejs.org/guide/essentials/component-basics.html). diff --git a/docs/guide/important-concepts/features.md b/docs/guide/important-concepts/features.md index 7b734856..cdfe358a 100644 --- a/docs/guide/important-concepts/features.md +++ b/docs/guide/important-concepts/features.md @@ -10,8 +10,10 @@ const addGainUpgrade = createUpgrade(() => ({ title: "Generator of Genericness", description: "Gain 1 point every second" }, - cost: 1, - resource: points + requirements: createCostRequirement(() => ({ + resource: noPersist(points), + cost: 1 + })) })); ``` @@ -28,6 +30,18 @@ const upgrades = { addGainUpgrade, gainMultUpgrade, upgMultUpgrade }; const numUpgrades = computed(() => Object.values(upgrades).length); ``` +## Displays + +Most times a feature has some sort of dynamic display, it'll allow you to pass a string or `JSX.Element`, the latter within a function or the former either dynamically or statically. + +### Template Strings + +Providing a string is the simplest method, although not suitable for complex displays and cannot use Vue components. Recommended for static or simple dynamic displays, such as titles or descriptions on features. These strings will be rendered as text and not be parsed as HTML. Some features may wrap the text in header elements or others as appropriate, but this can be overidden by using JSX instead. + +### Render Functions (JSX) + +You can provide a render function to have more control over what is displayed. Here you can include vue components and even other features, and is how the layer's display itself should be defined. You can use `<>` and `` as wrappers to render multiple elements without a containing element. You can read other details about how JSX works in the Vue docs on [Render Functions & JSX](https://vuejs.org/guide/extras/render-function.html#render-functions-jsx), particularly how advanced featuers like slots and models work. + ## Tree Shaking Since Profectus takes advantage of [tree shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking), any type of feature that is not used will not be included in the output of the project. That means users have less code to download, a slight performance boost, and you don't need to worry about feature type-specific settings appearing (such as whether to show maxed challenges). diff --git a/docs/guide/important-concepts/formulas.md b/docs/guide/important-concepts/formulas.md index f5914e98..a7f651ea 100644 --- a/docs/guide/important-concepts/formulas.md +++ b/docs/guide/important-concepts/formulas.md @@ -2,7 +2,7 @@ Profectus utilizes formulas for various features, such as increasing requirements for repeatables and challenges or determining resource gains in conversions. These formulas often need to be inverted or integrated to enable features like buying multiple levels of a repeatable at once or determining when a conversion will increase resource gains. The Formula class can handle these operations, supporting every function Decimal does, while tracking the operations internally. -For example, a cost function like `Decimal.pow(this.amount, 1.05).times(100)` can be represented using a Formula: `Formula.variable(this.amount).pow(1.05).times(100)`. +For example, a cost function like `Decimal.pow(amount, 1.05).times(100)` can be represented using a Formula: `Formula.variable(amount).pow(1.05).times(100)`. ```ts const myRepeatable = createRepeatable(() => ({ diff --git a/docs/guide/important-concepts/persistence.md b/docs/guide/important-concepts/persistence.md index 5dbb9b70..2dd02680 100644 --- a/docs/guide/important-concepts/persistence.md +++ b/docs/guide/important-concepts/persistence.md @@ -11,3 +11,5 @@ It's important for saving and loading these properties for these refs to be in a Additionally, this structure should typically remain consistent between project versions. If a value is in a new location, it will not load the value from localStorage correctly. This is exacerbated if two values swap places, such as when an array is re-ordered. In the event a creator changes this structure anyways, the [fixOldSave](../creating-your-project/project-entry.md#fixoldsave) function can be used to migrate the old player save data to the new structure expected by the current version of the project. As of Profectus 0.6, save data will now report warnings whenever there is redundancy - two locations for the same persistent data, which creates larger saves that can cause issues when loading after updates. To fix redundancies, wrap all but one location for the data in [noPersist](../../api/modules/game/persistence#nopersist). + +One place to look out for specifically is tree nodes, which typically have a persisten `pinned` value and can appear in both the nodes array as well as the links array. diff --git a/docs/guide/important-concepts/reactivity.md b/docs/guide/important-concepts/reactivity.md index 0dd32dd6..b1c78664 100644 --- a/docs/guide/important-concepts/reactivity.md +++ b/docs/guide/important-concepts/reactivity.md @@ -6,8 +6,8 @@ With a proper IDE, such as [Visual Studio Code](../getting-started/setup#visual- Vue's reactivity is probably the "quirkiest" part of Profectus, and not even the documentation makes all of those quirks clear. It is recommend to read [this thread](https://github.com/vuejs/docs/issues/849) of common misconceptions around Vue reactivity. -## Computable +## Optionally computable values -Most properties on features will accept `Computable` values. Computable values can either be a raw value, a ref to the value, or a function that returns the value. In the lattermost case it will be wrapped in `computed`, turning it into a ref. The feature type will handle it being a ref or a raw value by using `unref` when accessing those values. With type hints, your IDE should correctly identify these values as refs or raw values so you can treat them as the types they actually are. +Most properties on features will accept `MaybeRefOrGetter` values. These properties can receive either a raw value, a ref to the value, or a function that returns the value. In the lattermost case it will be wrapped in `computed`, turning it into a ref. The feature type will handle it being a ref or a raw value by using `unref` when accessing those values. With type hints, your IDE should correctly identify these values as refs or raw values so you can treat them as the types they actually are. Because functions are automatically wrapped in `computed` for many properties, it might be expected to happen to custom properties you add to a feature that isn't defined by the feature type. These functions will _not_ be wrapped, and if you want it cached you should wrap it in a `computed` yourself. This does, however, allow you to include custom methods on a feature without worry. diff --git a/docs/guide/important-concepts/requirements.md b/docs/guide/important-concepts/requirements.md index dc51cc6b..fff8a478 100644 --- a/docs/guide/important-concepts/requirements.md +++ b/docs/guide/important-concepts/requirements.md @@ -9,14 +9,13 @@ To create a requirement, you can use one of the provided utility functions like Cost requirements are probably the most common requirement you'll be using. For something with multiple levels, like repeatables, you'll typically want to use a formula for the cost instead of a function, and the input to the formula will be the repeatable's `amount` property. Typically that means the code will look like this: ```ts -createRepeatable(repeatable => ({ - requirements: createCostRequirement(() => ({ - resource: points, - cost: Formula.variable(repeatable.amount).add(1).times(100) - })) +const repeatable = createRepeatable(() => ({ + requirements: createCostRequirement(() => ({ + resource: noPersist(points), + cost: Formula.variable(repeatable.amount).add(1).times(100) + })) })); ``` -Important to note here is the parameter added to the `createRepeatable`'s options function. That is a reference to the repeatable being created, so you can access it's `amount` property in the formula. ## Using Requirements diff --git a/docs/guide/index.md b/docs/guide/index.md index 192ea76e..dfac4047 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -3,7 +3,7 @@ title: Introduction --- # Introduction -Profectus is a web-based game engine. You can write your content using many built in features, write your own features, and build up complex gameplay quickly and easily. +Profectus is a template for web-based games and projects. You can write your content using many built in features, write your own features, and build up complex gameplay quickly and easily. The purpose of creating profectus was to create an easy to use engine that does not create a ceiling for a programmer's personal growth. This engine will grow in complexity with you, empowering you to create increasingly complex designs and mechanics. diff --git a/docs/guide/recipes/particles.md b/docs/guide/recipes/particles.md index 91779e09..37c83768 100644 --- a/docs/guide/recipes/particles.md +++ b/docs/guide/recipes/particles.md @@ -19,11 +19,10 @@ Next, create the particles feature and render it. You'll also want to track the ```ts const particles = createParticles(() => ({ - fullscreen: false, - zIndex: -1, + style: { zIndex: "-1" }, boundingRect: ref(null), onContainerResized(boundingRect) { - this.boundingRect.value = boundingRect; + particles.boundingRect.value = boundingRect; } })); ``` @@ -70,11 +69,10 @@ If you're using hot reloading, you might need to reload the particle effect. Her ```ts const particles = createParticles(() => ({ - fullscreen: false, - zIndex: -1, + style: { zIndex: "-1" }, boundingRect: ref(null), onContainerResized(boundingRect) { - this.boundingRect.value = boundingRect; + particles.boundingRect.value = boundingRect; }, onHotReload() { Object.values(elements).forEach(element => element.refreshParticleEffect()); diff --git a/docs/guide/recipes/save-progress.md b/docs/guide/recipes/save-progress.md index baa1043a..68ab8b03 100644 --- a/docs/guide/recipes/save-progress.md +++ b/docs/guide/recipes/save-progress.md @@ -11,18 +11,16 @@ This recipe will involve modifying the `Save.vue` file within your project to in Let's start with creating the coerced component. For this recipe we're going to make a couple assumptions about what this display should be. We'll assume the text will be more complex than displaying a single value. That is, at different stages of the game progress will be indicated by different metrics. We'll also assume it will be a single line of descriptive text - no images or anything else that would justify making a new .vue component. Breaking these assumptions is left as an exercise for the reader. But for now, with those assumptions in mind, we'll write our component (in the `