diff --git a/db/db_queries.py b/db/db_queries.py index a1b4e4f..7f9cfaf 100644 --- a/db/db_queries.py +++ b/db/db_queries.py @@ -788,9 +788,11 @@ def get_cards_with_skill(skill_name): # 1. Check Hints cur.execute(""" - SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, sh.hint_description + SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, sh.hint_description, + CASE WHEN oc.card_id IS NOT NULL THEN 1 ELSE 0 END as is_owned FROM support_hints sh JOIN support_cards sc ON sh.card_id = sc.card_id + LEFT JOIN owned_cards oc ON sc.card_id = oc.card_id WHERE sh.hint_name = ? """, (skill_name,)) @@ -804,33 +806,44 @@ def get_cards_with_skill(skill_name): 'type': row[3], 'image_path': row[4], 'source': 'Training Hint', - 'details': row[5] or "Random hint event" + 'details': row[5] or "Random hint event", + 'is_owned': bool(row[6]) }) seen_entries.add(entry_key) # 2. Check Event Skills cur.execute(""" - SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, se.event_name + SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, se.event_name, se.event_id, + CASE WHEN oc.card_id IS NOT NULL THEN 1 ELSE 0 END as is_owned FROM event_skills es JOIN support_events se ON es.event_id = se.event_id JOIN support_cards sc ON se.card_id = sc.card_id + LEFT JOIN owned_cards oc ON sc.card_id = oc.card_id WHERE es.skill_name = ? """, (skill_name,)) - for row in cur.fetchall(): - # Clean event name if it has newlines or excessive spaces - event_name = row[5].replace('\n', ' ').strip() - entry_key = (row[0], f'Event: {event_name}') + rows = cur.fetchall() + for row in rows: + card_id, name, rarity, card_type, image_path, event_name, event_id, is_owned = row + event_name = event_name.replace('\n', ' ').strip() + + # Get ALL skills for this event to show in details + cur.execute("SELECT skill_name FROM event_skills WHERE event_id = ?", (event_id,)) + other_skills = [r[0] for r in cur.fetchall()] + skills_summary = ", ".join(other_skills) + + entry_key = (card_id, f'Event: {event_name}') if entry_key not in seen_entries: results.append({ - 'card_id': row[0], - 'name': row[1], - 'rarity': row[2], - 'type': row[3], - 'image_path': row[4], + 'card_id': card_id, + 'name': name, + 'rarity': rarity, + 'type': card_type, + 'image_path': image_path, 'source': 'Event', - 'details': event_name + 'details': f"{event_name} ({skills_summary})", + 'is_owned': bool(is_owned) }) seen_entries.add(entry_key) diff --git a/gui/hints_skills_view.py b/gui/hints_skills_view.py index 051bc39..03d356b 100644 --- a/gui/hints_skills_view.py +++ b/gui/hints_skills_view.py @@ -10,7 +10,7 @@ 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_unique_skills, get_cards_with_skill +from db.db_queries import get_all_unique_skills, get_cards_with_skill, get_card_by_id, get_hints, get_all_event_skills from utils import resolve_image_path from gui.theme import ( BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT, @@ -88,11 +88,12 @@ class SkillSearchFrame(ttk.Frame): tree_frame = create_card_frame(right_frame) tree_frame.pack(fill=tk.BOTH, expand=True, padx=10) - cols = ('name', 'rarity', 'type', 'source', 'details') + cols = ('owned', 'name', 'rarity', 'type', 'source', 'details') self.tree = ttk.Treeview(tree_frame, columns=cols, show='tree headings', style="Treeview") self.tree.heading('#0', text='') + self.tree.heading('owned', text='★') self.tree.heading('name', text='Card Name') self.tree.heading('rarity', text='Rarity') self.tree.heading('type', text='Type') @@ -100,6 +101,7 @@ class SkillSearchFrame(ttk.Frame): self.tree.heading('details', text='Details') self.tree.column('#0', width=50, anchor='center') + self.tree.column('owned', width=30, anchor='center') self.tree.column('name', width=180) self.tree.column('rarity', width=50, anchor='center') self.tree.column('type', width=80, anchor='center') @@ -173,8 +175,10 @@ class SkillSearchFrame(ttk.Frame): pass type_display = f"{get_type_icon(card['type'])} {card['type']}" + owned_mark = "★" if card.get('is_owned') else "" values = ( + owned_mark, card['name'], card['rarity'], type_display, @@ -191,10 +195,77 @@ class SkillSearchFrame(ttk.Frame): def set_card(self, card_id): """ - Legacy method compatibility for main window calls. - We don't need to do anything here since we are skill-centric now. - But main_window might call it when card is selected in tab 1. - We could potentially auto-search a skill from that card? - For now, just ignore it. + Show all skills (Hints and Events) for a specific card. + Called by main window when a card is selected in the list. """ - pass + card = get_card_by_id(card_id) + if not card: return + + card_name = card[1] + self.result_header.config(text=f"Skills for: {card_name}") + + # Clear tree + for item in self.tree.get_children(): + self.tree.delete(item) + + all_skills = [] + + # 1. Get Hints + hints = get_hints(card_id) + for h_name, h_desc in hints: + all_skills.append({ + 'card_id': card_id, + 'name': card_name, + 'rarity': card[2], + 'type': card[3], + 'image_path': card[6], + 'source': 'Training Hint', + 'details': f"{h_name}: {h_desc}", + 'is_owned': bool(card[7]) + }) + + # 2. Get Event Skills + event_dict = get_all_event_skills(card_id) + for ev_name, skills in event_dict.items(): + all_skills.append({ + 'card_id': card_id, + 'name': card_name, + 'rarity': card[2], + 'type': card[3], + 'image_path': card[6], + 'source': 'Event', + 'details': f"{ev_name} ({', '.join(skills)})", + 'is_owned': bool(card[7]) + }) + + # Display them + for skill in all_skills: + img = self.icon_cache.get(skill['card_id']) + if not img: + resolved_path = resolve_image_path(skill['image_path']) + if resolved_path and os.path.exists(resolved_path): + try: + pil_img = Image.open(resolved_path) + pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS) + img = ImageTk.PhotoImage(pil_img) + self.icon_cache[skill['card_id']] = img + except: pass + + type_display = f"{get_type_icon(skill['type'])} {skill['type']}" + owned_mark = "★" if skill.get('is_owned') else "" + + values = ( + owned_mark, + skill['name'], + skill['rarity'], + type_display, + skill['source'], + skill['details'] + ) + + if img: + self.tree.insert('', tk.END, text='', image=img, values=values) + else: + self.tree.insert('', tk.END, text='?', values=values) + + self.stats_label.config(text=f"Showing {len(all_skills)} skill sources for {card_name}") diff --git a/gui/main_window.py b/gui/main_window.py index a6378a5..682dd2c 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -14,7 +14,6 @@ from db.db_queries import get_database_stats, get_owned_count from gui.card_view import CardListFrame from gui.effects_view import EffectsFrame from gui.hints_skills_view import SkillSearchFrame -from gui.training_sim import TrainingSimFrame from gui.deck_builder import DeckBuilderFrame from gui.update_dialog import show_update_dialog from gui.theme import ( @@ -163,10 +162,6 @@ class MainWindow: # Skill Search Tab self.hints_frame = SkillSearchFrame(self.notebook) self.notebook.add(self.hints_frame, text=" 🔍 Skill Search (Beta) ") - - # Training Sim Tab - self.sim_frame = TrainingSimFrame(self.notebook) - self.notebook.add(self.sim_frame, text=" 📈 Training Sim (Beta) ") def create_status_bar(self, parent): """Create status bar at bottom""" diff --git a/gui/training_sim.py b/gui/training_sim.py deleted file mode 100644 index e9449ae..0000000 --- a/gui/training_sim.py +++ /dev/null @@ -1,235 +0,0 @@ -""" -Training Simulator View - Estimate training gains -""" - -import tkinter as tk -from tkinter import ttk -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from db.db_queries import get_all_decks, get_deck_combined_effects -from gui.theme import ( - BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT, - ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY, - ACCENT_SUCCESS, ACCENT_WARNING, ACCENT_ERROR, - TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED, - FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, - create_card_frame, create_styled_button -) - -class TrainingSimFrame(ttk.Frame): - """Frame for training simulation and calculus""" - - def __init__(self, parent): - super().__init__(parent) - self.current_deck_id = None - self.deck_bonuses = {} - - self.create_widgets() - self.refresh_decks() - - def create_widgets(self): - """Create the simulation interface""" - # Main layout: Top Controls, Bottom Results - - # === Controls Section === - controls_frame = tk.Frame(self, bg=BG_DARK) - controls_frame.pack(fill=tk.X, padx=20, pady=20) - - # Deck Selection - deck_group = tk.LabelFrame(controls_frame, text="1. Select Deck", - bg=BG_DARK, fg=ACCENT_PRIMARY, font=FONT_BODY_BOLD) - deck_group.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 20)) - - self.deck_combo = ttk.Combobox(deck_group, width=25, state='readonly') - self.deck_combo.pack(padx=15, pady=15) - self.deck_combo.bind('<>', self.on_deck_selected) - - # Motivation - mood_group = tk.LabelFrame(controls_frame, text="2. Motivation", - bg=BG_DARK, fg=ACCENT_PRIMARY, font=FONT_BODY_BOLD) - mood_group.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 20)) - - self.mood_var = tk.StringVar(value="Perfect (x1.2)") - moods = ["Perfect (x1.2)", "Good (x1.1)", "Normal (x1.0)", "Bad (x0.9)", "Terrible (x0.8)"] - self.mood_combo = ttk.Combobox(mood_group, textvariable=self.mood_var, - values=moods, width=15, state='readonly') - self.mood_combo.pack(padx=15, pady=15) - self.mood_combo.bind('<>', self.recalculate) - - # Facility Levels - fac_group = tk.LabelFrame(controls_frame, text="3. Facility Levels", - bg=BG_DARK, fg=ACCENT_PRIMARY, font=FONT_BODY_BOLD) - fac_group.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - slider_frame = tk.Frame(fac_group, bg=BG_DARK) - slider_frame.pack(fill=tk.X, padx=15, pady=5) - - self.fac_vars = {} - types = ['Speed', 'Stamina', 'Power', 'Guts', 'Wisdom'] - colors = [ACCENT_SECONDARY, ACCENT_SUCCESS, '#eab308', '#ef4444', ACCENT_TERTIARY] - - for i, (t, color) in enumerate(zip(types, colors)): - f = tk.Frame(slider_frame, bg=BG_DARK) - f.pack(side=tk.LEFT, expand=True, fill=tk.X) - - tk.Label(f, text=t, fg=color, bg=BG_DARK, font=FONT_SMALL).pack() - - var = tk.IntVar(value=1) - self.fac_vars[t] = var - scale = tk.Scale(f, from_=1, to=5, orient=tk.HORIZONTAL, variable=var, - bg=BG_DARK, fg=TEXT_SECONDARY, highlightthickness=0, - command=lambda v: self.recalculate()) - scale.pack(fill=tk.X, padx=5) - - # === Results Section === - results_container = tk.Frame(self, bg=BG_DARK) - results_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20)) - - tk.Label(results_container, text="📊 Estimated Gains per Turn", - font=FONT_HEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(anchor='w', pady=(0, 10)) - - # Grid Frame - self.grid_frame = create_card_frame(results_container) - self.grid_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - - # Initialize Grid - self.create_results_grid() - - def create_results_grid(self): - """Build the results table grid""" - # Headers - headers = ["Stat", "Speed", "Stamina", "Power", "Guts", "Wisdom"] - self.grid_frame.columnconfigure(0, weight=1) # Labels - for i in range(1, 6): - self.grid_frame.columnconfigure(i, weight=1) - - # Header Label - tk.Label(self.grid_frame, text=headers[i], font=FONT_SUBHEADER, - bg=BG_LIGHT, fg=TEXT_PRIMARY, - pady=10).grid(row=0, column=i, sticky='nsew', padx=1, pady=1) - - # Row Labels - row_labels = ["Speed", "Stamina", "Power", "Guts", "Wisdom", "Skill Pt", "Energy"] - self.result_cells = {} # Map (row_name, col_name) -> widget - - for r, label in enumerate(row_labels, 1): - # Row Header - tk.Label(self.grid_frame, text=label, font=FONT_BODY_BOLD, - bg=BG_MEDIUM, fg=TEXT_SECONDARY, - padx=10).grid(row=r, column=0, sticky='nsew', padx=1, pady=1) - - for c, col_type in enumerate(["Speed", "Stamina", "Power", "Guts", "Wisdom"], 1): - # Cell - lbl = tk.Label(self.grid_frame, text="-", font=FONT_BODY, - bg=BG_DARK, fg=TEXT_PRIMARY) - lbl.grid(row=r, column=c, sticky='nsew', padx=1, pady=1) - self.result_cells[(label, col_type)] = lbl - - def refresh_decks(self): - """Load decks into combobox""" - decks = get_all_decks() - self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks] - if decks: - self.deck_combo.current(0) - self.on_deck_selected(None) - - def on_deck_selected(self, event): - """Handle deck selection""" - selection = self.deck_combo.get() - if selection: - deck_id = int(selection.split(':')[0]) - self.current_deck_id = deck_id - - # Get bonuses - effects = get_deck_combined_effects(deck_id) - self.deck_bonuses = {k: v['total'] for k, v in effects.items()} - - self.recalculate() - - def recalculate(self, *args): - """Calculate and update stats""" - if not self.current_deck_id: - return - - # 1. Get Environment - mood_multi = float(self.mood_var.get().split('x')[1].replace(')', '')) - - # 2. Base Gains (URA Scenario approx) - # Structure: {TrainingType: {Stat: BaseVal}} - # Scaling with Facility Level (1-5) - - def get_base(train_type, level): - # Approximation of base gains - # Level scaling: approx +1 or +2 per level - lvl_mod = level - 1 - - base = {} - if train_type == 'Speed': - base = {'Speed': 10 + (lvl_mod*2), 'Power': 0 + (lvl_mod if lvl_mod>2 else 0)} - elif train_type == 'Stamina': - base = {'Stamina': 9 + (lvl_mod*2), 'Guts': 4 + lvl_mod} - elif train_type == 'Power': - base = {'Power': 9 + (lvl_mod*2), 'Stamina': 4 + lvl_mod} - elif train_type == 'Guts': - base = {'Guts': 8 + (lvl_mod*2), 'Speed': 4 + lvl_mod, 'Power': 3} - elif train_type == 'Wisdom': - base = {'Wisdom': 9 + (lvl_mod*2), 'Speed': 2} - - # Common Skill Pt - base['Skill Pt'] = 2 - - # Energy cost - if train_type == 'Wisdom': - base['Energy'] = 5 # Gain - else: - base['Energy'] = -20 # Loss (simplified) - - return base - - # 3. Calculate for each column (Training Type) - types = ['Speed', 'Stamina', 'Power', 'Guts', 'Wisdom'] - - for t_type in types: - fac_level = self.fac_vars[t_type].get() - base_stats = get_base(t_type, fac_level) - - # Apply Bonuses - # Formula: (Base + StatBonus) * (1 + TrainingEffect%) * Mood - - train_effect_bonus = self.deck_bonuses.get('Training Effectiveness', 0) / 100.0 - # Motivation bonus (from deck) is complex, let's ignore for MVP sim - - for stat_row in ["Speed", "Stamina", "Power", "Guts", "Wisdom", "Skill Pt", "Energy"]: - cell = self.result_cells[(stat_row, t_type)] - - if stat_row == 'Energy': - # Energy calculation - val = base_stats.get('Energy', 0) - if val < 0: # Usage - # Apply Energy Discount - discount = self.deck_bonuses.get('Energy Discount', 0) / 100.0 - val = val * (1 - discount) - cell.config(text=f"{int(val)}", fg=ACCENT_SUCCESS if val > 0 else TEXT_MUTED) - continue - - base_val = base_stats.get(stat_row, 0) - - if base_val > 0: - # Apply Bonuses - stat_bonus = self.deck_bonuses.get(f'{stat_row} Bonus', 0) - - # Core calc - final = (base_val + stat_bonus) * (1 + train_effect_bonus) * mood_multi - - # Highlight good values - color = TEXT_PRIMARY - if final > 20: color = ACCENT_SUCCESS - if final > 30: color = ACCENT_SECONDARY - if final > 40: color = ACCENT_PRIMARY - - cell.config(text=f"+{int(final)}", fg=color) - else: - cell.config(text="-", fg=TEXT_MUTED)