Skip to content

fix(troop): field extraction reflects real casualties (FIX-FIELD-EXTRACT-CASUALTIES) - #111

Merged
FullGas1 merged 7 commits into
developfrom
fix/field-extract-casualties
Aug 8, 2026
Merged

fix(troop): field extraction reflects real casualties (FIX-FIELD-EXTRACT-CASUALTIES)#111
FullGas1 merged 7 commits into
developfrom
fix/field-extract-casualties

Conversation

@FullGas1

@FullGas1 FullGas1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Field extraction (embarkFromField) now counts live survivors, not the headcount frozen at deploy time — matching the legacy monolith's live-unit-count behavior instead of an undeclared parity deviation. Ten troops dropped, three killed, seven re-embarked. Cosmetic mortar-servant units (SVNT_*) stay excluded from the count; the mortar unit itself always counts.
  • The "Extract from field" F10 menu now shows each nearby group's current troop count (Extract: Bravo (7 troops) / Bravo (7 troops, 25m)), so a pilot can choose which group to extract based on what it will actually cost in capacity. A group reduced to zero real troops no longer appears.
  • Fixed a pre-existing, unrelated bug found along the way: onUnitDead is registered as a raw S_EVENT_DEAD bridge handler but was written to expect a plain unit-name string instead of unwrapping event.initiator (unlike every other death handler in the codebase) — so it never matched a real dead unit in a live mission, silently disabling both its own bookkeeping and JTAC deregistration-on-death. Fixed, and used to add the ticket's own reactive cleanup: a dropped group reduced to zero real troops while a mortar servant survives (operator killed, servant alive) is now despawned immediately instead of lingering on the battlefield.

Details

  • .backlog/FIX-FIELD-EXTRACT-CASUALTIES/PRD.md + 3 tickets (dependency order, all delivered in this PR).
  • i18n: 2 new menu-label keys synced and translated EN/FR/ES/KO.
  • Docs: docs/pilot/troop-transport.md + .fr.md updated to the new label format.
  • CHANGELOG.md [Unreleased] updated.
  • Live DCS validation: tests/dcs/pilotPassive/scenarioTroopsFullCycle_v2.lua re-run via dcs-bridge after adapting its onUnitDead call to the corrected signature — step 6 (JTAC death / onUnitDead checks, F-T6.3-F-T6.7) passes cleanly. An unrelated, pre-existing failure surfaced at step 7 (JTAC target reacquisition, F-T7.2) — flagged separately, not touched here.

Test plan

  • busted tests/ci/ — 1297/1297 passing
  • Local rebuild (merge_CTLD.ps1) — clean, i18n dicts in sync
  • Live DCS run of the modified scenario via dcs-bridge — step 6 (the part this PR touches) passes
  • luacheck --config .luacheckrc src/ — not available locally, relying on CI
  • CI green

🤖 Generated with Claude Code

Summary by Sourcery

Align field troop extraction with live survivor counts, surface accurate troop numbers in extraction menus, and fix event handling so death-driven cleanup and JTAC deregistration behave correctly.

New Features:

  • Show current troop counts (and distance for multiple groups) in the F10 "Extract from field" troop extraction menus so pilots can choose based on actual capacity impact.

Bug Fixes:

  • Make embarkFromField compute extracted troop count and cargo weight from live survivors instead of the original deployed headcount while still excluding cosmetic mortar servants from the count.
  • Filter nearby dropped-group lookups and extraction eligibility to omit servant-only residues with zero real troops.
  • Fix CTLDTroopManager:onUnitDead to unwrap the DCS S_EVENT_DEAD initiator event properly so group bookkeeping and JTAC deregistration-on-death fire in live missions.
  • Despawn and purge orphaned mortar-servant groups when the last real troop in a deployed group dies, preventing servant-only remnants from lingering on the battlefield.

Enhancements:

  • Introduce shared logical troop-count helpers on troop groups and DCS groups to consistently exclude mortar servants from capacity and display calculations.
  • Define "logical troop count" in CONTEXT.md to document the distinction from raw DCS unit counts across the codebase.

Documentation:

  • Update English and French pilot troop-transport guides to describe survivor-based extraction counts and the new menu label formats for field extraction.

Tests:

  • Extend functional troop manager specs to cover survivor-based extraction counts, servant exclusion, weight scaling, nearby-group filtering, and onUnitDead-driven cleanup and JTAC deregistration.
  • Add unit-level menu gating tests verifying single- and multi-group extraction menus render troop counts and distances correctly and exclude servant-only groups.
  • Adjust the DCS pilotPassive scenarioTroopsFullCycle_v2 test to call onUnitDead with a realistic event table shape matching the DCS bridge.

