Skip to content

Combat and siege#

Purpose#

Real-time tactical battles: infantry and siege unit figures, projectile resolution, and (during castle sieges) an 80×80 destructible map with gates, walls, boiling oil, and a capture flag.

Covers open-field battles and siege assaults. Unit display names live in external l2.eng; the client uses numeric type IDs 0–10.

Key entry points#

Battle tick#

Symbol Address Role
Per-type tick dispatch PTR_LAB_004f46a0 11 handlers indexed by lotr2_combat_figure_unit_type
lotr2_combat_init_battle_state 0x004907ec Initialize battle state, clear figure/projectile/group slots, seed combat setup
lotr2_combat_tick_all_figures 0x004934a2 Walk active figures 1..0x50; dispatch unit tick handlers
lotr2_combat_clear_all_figures 0x00480a81 Clear all 0x1b0-byte battle figure slots
lotr2_combat_clear_figure_slot 0x004804d0 Clear one 0x1b0-byte battle figure slot
lotr2_combat_remove_figure 0x00480575 Remove a figure from its tile and clear its slot
lotr2_combat_update_all_move_steps 0x004936ff Update movement steps for all active figures
lotr2_combat_update_walk_anim_step 0x00496f63 Per-figure walk animation step (wrapper)
lotr2_combat_advance_walk_sprite 0x00498008 Advance walk sprite frame/direction for infantry types 0–6
lotr2_combat_update_counter_anim_step 0x00496f73 Counter-attack animation step wrapper
lotr2_combat_advance_counter_sprite 0x00498662 Advance counter-attack sprite frame
lotr2_combat_update_idle_anim_step 0x00496f83 Idle/stance animation step wrapper
lotr2_combat_advance_idle_sprite 0x00498a3e Advance idle sprite; siege types use sprite_b path
lotr2_combat_update_fire_anim_step 0x00496f93 Ranged fire animation step wrapper
lotr2_combat_advance_fire_sprite 0x00498da4 Archer/crossbow fire sprite from facing quadrant
lotr2_combat_direction_from_points 0x00480dc5 Convert source/destination coordinates to an 8-way direction
lotr2_combat_pop_queued_move_dir 0x004a1f0f Pop a queued path coordinate and return the next direction
lotr2_combat_step_figure_tile 0x004a1ffa Step a figure one tile in direction 0..7 and update occupancy
lotr2_combat_update_attack_damage 0x00492cb1 Load melee attack_damage from 0x004f4c18 by type + formation tier
lotr2_combat_apply_melee_hit 0x004a48c6 Apply damage_acc; counter-attack; casualties
lotr2_combat_update_move_step 0x00496fa3 Infantry movement animation steps (types 0–6)
lotr2_combat_siege_update_sprite_a 0x004994ed Siege sprite step (types 7–10)
lotr2_combat_siege_update_sprite_b 0x004996b8 Siege sprite step variant
lotr2_combat_load_formation_thresholds 0x00490b60 Load formation tier thresholds from 0x4f4b28
lotr2_combat_place_figures_on_tiles 0x0049055b Write active figure slot into tile occupancy
lotr2_combat_handle_map_click 0x00490648 Mouse coords → tile index; resolve selected figure
lotr2_combat_clamp_scroll_pos 0x004905e1 Clamp battle scroll position to map bounds
lotr2_combat_tally_army_survivors 0x00490ba7 Sum surviving infantry back into province army records
lotr2_combat_compute_unit_score 0x00490e96 Weighted strength score by unit type multiplier
lotr2_combat_rout_player_figures 0x00491077 Set all figures of a player to routing state
lotr2_combat_reset_figure_move_targets 0x00492127 Clear move-target field for all active figures
lotr2_combat_assign_figure_sprite_ptrs 0x00492184 Assign sprite pointers by unit type and side
lotr2_combat_draw_figure_tooltip 0x00489505 Draw unit hover tooltip; mark tile dirty
lotr2_combat_poll_figure_hover 0x004897a1 Poll hover state; compute tooltip screen position
lotr2_combat_tick_figure_walk 0x004940d6 Walk-phase state handler; walk anim + counter decrement
lotr2_combat_tick_figure_move 0x0049436e Move-phase state handler; move-step + walk anim
lotr2_combat_tick_figure_melee_engage 0x004943f8 Melee engage state; calls lotr2_combat_apply_melee_hit
lotr2_combat_tick_figure_walk_and_fire 0x0049450a Walk + conditional ranged fire dispatch
lotr2_combat_tick_figure_ranged_fire 0x00494557 Spawn projectile via lotr2_combat_alloc_projectile_slot
Per-state tick dispatch PTR_LAB_004f46d0 18 handlers indexed by figure state byte lotr2_combat_figure_state
lotr2_combat_tick_all_groups 0x0049a0b5 Group AI tick; dispatches PTR_LAB_004f4718 / 004f4758 order handlers
lotr2_combat_try_advance_figure_path 0x0049f7b9 Pathfind step + tile step + melee engage probe
lotr2_combat_scan_adjacent_melee_target 0x004a4dfd 8-neighbor enemy scan; sets engage direction
lotr2_combat_spawn_army_open_field 0x00491113 Spawn figures from province army records (open battle)
lotr2_combat_spawn_army_siege 0x00491468 Spawn siege defender figures from army table
lotr2_combat_tick_figure_start_melee 0x00494dfb Walk toward target; mutual melee engage on close range
lotr2_combat_tick_figure_chase_target 0x00494f6f Chase assigned target tile; pathfind + move step
lotr2_combat_tick_figure_siege_move 0x004953b0 Siege engine move + sprite update
lotr2_combat_tick_figure_catapult_fire 0x004954a6 Catapult/oil ranged fire state
lotr2_combat_tick_battle_screen 0x004aeaf5 Per-frame battle screen handler

