Skip to main content
The shop system (tienda/) allows players to spend coins earned from minigames to unlock and equip character customizations. These customizations are immediately reflected in the main menu and throughout the game.

Shop Scene Structure

tienda/
├── scenes/
│   └── StoreMenu.tscn          # Main shop UI scene
├── Script/
│   └── store_menu.gd           # Shop logic and purchase handling
├── icons/                      # Item preview images
│   ├── Corona.png
│   ├── Lentes.png
│   ├── Gorro.png
│   └── Cadena.png
└── Images/                     # Full character skin previews
    ├── Skin_Blue.png
    ├── Skin_Green.png
    ├── Skin_Purple.png
    └── Skin_Gray.png

Available Items

Accessories (Accesorios)

Worn on the character’s head/body:
tienda/Script/store_menu.gd
var accesories = [
    { 
        name="Corona", 
        category="accesorio", 
        price=25, 
        icon=preload("res://tienda/icons/Corona.png"), 
        preview=preload("res://tienda/icons/Corona.png") 
    },
    { name="Gafas", category="accesorio", price=15, ... },
    { name="Gorro", category="accesorio", price=20, ... },
    { name="Cadena", category="accesorio", price=50, ... }
]

Shells (Caparazones)

Change the character’s body color:
var shells = [
    { 
        name="Caparazón Azul", 
        category="caparazon", 
        price=10, 
        icon=preload("res://tienda/Images/Skin_Blue.png"), 
        preview=preload("res://tienda/Images/Skin_Blue.png") 
    },
    { name="Caparazón Verde", category="caparazon", price=10, ... },
    { name="Caparazón Purpura", category="caparazon", price=10, ... },
    { name="Caparazón Gris", category="caparazon", price=10, ... }
]

Shop UI Flow

Opening the Shop

From the main menu:
scripts/main_menu.gd
@onready var tienda_button := $CanvasLayer/HBoxContainer/Tienda

func _ready() -> void:
    tienda_button.pressed.connect(_on_tienda_pressed)

func _on_tienda_pressed():
    get_tree().change_scene_to_file("res://tienda/scenes/StoreMenu.tscn")

Initialization

tienda/Script/store_menu.gd
func _ready():
    show_accessories()  # Default to accessories view
    title_label.text = "¡Bienvenido a la Tienda!"
    update_coins()
    
    # Connect category buttons
    btn_accesorios.pressed.connect(func():
        $SFX_Back.play()
        _on_btn_accesorios_pressed()
    )
    btn_caparazones.pressed.connect(func():
        $SFX_Back.play()
        _on_btn_caparazones_pressed()
    )
    
    # Connect item buttons
    btn_corona.pressed.connect(func(): _show_item_info(accesories[0]))
    btn_shell1.pressed.connect(func(): _show_item_info(shells[0]))
    # ...

Purchase System

Item Info Panel

Clicking an item opens a detailed info panel:
func _show_item_info(item: Dictionary):
    info_panel.visible = true
    shop_dim_overlay.visible = true
    
    # Animate overlay
    var tween = create_tween()
    tween.tween_property(shop_dim_overlay, "color:a", 0.6, 0.25)
    
    # Display item details
    info_icon.texture = item.preview
    info_name.text = item.name
    info_price.text = "Precio: %s monedas" % item.price
    
    # Configure button state (see below)
    _update_button_state(item)

Button States

