The internal damage system
Manage player ship engineering grid damage, repair, and ship destruction.
Overview
The internal damage system maps 3D world hit positions to grid cells on a player ship's engineering layout. Grid objects are tagged __undamaged__ or __damaged__ as system roles, and damage coefficients (beam, torpedo, impulse, warp, etc.) are recomputed after every change to keep the engine in sync.
A typical damage event flows like this:
//damage/internalroute fires withEVENT.source_point- Call
grid_take_internal_damage_at(DAMAGE_TARGET_ID, EVENT.source_point)to map the hit to the nearest grid cell and damage it - If all undamaged system nodes are gone,
explode_player_shipis called automatically and theplayer_ship_destroyedsignal is emitted
grid_rebuild_grid_objects recreates the entire grid from the ship's art-ID JSON (called at mission start or respawn). grid_restore_damcons resets or creates the three damcon-team crew members.
Quick example
//damage/internal
grid_take_internal_damage_at(DAMAGE_TARGET_ID, EVENT.source_point)
//signal/player_ship_destroyed
log("A ship has been destroyed!")
jump game_over
from sbs_utils.procedural.internal_damage import (
grid_rebuild_grid_objects,
grid_damage_system,
grid_repair_system_damage,
explode_player_ship,
respawn_player_ship,
)
# At mission start
grid_rebuild_grid_objects(ship_id)
# Programmatic damage (e.g. random engine hit)
grid_damage_system(ship_id, "engine")
# Repair one node
grid_repair_system_damage(ship_id, "engine")
# Manual destroy / respawn
explode_player_ship(ship_id)
respawn_player_ship(ship_id)
Key signals
| Signal | Data keys |
|---|---|
player_ship_destroyed |
DESTROYED_ID |
life_form_died |
SHIP_ID, LIFE_FORM_NAME |
life_form_hp_changed |
SHIP_ID, LIFE_FORM_ID, HP |
API
convert_system_to_string(the_system)
Convert a ship system enum or integer to its role-name string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
the_system
|
SHPSYS | int | str
|
The system enum, integer index, or role-name string. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
Role name for the system ( |
explode_player_ship(id_or_obj)
Mark a player ship as destroyed and emit the player_ship_destroyed signal.
The ship is made invisible and tagged "exploded" rather than deleted
immediately, allowing scripts to react before removal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
grid_add_node_wear(id_or_obj, amount, ship_id=None)
Add to a node's wear. Negative restores it.
grid_apply_system_damage(id_or_obj)
Update system-damage counts and coefficients; explode the ship if all nodes are damaged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
grid_count_grid_data(ship_key, role, default=0)
Count the number of grid items that have a given role in the ship's JSON data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_key
|
str
|
The ship art-ID key to look up in the grid data. |
required |
role
|
str
|
Role name to match against each grid item's role list. |
required |
default
|
int
|
Value returned if the ship key is not found in the grid data. Defaults to 0. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
Number of grid items with the specified role. |
grid_damage_grid_object(ship_id, grid_id, damage_color)
Mark a grid object as damaged and apply a damage color to its icon.
Tools, markers, and rally-point objects are ignored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
Agent | int
|
The player ship agent ID or object. |
required |
grid_id
|
Agent | int
|
The grid object to damage. |
required |
damage_color
|
str
|
Color to apply to the damaged grid-object icon. |
required |
grid_damage_hallway(id_or_obj, loc_x, loc_y, damage_color)
Spawn a fire/damage marker at an empty hallway grid cell.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
loc_x
|
int
|
Grid column of the hallway cell. |
required |
loc_y
|
int
|
Grid row of the hallway cell. |
required |
damage_color
|
str
|
Color to apply to the damage marker icon. |
required |
grid_damage_pos(id_or_obj, loc_x, loc_y)
Apply internal damage at a specific grid cell.
If no grid object occupies the cell a hallway-fire marker is placed instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
loc_x
|
int
|
Grid column to damage. |
required |
loc_y
|
int
|
Grid row to damage. |
required |
grid_damage_system(id_or_obj, the_system=None)
Damage a random undamaged grid node for the specified ship system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int | CloseData | SpawnData
|
The player ship. |
required |
the_system
|
SHPSYS | int | str
|
The system to damage.
If |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
grid_damcon_count(id_or_obj, layout=None)
How many damcon teams this ship's interior declares.
3 for every hull that declares nothing, which is nearly all of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
layout
|
str
|
Layout name. Defaults to the ship's |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
The team count. |
grid_get_max_hp()
Return the current global maximum HP value for damcon-team grid objects.
Returns:
| Name | Type | Description |
|---|---|---|
int |
The max HP setting (default 6). |
grid_interior_arm(over=None, chunk=None)
The hulls are final: build every interior that was asked for, phased over ticks.
Call this once a map has settled - past the roster cull and past whatever re-hulling the map does for itself. Requests made after this point are queued immediately, so a mid-game refit still gets an interior without anyone re-arming anything.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
over
|
float
|
sim-seconds to spread the work across (default 4). |
None
|
chunk
|
int
|
rooms created per slice (default 16). |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
how many ships were released to build. |
grid_interior_flush()
Build everything outstanding right now.
For a test, a headless conformance run, or anything that cannot wait for the drip.
grid_interior_is_armed()
Whether interiors are being built as they are requested.
grid_interior_pending()
Ships recorded but not yet built, plus queued work still to run.
grid_interior_request(id_or_obj, layout=None)
Ask for this ship's engineering interior. Built ONCE, when the hull has settled.
The call a //spawn route should make. Nothing is created here: before
:func:grid_interior_arm the ship is simply recorded, and after it the build is
queued and dripped over ticks. Either way the hull is read when the build RUNS, so a
ship re-hulled between the request and the build gets the interior it ends up
needing - not the one it had when it spawned.
Idempotent by ship: requesting the same ship repeatedly produces one build.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
the ship. |
required |
layout
|
str
|
a named layout; defaults to the ship's own. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
whether the request was recorded. |
grid_interior_reset()
Drop queued interior work and disarm (mission reset).
grid_node_apply_color(id_or_obj, theme_name=None)
THE place a grid node's icon color is decided.
Four tiers, one write point - so a node can never be drawn in a color that disagrees with its condition:
- damaged -> the theme's
damage_colors - worn -> the theme's
worn_colors(Gold by default) - tuned -> the theme's
tuned_colors(cyan by default) - nominal -> the node's OWN cached healthy color, from inventory
color, written at spawn - so a re-skinned room keeps its own hue instead of being flattened to a theme default.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
the grid node. |
required | |
theme_name
|
str
|
theme to read; None uses the current one. |
None
|
Returns:
| Type | Description |
|---|---|
|
str | None: the color written, or None if there was no blob to write to. |
grid_node_efficiency(id_or_obj)
What this node contributes to its system's effectiveness.
damaged 0.0 | worn WEAR_WORN_FACTOR | nominal 1.0 | tuned 1.0 + WEAR_TUNED_BONUS.
A node nothing has ever worn reads WEAR_NOMINAL and so weighs exactly 1.0 - which is what makes the whole idea inert until something writes wear, and why set_damage_coefficients produces numbers identical to the old undamaged/total fraction on a ship that has never worn anything.
grid_node_icon_index(id_or_obj)
The sheet index this node is DRAWN with on the interior view.
A panel that names a node beside a generic cog is asking the engineer to hold two pictures of one room. This is the index the node actually wears, so a header glyph and the node under the cursor are the same shape.
Written to inventory at spawn (_grid_spawn_chunk), which is also what the View
tab re-applies, so inventory is the unscaled source of truth. Falls back to asking
the theme for the node's roles - a node built by something other than
grid_rebuild still answers - and finally to None, which a caller draws as
nothing rather than as an arbitrary glyph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
the grid node. |
required |
Returns:
| Type | Description |
|---|---|
|
int | None: the icon index, or None if neither source has one. |
grid_node_is_system(id_or_obj)
Is this node part of a ship SYSTEM - the only kind of node that wears?
Every shipped interior says so in its own roles: a system room's roles begin with
system (system,weapon,beam, system,ENGINE,impulse, system,shield,fwd)
and a crew space's begin with room (room,cabin,gym, room,cabin,quarters,
room,bay,cargo). Measured across data/grid_data.json: 38 rolesets, 11 of them
systems, and not one where system appears anywhere but first. LegendaryMissions'
docking repair and the EPad room list already read the same role.
Wear is a SYSTEM idea - a tuned beam array fires harder, a worn impulse drive pushes less - and none of that means anything for a gymnasium. Before this, upkeep aged every node on the ship, so the gym went worn on schedule and Engineering offered a damage-control team to go and tune it.
Damage is different and is deliberately NOT gated: a fire in the galley is a real fire, and a team still goes and puts it out.
grid_node_state(id_or_obj)
The node's condition as one word.
Damage wins over wear - a broken node is "damaged" whatever its wear says,
which is why grid_damage_grid_object clears __worn__.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
The grid node (id or Agent). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
"damaged", "worn", "tuned" or "nominal". |
grid_node_wear(id_or_obj)
How worn a grid node is, 0.0 (perfect) to 1.0 (worn out).
A node nothing has ever worn reads WEAR_NOMINAL, so callers never have to special-case "no wear recorded".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
The grid node (id or Agent). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
the node's wear. |
grid_rebuild_grid_objects(id_or_obj, grid_data=None, layout=None)
Rebuild all engineering-grid objects on a ship, NOW, in this frame.
Deletes any existing grid objects, re-creates them from the layout registered for the ship's shipData key, and re-creates the damcon teams, the position marker and the EPad.
Prefer :func:grid_interior_request for a ship that is being set up. This builds
immediately, which is right for a mid-game refit a player is watching, and wrong at
game start - see that function for why.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
grid_data
|
dict
|
Accepted and deliberately ignored. Kept because
missions pass it positionally ( |
None
|
layout
|
str
|
Which named layout to build. Defaults to the ship's own
|
None
|
grid_data stopped being read when the lookup moved to :func:grid_get_layout,
which resolves the module-level store itself. That is not an oversight to tidy up -
honoring the argument again would REINTRODUCE a restart bug. The one caller that
passes it captures it once, at top level, into a MAST shared variable; but
grid_reset_caches() rebinds the store to a fresh dict on a mission restart, so
that snapshot pins run 1's dict while every floor plan merged for run 2 lands in the
new one. Reading the global each time is what keeps the two in step.
grid_repair_grid_objects(player_ship, id_or_set, who_repaired=None)
Repair one or more grid objects and update the ship's damage state.
Hallway-fire markers are deleted; system nodes have their icon color restored and the system-damage count decremented. Recomputes damage coefficients if any system node was healed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
player_ship
|
Agent | int
|
The player ship agent ID or object. |
required |
id_or_set
|
Agent | int | set[Agent | int]
|
Grid object(s) to repair. |
required |
who_repaired
|
Agent | int
|
The damcon-team agent that performed the repair (used to remove work-order links). Defaults to None. |
None
|
grid_repair_system_damage(id_or_obj, the_system=None)
Repair a single damaged grid node for the specified system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
the_system
|
SHPSYS | int | str
|
The system to repair.
If |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
grid_restore_damcons(id_or_obj, layout=None)
Restore all damcon teams on a ship to full health, creating them if missing.
How many teams there are, and where they stand, come from the hull's interior data
when it says (grid_get_damcons); otherwise three teams wherever the engine puts
them, exactly as before. A declared post is also the team's permanent rally point,
because the prefab spawns the rally marker on the cell it is handed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
layout
|
str
|
Layout name. Defaults to the ship's |
None
|
grid_set_hp(ship_id, GRID_OBJECT_ID, hp)
Set the HP of a damcon-team grid object and emit the life_form_hp_changed signal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
Agent | int
|
The player ship agent ID or object. |
required |
GRID_OBJECT_ID
|
Agent | int
|
The damcon-team grid object ID or agent. |
required |
hp
|
int
|
The new HP value to assign. |
required |
grid_set_max_hp(max_hp)
Set the global maximum hit-point value for damcon-team grid objects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_hp
|
int
|
New maximum HP value. Defaults to 6 at module load. |
required |
grid_set_node_wear(id_or_obj, value, ship_id=None)
Set a node's wear, reconciling everything that follows from it.
The ONLY writer. It clamps, stores, adds or removes __worn__, repaints
through grid_node_apply_color, and recomputes the ship's coefficients - but only
when the TIER actually changed, so wear moving within a band costs one dict write
and nothing else.
Only a SYSTEM node carries wear at all (see grid_node_is_system); on anything
else this is a no-op that answers with the nominal reading. Gating the one writer
rather than each caller is what makes the whole model system-only: upkeep, the
wear a damcon patch leaves behind, a mission's own call, all of it.
__worn__ never coexists with __damaged__: damage supersedes wear, and a
worn node keeps __undamaged__ so nothing that counts undamaged system nodes
changes meaning because a node got tired.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
the grid node. |
required | |
value
|
float
|
the new wear, clamped to 0.0 - 1.0. |
required |
ship_id
|
optional
|
the host, for the coefficient recompute. Read from the node when not given. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
the wear actually stored. |
grid_set_wear_tuning(worn_factor=None, tuned_bonus=None, worn_min=None, tuned_max=None, upkeep_rate=None, **rates)
Retune the wear model for a mission that wants different numbers.
grid_set_wear_tuning(tuned_bonus=0.0) gives strict parity with the old
coefficients even on a ship with tuned nodes - the escape hatch for a mission
that wants the maintenance loop without any over-unity.
Any arrival RATE can be passed by its short name as a keyword, so a mission tunes the feel without touching the library::
grid_set_wear_tuning(beam_hit=0.0005, warp_minute=0.05)
grid_set_wear_tuning(upkeep_rate=0) # no time-based upkeep at all
An unknown rate name is a warning, not a silent no-op - a typo here would look exactly like "the dial does nothing".
grid_system_signature(id_or_obj)
A value that CHANGES whenever an indicator row should be redrawn.
For on change grid_system_signature(ship_id): - the polling form, which is
what a console layout can actually use. A //damage/internal route fires on
the SERVER and would not repaint a client's panel; on change re-evaluates on
the console itself and so cannot miss a hit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
The ship (id, Agent or SpaceObject). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
e.g. |
grid_system_states(id_or_obj)
What each of the ship's system pools is worth right now.
Only pools the ship actually HAS are returned - a fighter with no shield rooms gets no shield light rather than a permanently-green one for a system it cannot lose. Order is fixed by GRID_SYSTEM_ICONS so a row built from this never reshuffles under the player between repaints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
The ship (id, Agent or SpaceObject). |
required |
Returns:
| Type | Description |
|---|---|
|
list[dict]: one per pool, with |
|
|
|
|
|
|
grid_take_internal_damage_at(id_or_obj, source_point, system_hit=None, damage_amount=None)
Apply internal damage to a ship at a 3D world position.
Maps the 3D position to the nearest grid cell, then damages the grid objects at that cell (or a hallway marker if the cell is empty). Also injures any damcon-team lifeforms at the impact location.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
source_point
|
Vec3
|
3D position of the hit. |
required |
system_hit
|
SHPSYS | int | str
|
Unused. Defaults to None. |
None
|
damage_amount
|
int
|
Unused. Defaults to None. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
grid_tune_grid_object(ship_id, node_id, who=None)
A node brought back to spec - what a maintenance order delivers.
Wear to zero, __worn__ off, drawn in the tuned color, coefficients
recomputed, and the order closed for EVERY team on it rather than just whoever
happened to be standing there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
the host ship. |
required | |
node_id
|
the node to tune. |
required | |
who
|
optional
|
the team that did it, carried on the signal. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
whether anything was tuned. |
grid_wear_beam_hit(ship_id, count=1)
Wear a ship's beam systems for landing count beam hits.
THE AMOUNT IS LOOKED UP HERE, which is the whole reason this exists. WEAR_PER_* are
module-level constants, and only FUNCTIONS become MAST globals - a constant named in
a .mast expression is a NameError, every time, at the moment the route fires. So
grid_wear_system(id, "beam", WEAR_PER_BEAM_HIT) in a route reads perfectly and
crashes on the first shot.
grid_wear_shield_hit and grid_wear_travel already had this shape; beams and tubes
did not, and those were the two routes that fired.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
the ship that fired. |
required | |
count
|
int
|
how many hits to charge for. Defaults to 1. |
1
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
how many nodes were worn. |
grid_wear_shield_hit(ship_id, face)
Wear the shield facing that took a hit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
the ship that was hit. |
required | |
face
|
int
|
0 forward, anything else aft - the same two pools set_damage_coefficients writes as shield_damage_coeff[0] and [1]. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
int |
how many nodes were worn. |
grid_wear_system(ship_id, sys_role, amount, count=1)
Wear count random working nodes of a system.
Random rather than spread evenly: wearing every node a hair each time would move a whole pool across the threshold together, so the ship would go from fine to fully worn in one tick with nothing in between.
Damaged nodes are skipped - they are already at zero effectiveness, so wear on them would mean nothing and would be lost the moment they were repaired.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
the ship. |
required | |
sys_role
|
str
|
a role or comma-separated roles, e.g. "beam", "shield,fwd". |
required |
amount
|
float
|
wear to add to each chosen node. |
required |
count
|
int
|
how many nodes. Defaults to 1. |
1
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
how many nodes were worn. |
grid_wear_travel(ship_id, throttle=None)
Wear the drive a ship is actually using, for one minute of travel.
playerThrottle is <= 1.0 for impulse and > 1.0 for warp (the mock's model is
calibrated against the engine's own speed capture), so this splits the wear
between the impulse and warp pools rather than charging both.
The read is coalesced because the engine answers None for a blob field nothing
has set - a ship sitting still since spawn has never had a throttle written.
An unguarded compare is None > 1.0 on a real bridge, and since a failing
expression stops the command, the caller would simply stop working with no
symptom beyond "wear stopped happening".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
the ship. |
required | |
throttle
|
float
|
override, for tests. Read from the blob when None. |
None
|
Returns:
| Type | Description |
|---|---|
|
str | None: which pool was worn - "warp", "impulse", or None when stopped. |
grid_wear_tube_shot(ship_id, count=1)
Wear a ship's torpedo systems for launching count rounds.
See grid_wear_beam_hit for why the amount is looked up here rather than passed in
from MAST.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
the ship that launched. |
required | |
count
|
int
|
how many launches to charge for. Defaults to 1. |
1
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
how many nodes were worn. |
grid_wear_upkeep(ship_id, amount=None)
Age every working SYSTEM node on a ship by one upkeep step.
Crew spaces are left alone: a gymnasium does not drift out of tune, and one that
read worn put a tuning job for it on the Engineering board.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_id
|
the ship. |
required | |
amount
|
float
|
defaults to WEAR_UPKEEP_RATE. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
how many nodes were aged. |
respawn_player_ship(id_or_obj)
Respawn a previously destroyed player ship at its original spawn position.
Restores the ship's art ID, repositions it to the spawn point, and removes
the "exploded" role.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |
set_damage_coefficients(id_or_obj)
Recalculate and write the damage coefficients for all ship systems.
For each system (beam, torpedo, impulse, warp, maneuver, sensors, shields) computes the ratio of undamaged to total nodes and writes it to the blob.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The player ship agent ID or object. |
required |