Battle viewport render glue (pass 43)#

Called each frame from lotr2_game_main_frame_tick when lotr2_game_active_view == 2 and screen mode is battle (0x280x2f). Uses battle grid globals DAT_00502df8 / DAT_00502da0 (tile size 0x10 or 0x20).

Symbol Address Role
lotr2_game_main_frame_tick 0x004d49a0 Per-frame winmain tick: combat sim, viewport, UI, present
lotr2_combat_init_battle_grid_layout 0x004d7020 Bind battle map pointer, stride, viewport dimensions
lotr2_combat_init_battle_sprite_tables 0x004d7107 Bind terrain/overlay PL8 sprite tables
lotr2_combat_mp_frame_gate 0x004d6479 MP debounce + host sync before advancing battle sim
lotr2_combat_tick_battle_viewport 0x004d7142 Orchestrate dirty-tile + figure + projectile blit passes
lotr2_combat_blit_dirty_tiles_16px 0x004d78f0 16px-tile dirty terrain pass (lotr2_gfx_blit_tile16_strip)
lotr2_combat_blit_dirty_tiles_32px 0x004d7bcf 32px-tile dirty terrain + wall overlay pass
lotr2_combat_blit_siege_damage_tiles 0x004d8071 Siege damage-layer tile refresh
lotr2_combat_build_figure_paint_list 0x004d892b Build Y-sorted list of figures in viewport
lotr2_combat_blit_figure_sprite 0x004d8c24 Blit one figure with walk/siege sprite offsets
lotr2_combat_blit_projectile_chain 0x004d9ec7 Walk projectile linked list and blit sprites

Battle session entry / exit#

