| """Explorer Tab — location exploration, NPC encounters, quest tracking, actions.""" |
|
|
| import random |
| import gradio as gr |
|
|
| from core.world import WorldGraph |
| from core.player import load_player, save_player |
| from core.model_manager import ModelManager |
| from ui.i18n import t |
|
|
|
|
| def _get_location_choices() -> list: |
| """Get list of location names for the dropdown.""" |
| world = WorldGraph() |
| return [loc.name for loc in world.get_entities_by_type("location")] |
|
|
|
|
| def _render_location(location_name: str) -> str: |
| if not location_name: |
| return f""" |
| <div style="text-align:center;padding:60px 20px;color:#555;"> |
| <div style="margin-bottom:16px;"></div> |
| <p>{t('exp_empty')}</p> |
| </div> |
| """ |
|
|
| world = WorldGraph() |
| player = load_player() |
|
|
| locations = world.get_entities_by_type("location") |
| location = next((l for l in locations if l.name == location_name), None) |
| if not location: |
| return f"<p>{t('exp_not_found')}</p>" |
|
|
| player.current_location_id = location.id |
| save_player(player) |
|
|
| all_npcs = world.get_entities_by_type("npc") |
| npcs_here = [n for n in all_npcs if n.location_id == location.id] |
|
|
| all_quests = world.get_entities_by_type("quest") |
| quests_here = [q for q in all_quests if q.location_id == location.id and not player.is_quest_completed(q.id)] |
|
|
| neighbors = world.get_neighbors(location.id, "connects_to") |
| connected = [n.name for n in neighbors if n.name != location_name] |
|
|
| html = f""" |
| <div style="max-width:650px;margin:0 auto;"> |
| <div style="text-align:center;margin-bottom:20px;padding:20px;background:linear-gradient(180deg,rgba(74,158,255,0.08),transparent);border-radius:12px;"> |
| <div style="font-family:Georgia,serif;font-size:1.6rem;color:#4a9eff;margin-bottom:6px;"> |
| {location.name} |
| </div> |
| <div style="color:#aaa;font-size:0.9rem;line-height:1.7;max-width:500px;margin:0 auto;"> |
| {location.description} |
| </div> |
| </div> |
| """ |
|
|
| if npcs_here: |
| html += f'<div style="font-family:Georgia,serif;color:#50c878;font-size:1rem;margin:16px 0 10px;">{t("exp_encounter")}</div>' |
| for npc in npcs_here: |
| dialog = npc.dialog or "..." |
| html += f""" |
| <div style="background:#16162a;border:1px solid #2a2a4a;border-radius:10px;padding:14px 18px;margin-bottom:10px;border-left:3px solid #50c878;"> |
| <div style="color:#50c878;font-weight:600;margin-bottom:4px;">{npc.name}</div> |
| <div style="color:#a0d0a0;font-style:italic;font-size:0.9rem;">"{dialog}"</div> |
| </div> |
| """ |
|
|
| if quests_here: |
| html += f'<div style="font-family:Georgia,serif;color:#ffd700;font-size:1rem;margin:16px 0 10px;">{t("exp_quests")}</div>' |
| for q in quests_here: |
| html += f""" |
| <div style="background:#1a1a10;border:1px solid #3a3a2a;border-radius:10px;padding:14px 18px;margin-bottom:10px;border-left:3px solid #ffd700;"> |
| <div style="color:#ffd700;font-weight:600;margin-bottom:4px;">{q.name}</div> |
| <div style="color:#aaa;font-size:0.85rem;line-height:1.5;">{q.description[:100]}...</div> |
| </div> |
| """ |
|
|
| if connected: |
| html += f'<div style="font-family:Georgia,serif;color:#b8a9ff;font-size:1rem;margin:16px 0 10px;">{t("exp_travel")}</div>' |
| html += '<div style="display:flex;flex-wrap:wrap;gap:8px;">' |
| for loc_name in connected: |
| html += f""" |
| <div style="padding:6px 14px;background:#16162a;border:1px solid #2a2a4a;border-radius:8px;color:#4a9eff;font-size:0.85rem;"> |
| {loc_name} |
| </div> |
| """ |
| html += "</div>" |
|
|
| html += "</div>" |
| return html |
|
|
|
|
| def explore_location(location_name: str) -> tuple: |
| if not location_name: |
| return t("exp_select_first"), "" |
|
|
| world = WorldGraph() |
| player = load_player() |
| manager = ModelManager() |
|
|
| locations = world.get_entities_by_type("location") |
| location = next((l for l in locations if l.name == location_name), None) |
| if not location: |
| return t("exp_not_exist"), "" |
|
|
| outcomes = [t("exp_outcome_1"), t("exp_outcome_2"), t("exp_outcome_3"), t("exp_outcome_4"), t("exp_outcome_5")] |
| outcome = outcomes[0] |
|
|
| prompt = f"你在{location.name}探索。{location.description[:100]}\n\n请描述一个有趣的发现(物品或线索),50字以内。用中文回答。" |
| gen = manager.generate_fn |
| discovery = gen(prompt) or outcome |
|
|
| level_msg = player.add_xp(15) |
|
|
| item_found = None |
| if random.random() > 0.5: |
| item_names = [t("item_key"), t("item_crystal"), t("item_scroll"), t("item_potion"), t("item_dust"), t("item_shard")] |
| item_name = random.choice(item_names) |
| player.add_item(item_name, discovery[:30], "misc") |
| item_found = item_name |
|
|
| save_player(player) |
|
|
| result = f"{t('exp_discovery')}: {discovery}" |
| if item_found: |
| result += f"\n{t('exp_item_found')}: {item_found} (+15 XP)" |
| if level_msg: |
| result += f"\n{level_msg}" |
|
|
| return result, _render_location(location_name) |
|
|
|
|
| def rest_action(location_name: str) -> tuple: |
| player = load_player() |
| player.heal(hp=30, mp=20) |
| save_player(player) |
| return f"{t('exp_rest_result')} (HP:{player.hp}/{player.max_hp}, MP:{player.mp}/{player.max_mp})", _render_location(location_name) |
|
|
|
|
| def create_explorer_tab() -> tuple: |
| """Create the Explorer tab.""" |
| with gr.Row(): |
| location_dropdown = gr.Dropdown( |
| label=t("exp_select_label"), |
| choices=_get_location_choices(), |
| interactive=True, |
| ) |
|
|
| location_display = gr.HTML( |
| value=f""" |
| <div style="text-align:center;padding:60px 20px;color:#555;"> |
| <div style="margin-bottom:16px;"></div> |
| <p>{t('exp_empty')}</p> |
| </div> |
| """ |
| ) |
|
|
| with gr.Row(): |
| explore_btn = gr.Button(t("exp_explore"), variant="primary") |
| rest_btn = gr.Button(t("exp_rest"), variant="secondary") |
|
|
| action_result = gr.HTML("") |
|
|
| |
| location_dropdown.change( |
| fn=_render_location, |
| inputs=[location_dropdown], |
| outputs=[location_display], |
| ) |
|
|
| |
| explore_btn.click( |
| fn=explore_location, |
| inputs=[location_dropdown], |
| outputs=[action_result, location_display], |
| ) |
|
|
| |
| rest_btn.click( |
| fn=rest_action, |
| inputs=[location_dropdown], |
| outputs=[action_result, location_display], |
| ) |
|
|
| return location_dropdown, location_display, action_result |
|
|