Skip to content

Turret

An emplacement: an object whose whole job is to acquire a target and shoot it, and which never moves. Deployable defense towers and autonomous weapon mounts are the same code here - they differ only in where their position comes from (mount).

The engine's beams do the firing; this module only ever writes target_id, through target_shoot. That is what keeps a turret from chasing its victim.

A turret must spawn a hull the mission ships itself

Engine-measured (1.3.5): a behav_station fires only from a hull declared through ship_data_merge_mod. Two stock hulls stayed silent with the identical target_shoot() call; two add-on hulls fired. A turret on stock starbase art is a decorative box.

Beam stats cannot be read or tuned per object

beamRange / beamCount / beamDamage read None on stock engine hulls - they live in the engine's ship table, not the data_set. turret_range() returns what the author configured, and a turret variant with different beams needs its own shipData entry. Keep the configured range in step with the hull, because nothing can check it for you.

Targeting policy

turret_acquire() is the single place the policy lives, in priority order:

rule
1 a designated target (a player or GM order), while it lives and is in range - never re-evaluated
2 the current target, until hold_seconds expires, while inside range * hold_slack
3 otherwise scan: nearest (or weakest) hostile matching the targets role expression

Rule 2 is the whole anti-thrash rule. Without it a turret between two enemies re-picks every scan and effectively never fires.

Allegiance comes from side_hostile_ships, so a ceasefire stops a turret with no tag to keep in sync, and wrecks and surrendered ships are spared automatically.

API

Turrets: emplacements that acquire a target and fire, and never move.

A turret is an ACTIVE space object (npc_spawn) whose entire behavior is "find something worth shooting, point the weapons at it, hold still". The engine's beams do the actual firing - there is no sbs.fire_beam - so all this module ever writes is target_id, via :func:target_shoot. That one fact is what makes a deployable tower and a turret bolted to a ship's hull the same thing: they differ only in where their POSITION comes from (see :mod:sbs_utils.procedural.mount), never in how they fight.

ENGINE-MEASURED (1.3.5, LM_TestRange/maps/test_turret_probe.mast) - three findings shape everything here:

  • target_shoot() ALONE is enough. Writing only target_id, with no throttle and no destination, made an NPC fire (13 hits / 65 damage). Nothing else needs writing, and writing more would make the turret move.
  • A behav_station fires ONLY from a hull declared through ship_data_merge_mod. Both stock hulls tested stayed silent with the identical call; both add-on hulls fired. A turret must therefore spawn art the mission ships itself - see LegendaryMissions/turrets/shipData_turrets.yaml.
  • beamRange / beamCount / beamDamage are MOCK INVENTIONS. They read None on stock engine hulls, because beam stats live in the engine's ship table and not in the object's data_set. So range CANNOT be read off the object or tuned per-object - :func:turret_range returns what the author configured, and each turret variant that needs different beams needs its own hull entry.

Every module-level function here is prefixed, private ones included. MAST imports a module's functions into ONE flat, mission-wide namespace with no underscore filtering, so a helper named _key turns any script's _key = ... into "Variable assignment to a keyword" - which desyncs the compiler and empties the whole story.

No module-level state. Every per-turret value lives in that turret's own inventory, so Agent._remove purges it on delete: config cannot outlive its object, a recycled id cannot inherit stale settings, and there is nothing to register with register_reset_state.

turret_acquire(id_or_obj)

Decide what this turret should be shooting, in priority order.

  1. A DESIGNATED target, while it lives and is in range. Never re-evaluated.
  2. The CURRENT target, until hold_seconds expires, while it is inside range * hold_slack. A marginally closer candidate does not steal it - this is the entire anti-thrash rule, and without it a turret between two enemies flips every scan and effectively never fires.
  3. Otherwise, scan: nearest (or weakest) hostile inside range that matches the targets role expression.

Returns:

Type Description

int | None: The id to engage, or None if there is nothing to shoot.

turret_all()

Every live turret, as a set of ids.

turret_candidates(id_or_obj)

Everything this turret is willing to shoot, before distance and arc.

Diplomacy decides allegiance via :func:side_hostile_ships, which already drops wrecks, surrendered ships, and any side that has ceasefired - so a turret stops firing the moment a truce is signed, with no tag to keep in sync. Turrets are then removed from their own candidate set: an emplacement duelling another emplacement while the ships it was built to stop fly past is never what an author wanted.

turret_config(id_or_obj, key, default=None)

Read one configured value (range, arc, targets, ...).

turret_designate(id_or_obj, target_id)

Order a turret to shoot a specific thing (a player or GM command).

A designated target beats acquisition entirely and is never re-evaluated against closer candidates - the point of an order is that it is not second-guessed. It is still dropped when the target dies or leaves range * hold_slack. Pass None to return the turret to free-fire.

turret_disengage(id_or_obj)

Stop shooting and forget the current target.

turret_engage(id_or_obj, target_id=None)

Point the weapons at a target and NOTHING else.

This is the whole firing mechanism: the engine's beams fire on their own at whatever target_id holds. Deliberately does not touch throttle or target_pos_* - that is the difference between :func:target_shoot and :func:target, and it is what keeps a turret from wandering off after its victim.

Returns:

Type Description

int | None: The engaged target id, or None.

turret_in_range(id_or_obj, target_id, slack=True)

Whether a target is close enough to keep or take, honoring the hysteresis.

turret_is(id_or_obj)

Whether an object is a turret.

turret_make(id_or_obj, range=None, arc=None, targets=None, priority='closest', hold_seconds=None, hold_slack=None)

Turn an existing space object into a turret.

Does NOT spawn anything and does NOT make it stand still - a turret holds position because of the behavior it was spawned with (behav_station) or because a mount is placing it, not because of anything written here.

Parameters:

Name Type Description Default
id_or_obj Agent | int

The object to arm.

required
range float

Engagement range. Defaults to :data:TURRET_DEFAULT_RANGE. Note this gates ACQUISITION only - what the beams can actually reach comes from the hull's shipData and is not readable.

None
arc float

Firing arc in degrees, centered on the turret's heading. Defaults to None, meaning 360 (no arc test). Every stock station beam is already 360, so an arc is rarely what you want.

None
targets str

Role EXPRESSION of what to shoot. Defaults to :data:TURRET_DEFAULT_TARGETS.

None
priority str

"closest" (default) or "weakest".

'closest'
hold_seconds float

Target persistence. Defaults to :data:TURRET_HOLD_SECONDS.

None
hold_slack float

Range hysteresis multiplier. Defaults to :data:TURRET_HOLD_SLACK.

None

Returns:

Type Description

int | None: The turret's id, or None if the object does not exist.

turret_range(id_or_obj)

The turret's configured acquisition range.

Reads what the author set, NOT the hull's beams: the engine keeps beam stats in its ship table, so beamRange on the object reads None (it exists only in the mock and on add-on hulls, where sbs_utils wrote it). A turret whose configured range disagrees with its hull will acquire targets it cannot hit - keep them in step in shipData.

turret_set(id_or_obj, key, value)

Change one configured value on a live turret.

turret_target(id_or_obj)

The turret's current target id, or None.

turret_tick(id_or_obj)

Acquire and engage in one call - the whole turret loop.

The brain label is a thin wrapper over this so the policy lives in exactly one place and Python callers do not have to reimplement it.

Returns:

Type Description

int | None: The engaged target, or None if it stood down.