Symbol Address Role
lotr2_combat_enter_open_battle 0x0048b180 Initialize open-field battle; set screen mode 0x2c
lotr2_combat_enter_siege_battle 0x0048b469 Load castle layout; init pathfinding and figures
lotr2_combat_start_battle_music 0x0048b35b Start battle1_wav / battle2_wav stream by siege and MP mode
lotr2_combat_check_battle_outcome 0x0048b58d Detect zero-headcount side; transition to defeat screen
lotr2_combat_set_battle_outcome_code 0x0048b8fb Writes win/loss/draw code from attacker/defender sides
lotr2_combat_record_siege_battle_score 0x0048b9ac Persists survivor counts into 0x300-byte battle record
lotr2_combat_return_to_campaign_map 0x0048bda8 Post-battle: reload campaign map; screen mode 0x13
lotr2_combat_load_open_field_map 0x0048d4c9 Load batfield.pl8; build 80×80 open-field tile grid
lotr2_combat_load_siege_field_map 0x0048e127 Load stnfield.pl8; decode siege terrain markers
lotr2_combat_draw_unit_info_panel 0x00488769 Draw unit portrait, name, and stats panel
lotr2_combat_exit_to_campaign 0x00488dea Stop audio; restore campaign screen after battle

Pathfinding / map init#

Symbol Address Role
lotr2_combat_init_pathfinding_grid 0x00483570 Fill 80×80 cost grid (0 passable, 999 blocked)
lotr2_combat_pack_tile_heights 0x00483662 Copy tile height bytes into compact 80×80 array
lotr2_combat_init_wall_adjacency_map 0x004836d8 Mark wall-adjacent tiles with movement cost 4
lotr2_combat_mark_figure_tiles_blocked 0x004837f2 Block pathfinding cells occupied by friendly figures
lotr2_combat_build_path_cost_grid 0x004818c5 Build 64×64 tile movement cost table from battle map flags
lotr2_combat_reset_pathfind_search 0x0048101f Clear pathfind buffer; set origin tile for A* search
lotr2_combat_pathfind_query_neighbor 0x00481b2e Trace 8-way path through cost grid; store waypoint list

Combat groups#

Symbol Address Role
lotr2_combat_alloc_group_slot 0x0048030b Find and initialize a free 0x34-byte group slot
lotr2_combat_clear_group_slot 0x0048053f Clear one 0x34-byte combat group slot
lotr2_combat_clear_all_group_slots 0x00480b1e Clear group slots 1..0x50
lotr2_combat_assign_group_orders 0x0048be59 Build side-specific combat groups and assign order types
lotr2_combat_set_group_siege_order_type 0x00492634 Scan group for siege units; assign order types 5–8
lotr2_combat_zero_figure_bytes 0x00480461 Zero 0x1a4 bytes of province army record
lotr2_combat_zero_group_bytes_300 0x004803b9 Zero 0x300-byte spawn record buffer
lotr2_combat_zero_group_bytes_2c 0x0048042b Zero 0x2c-byte troop order buffer
lotr2_combat_unlink_province_army_tile 0x004805ff Remove province army from tile occupancy linked list
lotr2_combat_link_province_army_tile 0x00480770 Link province army into tile occupancy linked list (pair of unlink)

Group globals (pass 53): lotr2_combat_group_table @ 0x00583140 (0x34 stride, slots 1..0x50); lotr2_combat_group_alloc_index scan cursor; lotr2_combat_cur_group_index for group-order handlers; lotr2_combat_focus_group_index for move-to-tile focus. Struct: lotr2_battle_group.md.

Hover tooltip (pass 53): lotr2_combat_hover_poll_time_ms, lotr2_combat_tooltip_blit_x / _y in lotr2_combat_poll_figure_hover.

Projectile lifecycle#

Symbol Address Role
lotr2_combat_clear_all_projectiles 0x00480acf Clear 100 projectile slots at stride 0x4c
lotr2_combat_clear_projectile_slot 0x00480509 Clear one 0x4c-byte projectile slot
lotr2_combat_tick_projectile 0x004a3166 Advance active projectile, apply terrain/unit effects, expire slots
lotr2_combat_unlink_projectile_tiles 0x004808cd Clear per-tile projectile occupancy links
lotr2_combat_link_projectile_tile 0x0048094f Link projectile slot into its current battle-tile list
lotr2_combat_alloc_projectile_slot 0x004800f8 Find free 0x4c-byte slot; init position, direction, tile link
lotr2_combat_probe_los_neighbors 0x0048009f Scan 0x14 neighbor offsets for line-of-sight obstruction
lotr2_combat_mark_projectile_tiles 0x004932c6 Mark tile dirty bits under active projectiles (siege mode)

