Breakable Destructables
The first breakable-destructable slice gives placed crates, barrels, trees, gates, and similar map objects an explicit server-authoritative lifecycle. A destructable is neither a unit nor an item, although it uses the normal attack pipeline and can publish the existing widget death event.
Placement State
Destructables spawned from war3map.doo retain their editor creation ID and
apply the placement's initial-life percentage and visibility/pathing flags.
Their maximum life, target type, model, selection radius, and alive/death
pathing textures come from destructable object data.
For HumanX06 elevator placement groups, trigger-driven height transitions, and DTep wall-blocker placement policy, see Warcraft III elevators.
Runtime state records whether the object is initialized, dead, solid at its placement, and currently contributing a static pathing footprint. Alive and death pathing resources are retained separately so the footprint can change at the death transition.
The resource lookup is data-driven: dest_schema maps the pathTex and
pathTexDeath columns from Units\\DestructableData.slk into
DestructableData_t; SP_SpawnDestructable loads both through
M_LoadPathTex and keeps them as alive_pathtex and death_pathtex.
LoadTGA accepts the uncompressed 8-bit grayscale and 24/32-bit BGR(A)
formats used by WC3 pathing resources. It validates the complete header, ID
field, dimensions, allocation size, and pixel payload before decoding.
Every destructable path texture follows its entity's authored facing in
entity_pathtex_transform() in games/warcraft-3/game/g_world.c; the policy
does not depend on a gate rawcode or targType. Unit/building path textures
retain their existing axis-aligned behavior for now. UnitData.slk also
assigns path textures to buildings, including the orientation-named
CityBuildingLarge_0/_45/_90/_135 resources; building path textures may
also need to follow facing, but verify how map placement angles and those
authored variants interact before changing their policy.
DestructableData.slk assigns path textures to many shapes besides gates,
including the 10x2 stone wall (LTw0), the 16x4 elevator blocker (DTep), and
Tree Bridge destructables (LTt0-LTt5, ATt0-ATt1) whose target type is
debris. Square or symmetric textures may look unchanged under rotation, but
restricting orientation to selected rawcodes omits other authored shapes. When
pathing does not match a destructable, confirm its pathTex/pathTexDeath
resolved and check the actual stamped pathmap against the facing. The regression
suite covers the transform and a rotated non-gate pathing blocker through
CM_BakeStaticObstacles().
Walkable Model Height
Walkable destructable routing remains a 2D pathing concern, but their visible
support height is model-authored. Live walkable destructables publish
EF_GROUND_SURFACE; non-floating units publish EF_GROUND_CONFORM. The client
maps those to renderer flags, and the WC3 renderer casts a vertical ray through
each candidate surface with MDLX_TraceModel. It replaces the server's coarse
surface->s.origin.z support with the highest MDX intersection while preserving
the server-authored vertical offset (including mutable FlyHeight).
This deliberately does not modify routing.c, g_ai.c, or the static pathing
bake. pathTex/pathTexDeath remain authoritative for where units may route;
the MDX trace is presentation-only and determines how the actor follows the
bridge deck in Z. Dead or placement-disabled destructables clear
EF_GROUND_SURFACE, so their retained Death geometry cannot lift units.
Destructable Animation Speed (JASS)
SetDestructableAnimationSpeed(d, speedFactor) is registered in
api/api_destructable.h and api/api_module.c. common.txt already declares its
retail JASS signature. It checks that the handle represents a live destructable
and writes the per-edict animation_speed multiplier. Spawned entities start at
1.0; 0.0 freezes and positive factors speed up/slow down playback. In line
with SetUnitTimeScale, OpenWarcraft currently clamps negative factors to zero;
negative retail behaviour has not been conclusively established.
G_RunDestructableAnimation advances the server-owned s.frame at
FRAMETIME * animation_speed for scripted animation overrides, including
SetDestructableAnimation followed by QueueDestructableAnimation. It preserves
the rate when switching to a queued clip. The normal entity state/animation
clock and the existing save field (g_save.c) already carry the value; no new
network or save-format fields are necessary. This native does not change
health, pathing, collision, occluder height or animation selection.
The wc3_destructable.set_animation_selects_only_resolved_model_sequences
regression covers JASS dispatch, independent instances, zero freeze, half-speed
scheduler advancement, queued sequence rate retention, negative clamping and
resetting to 1.0. Verify visual frame timing in-game separately; runtime-model
sampling is not proven by a headless frame test. See Elevators
for the script-owned transition and height contract.
Combat And Death
Both explicit attack orders and contextual right-click orders accept an alive, visible, targetable destructable. Neutral ownership does not turn a crate click into a move order. Units approach and execute their existing melee or ranged attack; the resulting damage is then routed through the destructable lifecycle.
Lethal damage performs one built-in transition:
- Mark the destructable dead and clamp life to zero.
- Disable further damage and targeting.
- Leave combat and switch to its death pathing state.
- Start the model's
deathanimation when present. - Rebuild static obstacles from the unchanged terrain baseline.
- Publish
EVENT_UNIT_DEATHand invoke an optional compatibility callback.
The dead guard is set before events or callbacks, so overlapping hits cannot
repeat death processing. A missing death callback or animation does not prevent
the state transition.
Alive and dead visuals use sequences from the same authored MDX model; there is
no separate dead-model field in DestructableData.slk. The persistent destroyed
bridge geometry is selected by the model's final Death frame. Once that
sequence completes, tree_decay1 pins the entity to that exact final frame so
death-only geosets remain visible. Restoration releases the held frame and
selects Birth or Stand as requested.
Single-variation destructables register the unsuffixed model named by object
data. For LT05 this is
Doodads\\Terrain\\WoodBridgeLarge45\\WoodBridgeLarge45.mdx; registering an
invented WoodBridgeLarge450.mdx lets the renderer's filename fallback find the
model but leaves the game-side animation cache unable to resolve its sequences.
The authoritative model has looping Stand at 133..1333, non-looping Death
at 2000..3000, and non-looping Birth at 3333..10000. Restore with
birth=true plays the full Birth sequence before Stand; birth=false selects
Stand immediately.
Inline Item Drops
Placed destructables retain borrowed references to their parsed
war3map.doo inline item sets for the lifetime of the loaded map. On a normal
death, each set receives one 0–99 roll and selects at most one entry by
cumulative percentage. Probability left below 100 intentionally produces no
item.
Loot is marked processed before selection and spawning, so duplicate lethal
hits, callbacks, or repeated kill requests cannot create extra items. Static
pathing is rebuilt first; selected valid rawcodes then spawn through
SP_SpawnAtLocation as ordinary neutral-passive world items. Multiple results
are distributed around the destroyed object rather than stacked at one point.
Invalid item rawcodes are reported and skipped.
Map Random-Item Tables
A placement's droppedItemSetPtr is retained alongside its inline item sets.
At death it is resolved against the war3map.w3i random-item tables by the
table's explicit tableNumber; it is not treated as an array index. Each set
in the resolved table receives its own 0–99 roll and contributes at most one
item using the same cumulative-percentage and no-item-remainder rules as an
inline set.
Inline and table results are collected before spawning, so all selected items
share the same radial placement behavior. Missing tables, empty tables and
sets, zero IDs, and invalid item rawcodes are skipped safely. Encoded YYI*
random-item placeholders are recognized and reported but are not yet expanded
into item-level/class selections.
Events And Scripted Lifecycle
Death events retain both the dying widget and the damage source. Widget death
registrations still receive the destructable through GetTriggerWidget(), while
GetKillingUnit() returns the attacking unit or null for a scripted kill. The
same source propagation is used for ordinary unit death events.
All destructable lifecycle natives now use the authoritative transitions:
KillDestructableperforms death animation, pathing replacement, loot, and one death event.RemoveDestructableunlinks and frees the object without death, loot, or a death event, then rebuilds static pathing.SetDestructableLifekills at zero and restores a dead object when assigned positive life.DestructableRestoreLiferestores targetability, alive pathing, and either the birth or stand animation.CreateDeadDestructableand its Z variant create the dead presentation without processing a gameplay death.
Restoring a dead destructable begins a new lifecycle and resets its one-life loot guard. A later genuine death can therefore publish another event and resolve its configured drops again. Initially dead and explicitly dead-created objects are marked as already processed until restored.
Dead remains keep their model and continue rendering. A transmitted entity
flag maps to a renderer-only RF_NOT_SELECTABLE flag, excluding the remains
from point and rectangle selection without hiding them.
Pathing Model
The path map now keeps an immutable terrain layer separate from the baked static-obstacle layer. When a destructable changes state, static obstacles are rebuilt from terrain plus the current footprints of live entities. This removes an alive footprint without erasing terrain restrictions and permits a type's optional death-pathing texture to replace it.
Walkable destructables use a narrower contract. While alive, blocked cells in
the authored pathing texture retain the bridge rails, while clear cells only
replace terrain no-walk when they are enclosed by blocked authored pathing
across a texture axis. Clear padding outside those rails leaves the underlying
terrain untouched, so water beside the bridge does not become an alternate
crossing route. This derives the live deck from the pathing texture itself; it
does not use TGA alpha, model bounds, destructable radius, or a hard-coded
bridge width. The surface overlay is baked before normal obstacles so it cannot
erase an overlapping building, and the live bridge is not treated as a circle
blocker during movement. On death it stops supplying bridge support, terrain
pathing is restored, and its optional pathTexDeath enters the normal
static-obstacle bake. The authored TGA is not modified or widened, and there is
no bridge-specific collision-radius exception. For rectangular destructable
textures, WC3 first applies the authored path-texture axis transpose and then
the entity's nearest quarter-turn from s.angle; square textures keep the same
cell footprint for every quarter-turn. M_CheckGround consumes the same
transform for walkable-surface support bounds, so the routeable deck and the
support-height region remain aligned. This facing policy applies to all
destructables with path textures, regardless of targType; unit/building path
textures remain axis-aligned pending a separate contract review.
Phase Boundary
This slice implements the first eight steps of the clean-room specification: placement life, runtime health, normal attacks, lethal detection, one-time death, death animation, post-death target disabling, and pathing replacement.
Inline configured item sets, ordinary map random-item tables, world-item spawning, killer event context, and scripted kill/remove/restore behavior are implemented. Encoded random-item placeholder expansion remains deferred. Death works normally when no loot or callback exists.
Validation
The wc3_destructable.* in-engine tests cover placement state, hidden and
non-solid objects, callback-independent lethal damage, one-time events and
callbacks, neutral contextual targeting, dead-order rejection, alive-footprint
removal, death-footprint replacement, weighted selection, intentional no-item
results, multiple world-item drops, and one-time loot processing.
bridge_path_texture_rotation_covers_all_quarter_turns also bakes a
rectangular YT20 bridge at 0, 90, 180, and 270 degrees, checks both route
orientation and the inverse after death, and verifies the transform dimensions
used by support queries.
Random-table tests additionally cover weighted boundaries, explicit table-number
lookup, multiple table sets, missing/empty data, encoded-placeholder rejection,
normal world-item state, and one-time spawning.
Confirmed Failure Modes
- Blob shadows persisted because death kept the alive shadow state. The death
transition now sets
RF_NO_SHADOW; restore and scripted activation clear it. - Death emitters persisted because the held final death frame continued to be
evaluated. The WC3 MDX renderer now stops new emission once a non-selectable
death remain has
oldframe == frame; particles already emitted expire normally. - Campaign crate loot was attached to hidden
war3map.dooplaceholders while generatedwar3map.jhandles were being created separately. During initial script execution,CreateDestructablerebinds the matching preplaced object, activates it, and preserves its parsed inline and table drop metadata.
Temporary diagnostics used to establish these causes were removed after the transitions and regression tests were added.
For a bounded path-texture diagnostic build, enable WC3_DEBUG_ROUTING=1 and
run the relevant engine test. The router reports authored dimensions, stamped
dimensions, the selected quarter-turn, and live-deck counts for each walkable
path texture:
Scripted-lifecycle tests cover silent dead creation, kill versus remove,
zero-life death, positive-life restoration, birth/stand animation selection,
second death after restoration, and JASS GetTriggerWidget() / GetKillingUnit()
context.