Objects define everything with gameplay behavior attached. They are used for ships, projectiles, bases, weapons, special effects, spawners and everything else.
The objects defined in a plugin are often called “base objects”. They define the initial properties of an object, like its health and energy, but once a real object is created from it, the attributes of the real object can differ from the values it was based on.
There are five fundamental object types:
There is also a system of templates that helps manage these distinctions.
Probably.
Any new ships, weapons, or locations need to be defined by an object. Also, if an existing object needs new behavior, then that new behavior is usually defined by a new object.
On the other hand, it is possible to make a level pack entirely using existing objects, in which case no new objects are needed.
Objects are in the objects directory and have a .pn extension.
By convention, all objects are within a directory named with a three-letter code. The factory scenario uses a handful of conventions:
Objects intended only as templates for other objects (and not to be used directly) are named tpl/(name).
For example, these are the templates used to define the Ishiman heavy cruiser:
Ships associated with a race are within a directory with the same name as that race. If the ship is of a standard, buildable ship class, then the ship is named (race)/(class). If not, it is named (race)/etc/(type) or possibly (race)/etc/(type)/(description).
For example, the following are all names of Ishiman ships:
Objects which represent fixed locations like planets and space stations are in the loc directory.
Some examples:
Devices and objects associated with them are in the dev directory, grouped by weapon. The device is commonly named dev/(weapon)/gun or dev/(weapon)/turret, and the projectile is commonly named dev/(weapon)/pulse or dev/(weapon)/beam.
For example, these are the objects associated with the Humans’ Magneto Pulse Gun:
Asteroids are in ast.
For example, these are the three asteroid sizes used in Astrotrash Plus!:
Explosions and other special effects are in sfx. These are generally visual and non-interactive.
Some examples:
Among these are four very special objects, sometimes known as the “four blessed objects”. Antares uses these objects directly, but unless you’re doing something very special, it’s fine to use the versions from the factory scenario:
Objects which are no longer used are moved to zzz, or deleted.
An object is a procyon file with the following fields:
Field | Req? | Type |
---|---|---|
template | no | name of an object |
long_name | yes | string |
short_name | yes | string |
tags | no | tags |
notes | no | ignored |
class, race | no | ignored |
portrait | no | name of a picture |
price | yes | money |
build_time | yes | duration |
health | yes | integer |
energy | yes | integer |
shield_color | no | color |
occupy_count | yes | integer |
mass | yes | number |
max_velocity | yes | speed |
thrust | yes | acceleration |
warp_speed | yes | speed |
warp_out_distance | yes | distance |
turn_rate | yes | angular speed |
initial_velocity | no | range of speeds |
initial_direction | yes | range of angles |
autotarget | yes | boolean |
icon | no | icon |
weapons | no | weapons |
destroy | yes | destroy |
expire | yes | expire |
create | no | create |
collide | yes | collide |
activate | no | activate |
arrive | yes | arrive |
target | yes | target |
rotation | no | rotation |
animation | no | animation |
ray | no | ray |
bolt | no | bolt |
device | no | device |
ai | yes | ai |
The name of another object. Antares merges together an object and its template to get the final attributes for that object. For example, each of the following objects uses the previous object as a template:
Antares merges together two maps key by key. If an object and its template both have a value for some key, then it recursively merges the values for that key. If only one has a value for some key, it takes that value. When merging any other values, it takes the object’s value and discards the template’s.
There are several templates in the factory scenario that make it easier to create new objects.
A long-form string naming the object. It should be at most 25 characters, with words capitalized. It is displayed:
Typical values are “Cruiser” or “Heavy Destroyer” for ships, and “PK Beam” or “Fusion Pulse” for weapons.
A short-form string naming the object. It should be at most 8 characters, with all letters capitalized. It is displayed as part of the object info for a player’s control and target objects in the in-game instruments.
Typical values are “CRUISER” or “HVDSTRYR” for ships, and “PKBEAM” or “F PULSE” for weapons.
A tags map. An object’s tags can be referenced from:
Notes about the object. Ignored by Antares, but kept around for informational purposes. It might contain an annotated version of long_name, such as “Cruiser (no warp drive)” or “Transport (fleeing, for chapter 9)”
Ignored by Antares, but kept around for informational purposes. These used to be the way that ships were mapped to different races.
The name of a picture representing this object. If a briefing references this object, this is the picture that the game displays.
Portraits should be 200 pixels wide, and are generally 100 pixels tall.
The amount of money it takes to build this object.
The duration of time it takes to build this object.
The amount of shielding this object has. An object’s health is modified when an object with collide.damage hits it or through the heal action. Objects can also recharge their shields, converting energy into health over time.
When an object’s health is reduced to zero, the object is destroyed.
The amount of energy this object has. An object also has a battery, with 5 times this amount of energy. Energy is used to:
The battery is used to recharge the available energy.
A color shown when the object collides with another object. The stronger the object’s remaining shields, the more completely the color covers the object’s sprite.
Good shield colors are "white", {indigo: 240}, {gold: 240}, or a shade of 240 with any hue that contrasts with the ship’s sprite.
Only meaningful for objects with sprites (rotations and animations).
An integer used in conjunction with the occupy action to capture objects. If the occupy action increases a player’s occupation counter for this object past occupy_count, that player captures the object.
Only meaningful if destroy.neutralize and target.base are both true.
A number which determines how the object reacts to a collision. Collisions are not realistic, as both objects in the collision will bounce backward. The higher an object’s mass, the slower its new velocity will be after the collision.
Values are typically very small: cruisers have a mass of 1.0, the smallest ships, such as fighters, have 0.8, and the largest, such as carriers and gateships, have 2.0.
The maximum speed of the object. Thrust cannot accelerate an object past its max_velocity, but other sources of velocity can, such as initial_velocity or the push action. If zero, the object is completely stationary, and nothing can move it.
Most ships have a max_velocity between 3.0 and 7.0.
The amount of acceleration an object can apply. Thinking objects decide whether or not they want to accelerate. Non-thinking objects always apply full thrust.
Most ships have a thrust between 0.008 and 0.156.
The speed of the object when traveling at warp speed. If zero, the object cannot enter warp.
Only meaningful for thinking objects.
The distance at which an object will start to exit warp in order to arrive at its target. Objects with a high max_velocity, high warp_speed, or low thrust should have a large warp_out_distance.
Most ships have a warp_out_distance between 1000 and 7200.
Only meaningful for objects with a warp_speed.
The rate at which an object can turn. If zero, the object cannot turn. If null, the object has no facing at all, and always applies thrust in the direction of its nearest enemy.
Most ships have a turn_rate between 1.0 and 3.0.
A range of possible speeds for the object at its time of creation. If zero, the object is initially stationary. If null, its max_velocity is used. If a range, a velocity is chosen at random.
Generally, initial_velocity should be null, to defer to max_velocity. Two exceptions are:
A range of angles indicating the possible directions for the object at its time of creation. The initial direction may be relative to some other direction, depending on how it was created.
Typical values are:
If true, this object is created facing its nearest enemy. If initial_direction is not 0, it will be added, making the turret somewhat inaccurate.
Field | Req? | Type |
---|---|---|
shape | yes | “square”, “triangle”, “diamond”, or “plus” |
size | yes | integer |
Determines the icon shown when the zoom level is 1:16 or smaller. The actual pixel size of the icon will be about twice icon.size.
Field | Req? | Type |
---|---|---|
pulse.base | yes | name of an object |
pulse.positions | yes | array of points |
beam.base | yes | name of an object |
beam.positions | yes | array of points |
special.base | yes | name of an object |
special.positions | yes | array of points |
Up to three weapons mounted on the ship. The names weapons.pulse, weapons.beam, and weapons.special refer to weapon 1, weapon 2, and the special weapon of a ship, respectively.
A weapon’s base must be a device. When a ship activates one of its weapons, it executes that weapon’s activate.action with the nearest enemy as the direct object. The weapon itself is never created, though the weapon’s activate.action usually executes a create action to create a projectile.
Each weapon can have up to three positions. These allow the ship to fire from different parts of its sprite, such as its cannons. These positions rotate with the ship. A (+x, +y) value would be in the rear right of the ship, and a (–x, –y) value would be in the front left. If omitted, the weapon fires from the center of the ship.
Only thinking objects use weapons, but a fire action could explicitly activate a non-thinking object’s weapons.
An object’s destroy block specifies how it should act when its health is reduced to zero.
Field | Req? | Type |
---|---|---|
die | yes | boolean |
neutralize | yes | boolean |
release_energy | yes | boolean |
action | yes | array of actions |
If destroy.neutralize is true, then the object’s owner loses control of it, and the object is restored to full health. After it becomes neutral, it executes its destroy.action with itself as the direct object. Until it gains a new owner, the object won’t participate in combat.
If destroy.neutralize is false, then:
Many (most) objects are not permanent and dissappear after some time. The expire block determines when and how they should disappear.
Field | Req? | Type |
---|---|---|
after.age | no | range of durations |
after.animation | yes | boolean |
die | yes | boolean |
action | yes | array of actions |
If expire.after.age is not null, the object is given a random age in that range at creation. After this length of time (unless modified by the age action), the object expires. This is useful for projectiles that should travel some distance and then disappear.
If expire.after.animation is true, and the object is an animation, then the object expires after its animation completes, instead of cycling. This is useful for effects like explosions that should disappear at the end of their animation.
When an object expires, it executes its expire.action with itself as the direct object. Then, if expire.die is true, it removes itself from play.
Note
there is a separate meaning of expire.action used by the land action.
The create block specifies actions that takes place when an object is created.
Field | Req? | Type |
---|---|---|
action | yes | array of actions |
When an object is created, it executes its create.action with itself as the direct object.
The collide block specifies when and how an object should hit other objects and be hit by them.
Field | Req? | Type |
---|---|---|
as.subject | yes | boolean |
as.direct | yes | boolean |
solid | yes | boolean |
edge | yes | boolean |
damage | yes | integer |
action | yes | array of actions |
When two objects overlap, they collide if one object’s collide.as.subject is true and the other’s collide.as.direct is true. The subject object will:
If both objects’s collide.as.subject and collide.as.direct are true, then the collision then happens in the other direction. After that, if both objects’ collide.solid is true, then the objects bounce away from each other according to their mass.
If collide.edge is true, then the object will bounce off the edge of the universe instead of passing through it. Thinking, permanent objects such as ships should set collide.edge to true, and temporary objects like projectiles should set it to false.
Some objects execute actions periodically. The activate block determines when and how they do so. The activate action is also used by devices.
Field | Req? | Type |
---|---|---|
period | no | range of durations |
action | yes | array of actions |
If activate.period is not null, then the object will periodically activate. Each time, a random duration from activate.period is chosen. After that duration, the object executes its activate.action with itself as the direct object.
Weapons can also be activated. When a ship activates one of its equipped weapons, it executes that weapon’s activate.action with itself as the subject object and its nearest foe as the direct object.
Some actions execute actions when they reach certain targets. The arrive block determines when and how they take action.
Field | Req? | Type |
---|---|---|
distance | yes | distance |
action | yes | array of actions |
When an object is within arrive.distance of its target, it executes its arrive.action with its target as the direct object. The object won’t execute it again until after receiving a new target.
It’s not usually useful for an object to react identically when it reaches every target, so an arrive.action usually specifies if.tags.
An object’s target block determines how it interacts with selection and orders.
Field | Req? | Type |
---|---|---|
base | yes | boolean |
hide | yes | boolean |
radar | yes | boolean |
order | yes | boolean |
select | yes | boolean |
lock | yes | boolean |
If target.base is true, then this object is a planet, station, or other strategic location. Bases are selected with the “select base” key instead of “select friendly” or “select foe”.
If target.hide is true, then this object hides nearby objects when zoomed out to 1:16 or further. It does not objects friendly to it or bases. Unless a player also has a nearby object, that player cannot select objects in this object’s vicinity.
If target.radar is true, then this object appears on the player’s radar when nearby.
If target.order is true, then this object can have a target. Without this, target.lock is probably not meaningful.
If target.select is true, then this object can be selected. Without this, target.base and target.order are probably not meaningful.
If target.lock is true, then this object can accept a new target when selected by a player and given an order.
Field | Req? | Type |
---|---|---|
sprite | yes | name of a sprite |
layer | yes | 1, 2, or 3 |
scale | yes | number |
frames | yes | range of integers |
A rotation has a rotation.sprite and picks a frame to display based on the orientation of the object. The angles [0, 360) are mapped to rotation.frames. In order to ensure that the object’s rotation is smooth, the sprite should have at least 24 frames.
The object’s sprite is scaled up or down according to rotation.scale. At 1:1 zoom, a sprite with a scale of 1.0 displays at its original size. A scale of 2.0 makes it twice as large, and a scale of 0.5 halves the size.
Sprites are drawn in layers. Layer 1 is for background objects like planets and stations. Layer 2 is for ships. Layer 3 is for foreground objects like projectiles and explosions.
Field | Req? | Type |
---|---|---|
sprite | yes | name of a sprite |
layer | yes | 1, 2, or 3 |
scale | yes | number |
frames | yes | range of numbers |
direction | yes | “+”, “-”, or “?” |
speed | yes | number |
first | yes | range of numbers |
An animation has an animation.sprite and changes its frame over time. the frame cycles within animation.frames [1] (or expires at the end if expires.after.animation is true)
The animation cycles at animation.speed frames per tick. If animation.direction is "+", the frame index increases over time. If "-", the frame index decreases over time. If "?", each object chooses between "+" and "-" randomly.
The animation starts with a random frame from within animation.first. This is usually set to either 0 (start at the first frame) or the same as animation.frames (start at any random frame in the animation).
An animation’s sprite has a animation.layer and animation.scale. These function the same as in rotation.
Field | Req? | Type |
---|---|---|
hue | no | hue |
to | yes | “object” or “coord” |
lightning | yes | boolean |
accuracy | yes | distance |
range | yes | distance |
Rays are drawn as lines between two points in space. The origin point is locked to the object that created the ray (typically a ship firing a weapon). The end point (ray.to) is either locked to a nearby foe ("object", like the Salrilian Carrier’s T-Space Bolt) or to the relative point a foe occupied at the time the ray was created ("coord", like the Audemedon Carrier’s Trazer Beam).
The ray shimmers, taking on different shades of ray.hue.
The distance from origin to end point may be at most ray.range. If ray.accuracy is non-zero and ray.to is "coord", a random distance up to ray.accuracy is added to the ray’s end point.
If ray.lightning is true, extra zigzags are added to give the ray the appearance of a lightning bolt.
Rays are drawn on top of all sprites.
Field | Req? | Type |
---|---|---|
color | yes | color |
A ray is drawn as a moving line of a given bolt.color. The faster the bolt moves, the longer the line.
Bolts are drawn on top of all sprites.
Field | Req? | Type |
---|---|---|
usage.attacking | yes | boolean |
usage.defense | yes | boolean |
usage.transportation | yes | boolean |
fire_time | yes | duration |
energy_cost | yes | integer |
ammo | yes | integer |
restock_cost | yes | integer |
range | yes | distance |
speed | yes | speed |
direction | yes | “fore” or “omni” |
Devices are not drawn at all. They define weapons wielded by other objects. Generally, the only attributes meaningful for a device are its names, activate.action, and device block.
device.usage determines when an CPU pilot will activate the device:
device.fire_time determines how long must pass after each activation before the device can be activated again.
If device.energy_cost is non-zero, the device consumes that much energy with each activation, and the wielder must have at least that much energy to activate it.
If device.ammo is non-zero, the device has a limited number of charges, and the wielder must have at least one charge remaining to activate it. If device.restock_cost is non-zero, then the wielder can restock the charges, up to half device.ammo, by consuming device.restock_cost energy for each charge.
The values of device.range, device.speed, and device.direction depend on the objects created by the device’s activate.action. They are used as hints to CPU pilots about how to use the weapon, and don’t control the weapon’s actual parameters.
A device is a projectile weapon if it creates rotations, animations, or bolts as projectiles. These weapons take time to hit their target, so CPU pilots need to know how to adjust their aiming.
device.speed is the projectile’s velocity. If the device doesn’t have a fixed velocity, it should be an average velocity.
device.range is the furthest distance that the weapon can hit. It is typically the projectile’s velocity × age. If the device doesn’t have a fixed velocity, or expires in some other manner than expire.after.age, calculating it may be more involved.
If the projectile’s autotarget is true, then the weapon’s device.direction is “omni”. Otherwise, it is “fore”.
A device is a ray weapon if it creates rays as projectiles. These weapons hit targets in any direction instantaneously.
device.range should match the projectile’s ray.range.
device.speed should be inf (infinite speed), as no aiming correction is required.
device.direction should be “omni”.
A device is a direct weapon if it doesn’t create a projectile at all, like the Cantharan gateship’s Electronic Jammer. These weapons act directly on the nearest foe.
device.range should be the furthest effective range of the weapon. device.speed should be inf. device.direction should be “omni”.
Not every device is intended for shooting enemies. Some examples of non-weapons are:
For stealth fields and launching bays, device.range is still relevant. It determines how near an enemy ship need be before the wielder feels threatened and activates the device. device.speed and device.direction are not relevant, as the wielder doesn’t need to aim.
For assault teams, none of the three fields are relevant, as the weapon is fired only as part of arrive.action, never by a CPU pilot.
An object’s AI block determines how CPU players and pilots handle the object. It is generally meaningful only for thinking objects.
Field | Req? | Type |
---|---|---|
combat | yes | ai.combat |
target | yes | ai.target |
escort | yes | ai.escort |
build | yes | ai.build |
The AI combat block determines how a CPU pilot handles the object in battle.
Field | Req? | Type |
---|---|---|
hated | yes | boolean |
guided | yes | boolean |
engages | yes | boolean or map |
engages.if.tags | yes | tags |
engaged | yes | boolean or map |
engaged.if.tags | yes | tags |
evades | yes | boolean |
evaded | yes | boolean |
skill.num | yes | integer |
skill.den | yes | integer |
The main purpose of the AI combat block is to determine whether an object:
A defending object hates any nearby hostile object which has its ai.combat.hated flag set to true. This object will not shoot at a hated object unless it also engages it (and has weapons to engage it with). Instead, it will attempt to get close to that object and touch it. This is used for the energy blob.
An attacking object engages an object that it hates, attempting to shoot it, if the attacking object’s ai.combat.engages and the defending object’s ai.combat.engaged fields allow. This requires:
A defending object evades any nearby hostile object if the attacking object is facing it, the attacking object’s ai.combat.evaded is true, and the defending object’s ai.combat.evades is true.
If ai.combat.guided is true, the object steers towards hostiles, but may lose track of a hostile object if it is no longer in front of the guided object.
skill.num and skill.den introduce some randomness into an CPU pilot’s actions. Whenever a CPU pilot wants to press a virtual key, the likelihood that the keypress succeeds is skill.num / skill.den. The default of 3/21 should be kept for most thinking objects.
The AI target block determines how a CPU player assigns targets to the object.
Field | Req? | Type |
---|---|---|
prefer.base | no | boolean |
prefer.local | no | boolean |
prefer.owner | yes | “any”, “same”, or “different” |
prefer.tags | yes | tags |
force.base | no | boolean |
force.local | no | boolean |
force.owner | yes | “any”, “same”, or “different” |
force.tags | yes | tags |
If prefer.base is true, a CPU player prefers to assign targets with target.base: true. If force.base is true, a CPU player only assigns targets with target.base: true.
If prefer.local is true, a CPU player prefers to assign targets in the vicinity of this object. If force.local is true, a CPU player only assigns targets in the vicinity of this object.
If prefer.owner is "same", a CPU player prefers to assign friendly targets. If "different", a CPU player prefers to assign hostile targets. If force.owner is either value, a CPU player only assigns such targets.
If prefer.tags is non-empty, a CPU player prefers to assign targets with matching tags. If force.tags is non-empty, a CPU player only assigns targets with matching tags.
The AI escort block determines how a CPU player assigns escorts to friendly objects.
Field | Req? | Type |
---|---|---|
class | yes | integer |
power | yes | number |
need | yes | number |
An object is only ever assigned escorts with a lower ai.escort.class than its own. This prevents the possibility of a carrier escorting a fighter, for example. Sample values are 1 (fighters), 3 (cruisers), 5 (gunships), 7 (carriers), 9 (transports), and 10 (planets).
When determining whether an object needs escorts, a CPU player compares that object’s ai.escort.need to the sum of the ai.escort.power of friendly objects in its vicinity. If the friendly escort power does not meet the need, the CPU player attempts to add more escorts.
Field | Req? | Type |
---|---|---|
ratio | yes | number |
needs_escort | yes | boolean |
legacy_non_builder | yes | boolean |
When deciding what to build, a CPU player considers all options, and picks one randomly with a likelihood proportional to each object’s ai.build.ratio. It won’t build anything it can’t assign a target to (per ai.target.force), but otherwise isn’t any smarter about what it builds.
If ai.build.needs_escort is true, then a CPU player won’t build a second object of this type until the first one’s ai.escort.need is satisfied.
(use of legacy_non_builder [2] fields is not recommended. It exists only for compatibility with some Hera-created scenarios)
[1] | In the factory scenario, most animations’ frames are something like {begin: 0.0, end: 5.004} for a six-frame animation. This is not recommended. It actually preserves a bug in Ares, where the last frame would be shown too briefly. In this example, it should be {begin: 0, end: 6} for a six-frame animation, but fixing it would break compatibility. |
[2] | In Antares, all target.base objects can build ships. In Ares, this was usually the case, and legacy_non_builder preserves the exceptions. This matters because CPU players roll a die for each build object to determine what to build, and compatibility requires rolling the same dice in the same order. legacy_non_builder tells Antares not to roll a die even if target.base is true. |