Per-unit tick handlers#

Type Symbol Address
0 Peasant lotr2_combat_tick_peasant 0x00493754
1 Archer lotr2_combat_tick_archer 0x0049382d
2 Swordsman lotr2_combat_tick_swordsman 0x00493906
3 Maceman lotr2_combat_tick_maceman 0x004939df
4 Pikesman lotr2_combat_tick_pikesman 0x00493ab8
5 Crossbowman lotr2_combat_tick_crossbowman 0x00493b91
6 Knight lotr2_combat_tick_knight 0x00493c6a
7 Catapult lotr2_combat_tick_catapult 0x00493d43
8 Battering ram lotr2_combat_tick_battering_ram 0x00493e25
9 Siege tower lotr2_combat_tick_siege_tower 0x00493f07
10 Moat / ladder lotr2_combat_tick_moat_diggers 0x00493fe9

Siege / castle defense#

Symbol Address Role
lotr2_siege_pour_boiling_oil 0x004966c5 Start oil pour; projectile kind 5
lotr2_siege_mark_gate_hit 0x00496931 Mark gate tile after ram contact
lotr2_siege_open_gate 0x0048f83c Destroy gate → open passage (tile type 1)
lotr2_siege_fire_catapult 0x0048d23a Type-10 / catapult aim shot (proj kind 7)
lotr2_siege_tick_gate_hack 0x004950fb Infantry hacking gate (type 2 tile)
lotr2_combat_siege_modify_tile 0x0049422e Moat/ladder terrain modification
lotr2_siege_load_castle_layout 0x004c8dfb Read castles.dat record into the 80×80 battle map
lotr2_siege_draw_castle_border 0x0041c1ae Draw siege-map bottom/left border lines when siege mode is active
lotr2_siege_init_castle_tile_layout 0x0048fbee Top-level castle tile init; six sub-passes
lotr2_siege_flood_fill_exterior 0x0048fc21 Flood-fill exterior ground tiles (type 6)
lotr2_siege_flood_fill_wall_face 0x0048fd45 Flood-fill wall-face tiles (type 5)
lotr2_siege_set_tile_gate_closed 0x0048f784 Set gate tile type 2; mark impassable
lotr2_siege_clear_tile_gate_face 0x0048f752 Clear gate-face tile flags
lotr2_siege_set_tile_drawbridge_open 0x0048f99e Open drawbridge tile (type 9); rebuild adjacency
lotr2_siege_set_tile_owner_color 0x0048fb9a Write owner color nibble into tile byte

Key globals#

Symbol Address Role
lotr2_combat_cur_figure 0x0059c32c Current figure index during tick loop
lotr2_combat_figure_unit_type 0x00549552 Unit type byte (+0x0A in figure)
lotr2_combat_figure_head_count 0x005496e0 Stack / crew size
lotr2_combat_figure_damage_acc 0x005496da 100-point casualty pool
lotr2_combat_figure_attack_damage 0x005496dc Current melee hit damage
lotr2_combat_figure_state 0x00549571 Figure state-machine byte (+0x31); indexes PTR_LAB_004f46d0
lotr2_combat_figure_side 0x00549549 Side byte (+0x09)
lotr2_combat_figure_tile_index 0x0054955c Current battle tile index (+0x1C)
lotr2_combat_figure_target_slot 0x005496be Melee/ranged target figure slot (+0x186)
lotr2_battle_tile_figure_link 0x005841c5 Tile occupant figure index link
lotr2_battle_tile_dirty_flag 0x005841c2 Tile needs redraw when set
lotr2_battle_tile_type 0x005841c7 Map tile object type
lotr2_battle_tile_damage_ctr 0x005841c0 Gate/wall hit counter
lotr2_projectile_slots 0x005476c0 Base of 100 projectile slots at stride 0x4c
lotr2_projectile_kind 0x005476c9 Projectile type (+0x09 in proj slot)
lotr2_siege_gate_break_threshold 0x00543688 Gate breaks after 15 hits
lotr2_combat_group_order_type 0x00583148 Group order (1–4 infantry, 5–8 siege)

