Profectus Dev Log #3

I wasn’t expecting it to be this long, but in fairness writing game engines is pretty tricky. This is by far the largest amount of changes for a single dev log, including the first one. Let’s talk about what’s happened since devlog 2.

Profectus

The biggest thing to announce, is the actual name of the engine. It is called Profectus, which is Latin for progressed or advanced. While clearly reminiscent of incremental games, it also does a good job at underlining what I see as the main goal/appeal of this engine: It is designed to naturally progress in complexity with the creator. That is, it shouldn’t ever feel too restrictive, but should also provide enough support to be used by someone who is still fairly new to JS/game development (although not quite as low of a floor as TMT has). To that end, the tagline you’ll see on the new homepage is “A game engine that grows with you”. You can see more explanation of the design goals of the engine in the guide.

Speaking of, there’s a new homepage! If you go to moddingtree.com (not the forums), you’ll now see information about Profectus, as well as the docs for it. I anticipate we’ll probably squeeze TMT on there once it gets its rebranding with v3.0, but me and Aca will have to discuss that. I’ll be referencing these docs throughout this post: they’re still being written but already contain a lot of the information that would otherwise have gone here.

Beta

At this point, I’d like to officially start encouraging (some) people to use Profectus. While the incomplete docs makes the skill barrier higher than it should be, things do work and the API shouldn’t change too drastically from this point on. So if you feel comfortable with JavaScript and are okay with the occasional issue or breaking change, I’d encourage you to get started! There will probably be breaking changes between now and release, but after that they’ll only happen in major versions (e.g. v1 → v2). If you look at the changelog, there’s already been a couple breaking changes in the 2 patches released in the last week.

What’s Changed

New Layer Syntax

I’ve been mentioning this around the discord server, and sought feedback from mod creators about this (although its changed significantly since that post). But to summarize, I was struggling significantly with how to create layers such that TS could correctly parse through them. The proxies, all the abstract properties, needing to include modified this types everywhere that were based on the entire input object - it was a mess! Last dev log focused on how this would work, but shortly after that point I decided a new approach, that was very different from TMT, could work much better.

The new syntax basically looks like this:

const layer = createLayer(() => {
    const id = "p";
    const name = "Prestige";
    const color = "#4BDC13";
    const points = createResource<DecimalSource>(0, "prestige points");

    const conversion = createCumulativeConversion(() => ({
        scaling: createExponentialScaling(10, 0.5),
        baseResource: main.points,
        gainResource: points,
        roundUpCost: true
    }));

    const reset = createReset(() => ({
        thingsToReset: (): Record<string, unknown>[] => [layer]
    }));

    const treeNode = createLayerTreeNode(() => ({
        layerID: id,
        color,
        reset
    }));

    const resetButton = createResetButton(() => ({
        conversion,
        tree: main.tree,
        treeNode
    }));

    return {
        id,
        name,
        color,
        points,
        display: jsx(() => (
            <>
                <MainDisplay resource={points} color={color} />
                {render(resetButton)}
            </>
        )),
        treeNode
    };
});

This is a simplified example that would create the default “prestige” layer in TMT; a real layer would have many more properties. Now let’s talk about the benefits of this system.

Reactivity

Previously, layers were wrapped in a proxy that would automatically call computed refs instead of the actual functions, unless the function signified it shouldn’t be cached. This was very slow for the typescript compiler to process, and it would often fail and say layers were too complicated to display the types of. When it did show the type, it would get cut off by all the generics being merged together.

The new syntax is fast, and shows you actually useful information.
image

This also means only the properties that are specified as computable, actually are. In exchange you gain access to the refs themselves, which was not possible in the old syntax. So previously if you wanted to “re-use” a ref, you’d have to create a new computed that just points at the original. This adds additional overhead that’s completely unnecessary.

For example, in the old syntax you’d use a function that returns the amount of points you currently have for an upgrade. In the new syntax you just pass the resource itself, which does not create a new computed.

The biggest part of all this, to creators, is Vue reactivity being a first-class citizen, and something creators will be expected to use directly a lot of the time.

Resources

Speaking of, “resources” are a new object I’m very happy about. They’re essentially just a ref<DecimalSource>, but you can specify their display name and data about how to format their amount. Now features that have costs associated with them can accept resources without needing to go through the mess of specifying their location, property name, display name, etc.

