368 lines
26 KiB
Markdown
368 lines
26 KiB
Markdown
# 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<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. `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<TagRuntimeData>()`.
|
||
|
||
- **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.
|
||
|