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

32 KiB
Raw Blame History

Shop System

Status: Revised — blocking fixes applied (Pillar placeholder removed; duplicate component exclusion mechanism specified; dependency statuses updated; Tower sell price formula clarified) Author: SepComet Last Updated: 2026-04-30 Implements Pillar: Run-level economy — gold management, component acquisition, and sell/buy tension

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 InstanceIds, 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(muzzleRarity) + ResolveComponentSalePrice(bearingRarity) + ResolveComponentSalePrice(baseRarity) — via ShopPriceRuleService.TryResolveTowerSalePrice. Each component's sell price is resolved from its rarity tier's DRShopPrice midpoint: Round((MinPrice + MaxPrice) / 2.0f). The three midpoints are summed. Tower must be fully assembled and not in the combat roster. If any constituent component is missing, TryResolveTowerSalePrice returns false.

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) — Approved
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 for future features (e.g., "lifetime gold spent" display). Currently not required. GDD exists (design/gdd/progression.md) — Approved
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: RESOLVED — Per the approved Node System GDD, the Plain theme has exactly 2 Shop nodes per run (Node 4 and Node 8). The first shop (Node 4) offers White and Green rarity components only; the second shop (Node 8) offers all rarity tiers. MaxPlayerGold = 9999 is calibrated against approximately 1100 gold per full run (6 combat nodes + boss), giving ~9 runs to cap without spending.

3. Duplicate Component Exclusion Across Shop Visits

Status: RESOLVED — BuildShopGoods(goodsCount, runSeed, sequenceIndex, excludedConfigIds) accepts an optional HashSet<ConfigId> excludedConfigIds parameter. Before placing a component in the shop offer pool, the builder checks excludedConfigIds.Contains(component.ConfigId) and skips that component if present. The excluded set is built by ShopNodeComponent: it tracks all purchased ConfigIds for the current run and passes them to BuildShopGoods on each visit. If fewer than goodsCount eligible components remain after exclusions, the shop offer is smaller than 6. The excludedConfigIds set is run-scoped (reset on new run).

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.