Battle HUD strip (pass 45)#

Dispatched from lotr2_game_main_frame_tick when lotr2_game_active_view == 2 (battle active) and screen modes 0x280x2f. Frame init also used for modes 0x28 and 0x2c via lotr2_game_screen_mode_tick.

Symbol Address Role
lotr2_combat_init_battle_hud_frame 0x00428de3 Status bar, viewport scroll, palette, first present
lotr2_combat_redraw_hud_resource_counters 0x00428f1c Food/gold counters on bottom HUD strip
lotr2_combat_tick_hud_army_roster 0x00429113 Refresh roster when army headcount changes
lotr2_combat_blit_hud_roster_icons 0x00429292 Unit-type icon sprites for active side
lotr2_combat_draw_hud_roster_headcounts 0x004293af Head-count labels beside roster icons
lotr2_combat_draw_paused_overlay 0x00429527 Paused-game banner overlay
lotr2_combat_tick_dumb_view_banner 0x0042a098 Debug “dumb view” banner tick
lotr2_combat_draw_debug_hud_stats 0x0042a146 Debug sync stat panel
lotr2_combat_draw_debug_figure_inspector 0x0042a235 Selected-figure debug dump

Dependencies#

  • Graphics: battle sprites (trp_*.pl8, siege engine.pl8), castle castle*.pl8, gateway.pl8, flags*.pl8
  • Audio: Peas_U1.wav, pouroil.wav, movoil.wav, unit strike sounds
  • I/O: castles.dat via lotr2_io_open / __read
  • Network: combat state is part of MP sync, but checksum ownership is not yet mapped in this module.

Data files#

File Role
castles.dat Castle siege layouts (0x3200 bytes per castle)
l2.eng Unit and UI display names
gateway.pl8, gateway.256 Drawbridge / gate art
flags.pl8, flags1a.pl8, … Victory flag
engine.pl8 Siege engine sprites
arm_*.pl8 Armoury unit portraits (UI index 1–6 ≠ battle type 0–10)

Static tables#

Dump all values: python3 tools/ghidra/dump_combat_tables.py

VA Contents
0x004f4c18 Melee attack damage (11 types × 4 tiers)
0x004f4b28 Formation tier thresholds
0x004f4980 Base squad size
0x004f4be8 Unit target range
0x004f49f8 Spawn templates
0x004f4ad8 Siege sub-templates
0x004f4c70 Catapult boulder damage (19–24)
0x004f4cc8 Oil pour damage (25–32)
0x004f4f50 Knight movement
PTR_LAB_004f46a0 Unit tick handler pointers
PTR_LAB_004f46d0 Figure state tick handler pointers
PTR_LAB_004f4718 Field-battle group order handlers (indices 0–4)
PTR_LAB_004f4758 Siege attacker group order handlers (indices 0–9)
PTR_LAB_004f4730 Siege defender group order handlers (indices 0–8)

Full stat reference: ../structs/lotr2_unit_stats.md.

Group order handler tables (retail)#

Normative AI rules: ../../spec/combat/npc-battle-ai.md.

Open fieldPTR_LAB_004f4718 @ 0x004f4718:

Index Handler
0 lotr2_combat_group_tick_stub_knight
1 lotr2_combat_group_tick_order_melee
2 lotr2_combat_group_tick_order_ranged
3–4 lotr2_combat_group_tick_order_defend

Siege attackerPTR_LAB_004f4758 @ 0x004f4758:

Index Handler
0, 5–7 lotr2_combat_group_tick_stub_knight
1 lotr2_combat_group_tick_order_knight_capture
2 lotr2_combat_group_tick_order_defend_lane
3 lotr2_combat_group_tick_order_defend_wall
4 lotr2_combat_group_tick_order_hold_line
8 lotr2_combat_group_tick_order_seek_flag
9 lotr2_combat_group_tick_stub_tower

