How to Write a Story
When Thorium Classic came out, mission timelines were one of the most impactful features. Before Thorium, a mission consisted of a binder, a DVD (or VHS back in the day!), a tactical stack (like a glorified PowerPoint presentation), and maybe some message or sensor presets built into the controls.
The Timeline changed all of that. It eliminated the need for all of those things by integrating them neatly into the simulation. Message sending, lighting and sound effects, the Viewscreen, sensor data, tactical targets... just about everything in the simulation could be automatically adjusted by the timeline at a press of a button from the Flight Director.
Everyone who switched to Thorium has told me how much they love the timeline. It's basically a table-stakes feature for this family of bridge simulators, so of course Thorium Nova will have something like it. But Nova's timeline will have to be significantly different. After all, without a Flight Director, who will advance the timeline?
Mission or timeline?
In Thorium Classic, Missions and Timelines are used interchangeably. In Thorium Nova, they mean distinct things.
Timelines are a sequence of steps that do stuff in a flight. Some timelines can be marked as "Missions" that let them be chosen when a flight is started.
What can a timeline do?
Consider a Flight Director operating a timeline. Typically what happens is the Flight Director recognizes the crew has done a necessary task, and clicks the button to advance the timeline so the mission can continue. To illustrate what kinds of things an automated timeline needs to be able to do, lets look at some examples.
- When the player enters a certain solar system, they should get a long range message from headquarters (like Earth) warning them of danger.
- Destroying the last enemy ship in the area should mark an objective as completed on the Captain's computer.
- Sending the correct remote access code to an enemy ship should deactivate the enemy's shields.
- A group of pirates is holding cargo ships hostage. Moving within 10,000km of the pirates will cause them to attack the hostages (not the player).
- 20 minutes after the player sends a message to headquarters, the player gets a message from headquarters reminding them to send an update (but resets when the player sends a message).
- Refugees from a planet are being brought aboard a fleet. When the number of refugees across all fleet ships reaches a certain number, send a message to the player ship.
Easy for a Flight Director, not so easy for a computer. These are just a few examples, but there are loads of others that you might want to implement in your timeline.
What makes these hard for a computer? The mission writer essentially has to program these cause and effect relationships into the mission before the mission is even run. I don't want to require mission writers to need to learn a programming language, so instead I built out a few components that provide all the power a mission writer might need.
Actions
Let's talk about the "effect" part first. I call these "Actions", both in Thorium Classic and Thorium Nova. Thorium gives you a list of all the possible ways you can change the simulation. You pick one, (such as "Change the alert condition on the ship"), set some parameters (alertLevel: 2), and when the action executes, it does what you told it to.

This part is almost exactly the same as Thorium Classic, with two major differences:
- In Thorium Classic, actions run in parallel and delays are built into the actions themselves. In Thorium Nova, actions run sequentially and
Thorium: Delayis its own action. This makes it much easier to orchestrate timed actions one after another. - There can be more than one player ship in Thorium Nova, so you have to specifically pick which ship you want to perform actions on. Note in the screenshot the "Ship Id" parameter is blank, rendering this action useless.
So how do we fix this? How do we identify a specific ship when there might be several player ships in a flight? How do we identity a specific... anything?
I call the solution "Entity Queries", and it's my favorite part of the new timeline.
What is an Entity?
Pretty much anything in the flight is an entity. Ships are entities. Crew members are entities too. So are stars, planets, ship systems, messages, crew stations. Heck, even the flight itself is an entity!
Entities have a unique id and can each be assigned different components that represent different properties. Components include things like "isShip" or "isPlayerShip" for ships that are controlled by a player. Also, "heat", "size", "position", "isImpulseEngine", "efficiency", etc. Each of these components has different properties that define everything about that entity.
This whole setup is the database of the flight.
Entity Queries
See that sparkle button next to the "Ship Id" input in the image above? That's the "Entity Query" button. It lets you insert any value from any entity in the database into an action parameter. Think of it like a function, where you pass in some search parameters and it returns entity values. Handy!
Take a good look at the screenshot below and see if you can figure out what's going on.

It's overwhelming, I know, but it's also powerful. The top section, labeled "Entity Query" is how you pick the entity you want. First you choose a component from a list of options - components like "Is Player Ship" which is only present on ships controlled by the player. The "Tags" component lets you put tags on just about any entity, which makes it easy to select them later on. Here, we're checking the "tags" property of the "Tags" component to see if it contains the tag "player1". Clicking the "Add Filter" button underneath lets us add even more components to filter by.
So if we read this query, we're getting all player ships that have the tag "player1". This provides us with a list of those entities (if any).
Next we need to choose what value we want. That's what the "Entity Select" section is for. We can either choose "id" (as we're doing here) which gives us the unique ID of the entity, or we can choose any component's value. For example, we could choose to have it return the "name" property from the "Identity" component (likely something like "USS Voyager") but in this case that wouldn't be useful - we just need the id of the ship to send alert level command to.
Query Matches
Finally, we need to choose which of the matched entities we want the value from. If we only expect a single match, we choose "First Match". If we want the value from every match, we choose "All Matches". And if we only want the value from a random match, we choose "Random Match".
Suppose we choose "All Matches" and our query returns multiple entities - what happens then? Thorium will take the values that were selected and execute the action once for every permutation of all of the values. This makes it easy to run an action across a whole bunch of entities all at once.
For example, lets suppose our action is "Alert Level: Update" and our Entity Query selects all entities with the "pirate" tag - that would set the alert level of every pirate ships with that tag.
Nested Queries
Note also that the sparkle button is present on the entity queries too. This lets us nest entity queries within each other, so we can select entities based on other entities. Take this one for example:

There's a lot going on here, but if you start from the center and work your way out, it makes sense. The inner-most Entity Query is just doing "Match All" on any "Is Player Ship" entities - so all player ships. That's being inserted into the "Ship Id" value of the "Is Ship System" component in the parent query - so we're getting all ship system entities associated with the player ships. And then another filter for the "Is Impulse Engine" component filters it to just Impulse Engine systems.
So this entity query gets the id of the first Impulse Engine system associated with any player ship. Makes sense, since this action is for setting the efficiency of a ship system.
Hopefully that gives a glimpse into how powerful the Entity Query system is. They'll be used in a lot of places in Thorium (we'll see another in just a moment), so get familiar with how they work. In the future, I'll add query presets, which let you save common queries so you don't have to build them from scratch every time. Any other suggestions for how to make this simpler, easier to use, or even more powerful are welcome.
That's Actions! Just like in Thorium Classic, we'll see actions pop up in a couple of places, but they'll all work pretty similar to this - pick an action, set the parameters, optionally use an entity query if the action needs it (most will).
And that's just the action part of timelines. What about the "cause" part of "cause and effect"? I call those "Triggers".
Triggers
In principle, Triggers perform Actions when "something" happens. You just have to define the "something", or "condition" as I call them, and the Actions.
There are (currently, I might add more later) three Trigger conditions:
- Event Listener
- Entity Match
- Distance
Event Listeners
If you've used the Triggers feature in Thorium Classic, this one should be familiar to you. When a discrete action happens - like shields are raised, a message is sent, or a damage team is created - the condition passes and the trigger executes.

This might look like the same Action we configured earlier, but instead it configures a condition. More specifically, it configures what parameters should make the condition pass. If you leave both "Ship Id" and "Alert Level" blank, it will match whenever any ship changes its alert condition to any level. By setting both "Ship Id" and "Alert Level", we're saying "Only trigger when the player ship sets its alert level to 1".
This is by far the simplest of the conditions to configure - pick an event, add the matching parameters.
Entity Match
Yep, there's a Trigger condition based on entity queries. This condition checks whether an entity query matches a certain number of entities.

By now you should be able to read this Entity Query. "Match any entity with the 'Is Player Ship' component, and the 'alertLevel' property of the 'Is Ship' component is set to 5." (Yep, the alert condition is stored on the "Is Ship" component - you'll have to spend some time learning what components there are and what data they have. Don't worry, they'll all be documented).
At the bottom, it lets you choose how many matches should activate the trigger. In this case, if there are one or more entities that match, the condition passes. You can also choose 0, 1, or a custom number.
This example especially might seem very similar to the Event Listeners - you might be wondering whether you'd need Entity Match conditions. Event Listener conditions pass when discrete events happen; Entity Match conditions pass when the state of the simulation hits a certain point. So Entity Match works well when:
- You need a trigger to activate in response to simulator-driven changes, such as playing an alarm when a ship system heat is too high.
- Some player actions don't have a corresponding events - entering or leaving a system doesn't trigger an event, but you can use an Entity Match to see if a player's ship is in a specific solar system.
- You want to check a condition across many entities at once, instead of a distinct event that only applies to one entity.
At this point, I don't know what condition will be used more - Event Listeners or Entity Match. Both have uses, so it's good they're both options.
Distance
One thing that's difficult to do with Entity Match is to compare the distance between two entities, but it's crucial to certain situations I want to model with the timeline. So Distance is just that - pick two entities (yep, with entity queries), and measure whether the distance between them is greater than or less than some value.

As you can see, the two entities are a player ship and the planet named "Mars", and the distance match is less than 50,000 km — so if their distance is less than 50,000 km, the trigger will execute.
Multiple Conditions
There might be situations where you only want conditions to execute in certain circumstances. In other words, you want to put multiple conditions together. Triggers let you do that.

This is what the full Trigger configuration looks like. If this looks like a funky action, you'd be right - Triggers are created through an action called "Triggers: Create". For your own convenience, you can label trigger with a name and add tags so you can query them later. (Yep, Triggers are entities too!)
There on the left are the conditions. This Trigger will only execute its actions when all of the conditions pass. So both the Distance and the Entity Match must pass before the trigger executes.
One interesting thing to consider here - a Trigger can only ever have one Event Listener condition, since the simulation can only process one event at a time. There will be ways to have an event happen first and then have another event happen which executes a Trigger, but we won't get in