Additionally, while the default behavior is to pass createResource an initial value and it then creates a persistent ref. However, you can also pass an existing ref and it’ll just wrap it. This allows you to convert any ref<DecimalSource> into a resource that can be gained and used. For example, you can use a Buyable’s amount ref as a resource and allow upgrades to “cost” buyable amounts, very easily. This can also be used to make more complex types of resources by making computed refs, just make sure to have a setter if you plan on being able to change the amount of this resource.

In TMT, there were (unintuitively) special properties you could add to a layer’s getStartData to start tracking certain stats, such as the total amount of a resource gained, or the best ever value. Since in Profectus you just create the persistent refs without the use of a getStartData function, it made sense to just create utility functions for tracking these kinds of stats. Hence why we have trackTotal(resource) and trackBest(resource) and even trackOOMPS(resource), which each return refs for their current values. While I already like that its less quirky, this has the additional benefit of allowing you to track any resource, including multiple within the same layer. I can’t wait to see projects show how many buyable levels you’re gaining per second ;). The code for tracking best and total is also super simple, which I think illustrates well how easy to use Vue reactivity is.

Cyclical References

Another issue previously was handling cyclical references. These are cases where layers or features use each other in their options objects. The issue comes from how objects are imported. If I have two files that import each other, you have to be careful about when you actually access the values. Essentially, one of them might be an empty object until they’ve both been allowed to fully process their files. What this means in practice is you can only use cyclically dependent variables in functions is fine (so long as you don’t immediately call the function).

While most features avoid this well enough, there are some important exceptions that warranted coming up with a more general solution. The one I’ve come up with is lazy evaluating all layers and features. Essentially, all the createX functions take a function, and the returned object is a proxy that doesn’t actually call the function until the first time a property is accessed. That means when all the layers are imported (usually from projEntry.tsx), none of the code for actually creating the layers is run yet. That way when the layers are actually first used (when loading the player save data), all the layers have successfully been imported and cyclical dependencies aren’t an issue anymore. The same way that works for layers, it works for features as well - by time any specific feature has a property accessed, all the features have already been defined.

Displays

The last time we talked about this was in dev log 1. Displays still look for CoercableComponents, but what constitutes one of those has changed a bit, namely by introducing JSX and disallowing just using the name of a component.

The reason for the former is simply because it was always something I’d wanted to consider, but with the new layer syntax making this a bit less reliable, it seemed like a good time to implement it. Simply wrap a function in the jsx function and Profectus will treat it like a render function, which is what JSX compiles into. JSX has a lot of advantages over, say, creating a dedicated vue component for each layer, but mainly its just simpler. You can still use plain old strings, and are encouraged to do so in the case of static text or dynamic text. JSX is mainly to allow you to use components and other vue features - such as for the layer’s display itself.

There are several components to use in src/components, and many features are renderable. To display a renderable component, simply add {render(feature)} in your JSX. You can also use renderRow() and renderCol() to display rows and columns of renderable features. You can read more details about it on the docs’ Coercable Components page.

Also, previously features would have an “unlocked” property that was pretty inconsistent with its meaning between features and layers. Now everything has a “visibility” property, which takes one of three values stored in an enum: Visible, Hidden, and None. Visible means its shown, None means its not, and Hidden is similar to TMT’s “ghost” value for layer visibility - the component will exist on the page and take up space, but it will be invisible.

Trees and conversion and resets, oh my!

While the switch to the new syntax didn’t drastically change the actual functionality of any features, there are two/three notable exception - although I’d argue it was just incomplete previously. Trees and resets have been drastically changed, including splitting off part of their functionality into a conversion feature type.

Let’s start with resets first. Since challenges reset things, and presumably other features, it made sense to me to not marry resets with trees or conversions. Resets track time since last reset, can be setup to reset automatically, and most importantly have a computable list of things to reset. This is very different from TMT, which asks for a list of things to not reset, in a fairly quirky function you have to know the name of, have to know to call a specific function within, and have to write some particularly annoying code if you want to reset only some features of a specific type.

