""" Deck Builder Frame Build decks with 6 cards and view combined effects with breakdown """ import tkinter as tk from tkinter import ttk, messagebox import sys import os from PIL import Image, ImageTk sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from db.db_queries import ( get_all_cards, get_all_decks, create_deck, delete_deck, add_card_to_deck, remove_card_from_deck, get_deck_cards, get_effects_at_level ) from utils import resolve_image_path from gui.theme import ( BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT, ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR, TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED, FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_TINY, TYPE_COLORS, get_type_color, get_type_icon, create_styled_button, create_styled_text, create_card_frame ) class CardSlot(tk.Frame): """Visual component for a single card slot""" def __init__(self, parent, index, remove_callback, level_callback): super().__init__(parent, bg=BG_MEDIUM, highlightthickness=2, highlightbackground=BG_LIGHT) self.index = index self.remove_callback = remove_callback self.level_callback = level_callback self.image_ref = None # Keep reference to prevent GC self.setup_ui() def setup_ui(self): # Configure grid weight self.columnconfigure(1, weight=1) # Slot number indicator slot_label = tk.Label(self, text=f"#{self.index + 1}", font=FONT_TINY, bg=BG_LIGHT, fg=TEXT_MUTED, padx=4, pady=2) slot_label.place(x=2, y=2) # Image Area (Left) self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED, font=('Segoe UI', 32)) self.image_label.grid(row=0, column=0, rowspan=3, padx=12, pady=12) # Details Area (Right) self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_PRIMARY, font=FONT_BODY_BOLD, anchor='w', wraplength=180) # Increased wrap self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(15, 0)) self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED, font=FONT_SMALL, anchor='w') self.meta_label.grid(row=1, column=1, sticky='w', padx=8) # Controls (Bottom Right) ctrl_frame = tk.Frame(self, bg=BG_MEDIUM) ctrl_frame.grid(row=2, column=1, sticky='ew', padx=8, pady=8) # Level Selector tk.Label(ctrl_frame, text="Lv:", bg=BG_MEDIUM, fg=TEXT_MUTED, font=FONT_SMALL).pack(side=tk.LEFT) self.level_var = tk.StringVar(value="50") self.level_combo = ttk.Combobox(ctrl_frame, textvariable=self.level_var, values=[], width=4, state='readonly') self.level_combo.pack(side=tk.LEFT, padx=4) self.level_combo.bind('<>', self._on_level_change) # Remove Button self.remove_btn = tk.Button(ctrl_frame, text="✕", bg=BG_LIGHT, fg=ACCENT_ERROR, bd=0, font=FONT_BODY_BOLD, width=2, activebackground=ACCENT_ERROR, activeforeground=TEXT_PRIMARY, cursor='hand2', command=lambda: self.remove_callback(self.index)) self.remove_btn.pack(side=tk.RIGHT) # Hide controls initially self.toggle_controls(False) def toggle_controls(self, visible): state = 'normal' if visible else 'disabled' self.level_combo.config(state='readonly' if visible else 'disabled') if not visible: self.remove_btn.pack_forget() else: self.remove_btn.pack(side=tk.RIGHT) def set_card(self, card_data): """Set card data: (id, name, rarity, type, image_path, level)""" if not card_data: self.reset() return card_id, name, rarity, card_type, image_path, level = card_data # Calculate valid levels based on rarity if rarity == 'SSR': valid_levels = [50, 45, 40, 35, 30] max_lvl = 50 elif rarity == 'SR': valid_levels = [45, 40, 35, 30, 25] max_lvl = 45 else: # R valid_levels = [40, 35, 30, 25, 20] max_lvl = 40 self.level_combo['values'] = [str(l) for l in valid_levels] # Snap level to valid value if not present (e.g. old data) if level not in valid_levels: level = max_lvl # Update styling based on type color = get_type_color(card_type) type_icon = get_type_icon(card_type) self.name_label.config(text=name, fg=TEXT_PRIMARY) self.meta_label.config(text=f"{type_icon} {rarity} │ {card_type}", fg=color) self.level_var.set(str(level)) # Update border color based on rarity rarity_borders = {'SSR': '#ffd700', 'SR': '#c0c0c0', 'R': '#cd853f'} self.config(highlightbackground=rarity_borders.get(rarity, BG_LIGHT)) # Load Image self._load_image(image_path) self.toggle_controls(True) def reset(self): self.name_label.config(text="Empty Slot", fg=TEXT_MUTED) self.meta_label.config(text="Click a card to add") self.image_label.config(image='', text="📭", font=('Segoe UI', 32)) self.config(highlightbackground=BG_LIGHT) self.image_ref = None self.toggle_controls(False) def _load_image(self, path): resolved_path = resolve_image_path(path) if resolved_path and os.path.exists(resolved_path): try: pil_img = Image.open(resolved_path) # Significantly larger images as requested (120x120) pil_img.thumbnail((120, 120), Image.Resampling.LANCZOS) self.image_ref = ImageTk.PhotoImage(pil_img) self.image_label.config(image=self.image_ref, text='') except Exception as e: print(f"Failed to load image: {e}") self.image_label.config(image='', text="⚠️") else: self.image_label.config(image='', text="🖼️") def _on_level_change(self, event): self.level_callback(self.index, int(self.level_var.get())) class DeckBuilderFrame(ttk.Frame): """Deck builder with combined effects breakdown""" def __init__(self, parent): super().__init__(parent) self.current_deck_id = None self.deck_slots = [None] * 6 # 6 card slots self.setup_ui() self.refresh_decks() def setup_ui(self): # Main container with split view main_split = ttk.PanedWindow(self, orient=tk.HORIZONTAL) main_split.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # === Left Panel: Card Browser === left_panel = ttk.Frame(main_split) main_split.add(left_panel, weight=1) # Header header = tk.Frame(left_panel, bg=BG_DARK) header.pack(fill=tk.X, pady=(0, 10)) tk.Label(header, text="📋 Available Cards", font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT) # Filters filter_frame = tk.Frame(left_panel, bg=BG_DARK) filter_frame.pack(fill=tk.X, pady=(0, 8)) # Filters - Initialize vars FIRST self.type_var = tk.StringVar(value="All") self.owned_only_var = tk.BooleanVar(value=False) self.search_var = tk.StringVar() # Search Entry self.search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=18) self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8)) # Placeholder behavior (before trace) self.search_entry.insert(0, "Search...") self.search_entry.config(foreground=TEXT_MUTED) self.search_entry.bind('', self._on_search_focus_in) self.search_entry.bind('', self._on_search_focus_out) # Add trace AFTER placeholder is set self.search_var.trace('w', lambda *args: self.filter_cards()) types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"] type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var, values=types, width=9, state='readonly') type_combo.pack(side=tk.LEFT) type_combo.bind('<>', lambda e: self.filter_cards()) ttk.Checkbutton(filter_frame, text="Owned", variable=self.owned_only_var, command=self.filter_cards).pack(side=tk.LEFT, padx=8) # Card List list_frame = tk.Frame(left_panel, bg=BG_DARK) list_frame.pack(fill=tk.BOTH, expand=True) self.card_tree = ttk.Treeview(list_frame, columns=('name', 'rarity', 'type'), show='tree headings', style="DeckList.Treeview") self.card_tree.heading('#0', text='') self.card_tree.column('#0', width=45, anchor='center') self.card_tree.heading('name', text='Name') self.card_tree.heading('rarity', text='Rarity') self.card_tree.heading('type', text='Type') self.card_tree.column('name', width=130) self.card_tree.column('rarity', width=45, anchor='center') self.card_tree.column('type', width=65, anchor='center') scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.card_tree.yview) self.card_tree.configure(yscrollcommand=scrollbar.set) self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # Double-click to add self.card_tree.bind('', lambda e: self.add_selected_to_deck()) # Add Button add_btn = create_styled_button(left_panel, text="➕ Add to Deck", command=self.add_selected_to_deck, style_type='accent') add_btn.pack(fill=tk.X, pady=10) # === Right Panel: Deck & Stats === right_panel = ttk.Frame(main_split) main_split.add(right_panel, weight=2) # Deck Controls deck_ctrl = tk.Frame(right_panel, bg=BG_DARK) deck_ctrl.pack(fill=tk.X, pady=(0, 15)) tk.Label(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY, bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT) self.deck_combo = ttk.Combobox(deck_ctrl, width=25, state='readonly') self.deck_combo.pack(side=tk.LEFT, padx=10) self.deck_combo.bind('<>', self.on_deck_selected) ttk.Button(deck_ctrl, text="+ New", command=self.create_new_deck, style='Small.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck, style='Small.TButton').pack(side=tk.LEFT) # Card count indicator self.deck_count_label = tk.Label(deck_ctrl, text="0/6 cards", font=FONT_SMALL, bg=BG_DARK, fg=ACCENT_PRIMARY) self.deck_count_label.pack(side=tk.LEFT, padx=15) # Deck Grid (3x2) self.slots_frame = tk.Frame(right_panel, bg=BG_DARK) self.slots_frame.pack(fill=tk.X) self.card_slots = [] for i in range(6): slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed) r, c = divmod(i, 3) slot.grid(row=r, column=c, padx=6, pady=6, sticky='nsew') self.slots_frame.columnconfigure(c, weight=1) self.card_slots.append(slot) # Stats / Effects Area effects_header = tk.Frame(right_panel, bg=BG_DARK) effects_header.pack(fill=tk.X, pady=(20, 10)) tk.Label(effects_header, text="📊 Combined Effects Breakdown", font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT) effects_frame = create_card_frame(right_panel) effects_frame.pack(fill=tk.BOTH, expand=True) self.effects_tree = ttk.Treeview(effects_frame, columns=('effect', 'total', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'), show='headings', height=8) self.effects_tree.heading('effect', text='Effect') self.effects_tree.heading('total', text='TOTAL') self.effects_tree.column('effect', width=140) self.effects_tree.column('total', width=60, anchor='center') for i in range(1, 7): self.effects_tree.heading(f'c{i}', text=f'#{i}') self.effects_tree.column(f'c{i}', width=45, anchor='center') vsb = ttk.Scrollbar(effects_frame, orient=tk.VERTICAL, command=self.effects_tree.yview) self.effects_tree.configure(yscrollcommand=vsb.set) self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2) vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2) # Unique Effects Area unique_header = tk.Frame(right_panel, bg=BG_DARK) unique_header.pack(fill=tk.X, pady=(15, 8)) tk.Label(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD, bg=BG_DARK, fg=ACCENT_SECONDARY).pack(side=tk.LEFT) unique_frame = create_card_frame(right_panel) unique_frame.pack(fill=tk.X) self.unique_text = create_styled_text(unique_frame, height=5) self.unique_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) self.unique_text.config(state=tk.DISABLED) self.icon_cache = {} self.filter_cards() # Helper methods for placeholder def _on_search_focus_in(self, event): """Clear placeholder on focus""" if self.search_entry.get() == "Search...": self.search_entry.delete(0, tk.END) self.search_entry.config(foreground=TEXT_PRIMARY) def _on_search_focus_out(self, event): """Show placeholder if empty""" if not self.search_entry.get(): self.search_entry.insert(0, "Search...") self.search_entry.config(foreground=TEXT_MUTED) # --- Logic Methods --- def filter_cards(self): for item in self.card_tree.get_children(): self.card_tree.delete(item) type_filter = self.type_var.get() if self.type_var.get() != "All" else None # Ignore placeholder search_text = self.search_var.get() search = search_text if search_text and search_text != "Search..." else None owned_only = self.owned_only_var.get() cards = get_all_cards(type_filter=type_filter, search_term=search, owned_only=owned_only) for card in cards: card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card # Load Icon img = self.icon_cache.get(card_id) resolved_path = resolve_image_path(image_path) if not img and resolved_path and os.path.exists(resolved_path): try: pil_img = Image.open(resolved_path) # Larger thumbnails in the list too (48x48) pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS) img = ImageTk.PhotoImage(pil_img) self.icon_cache[card_id] = img except: pass type_icon = get_type_icon(card_type) if img: self.card_tree.insert('', tk.END, text='', image=img, values=(name, rarity, f"{type_icon}"), iid=str(card_id)) else: self.card_tree.insert('', tk.END, text='?', values=(name, rarity, f"{type_icon}"), iid=str(card_id)) def refresh_decks(self): decks = get_all_decks() self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks] if decks and not self.current_deck_id: self.deck_combo.current(0) self.on_deck_selected(None) def on_deck_selected(self, event): selection = self.deck_combo.get() if selection: self.current_deck_id = int(selection.split(':')[0]) self.load_deck() def load_deck(self): if not self.current_deck_id: return # Reset visual slots for s in self.card_slots: s.reset() self.deck_slots = [None] * 6 # Load from DB deck_cards = get_deck_cards(self.current_deck_id) for card in deck_cards: slot_pos, level, card_id, name, rarity, card_type, image_path = card if 0 <= slot_pos < 6: self.deck_slots[slot_pos] = card_id self.card_slots[slot_pos].set_card((card_id, name, rarity, card_type, image_path, level)) self.update_deck_count() self.update_effects_breakdown() def create_new_deck(self): name = tk.simpledialog.askstring("New Deck", "Enter deck name:") if name: deck_id = create_deck(name) self.current_deck_id = deck_id self.refresh_decks() self.deck_combo.set(f"{deck_id}: {name}") self.load_deck() def delete_current_deck(self): if self.current_deck_id: if messagebox.askyesno("Delete Deck", "Are you sure you want to delete this deck?"): delete_deck(self.current_deck_id) self.current_deck_id = None self.deck_combo.set('') self.refresh_decks() self.load_deck() def add_selected_to_deck(self): if not self.current_deck_id: messagebox.showwarning("No Deck", "Select or create a deck first.") return selection = self.card_tree.selection() if not selection: return card_id = int(selection[0]) # Check for duplicates if card_id in self.deck_slots: messagebox.showinfo("Duplicate Card", "This card is already in the deck.") return # Find empty slot for i in range(6): if self.deck_slots[i] is None: # Get the last selected level for this card from main window level = 50 parent = self.winfo_toplevel() if hasattr(parent, 'last_selected_levels'): level = parent.last_selected_levels.get(card_id, 50) add_card_to_deck(self.current_deck_id, card_id, i, level) self.load_deck() return messagebox.showinfo("Deck Full", "Remove a card first to add a new one.") def remove_from_slot(self, index): if self.current_deck_id and self.deck_slots[index]: remove_card_from_deck(self.current_deck_id, index) self.deck_slots[index] = None self.card_slots[index].reset() self.update_deck_count() self.update_effects_breakdown() def update_deck_count(self): """Update the X/6 cards display""" count = sum(1 for slot in self.deck_slots if slot is not None) self.deck_count_label.config(text=f"{count}/6 cards") def on_slot_level_changed(self, index, new_level): if self.current_deck_id and self.deck_slots[index]: card_id = self.deck_slots[index] add_card_to_deck(self.current_deck_id, card_id, index, new_level) self.update_effects_breakdown() def update_effects_breakdown(self): for item in self.effects_tree.get_children(): self.effects_tree.delete(item) # Clear Unique Text self.unique_text.config(state=tk.NORMAL) self.unique_text.delete('1.0', tk.END) if not self.current_deck_id: self.unique_text.insert(tk.END, "No deck selected") self.unique_text.config(state=tk.DISABLED) return # Prepare data for calculation card_info = [] for i in range(6): if self.deck_slots[i]: level = int(self.card_slots[i].level_var.get()) card_info.append((self.deck_slots[i], level)) else: card_info.append(None) # Gather effects all_effects = {} unique_effects_list = [] for i, info in enumerate(card_info): if info: card_id, level = info card_name = self.card_slots[i].name_label.cget("text") effects = get_effects_at_level(card_id, level) for name, value in effects: if name == "Unique Effect": unique_effects_list.append(f"• {card_name}: {value}") continue if name not in all_effects: all_effects[name] = [''] * 6 all_effects[name][i] = value # Configure tags self.unique_text.tag_configure('card_name', foreground=ACCENT_PRIMARY) # Fill Unique Effects if unique_effects_list: self.unique_text.insert(tk.END, "\n".join(unique_effects_list)) else: self.unique_text.insert(tk.END, "No unique effects in this deck", 'card_name') self.unique_text.config(state=tk.DISABLED) # Sum totals for effect_name, values in sorted(all_effects.items()): total = 0 is_percent = False for v in values: if v: if '%' in str(v): is_percent = True try: total += float(str(v).replace('%','').replace('+','')) except: pass total_str = f"{total:.0f}%" if is_percent else (f"+{total:.0f}" if total > 0 else str(int(total))) row_vals = [effect_name, total_str] + values self.effects_tree.insert('', tk.END, values=row_vals) import tkinter.simpledialog