The Modding Tree X Dev Log #2

Howdy! Quite a bit has happened since the last devlog! Most notably I took advantage of the incremental game jam to make a game using TMT-X! As part of that I also added a new very complex feature type. Finally I also have a pretty major change to demonstrate with how layers are created.

The Side Project

This is my entry to Summer IGJ 2021. You can play it here, although I’m not sure I’d really recommend it. This idea was a bit overly ambitious, and lacking in actual gameplay. But, if you’d like to check out what a TMT-X project will look like, this is a nice little prototype to explore. It only uses one feature type though, so it’s not super indicative of what you can expect from real mods made once TMT-X is finished.

That one feature type it uses, though, is one you haven’t seen before! This is the new “board” feature. It’s a pannable, zoomable board with nodes on it, that can be very highly customized. The creator can make these nodes draggable, add labels to them, draw lines connecting them, and more. Creating this took most of the time I spent on the jam (which is why there’s so little content), but I’m pretty excited to see what people can make with this mechanic.

That said, I think we can say this feature is still an experimental version, really. I’ll be polishing it up and adding a ton of utility functions and node types for common use cases, at some point before release.

createLayer

Up till now, layers have been created by returning a plain old object, with very minimal typescript support. Basically it could know stuff such that buyables may exist on a layer, and if it does then anything in buyables.data will be a buyable, if it exists. There’s no type safety on getting actual buyables that exist, and for things that can return arbitrary data (for example effect properties can be anything really), you’d need to know what it is for the specific buyable and cast it as so. This was particularly annoying with needing to write a ton of (<snip>.effect as Decimal), none of which would have type safety.

So I’ve been messing with alternatives. Let me assure you, this is a really hard problem. I’m trying to make it infer a ton of information about the layer without the creator needing to annotate everything (ideally not requiring any annotations at all), which makes it really hard. Especially with the way everything can be a function that returns the actual value, but every function needs this to refer to the proxied version of itself. I’ve been testing different stuff for awhile and basically, what I want to do is impossible. Trust me when I say I’ve been thorough. I’ve also been asking for help within the Typescript community on various aspects. They’ve been a massive help, and with them I was able to come up with something pretty close to my original goals. Since there are still some compromises I’m going to say this aspect of TMT-X is also experimental, because I’d like to fix it up later if I can figure out how. Let’s take a look.

The big change is now you call a createLayer function and pass it the massive object we’re all familiar with. It’ll (for the most part) type everything correctly inside the functions inside this object. It’ll return a Layer object that is also appropriately typed. This means you can import its type in other files (and even have circular references!) to get type safety on everything. This means no needing to cast anything(with a few exceptions), and the compiler/IDE will yell at you if you try accessing something that doesn’t exist. Plus, it means autocomplete and intellisense will be able to help you! If this sounds like greek I apologize; it’ll make more sense once you’re using it. Just know this means while writing code this’ll help catch bugs you might not have caught otherwise, and can even help you develop faster.

Here’s an example of defining a very small, incomplete layer, just to show what the syntax looks like:

const prestige = createLayer({
	buyables: {
		11: {
			effect() {
				return 4;
			},
			onClick() {
				console.log("clicked!", this.layer.buyables[11].effect)
			},
			test() {
				assertProxy<Buyable>(this);
				this.effect as number + 3;
				this.layer.buyables[11].onClick();
			},
			amount() {
				return this.player.buyables[11];
			}
		},
		12: {
			effect() {
				if (this.layer.buyables[11].amount > 0) {
					return 4;
				}
				return "none";
			}
		}
	}
});

From here, you could do stuff like use prestige.buyables[12].effect in other layers (or potentially (layers.p as typeof prestige).buyables[12].effect to get around circular references), or call prestige.buyables[11].onClick(). Typescript will know whether that buyable actually exists, what properties exist on the buyable, and what type each of them is - note the code snippet never even needs to annotate any properties or functions. You can basically just write normal javascript and everything “Just Works”.

You might be wondering how you get other things on the same layer though, since prestige won’t exist within the layer object itself. That’s why there is a breaking change to the layer property usually found on this in all of these functions. Instead of being the layer id, it’s now a reference to the layer proxy itself, which through an immense amount of typescript magic/work will also be properly typed even though the object doesn’t exist yet! There’s also this.player which is a reference to the player.layers[layer.id] proxy, also properly typed (so it’ll only have .buyables if the layer actually has any buyables, and it’ll know if you’re accessing a buyable ID that actually exists on that layer).


I’m very excited about all this. It’s all very complex, but I believe I’ve managed to hide all the complexity behind the scenes, and even people who want to add their own features or otherwise modify the engine itself shouldn’t be overwhelmed. There are, unfortunately, two notable caveats of this system.

First off, any custom properties added to a layer won’t be known about by the typescript compiler. You can still add new properties, but you’ll have to cast them when using them (similar to what you’d have to do with any dynamically constructed layers).

const prestige = createLayer({
    ...,
    customProperty() {
        return 4;
    }
});

console.log(prestige.customProperty as number);

And secondly, the this property in these functions actually only has the .layer and .player properties. For the life of me, I can’t figure out how to modify the this variable to be a proxied version of what it would be by default. So instead, in the example above you’ll notice before using this.effect in one of a buyable’s functions, it calls assertProxy<Buyable>(this). This line tells the compiler to treat this as if its a proxied version of Buyable. This works for the most part (albeit slightly annoying), but you’ll also want to note that it won’t be typed appropriately for that specific buyable - it’ll treat it like a generic Buyable, so you’ll need to cast this.effect to its actual type, as well as for any custom properties. Note that if you access this by doing this.layer.buyables[id].effect it’ll be appropriately typed. Although unfortunately you won’t be able to use this.id anymore.


If you’re interested in seeing all the weird and complicated typescript, I’m still working on moving it into the engine itself but here’s the typescript playground for the current version of the scratch work I’ve been doing:

Conclusion

I hope you enjoyed this fairly technical dev log update! Just to speak to how progress is going overall, really I feel like working on typescript stuff can go on for a very long time, but it’s at a point where I think its manageable. My focus now is going to be on implementing the features it still needs, adding the changes that’ve happened to TMT since development on TMT-X has begun, and hopefully releasing by New Years. It may still be a bit rocky (as any 1.0 release can be expected to be haha), but it should be usable.

BTW, if you’re interested in playing around with TMT-X: you can! Some parts may be different amounts of broken, and things may drastically change as I continue development, and of course there’s currently no documentation to lean on. But if you’re okay with all that, you can try out making a TMT-X mod by forking the repo or by clicking this repl.it link. Enjoy! I encourage only those who are very comfortable with javascript try this at this point, due to lack of documentation. If you are comfortable with javascript but don’t understand how part of TMT-X works, by all means ask for help on my discord server.

5 Likes

Was taking another look at the typescript playground before implementing it in TMT-X, and found I could make it quite a bit simpler, and even fixed a couple issues with it! Although I’ve discovered one pretty annoying bug that I’ll need to solve somehow: Some functions default as not being cached, so the creator doesn’t need to specify it manually. Unfortunately, the assertProxy call isn’t properly acknowledging those defaults. I’ll try fixing that today/tomorrow.

Edit: Got it all working!

3 Likes