So for Profectus the idea is to return the things to reset, which can be organized however you like since layers are structured by the creator. For example, if you want to preserve a certain group of upgrades, just store that group in an object in the layer itself, and pass that object as a thing to reset - the reset code will traverse it recursively looking for persistent refs and reset them to their default values.

One tricky thing about resets is they’re often very context sensitive. To this end, challenges and trees set variables prior to calling the reset (e.g. tree’s resettingNode or challenge’s active boolean, so if the computable list depends on them then the list will be re-calculated when its accessed.

Conversions are a feature that convert one resource into another, with a given scaling function. These are not attached to resets, and can be setup to passively generate without actually draining the cost resource. There are functions for creating “cumulative” and “independent” conversions, which are analogous to TMT’s “normal” and “static” reset types, respectively. Unlike TMT the scaling function is not affected by which type you choose, however. There are built in scaling functions for linear and polynomial scaling functions, but more should be easy enough to create and share with the community.

The big takeaway here is allowing these features to work independently of each other. There’s code in the default src/data/common.tsx file to make reset buttons and layer nodes that work like they do in TMT, but the point of putting them there is to emphasize they’re not an assumption Profectus makes about how they are used

Utilities

Part of the whole “extensibility” and “grows with you” goals of the engine really focuses on encouraging creators to find situations where utilities would be useful, and to then be able to create one easily. It’s a good coding practice to get into, naturally progresses creators into writing more complex mechanics, and is just darn useful in general. This is done firstly by having many examples scattered throughout the code, such as the utility to enable holding to rapidly click on a button. They are typically pretty small but versatile, and often return refs to emphasize how useful those are.

There’s also an explicit example file of utlities, that src/data/common.tsx file mentioned earlier, with some “starter” utilities for an anticipated common use case of the engine.

This might be overly optimistic, but I’m hoping to actually see these utilities written and then shared amongst the community, along with any custom feature types creators write.

Status/Docs

This new syntax is considered finished, but may still see more improvements, such as better handling of arrays, in future updates. You can read more about how Profectus works in the docs, which can be considered a living document at this point. In fact, after this there’ll probably just be one more dev log at the official 1.0 release of Profectus - between now and then I’d recommend just checking out the Profectus changelog and reading any new doc pages that pop up. Even while writing this it’s been blurring what I’ve written here vs the docs, and to be totally honest I prefer just writing about how something is, without needing to remember what it was the last time it was discussed. I’m sure there’s probably things that didn’t get mentioned in here despite being plenty notable enough. It’s been a long six months.

The documentation is still very incomplete. The most important guide pages are all written, but most notably there are no API docs. I’m currently working on patching typedoc to use a proxy the same way vue-tsc does, which will allow for nice looking, automatically generated documentation from TSDoc comments in the code, which will also appear in your IDE for quick reference. This should be done Soon :tm: , but no official ETA.

What’s Next?

So… this is late. I was hoping to reach this point by 2021 EOY, and now its March. I really need to get to work on Kronos, so I’m switching gears to that after this. As I’m doing so I’ll undoubtedly be finding bugs, and I’m sure having bugs reported to me - I’ll definitely fix any trivial ones, and harder ones based on how much of an impact they have. Any features I implement in Kronos that make sense to be in the engine itself will be backported.

To be totally honest, once I get the API docs working, it’ll probably be really hard for me to not want to document everything immediately, so that’ll probably happen sooner rather than later.

If I catch back up on Kronos (targeting its release EOY 2022), I can find some time to work primarily on Profectus. There’s still several features I have yet to implement - I’d like to at least reach parity with TMT before 1.0.

As an aside, my basic timeline for Kronos is chapters 1-3 to be implemented (but potentially with some placeholder art still for the story) by end of May. Another couple of months for chapter 4, and another couple for 5. I’m leaving enough room for art to lag behind, but even with the delay that should be a pretty comfortable schedule.

All in all, I think I can expect 1.0 to probably happen around Q3, but no promises. After Kronos I have other big project ideas to work on, but I’ll probably take some time to really polish Profectus first, and get a few more demo projects out for people to use as reference. You can see one at TMT-Demo, which is loosely based off the Demo tree from TMT (as the name would imply).

Well, I think that’s about it! This is a pretty big moment for Profectus, and I’m glad to finally be able to share it with you!

6 Likes