Chores:

  • Add backlog PRD and ticket documents for FIX-FIELD-EXTRACT-CASUALTIES and link it into the backlog README.
  • Sync i18n dictionaries (EN/FR/ES/KO) with new menu-label keys and bump translation versions to 1.17.
  • Record the field extraction casualty-handling fix in the Unreleased section of the changelog.

FullGas1 and others added 5 commits August 9, 2026 00:01
…e count

embarkFromField now counts a dropped group's currently alive DCS units
(excluding SVNT_* mortar servants) instead of preferring the headcount
frozen at deploy time (stored.total). Ten troops dropped, three killed,
seven re-embarked - matching the legacy monolith's live-unit-count
extraction behavior instead of the undeclared parity deviation.

_findNearestDropped and _findAllNearbyDropped now exclude a dropped
group reduced to zero real troops (mortar operator dead, servant still
standing) from extraction candidates.

Ticket: FIX-FIELD-EXTRACT-CASUALTIES/01

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bash(*) already covers every Bash-tool invocation; the PowerShell tool
had no rule at all and re-prompted on every retry, including repeated
busted test runs this session. Adds PowerShell(busted*) only - a
read-only test runner, not a shell wildcard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The single-group direct button and the multi-group submenu now show
each nearby dropped group's current logical troop count alongside
its name (and distance, in the submenu), reusing the logical-count
helper from ticket 01. Lets a pilot choose which group to extract
based on what it will actually cost in capacity.

New i18n keys synced and translated (EN/FR/ES/KO); troop-transport
docs (EN+FR) updated to the new label format.

Ticket: FIX-FIELD-EXTRACT-CASUALTIES/02

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The narrower PowerShell(busted*) rule still left every other
PowerShell invocation (build script, ad-hoc checks) prompting for
approval mid-session. Mirrors the existing Bash(*) posture already
established for this project.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…servant

onUnitDead was registered as a raw S_EVENT_DEAD handler
(bridge:register(tm, world.event.S_EVENT_DEAD, "onUnitDead")) but written
to expect a plain unit name string. Every other bridge-registered death
handler in this codebase (onTransportDead, CTLDVehicleSpawner:onDead,
CTLDZoneManager:onDead, CTLDFOBManager:onDead) unwraps event.initiator
first - onUnitDead did not, so it never matched a real dead unit in a
live mission. This silently disabled its own bookkeeping and the JTAC
deregistration-on-death path that depends on it. Fixed to unwrap
event.initiator, matching the established pattern. Updated the one
live-DCS scenario that called onUnitDead directly with a bare name.

With that fixed, onUnitDead now also despawns a dropped group's residual
DCS units when a death brings its logical troop count to zero while it
still has units standing (a mortar servant orphaned by its operator's
death) - it no longer lingers on the battlefield unable to be extracted.

Ticket: FIX-FIELD-EXTRACT-CASUALTIES/03

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FullGas1
FullGas1 requested a review from davidp57 as a code owner August 8, 2026 22:30
@sourcery-ai

sourcery-ai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

Field extraction now uses a live logical troop count (excluding mortar servants) for capacity and weight, the F10 "Extract from field" menu shows per-group troop counts, and the onUnitDead handler is fixed to process real DCS death events and despawn orphaned servant-only groups, with tests, docs, changelog, backlog, and i18n updated accordingly.

Sequence diagram for field extraction using live logical troop count and updated menu labels

sequenceDiagram
    actor Pilot
    participant F10Menu as F10Menu
    participant CTLDTroopManager as CTLDTroopManager
    participant CTLDTroopGroup as CTLDTroopGroup

    Pilot->>F10Menu: Open Embark_Extract_Troops
    F10Menu->>CTLDTroopManager: refreshMenuSection(playerObj)
    CTLDTroopManager->>CTLDTroopManager: _findAllNearbyDropped(unit, coalition)
    loop for each nearby group
        CTLDTroopManager->>CTLDTroopManager: _countLogicalUnits(group)
        CTLDTroopManager-->>F10Menu: logicalCount for menu label
    end
    F10Menu-->>Pilot: Show Extract commands with troop counts

    Pilot->>F10Menu: Select Extract for group
    F10Menu->>CTLDTroopManager: embarkFromField(unit)
    CTLDTroopManager->>CTLDTroopManager: _findNearestDropped(unit, coalition)
    CTLDTroopManager->>CTLDTroopManager: _countLogicalUnits(nearest.group)
    CTLDTroopManager->>CTLDTroopGroup: getAliveCount()
    CTLDTroopManager-->>Pilot: Troops embarked based on live logicalCount
