""" 아이템 생성, 관리, 사용 관련 기능을 제공하는 모듈 """ import re import json import streamlit as st from config.constants import ITEM_TYPES, ITEM_RARITY from modules.ai_service import generate_gemini_text class Item: """게임 내 아이템 기본 클래스""" def __init__(self, name, description, type="일반", consumable=False, durability=None, max_durability=None, quantity=1, rarity="일반"): self.name = name # 아이템 이름 self.description = description # 아이템 설명 self.type = type # 아이템 유형 (무기, 방어구, 소비품, 도구, 일반) self.consumable = consumable # 소비성 여부 (사용 후 사라짐) self.durability = durability # 현재 내구도 (None이면 내구도 없음) self.max_durability = max_durability or durability # 최대 내구도 self.quantity = quantity # 수량 self.rarity = rarity # 희귀도 (일반, 희귀, 영웅, 전설) def to_dict(self): """아이템을 사전 형태로 변환""" return { 'name': self.name, 'description': self.description, 'type': self.type, 'consumable': self.consumable, 'durability': self.durability, 'max_durability': self.max_durability, 'quantity': self.quantity, 'rarity': self.rarity } @classmethod def from_dict(cls, data): """사전 형태에서 아이템 객체 생성""" return cls( name=data['name'], description=data.get('description', ''), type=data.get('type', '일반'), consumable=data.get('consumable', False), durability=data.get('durability', None), max_durability=data.get('max_durability', None), quantity=data.get('quantity', 1), rarity=data.get('rarity', '일반') ) def use(self): """아이템 사용""" if self.consumable: if self.quantity > 1: self.quantity -= 1 return f"{self.name}을(를) 사용했습니다. 남은 수량: {self.quantity}" else: return f"{self.name}을(를) 사용했습니다. 모두 소진되었습니다." elif self.durability is not None: self.durability -= 1 if self.durability <= 0: return f"{self.name}의 내구도가 다 되어 사용할 수 없게 되었습니다." else: return f"{self.name}을(를) 사용했습니다. 남은 내구도: {self.durability}/{self.max_durability}" else: return f"{self.name}을(를) 사용했습니다." def get_icon(self): """아이템 유형에 따른 아이콘 반환""" return ITEM_TYPES.get(self.type, "📦") def get_rarity_color(self): """아이템 희귀도에 따른 색상 코드 반환""" return ITEM_RARITY.get(self.rarity, "#AAAAAA") def get_durability_percentage(self): """내구도 백분율 계산""" if self.durability is None or self.max_durability is None or self.max_durability <= 0: return 100 return (self.durability / self.max_durability) * 100 def initialize_inventory(theme): """ 테마별 기본 인벤토리 초기화 Args: theme (str): 게임 테마 Returns: list: 기본 인벤토리 아이템 목록 """ inventory = [] if theme == 'fantasy': inventory = [ Item("기본 의류", "일반적인 모험가 복장입니다.", type="방어구", consumable=False), Item("여행용 가방", "다양한 물건을 담을 수 있는 가방입니다.", type="도구", consumable=False), Item("횃불", "어두운 곳을 밝힐 수 있습니다. 약 1시간 정도 사용 가능합니다.", type="소비품", consumable=True, quantity=3), Item("단검", "기본적인 근접 무기입니다.", type="무기", consumable=False, durability=20, max_durability=20), Item("물통", "물을 담아 갈 수 있습니다.", type="도구", consumable=False), Item("식량", "하루치 식량입니다.", type="소비품", consumable=True, quantity=5), Item("치유 물약", "체력을 회복시켜주는 물약입니다.", type="소비품", consumable=True, quantity=2, rarity="고급") ] elif theme == 'sci-fi': inventory = [ Item("기본 의류", "표준 우주 여행자 복장입니다.", type="방어구", consumable=False), Item("휴대용 컴퓨터", "간단한 정보 검색과 해킹에 사용할 수 있습니다.", type="도구", consumable=False, durability=30, max_durability=30), Item("에너지 셀", "장비 작동에 필요한 에너지 셀입니다.", type="소비품", consumable=True, quantity=3), Item("레이저 포인터", "기본적인 레이저 도구입니다.", type="도구", consumable=False, durability=15, max_durability=15), Item("통신 장치", "다른 사람과 통신할 수 있습니다.", type="도구", consumable=False, durability=25, max_durability=25), Item("비상 식량", "우주 여행용 압축 식량입니다.", type="소비품", consumable=True, quantity=5), Item("의료 키트", "부상을 치료할 수 있는 기본 의료 키트입니다.", type="소비품", consumable=True, quantity=2, rarity="고급") ] else: # dystopia inventory = [ Item("작업용 의류", "튼튼하고 방호력이 있는 작업복입니다.", type="방어구", consumable=False, durability=15, max_durability=15), Item("가스 마스크", "유해 가스를 걸러냅니다.", type="방어구", consumable=False, durability=20, max_durability=20), Item("필터", "가스 마스크에 사용하는 필터입니다.", type="소비품", consumable=True, quantity=3), Item("생존 나이프", "다용도 생존 도구입니다.", type="무기", consumable=False, durability=25, max_durability=25), Item("정수 알약", "오염된 물을 정화할 수 있습니다.", type="소비품", consumable=True, quantity=5), Item("식량 배급 카드", "배급소에서 식량을 받을 수 있는 카드입니다.", type="도구", consumable=False), Item("응급 주사기", "위급 상황에서 생명 유지에 도움이 됩니다.", type="소비품", consumable=True, quantity=1, rarity="희귀") ] return inventory def display_inventory(inventory): """ 인벤토리 아이템을 시각적으로 표시하는 함수 Args: inventory (list): 표시할 인벤토리 아이템 목록 """ # 인벤토리가 비어있는 경우 처리 if not inventory: st.write("인벤토리가 비어있습니다.") return # 아이템 유형별 분류 categorized_items = { "무기": [], "방어구": [], "소비품": [], "도구": [], "마법": [], "기술": [], "일반": [] } # 아이템을 카테고리별로 분류 for item in inventory: try: item_type = item.type if hasattr(item, 'type') else "일반" if item_type in categorized_items: categorized_items[item_type].append(item) else: categorized_items["일반"].append(item) except: # 문자열이나 다른 형태의 아이템은 일반으로 분류 categorized_items["일반"].append(item) # 카테고리별로 아이템 표시 for category, items in categorized_items.items(): if items: # 해당 카테고리에 아이템이 있는 경우에만 표시 # 카테고리 아이콘 선택 category_icon = ITEM_TYPES.get(category, "📦") st.write(f"{category_icon} **{category}**") # 카테고리 내 아이템 표시 - 간소화된 버전 for item in items: try: # 아이템 정보 안전하게 추출 if hasattr(item, 'name'): item_name = item.name item_desc = getattr(item, 'description', '설명 없음') item_quantity = getattr(item, 'quantity', 1) # 아이콘 가져오기 icon = getattr(item, 'get_icon', lambda: "📦") if callable(icon): icon = icon() # 수량 표시 quantity_text = f" x{item_quantity}" if item_quantity > 1 else "" # 단순화된 표시 방식 st.markdown(f"{icon} **{item_name}**{quantity_text} - {item_desc}") else: # 문자열 아이템 st.markdown(f"📦 {str(item)}") except Exception as e: st.markdown(f"📦 {str(item)} (표시 오류: {str(e)})") def extract_items_from_story(story_text): """ 스토리 텍스트에서 획득한 아이템을 자동 추출 Args: story_text (str): 스토리 텍스트 Returns: list: 추출된 아이템 목록 """ # 굵게 표시된 텍스트를 우선 추출 (** 사이의 내용) bold_items = re.findall(r'\*\*(.*?)\*\*', story_text) prompt = f""" 다음 TRPG 스토리 텍스트를 분석하여 플레이어가 획득했거나 발견한 모든 아이템을 추출해주세요. 일반적인 배경 요소가 아닌, 플레이어가 실제로 소지하거나 사용할 수 있는 아이템만 추출하세요. 특히 굵게 표시된 아이템(**, ** 사이의 텍스트)에 주목하세요. 스토리 텍스트: {story_text} 다음 JSON 형식으로 반환해주세요: [ {{ "name": "아이템 이름", "description": "아이템 설명 (없으면 빈 문자열)", "consumable": true/false (소비성 여부, 기본값 false), "durability": 숫자 (내구도, 없으면 null), "quantity": 숫자 (수량, 기본값 1), "type": "아이템 유형" }}, ... ] 아이템이 없으면 빈 배열 []을 반환하세요. """ try: response = generate_gemini_text(prompt, 300) # 응답에서 JSON 구조 추출 시도 try: # 응답 텍스트에서 JSON 부분만 추출 시도 json_match = re.search(r'\[\s*\{.*\}\s*\]', response, re.DOTALL) if json_match: items_data = json.loads(json_match.group(0)) else: # 전체 응답을 JSON으로 파싱 시도 items_data = json.loads(response) except: # JSON 파싱 실패 시 기본 아이템 생성 items_data = [] for item_name in bold_items: items_data.append({ "name": item_name, "description": "발견한 아이템입니다.", "consumable": False, "durability": None, "quantity": 1, "type": "일반" }) # Item 객체 목록 생성 items = [] for item_data in items_data: items.append(Item.from_dict(item_data)) # 굵게 표시된 아이템이 있지만 JSON에 포함되지 않은 경우 추가 existing_names = [item.name for item in items] for bold_item in bold_items: if bold_item not in existing_names: items.append(Item( name=bold_item, description="발견한 아이템입니다.", consumable=False, quantity=1 )) return items except Exception as e: st.error(f"아이템 추출 오류: {e}") # 오류 시 기본 아이템 생성 items = [] for item_name in bold_items: items.append(Item( name=item_name, description="발견한 아이템입니다.", consumable=False, quantity=1 )) return items def extract_used_items_from_story(story_text, inventory): """ 스토리 텍스트에서 사용한 아이템 추출 Args: story_text (str): 스토리 텍스트 inventory (list): 현재 인벤토리 Returns: list: 사용된 아이템 데이터 """ # 인벤토리 아이템 이름 목록 생성 inventory_names = [item.name if hasattr(item, 'name') else str(item) for item in inventory] # 굵게 표시된 텍스트를 우선 추출 (** 사이의 내용) bold_items = re.findall(r'\*\*(.*?)\*\*', story_text) prompt = f""" 다음 TRPG 스토리 텍스트를 분석하여 플레이어가 사용한 아이템을 추출해주세요. 특히 굵게 표시된 아이템(**, ** 사이의 텍스트)에 주목하세요. 인벤토리에 있는 아이템: {', '.join(inventory_names)} 스토리 텍스트: {story_text} 다음 JSON 형식으로 반환해주세요: [ {{ "name": "아이템 이름", "quantity": 사용한 수량 (기본값 1) }}, ... ] 아무 아이템도 사용하지 않았다면 빈 배열 []을 반환하세요. """ try: response = generate_gemini_text(prompt, 200) # 응답에서 JSON 구조 추출 시도 try: # 응답 텍스트에서 JSON 부분만 추출 시도 json_match = re.search(r'\[\s*\{.*\}\s*\]', response, re.DOTALL) if json_match: used_items_data = json.loads(json_match.group(0)) else: # 전체 응답을 JSON으로 파싱 시도 used_items_data = json.loads(response) except: # JSON 파싱 실패 시 기본 데이터 생성 used_items_data = [] for item_name in bold_items: if item_name in inventory_names: used_items_data.append({ "name": item_name, "quantity": 1 }) # 사용된 아이템 데이터 필터링 (인벤토리에 있는 아이템만) filtered_items_data = [] for item_data in used_items_data: if item_data["name"] in inventory_names: filtered_items_data.append(item_data) # 굵게 표시된 아이템이 있지만 JSON에 포함되지 않은 경우 추가 existing_names = [item["name"] for item in filtered_items_data] for bold_item in bold_items: if bold_item in inventory_names and bold_item not in existing_names: filtered_items_data.append({ "name": bold_item, "quantity": 1 }) return filtered_items_data except Exception as e: st.error(f"사용된 아이템 추출 오류: {e}") # 오류 시 기본 데이터 생성 used_items_data = [] for item_name in bold_items: if item_name in inventory_names: used_items_data.append({ "name": item_name, "quantity": 1 }) return used_items_data def update_inventory(action, item_data, inventory): """ 인벤토리 아이템 추가/제거/사용 Args: action (str): 'add', 'use', 'remove' 중 하나 item_data (Item, dict, str): 추가/사용/제거할 아이템 정보 inventory (list): 현재 인벤토리 Returns: str: 작업 결과 메시지 """ if action == "add": # 새 아이템인 경우 if isinstance(item_data, Item): item = item_data else: # 딕셔너리 형태로 전달된 경우 if isinstance(item_data, dict): item = Item.from_dict(item_data) else: # 문자열인 경우 item = Item(name=str(item_data), description="획득한 아이템입니다.") # 기존 아이템인지 확인 for existing_item in inventory: if hasattr(existing_item, 'name') and existing_item.name == item.name: # 유형이 같은지 확인 (다른 유형이면 별도 아이템으로 처리) existing_type = getattr(existing_item, 'type', '일반') new_type = getattr(item, 'type', '일반') if existing_type == new_type: # 수량 증가 existing_item.quantity += item.quantity return f"**{item.name}** {item.quantity}개가 추가되었습니다. (총 {existing_item.quantity}개)" # 새 아이템 추가 inventory.append(item) quantity_text = f" {item.quantity}개" if item.quantity > 1 else "" return f"새 아이템 **{item.name}**{quantity_text}을(를) 획득했습니다!" elif action == "use": # 아이템 사용 (소비성 아이템 소모 또는 내구도 감소) if isinstance(item_data, dict): item_name = item_data.get("name", "") quantity = item_data.get("quantity", 1) else: item_name = str(item_data) quantity = 1 for i, item in enumerate(inventory): item_n = item.name if hasattr(item, 'name') else str(item) if item_n == item_name: # 소비성 아이템인지 확인 if hasattr(item, 'consumable') and item.consumable: # 소비성 아이템 수량 감소 if item.quantity <= quantity: # 모두 소모 removed_item = inventory.pop(i) return f"**{removed_item.name}**을(를) 모두 사용했습니다." else: # 일부 소모 item.quantity -= quantity return f"**{item.name}** {quantity}개를 사용했습니다. (남은 수량: {item.quantity})" # 내구도 있는 아이템인지 확인 elif hasattr(item, 'durability') and item.durability is not None: # 내구도 감소 item.durability -= 1 if item.durability <= 0: # 내구도 소진으로 파괴 removed_item = inventory.pop(i) return f"**{removed_item.name}**의 내구도가 다 되어 사용할 수 없게 되었습니다." else: # 내구도 감소 max_durability = getattr(item, 'max_durability', item.durability) return f"**{item.name}**의 내구도가 감소했습니다. (남은 내구도: {item.durability}/{max_durability})" else: # 일반 아이템 사용 (변화 없음) return f"**{item.name}**을(를) 사용했습니다." return f"**{item_name}**이(가) 인벤토리에 없습니다." elif action == "remove": # 아이템 제거 if isinstance(item_data, dict): item_name = item_data.get("name", "") else: item_name = str(item_data) for i, item in enumerate(inventory): item_n = item.name if hasattr(item, 'name') else str(item) if item_n == item_name: removed_item = inventory.pop(i) item_name = removed_item.name if hasattr(removed_item, 'name') else str(removed_item) return f"**{item_name}**을(를) 인벤토리에서 제거했습니다." return f"**{item_name}**이(가) 인벤토리에 없습니다." return "아이템 작업에 실패했습니다." def display_item_notification(notification): """ 아이템 관련 알림 표시 - 더 눈에 띄게 개선 Args: notification (str): 표시할 알림 텍스트 """ if notification: # 아이템 이름 강조를 위한 정규식 처리 import re # 아이템 이름을 추출하여 강조 처리 highlighted_notification = notification item_names = re.findall(r'아이템: (.*?)(,|$|\))', notification) for item_name in item_names: # 아이템 이름에 강조 스타일 적용 (더 눈에 띄게 수정) highlighted_notification = highlighted_notification.replace( item_name[0], f'{item_name[0]}' ) # 획득/사용 키워드에 더 눈에 띄는 스타일 적용 highlighted_notification = highlighted_notification.replace( "획득한 아이템", '🆕 획득한 아이템' ).replace( "사용한 아이템", '⚙️ 사용한 아이템' ) st.markdown(f"""
🎁
{highlighted_notification}
""", unsafe_allow_html=True)