26 KiB
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
IsAssembledIntoTowerflag isfalse
R2. Assembly Process (PlayerInventoryTowerAssemblyService.TryAssembleTower):
- Player selects one Muzzle, one Bearing, and one Base component from inventory
- System validates all three components are eligible (R1)
- System looks up per-level delta values from data tables (
DRMuzzleComp.AttackDamagePerLevel,DRBearingComp.RotateSpeedPerLevel/AttackRangePerLevel,DRBaseComp.AttackSpeedPerLevel) - Tower rarity computed via
InventoryRarityRuleService.ResolveTowerRarity(muzzleRarity, bearingRarity, baseRarity)— arithmetic mean of three rarities, rounded and clamped toRarityTypeenum range - Stat arrays built at
TowerLevelCount = 5granularity using rarity-scaled base values plus per-level deltas - Tags aggregated via
TowerTagAggregationService.AggregateTowerTags()— stack counts merged across all three components - New
TowerItemDatacreated with a system-allocatedInstanceId, display name, and the computed stats - All three components'
IsAssembledIntoTowerflags are set totrue - 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'
IsAssembledIntoTowerflags tofalse - Components'
Endurancevalues 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.ParticipantTowerInstanceIdsand 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 (
TryAddParticipantTowerreturns 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<TagType, int> |
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.
TryGetComponentByIdsearches type-specific lists, so the second slot lookup fails and returnsfalse. -
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, orDRBaseCompreturnsnullfor the component'sConfigId): Assembly fails. Components without valid data table entries cannot be assembled. -
If all three components have duplicate tags (e.g., all have
TagType.Fire):TagRuntimeDatais produced withTotalStack = 3. Stack count equals the number of components carrying that tag (max 3). -
If one or more components have empty or null tags arrays:
AggregateTowerTagsskips 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.
AggregateTowerTagsreturnsArray.Empty<TagRuntimeData>(). -
If tags contain
TagType.Noneor invalid enum values: These are filtered out byAggregateTowerTagsviaif (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:
TryAddParticipantTowerreturnsParticipantTowerAssignFailureReason.ParticipantAreaFull. The tower is not added. -
If a tower with 0-endurance component is attempted to be rostered:
CombatParticipantTowerValidationService.ValidateTowerreturns a validation failure (BrokenMuzzleComponent/BrokenBearingComponent/BrokenBaseComponent).TryAddParticipantTowerreturnsParticipantTowerAssignResultwithFailureReason = InvalidTower. The tower cannot participate. -
If a tower in the combat roster has a component reach 0 endurance mid-combat: The tower remains in
ParticipantTowerInstanceIdsbut becomes degraded.CombatParticipantTowerValidationService.ValidateParticipantTowersmarks it invalid on the next validation. No automatic removal from roster occurs. -
If a tower's component stat arrays are shorter than 5 elements:
ResolveRarityBaseValueusesClamp(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 formulabaseValue + perLevel * icorrectly 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
TryDisassembleTowermethod 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).
CombatParticipantTowerValidationServicehandles 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 levelDRBearingComp.RotateSpeedPerLevel— rotation speed increase per levelDRBearingComp.AttackRangePerLevel— range increase per levelDRBaseComp.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 newTowerItemDatais created with aggregated stats, rarity is computed correctly, tags are merged, and all three components'IsAssembledIntoTowerflags are set totrue. - GIVEN a Muzzle component is already assembled into a tower, WHEN the player attempts to use that component in a new
TryAssembleTowercall, THEN the call returnsfalseand no tower is created. - GIVEN any component's DR config row is missing, WHEN
TryAssembleToweris called, THEN the call returnsfalseand 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 frominventory.Towers, all three components'IsAssembledIntoTowerflags are set tofalse, and theirEndurancevalues 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 toParticipantTowerInstanceIds. - GIVEN 4 towers are already in the roster, WHEN
TryAddParticipantToweris called with a valid tower, THEN the call returnsParticipantTowerAssignFailureReason.ParticipantAreaFulland no change occurs. - GIVEN a tower has a component with
Endurance = 0, WHENTryAddParticipantToweris called, THEN the call returnsParticipantTowerAssignResultwithFailureReason = InvalidTowerand the tower is not added.
Stats and Formulas
- GIVEN a Green-rarity Muzzle with
AttackDamage = [10, 20, 30, 40, 50]andAttackDamagePerLevel = 3, WHEN a tower is assembled from it, THEN the tower'sAttackDamagearray 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=2and Ice withTotalStack=1.
Endurance
- GIVEN a tower with all components at
Endurance > 0is in the roster, WHEN combat ends andReduceTowerEnduranceis called, THEN all three components' endurance is reduced. - GIVEN a component in an assembled tower reaches
Endurance = 0, WHENTryAddParticipantToweris 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.