Loading

Sequence diagram for onUnitDead handling and orphan servant cleanup

sequenceDiagram
    participant DCSWorld as DCSWorld
    participant CTLDDCSEventBridge as CTLDDCSEventBridge
    participant CTLDTroopManager as CTLDTroopManager
    participant CTLDTroopGroup as CTLDTroopGroup
    participant CTLDJTACManager as CTLDJTACManager
    participant DCSGroup as DCSGroup

    DCSWorld-->>CTLDDCSEventBridge: S_EVENT_DEAD(event)
    CTLDDCSEventBridge->>CTLDTroopManager: onUnitDead(event)
    CTLDTroopManager->>DCSWorld: event.initiator:getName()
    CTLDTroopManager->>CTLDTroopManager: _findGroupByAliveUnit(unitName)
    CTLDTroopManager-->>CTLDTroopGroup: grp

    CTLDTroopManager->>CTLDTroopGroup: _removeDeadUnit(unitName)
    alt [unit was JTAC]
        CTLDTroopManager->>CTLDJTACManager: deregisterJTAC(unitName)
    end

    alt [grp.state == DEPLOYED and grp:getLogicalCount() == 0 and grp:getAliveCount() > 0]
        CTLDTroopManager->>CTLDTroopGroup: getLogicalCount()
        CTLDTroopManager->>CTLDTroopGroup: getAliveCount()
        CTLDTroopGroup->>DCSGroup: destroy()
        CTLDTroopManager->>CTLDTroopManager: _removeFromDropped(grp.coalitionId, groupName)
    end
Loading

File-Level Changes

Change Details Files
Field extraction uses live logical troop counts and proportional weight based on survivors rather than the original deploy-time total.
  • embarkFromField now computes logicalCount via a new _countLogicalUnits helper against the nearest DCS group instead of preferring stored.total or raw group size.
  • _countLogicalUnits iterates a live group’s units, counting only existing units whose names do not start with the SVNT prefix, mirroring the existing servant exclusion behavior.
  • Cargo weight calculation continues to use the original average weight (stored.weight / stored.total) multiplied by the new survivor-based logicalCount.
src/CTLD_troop.lua
tests/ci/functional/troop_manager_spec.lua
Nearby dropped-group lookups and F10 menu entries exclude servant-only residues and display current troop counts (and distance) for each extractable group.
  • _findNearestDropped and _findAllNearbyDropped now require countLogicalUnits(group) > 0 so groups with only SVNT* servants are not considered.
  • refreshMenuSection uses _countLogicalUnits for each nearby group to build labels "Extract: %1 (%2 troops)" for the single-group case and "%1 (%2 troops, %3m)" for the multi-group submenu.
  • New unit tests verify single and multi-group menu paths render labels with correct troop counts and that servant-only groups are filtered out.
  • Documentation for troop field extraction is updated to describe the new label formats and survivor-based counts.
src/CTLD_troop.lua
tests/ci/unit/menu_gating_spec.lua
docs/pilot/troop-transport.md
docs/pilot/troop-transport.fr.md
The onUnitDead handler is corrected to unwrap the DCS event, maintain JTAC bookkeeping, and despawn deployed groups that become servant-only residues.
  • CTLDTroopManager:onUnitDead now accepts a DCS S_EVENT_DEAD event, safely unwraps event.initiator:getName(), and early-returns on missing/invalid initiator.
  • A new CTLDTroopGroup:getLogicalCount helper counts alive non-SVNT units in the group’s in-memory _aliveUnits snapshot for use in death handling.
  • onUnitDead now destroys the DCS group and removes it from droppedGroups and droppedTemplates when a deployed group has zero logical troops but still has alive units (i.e., only servants).
  • Unit tests cover nil/missing initiator handling, last-troop death triggering cleanup when a servant remains, non-last troop deaths not cleaning up, JTAC death deregistration, and unchanged behavior when no servant exists.
  • The DCS scenarioTroopsFullCycle_v2 test is updated to call onUnitDead with an event table matching the bridge’s real S_EVENT_DEAD call shape.
src/CTLD_troop.lua
tests/ci/functional/troop_manager_spec.lua
tests/dcs/pilotPassive/scenarioTroopsFullCycle_v2.lua
Internationalization dictionaries, changelog, context, and backlog are updated for the new behavior and menu strings.
  • New i18n keys for "%1 (%2 troops, %3m)" and "Extract: %1 (%2 troops)" are added with translations for EN, FR, ES, and KO; the old "Extract: %1" key is marked stale.
  • CHANGELOG.md gains an Unreleased section describing survivor-based field extraction counts, servant-only group filtering, menu label changes, and the fixed onUnitDead behavior.
  • CONTEXT.md adds a glossary definition for "Logical troop count" clarifying the exclusion of SVNT_* mortar servants versus raw DCS unit count.
  • The backlog README and FIX-FIELD-EXTRACT-CASUALTIES PRD/ticket files document the problem, solution, and testing decisions for this lot.