The purchase/equip button has three states:
func _show_item_info(item: Dictionary):
    # ... previous code ...
    
    var tengo_item = ScoreManager.get_inventory().has(item.name)
    var esta_equipado = ScoreManager.is_equipped(item.name)
    
    if esta_equipado:
        # STATE 1: Already equipped
        btn_comprar.disabled = true
        btn_comprar.modulate = Color(0.5, 0.5, 0.5)
        label_btn_comprar.text = "EQUIPADO"
        
    elif tengo_item:
        # STATE 2: Owned but not equipped
        btn_comprar.disabled = false
        btn_comprar.modulate = Color(1, 1, 1)
        label_btn_comprar.text = "EQUIPAR"
        btn_comprar.pressed.connect(func(): _on_equip_item(item))
        
    else:
        # STATE 3: Not owned (can purchase)
        btn_comprar.disabled = false
        btn_comprar.modulate = Color(1, 1, 1)
        label_btn_comprar.text = "COMPRAR"
        btn_comprar.pressed.connect(func(): _on_buy_item(item))

Buying Items

func _on_buy_item(item: Dictionary):
    if ScoreManager.get_coins() >= item.price:
        # Purchase successful
        ScoreManager.add_to_inventory(item.name)
        ScoreManager.add_coins(-item.price)  # Deduct coins
        
        update_coins()
        print("Comprado:", item.name)
        $SFX_Boton.play()
        
        # Refresh panel to show "EQUIPAR" button
        _show_item_info(item)
    else:
        # Insufficient funds
        print("No tienes monedas suficientes")
        $SFX_Incorrect.play()

Equipping Items

func _on_equip_item(item: Dictionary):
    ScoreManager.equip_item(item.category, item.name)
    $SFX_Boton.play()
    print("Equipado:", item.name)
    
    # Refresh panel to show "EQUIPADO" button
    _show_item_info(item)

Visual Updates

When returning to the main menu, the character sprite updates to reflect equipped items:
scripts/main_menu.gd
@onready var nyuron_visual: AnimatedSprite2D = $NyuronVisual

var menu_color_codes = {
    "default": "",
    "caparazon": "",
    "Caparazón Azul": "_blue",
    "Caparazón Verde": "_green",
    "Caparazón Purpura": "_purple",
    "Caparazón Gris": "_gray"
}

var menu_accessory_codes = {
    "none": "",
    "ninguno": "",
    "Corona": "_corona",
    "Gafas": "_lentes",
    "Gorro": "_gorro",
    "Cadena": "_cadena"
}

func _ready() -> void:
    load_and_update_coins()
    update_menu_skin()  # Apply customizations
    # ...

func update_menu_skin():
    var id_cuerpo = ScoreManager.get_equipped_item("caparazon")
    var id_accesorio = ScoreManager.get_equipped_item("accesorio")
    
    # Build filename from equipped items
    var color_suffix = menu_color_codes.get(id_cuerpo, "")
    var acc_suffix = menu_accessory_codes.get(id_accesorio, "")
    
    var folder_path = "res://Accesorios/global/"
    var base_filename = "spr_rest"
    
    # Example: spr_rest_blue_corona.png
    var final_path = folder_path + base_filename + color_suffix + acc_suffix + ".png"
    
    if ResourceLoader.exists(final_path):
        var new_texture = load(final_path)
        _apply_texture_to_menu_anim(new_texture)
        print("Skin Menú aplicada: ", final_path)
    else:
        print("ERROR: No se encontró skin de menú: ", final_path)

Applying Texture to Animation

The character uses AnimatedSprite2D with AtlasTexture frames:
func _apply_texture_to_menu_anim(new_texture: Texture2D):
    if nyuron_visual == null:
        print("ERROR: No encuentro el nodo AnimatedSprite2D en el menú.")
        return
    
    var frames = nyuron_visual.sprite_frames
    var anim_name = "default"
    
    if frames.has_animation(anim_name):
        var frame_count = frames.get_frame_count(anim_name)
        
        # Replace atlas texture for each frame
        for i in range(frame_count):
            var frame_texture = frames.get_frame_texture(anim_name, i)
            
            if frame_texture is AtlasTexture:
                frame_texture.atlas = new_texture
        
        # Restart animation with new texture
        nyuron_visual.stop()
        nyuron_visual.play(anim_name)
    else:
        print("Error: Falta animación '", anim_name, "'")

