Skip to content

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:

  1. //damage/internal route fires with EVENT.source_point
  2. Call grid_take_internal_damage_at(DAMAGE_TARGET_ID, EVENT.source_point) to map the hit to the nearest grid cell and damage it
  3. If all undamaged system nodes are gone, explode_player_ship is called automatically and the player_ship_destroyed signal 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 ("weapon", "engine", "sensor", or "shield").

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

True if the ship has been destroyed, False otherwise.

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, a system is chosen at random. Defaults to None.

None

Returns:

Name Type Description
bool

True if a node was damaged; False if no undamaged nodes remain or the ship has already exploded.

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 grid_layout.

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 (LegendaryMissions/ai/grid_ai.mast) and removing it would break them.

None
layout str

Which named layout to build. Defaults to the ship's own grid_layout inventory value, then "default".

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, a system is chosen at random. Defaults to None.

None

Returns:

Name Type Description
bool

True if a node was repaired; False if no damaged nodes remain for that system.

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 grid_layout inventory value. Pass it explicitly when rebuilding into a layout the ship has not been switched to yet.

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. "weapon1/0/0/6,engine0/1/0/4" - hurt/worn/tuned/total per pool.

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 role, icon, hurt, worn,

tuned, total, state ("hurt"/"worn"/"tuned"/"ok") and the theme

color to draw it in.

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

True if the ship was destroyed by this damage.

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