The objectives array awards points when a spacecraft reaches a state, rather than when an operator answers a question. An objective is the inverse of an event: an event runs on the clock and changes the spacecraft, while an objective watches the spacecraft and changes the score.
Objectives and questions both feed the same per-team score. Questions score what a team knows; objectives score what a team does. A scenario can use either, both, or neither.
Minimal objective
"objectives": [
{
"enabled": true,
"name": "Battery Recovered",
"description": "Restore the battery to at least 80% charge.",
"type": "spacecraft",
"assets": [],
"target": "Battery",
"target name": "",
"variable": "Charge Fraction",
"operation": ">=",
"value": "0.8",
"award": {
"points": 25.0,
"repeatable": false
}
}
]Keys are case-insensitive on load. Studio writes objective keys in lowercase, unlike the PascalCase it uses inside events, so prefer lowercase here to keep hand-authored files looking like exports.
Fields
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | boolean | true | A disabled objective never fires and never pays out |
name | string | "Objective" | Label shown on the timeline and used as the award reason when description is empty |
description | string | "" | What the team must do; shown to operators unless hidden is true |
type | string | "spacecraft" | Only spacecraft is implemented; treat as reserved |
assets | string[] | [] | Asset IDs this applies to; empty means every spacecraft |
target | string | "" | Component instance name or class alias to watch; required |
target name | string | "" | Narrows target to components with this name |
variable | string | "" | The reflected property compared; required |
operation | string | ">=" | The comparison operator |
value | string | "" | The value compared against, written as a string |
min_time | number | omitted | Lower simulation-time bound for scoring |
max_time | number | omitted | Upper simulation-time bound for scoring |
award.points | number | 0.0 | Points added; negative subtracts, which is how a penalty is written |
award.repeatable | boolean | false | Whether a team can earn it more than once per run |
hidden | boolean | false | Hides the name and description until the team completes it |
Do not author an id. Studio generates one on load to pair the file back up with what it builds.
NOTE
You author one objective. Studio expands it into one generated event per resolved component, on every spacecraft it applies to, each wired to the team that owns that spacecraft. You never write anything per team; teams are discovered from the spacecraft that resolve.
Choosing a target
target resolves exactly like an event Target:
- An exact component name (
components[].name) is matched first, ignoring case. - Otherwise the string is treated as a class alias such as
BatteryorSolar Panel, matching every component of that class. - If
target nameis set, the matches are narrowed to components carrying that name.
A - suffix watches a model on the component, for example "Battery-BatteryLeakageErrorModel". Unlike an event, an objective never attaches a model; it only observes, so a component without that model is skipped.
In the Studio Objectives tab these keys appear as three separate fields: Target Class and Target Component together form target and target name, and Target Model adds the -Model suffix.
See Writing Spacecraft Events for the shared target and target-name rules, and Defining Components for the class aliases.
Choosing a variable
variable is the reflected name of a property on the component or model, not a Data key and not a telemetry field. Matching ignores case and strips spaces, so "Charge Fraction" and "chargefraction" reach the same property. Nothing else is normalized.
Read-only properties are allowed, which is the main difference from events: an event must write a settable property, while an objective only reads. The most useful state to score on is often read-only, such as Charge Fraction on a battery, Is Docked on a docking adapter, Amount on a fuel tank, or Allocated on storage.
| Variable type | Watchable | Notes |
|---|---|---|
| Float, Int | Yes | The usual cases |
| Bool | Yes | Compared as 1 / 0 |
| Enum | Yes | Compared as its integer index |
| Vector2/3/4 | Yes | Reduced to magnitude before comparison |
| String, Guid, DateTime, references, arrays | No | Cannot be watched |
Studio’s objective editor lists every variable the chosen target exposes for the build you are running. Confirm names there before relying on them.
Writing the condition
operation accepts a symbol or a long name:
| Operation | Fires while |
|---|---|
>= | value is at or above the threshold |
<= | value is at or below the threshold |
> | value is strictly above the threshold |
< | value is strictly below the threshold |
== | value matches within a tolerance of 1e-6 |
!= | value differs by more than 1e-6 |
passes | value crosses the threshold in either direction |
Any other operator parses but never evaluates true, so the objective silently never scores.
value is always a string, decoded by the variable’s type:
| Variable type | Write | Example |
|---|---|---|
| Float | the number | "0.8" |
| Int | the integer | "3" |
| Bool | "true" or "false" | "true" |
| Enum | the integer index | "0" |
| Vector | a single number, compared against magnitude | "250.0" |
WARNING
Do not use
==on a continuously varying float; the1e-6tolerance means it almost never matches. Use>=or<=with a threshold. For a bool, the only string that means true is the literal texttrue;"1"reads as false.
The condition is evaluated every simulation step while running, and the objective fires on the step the condition becomes true. It re-arms once the condition stops holding.
Limiting by time
min_time and max_time gate the condition by elapsed simulation time:
{
"name": "Recover During Contact",
"target": "Battery",
"variable": "Charge Fraction",
"operation": ">=",
"value": "0.9",
"min_time": 300.0,
"max_time": 900.0,
"award": { "points": 25.0, "repeatable": false }
}| Keys present | Permitted time |
|---|---|
| Neither | Any time |
min_time only | Strictly after min_time |
max_time only | Strictly before max_time |
| Both | Between the two values, inclusive |
0 is a valid time, so write "min_time": 0 only when that is intentional, and omit the key to disable the bound.
The award
Points go to the team that owns the spacecraft carrying the component.
award.repeatable: falsepays the team at most once per run, scoped to the team rather than the component.award.repeatable: truepays on every entry into the condition.award.pointscan be negative to write a penalty.
{
"name": "Battery Depleted",
"description": "Do not let the battery fall below 5% charge.",
"target": "Battery", "variable": "Charge Fraction",
"operation": "<=", "value": "0.05",
"award": { "points": -20.0, "repeatable": false }
}A hidden objective still scores. Operators see that a secret objective exists and its point value, but its name and description are withheld until their team completes it. The award history clears on reset.
WARNING
Ask whether the condition already holds at
t=0. A once-only objective whose condition is true at the start is claimed in the first step for doing nothing. Either invert the condition, usemin_time, or pair it with an event that first puts the spacecraft into the state the team must recover from.
When an objective never scores
Work down this list, roughly ordered by how often each is the cause:
- the variable name is wrong
- the variable is not a watchable type
- the target resolves to nothing
target namenarrowed it to nothing- the model is not already attached
- the spacecraft has no team to pay, such as a neutral asset
assetsdoes not match anassets.space[].id- the operator never evaluates
==was used on a float- a bool
valueis not the literal texttrue - the time window is inactive
- it already fired this run
- it is disabled, or the simulation is not running