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 onlytarget_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_stationfires ONLY from a hull declared throughship_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 - seeLegendaryMissions/turrets/shipData_turrets.yaml. beamRange/beamCount/beamDamageare MOCK INVENTIONS. They readNoneon 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_rangereturns 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.
- A DESIGNATED target, while it lives and is in range. Never re-evaluated.
- The CURRENT target, until
hold_secondsexpires, while it is insiderange * 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. - Otherwise, scan: nearest (or weakest) hostile inside range that matches the
targetsrole 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: |
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: |
None
|
priority
|
str
|
|
'closest'
|
hold_seconds
|
float
|
Target persistence. Defaults to
:data: |
None
|
hold_slack
|
float
|
Range hysteresis multiplier. Defaults to
:data: |
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. |