# Tower Assembly (塔组装系统) > **Status**: Designed > **Author**: SepComet + agents > **Last Updated**: 2026-04-29 > **Implements Pillar**: Tactical preparation and adaptation — players optimize tower builds between combat encounters ## Overview The Tower Assembly system is the **component combination engine** that transforms individual Muzzle, Bearing, and Base components into functional Tower instances. During Assembly Phase (between combat nodes), the `PlayerInventoryTowerAssemblyService.TryAssembleTower()` method accepts three component instance IDs and produces a `TowerItemData` containing aggregated stats across 5 level tiers. The system resolves tower rarity from component rarities, computes per-level stat arrays from rarity-scaled base values plus data-table-defined per-level deltas, and merges Tags from all constituent components. Assembled towers can be rostered for combat (max 4 active) via the `PlayerInventoryTowerRosterService`. The system operates purely on in-memory inventory state; all data is ephemeral per run. ## Player Fantasy **"Every battle is a puzzle. You have the pieces—arrange them to solve it."** The Tower Assembly delivers the fantasy of **tactical threat assessment and counter-build satisfaction**. The player arrives at Assembly Phase knowing exactly what challenges lie ahead (the 2 outgoing node types are visible), and must decide which components to combine into towers that answer those specific threats. The core feeling is **the satisfaction of feeling clever** — not raw power, but the right tool for the known job. The player should feel: - **Analyzing incoming threats** — the next node types are known; the question is "what does this enemy fear?" - **Making irreversible commitments** — once components are assembled into a tower, they cannot be reclaimed until the tower is disassembled or the run ends. Every build decision is permanent within its context. - **Seeing the consequences of their choices** — a well-matched tower against the right enemy type is viscerally effective; a mismatched build feels wrong and the player knows exactly what they could have done differently. - **Building toward the boss** — each assembly decision accumulates toward the final confrontation; the boss fight is where build quality is ultimately tested. **Reference**: Into the Breach's "I can see exactly what will happen and I planned for it" feeling — the Tower Assembly achieves this through visible upcoming threats during assembly, and transparent tower stats that make the outcome predictable. ## Detailed Design ### Core Rules **Tower Assembly** combines exactly one Muzzle, one Bearing, and one Base component into a Tower instance. **R1. Assembly Eligibility**: A component is eligible for assembly if and only if: - It exists in the player's inventory (identified by `InstanceId`) - Its `IsAssembledIntoTower` flag is `false` **R2. Assembly Process** (`PlayerInventoryTowerAssemblyService.TryAssembleTower`): 1. Player selects one Muzzle, one Bearing, and one Base component from inventory 2. System validates all three components are eligible (R1) 3. System looks up per-level delta values from data tables (`DRMuzzleComp.AttackDamagePerLevel`, `DRBearingComp.RotateSpeedPerLevel`/`AttackRangePerLevel`, `DRBaseComp.AttackSpeedPerLevel`) 4. Tower rarity computed via `InventoryRarityRuleService.ResolveTowerRarity(muzzleRarity, bearingRarity, baseRarity)` — arithmetic mean of three rarities, rounded and clamped to `RarityType` enum range 5. Stat arrays built at `TowerLevelCount = 5` granularity using rarity-scaled base values plus per-level deltas 6. Tags aggregated via `TowerTagAggregationService.AggregateTowerTags()` — stack counts merged across all three components 7. New `TowerItemData` created with a system-allocated `InstanceId`, display name, and the computed stats 8. All three components' `IsAssembledIntoTower` flags are set to `true` 9. The new tower is added to `inventory.Towers` **R3. Disassembling**: Any assembled tower may be disassembled at no cost. Disassembly: - Removes the tower from `inventory.Towers` - Sets all three constituent components' `IsAssembledIntoTower` flags to `false` - Components' `Endurance` values are preserved (not reset) - If the tower is currently in the combat roster (`ParticipantTowerInstanceIds`), it is automatically removed from the roster before disassembling **R4. Tower Roster** (`PlayerInventoryTowerRosterService`): - Maximum 4 towers may be rostered for combat (`MaxParticipantTowerCount`) - Roster is set during Assembly Phase via `TryAddParticipantTower(towerInstanceId)` / `TryRemoveParticipantTower(towerInstanceId)` - Only rostered towers participate in combat - Roster state persists in `inventory.ParticipantTowerInstanceIds` and is reset on each new run **R5. Tower Endurance**: Each component has an `Endurance` value (0–100). When a tower participates in combat, all three components' endurance is reduced proportionally via `InventoryTowerEnduranceUtility.ReduceTowerEndurance()`. **R6. Endurance at Zero**: If any constituent component's `Endurance` reaches 0: - The tower **cannot be rostered** for combat (`TryAddParticipantTower` returns failure) - The tower **cannot be used** in combat — it is treated as non-functional - The components retain their 0 endurance state until repaired or the run ends - A 0-endurance component cannot be disassembled (must be repaired first — repair is out of scope for this GDD, see Open Questions) **R7. No Component Compatibility Constraints**: Any MuzzleCompItemData may combine with any BearingCompItemData and any BaseCompItemData. No affinity rules, type matching, or stat constraints are enforced. `AttackMethodType` and `AttackPropertyType` are independent dimensions that do not affect assembly eligibility. ### States and Transitions Tower Assembly has no standalone state machine — it operates as a service within the `PlayerInventoryComponent`. State transitions are driven by the Node System's Assembly Phase. **Component States** (per component instance): | State | Description | Exits | |-------|-------------|-------| | `InInventory` | Component resides in inventory, not assembled | → `Assembled` on successful `TryAssembleTower` | | `Assembled` | Component is part of a tower, `IsAssembledIntoTower = true` | → `InInventory` on successful `TryDisassembleTower`; → `InRoster` when tower is added to roster | | `InRoster` | Tower containing this component is in the combat roster | → `Assembled` when tower removed from roster | | `Degraded` | Any constituent component has `Endurance = 0`; tower cannot be rostered | → `Assembled` if endurance is repaired (out of scope) | **Tower States** (per tower instance): | State | Description | Exits | |-------|-------------|-------| | `AssembledIdle` | Tower exists in `inventory.Towers`, not in combat roster | → `AssembledRostered` on `TryAddParticipantTower` | | `AssembledRostered` | Tower is in `ParticipantTowerInstanceIds`, eligible for combat | → `AssembledIdle` on `TryRemoveParticipantTower`; → `Degraded` if any component reaches 0 endurance | | `Degraded` | Tower cannot be rostered due to 0-endurance component | → `AssembledRostered` if endurance is repaired (out of scope) | ### Interactions with Other Systems | System | Direction | Interface | |--------|-----------|------------| | **Node System** | Driven by | Assembly Phase is triggered by `NodeSystem` after each node resolves. During Assembly Phase, the player may call `TryAssembleTower`, `TryDisassembleTower`, `TryAddParticipantTower`, and `TryRemoveParticipantTower`. | | **PlayerInventoryComponent** | Reads/writes | Tower Assembly operates entirely through `PlayerInventoryComponent`'s inventory state. Components and towers are stored in `BackpackInventoryData`. | | **Combat System** | Delegates to | `PlayerInventoryComponent.GetParticipantTowerSnapshot()` returns the current combat roster (up to 4 towers). Combat system reads tower stats from this snapshot. | | **Progression** | Writes | On `RunEnd`, final tower configurations are not persisted (run resets). Progression may read aggregate stats (e.g., total towers assembled across all runs) — pending Progression GDD. | ## Formulas ### 1. Tower Stat Per-Level Scaling Each tower stat (AttackDamage, RotateSpeed, AttackRange, AttackSpeed) is built as a 5-element array via `BuildLevelIntArray` or `BuildLevelFloatArray`. All stats follow the same structural formula: `statValue[i] = baseValue + perLevel * i` for `i` in `0..4` **Variables:** | Variable | Symbol | Type | Range | Description | |----------|--------|------|-------|-------------| | rarityBaseArray | B | int[5] or float[5] | varies | Per-rarity base values indexed by rarity (White=0..Red=4) | | rarity | R | RarityType | White..Red | Component's rarity, converted to 0-based index | | rarityIndex | ri | int | 0–4 | `Clamp((int)R - 1, 0, 4)` | | baseValue | B[ri] | int or float | varies | Starting value at level 0, selected by rarity | | perLevel | P | int or float | varies | Per-level increment from data table | | statValue[i] | V_i | int or float | varies | Stat value at level i (level = i + 1) | **Output Range:** Level 1 value = `B[ri]`; Level 5 value = `B[ri] + P * 4`. Actual range depends on component data tables. **Example — AttackDamage** (Muzzle, Green rarity, `perLevel=3`, `AttackDamage = [10, 20, 30, 40, 50]`): | Level | Index | Value | |-------|-------|-------| | 1 | 0 | 20 + 3×0 = **20** | | 2 | 1 | 20 + 3×1 = **23** | | 3 | 2 | 20 + 3×2 = **26** | | 4 | 3 | 20 + 3×3 = **29** | | 5 | 4 | 20 + 3×4 = **32** | --- ### 2. Tower Rarity Resolution `InventoryRarityRuleService.ResolveTowerRarity(muzzleRarity, bearingRarity, baseRarity)` resolves tower rarity as the arithmetic mean of the three constituent component rarities, rounded and clamped: `towerRarity = Clamp(Round((mR + bR + baseR) / 3), White, Red)` **Variables:** | Variable | Symbol | Type | Range | Description | |----------|--------|------|-------|-------------| | muzzleRarity | mR | RarityType | White..Red | Muzzle component rarity | | bearingRarity | bR | RarityType | White..Red | Bearing component rarity | | baseRarity | baseR | RarityType | White..Red | Base component rarity | | normalized | n | int | 1–5 | (int)clampedRarity - 1 (0-based index) | | average | avg | float | 1–5 | (mR + bR + baseR) / 3f | | rounded | rnd | int | 1–5 | Math.Round(average) | | towerRarity | — | RarityType | White..Red | Final tower rarity | **Output Range:** White to Red. Average rounding: 1.50–1.99 → Green; 2.50–2.99 → Blue; etc. **Example** — Muzzle=Green(2), Bearing=Blue(3), Base=Purple(4): `(2+3+4)/3 = 3.0` → **Blue** --- ### 3. Tag Aggregation `TowerTagAggregationService.AggregateTowerTags(muzzleTags, bearingTags, baseTags)` merges tags from all three components into `TagRuntimeData[]`: **Variables:** | Variable | Type | Description | |----------|------|-------------| | componentTags | `TagType[][]` | Tags array from each of the 3 components | | stackByTag | `Dictionary` | Running count of tag occurrences | | TotalStack | int | `Max(1, occurrenceCount)` per tag — guaranteed at least 1 | **Output:** `TagRuntimeData[]` sorted by `TagType`. `TotalStack` represents how many of the 3 components carry this tag. Flat unique tag list (`Tags[]`) is derived by `FlattenUniqueTags(TagRuntimeData[])`. **Example** — Muzzle=[Fire], Bearing=[Ice], Base=[Fire]: `{ Fire: 2, Ice: 1 }` → `[{ Fire, TotalStack=2 }, { Ice, TotalStack=1 }]` ## Edge Cases - **If the same component instance ID is passed for multiple slots**: Assembly fails. `TryGetComponentById` searches type-specific lists, so the second slot lookup fails and returns `false`. - **If any component is already assembled into a tower** (`IsAssembledIntoTower = true`): Assembly fails immediately. Components may only belong to one tower at a time. - **If any component's DR config row is missing** (`DRMuzzleComp`, `DRBearingComp`, or `DRBaseComp` returns `null` for the component's `ConfigId`): Assembly fails. Components without valid data table entries cannot be assembled. - **If all three components have duplicate tags** (e.g., all have `TagType.Fire`): `TagRuntimeData` is produced with `TotalStack = 3`. Stack count equals the number of components carrying that tag (max 3). - **If one or more components have empty or null tags arrays**: `AggregateTowerTags` skips null/empty arrays. The resulting tower only has tags from components with non-empty arrays. - **If all three components have empty/null tags**: Tower has no tags. `AggregateTowerTags` returns `Array.Empty()`. - **If tags contain `TagType.None` or invalid enum values**: These are filtered out by `AggregateTowerTags` via `if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType)) continue;`. Invalid tags do not appear in output. - **If the combat roster is full (4 towers) and user attempts to add another**: `TryAddParticipantTower` returns `ParticipantTowerAssignFailureReason.ParticipantAreaFull`. The tower is not added. - **If a tower with 0-endurance component is attempted to be rostered**: `CombatParticipantTowerValidationService.ValidateTower` returns a validation failure (`BrokenMuzzleComponent`/`BrokenBearingComponent`/`BrokenBaseComponent`). `TryAddParticipantTower` returns `ParticipantTowerAssignResult` with `FailureReason = InvalidTower`. The tower cannot participate. - **If a tower in the combat roster has a component reach 0 endurance mid-combat**: The tower remains in `ParticipantTowerInstanceIds` but becomes degraded. `CombatParticipantTowerValidationService.ValidateParticipantTowers` marks it invalid on the next validation. No automatic removal from roster occurs. - **If a tower's component stat arrays are shorter than 5 elements**: `ResolveRarityBaseValue` uses `Clamp(rarityIndex, 0, array.Length - 1)`. If the array is empty, the stat defaults to 0. - **If per-level delta is negative** (e.g., `AttackSpeedPerLevel = -0.25`): The formula `baseValue + perLevel * i` correctly handles negative values. Level 5 stat will be lower than Level 1 stat for that dimension. - **If a tower's rarity resolves to a boundary value** (e.g., `(Green + Blue + Purple) / 3 = 3.0`): `Mathf.RoundToInt(3.0f) = 3` (Blue). Standard rounding applies. ## Dependencies ### Upstream Dependencies (what Tower Assembly depends on) | System | Type | Interface | Status | |--------|------|-----------|--------| | **Node System** | Hard | Assembly Phase triggers Tower Assembly. `PlayerInventoryComponent.TryAssembleTower` and roster management are called during Assembly Phase. | GDD exists (`design/gdd/node-system.md`) | | **Inventory** | Hard | `PlayerInventoryComponent` owns all component and tower state. Assembly reads/writes via `BackpackInventoryData`. | Implemented | | **DataTable (DRMuzzleComp, DRBearingComp, DRBaseComp)** | Hard | Per-level delta lookups during stat building. Missing rows cause assembly failure. | Implemented | ### Downstream Dependents (what depends on Tower Assembly) | System | Type | Interface | Status | |--------|------|-----------|--------| | **Node System** | Soft | Reads assembled tower configs during Assembly Phase; passes roster to combat. | GDD exists | | **Combat System** | Hard | `PlayerInventoryComponent.GetParticipantTowerSnapshot()` returns up to 4 rostered towers with their stats. Combat reads `TowerStatsData` for damage calculations. | GDD exists (`docs/CombatNodeArchitecture.md`) | | **Progression** | Soft | May read aggregate tower assembly stats across runs. Pending Progression GDD. | Not yet designed | ### Provisional Assumptions - `TryDisassembleTower` method is not yet implemented — the design doc R3 specifies it should exist. Implementation is pending. - Repair mechanism for 0-endurance components is out of scope for this GDD (see Open Questions). - `CombatParticipantTowerValidationService` handles degraded tower detection — this is owned by the Combat System GDD. ## Tuning Knobs | Knob | Default | Safe Range | Extreme: Too Low | Extreme: Too High | |------|---------|-----------|-----------------|------------------| | `TowerLevelCount` | 5 | 3–10 | Fewer upgrade tiers — less meaningful progression within a tower's lifetime | More tiers — stat arrays grow; UI scalability issues; balance complexity | | `MaxParticipantTowerCount` | 4 | 2–6 | Too few towers — limited tactical variety in combat roster | Too many — combat UI cluttered; player decision paralysis | | Per-level delta (`DRMuzzleComp.AttackDamagePerLevel`, etc.) | varies by row | varies | Too high → towers scale exponentially; late-game dominance | Too low → leveling feels pointless; stats converge | | Rarity base arrays | varies by component row | varies | Too high → rarity gaps become massive | Too low → rarity feels meaningless | **Data-table-driven knobs** (not code constants): - `DRMuzzleComp.AttackDamagePerLevel` — flat AttackDamage increase per tower level - `DRBearingComp.RotateSpeedPerLevel` — rotation speed increase per level - `DRBearingComp.AttackRangePerLevel` — range increase per level - `DRBaseComp.AttackSpeedPerLevel` — attack speed change per level (can be negative) ## Visual/Audio Requirements ### VFX Event Specifications | Event | Visual Effect | Audio Cue | Duration | |-------|--------------|-----------|----------| | **Tower Assembled** | 6–10 geometric particles (triangles/diamonds) burst from assembly point, rarity-colored. Component icons collapse inward, tower card materializes with scale pulse (1.0x→1.15x→1.0x). Purple/Red rarity adds golden shimmer particles. | Ascending C5-E5-G5 arpeggio (200ms). Blue+ adds C6 for premium signal. | ~300ms | | **Tower Added to Roster** | Roster slot glows rarity color (200ms). Tower icon animates to roster slot (200ms ease-out). Roster full: ring pulse from panel border. | Two-tone lock-in (G5→E5→G5, 80ms). Roster full adds C6. | ~250ms | | **Tower Removed from Roster** | Slot fades from rarity color to empty (150ms). Tower icon animates back to inventory. Roster-full indicator: "-1" pulse above panel. | Descending G5→D5 (100ms) — "slot opened". | ~200ms | | **Tower Degraded (0 Endurance)** | Red-orange geometric crack propagates across tower icon (300ms). Tower dims to 40% opacity, desaturated. Broken shard overlay icon. Roster slot flashes red-orange if affected. | Dissonant minor-2nd (C5→C5♭, 150ms) + descending power-down sweep (400Hz→200Hz, 200ms). | ~400ms | | **Tower Disassembled** | Tower icon explodes into 3 component icons flying to inventory positions. Particle burst at tower's former position. Components pulse on arrival. | Reverse arpeggio G5→E5→C5 (200ms). 3x short clicks (30ms each) as components snap back. | ~300ms | ### Rarity Color Palette | Rarity | Hex | VFX Color | Audio Signal | |--------|-----|-----------|--------------| | White | `#E8E8E8` | White particles | 1-note chime | | Green | `#4ADE80` | Green particles | 2-note chime | | Blue | `#60A5FA` | Blue particles | 3-note chime | | Purple | `#C084FC` | Purple + shimmer | 4-note + shimmer VFX | | Red | `#F87171` | Red + shimmer | 5-note + shimmer VFX | ### Animation & Style Constraints - **Particle shapes**: Triangles, diamonds, hexagons ONLY — no circles, no organic curves - **Waveforms**: Clean sine or triangle waves — digital-mathematical character - **Easing**: All UI animations use ease-out entry. No bounce, no elastic overshoot - **Duration budget**: Assembly/disassemble 200–300ms; roster add/remove 200–250ms; degraded 300–400ms. Max 400ms per transition - **Accessibility**: All audio cues have visual alternatives (color flash, icon change, screen pulse). Degraded state uses geometric crack overlay, not color alone - **No simultaneous full-screen effects**: VFX is localized to the relevant card/icon; full-screen flashes reserved only for degraded warning at ≤30% opacity ### DataTable Extension `DRTowerAssemblySound` (or extension of `DRSound`): | SoundId | AssetName | Volume | Notes | |---------|-----------|--------|-------| | TowerAssemble | Tower_Assemble | 0.8 | C5-E5-G5 arpeggio, 200ms | | TowerAssemblePremium | Tower_Assemble_Premium | 0.8 | C5-E5-G5-C6, 250ms (Blue+) | | TowerRosterAdd | Tower_Roster_Add | 0.6 | G5-E5-G5, 80ms | | TowerRosterRemove | Tower_Roster_Remove | 0.5 | G5-D5, 100ms | | TowerDegrade | Tower_Degrade | 0.7 | Minor-2nd + power-down sweep, 350ms | | TowerDisassemble | Tower_Disassemble | 0.7 | G5-E5-C5 reverse arpeggio, 200ms | ## UI Requirements ### Assembly Phase Screen **Trigger**: Auto-displayed after any node resolves (per Node System). **Content**: - **Inventory Grid**: All owned unassembled components, grouped by type (Muzzle, Bearing, Base) - **Tower Slots**: 3 assembly slots (Muzzle, Bearing, Base) — drop targets for components - **Assembled Towers Panel**: All towers built so far in this run - **Combat Roster**: 4 slots showing currently rostered towers (ready for next combat) - **Next Node Preview**: 2 outgoing edge destinations visible during Assembly Phase (from Node System) - **Ready Button**: Confirms Assembly Phase is complete; triggers Node Choice **Interactions**: - Drag components from inventory into assembly slots - Click "Assemble" button when 3 slots are filled → creates tower - Click tower → shows tower stats, rarity, tags; options to "Add to Roster" or "Disassemble" - Drag tower from Assembled Towers to Roster slots - Click "Disassemble" on tower → free disassemble, components return to inventory - Click "Ready" → proceeds to Node Choice **Empty State**: - No unassembled components: inventory grid shows "No components — combat drops will appear here" - No assembled towers: Assembled Towers panel shows "Assemble towers from components above" - Roster empty: slots show dotted outline placeholder **Tower Info Tooltip/Panel** (on tower click): - Tower name and rarity (color-coded) - Stats: AttackDamage (5 levels), RotateSpeed, AttackRange, AttackSpeed - Tags with stack counts - Component sources (Muzzle/Bearing/Base names) - Endurance bars for each component (0–100%) ### Roster Management UI **Roster Slots** (4 slots): - Each slot shows: tower icon, rarity color border, tower name - Drag tower to roster slot to add - Click "X" on rostered tower to remove from roster - Degraded tower (0 endurance): slot shows crack overlay, cannot be deployed - Roster full (4/4): slots show "FULL" indicator; drag-and-drop returns tower to Assembled Towers ### Accessibility - All rarity colors are paired with distinct iconography - Degraded state uses geometric crack shape, not color alone - Tag stack counts shown numerically, not just visually - Component endurance shown as percentage + bar - All interactions possible via keyboard (tab navigation, enter to confirm) ### Assembly - **GIVEN** the player has 3 unassembled components (Muzzle, Bearing, Base), **WHEN** `TryAssembleTower(muzzleId, bearingId, baseId)` is called, **THEN** a new `TowerItemData` is created with aggregated stats, rarity is computed correctly, tags are merged, and all three components' `IsAssembledIntoTower` flags are set to `true`. - **GIVEN** a Muzzle component is already assembled into a tower, **WHEN** the player attempts to use that component in a new `TryAssembleTower` call, **THEN** the call returns `false` and no tower is created. - **GIVEN** any component's DR config row is missing, **WHEN** `TryAssembleTower` is called, **THEN** the call returns `false` and no tower is created. ### Disassembling - **GIVEN** an assembled tower is not in the combat roster, **WHEN** `TryDisassembleTower(towerInstanceId)` is called, **THEN** the tower is removed from `inventory.Towers`, all three components' `IsAssembledIntoTower` flags are set to `false`, and their `Endurance` values are preserved. - **GIVEN** an assembled tower is currently in the combat roster, **WHEN** `TryDisassembleTower(towerInstanceId)` is called, **THEN** the tower is automatically removed from the roster before disassembling. ### Roster Management - **GIVEN** fewer than 4 towers are in the roster, **WHEN** `TryAddParticipantTower(towerInstanceId)` is called with a valid non-degraded tower, **THEN** the tower is added to `ParticipantTowerInstanceIds`. - **GIVEN** 4 towers are already in the roster, **WHEN** `TryAddParticipantTower` is called with a valid tower, **THEN** the call returns `ParticipantTowerAssignFailureReason.ParticipantAreaFull` and no change occurs. - **GIVEN** a tower has a component with `Endurance = 0`, **WHEN** `TryAddParticipantTower` is called, **THEN** the call returns `ParticipantTowerAssignResult` with `FailureReason = InvalidTower` and the tower is not added. ### Stats and Formulas - **GIVEN** a Green-rarity Muzzle with `AttackDamage = [10, 20, 30, 40, 50]` and `AttackDamagePerLevel = 3`, **WHEN** a tower is assembled from it, **THEN** the tower's `AttackDamage` array is `[20, 23, 26, 29, 32]`. - **GIVEN** a tower is assembled from Muzzle=Green, Bearing=Blue, Base=Purple, **WHEN** rarity is computed, **THEN** the tower rarity is Blue (average of 2+3+4 = 3.0). - **GIVEN** a tower is assembled from Muzzle=[Fire], Bearing=[Ice], Base=[Fire], **WHEN** tags are aggregated, **THEN** the tower has Fire with `TotalStack=2` and Ice with `TotalStack=1`. ### Endurance - **GIVEN** a tower with all components at `Endurance > 0` is in the roster, **WHEN** combat ends and `ReduceTowerEndurance` is called, **THEN** all three components' endurance is reduced. - **GIVEN** a component in an assembled tower reaches `Endurance = 0`, **WHEN** `TryAddParticipantTower` is called for that tower, **THEN** the call fails and the tower cannot be rostered. ## Open Questions ### 1. TryDisassembleTower Implementation Gap **Status**: OPEN — The design doc (R3) specifies free disassembling, but `TryDisassembleTower` method does not exist in `PlayerInventoryComponent` or `PlayerInventoryTowerAssemblyService`. Implementation is needed. ### 2. Auto-Cleanup of Degraded Rostered Towers **Status**: OPEN — If a tower in the roster has a component reach 0 endurance mid-combat, the tower remains in `ParticipantTowerInstanceIds` but becomes non-functional. Should there be an automatic removal from roster when a tower becomes degraded? Currently no such mechanism exists. ### 3. Repair Mechanism for 0-Endurance Components **Status**: OUT OF SCOPE — Design doc R6 notes that 0-endurance components cannot be disassembled and must be repaired. Repair mechanism (e.g., gold cost to restore endurance) is out of scope for this GDD. A future Repair GDD should address this. ### 4. Component Compatibility Rules **Status**: RESOLVED — No affinity or compatibility rules are enforced. Any Muzzle+Bearing+Base combination is valid. `AttackMethodType` and `AttackPropertyType` are independent dimensions.