Texture Naming Convention

All character textures follow this pattern:
res://Accesorios/global/spr_rest[color][accessory].png
Examples:
  • spr_rest.png - Default (no customization)
  • spr_rest_blue.png - Blue shell, no accessory
  • spr_rest_corona.png - Default shell, crown accessory
  • spr_rest_blue_corona.png - Blue shell + crown
  • spr_rest_purple_lentes.png - Purple shell + glasses

Category Switching

Players can toggle between accessories and shells:
func show_accessories():
    for node in accessories_group:
        node.visible = true
    for node in shells_group:
        node.visible = false

func show_shells():
    for node in accessories_group:
        node.visible = false
    for node in shells_group:
        node.visible = true

func _on_btn_accesorios_pressed():
    show_accessories()

func _on_btn_caparazones_pressed():
    show_shells()

Audio Feedback

@onready var sfx_boton: AudioStreamPlayer = $SFX_Boton
@onready var sfx_incorrect: AudioStreamPlayer = $SFX_Incorrect
@onready var sfx_back: AudioStreamPlayer = $SFX_Back
@onready var sfx_volver: AudioStreamPlayer = $SFX_Volver

func _on_buy_item(item: Dictionary):
    if ScoreManager.get_coins() >= item.price:
        # ...
        $SFX_Boton.play()
    else:
        # ...
        $SFX_Incorrect.play()

func _on_equip_item(item: Dictionary):
    # ...
    $SFX_Boton.play()

Button Visual Effects

The purchase button has a press animation:
func _ready():
    # ...
    btn_comprar.button_down.connect(_on_btn_comprar_down)
    btn_comprar.button_up.connect(_on_btn_comprar_up)

func _on_btn_comprar_down():
    if label_btn_comprar:
        label_btn_comprar.position.y += 2  # Move down 2 pixels

func _on_btn_comprar_up():
    if label_btn_comprar:
        label_btn_comprar.position.y -= 2  # Restore position

Returning to Main Menu

func _on_volver_pressed():
    get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
The main menu automatically calls update_menu_skin() in _ready(), so changes are immediately visible.

Complete Purchase Flow Example

  1. Player clicks “Corona” item
    • _show_item_info(accesories[0]) is called
    • Info panel shows: “Corona, Precio: 25 monedas”
    • Button shows “COMPRAR” (if not owned)
  2. Player clicks “COMPRAR”
    • _on_buy_item() checks if player has 25+ coins
    • If yes:
      • Adds “Corona” to inventory via ScoreManager.add_to_inventory()
      • Deducts 25 coins via ScoreManager.add_coins(-25)
      • Updates coin display
      • Plays success sound
      • Refreshes panel (button now shows “EQUIPAR”)
  3. Player clicks “EQUIPAR”
    • _on_equip_item() calls ScoreManager.equip_item("accesorio", "Corona")
    • ScoreManager saves equipped state
    • Emits skin_updated signal
    • Panel refreshes (button now shows “EQUIPADO” and is disabled)
  4. Player returns to main menu
    • Main menu loads
    • update_menu_skin() reads equipped items
    • Loads spr_rest_corona.png (or combined texture)
    • Character visually updates

Common Issues

Texture Not Found

ERROR: No se encontró skin de menú: res://Accesorios/global/spr_rest_blue_corona.png
Solution: Ensure all texture combinations exist:
  • Base textures for each shell color
  • Combined textures for each accessory + shell combination
  • Follow exact naming convention

Items Not Persisting

If purchases don’t save:
  • Check that ScoreManager.save_to_file() is called in add_to_inventory()
  • Verify user://high_scores.cfg is writable
  • Check console for save errors

Button Not Updating

If button state doesn’t change after purchase:
  • Ensure _show_item_info(item) is called after purchase
  • Clear previous button connections before adding new ones
  • Check that ScoreManager.is_equipped() returns correct value

Build docs developers (and LLMs) love