src/CTLD_i18n_en.lua
src/CTLD_i18n_fr.lua
src/CTLD_i18n_es.lua
src/CTLD_i18n_ko.lua
CHANGELOG.md
CONTEXT.md
.backlog/README.md
.backlog/FIX-FIELD-EXTRACT-CASUALTIES/PRD.md
.backlog/FIX-FIELD-EXTRACT-CASUALTIES/tickets/01-logical-count-fix-and-filtering.md
.backlog/FIX-FIELD-EXTRACT-CASUALTIES/tickets/02-menu-troop-count-display.md
.backlog/FIX-FIELD-EXTRACT-CASUALTIES/tickets/03-orphan-servant-cleanup.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • You now have two separate implementations of the “logical troop count” filter (CTLDTroopManager:_countLogicalUnits and CTLDTroopGroup:getLogicalCount); consider consolidating this into a single shared helper to avoid future divergence if the naming convention or filter rules change.
  • The servant exclusion pattern uses name:match("^SVNT") while the spec describes servants as SVNT_*; tightening this to match the full prefix (e.g. ^SVNT_) would reduce the chance of accidentally excluding unrelated units whose names happen to start with SVNT.
  • In onUnitDead, failures in initiator:getName() are silently ignored; adding a debug log in the not ok or not unitName branch would make it easier to diagnose event-shape issues or unexpected nil initiators in live missions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- You now have two separate implementations of the “logical troop count” filter (`CTLDTroopManager:_countLogicalUnits` and `CTLDTroopGroup:getLogicalCount`); consider consolidating this into a single shared helper to avoid future divergence if the naming convention or filter rules change.
- The servant exclusion pattern uses `name:match("^SVNT")` while the spec describes servants as `SVNT_*`; tightening this to match the full prefix (e.g. `^SVNT_`) would reduce the chance of accidentally excluding unrelated units whose names happen to start with `SVNT`.
- In `onUnitDead`, failures in `initiator:getName()` are silently ignored; adding a debug log in the `not ok or not unitName` branch would make it easier to diagnose event-shape issues or unexpected nil initiators in live missions.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…o-ops

Addresses sourcery-ai review on PR #111:
- _syncFromDCSGroup, CTLDTroopManager:_countLogicalUnits, and
  CTLDTroopGroup:getLogicalCount each re-implemented the same
  "^SVNT" servant-name check. Extracted into one shared
  _isServantUnitName predicate used by all three.
- onUnitDead now logs (DEBUG) when it skips a nil/malformed event
  instead of failing silently, to help diagnose event-shape issues
  in a live mission.

Did not tighten the match to "^SVNT_": CTLDObjectRegistry.spawnObject
names servant units "SVNT-<uid>" (hyphen, via
string.format("%s-%d", namePrefix, uid)) - requiring a trailing
underscore would stop matching real servant units and reintroduce
the bug this lot fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FullGas1

FullGas1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review! Addressed in 1d6c247:

  • Consolidated the logical-count filter: _syncFromDCSGroup, CTLDTroopManager:_countLogicalUnits, and CTLDTroopGroup:getLogicalCount now all share a single _isServantUnitName predicate instead of each re-implementing the ^SVNT check.
  • Debug logging in onUnitDead: both the missing-initiator and the initiator:getName() failure branches now log at DEBUG level before returning.
  • Did not tighten the match to ^SVNT_: verified that CTLDObjectRegistry.spawnObject names servant units SVNT-<uid> (hyphen, via string.format("%s-%d", namePrefix, uid), not underscore) — the SVNT_* notation in the PRD/docs/comments is informal wildcard shorthand, not the literal separator. Requiring a trailing underscore would stop matching real servant units in production and reintroduce the exact bug this PR fixes. Left the existing ^SVNT prefix match as-is (now centralized in _isServantUnitName) and added a comment there pointing at the real naming format.

🤖 Addressed by Claude Code

@FullGas1
FullGas1 merged commit b0eaf03 into develop Aug 8, 2026
8 checks passed
FullGas1 added a commit that referenced this pull request Aug 8, 2026
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FullGas1
FullGas1 deleted the fix/field-extract-casualties branch August 8, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant