Spaces:
Running
Running
""" | |
Redesigned Library System with Instant Discovery and Cooldown for Tag Collector Game | |
""" | |
import streamlit as st | |
import random | |
import time | |
import math | |
import pandas as pd | |
import datetime | |
from game_constants import ( | |
TAG_CURRENCY_NAME, | |
RARITY_LEVELS, | |
ENKEPHALIN_CURRENCY_NAME, | |
ENKEPHALIN_ICON, | |
TAG_POWER_BONUSES | |
) | |
from essence_generator import display_essence_generator | |
from tag_categories import ( | |
TAG_CATEGORIES, | |
get_collection_power_level | |
) | |
import tag_storage | |
# Define library properties | |
LIBRARY_INFO = { | |
"name": "The Library", | |
"description": "A vast repository of knowledge where tags are discovered through patient exploration and research.", | |
"color": "#4A148C", # Deep purple | |
"rarities_available": ["Canard", "Urban Myth", "Urban Legend", "Urban Plague", "Urban Nightmare", "Star of the City", "Impuritas Civitas"], | |
"odds_multiplier": 2.0 | |
} | |
# Define library floors with their unlocking requirements and rarity boosts | |
LIBRARY_FLOORS = [ | |
{ | |
"name": "Floor of General Works", | |
"description": "The foundation of knowledge. Contains basic tags with limited rarity.", | |
"required_tags": 0, # Available from the start | |
"rarity_boost": 0.0, | |
"color": "#8D99AE", # Light blue-gray | |
"unlocked": True, # Always unlocked | |
"rarities": ["Canard", "Urban Myth", "Urban Legend"], | |
"odds_multiplier": 1.0 # Base odds multiplier | |
}, | |
{ | |
"name": "Floor of History", | |
"description": "Archives of past knowledge. Offers more access to uncommon tags.", | |
"required_tags": 25, # Unlocked after collecting 25 tags | |
"rarity_boost": 0.2, | |
"color": "#457B9D", # Moderate blue | |
"rarities": ["Canard", "Urban Myth", "Urban Legend", "Urban Plague"], | |
"odds_multiplier": 1.2 | |
}, | |
{ | |
"name": "Floor of Technological Sciences", | |
"description": "Repository of technical knowledge. Access to rare tags begins here.", | |
"required_tags": 75, # Unlocked after collecting 75 tags | |
"rarity_boost": 0.4, | |
"color": "#2B9348", # Green | |
"rarities": ["Canard", "Urban Myth", "Urban Legend", "Urban Plague"], | |
"odds_multiplier": 1.5 | |
}, | |
{ | |
"name": "Floor of Literature", | |
"description": "A vast collection of narrative concepts. Higher chance of rare discoveries.", | |
"required_tags": 150, # Unlocked after collecting 150 tags | |
"rarity_boost": 0.6, | |
"color": "#6A0572", # Purple | |
"rarities": ["Canard", "Urban Myth", "Urban Legend", "Urban Plague", "Urban Nightmare"], | |
"odds_multiplier": 1.8 | |
}, | |
{ | |
"name": "Floor of Art", | |
"description": "The realm of aesthetic concepts. First access to Urban Nightmare tags.", | |
"required_tags": 250, # Unlocked after collecting 250 tags | |
"rarity_boost": 0.8, | |
"color": "#D90429", # Red | |
"rarities": ["Canard", "Urban Myth", "Urban Legend", "Urban Plague", "Urban Nightmare"], | |
"odds_multiplier": 2.2 | |
}, | |
{ | |
"name": "Floor of Natural Sciences", | |
"description": "Where empirical knowledge is cataloged. Significant chance of very rare tags.", | |
"required_tags": 500, # Unlocked after collecting 400 tags | |
"rarity_boost": 1.0, | |
"color": "#1A759F", # Deep blue | |
"rarities": ["Urban Myth", "Urban Legend", "Urban Plague", "Urban Nightmare", "Star of the City"], | |
"odds_multiplier": 2.5 | |
}, | |
{ | |
"name": "Floor of Language", | |
"description": "The domain of linguistic concepts. First glimpse of Star of the City tags.", | |
"required_tags": 1000, # Unlocked after collecting 600 tags | |
"rarity_boost": 1.2, | |
"color": "#FF8C00", # Orange | |
"rarities": ["Urban Myth", "Urban Legend", "Urban Plague", "Urban Nightmare", "Star of the City"], | |
"odds_multiplier": 3.0 | |
}, | |
{ | |
"name": "Floor of Social Sciences", | |
"description": "Complex social patterns and abstractions. Notable chance of exceptional rarities.", | |
"required_tags": 2000, # Unlocked after collecting 1000 tags | |
"rarity_boost": 1.4, | |
"color": "#76B041", # Brighter green | |
"rarities": ["Urban Legend", "Urban Plague", "Urban Nightmare", "Star of the City"], | |
"odds_multiplier": 3.5 | |
}, | |
{ | |
"name": "Floor of Philosophy", | |
"description": "The realm of profound thought. First access to the rarest 'Impuritas Civitas' tags.", | |
"required_tags": 5000, # Unlocked after collecting 1500 tags | |
"rarity_boost": 1.6, | |
"color": "#7209B7", # Deep purple | |
"rarities": ["Urban Legend", "Urban Plague", "Urban Nightmare", "Star of the City", "Impuritas Civitas"], | |
"odds_multiplier": 5.0 | |
}, | |
{ | |
"name": "Floor of Religion", | |
"description": "The ultimate repository of the most profound conceptual territories.", | |
"required_tags": 10000, | |
"rarity_boost": 2.0, | |
"color": "#FFBD00", # Gold | |
"rarities": ["Urban Plague", "Urban Nightmare", "Star of the City", "Impuritas Civitas"], | |
"odds_multiplier": 10.0 | |
} | |
] | |
def start_instant_expedition(): | |
""" | |
Start an instant expedition with a cooldown before next expedition. | |
Returns: | |
List of discoveries or None if on cooldown | |
""" | |
# Check if we're on cooldown | |
current_time = time.time() | |
if hasattr(st.session_state, 'last_expedition_time'): | |
elapsed_time = current_time - st.session_state.last_expedition_time | |
cooldown_duration = calculate_expedition_duration() | |
if elapsed_time < cooldown_duration: | |
# Still on cooldown | |
time_remaining = cooldown_duration - elapsed_time | |
minutes, seconds = divmod(int(time_remaining), 60) | |
st.error(f"Expedition on cooldown. {minutes:02d}:{seconds:02d} remaining.") | |
return None | |
# Generate instant discoveries | |
discoveries = generate_expedition_discoveries() | |
# Set the cooldown timer | |
st.session_state.last_expedition_time = current_time | |
# Save state | |
tag_storage.save_game(st.session_state) | |
# Preserve the current tab | |
if 'library_tab_index' not in st.session_state: | |
st.session_state.library_tab_index = 0 | |
return discoveries | |
def generate_expedition_discoveries(): | |
""" | |
Generate expedition discoveries instantly. | |
Returns: | |
List of discovered tags and their info | |
""" | |
# Get current library floor | |
current_floor = None | |
collection_size = len(st.session_state.collected_tags) if hasattr(st.session_state, 'collected_tags') else 0 | |
if hasattr(st.session_state, 'library_floors'): | |
# Find the highest unlocked floor | |
for floor in reversed(st.session_state.library_floors): | |
if collection_size >= floor["required_tags"]: | |
current_floor = floor | |
break | |
# Default to first floor if we couldn't find one | |
if not current_floor: | |
current_floor = st.session_state.library_floors[0] if hasattr(st.session_state, 'library_floors') else { | |
"name": "Archival Records", | |
"rarities": ["Canard", "Urban Myth"], | |
"rarity_boost": 0.0 | |
} | |
# Calculate rarity odds for discoveries | |
rarity_odds = calculate_rarity_odds() | |
# Calculate capacity from upgrades | |
tags_capacity = calculate_expedition_capacity() | |
# Generate discoveries | |
discoveries = [] | |
for _ in range(tags_capacity): | |
# Select a rarity based on calculated odds | |
rarities = list(rarity_odds.keys()) | |
weights = list(rarity_odds.values()) | |
selected_rarity = random.choices(rarities, weights=weights, k=1)[0] | |
# Now select a random tag with this rarity that hasn't been discovered yet | |
possible_tags = [] | |
# Check if we have tag metadata with rarity info | |
if hasattr(st.session_state, 'tag_rarity_metadata') and st.session_state.tag_rarity_metadata: | |
# Find all tags of the selected rarity | |
for tag, tag_info in st.session_state.tag_rarity_metadata.items(): | |
# Skip if already discovered | |
if tag in st.session_state.discovered_tags: | |
continue | |
# Handle both formats - new (dict with rarity) and old (just rarity string) | |
if isinstance(tag_info, dict) and "rarity" in tag_info: | |
if tag_info["rarity"] == selected_rarity: | |
possible_tags.append(tag) | |
elif tag_info == selected_rarity: | |
possible_tags.append(tag) | |
# If no undiscovered tags found in the selected rarity, fallback to already discovered tags | |
if not possible_tags: | |
if hasattr(st.session_state, 'tag_rarity_metadata') and st.session_state.tag_rarity_metadata: | |
for tag, tag_info in st.session_state.tag_rarity_metadata.items(): | |
# Skip if in different rarity | |
tag_rarity = tag_info.get("rarity", tag_info) if isinstance(tag_info, dict) else tag_info | |
if tag_rarity != selected_rarity: | |
continue | |
possible_tags.append(tag) | |
# If still no tags found (or no metadata), create a fallback | |
if not possible_tags: | |
# If we have the model's full tag list, use it | |
if hasattr(st.session_state, 'metadata') and 'idx_to_tag' in st.session_state.metadata: | |
all_tags = list(st.session_state.metadata['idx_to_tag'].values()) | |
# Just pick a random tag and assign the selected rarity | |
possible_tags = random.sample(all_tags, min(20, len(all_tags))) | |
else: | |
# Complete fallback - use some generic tags | |
possible_tags = ["portrait", "landscape", "digital_art", "anime", "realistic", | |
"fantasy", "sci-fi", "city", "nature", "character"] | |
# If we found possible tags, select one randomly | |
if possible_tags: | |
selected_tag = random.choice(possible_tags) | |
# Get category from metadata if available | |
category = "unknown" | |
if hasattr(st.session_state, 'metadata') and 'tag_to_category' in st.session_state.metadata: | |
if selected_tag in st.session_state.metadata['tag_to_category']: | |
category = st.session_state.metadata['tag_to_category'][selected_tag] | |
# Use the enhanced tag storage function to add the discovered tag | |
is_new = tag_storage.add_discovered_tag( | |
tag=selected_tag, | |
rarity=selected_rarity, | |
session_state=st.session_state, | |
library_floor=current_floor["name"], | |
category=category # Pass the category we found | |
) | |
# Record for library growth | |
st.session_state.library_growth["total_discoveries"] += 1 | |
st.session_state.library_growth["last_discovery_time"] = time.time() | |
# Create timestamp for display | |
timestamp = time.strftime("%Y-%m-%d %H:%M:%S") | |
# Add to results | |
discoveries.append({ | |
"tag": selected_tag, | |
"rarity": selected_rarity, | |
"is_new": is_new, | |
"timestamp": timestamp, | |
"library": current_floor["name"] | |
}) | |
# Save the game state after discoveries | |
tag_storage.save_library_state(session_state=st.session_state) | |
tag_storage.save_game(st.session_state) | |
return discoveries | |
def update_discovered_tag_categories(): | |
"""Update categories of discovered tags from metadata if they're unknown""" | |
if not hasattr(st.session_state, 'discovered_tags') or not st.session_state.discovered_tags: | |
return 0 | |
updated_count = 0 | |
# First try from metadata.tag_to_category | |
if hasattr(st.session_state, 'metadata') and 'tag_to_category' in st.session_state.metadata: | |
tag_to_category = st.session_state.metadata['tag_to_category'] | |
for tag, info in st.session_state.discovered_tags.items(): | |
if info.get('category', 'unknown') == 'unknown' and tag in tag_to_category: | |
info['category'] = tag_to_category[tag] | |
updated_count += 1 | |
# Then try from tag_rarity_metadata | |
if hasattr(st.session_state, 'tag_rarity_metadata') and st.session_state.tag_rarity_metadata: | |
for tag, info in st.session_state.discovered_tags.items(): | |
if info.get('category', 'unknown') == 'unknown' and tag in st.session_state.tag_rarity_metadata: | |
tag_metadata = st.session_state.tag_rarity_metadata[tag] | |
if isinstance(tag_metadata, dict) and "category" in tag_metadata: | |
info['category'] = tag_metadata["category"] | |
updated_count += 1 | |
if updated_count > 0: | |
print(f"Updated categories for {updated_count} discovered tags") | |
tag_storage.save_library_state(session_state=st.session_state) | |
return updated_count | |
def calculate_expedition_duration(): | |
""" | |
Calculate the duration of cooldown after an expedition based on upgrades. | |
Returns: | |
Duration in seconds for the cooldown | |
""" | |
base_duration = 10 # Default to 10 seconds | |
# Apply speed upgrades if they exist | |
speed_level = 1 | |
if hasattr(st.session_state, 'library_upgrades'): | |
speed_level = st.session_state.library_upgrades.get("speed", 1) | |
# Each speed level reduces duration by 10% (multiplicative) | |
duration_multiplier = 0.9 ** (speed_level - 1) | |
# Calculate final duration (minimum 1 second) | |
duration = max(1, base_duration * duration_multiplier) | |
return duration | |
def calculate_expedition_capacity(): | |
""" | |
Calculate how many tags can be discovered in one expedition. | |
Returns: | |
Number of tags that can be discovered | |
""" | |
base_capacity = 1 # Default to 1 discovery per expedition | |
# Apply capacity upgrades if they exist | |
capacity_level = 1 | |
if hasattr(st.session_state, 'library_upgrades'): | |
capacity_level = st.session_state.library_upgrades.get("capacity", 1) | |
# Each capacity level increases discoveries by 1 | |
capacity = base_capacity + (capacity_level - 1) | |
return capacity | |
def calculate_rarity_odds(): | |
""" | |
Calculate rarity odds based on library floor level and upgrades. | |
Returns: | |
Dictionary of {rarity: probability} for available rarities | |
""" | |
# Get current library floor | |
current_floor = None | |
if hasattr(st.session_state, 'library_floors'): | |
collection_size = len(st.session_state.collected_tags) if hasattr(st.session_state, 'collected_tags') else 0 | |
# Find the highest unlocked floor | |
for floor in reversed(st.session_state.library_floors): | |
if collection_size >= floor["required_tags"]: | |
current_floor = floor | |
break | |
# Default to first floor if we couldn't find one | |
if not current_floor: | |
current_floor = st.session_state.library_floors[0] if hasattr(st.session_state, 'library_floors') else { | |
"rarities": ["Canard", "Urban Myth"], | |
"rarity_boost": 0.0, | |
"odds_multiplier": 1.0 | |
} | |
# Get available rarities from current floor | |
available_rarities = current_floor.get("rarities", ["Canard", "Urban Myth"]) | |
odds_multiplier = current_floor.get("odds_multiplier", 1.0) | |
# Base weights for each rarity | |
base_weights = { | |
"Canard": 70, | |
"Urban Myth": 20, | |
"Urban Legend": 7, | |
"Urban Plague": 2, | |
"Urban Nightmare": 1, | |
"Star of the City": 0.1, | |
"Impuritas Civitas": 0.01 | |
} | |
# Apply floor's rarity boost | |
floor_rarity_boost = current_floor.get("rarity_boost", 0.0) | |
# Apply rarity upgrades if they exist | |
rarity_level = 1 | |
if hasattr(st.session_state, 'library_upgrades'): | |
rarity_level = st.session_state.library_upgrades.get("rarity", 1) | |
# Calculate boost based on rarity level | |
upgrade_rarity_boost = (rarity_level - 1) * 0.2 # Each level gives 20% more chance for rare tags | |
# Combine boosts | |
total_boost = floor_rarity_boost + upgrade_rarity_boost | |
# Adjust weights based on rarity boost | |
adjusted_weights = {} | |
for rarity in available_rarities: | |
if rarity == "Canard": | |
# Reduce common tag odds as rarity level increases | |
adjusted_weights[rarity] = base_weights[rarity] * (1.0 - total_boost * 0.7) | |
elif rarity == "Urban Myth": | |
# Slight reduction for uncommon as rarity level increases | |
adjusted_weights[rarity] = base_weights[rarity] * (1.0 - total_boost * 0.3) | |
else: | |
# Increase rare tag odds as rarity level increases | |
rarity_index = list(RARITY_LEVELS.keys()).index(rarity) | |
# Higher rarities get larger boosts | |
boost_factor = 1.0 + (total_boost * odds_multiplier * (rarity_index + 1)) | |
adjusted_weights[rarity] = base_weights[rarity] * boost_factor | |
# Normalize weights | |
total = sum(adjusted_weights.values()) | |
normalized_weights = {r: w/total for r, w in adjusted_weights.items()} | |
return normalized_weights | |
def format_time_remaining(seconds): | |
""" | |
Format seconds into a human-readable time remaining format. | |
Args: | |
seconds: Seconds remaining | |
Returns: | |
String with formatted time | |
""" | |
if seconds < 60: | |
return f"{int(seconds)} seconds" | |
elif seconds < 3600: | |
minutes = seconds / 60 | |
return f"{int(minutes)} minutes" | |
else: | |
hours = seconds / 3600 | |
minutes = (seconds % 3600) / 60 | |
if minutes > 0: | |
return f"{int(hours)} hours, {int(minutes)} minutes" | |
else: | |
return f"{int(hours)} hours" | |
def display_cooldown_timer(): | |
"""Display a countdown timer until the next expedition is available""" | |
# Check if on cooldown | |
current_time = time.time() | |
cooldown_remaining = 0 | |
cooldown_duration = calculate_expedition_duration() | |
if hasattr(st.session_state, 'last_expedition_time'): | |
elapsed_time = current_time - st.session_state.last_expedition_time | |
if elapsed_time < cooldown_duration: | |
cooldown_remaining = cooldown_duration - elapsed_time | |
# If on cooldown, show timer | |
if cooldown_remaining > 0: | |
minutes, seconds = divmod(int(cooldown_remaining), 60) | |
# Create a timer display with dark-mode styling | |
st.markdown(""" | |
<div style="background-color: rgba(255, 152, 0, 0.15); | |
border: 1px solid #FF9800; | |
border-radius: 5px; | |
padding: 10px; | |
text-align: center; | |
margin-bottom: 15px; | |
color: #ffffff;"> | |
<p style="margin: 0; font-weight: bold;">⏱️ Next expedition available in:</p> | |
<p style="font-size: 1.2em; margin: 5px 0;">{:02d}:{:02d}</p> | |
</div> | |
""".format(minutes, seconds), unsafe_allow_html=True) | |
# Add refresh button for the timer | |
if st.button("🔄 Refresh Timer", key="refresh_timer"): | |
st.rerun() | |
return True # Still on cooldown | |
return False # Not on cooldown | |
def display_library_exploration_interface(): | |
"""Display the unified interface for library exploration using Streamlit elements.""" | |
# Tag collection progress | |
tag_count = len(st.session_state.collected_tags) if hasattr(st.session_state, 'collected_tags') else 0 | |
# Check if we have tags to start exploring | |
if not hasattr(st.session_state, 'collected_tags') or not st.session_state.collected_tags: | |
st.warning("Start scanning images to collect tags first. The library will grow as you collect more tags!") | |
return | |
# Get current library floor | |
current_floor = None | |
if hasattr(st.session_state, 'library_floors'): | |
# Find the highest unlocked floor | |
for floor in reversed(st.session_state.library_floors): | |
if tag_count >= floor["required_tags"]: | |
current_floor = floor | |
break | |
# Default to first floor if we couldn't find one | |
if not current_floor: | |
current_floor = st.session_state.library_floors[0] if hasattr(st.session_state, 'library_floors') else { | |
"name": "Floor of General Works", | |
"description": "The foundational level of knowledge.", | |
"color": "#607D8B", | |
"rarities": ["Canard", "Urban Myth"] | |
} | |
# Library growth progress | |
total_discoveries = st.session_state.library_growth["total_discoveries"] | |
# Create container with colored border for current floor | |
floor_container = st.container() | |
with floor_container: | |
# Use a stylized container with dark mode theme | |
st.markdown(f""" | |
<div style="border-left: 5px solid {current_floor['color']}; | |
border-radius: 5px; | |
background-color: rgba({int(current_floor['color'][1:3], 16)}, | |
{int(current_floor['color'][3:5], 16)}, | |
{int(current_floor['color'][5:7], 16)}, 0.15); | |
padding: 15px 10px 10px 15px; | |
margin-bottom: 15px; | |
color: #ffffff;"> | |
<h3 style="margin-top: 0; color: {current_floor['color']};">{current_floor['name']}</h3> | |
<p>{current_floor['description']}</p> | |
<p>Total Discoveries: <strong>{total_discoveries}</strong></p> | |
</div> | |
""", unsafe_allow_html=True) | |
# Create a nice divider for dark theme | |
st.markdown("<hr style='margin: 20px 0; border: 0; height: 1px; background-image: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0));'>", unsafe_allow_html=True) | |
# Display expedition details | |
st.subheader("Expedition Details") | |
# Calculate capacity | |
capacity = calculate_expedition_capacity() | |
# Two columns for expedition stats | |
col1, col2 = st.columns(2) | |
with col1: | |
# Expedition duration/timer | |
cooldown_duration = calculate_expedition_duration() | |
st.write(f"📊 Cooldown: {format_time_remaining(cooldown_duration)}") | |
st.write(f"🔍 Tag Discoveries: {capacity} per expedition") | |
with col2: | |
# Calculate and display rarity odds with Streamlit elements | |
rarity_odds = calculate_rarity_odds() | |
available_rarities = current_floor.get("rarities", ["Canard", "Urban Myth"]) | |
# Display rarity chances with dark theme styling | |
for rarity in available_rarities: | |
if rarity in rarity_odds: | |
color = RARITY_LEVELS[rarity]["color"] | |
percentage = rarity_odds[rarity]*100 | |
# Custom styling based on rarity | |
if rarity == "Impuritas Civitas": | |
st.markdown(f""" | |
<div style="display: flex; align-items: center; margin-bottom: 5px;"> | |
<span style="animation: rainbow-text 4s linear infinite; font-weight: bold; width: 140px;">{rarity}:</span> | |
<div style="flex-grow: 1; background-color: #2c2c2c; border-radius: 5px; height: 10px;"> | |
<div style="width: {min(percentage*5, 100)}%; background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); height: 10px; border-radius: 5px;"></div> | |
</div> | |
<span style="margin-left: 10px;">{percentage:.1f}%</span> | |
</div> | |
""", unsafe_allow_html=True) | |
elif rarity == "Star of the City": | |
st.markdown(f""" | |
<div style="display: flex; align-items: center; margin-bottom: 5px;"> | |
<span style="color:{color}; text-shadow: 0 0 3px gold; font-weight: bold; width: 140px;">{rarity}:</span> | |
<div style="flex-grow: 1; background-color: #2c2c2c; border-radius: 5px; height: 10px;"> | |
<div style="width: {min(percentage*5, 100)}%; background-color: {color}; box-shadow: 0 0 5px gold; height: 10px; border-radius: 5px;"></div> | |
</div> | |
<span style="margin-left: 10px;">{percentage:.1f}%</span> | |
</div> | |
""", unsafe_allow_html=True) | |
elif rarity == "Urban Nightmare": | |
st.markdown(f""" | |
<div style="display: flex; align-items: center; margin-bottom: 5px;"> | |
<span style="color:{color}; text-shadow: 0 0 1px #FF5722; font-weight: bold; width: 140px;">{rarity}:</span> | |
<div style="flex-grow: 1; background-color: #2c2c2c; border-radius: 5px; height: 10px;"> | |
<div style="width: {min(percentage*5, 100)}%; background-color: {color}; animation: pulse-bar 3s infinite; height: 10px; border-radius: 5px;"></div> | |
</div> | |
<span style="margin-left: 10px;">{percentage:.1f}%</span> | |
</div> | |
""", unsafe_allow_html=True) | |
else: | |
st.markdown(f""" | |
<div style="display: flex; align-items: center; margin-bottom: 5px;"> | |
<span style="color:{color}; font-weight: bold; width: 140px;">{rarity}:</span> | |
<div style="flex-grow: 1; background-color: #2c2c2c; border-radius: 5px; height: 10px;"> | |
<div style="width: {min(percentage*5, 100)}%; background-color: {color}; height: 10px; border-radius: 5px;"></div> | |
</div> | |
<span style="margin-left: 10px;">{percentage:.1f}%</span> | |
</div> | |
""", unsafe_allow_html=True) | |
# Check for cooldown and display timer if needed | |
on_cooldown = display_cooldown_timer() | |
# Add button to start expedition regardless of cooldown status | |
# The button will always be displayed, but if on cooldown, expedition won't start | |
if st.button("🚀 Start Expedition", key="start_expedition", use_container_width=True, disabled=on_cooldown): | |
if not on_cooldown: | |
discoveries = start_instant_expedition() | |
if discoveries: | |
# Store discovery results for display | |
st.session_state.expedition_results = discoveries | |
# Show success message | |
st.success(f"Expedition completed! Discovered {len(discoveries)} new tags!") | |
# Show balloons for celebration | |
st.balloons() | |
# Display the results | |
display_expedition_results(discoveries) | |
# Save state | |
tag_storage.save_game(st.session_state) | |
else: | |
# This should not be reached due to disabled button, but just in case | |
st.error("Expedition on cooldown. Please wait until the timer expires.") | |
# Display library upgrades | |
display_library_upgrades() | |
def display_expedition_results(results): | |
"""Display results from completed expeditions using Streamlit elements with enhanced dark-mode visuals.""" | |
st.subheader("Expedition Discoveries") | |
# Add animations CSS for dark theme | |
st.markdown(""" | |
<style> | |
@keyframes rainbow-text { | |
0% { color: red; } | |
14% { color: orange; } | |
28% { color: yellow; } | |
42% { color: green; } | |
57% { color: blue; } | |
71% { color: indigo; } | |
85% { color: violet; } | |
100% { color: red; } | |
} | |
@keyframes rainbow-border { | |
0% { border-color: red; } | |
14% { border-color: orange; } | |
28% { border-color: yellow; } | |
42% { border-color: green; } | |
57% { border-color: blue; } | |
71% { border-color: indigo; } | |
85% { border-color: violet; } | |
100% { border-color: red; } | |
} | |
@keyframes star-glow { | |
0% { box-shadow: 0 0 5px #FFD700; } | |
50% { box-shadow: 0 0 15px #FFD700; } | |
100% { box-shadow: 0 0 5px #FFD700; } | |
} | |
@keyframes nightmare-pulse { | |
0% { border-color: #FF9800; } | |
50% { border-color: #FF5722; } | |
100% { border-color: #FF9800; } | |
} | |
@keyframes pulse-bar { | |
0% { opacity: 0.8; } | |
50% { opacity: 1; } | |
100% { opacity: 0.8; } | |
} | |
.expedition-tag-impuritas { | |
animation: rainbow-text 4s linear infinite; | |
font-weight: bold; | |
} | |
.expedition-card-impuritas { | |
background-color: rgba(255, 0, 0, 0.15); | |
border-radius: 8px; | |
border: 3px solid red; | |
padding: 12px; | |
animation: rainbow-border 4s linear infinite; | |
color: #ffffff; | |
} | |
.expedition-card-star { | |
background-color: rgba(255, 215, 0, 0.15); | |
border-radius: 8px; | |
border: 2px solid gold; | |
padding: 12px; | |
animation: star-glow 2s infinite; | |
color: #ffffff; | |
} | |
.expedition-card-nightmare { | |
background-color: rgba(255, 152, 0, 0.15); | |
border-radius: 8px; | |
border: 2px solid #FF9800; | |
padding: 12px; | |
animation: nightmare-pulse 3s infinite; | |
color: #ffffff; | |
} | |
.expedition-card-plague { | |
background-color: rgba(156, 39, 176, 0.12); | |
border-radius: 8px; | |
border: 1px solid #9C27B0; | |
padding: 12px; | |
box-shadow: 0 0 3px #9C27B0; | |
color: #ffffff; | |
} | |
.expedition-card-legend { | |
background-color: rgba(33, 150, 243, 0.15); | |
border-radius: 8px; | |
border: 1px solid #2196F3; | |
padding: 12px; | |
color: #ffffff; | |
} | |
.expedition-card-myth { | |
background-color: rgba(76, 175, 80, 0.15); | |
border-radius: 8px; | |
border: 1px solid #4CAF50; | |
padding: 12px; | |
color: #ffffff; | |
} | |
.expedition-card-canard { | |
background-color: rgba(170, 170, 170, 0.15); | |
border-radius: 8px; | |
border: 1px solid #AAAAAA; | |
padding: 12px; | |
color: #ffffff; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Group by rarity first for better organization | |
results_by_rarity = {} | |
for result in results: | |
rarity = result["rarity"] | |
if rarity not in results_by_rarity: | |
results_by_rarity[rarity] = [] | |
results_by_rarity[rarity].append(result) | |
# Get ordered rarities (rarest first) | |
ordered_rarities = list(RARITY_LEVELS.keys()) | |
ordered_rarities.reverse() # Reverse to display rarest first | |
# Display rare discoveries first | |
for rarity in ordered_rarities: | |
if rarity not in results_by_rarity: | |
continue | |
rarity_results = results_by_rarity[rarity] | |
if not rarity_results: | |
continue | |
color = RARITY_LEVELS[rarity]["color"] | |
# Special styling for rare discoveries | |
if rarity == "Impuritas Civitas": | |
st.markdown(f""" | |
<div style="background-color: rgba(255, 0, 0, 0.15); border-radius: 10px; padding: 15px; margin-bottom: 20px; border: 2px solid red; animation: rainbow-border 4s linear infinite; color: #ffffff;"> | |
<h3 style="margin-top: 0; animation: rainbow-text 4s linear infinite;">✨ EXTRAORDINARY DISCOVERY!</h3> | |
<p>You found {len(rarity_results)} Impuritas Civitas tag(s)!</p> | |
</div> | |
""", unsafe_allow_html=True) | |
st.balloons() # Add celebration effect | |
elif rarity == "Star of the City": | |
st.markdown(f""" | |
<div style="background-color: rgba(255, 215, 0, 0.15); border-radius: 10px; padding: 15px; margin-bottom: 20px; border: 2px solid gold; animation: star-glow 2s infinite; color: #ffffff;"> | |
<h3 style="margin-top: 0; color: {color}; text-shadow: 0 0 3px gold;">🌟 EXCEPTIONAL DISCOVERY!</h3> | |
<p>You found {len(rarity_results)} Star of the City tag(s)!</p> | |
</div> | |
""", unsafe_allow_html=True) | |
elif rarity == "Urban Nightmare": | |
st.markdown(f""" | |
<div style="background-color: rgba(255, 152, 0, 0.15); border-radius: 10px; padding: 15px; margin-bottom: 20px; border: 2px solid #FF9800; animation: nightmare-pulse 3s infinite; color: #ffffff;"> | |
<h3 style="margin-top: 0; color: {color}; text-shadow: 0 0 1px #FF5722;">👑 RARE DISCOVERY!</h3> | |
<p>You found {len(rarity_results)} Urban Nightmare tag(s)!</p> | |
</div> | |
""", unsafe_allow_html=True) | |
elif rarity == "Urban Plague": | |
st.markdown(f""" | |
<div style="background-color: rgba(156, 39, 176, 0.12); border-radius: 10px; padding: 15px; margin-bottom: 20px; border: 1px solid #9C27B0; box-shadow: 0 0 3px #9C27B0; color: #ffffff;"> | |
<h3 style="margin-top: 0; color: {color}; text-shadow: 0 0 1px #9C27B0;">⚔️ UNCOMMON DISCOVERY!</h3> | |
<p>You found {len(rarity_results)} Urban Plague tag(s)!</p> | |
</div> | |
""", unsafe_allow_html=True) | |
else: | |
st.markdown(f"### {rarity} ({len(rarity_results)} discoveries)") | |
# Display tags in this rarity | |
cols = st.columns(3) | |
for i, result in enumerate(rarity_results): | |
col_idx = i % 3 | |
with cols[col_idx]: | |
tag = result["tag"] | |
floor_name = result.get("library", "Library") | |
# Get the appropriate card class based on rarity | |
rarity_class = rarity.lower().replace(' ', '-') | |
card_class = f"expedition-card-{rarity_class}" | |
# Create styled card for each tag | |
tag_html = f"""<div class="{card_class}">""" | |
# Special styling for the tag name based on rarity | |
if rarity == "Impuritas Civitas": | |
tag_html += f"""<p style="font-size: 1.1em; margin-bottom: 5px;"><span class="expedition-tag-impuritas">✨ {tag}</span></p>""" | |
elif rarity == "Star of the City": | |
tag_html += f"""<p style="font-size: 1.1em; margin-bottom: 5px;"><span style="color: {color}; text-shadow: 0 0 3px gold; font-weight: bold;">🌟 {tag}</span></p>""" | |
elif rarity == "Urban Nightmare": | |
tag_html += f"""<p style="font-size: 1.1em; margin-bottom: 5px;"><span style="color: {color}; text-shadow: 0 0 1px #FF5722; font-weight: bold;">👑 {tag}</span></p>""" | |
elif rarity == "Urban Plague": | |
tag_html += f"""<p style="font-size: 1.1em; margin-bottom: 5px;"><span style="color: {color}; text-shadow: 0 0 1px #9C27B0; font-weight: bold;">⚔️ {tag}</span></p>""" | |
else: | |
tag_html += f"""<p style="font-size: 1.1em; margin-bottom: 5px;"><span style="color: {color}; font-weight: bold;">{tag}</span></p>""" | |
# Mark as new if it is | |
is_new = result.get("is_new", False) | |
new_badge = """<span style="background-color: #4CAF50; color: white; padding: 2px 6px; border-radius: 10px; font-size: 0.7em; margin-left: 5px;">NEW</span>""" if is_new else "" | |
# Add other tag details | |
tag_html += f""" | |
<p style="margin: 0; font-size: 0.9em;">Found in: {floor_name} {new_badge}</p> | |
<p style="margin: 5px 0 0 0; font-size: 0.9em;">Rarity: <span style="color: {color};">{rarity}</span></p> | |
</div> | |
""" | |
st.markdown(tag_html, unsafe_allow_html=True) | |
# Add separator between rarity groups | |
st.markdown("<hr style='margin: 20px 0; border: 0; height: 1px; background-image: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0));'>", unsafe_allow_html=True) | |
def display_library_building(): | |
"""Display a visual representation of the library building with all floors.""" | |
st.subheader("The Great Library Building") | |
# Get collection size | |
collection_size = len(st.session_state.collected_tags) if hasattr(st.session_state, 'collected_tags') else 0 | |
# Determine current floor | |
current_floor_index = 0 | |
for i, floor in enumerate(st.session_state.library_floors): | |
if collection_size >= floor["required_tags"]: | |
current_floor_index = i | |
# Create a visual representation of the library building | |
total_floors = len(st.session_state.library_floors) | |
# Enhanced CSS for the library building with dark theme | |
st.markdown(""" | |
<style> | |
@keyframes floor-glow { | |
0% { box-shadow: 0 0 5px rgba(13, 110, 253, 0.5); } | |
50% { box-shadow: 0 0 15px rgba(13, 110, 253, 0.8); } | |
100% { box-shadow: 0 0 5px rgba(13, 110, 253, 0.5); } | |
} | |
.library-roof { | |
background: linear-gradient(90deg, #8D6E63, #A1887F); | |
height: 35px; | |
width: 90%; | |
margin: 0 auto; | |
border-radius: 8px 8px 0 0; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
color: white; | |
font-weight: bold; | |
box-shadow: 0 -2px 10px rgba(0,0,0,0.4); | |
} | |
.library-floor { | |
height: 65px; | |
width: 80%; | |
margin: 0 auto; | |
border: 1px solid #444; | |
display: flex; | |
align-items: center; | |
padding: 0 20px; | |
position: relative; | |
transition: all 0.3s ease; | |
color: #ffffff; | |
background-color: #2c2c2c; | |
} | |
.library-floor:hover { | |
transform: translateX(10px); | |
} | |
.library-floor.current { | |
box-shadow: 0 0 10px rgba(13, 110, 253, 0.5); | |
z-index: 2; | |
animation: floor-glow 2s infinite; | |
border-left: 5px solid #0d6efd; | |
} | |
.library-floor.locked { | |
background-color: #1e1e1e; | |
color: #777; | |
filter: grayscale(50%); | |
} | |
.library-floor-number { | |
position: absolute; | |
left: -30px; | |
width: 25px; | |
height: 25px; | |
background-color: #0d6efd; | |
color: white; | |
border-radius: 50%; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
font-weight: bold; | |
} | |
.library-floor.locked .library-floor-number { | |
background-color: #555; | |
} | |
.library-entrance { | |
background: linear-gradient(90deg, #5D4037, #795548); | |
height: 45px; | |
width: 35%; | |
margin: 0 auto; | |
border-radius: 10px 10px 0 0; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
color: white; | |
font-weight: bold; | |
box-shadow: 0 -2px 10px rgba(0,0,0,0.4); | |
} | |
.library-floor-details { | |
flex: 1; | |
} | |
.library-floor-name { | |
font-weight: bold; | |
margin: 0; | |
} | |
.library-floor-description { | |
font-size: 0.85em; | |
margin: 3px 0 0 0; | |
opacity: 0.9; | |
} | |
.library-floor-status { | |
display: flex; | |
align-items: center; | |
font-weight: bold; | |
} | |
.library-floor-rarities { | |
font-size: 0.8em; | |
margin-top: 4px; | |
} | |
.rarity-dot { | |
display: inline-block; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
margin-right: 3px; | |
} | |
/* Special animations for rarer floors */ | |
.library-floor.star { | |
background-color: rgba(255, 215, 0, 0.15); | |
} | |
.library-floor.impuritas { | |
background-color: rgba(255, 0, 0, 0.15); | |
} | |
@keyframes rainbow-border { | |
0% { border-color: red; } | |
14% { border-color: orange; } | |
28% { border-color: yellow; } | |
42% { border-color: green; } | |
57% { border-color: blue; } | |
71% { border-color: indigo; } | |
85% { border-color: violet; } | |
100% { border-color: red; } | |
} | |
.rainbow-border { | |
animation: rainbow-border 4s linear infinite; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Roof | |
st.markdown('<div class="library-roof">🏛️ The Great Library</div>', unsafe_allow_html=True) | |
# Display floors from top (highest) to bottom | |
for i in reversed(range(total_floors)): | |
floor = st.session_state.library_floors[i] | |
is_current = i == current_floor_index | |
is_unlocked = collection_size >= floor["required_tags"] | |
# Style based on floor status | |
floor_class = "library-floor" | |
if is_current: | |
floor_class += " current" | |
if not is_unlocked: | |
floor_class += " locked" | |
# Add special classes for highest floors | |
if i >= 8 and is_unlocked: # Impuritas Civitas level floors | |
floor_class += " impuritas" | |
elif i >= 6 and is_unlocked: # Star of the City level floors | |
floor_class += " star" | |
# Determine rarity dots HTML | |
rarity_dots = "" | |
for rarity in floor.get("rarities", []): | |
color = RARITY_LEVELS[rarity]["color"] | |
rarity_dots += f'<span class="rarity-dot" style="background-color:{color};"></span>' | |
# Floor style based on color | |
if is_unlocked: | |
floor_style = f"background-color: rgba({int(floor['color'][1:3], 16)}, {int(floor['color'][3:5], 16)}, {int(floor['color'][5:7], 16)}, 0.25);" | |
else: | |
floor_style = "" | |
# Special border animation for highest floor | |
border_class = "" | |
if i == 9 and is_unlocked: # Top floor | |
border_class = "rainbow-border" | |
# Display the floor | |
floor_content = f""" | |
<div class="{floor_class} {border_class}" style="{floor_style}"> | |
<span class="library-floor-number">{i+1}</span> | |
<div class="library-floor-details"> | |
<p class="library-floor-name">{floor['name']}</p> | |
<p class="library-floor-description">{floor['description'] if is_unlocked else 'Locked'}</p> | |
<div class="library-floor-rarities">{rarity_dots}</div> | |
</div> | |
<div class="library-floor-status"> | |
{"🔓" if is_unlocked else "🔒"} {floor['required_tags']} tags | |
</div> | |
</div> | |
""" | |
st.markdown(floor_content, unsafe_allow_html=True) | |
# Entrance | |
st.markdown('<div class="library-entrance">📚 Entrance</div>', unsafe_allow_html=True) | |
# Floor details expander | |
with st.expander("Floor Details", expanded=False): | |
# Create a table with styled rarities for dark theme | |
st.markdown(""" | |
<style> | |
.floor-details-table { | |
width: 100%; | |
border-collapse: collapse; | |
color: #ffffff; | |
} | |
.floor-details-table th { | |
background-color: #333; | |
padding: 8px; | |
text-align: left; | |
border: 1px solid #444; | |
} | |
.floor-details-table td { | |
padding: 8px; | |
border: 1px solid #444; | |
} | |
.floor-details-table tr:nth-child(even) { | |
background-color: rgba(255,255,255,0.03); | |
} | |
.floor-details-table tr:nth-child(odd) { | |
background-color: rgba(0,0,0,0.2); | |
} | |
.floor-details-table tr:hover { | |
background-color: rgba(13, 110, 253, 0.1); | |
} | |
.current-floor { | |
background-color: rgba(13, 110, 253, 0.15) !important; | |
} | |
</style> | |
<table class="floor-details-table"> | |
<tr> | |
<th>Floor</th> | |
<th>Name</th> | |
<th>Status</th> | |
<th>Req. Tags</th> | |
<th>Rarities</th> | |
<th>Rarity Boost</th> | |
</tr> | |
""", unsafe_allow_html=True) | |
# Add each floor to the table | |
for i, floor in enumerate(st.session_state.library_floors): | |
is_unlocked = collection_size >= floor["required_tags"] | |
is_current = i == current_floor_index | |
# Format rarities with colors | |
rarity_text = "" | |
for rarity in floor.get("rarities", []): | |
color = RARITY_LEVELS[rarity]["color"] | |
# Special styling based on rarity | |
if rarity == "Impuritas Civitas": | |
rarity_text += f"<span style='animation: rainbow-text 4s linear infinite;'>{rarity}</span>, " | |
elif rarity == "Star of the City": | |
rarity_text += f"<span style='color:{color}; text-shadow: 0 0 3px gold;'>{rarity}</span>, " | |
elif rarity == "Urban Nightmare": | |
rarity_text += f"<span style='color:{color}; text-shadow: 0 0 1px #FF5722;'>{rarity}</span>, " | |
elif rarity == "Urban Plague": | |
rarity_text += f"<span style='color:{color}; text-shadow: 0 0 1px #9C27B0;'>{rarity}</span>, " | |
else: | |
rarity_text += f"<span style='color:{color};'>{rarity}</span>, " | |
# Current floor class | |
row_class = "current-floor" if is_current else "" | |
# Add the floor row | |
st.markdown(f""" | |
<tr class="{row_class}"> | |
<td>{i+1}</td> | |
<td>{floor["name"]}</td> | |
<td>{"🔓 Unlocked" if is_unlocked else "🔒 Locked"}</td> | |
<td>{floor["required_tags"]}</td> | |
<td>{rarity_text[:-2] if rarity_text else ""}</td> | |
<td>+{int(floor.get('rarity_boost', 0) * 100)}%</td> | |
</tr> | |
""", unsafe_allow_html=True) | |
# Close the table | |
st.markdown("</table>", unsafe_allow_html=True) | |
def add_discovered_tag(tag, rarity, library_floor=None): | |
""" | |
Add a tag to the discovered tags with enriched metadata | |
Args: | |
tag: The tag name | |
rarity: The tag rarity level | |
library_floor: The library floor where it was discovered (optional) | |
Returns: | |
bool: True if it's a new discovery, False if already discovered | |
""" | |
is_new = tag not in st.session_state.discovered_tags | |
# Get current time | |
timestamp = time.strftime("%Y-%m-%d %H:%M:%S") | |
# Get tag category if metadata is available | |
category = "unknown" | |
if hasattr(st.session_state, 'tag_rarity_metadata') and st.session_state.tag_rarity_metadata: | |
if tag in st.session_state.tag_rarity_metadata: | |
tag_info = st.session_state.tag_rarity_metadata[tag] | |
if isinstance(tag_info, dict) and "category" in tag_info: | |
category = tag_info["category"] | |
# Get or update tag info | |
if is_new: | |
tag_info = { | |
"rarity": rarity, | |
"discovery_time": timestamp, | |
"category": category, | |
"discovery_count": 1, | |
"last_seen": timestamp | |
} | |
if library_floor: | |
tag_info["library_floor"] = library_floor | |
st.session_state.discovered_tags[tag] = tag_info | |
# Track exploration of library tiers | |
if 'explored_library_tiers' not in st.session_state: | |
st.session_state.explored_library_tiers = set() | |
if library_floor: | |
st.session_state.explored_library_tiers.add(library_floor) | |
else: | |
# Update existing tag | |
tag_info = st.session_state.discovered_tags[tag] | |
tag_info["discovery_count"] = tag_info.get("discovery_count", 1) + 1 | |
tag_info["last_seen"] = timestamp | |
# Only update library floor if provided | |
if library_floor: | |
tag_info["library_floor"] = library_floor | |
# Track exploration | |
if 'explored_library_tiers' not in st.session_state: | |
st.session_state.explored_library_tiers = set() | |
st.session_state.explored_library_tiers.add(library_floor) | |
# Save state after updating | |
tag_storage.save_library_state(session_state=st.session_state) | |
return is_new | |
def calculate_upgrade_cost(upgrade_type, current_level): | |
base_cost = { | |
"speed": 50, | |
"capacity": 100, | |
"rarity": 150 | |
} | |
# Cost increases with each level | |
return int(base_cost[upgrade_type] * (current_level * 1.5)) | |
def purchase_library_upgrade(upgrade_type): | |
""" | |
Purchase a library upgrade | |
Args: | |
upgrade_type: The type of upgrade ("speed", "capacity", or "rarity") | |
Returns: | |
bool: True if purchased successfully, False otherwise | |
""" | |
# Get current level and calculate cost | |
current_level = st.session_state.library_upgrades.get(upgrade_type, 1) | |
cost = calculate_upgrade_cost(upgrade_type, current_level) | |
# Check if player can afford it | |
if st.session_state.tag_currency < cost: | |
return False | |
# Apply purchase | |
st.session_state.tag_currency -= cost | |
st.session_state.library_upgrades[upgrade_type] = current_level + 1 | |
# Update stats | |
if hasattr(st.session_state, 'game_stats'): | |
if "currency_spent" not in st.session_state.game_stats: | |
st.session_state.game_stats["currency_spent"] = 0 | |
st.session_state.game_stats["currency_spent"] += cost | |
# Save state | |
tag_storage.save_library_state(session_state=st.session_state) | |
tag_storage.save_game(st.session_state) | |
return True | |
def display_library_upgrades(): | |
"""Display and manage upgrades for the library using Streamlit elements with enhanced visuals.""" | |
st.subheader("Library Upgrades") | |
# Add styling for upgrade cards with dark theme | |
st.markdown(""" | |
<style> | |
.upgrade-card { | |
border: 1px solid #444; | |
border-radius: 10px; | |
padding: 15px; | |
margin-bottom: 20px; | |
background-color: #222; | |
color: #ffffff; | |
transition: transform 0.2s, box-shadow 0.2s; | |
} | |
.upgrade-card:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.3); | |
} | |
.upgrade-title { | |
font-size: 1.2em; | |
font-weight: bold; | |
margin-bottom: 10px; | |
color: #ffffff; | |
} | |
.upgrade-level { | |
display: inline-block; | |
background-color: #0d6efd; | |
color: white; | |
padding: 3px 8px; | |
border-radius: 10px; | |
font-size: 0.8em; | |
margin-bottom: 10px; | |
} | |
.upgrade-stat { | |
display: flex; | |
align-items: center; | |
margin-bottom: 5px; | |
} | |
.upgrade-stat-label { | |
width: 100px; | |
font-size: 0.9em; | |
color: #adb5bd; | |
} | |
.upgrade-stat-value { | |
font-weight: bold; | |
} | |
.upgrade-cost { | |
margin-top: 10px; | |
font-weight: bold; | |
color: #6610f2; | |
} | |
.level-bar { | |
height: 6px; | |
background-color: #333; | |
border-radius: 3px; | |
margin-bottom: 10px; | |
overflow: hidden; | |
} | |
.level-progress { | |
height: 100%; | |
background-color: #0d6efd; | |
border-radius: 3px; | |
} | |
@keyframes pulse-button { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.05); } | |
100% { transform: scale(1); } | |
} | |
.pulse-button { | |
animation: pulse-button 2s infinite; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
st.write("Improve your expeditions with these upgrades:") | |
# Create columns for each upgrade type | |
col1, col2, col3 = st.columns(3) | |
# Get current upgrade levels | |
upgrades = st.session_state.library_upgrades | |
# Speed upgrade | |
with col1: | |
speed_level = upgrades.get("speed", 1) | |
speed_cost = calculate_upgrade_cost("speed", speed_level) | |
# Calculate max level for progress bar | |
max_level = 10 | |
progress_percentage = min(100, (speed_level / max_level) * 100) | |
# Create an upgrade card with progress bar in dark theme | |
st.markdown(f""" | |
<div class="upgrade-card"> | |
<div class="upgrade-title">⏱️ Speed Upgrade</div> | |
<div class="upgrade-level">Level {speed_level}</div> | |
<div class="level-bar"> | |
<div class="level-progress" style="width: {progress_percentage}%;"></div> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Effect:</span> | |
<span class="upgrade-stat-value">Reduces cooldown time</span> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Current:</span> | |
<span class="upgrade-stat-value">{format_time_remaining(calculate_expedition_duration())}</span> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Next Level:</span> | |
<span class="upgrade-stat-value">{format_time_remaining(calculate_expedition_duration() * 0.9)}</span> | |
</div> | |
<div class="upgrade-cost" style="color: #9D4EDD;">Cost: {speed_cost} {TAG_CURRENCY_NAME}</div> | |
</div> | |
""", unsafe_allow_html=True) | |
# Upgrade button | |
can_afford = st.session_state.tag_currency >= speed_cost | |
button_class = "pulse-button" if can_afford else "" | |
if st.button(f"Upgrade Speed", key=f"upgrade_speed", disabled=not can_afford, use_container_width=True): | |
if purchase_library_upgrade("speed"): | |
st.success(f"Speed upgraded to level {speed_level + 1}!") | |
st.rerun() | |
else: | |
st.error(f"Not enough {TAG_CURRENCY_NAME}. Need {speed_cost}.") | |
# Capacity upgrade | |
with col2: | |
capacity_level = upgrades.get("capacity", 1) | |
capacity_cost = calculate_upgrade_cost("capacity", capacity_level) | |
# Calculate max level for progress bar | |
max_level = 10 | |
progress_percentage = min(100, (capacity_level / max_level) * 100) | |
# Create an upgrade card with progress bar in dark theme | |
st.markdown(f""" | |
<div class="upgrade-card"> | |
<div class="upgrade-title">🔍 Capacity Upgrade</div> | |
<div class="upgrade-level">Level {capacity_level}</div> | |
<div class="level-bar"> | |
<div class="level-progress" style="width: {progress_percentage}%;"></div> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Effect:</span> | |
<span class="upgrade-stat-value">Increases tags discovered</span> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Current:</span> | |
<span class="upgrade-stat-value">{calculate_expedition_capacity()} tags</span> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Next Level:</span> | |
<span class="upgrade-stat-value">{calculate_expedition_capacity() + 1} tags</span> | |
</div> | |
<div class="upgrade-cost" style="color: #9D4EDD;">Cost: {capacity_cost} {TAG_CURRENCY_NAME}</div> | |
</div> | |
""", unsafe_allow_html=True) | |
# Upgrade button | |
can_afford = st.session_state.tag_currency >= capacity_cost | |
button_class = "pulse-button" if can_afford else "" | |
if st.button(f"Upgrade Capacity", key=f"upgrade_capacity", disabled=not can_afford, use_container_width=True): | |
if purchase_library_upgrade("capacity"): | |
st.success(f"Capacity upgraded to level {capacity_level + 1}!") | |
st.rerun() | |
else: | |
st.error(f"Not enough {TAG_CURRENCY_NAME}. Need {capacity_cost}.") | |
# Rarity upgrade | |
with col3: | |
rarity_level = upgrades.get("rarity", 1) | |
rarity_cost = calculate_upgrade_cost("rarity", rarity_level) | |
# Calculate max level for progress bar | |
max_level = 10 | |
progress_percentage = min(100, (rarity_level / max_level) * 100) | |
# Create an upgrade card with progress bar in dark theme | |
st.markdown(f""" | |
<div class="upgrade-card"> | |
<div class="upgrade-title">💎 Rarity Upgrade</div> | |
<div class="upgrade-level">Level {rarity_level}</div> | |
<div class="level-bar"> | |
<div class="level-progress" style="width: {progress_percentage}%;"></div> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Effect:</span> | |
<span class="upgrade-stat-value">Improves rare tag chance</span> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Current:</span> | |
<span class="upgrade-stat-value">+{(rarity_level - 1) * 20}% boost</span> | |
</div> | |
<div class="upgrade-stat"> | |
<span class="upgrade-stat-label">Next Level:</span> | |
<span class="upgrade-stat-value">+{rarity_level * 20}% boost</span> | |
</div> | |
<div class="upgrade-cost" style="color: #9D4EDD;">Cost: {rarity_cost} {TAG_CURRENCY_NAME}</div> | |
</div> | |
""", unsafe_allow_html=True) | |
# Upgrade button | |
can_afford = st.session_state.tag_currency >= rarity_cost | |
button_class = "pulse-button" if can_afford else "" | |
if st.button(f"Upgrade Rarity", key=f"upgrade_rarity", disabled=not can_afford, use_container_width=True): | |
if purchase_library_upgrade("rarity"): | |
st.success(f"Rapacity upgraded to level {rarity_level + 1}!") | |
st.rerun() | |
else: | |
st.error(f"Not enough {TAG_CURRENCY_NAME}. Need {rarity_cost}.") | |
# Add a styled info box about library growth with dark theme | |
st.markdown(""" | |
<div style="background-color: rgba(13, 110, 253, 0.15); | |
border-left: 4px solid #0d6efd; | |
border-radius: 4px; | |
padding: 15px; | |
margin-top: 20px; | |
color: #ffffff;"> | |
<h4 style="margin-top: 0; color: #6495ED;">📚 Library Growth</h4> | |
<p style="margin-bottom: 0;"> | |
Your library will grow as you collect more tags. Each floor of the library unlocks new rarities and | |
improves your chances of finding rare tags. Continue collecting tags to unlock deeper levels of the library! | |
</p> | |
</div> | |
""", unsafe_allow_html=True) | |
def initialize_library_system(): | |
"""Initialize the library system state in session state if not already present.""" | |
if 'library_system_initialized' not in st.session_state: | |
st.session_state.library_system_initialized = True | |
# Try to load from storage first | |
library_state = tag_storage.load_library_state(st.session_state) | |
if library_state: | |
# We already have the state loaded into session_state by the load function | |
print("Library system loaded from storage.") | |
else: | |
# Initialize with defaults | |
st.session_state.discovered_tags = {} # {tag_name: {"rarity": str, "discovery_time": timestamp, "category": str}} | |
st.session_state.library_exploration_history = [] # List of recent library explorations | |
# Initialize enkephalin if not present | |
if 'enkephalin' not in st.session_state: | |
st.session_state.enkephalin = 0 | |
# For the library interface | |
st.session_state.expedition_results = [] # Results from completed expeditions | |
# Library growth system | |
st.session_state.library_growth = { | |
"total_discoveries": 0, | |
"last_discovery_time": time.time() | |
} | |
# Upgrade system for library | |
st.session_state.library_upgrades = { | |
"speed": 1, # Expedition speed (reduces cooldown time) | |
"capacity": 1, # Tags discovered per expedition | |
"rarity": 1 # Rare tag chance | |
} | |
# Set of explored library tiers | |
st.session_state.explored_library_tiers = set() | |
print("Library system initialized with defaults.") | |
# Store library floors in session state if not already there | |
if 'library_floors' not in st.session_state: | |
st.session_state.library_floors = LIBRARY_FLOORS | |
# Update categories for any "unknown" category tags | |
update_discovered_tag_categories() | |
# Add CSS animations for styling | |
st.markdown(""" | |
<style> | |
/* Star of the City animation */ | |
@keyframes star-glow { | |
0% { text-shadow: 0 0 5px #FFD700; } | |
50% { text-shadow: 0 0 15px #FFD700; } | |
100% { text-shadow: 0 0 5px #FFD700; } | |
} | |
/* Impuritas Civitas animation */ | |
@keyframes rainbow-text { | |
0% { color: red; } | |
14% { color: orange; } | |
28% { color: yellow; } | |
42% { color: green; } | |
57% { color: blue; } | |
71% { color: indigo; } | |
85% { color: violet; } | |
100% { color: red; } | |
} | |
@keyframes rainbow-border { | |
0% { border-color: red; } | |
14% { border-color: orange; } | |
28% { border-color: yellow; } | |
42% { border-color: green; } | |
57% { border-color: blue; } | |
71% { border-color: indigo; } | |
85% { border-color: violet; } | |
100% { border-color: red; } | |
} | |
/* Urban Nightmare animation */ | |
@keyframes nightmare-pulse { | |
0% { border-color: #FF9800; } | |
50% { border-color: #FF5722; } | |
100% { border-color: #FF9800; } | |
} | |
/* Urban Plague subtle effect */ | |
.glow-purple { | |
text-shadow: 0 0 3px #9C27B0; | |
} | |
/* Apply the animations to specific rarity classes */ | |
.star-of-city { | |
animation: star-glow 2s infinite; | |
font-weight: bold; | |
} | |
.impuritas-civitas { | |
animation: rainbow-text 4s linear infinite; | |
font-weight: bold; | |
} | |
.urban-nightmare { | |
animation: nightmare-pulse 3s infinite; | |
font-weight: bold; | |
} | |
.urban-plague { | |
text-shadow: 0 0 3px #9C27B0; | |
font-weight: bold; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
def display_library_extraction(): | |
"""Display the library exploration interface.""" | |
initialize_library_system() | |
st.title(f"Welcome to {LIBRARY_INFO['name']}") | |
st.markdown(f""" | |
<div style="background-color: rgba(74, 20, 140, 0.15); | |
border-radius: 10px; | |
padding: 15px; | |
margin-bottom: 20px; | |
border-left: 5px solid {LIBRARY_INFO['color']}; | |
color: #ffffff;"> | |
<p style="margin: 0;">{LIBRARY_INFO['description']}</p> | |
</div> | |
""", unsafe_allow_html=True) | |
# Create tabs with enhanced styling for dark theme | |
st.markdown(""" | |
<style> | |
/* Custom styling for tabs */ | |
.stTabs [data-baseweb="tab-list"] { | |
gap: 2px; | |
} | |
.stTabs [data-baseweb="tab"] { | |
border-radius: 5px 5px 0 0; | |
padding: 10px 16px; | |
font-weight: 600; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Store current tab index in session state if not present | |
if 'library_tab_index' not in st.session_state: | |
st.session_state.library_tab_index = 0 | |
# Create expanded tabs - removed the essence tab | |
explore_tab, discovered_tab, building_tab = st.tabs([ | |
"📚 Library Exploration", | |
"🔍 Discovered Tags", | |
"🏛️ Library Building" | |
]) | |
with explore_tab: | |
st.session_state.library_tab_index = 0 | |
display_library_exploration_interface() | |
with discovered_tab: | |
st.session_state.library_tab_index = 1 | |
st.subheader("Your Discovered Tags") | |
st.write("These are tags you've discovered through the library system. They differ from your collected tags, which are obtained from scanning images.") | |
# Display discovered tags using our new function | |
display_discovered_tags() | |
with building_tab: | |
st.session_state.library_tab_index = 2 | |
display_library_building() | |
def display_discovered_tags(): | |
"""Display the user's discovered tags with the same visual style as the tag collection""" | |
# Show total unique discovered tags | |
if not hasattr(st.session_state, 'discovered_tags') or not st.session_state.discovered_tags: | |
st.info("Explore the library to discover new tags!") | |
return | |
unique_tags = len(st.session_state.discovered_tags) | |
st.write(f"You have discovered {unique_tags} unique tags.") | |
# Count tags by rarity | |
rarity_counts = {} | |
for tag_info in st.session_state.discovered_tags.values(): | |
rarity = tag_info.get("rarity", "Unknown") | |
if rarity not in rarity_counts: | |
rarity_counts[rarity] = 0 | |
rarity_counts[rarity] += 1 | |
# Only display rarity categories that have tags | |
active_rarities = {r: c for r, c in rarity_counts.items() if c > 0} | |
# If there are active rarities to display | |
if active_rarities: | |
display_discovered_rarity_distribution(active_rarities) | |
# Add a sorting option | |
sort_options = ["Category (rarest first)", "Rarity", "Discovery Time"] | |
selected_sort = st.selectbox("Sort tags by:", sort_options, key="discovered_tags_sort") | |
# Group tags by the selected method | |
if selected_sort == "Category (rarest first)": | |
# Group tags by category | |
categories = {} | |
for tag, info in st.session_state.discovered_tags.items(): | |
category = info.get("category", "unknown") | |
if category not in categories: | |
categories[category] = [] | |
categories[category].append((tag, info)) | |
# Display tags by category in expanders | |
for category, tags in sorted(categories.items()): | |
# Get rarity order for sorting | |
rarity_order = list(RARITY_LEVELS.keys()) | |
# Sort tags by rarity (rarest first) | |
def get_rarity_index(tag_tuple): | |
tag, info = tag_tuple | |
rarity = info.get("rarity", "Unknown") | |
if rarity in rarity_order: | |
return len(rarity_order) - rarity_order.index(rarity) | |
return 0 | |
sorted_tags = sorted(tags, key=get_rarity_index, reverse=True) | |
# Check if category has any rare tags | |
has_rare_tags = any(info.get("rarity") in ["Impuritas Civitas", "Star of the City"] | |
for _, info in sorted_tags) | |
# Get category info if available | |
category_display = category.capitalize() | |
if category in TAG_CATEGORIES: | |
category_info = TAG_CATEGORIES[category] | |
category_icon = category_info.get("icon", "") | |
category_color = category_info.get("color", "#888888") | |
category_display = f"<span style='color:{category_color};'>{category_icon} {category.capitalize()}</span>" | |
# Create header with information about rare tags if present | |
header = f"{category_display} ({len(tags)} tags)" | |
if has_rare_tags: | |
header += " ✨ Contains rare tags!" | |
# Display the header and expander | |
st.markdown(header, unsafe_allow_html=True) | |
with st.expander("Show/Hide", expanded=has_rare_tags): | |
# Group by rarity within category | |
rarity_groups = {} | |
for tag, info in sorted_tags: | |
rarity = info.get("rarity", "Unknown") | |
if rarity not in rarity_groups: | |
rarity_groups[rarity] = [] | |
rarity_groups[rarity].append((tag, info)) | |
# Display each rarity group in order (rarest first) | |
for rarity in reversed(rarity_order): | |
if rarity in rarity_groups: | |
tags_in_rarity = rarity_groups[rarity] | |
if tags_in_rarity: | |
color = RARITY_LEVELS[rarity]["color"] | |
# Special styling for rare rarities | |
if rarity == "Impuritas Civitas": | |
rarity_style = f"animation:rainbow-text 4s linear infinite;font-weight:bold;" | |
elif rarity == "Star of the City": | |
rarity_style = f"color:{color};text-shadow:0 0 3px gold;font-weight:bold;" | |
elif rarity == "Urban Nightmare": | |
rarity_style = f"color:{color};text-shadow:0 0 1px #FF5722;font-weight:bold;" | |
else: | |
rarity_style = f"color:{color};font-weight:bold;" | |
st.markdown(f"<span style='{rarity_style}'>{rarity.capitalize()}</span> ({len(tags_in_rarity)} tags)", unsafe_allow_html=True) | |
display_discovered_tag_grid(tags_in_rarity) | |
st.markdown("---") | |
elif selected_sort == "Rarity": | |
# Group tags by rarity level | |
rarity_groups = {} | |
for tag, info in st.session_state.discovered_tags.items(): | |
rarity = info.get("rarity", "Unknown") | |
if rarity not in rarity_groups: | |
rarity_groups[rarity] = [] | |
rarity_groups[rarity].append((tag, info)) | |
# Get ordered rarities (rarest first) | |
ordered_rarities = list(RARITY_LEVELS.keys()) | |
ordered_rarities.reverse() # Reverse to show rarest first | |
# Display tags by rarity | |
for rarity in ordered_rarities: | |
if rarity in rarity_groups: | |
tags = rarity_groups[rarity] | |
color = RARITY_LEVELS[rarity]["color"] | |
# Add special styling for rare rarities | |
rarity_html = f"<span style='color:{color};font-weight:bold;'>{rarity.capitalize()}</span>" | |
if rarity == "Impuritas Civitas": | |
rarity_html = f"<span style='animation:rainbow-text 4s linear infinite;font-weight:bold;'>{rarity.capitalize()}</span>" | |
elif rarity == "Star of the City": | |
rarity_html = f"<span style='color:{color};text-shadow:0 0 3px gold;font-weight:bold;'>{rarity.capitalize()}</span>" | |
elif rarity == "Urban Nightmare": | |
rarity_html = f"<span style='color:{color};text-shadow:0 0 1px #FF5722;font-weight:bold;'>{rarity.capitalize()}</span>" | |
# First create the title with HTML, then use it in the expander | |
st.markdown(f"### {rarity_html} ({len(tags)} tags)", unsafe_allow_html=True) | |
with st.expander("Show/Hide", expanded=rarity in ["Impuritas Civitas", "Star of the City"]): | |
# Group by category within rarity | |
category_groups = {} | |
for tag, info in tags: | |
category = info.get("category", "unknown") | |
if category not in category_groups: | |
category_groups[category] = [] | |
category_groups[category].append((tag, info)) | |
# Display each category within this rarity level | |
for category, category_tags in sorted(category_groups.items()): | |
# Get category info if available | |
category_display = category.capitalize() | |
if category in TAG_CATEGORIES: | |
category_info = TAG_CATEGORIES[category] | |
category_icon = category_info.get("icon", "") | |
category_color = category_info.get("color", "#888888") | |
category_display = f"<span style='color:{category_color};'>{category_icon} {category.capitalize()}</span>" | |
st.markdown(f"#### {category_display} ({len(category_tags)} tags)", unsafe_allow_html=True) | |
display_discovered_tag_grid(category_tags) | |
st.markdown("---") | |
elif selected_sort == "Discovery Time": | |
# Sort all tags by discovery time (newest first) | |
sorted_tags = [] | |
for tag, info in st.session_state.discovered_tags.items(): | |
discovery_time = info.get("discovery_time", "") | |
sorted_tags.append((tag, info, discovery_time)) | |
sorted_tags.sort(key=lambda x: x[2], reverse=True) # Sort by time, newest first | |
# Group by date | |
date_groups = {} | |
for tag, info, time_str in sorted_tags: | |
# Extract just the date part if timestamp has date and time | |
date = time_str.split()[0] if " " in time_str else time_str | |
if date not in date_groups: | |
date_groups[date] = [] | |
date_groups[date].append((tag, info)) | |
# Display tags grouped by discovery date | |
for date, tags in date_groups.items(): | |
date_display = date if date else "Unknown date" | |
st.markdown(f"### Discovered on {date_display} ({len(tags)} tags)") | |
with st.expander("Show/Hide", expanded=date == list(date_groups.keys())[0]): # Expand most recent by default | |
display_discovered_tag_grid(tags) | |
st.markdown("---") | |
def display_discovered_rarity_distribution(active_rarities): | |
"""Display distribution of discovered tags by rarity with themed animations""" | |
# Add the necessary CSS for animations | |
st.markdown(""" | |
<style> | |
@keyframes grid-glow { | |
0% { text-shadow: 0 0 2px gold; } | |
50% { text-shadow: 0 0 6px gold; } | |
100% { text-shadow: 0 0 2px gold; } | |
} | |
@keyframes grid-rainbow { | |
0% { color: red; } | |
14% { color: orange; } | |
28% { color: yellow; } | |
42% { color: green; } | |
57% { color: blue; } | |
71% { color: indigo; } | |
85% { color: violet; } | |
100% { color: red; } | |
} | |
@keyframes grid-pulse { | |
0% { opacity: 0.8; } | |
50% { opacity: 1; } | |
100% { opacity: 0.8; } | |
} | |
.grid-star { | |
text-shadow: 0 0 3px gold; | |
animation: grid-glow 2s infinite; | |
} | |
.grid-impuritas { | |
animation: grid-rainbow 4s linear infinite; | |
} | |
.grid-nightmare { | |
text-shadow: 0 0 1px #FF5722; | |
animation: grid-pulse 3s infinite; | |
} | |
.grid-plague { | |
text-shadow: 0 0 1px #9C27B0; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
rarity_cols = st.columns(len(active_rarities)) | |
for i, (rarity, count) in enumerate(active_rarities.items()): | |
with rarity_cols[i]: | |
# Get color with fallback | |
color = RARITY_LEVELS.get(rarity, {}).get("color", "#888888") | |
# Apply special styling based on rarity | |
style = f"color:{color};font-weight:bold;" | |
class_name = "" | |
if rarity == "Impuritas Civitas": | |
class_name = "grid-impuritas" | |
elif rarity == "Star of the City": | |
class_name = "grid-star" | |
elif rarity == "Urban Nightmare": | |
class_name = "grid-nightmare" | |
elif rarity == "Urban Plague": | |
class_name = "grid-plague" | |
if class_name: | |
st.markdown( | |
f"<div style='text-align:center;'><span class='{class_name}' style='font-weight:bold;'>{rarity.capitalize()}</span><br>{count}</div>", | |
unsafe_allow_html=True | |
) | |
else: | |
st.markdown( | |
f"<div style='text-align:center;'><span style='{style}'>{rarity.capitalize()}</span><br>{count}</div>", | |
unsafe_allow_html=True | |
) | |
def display_discovered_tag_grid(tags): | |
"""Display discovered tags in a grid layout with discovery information""" | |
# Create a grid layout for tags | |
cols = st.columns(3) | |
for i, (tag, info) in enumerate(sorted(tags)): | |
col_idx = i % 3 | |
with cols[col_idx]: | |
rarity = info.get("rarity", "Unknown") | |
discovery_time = info.get("discovery_time", "") | |
library_floor = info.get("library_floor", "") | |
discovery_count = info.get("discovery_count", 1) | |
color = RARITY_LEVELS.get(rarity, {}).get("color", "#888888") | |
# Get sample count if available | |
sample_count = None | |
if hasattr(st.session_state, 'tag_rarity_metadata') and st.session_state.tag_rarity_metadata: | |
if tag in st.session_state.tag_rarity_metadata: | |
tag_info = st.session_state.tag_rarity_metadata[tag] | |
if isinstance(tag_info, dict) and "sample_count" in tag_info: | |
sample_count = tag_info["sample_count"] | |
# Format sample count | |
sample_display = "" | |
if sample_count is not None: | |
if sample_count >= 1000000: | |
sample_display = f"<span style='font-size:0.8em;color:#666;'>({sample_count/1000000:.1f}M)</span>" | |
elif sample_count >= 1000: | |
sample_display = f"<span style='font-size:0.8em;color:#666;'>({sample_count/1000:.1f}K)</span>" | |
else: | |
sample_display = f"<span style='font-size:0.8em;color:#666;'>({sample_count})</span>" | |
# Apply special styling for rare tags | |
tag_html = tag | |
if rarity == "Impuritas Civitas": | |
tag_html = f"<span style='animation: rainbow-text 4s linear infinite;'>{tag}</span>" | |
elif rarity == "Star of the City": | |
tag_html = f"<span style='text-shadow: 0 0 3px gold;'>{tag}</span>" | |
elif rarity == "Urban Nightmare": | |
tag_html = f"<span style='text-shadow: 0 0 1px #FF9800;'>{tag}</span>" | |
# Display tag with rarity badge and discovery info | |
st.markdown( | |
f"{tag_html} <span style='background-color:{color};color:white;padding:2px 6px;border-radius:10px;font-size:0.8em;'>{rarity.capitalize()}</span> {sample_display}", | |
unsafe_allow_html=True | |
) | |
# Show discovery details | |
if library_floor: | |
st.markdown(f"<span style='font-size:0.85em;'>Found in: {library_floor}</span>", unsafe_allow_html=True) | |
if discovery_count > 1: | |
st.markdown(f"<span style='font-size:0.85em;'>Seen {discovery_count} times</span>", unsafe_allow_html=True) |