# character_creation.py 시작 부분 (임포트 부분)
import streamlit as st
import random
import time
from typing import Dict, List, Any, Tuple, Optional
from ..config.constants import ABILITY_NAMES
from ..utils.dice_roller import roll_dice, display_dice_animation
from ..modules.ai_service import generate_gemini_text
from ..modules.character_utils import extract_background_tags, get_stat_info
def initialize_character_creation_state():
"""캐릭터 생성 관련 상태 초기화"""
if 'character_creation_step' not in st.session_state:
st.session_state.character_creation_step = 'race'
if 'background_options_generated' not in st.session_state:
st.session_state.background_options_generated = False
if 'dice_rolled' not in st.session_state:
st.session_state.dice_rolled = False
if 'reroll_used' not in st.session_state:
st.session_state.reroll_used = False
def display_character_creation_page():
"""캐릭터 생성 페이지 전체 표시"""
st.header("2️⃣ 캐릭터 생성")
# 마스터 메시지 표시
st.markdown(f"
{st.session_state.master_message}
", unsafe_allow_html=True)
# 상태 초기화
initialize_character_creation_state()
# 현재 단계에 따라 다른 UI 표시
if st.session_state.character_creation_step == 'race':
display_race_selection()
elif st.session_state.character_creation_step == 'profession':
display_profession_selection()
elif st.session_state.character_creation_step == 'background':
display_background_selection()
elif st.session_state.character_creation_step == 'abilities':
display_abilities_selection()
elif st.session_state.character_creation_step == 'review':
display_character_review()
def display_background_selection():
"""배경 선택 UI"""
st.subheader("배경 이야기 선택")
# 배경 선택 설명 추가
st.markdown("""
캐릭터의 배경 이야기는 당신이 어떻게 모험가가 되었는지, 어떤 경험을 했는지를 설명합니다.
배경은 캐릭터의 동기와 성격을 형성하며, 게임 내에서의 역할 플레이에 큰 도움이 됩니다.
""", unsafe_allow_html=True)
# 선택된 종족 및 직업 정보 표시 (개선된 UI)
race_icon = st.session_state.get('race_icon', '👤')
profession_icon = st.session_state.get('profession_icon', '👤')
race_ability = st.session_state.get('race_ability', "특수 능력 없음")
profession_skill = st.session_state.get('profession_skill', "특수 기술 없음")
st.markdown(f"""
{race_icon}
{st.session_state.selected_race} {st.session_state.selected_profession}
{st.session_state.selected_race}: {race_ability}
{st.session_state.selected_profession}: {profession_skill}
{profession_icon}
""", unsafe_allow_html=True)
# 배경 옵션 생성
if not st.session_state.background_options_generated:
with st.spinner("캐릭터 배경 옵션을 생성 중..."):
from src.modules.character_utils import generate_character_options
st.session_state.character_backgrounds = generate_character_options(
st.session_state.selected_profession,
st.session_state.theme
)
st.session_state.background_options_generated = True
# 생성된 배경 옵션 표시
if 'character_backgrounds' in st.session_state and st.session_state.character_backgrounds:
# 옵션 숫자 및 탭 생성
tabs = st.tabs([f"옵션 {i+1}" for i in range(len(st.session_state.character_backgrounds))])
for i, (tab, background) in enumerate(zip(tabs, st.session_state.character_backgrounds)):
with tab:
# 각 배경 스토리 표시
st.markdown(f"""
{background}
""", unsafe_allow_html=True)
# 배경 선택 버튼
if st.button(f"이 배경으로 선택", key=f"bg_select_{i}", use_container_width=True):
# 선택한 배경 저장
st.session_state.selected_background = background
# 배경에서 태그 추출
from src.modules.character_utils import extract_background_tags
st.session_state.background_tags = extract_background_tags(background)
# 다음 단계로 진행
st.session_state.character_creation_step = 'abilities'
st.session_state.master_message = f"흥미로운 배경이네요! 이제 당신의 능력치를 결정해 볼까요?"
st.rerun()
else:
st.error("배경 옵션을 생성하는데 문제가 발생했습니다. 다시 시도해주세요.")
# 직접 작성 옵션 추가
st.markdown("", unsafe_allow_html=True)
st.write("### 직접 배경 작성하기")
st.write("원하는 배경 스토리를 직접 작성할 수 있습니다.")
custom_background = st.text_area("당신의 캐릭터 배경 이야기를 작성하세요:", height=200)
if st.button("이 배경으로 선택", key="custom_bg_select", use_container_width=True):
if custom_background:
# 선택한 배경 저장
st.session_state.selected_background = custom_background
# 배경에서 태그 추출
from src.modules.character_utils import extract_background_tags
st.session_state.background_tags = extract_background_tags(custom_background)
# 다음 단계로 진행
st.session_state.character_creation_step = 'abilities'
st.session_state.master_message = f"자신만의 배경 이야기를 만드셨군요! 이제 능력치를 결정해 볼까요?"
st.rerun()
else:
st.warning("배경 이야기를 입력해주세요.")
st.markdown("
", unsafe_allow_html=True)
def display_race_selection():
"""종족 선택 UI"""
st.subheader("종족 선택")
# 종족 선택 설명 추가
st.markdown("""
캐릭터의 종족은 당신의 모험에 큰 영향을 미칩니다. 각 종족은 고유한 특성과 문화적 배경을 가지고 있습니다.
종족에 따라 특정 능력치에 보너스가 부여될 수 있으며, 스토리텔링에도 영향을 줍니다.
""", unsafe_allow_html=True)
# 종족 목록 가져오기
from src.modules.character_utils import generate_races
races = generate_races(st.session_state.theme)
# 종족별 아이콘 매핑
from src.modules.character_utils import RACE_ICONS, RACE_BONUSES, RACE_ABILITIES, RACE_DESCRIPTIONS
# 종족 선택 버튼 표시 (개선된 카드 형식)
race_cols = st.columns(3)
for i, race in enumerate(races):
with race_cols[i % 3]:
icon = RACE_ICONS.get(race, '👤') # 기본 아이콘
bonus = RACE_BONUSES.get(race, {'??': '+?'}) # 기본 보너스
ability = RACE_ABILITIES.get(race, '특수 능력 없음') # 기본 특수 능력
# 종족 카드 생성 (개선된 UI)
st.markdown(f"""
{icon}
{race}
능력치 보너스:
{"
".join([f"{k}: {v}" for k, v in bonus.items()])}
특수 능력:
{ability}
""", unsafe_allow_html=True)
# 종족별 간단한 설명
if race in RACE_DESCRIPTIONS:
st.markdown(f"""
{RACE_DESCRIPTIONS[race]}
""", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
if st.button(f"선택", key=f"race_{race}"):
st.session_state.selected_race = race
st.session_state.race_bonus = bonus
st.session_state.race_ability = ability
st.session_state.race_icon = icon
st.session_state.character_creation_step = 'profession'
st.session_state.master_message = f"{race} 종족을 선택하셨군요! 이제 당신의 직업을 선택해보세요."
st.rerun()
# 직접 입력 옵션
st.markdown("", unsafe_allow_html=True)
st.write("### 다른 종족 직접 입력")
st.write("원하는 종족이 목록에 없다면, 직접 입력할 수 있습니다.")
custom_race = st.text_input("종족 이름:")
custom_icon = st.selectbox("아이콘 선택:", ['👤', '🧙', '🧝', '🧟', '👻', '👽', '🤖', '🦊', '🐲', '🌟'])
# 능력치 보너스 선택 (최대 2개)
st.write("능력치 보너스 선택 (최대 2개):")
bonus_cols = st.columns(3)
all_stats = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
custom_bonuses = {}
for i, stat in enumerate(all_stats):
with bonus_cols[i % 3]:
bonus_value = st.selectbox(f"{stat} 보너스:", ['+0', '+1', '+2'], key=f"custom_bonus_{stat}")
if bonus_value != '+0':
custom_bonuses[stat] = bonus_value
# 특수 능력 입력
custom_ability = st.text_area("특수 능력 (선택사항):",
placeholder="예: 어둠 속에서도 잘 볼 수 있는 능력")
if custom_race and st.button("이 종족으로 선택"):
st.session_state.selected_race = custom_race
st.session_state.race_bonus = custom_bonuses if custom_bonuses else {'없음': '+0'}
st.session_state.race_ability = custom_ability if custom_ability else "특수 능력 없음"
st.session_state.race_icon = custom_icon
st.session_state.character_creation_step = 'profession'
st.session_state.master_message = f"{custom_race} 종족을 선택하셨군요! 이제 당신의 직업을 선택해보세요."
st.rerun()
st.markdown("
", unsafe_allow_html=True)
def display_profession_selection():
"""직업 선택 UI"""
st.subheader("직업 선택")
# 직업 선택 설명 추가
st.markdown("""
직업은 캐릭터가 세계에서 수행하는 역할과 전문 기술을 결정합니다.
각 직업마다 중요한 능력치가 다르며, 독특한 기술과 성장 경로를 가집니다.
""", unsafe_allow_html=True)
# 선택된 종족 표시 (개선된 UI)
race_icon = st.session_state.get('race_icon', '👤')
race_bonuses = st.session_state.get('race_bonus', {})
race_ability = st.session_state.get('race_ability', "특수 능력 없음")
st.markdown(f"""
{race_icon}
선택한 종족: {st.session_state.selected_race}
능력치 보너스: {', '.join([f"{k} {v}" for k, v in race_bonuses.items()])}
특수 능력: {race_ability}
""", unsafe_allow_html=True)
# 직업 선택 방식
profession_method = st.radio(
"직업 선택 방식:",
["기본 직업 선택", "직접 직업 만들기"],
horizontal=True
)
if profession_method == "기본 직업 선택":
# 직업 목록 가져오기
from src.modules.character_utils import generate_professions
professions = generate_professions(st.session_state.theme)
# 직업별 아이콘 및 정보 가져오기
from src.modules.character_utils import (
PROFESSION_ICONS, PROFESSION_STATS,
PROFESSION_EQUIPMENT, PROFESSION_SKILLS
)
# 직업 선택 버튼 표시 (개선된 카드 형식)
profession_cols = st.columns(3)
for i, profession in enumerate(professions):
with profession_cols[i % 3]:
icon = PROFESSION_ICONS.get(profession, '👤') # 기본 아이콘
key_stats = PROFESSION_STATS.get(profession, ['??', '??']) # 주요 능력치
equipment = PROFESSION_EQUIPMENT.get(profession, ['기본 장비']) # 시작 장비
skill = PROFESSION_SKILLS.get(profession, '특수 기술 없음') # 특수 기술
# 직업 카드 생성 (개선된 UI)
st.markdown(f"""
{icon}
{profession}
주요 능력치: {' & '.join(key_stats)}
시작 장비:
{"".join([f"- {item}
" for item in equipment[:3]])}
{"" if len(equipment) <= 3 else "- ...
"}
특수 기술:
{skill}
""", unsafe_allow_html=True)
if st.button(f"선택", key=f"prof_{profession}"):
st.session_state.selected_profession = profession
st.session_state.profession_icon = icon
st.session_state.profession_stats = key_stats
st.session_state.profession_equipment = equipment
st.session_state.profession_skill = skill
# 배경 옵션 생성 상태 확인
if not st.session_state.background_options_generated:
with st.spinner("캐릭터 배경 옵션을 생성 중..."):
st.session_state.character_backgrounds = generate_character_options(
profession, st.session_state.theme
)
st.session_state.background_options_generated = True
st.session_state.character_creation_step = 'background'
st.session_state.master_message = f"{profession} 직업을 선택하셨군요! 이제 캐릭터의 배경 이야기를 선택해보세요."
st.rerun()
else: # 직접 직업 만들기
st.markdown("", unsafe_allow_html=True)
st.write("### 나만의 직업 만들기")
st.write("세계관에 맞는 독특한 직업을 직접 만들어보세요")
custom_profession = st.text_input("직업 이름:")
custom_icon = st.selectbox("아이콘 선택:", ['🧙', '⚔️', '🗡️', '🧪', '📚', '🔮', '🎭', '⚗️', '🛡️', '🚀', '💻', '🧬', '👽', '⚙️', '📡', '📦', '💉', '🔭'])
# 주요 능력치 선택 (최대 2개)
st.write("주요 능력치 선택 (최대 2개):")
stat_cols = st.columns(3)
all_stats = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
selected_stats = []
for i, stat in enumerate(all_stats):
with stat_cols[i % 3]:
if st.checkbox(f"{stat}", key=f"custom_prof_stat_{stat}"):
selected_stats.append(stat)
# 3개 이상 선택 시 경고
if len(selected_stats) > 2:
st.warning("주요 능력치는 최대 2개까지만 선택할 수 있습니다. 처음 2개만 적용됩니다.")
selected_stats = selected_stats[:2]
elif len(selected_stats) == 0:
st.info("주요 능력치를 1~2개 선택하세요.")
# 시작 장비 입력
st.write("시작 장비 (콤마로 구분):")
equipment_input = st.text_area("예: 검, 방패, 물약 3개", height=100)
# 특수 기술 입력
special_skill = st.text_input("특수 기술 (예: 숨기: 은신 판정에 +2 보너스):")
# 직업 설명
profession_desc = st.text_area("직업 설명:",
placeholder="이 직업의 역할, 행동 방식, 세계관에서의 위치 등을 설명해주세요.",
height=100)
if st.button("이 직업으로 선택", use_container_width=True):
if custom_profession and len(selected_stats) > 0 and special_skill:
# 사용자 정의 직업 정보 저장
st.session_state.selected_profession = custom_profession
st.session_state.profession_icon = custom_icon
st.session_state.profession_stats = selected_stats
# 장비 파싱
equipment_list = [item.strip() for item in equipment_input.split(',') if item.strip()]
if not equipment_list:
equipment_list = ["기본 장비"]
st.session_state.profession_equipment = equipment_list
st.session_state.profession_skill = special_skill
st.session_state.profession_description = profession_desc
# 배경 옵션 생성 상태 확인
if not st.session_state.background_options_generated:
with st.spinner("캐릭터 배경 옵션을 생성 중..."):
from src.modules.character_utils import generate_character_options
st.session_state.character_backgrounds = generate_character_options(
custom_profession, st.session_state.theme
)
st.session_state.background_options_generated = True
st.session_state.character_creation_step = 'background'
st.session_state.master_message = f"{custom_profession} 직업을 선택하셨군요! 이제 캐릭터의 배경 이야기를 선택해보세요."
st.rerun()
else:
st.error("직업 이름, 최소 1개의 주요 능력치, 특수 기술은 필수 입력사항입니다.")
st.markdown("
", unsafe_allow_html=True)
def display_abilities_selection():
"""능력치 설정 UI"""
st.subheader("능력치 설정")
# 능력치 설정 설명 추가
st.markdown("""
능력치는 캐릭터의 신체적, 정신적 역량을 수치화한 것입니다.
주사위를 굴려 결정하거나, 기본값을 사용할 수 있습니다.
""", unsafe_allow_html=True)
# 선택된 종족, 직업, 배경 태그 표시 (개선된 UI)
race_icon = st.session_state.get('race_icon', '👤')
profession_icon = st.session_state.get('profession_icon', '👤')
key_stats = st.session_state.get('profession_stats', ['??', '??'])
race_bonuses = st.session_state.get('race_bonus', {})
bg_tags = st.session_state.get('background_tags', ["신비로운"])
# 태그 표시용 HTML 생성
tags_html = ""
from modules.character_utils import BACKGROUND_TAGS_COLORS
for tag in bg_tags:
tag_color = BACKGROUND_TAGS_COLORS.get(tag, "#607D8B") # 기본값은 회색
tags_html += f"""
{tag}
"""
# 캐릭터 요약 표시
st.markdown(f"""
{race_icon}
{st.session_state.selected_race} {st.session_state.selected_profession}
{tags_html}
{profession_icon}
핵심 능력치
{"・".join(key_stats)}
종족 보너스
{"・".join([f"{k} {v}" for k, v in race_bonuses.items()])}
""", unsafe_allow_html=True)
ability_col1, ability_col2 = st.columns([3, 1])
with ability_col1:
# 능력치 설정 방법 선택
ability_method = st.radio(
"능력치 설정 방법:",
["3D6 주사위 굴리기", "기본 능력치 사용"],
horizontal=True
)
if ability_method == "3D6 주사위 굴리기":
ability_roll_section()
else: # 기본 능력치 사용
base_abilities_section()
with ability_col2:
# 능력치 설명 및 정보 표시
st.markdown("""
능력치 정보
| STR | 근력, 물리적 공격력 |
| DEX | 민첩성, 회피/정확도 |
| CON | 체력, 생존력 |
| INT | 지능, 마법/기술 이해력 |
| WIS | 지혜, 직관/인식력 |
| CHA | 매력, 설득력/교섭력 |
""", unsafe_allow_html=True)
# 능력치 점수 해석
st.markdown("""
능력치 점수 해석
| 1-3 | 심각한 약점 |
| 4-6 | 약함 |
| 7-9 | 평균 이하 |
| 10-12 | 평균적 |
| 13-15 | 평균 이상 |
| 16-17 | 매우 뛰어남 |
| 18+ | 전설적 수준 |
""", unsafe_allow_html=True)
# 배경 요약
st.markdown("""
배경 요약
""", unsafe_allow_html=True)
# 배경 텍스트에서 중요 부분만 추출 (첫 200자)
bg_summary = st.session_state.selected_background[:200]
if len(st.session_state.selected_background) > 200:
bg_summary += "..."
st.markdown(f"{bg_summary}", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
# 뒤로 가기 옵션
if st.button("← 배경 선택으로 돌아가기", use_container_width=True):
st.session_state.character_creation_step = 'background'
# 주사위 굴리기 관련 상태 초기화
if 'dice_rolled' in st.session_state:
del st.session_state.dice_rolled
if 'reroll_used' in st.session_state:
del st.session_state.reroll_used
if 'rolled_abilities' in st.session_state:
del st.session_state.rolled_abilities
st.session_state.master_message = "배경을 다시 선택해 보세요!"
st.rerun()
def ability_roll_section():
"""주사위 굴리기로 능력치 결정하는 UI 섹션"""
# 주사위 굴리기 관련 상태 초기화
if 'dice_rolled' not in st.session_state:
st.session_state.dice_rolled = False
if 'reroll_used' not in st.session_state:
st.session_state.reroll_used = False
# 주사위 굴리기 설명 추가
st.markdown("""
능력치는 각각 3D6(6면체 주사위 3개) 방식으로 결정됩니다.
각 능력치는 3~18 사이의 값을 가지며, 평균값은 10-11입니다.
14 이상은 뛰어난 능력, 16 이상은 탁월한 능력입니다.
다시 굴리기는 1번만 가능합니다.
""", unsafe_allow_html=True)
# 주사위 굴리기 버튼
if not st.session_state.dice_rolled and st.button("주사위 굴리기", use_container_width=True, key="roll_ability_dice"):
st.session_state.dice_rolled = True
# 능력치 목록
ability_names = ['STR', 'INT', 'DEX', 'CON', 'WIS', 'CHA']
rolled_abilities = {}
# 각 능력치별 주사위 굴리기 결과 애니메이션으로 표시
ability_placeholders = {}
for ability in ability_names:
ability_placeholders[ability] = st.empty()
# 순차적으로 각 능력치 굴리기
for ability in ability_names:
with st.spinner(f"{ability} 굴리는 중..."):
# 주사위 애니메이션 표시
dice_result = display_dice_animation(ability_placeholders[ability], "3d6", 0.5)
rolled_abilities[ability] = dice_result['total']
time.sleep(0.2) # 약간의 딜레이
# 세션에 저장
st.session_state.rolled_abilities = rolled_abilities
st.rerun()
# 굴린 결과 표시
if st.session_state.dice_rolled and 'rolled_abilities' in st.session_state:
st.write("#### 주사위 결과:")
cols = st.columns(3)
i = 0
# 직업 정보를 미리 가져옴
prof = st.session_state.selected_profession if 'selected_profession' in st.session_state else ""
# 직업별 중요 능력치 정보
profession_key_stats = st.session_state.get('profession_stats', [])
# 능력치 총점 계산 (나중에 보여주기 위함)
total_points = sum(st.session_state.rolled_abilities.values())
# 결과를 정렬하여 먼저 중요 능력치를 표시
sorted_abilities = sorted(
st.session_state.rolled_abilities.items(),
key=lambda x: (x[0] not in profession_key_stats, profession_key_stats.index(x[0]) if x[0] in profession_key_stats else 999)
)
for ability, value in sorted_abilities:
with cols[i % 3]:
# 직업에 중요한 능력치인지 확인
is_key_stat = ability in profession_key_stats
# 색상 및 설명 가져오기
color, description = get_stat_info(ability, value, prof)
# 중요 능력치 강조 스타일
highlight = "border: 2px solid gold; background-color: rgba(255, 215, 0, 0.1);" if is_key_stat else ""
key_badge = "핵심" if is_key_stat else ""
# 능력치 값에 따른 바 그래프 너비 계산 (백분율, 최대 18 기준)
bar_width = min(100, (value / 18) * 100)
# 개선된 능력치 표시
st.markdown(f"""
{ability}{key_badge}
{value}
{description}
""", unsafe_allow_html=True)
i += 1
# 능력치 총점 표시
avg_total = 63 # 3D6 6개의 평균
# 총점 평가 (낮음, 평균, 높음)
if total_points < avg_total - 5:
total_rating = "낮음"
total_color = "#F44336" # 빨간색
elif total_points > avg_total + 5:
total_rating = "높음"
total_color = "#4CAF50" # 녹색
else:
total_rating = "평균"
total_color = "#FFC107" # 노란색
st.markdown(f"""
능력치 총점:
{total_points}
{total_rating}
(평균 63, 70+ 우수, 80+ 탁월)
""", unsafe_allow_html=True)
# 버튼 열 생성
col1, col2 = st.columns(2)
with col1:
if st.button("이 능력치로 진행하기", use_container_width=True, key="use_these_stats"):
st.session_state.character['stats'] = st.session_state.rolled_abilities
st.session_state.character['profession'] = st.session_state.selected_profession
st.session_state.character['race'] = st.session_state.selected_race
st.session_state.character['backstory'] = st.session_state.selected_background
st.session_state.character_creation_step = 'review'
st.session_state.master_message = "좋습니다! 캐릭터가 거의 완성되었습니다. 최종 확인을 해 볼까요?"
# 다시 굴리기 관련 상태 초기화
st.session_state.dice_rolled = False
st.session_state.reroll_used = False
st.rerun()
with col2:
# 다시 굴리기 버튼 - 한번만 사용 가능하도록 제한
if st.button("다시 굴리기",
use_container_width=True,
key="reroll_ability_dice",
disabled=st.session_state.reroll_used):
if not st.session_state.reroll_used:
# 다시 굴리기 사용 표시
st.session_state.reroll_used = True
# 능력치 목록
ability_names = ['STR', 'INT', 'DEX', 'CON', 'WIS', 'CHA']
rerolled_abilities = {}
# 각 능력치별 재굴림 결과 표시
reroll_placeholders = {}
for ability in ability_names:
reroll_placeholders[ability] = st.empty()
# 순차적으로 각 능력치 다시 굴리기
for ability in ability_names:
with st.spinner(f"{ability} 다시 굴리는 중..."):
# 다시 굴림 애니메이션 표시
dice_result = display_dice_animation(reroll_placeholders[ability], "3d6", 0.5)
rerolled_abilities[ability] = dice_result['total']
time.sleep(0.1) # 약간의 딜레이
# 결과 저장 및 상태 업데이트
st.session_state.rolled_abilities = rerolled_abilities
st.session_state.reroll_message = "다시 굴리기 기회를 사용했습니다."
st.rerun()
# 다시 굴리기 사용 여부 표시
if st.session_state.reroll_used:
st.info("다시 굴리기 기회를 이미 사용했습니다.")
def base_abilities_section():
"""기본 능력치 설정 UI 섹션"""
st.write("#### 기본 능력치:")
base_abilities = {'STR': 10, 'INT': 10, 'DEX': 10, 'CON': 10, 'WIS': 10, 'CHA': 10}
# 직업에 따른 추천 능력치 조정
if 'selected_profession' in st.session_state:
profession = st.session_state.selected_profession
profession_key_stats = st.session_state.get('profession_stats', [])
# 주요 능력치에 보너스 부여
for stat in profession_key_stats:
if stat in base_abilities:
base_abilities[stat] = 14 # 주요 능력치는 14로 설정
# 종족에 따른 능력치 보너스 적용
if 'race_bonus' in st.session_state:
for stat, bonus in st.session_state.race_bonus.items():
if stat in base_abilities:
# 보너스값에서 '+'를 제거하고 정수로 변환
bonus_value = int(bonus.replace('+', ''))
base_abilities[stat] += bonus_value
elif stat == "모든 능력치":
# 모든 능력치에 보너스 적용
bonus_value = int(bonus.replace('+', ''))
for ability in base_abilities:
base_abilities[ability] += bonus_value
# 결과 표시 (향상된 시각적 표현)
cols = st.columns(3)
i = 0
# 직업 정보 가져오기
prof = st.session_state.selected_profession if 'selected_profession' in st.session_state else ""
key_stats = st.session_state.get('profession_stats', [])
# 정렬: 주요 능력치 먼저
sorted_abilities = sorted(
base_abilities.items(),
key=lambda x: (x[0] not in key_stats, key_stats.index(x[0]) if x[0] in key_stats else 999)
)
for ability, value in sorted_abilities:
with cols[i % 3]:
color, description = get_stat_info(ability, value, prof)
is_key_stat = ability in key_stats
# 중요 능력치 강조 스타일
highlight = "border: 2px solid gold; background-color: rgba(255, 215, 0, 0.1);" if is_key_stat else ""
key_badge = "핵심" if is_key_stat else ""
# 종족 보너스 표시
race_bonus_badge = ""
for stat, bonus in st.session_state.race_bonus.items():
if stat == ability or stat == "모든 능력치":
race_bonus_badge = f"{bonus}"
# 개선된 능력치 표시
st.markdown(f"""
{ability}{key_badge}{race_bonus_badge}
{value}
{description}
""", unsafe_allow_html=True)
i += 1
# 능력치 총점 표시
total_points = sum(base_abilities.values())
avg_total = 60 # 평균 총점
# 총점 평가 (낮음, 평균, 높음)
if total_points < avg_total - 5:
total_rating = "낮음"
total_color = "#F44336" # 빨간색
elif total_points > avg_total + 5:
total_rating = "높음"
total_color = "#4CAF50" # 녹색
else:
total_rating = "평균"
total_color = "#FFC107" # 노란색
st.markdown(f"""
능력치 총점:
{total_points}
{total_rating}
(평균 60-65, 70+ 우수, 80+ 탁월)
""", unsafe_allow_html=True)
if st.button("기본 능력치로 진행하기", use_container_width=True):
st.session_state.character['stats'] = base_abilities
st.session_state.character['profession'] = st.session_state.selected_profession
st.session_state.character['race'] = st.session_state.selected_race
st.session_state.character['backstory'] = st.session_state.selected_background
st.session_state.character_creation_step = 'review'
st.session_state.master_message = "좋습니다! 캐릭터가 거의 완성되었습니다. 최종 확인을 해 볼까요?"
st.rerun()
def display_character_review():
"""캐릭터 최종 확인 UI"""
st.subheader("캐릭터 최종 확인")
# 마지막 설명 추가
st.markdown("""
당신의 캐릭터가 완성되었습니다! 최종 정보를 확인하고 모험을 시작하세요.
능력치, 장비, 특수 능력을 확인하고 필요하다면 수정할 수 있습니다.
""", unsafe_allow_html=True)
review_col1, review_col2 = st.columns([2, 1])
with review_col1:
# 종족 및 직업 아이콘 가져오기
race_icon = st.session_state.get('race_icon', '👤')
profession_icon = st.session_state.get('profession_icon', '👤')
bg_tags = st.session_state.get('background_tags', ["신비로운"])
# 태그 표시용 HTML 생성
tags_html = ""
from modules.character_utils import BACKGROUND_TAGS_COLORS
for tag in bg_tags:
tag_color = BACKGROUND_TAGS_COLORS.get(tag, "#607D8B") # 기본값은 회색
tags_html += f"""
{tag}
"""
# 캐릭터 카드 생성 (화려한 디자인)
st.markdown(f"""
{race_icon}
{st.session_state.character['race']} {st.session_state.character['profession']}
{tags_html}
{profession_icon}
캐릭터 특성
{st.session_state.get('race_ability', '종족 특성 없음')}
{st.session_state.get('profession_skill', '직업 특성 없음')}
배경 스토리
{st.session_state.character['backstory']}
""", unsafe_allow_html=True)
# 인벤토리 표시 (개선된 버전)
st.markdown("""
인벤토리
""", unsafe_allow_html=True)
# 인벤토리 아이템 정렬
from modules.item_manager import display_inventory_for_review
display_inventory_for_review(st.session_state.character['inventory'])
st.markdown("", unsafe_allow_html=True)
# 특별한 특성 추가
if 'special_trait' not in st.session_state:
# 테마와 배경 태그에 따른 특성 선택
from modules.character_utils import generate_special_trait
st.session_state.special_trait = generate_special_trait(
st.session_state.theme,
st.session_state.get('background_tags', ["신비로운"])
)
# 특수 특성 표시
st.markdown(f"""
특별한 특성
🌟 {st.session_state.special_trait.split(":")[0]}
{":".join(st.session_state.special_trait.split(":")[1:])}
""", unsafe_allow_html=True)
with review_col2:
# 능력치 표시
st.markdown("""
능력치
""", unsafe_allow_html=True)
# 직업 정보 가져오기
prof = st.session_state.character['profession']
key_stats = st.session_state.get('profession_stats', [])
# 능력치 값 총합 계산
total_points = sum(st.session_state.character['stats'].values())
# 능력치 설정
for stat, value in st.session_state.character['stats'].items():
# 색상 및 설명 가져오기
color, description = get_stat_info(stat, value, prof)
is_key_stat = stat in key_stats
# 키 스탯 표시
key_badge = ""
if is_key_stat:
key_badge = f"
핵심"
# 바 그래프 너비 계산 (백분율, 최대 18 기준)
bar_width = min(100, (value / 18) * 100)
# 능력치 바 생성
st.markdown(f"""
{stat}
{key_badge}
{value}
{description}
""", unsafe_allow_html=True)
# 능력치 총점 표시
avg_total = 60 # 평균 총점
# 총점 평가 (낮음, 평균, 높음)
if total_points < avg_total - 5:
total_rating = "낮음"
total_color = "#F44336" # 빨간색
elif total_points > avg_total + 5:
total_rating = "높음"
total_color = "#4CAF50" # 녹색
else:
total_rating = "평균"
total_color = "#FFC107" # 노란색
st.markdown(f"""
능력치 총점:
{total_points}
{total_rating}
""", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
# 시작 위치 정보
st.markdown(f"""
시작 위치
{st.session_state.current_location}
""", unsafe_allow_html=True)
# 캐릭터 플레이 팁
st.markdown(f"""
플레이 팁
- 당신의 핵심 능력치({', '.join(key_stats)})를 활용하는 행동을 시도하세요.
- "{st.session_state.special_trait.split(':')[0]}" 특성을 중요한 순간에 활용하세요.
- 배경 스토리와 일관된 캐릭터 플레이를 하면 더 몰입감 있는 경험을 할 수 있습니다.
- 마스터에게 세계관에 대한 궁금한 점을 자유롭게 질문하세요.
- 창의적인 문제 해결 방법을 시도해보세요.
""", unsafe_allow_html=True)
# 최종 선택 버튼
col1, col2 = st.columns(2)
with col1:
if st.button("이 캐릭터로 게임 시작", use_container_width=True):
# 특별한 특성 저장
if 'special_trait' in st.session_state:
st.session_state.character['special_trait'] = st.session_state.special_trait
# 게임 시작 준비
with st.spinner("게임을 준비하는 중..."):
# 시작 메시지 생성
from modules.ai_service import generate_game_intro
intro = generate_game_intro(
st.session_state.world_description,
st.session_state.character,
st.session_state.current_location
)
st.session_state.story_log.append(intro)
# 행동 제안 생성 상태 설정
st.session_state.suggestions_generated = False
# 게임 시작
st.session_state.stage = 'game_play'
st.session_state.master_message = f"모험이 시작되었습니다! {st.session_state.character['race']} {st.session_state.character['profession']}으로서의 여정이 펼쳐집니다."
# 행동 단계 초기화
st.session_state.action_phase = 'suggestions'
st.rerun()
with col2:
if st.button("처음부터 다시 만들기", use_container_width=True):
# 캐릭터 생성 단계 초기화
st.session_state.character_creation_step = 'race'
st.session_state.background_options_generated = False
# 임시 데이터 삭제
for key in ['selected_race', 'selected_profession', 'character_backgrounds', 'selected_background',
'rolled_abilities', 'special_trait', 'race_bonus', 'race_ability', 'race_icon',
'profession_icon', 'profession_stats', 'profession_equipment', 'profession_skill',
'background_tags', 'dice_rolled', 'reroll_used']:
if key in st.session_state:
del st.session_state[key]
# 캐릭터 정보 초기화
from modules.item_manager import initialize_inventory
st.session_state.character = {
'profession': '',
'stats': {'STR': 0, 'INT': 0, 'DEX': 0, 'CON': 0, 'WIS': 0, 'CHA': 0},
'backstory': '',
'inventory': initialize_inventory(st.session_state.theme)
}
st.session_state.master_message = "다시 시작해봅시다! 어떤 종족을 선택하시겠어요?"
st.rerun()