geometry-tower-defense/design/gdd/shop.md

510 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Shop System
> **Status**: In Design
> **Author**: SepComet
> **Last Updated**: 2026-04-29
> **Implements Pillar**: [To be designed]
## Overview
The Shop system is a **run-time economy service** that surfaces component goods to the player at designated Shop nodes during a run. It reads available component templates from `InventoryGenerationComponent.BuildShopGoods()`, resolves per-item pricing from `DRShopPrice`, and processes purchase transactions against the player's current gold via `PlayerInventoryComponent`. Purchased components are instantiated with stable `InstanceId`s, applied Tags, and Endurance, then added to the player's inventory. The Shop is not a passive display — it is a **tactical decision point** where the player evaluates their gold reserves and build gaps against the current component offering, deciding whether to invest or conserve for future nodes. Shop nodes appear in the node graph as a distinct node type; their placement frequency and pricing are the primary levers for run-level economy balance.
## Player Fantasy
**"Your gold is your ammunition. Spend it like you mean it."**
The Shop fantasy is the feeling of **tactical urgency and deliberate investment**. The player has earned gold from the last combat — the question isn't "can I afford it?" but "do I need it right now, or will I need it more later?" Every component purchase is a bet on the future: this Muzzle closes a gap in my build, this Bearing makes my existing towers better, this Base hedges against an unknown threat two nodes from now. The shop should feel like a **weaponised pause** — a moment of calm strategy between fights, where the stakes are real because gold is finite and so are the shop's offerings.
The player should feel:
- **Assessing scarcity** — the shop doesn't have everything; what it has is what you get this run
- **Valuing anticipation** — saving gold for a future shop node, or spending it now on a component that "completes" a tower, both feel like valid strategies
- **Experiencing consequence** — a spent gold coin is gone; the tower it enabled (or didn't) is the consequence
## Detailed Design
### Core Rules
**SR1. Shop Node Entry**: When the player navigates to a Shop node in the node graph, `InventoryGenerationComponent.BuildShopGoods(goodsCount=6, runSeed, sequenceIndex)` generates a fixed pool of offered goods. The pool is deterministic for a given `runSeed + sequenceIndex` — revisiting the same shop node with the same seed produces the same goods. The pool is fixed for this visit once generated.
**SR2. Shop Offerings Display**: Each `GoodsItemRawData` is displayed as a component card showing: type (Muzzle/Bearing/Base), name, rarity, Tags, description, and buy price. `IsPurchased = true` items are removed from display for this visit.
**SR3. Purchase Transaction** (`PlayerInventoryTradeService.TryPurchaseComponent`): Player selects a component and pays the pre-rolled `GoodsItemRawData.Price`. `TryConsumeGold(price)` deducts gold; on success, `InventoryCloneUtility.CloneXxxComp()` clones the component with a new stable `InstanceId` and adds it to inventory. `IsPurchased` is set to `true`.
**SR4. Gold Cap**: Player gold is capped at `MaxPlayerGold` (hard cap). `TryConsumeGold` fails if insufficient; `AddGold` caps at `MaxPlayerGold`. Excess earned gold is lost.
**SR5. Shop Visit Exit**: Player may exit at any time via "Leave". No purchase is mandatory.
**SR6. Selling (via RepoForm)**: `PlayerInventoryTradeService.TrySellItems(itemIds)` processes sales. Sale price = midpoint of `MinPrice..MaxPrice` for the component's rarity. Assembled components must be disassembled first. Towers in the combat roster may not be sold.
**SR7. Determinism**: Shop goods are generated via `InventoryGenerationRandomContext(runSeed, sequenceIndex, Shop, goodsIndex)`. Same inputs always produce the same goods and prices — run reproducibility is guaranteed.
### States and Transitions
Shop has no persistent state machine of its own — it is entered and exited via the Node System. State is held in the `GoodsItemRawData.IsPurchased` flags and `PlayerInventoryComponent.Gold`.
**Shop Visit States**:
| State | Description | Exits |
|-------|-------------|-------|
| `Browsing` | Player is viewing the shop; no purchase attempted yet | → `PurchaseConfirmed` on successful buy; → `Left` on "Leave" |
| `PurchaseConfirmed` | A purchase just succeeded; shop display updates (item marked purchased) | → `Browsing` — player can continue shopping |
| `Left` | Player exited via "Leave" or all 6 items purchased | → (shop phase ends; Node System advances) |
**Player Gold States**:
| State | Condition | Display |
|-------|-----------|---------|
| `CanAfford` | `Gold >= min(shopPrices)` | Normal display |
| `CannotAffordAny` | `Gold < min(shopPrices)` | Greyed-out buy buttons |
| `AtCap` | `Gold == MaxPlayerGold` | Gold display shows cap icon; "Earned gold capped" note |
### Interactions with Other Systems
| System | Direction | Interface |
|--------|-----------|------------|
| **Node System** | Driven by | Shop node type triggers the Shop phase. "Leave" exits → Node System advances to next node choice. |
| **InventoryGenerationComponent** | Reads | `BuildShopGoods(goodsCount=6, runSeed, sequenceIndex)` generates the deterministic goods pool. |
| **PlayerInventoryComponent** | Reads/writes | Gold balance read for affordability; purchase deducts gold; sold items added via `MergeInventory()`. |
| **PlayerInventoryTradeService** | Reads | `TryPurchaseComponent(item, price)` executes purchase. `TrySellItems(itemIds)` executes sales. |
| **Tower Assembly** | Supplies goods to | Purchased components are available for assembly in the Assembly Phase. |
| **RepoForm** | Owns sell UI | RepoForm (not ShopNode) owns the selling interaction. `TrySellItems` is called by RepoForm's UseCase. |
## Formulas
### 1. Buy Price (Component)
`buyPrice = Random.Range(minPrice, maxPrice + 1)` — uniform random integer in `[MinPrice, MaxPrice]` inclusive, rolled at shop generation time and stored in `GoodsItemRawData.Price`.
**Variables:**
| Variable | Type | Range | Description |
|----------|------|-------|-------------|
| MinPrice | int | ≥ 0 | Per-rarity floor from `DRShopPrice[row].MinPrice` |
| MaxPrice | int | ≥ MinPrice | Per-rarity ceiling from `DRShopPrice[row].MaxPrice` |
**Output Range:** `[MinPrice, MaxPrice]` per purchase.
**Example** — White rarity, `MinPrice=50`, `MaxPrice=150`: `buyPrice ∈ [50, 150]`, uniformly random.
### 2. Sell Price (Component)
`sellPrice = Round((minPrice + maxPrice) / 2.0f)` — midpoint, rounded. Called by `ShopPriceRuleService.ResolveComponentSalePrice`.
**Example** — Blue rarity, `MinPrice=300`, `MaxPrice=600`: `sellPrice = 450`.
### 3. Sell Price (Tower)
`towerSellPrice = ResolveComponentSalePrice(muzzleComp) + ResolveComponentSalePrice(bearingComp) + ResolveComponentSalePrice(baseComp)` — sum of all three component sell prices via `ShopPriceRuleService.TryResolveTowerSalePrice`. Tower must be fully assembled and not in the combat roster.
### 4. Gold Cap
`effectiveGold = Min(actualGold, MaxPlayerGold)` where `MaxPlayerGold = 9999`. Hard cap applied on every `AddGold` call. Excess gold is discarded.
## Edge Cases
- **If `playerGold < itemPrice`**: Buy button is disabled. `TryConsumeGold` returns `false`. No change to gold or inventory.
- **If player has 0 gold at shop entry**: All buy buttons are disabled. Player may browse and skip.
- **If all 6 goods are purchased**: Shop shows empty state "All items sold". Player may exit.
- **If a purchased component is disassembled after purchase**: Component returns to inventory. Shop does not re-offer it — `IsPurchased` is visit-scoped. Disassembled components can be sold via RepoForm.
- **If `runSeed` or `sequenceIndex` changes between visits**: `BuildShopGoods` produces a different deterministic pool. Revisiting the same node with a different seed generates different goods.
- **If the same component config appears twice in one run**: Each `GoodsItemRawData` has its own `InstanceId`. Buying twice produces two separate component instances. No deduplication.
- **If player sells a component shown in the current shop visit**: Shop display is unaffected. `IsPurchased` is visit-scoped.
- **If a tower being sold has no sellable components**: `TryResolveTowerSalePrice` returns `false`. `FailureReason = MissingTowerComponent`. Tower cannot be sold.
- **If `MaxPlayerGold` is reached during a reward payout**: `AddGold` silently caps at 9999. Excess gold is discarded.
- **If player attempts to sell an assembled component via RepoForm**: `FailureReason = AssembledComponent`. Player must disassemble first.
- **If player attempts to sell a tower in the combat roster via RepoForm**: `FailureReason = ParticipantTower`. Tower must be removed from roster first.
## Dependencies
### Upstream Dependencies (what Shop depends on)
| System | Type | Interface | Status |
|--------|------|-----------|--------|
| **Node System** | Hard | Shop node type triggers Shop phase. `runSeed` and `sequenceIndex` passed to `BuildShopGoods`. | GDD exists (`design/gdd/node-system.md`) |
| **InventoryGenerationComponent** | Hard | `BuildShopGoods(goodsCount, runSeed, sequenceIndex)` generates the goods pool. `BuildRandomComponentItem` picks slot type, config, and applies tags. | Implemented |
| **DRShopPrice** | Hard | Price ranges per rarity tier (`MinPrice`, `MaxPrice`). Missing rows cause 0-price fallback. | Implemented |
| **DRMuzzleComp / DRBearingComp / DRBaseComp** | Hard | Component config lookup for shop-offered items. Missing rows cause null returns. | Implemented |
### Downstream Dependents (what depends on Shop)
| System | Type | Interface | Status |
|--------|------|-----------|--------|
| **Tower Assembly** | Soft | Purchased components become available for assembly. No direct coupling — Tower Assembly reads from inventory. | GDD exists |
| **Combat System** | Soft | Gold earned from combat is spent at Shop. Indirect — no direct coupling. | GDD exists |
| **Progression** | Soft | May read total gold spent at shop across runs. Pending Progression GDD. | Not yet designed |
| **RepoForm** | Soft | RepoForm reads `TrySellItems` to process sales. Sell prices derived from `ShopPriceRuleService`. | Implemented |
### Provisional Assumptions
- `MaxPlayerGold = 9999` is a design constant — pending confirmation from balance tuning.
- Shop node count per run is governed by Node System GDD — Shop GDD assumes at least one shop node appears per run.
## Tuning Knobs
| Knob | Default | Safe Range | Extreme: Too Low | Extreme: Too High |
|------|---------|-----------|-----------------|------------------|
| `MaxPlayerGold` | 9999 | 500099999 | Gold feels pointless — player never feels rich | Gold never feels scarce — no tension |
| `GoodsCountPerShop` | 6 | 312 | Fewer choices — shop feels unrewarding | More choices — decision paralysis |
| `DRShopPrice[rarity].MinPrice` | varies | varies | Cheap high-rarity items — gold becomes abundant | Expensive high-rarity — shop feels futile |
| `DRShopPrice[rarity].MaxPrice` | varies | ≥ MinPrice | High price variance — luck dominates | Low variance — price is predictable but boring |
| Sell price multiplier | 0.5 | 0.30.7 | Selling too rewarding — players flip components freely | Selling too punishing — players hoard everything |
**Data-table-driven knobs**:
- `DRShopPrice.MinPrice / MaxPrice` — per rarity tier, controls both buy and sell prices
- `DRShopPrice.Rarity` — which rarity tiers are available in the shop
## Visual/Audio Requirements
### VFX Event Specifications
All shop VFX is **localized to relevant cards and the gold display** — no full-screen flashes or global pulses. The shop is a tactical pause, not a cinematic. Every effect should reinforce the geometric/mathematical aesthetic and rarity hierarchy established in the Art Bible.
#### Rarity Shimmer Specification (Purple / Red)
Purple and Red rarity cards carry a persistent shimmer during the entire shop visit:
| Rarity | Shimmer Behavior | Shape |
|--------|----------------|-------|
| Purple `#C084FC` | Continuous horizontal sweep, 60% opacity, 2-second cycle, ease-in-out | Thin diamond outline traveling across card border |
| Red `#F87171` | Continuous horizontal sweep, 70% opacity, 1.5-second cycle, ease-in-out | Thin diamond outline + 2 small triangle particles orbiting card corners |
White through Blue cards have no shimmer — their rarity is communicated through border color and particle density on purchase only.
---
### Event: Shop Open / Card Cascade
**Trigger**: Player enters a Shop node; shop form appears.
**Sequence**:
1. **Background pulse** (t=0): A single hexagonal ring expands from screen center to full shop panel bounds, opacity 20%, rarity-neutral white `#E8E8E8`, 250ms ease-out. Signals "shop materialized."
2. **Gold display appears** (t=50ms): Gold counter scales in from 0.8x to 1.0x, 200ms ease-out.
3. **Card cascade** (t=100ms700ms): 6 component cards enter sequentially, 100ms apart. Each card: scale 0.7x → 1.0x, opacity 0 → 1, 250ms ease-out per card. Stagger: card 1 at t=100ms, card 6 at t=600ms.
4. **Rarity shimmer starts** (t=700ms): Purple/Red cards begin shimmer loop immediately upon entering. No shimmer during card-in animation.
**Particle spec for card cascade**: On each card's entry, 3 small triangles burst from the card's bottom edge, rarity-colored, 200ms lifetime, fade out. Originates from card center-bottom. This reinforces the geometric theme without being distracting.
**Audio**: Soft two-tone chord (C3-G3, 100ms each). No melody — the shop should feel like a calm briefing, not a reward screen.
**Duration**: ~800ms total.
---
### Event: Card Hover — Affordable
**Trigger**: Player cursor enters an affordable component card.
**Sequence**:
1. **Card lift** (t=0): Card translates Y+8px, 150ms ease-out. Shadow deepens (Y offset increases, blur expands, opacity 0.15 → 0.25).
2. **Border glow** (t=0): Card border brightens to full rarity color at 90% opacity, 150ms.
3. **Type icon pulse** (t=0): Component type icon (Muzzle/Bearing/Base geometric symbol) scales 1.0x → 1.15x → 1.0x, 300ms ease-out.
4. **Description reveal** (t=100ms): Description text fades in if previously truncated, 200ms ease-out. Name and tags remain visible always.
**Particle spec**: 4 tiny triangles emit from card corners (one per corner), rarity-colored, 150ms lifetime, outward drift 20px, fade out. Static emit — no continuous particle stream.
**Audio**: Subtle tick/chirp (C5, 40ms, volume 0.3). Very quiet — the player should barely notice it consciously.
**Duration**: Hover effects play while cursor is over card; reverse animation on cursor exit (150ms ease-out, no particle burst on exit).
---
### Event: Card Hover — Cannot Afford
**Trigger**: Player cursor enters a component card whose price exceeds current gold.
**Sequence**:
1. **Card tint** (t=0): Card background dims to 60% opacity, 150ms. Full-color border remains visible so rarity is still readable.
2. **Price badge shake** (t=0): Buy-price badge shakes horizontally — triangle waveform: `+3px → -3px → +3px → 0`, 300ms. Signals "I see the price but I can't."
3. **Gold display flash** (t=100ms): Gold display border flashes red `#F87171` at 40% opacity, 200ms, then returns to normal.
4. **No lift**: Card does NOT translate up. It stays in place, visually communicating "not interactable."
5. **Rarity shimmer continues**: Purple/Red shimmer plays normally — affordability state does not suppress shimmer.
**Particle spec**: None on hover (cannot afford = absence, not presence).
**Audio**: Low dissonant thud (C2, 60ms, volume 0.4). Subconsciously communicates rejection without being alarming.
**Duration**: Persists while cursor is over card; reverses on exit (150ms ease-out).
---
### Event: Purchase — Click and Confirm
**Trigger**: Player clicks an affordable component card's Buy button.
**Sequence**:
1. **Click confirmation** (t=0): Buy button scales 1.0x → 0.92x → 1.0x, 100ms (press feel).
2. **Gold deduction** (t=80ms): Gold counter does a rapid countdown animation — numbers tick down rapidly (50ms per digit-change), final value reached at t=200ms. No bounce or overshoot.
3. **Particle burst** (t=80ms): 812 geometric particles (triangles/diamonds mixed) burst from the card's center, rarity-colored. Particles: random velocity 80200px/s, 400ms lifetime, fade out over last 150ms. Burst is roughly circular but shaped by triangle/diamond outlines at edges.
4. **Rarity shimmer stop** (t=80ms): Purple/Red shimmer on this card ceases immediately — the card is now "spoken for."
5. **Card fade-out** (t=300ms): The purchased card fades to 0% opacity and scales to 0.85x over 250ms ease-out. Remaining cards do NOT shift to fill the gap — the empty slot remains as a visual record of purchase.
6. **Inventory indicator** (t=400ms): A small rarity-colored diamond icon pulses once near the inventory/accessory panel, 200ms ease-out. Signals "this component is now yours."
**Particle spec (rarity-weighted)**:
| Rarity | Particle Count | Extra |
|--------|--------------|-------|
| White | 8 | — |
| Green | 8 | — |
| Blue | 10 | — |
| Purple | 10 | + shimmer trace line connecting 3 particles (diamond outline, 60% opacity, 300ms) |
| Red | 12 | + shimmer trace line connecting 4 particles (diamond outline, 70% opacity, 250ms) |
**Audio**: Ascending arpeggio keyed to rarity:
| Rarity | Sound |
|--------|-------|
| White | C4-E4, 80ms total |
| Green | C4-E4-G4, 120ms total |
| Blue | C4-E4-G4-C5, 160ms total |
| Purple | C4-E4-G4-C5-E5 + shimmer overtone, 200ms total |
| Red | C4-E4-G4-C5-E5-G5 + shimmer overtone, 240ms total |
Volume: 0.7. Pitch-shifted slightly higher than tower assembly sounds — shop purchases feel like a personal acquisition, not a construction event.
**Duration**: ~450ms total. Player can continue shopping during this sequence.
---
### Event: All 6 Items Purchased / Empty State
**Trigger**: Final card is purchased; no items remain.
**Sequence**:
1. **Empty state fade-in** (t=0): "All items sold" message fades in at center of card grid, 300ms ease-out. Background behind message: `#1A1A2E` at 60% opacity, geometric diamond shape as backdrop.
2. **Geometric pulse** (t=100ms): A large diamond outline pulses once (scale 0.9x → 1.1x → 1.0x, 400ms ease-out) behind the message.
3. **Gold display remains visible**: Player can review their remaining gold.
**Audio**: Single resonant tone (G3, 400ms, volume 0.5, soft decay). Signals conclusion without urgency.
---
### Event: Shop Leave / Exit
**Trigger**: Player clicks "Leave" button or all items purchased.
**Sequence**:
1. **Leave button click** (t=0): Button press animation (scale 1.0x → 0.95x → 1.0x, 80ms).
2. **Card fade-out** (t=100ms): All remaining cards fade to 0% opacity, scale to 0.9x, staggered 50ms apart (back-to-front order), 200ms each ease-out.
3. **Gold display fade** (t=300ms): Gold counter fades out, 200ms ease-out.
4. **Hexagonal ring collapse** (t=350ms): Single hexagonal ring contracts from panel edges to center, opacity 15%, 200ms ease-out. Geometric "door closing."
5. **Panel exit** (t=500ms): Shop panel slides out or fades, standard node-system transition.
**Audio**: Reverse of shop-open chord (G3-C3, 150ms total). Clean, no reverb.
**Duration**: ~600ms total.
---
### Event: Gold Display — At Cap
**Trigger**: Player gold equals `MaxPlayerGold` (9999).
**Visual**: Gold display shows a cap icon (small filled hexagon) beside the gold number. Icon pulses once every 3 seconds — scale 1.0x → 1.2x → 1.0x, 500ms ease-out. Color: `#F87171` (red, warning) at 60% opacity.
**Audio**: No sound on cap indicator pulse. The cap warning is purely visual.
---
### Event: Sell Interaction (RepoForm)
> Selling is owned by RepoForm, not ShopNode. These effects apply when the player sells from the inventory view, not during a shop visit. They are documented here for VFX consistency.
**Sell confirm click**: A geometric diamond splits into two triangles that fly toward the gold display. Gold display increments with rarity-keyed arpeggio (same as purchase, one octave lower: C3-E3-G3 for Red).
**Sell price reveal**: When hovering over a sellable item in RepoForm, a small triangle pointer indicates the sell price (midpoint), color `#4ADE80` (green = positive return). On click: green particle burst, gold counter ticks up.
---
### Rarity Color Palette — Shop Application
| Rarity | Hex | Card Border | Shimmer | Purchase Particle | Purchase Audio |
|--------|-----|-------------|---------|-------------------|----------------|
| White | `#E8E8E8` | `#E8E8E8` solid | None | 8 white triangles | C4-E4, 80ms |
| Green | `#4ADE80` | `#4ADE80` solid | None | 8 green triangles | C4-E4-G4, 120ms |
| Blue | `#60A5FA` | `#60A5FA` solid | None | 10 blue diamonds | C4-E4-G4-C5, 160ms |
| Purple | `#C084FC` | `#C084FC` solid | Diamond sweep, 2s cycle | 10 purple diamonds + shimmer trace | C4-E4-G5-C5-E5 + overtone, 200ms |
| Red | `#F87171` | `#F87171` solid | Diamond sweep + corner triangles, 1.5s cycle | 12 red diamonds + shimmer trace | C4-E4-G4-C5-E5-G5 + overtone, 240ms |
---
### Animation & Style Constraints
**Shape vocabulary**: Triangles, diamonds, hexagons ONLY. No circles, no curves, no organic forms. Every particle, every icon, every UI border uses one of these three.
**Waveform character**: All motion uses clean triangle or sine waveforms — no exponential easing, no elastic overshoot, no bounce. Duration: 80250ms for micro-interactions, up to 500ms for structural transitions.
**Easing rule**: Ease-out for all entry animations. Linear for countdown/countup number animations (gold ticks). No ease-in-only — nothing should feel like it is "warming up."
**Rarity shimmer constraints**:
- Sweep direction: left-to-right, continuous loop
- Particle size: 48px (small, not dominant)
- Shimmer opacity: never exceeds 70% — rarity is communicated by shimmer quality, not intensity
- Corner triangles for Red: orbit path is a small equilateral triangle 16px from each corner
**Shop-form layout**: 6 cards in a 3-column × 2-row grid. Card aspect ratio: 3:4 (portrait). Card border radius: 0 (sharp corners — no rounded corners, consistent with geometric aesthetic). Cards must have 8px gaps minimum.
**No full-screen effects**: Shop VFX is card-scoped or gold-display-scoped. A full-screen flash or color wash is never appropriate for a shop event. The exception is the hexagonal ring on shop open/leave, which is a border-to-border panel effect, not full-screen.
**Particle lifecycle**: All particles fade over their final 30% of lifetime. No particles simply disappear (pop out of existence). Particle count per burst: max 12. Particle lifetime: 200400ms.
**Performance budget**: At most 3 simultaneous particle emitters active in the shop (card cascade burst, purchase burst, empty state pulse). Do not stack more.
---
### DataTable Extension — Shop Sounds
| SoundId | AssetName | Volume | Duration | Notes |
|---------|-----------|--------|----------|-------|
| ShopOpen | Shop_Open | 0.5 | 200ms | C3-G3 chord, no reverb |
| ShopCardHover | Shop_Card_Hover | 0.3 | 40ms | C5 tick, very quiet |
| ShopCardHover_CannotAfford | Shop_Card_Hover_Deny | 0.4 | 60ms | C2 thud, dissonant |
| ShopPurchase_White | Shop_Purchase_White | 0.7 | 80ms | C4-E4 |
| ShopPurchase_Green | Shop_Purchase_Green | 0.7 | 120ms | C4-E4-G4 |
| ShopPurchase_Blue | Shop_Purchase_Blue | 0.7 | 160ms | C4-E4-G4-C5 |
| ShopPurchase_Purple | Shop_Purchase_Purple | 0.7 | 200ms | C4-E4-G4-C5-E5 + shimmer |
| ShopPurchase_Red | Shop_Purchase_Red | 0.7 | 240ms | C4-E4-G4-C5-E5-G5 + shimmer |
| ShopEmpty | Shop_Empty | 0.5 | 400ms | G3, soft decay |
| ShopLeave | Shop_Leave | 0.5 | 150ms | G3-C3 reverse chord |
| GoldAtCap_Pulse | Gold_AtCap_Pulse | 0.0 | — | No audio — visual only |
| RepoForm_SellConfirm | RepoForm_Sell_Confirm | 0.7 | 200ms | C3-E3-G3 arpeggio (purchase sounds one octave lower) |
---
## UI Requirements
### Shop Form Layout
**Trigger**: Shop node type in Node System triggers this form.
**Gold Display** (top of form):
- Shows current gold as a numeric value with a small hexagon coin icon
- Position: top-center of shop panel, horizontally centered
- Size: enough for 4-digit display (up to "9999")
- States: normal (white text `#E8E8E8`), at-cap (red cap icon pulses)
- Animation: gold value counts up/down with rarity-keyed arpeggio on change
**Component Cards** (center of form):
- 6 cards in 3×2 grid
- Each card displays: component type icon (geometric symbol), name, rarity border (full-color), Tags (small icons), description (2-line max), buy price badge
- Card layout order: cards arranged left-to-right, top-to-bottom (1-2-3 / 4-5-6)
- Cards do NOT reorder after purchase — empty slots remain visible
**Card Anatomy** (per card):
```
[ Rarity Border (2px solid, full rarity color) ]
[ Type Icon (geometric symbol, 32x32) | Name (localized) ]
[ Rarity Label (text, rarity color) ]
[ Tag Icons (row of small geometric tag markers) ]
[ Description (2 lines max, truncated with "..." if needed) ]
[ Price Badge: [Gold Icon] [Price Number] [BUY] ]
```
**Buy Button**:
- Integrated into card bottom row
- States: Enabled (rarity-colored, pointer cursor), Disabled/cannot-afford (40% opacity, no-shader hover effect)
- No tooltip — all information is on the card itself
**Leave Button**:
- Position: bottom-right of shop panel
- Label: "LEAVE" or equivalent localized string
- Style: outlined button, white border `#E8E8E8`, no fill
- Hover: border brightens, 150ms
**Empty State** (all purchased):
- Geometric diamond shape as backdrop
- "All items sold" centered in card grid area
- Leave button remains accessible
### Accessibility
- All rarity colors are paired with distinct geometric symbols (triangle=Muzzle, diamond=Bearing, hexagon=Base) — color-blind safe
- Price badges use absolute number display, not icon counts
- Cannot-afford state uses border shake (triangle waveform) + red tint, not color alone
- All interactions have keyboard equivalents: Tab to navigate cards, Enter to purchase, Escape to leave
- Gold counter updates are announced via UIFocus system (not audio-only)
### Component Type Symbols
| Type | Geometric Symbol | Shape |
|------|-----------------|-------|
| Muzzle | Triangle | Equilateral triangle, pointing up |
| Bearing | Diamond | Rhombus / rotated square |
| Base | Hexagon | Regular hexagon |
These symbols appear as: card type icon, hover type icon pulse, particle shape origin. They are the primary shape vocabulary of the game.
### Animation Timing Reference
| Event | Entry Duration | Exit Duration | Notes |
|-------|---------------|---------------|-------|
| Shop Open | 800ms total | 600ms total | — |
| Card Cascade | 250ms per card, 100ms stagger | 200ms per card, 50ms stagger | — |
| Card Hover (affordable) | 150ms | 150ms | — |
| Card Hover (cannot afford) | 150ms | 150ms | No lift; price shake instead |
| Purchase Burst | 400ms particles | — | Card fade 250ms concurrent |
| Gold Countdown | ~150ms (rate: 50ms/digit) | — | Linear, not ease-out |
| Empty State | 400ms | — | — |
| Leave | 500ms total | — | — |
| Rarity Shimmer (Purple) | 2000ms cycle | — | Continuous while card visible |
| Rarity Shimmer (Red) | 1500ms cycle | — | Continuous while card visible |
## Acceptance Criteria
### Shop Visit
- **Given** the player enters a Shop node, **then** 6 component cards cascade into view in 3x2 grid with staggered entry animation (card 1 at t=100ms, card 6 at t=600ms).
- **Given** the player enters a Shop node, **then** the hexagonal ring expand animation plays once, opacity 20%, 250ms ease-out.
- **Given** Purple or Red rarity cards are in the shop, **then** the rarity shimmer loop runs continuously until the card is purchased or the player leaves.
### Card Interaction
- **Given** the player hovers over an affordable component card, **then** the card lifts Y+8px with shadow deepening, border glows full rarity color, and 4 corner triangles emit.
- **Given** the player hovers over a cannot-afford card, **then** the card dims to 60% opacity, price badge shakes (triangle waveform), and gold display border flashes red.
- **Given** the player clicks the Buy button on an affordable card, **then** the purchase burst plays (rarity-keyed particle count), gold counter ticks down, and the card fades to empty slot.
- **Given** the player clicks Buy with insufficient gold, **then** the buy button does not activate, gold display shakes, and no purchase occurs.
### Rarity Shimmer
- **Given** a Purple card is visible in the shop, **then** a diamond outline sweeps left-to-right across the card border every 2 seconds, 60% opacity, ease-in-out.
- **Given** a Red card is visible in the shop, **then** a diamond outline sweeps left-to-right every 1.5 seconds (70% opacity) and 2 small triangles orbit the card corners simultaneously.
### Gold Display
- **Given** the player purchases a component, **then** the gold counter counts down rapidly (linear, ~50ms per digit change) to the new value.
- **Given** the player reaches `MaxPlayerGold` (9999), **then** a red hexagon cap icon pulses once every 3 seconds beside the gold display.
### Empty State
- **Given** all 6 shop items are purchased, **then** the empty state appears with a diamond backdrop shape and "All items sold" message.
- **Given** the empty state is shown, **then** the Leave button remains accessible and functional.
### Shop Exit
- **Given** the player clicks Leave or all items are purchased, **then** remaining cards fade out staggered, gold display fades, and the hexagonal ring collapses to center (500ms total).
### Audio
- **Given** the shop opens, **then** a C3-G3 chord plays (200ms, volume 0.5).
- **Given** a component is purchased, **then** an ascending arpeggio plays keyed to rarity (White=2-note 80ms, Red=6-note 240ms + shimmer overtone).
- **Given** a cannot-afford card is hovered, **then** a low dissonant C2 thud plays (60ms, volume 0.4).
- **Given** the shop leaves, **then** a reverse G3-C3 chord plays (150ms).
### Accessibility
- **Given** all rarity colors are displayed, **then** each rarity also has a distinct geometric symbol (Muzzle=triangle, Bearing=diamond, Base=hexagon) visible on every card.
- **Given** the player navigates by keyboard, **then** Tab cycles through cards, Enter purchases, Escape leaves.
- **Given** a color-blind player views the shop, **then** the cannot-afford state is communicated via price badge shake + dim, not color alone.
## Open Questions
### 1. Shop Sell Multiplier
**Status**: OPEN — The sell price formula uses midpoint (`Round((min+max)/2)`), which is approximately 50% of average buy price. Should there be an explicit sell multiplier (e.g., `Round(midpoint * sellMultiplier)`)? Without it, the effective return rate is implicit. Adding an explicit multiplier makes it a tunable knob (see Tuning Knobs).
### 2. Shop Node Frequency per Run
**Status**: OPEN — How many Shop nodes appear per run is governed by the Node System GDD. Shop GDD needs this to calibrate `MaxPlayerGold` against expected gold income. Minimum recommended: 2 shop nodes per run to create meaningful save-vs-spend tension.
### 3. Duplicate Component Exclusion Across Shop Visits
**Status**: OPEN — Design decision: once a component config is purchased, it should not appear in subsequent shop visits this run. `BuildShopGoods` does not currently track purchased configs. This exclusion logic needs to be added: either as a filter in `ShopGoodsBuilder` or as a parameter passed to `BuildShopGoods`. **This is an implementation gap — the GDD specifies the behavior but the code does not yet implement it.**
### 4. Minimum Purchase Requirement
**Status**: OUT OF SCOPE — Not adopted. Design chose optional visits (SR5). Revisiting this would create a mandatory gold sink but risks feeling punitive on early runs with bad RNG.