Siege defenderPTR_LAB_004f4730 @ 0x004f4730:

Index Handler
0, 8 lotr2_combat_group_tick_stub_knight
1 lotr2_combat_group_tick_order_catapult
2 lotr2_combat_group_tick_order_battering_ram
3 lotr2_combat_group_tick_order_siege_tower
4 lotr2_combat_group_tick_order_capture_flag
5 lotr2_combat_group_tick_order_tower_wall_scan
6 lotr2_combat_group_tick_order_ram_wall_scan
7 lotr2_combat_group_tick_order_gatehouse

Structs#

Struct Doc
lotr2_battle_figure ../structs/lotr2_battle_figure.md
lotr2_battle_group ../structs/lotr2_battle_group.md
lotr2_battle_tile ../structs/lotr2_battle_tile.md
lotr2_projectile ../structs/lotr2_projectile.md

Auto-calculation (quick resolve)#

Campaign-layer battle resolution when the player chooses auto-calculate, the auto-resolve game option is off, or no human participates. Does not run the tactical figure tick.

Symbol Address Role
lotr2_game_battle_apply_casualty_split 0x004c365d Strength compare → survivor % → update army records
lotr2_game_army_calc_combat_strength 0x004c3c05 Weighted unit sum × static table + 20
lotr2_game_battle_resolve_siege_outcome 0x004c3cde Capture, army release, diplomacy after split
lotr2_game_battle_prep_init_combat 0x004bf4d0 Human-side check; battle prep globals
lotr2_game_ui_enter_battle_prep_screen 0x0043b622 Auto-calc from prep screen (mode 0x12)
lotr2_game_battle_halve_loser_army_strength 0x004c6159 Loser halving on tactical timeout path

Normative rules: ../../spec/combat/auto-calculation.md. Static tables: ../structs/lotr2_unit_stats.md.

Tactical melee/ranged casualties (lotr2_combat_apply_melee_hit, 100-point accumulator) are a separate model; played battles still call lotr2_game_battle_apply_casualty_split on return to the campaign map.

Combat flow (summary)#

  1. Tick loop (lotr2_combat_tick_all_figures) walks figures 1..0x50; dispatches PTR_LAB_004f46a0[unit_type].
  2. Handler calls lotr2_combat_update_attack_damage (infantry melee) and movement helpers.
  3. On contact, lotr2_combat_apply_melee_hit adds attacker's attack_damage to defender damage_acc.
  4. Casualty rule: when damage_acc > 99, decrement head_count and subtract 100.
  5. Projectiles (arrows, bolts, boulders, oil) use separate slots and the 100-point pool with optional multi-kill thresholds (100/200/300/400).
  6. Siege map: catapult → wall tiles (type 4); ram → gate face (type 5); oil cauldron (type 7) → spill (type 10); flag at type 14.

Skirmish battles (GoG)#

When DAT_0057a0f0 == 1, combat runs on a fixed preset map with no campaign return path. Setup loads armies from TROOPS*.ENG and map rows @ 0x004D8E18. Post-battle: lotr2_game_ui_on_battle_end_return skips siege resolve (0x13).

Many combat/UI branches gate campaign-only behavior with if (DAT_0057a0f0 == 0).

Full flow: ../../skirmish/battle-flow.md.

Symbol GoG addr Role
lotr2_combat_init_battle_ui 0x00499e5d Load map row by DAT_0053f258
FUN_0042bf46 0x0042bf46 Copy skirmish preset unit counts
lotr2_combat_enter_mp_lobby_from_battle 0x00497879 Gateway return after skirmish

Open questions#

  • Exact English names for unit types 7–10 and tile types from l2.eng
  • Full castles.dat on-disk record layout beyond the loaded 0x3200 tile slice
  • Siege sub-template param0/param1/param2 vs wall/gate HP
  • Drawbridge button vs tile type 2 state machine
  • Boiling oil cauldron placement rules per castle size