from __future__ import annotations from collections import deque from dataclasses import dataclass from typing import Any ACTION_TIME_COSTS: dict[str, int] = { "MOVE": 30, "ATTACK": 30, "COMBAT": 30, "CLAIM_REWARD": 10, "REST": 30, "OVERNIGHT_REST": 30, "TALK": 10, "SHOP_MENU": 10, "SCENE_OPTIONS": 10, "TRADE": 10, "EQUIP": 10, "USE_ITEM": 10, "VIEW_MAP": 10, "MAP": 10, "QUEST": 10, } MAX_OPTION_COUNT = 6 DEFAULT_OPTION_COUNT = 3 OVERNIGHT_REST_LOCATIONS = {"村庄旅店", "溪边营地"} MAIN_QUEST_ID = "main_quest_01" MAIN_QUEST_TROLL_ID = "main_quest_02" FOREST_GOBLIN_DEFEATED_FLAG = "encounter::dark_forest_gate_goblin_defeated" FOREST_TROLL_TRACKS_FOUND_FLAG = "clue::forest_troll_tracks_found" FOREST_TROLL_DEFEATED_FLAG = "encounter::forest_troll_defeated" FOREST_TROLL_INTRO_SEEN_FLAG = "scene::forest_troll_intro_seen" FOREST_TROLL_HOARD_PENDING_FLAG = "reward::forest_troll_hoard_pending" FOREST_TROLL_HOARD_CLAIMED_FLAG = "reward::forest_troll_hoard_claimed" DEEP_FOREST_BARRIER_SEEN_FLAG = "clue::deep_forest_barrier_seen" FOREST_CAUSE_OBJECTIVE = "调查怪物活动的原因" REPORT_TO_CHIEF_OBJECTIVE = "与村长老伯对话汇报发现" FOREST_TROLL_TRAVEL_OBJECTIVE = "前往森林深处" FOREST_TROLL_BOSS_OBJECTIVE = "击败森林巨魔" SIDE_QUEST_TRAVELER_ID = "side_quest_01" SIDE_QUEST_FERRY_ID = "side_quest_02" SIDE_QUEST_GUARDIAN_ID = "side_quest_03" TRAVELER_RUMOR_HEARD_FLAG = "rumor::traveler_lead_heard" MINE_RUMOR_HEARD_FLAG = "rumor::mine_ghost_heard" FERRY_ROUTE_UNLOCKED_FLAG = "rumor::ferry_route_unlocked" TRAVELER_ENCOUNTERED_FLAG = "scene::mysterious_traveler_encountered" GUARDIAN_INTRO_SEEN_FLAG = "scene::elf_guardian_introduced" LOCATION_MAP_REQUIREMENTS: dict[str, str] = { "村庄广场": "村庄地图", "村庄铁匠铺": "村庄地图", "村庄旅店": "村庄地图", "村庄杂货铺": "村庄地图", "村口小路": "村庄地图", # 黑暗森林入口 可以用村庄地图到达;击败哥布林后获得黑暗森林地图 "黑暗森林入口": "村庄地图", # 溪边营地/森林深处 需要黑暗森林地图(森林入口战斗奖励) "溪边营地": "黑暗森林地图", "森林深处": "黑暗森林地图", # 河边渡口 用村庄地图可到达;与老渔夫对话后获得山麓地图 "河边渡口": "村庄地图", # 以下地点需要山麓地图(老渔夫给予) "废弃矿洞入口": "山麓地图", "山麓盗贼营": "山麓地图", "精灵遗迹": "山麓地图", # 古塔废墟 从村口小路可直接发现,村庄地图即可 "古塔废墟": "村庄地图", } SHOP_LOCATION_TO_MERCHANT: dict[str, str] = { "村庄铁匠铺": "铁匠格林", "村庄旅店": "旅店老板娘莉娜", "村庄杂货铺": "杂货商人阿尔", } ARRIVAL_EVENT_CONFIG: dict[str, dict[str, str]] = { "村庄铁匠铺": { "event_key": "arrival::village_blacksmith", "story_text": ( "一间热气腾腾的铁匠铺,炉火正旺。墙上挂满了各式武器和护甲。\n" "炉火映红了铁匠粗犷的脸庞,空气中弥漫着金属和碳的气味。" ), }, "村庄旅店": { "event_key": "arrival::village_inn", "story_text": ( "一家温馨的小旅店,空气中弥漫着烤肉和麦酒的香气。壁炉里的火焰跳动着。\n" "壁炉噼啪作响,几位旅客正低声交谈,空气温暖而舒适。" ), }, "村庄杂货铺": { "event_key": "arrival::village_general_store", "story_text": ( "一家琳琅满目的杂货铺,从草药到绳索应有尽有。老板是个精明的商人。\n" "货架上摆满了稀奇古怪的商品,柜台后的商人正用算盘噼里啪啦地算账。" ), }, "黑暗森林入口": { "event_key": "arrival::dark_forest_gate", "story_text": ( "黑暗森林入口的树冠压低了天色,落叶间散着新鲜爪痕和被拖行过的泥印。\n" "一只拎着弯刀的哥布林正伏在断木后张望,时不时发出刺耳怪叫,像是在替林中的东西守门。" ), }, "河边渡口": { "event_key": "arrival::river_ferry", "story_text": ( "破旧的渡口被河水拍得吱呀作响,湿冷水汽贴着木桩往上爬。\n" "披着蓑衣的老渔夫正盯着对岸,矿洞与山麓营地的路径都从这里分开。" ), }, "山麓盗贼营": { "event_key": "arrival::bandit_camp", "story_text": ( "山麓盗贼营的篝火还留着余温,翻倒的酒桶和散落的干粮说明有人刚撤离不久。\n" "营帐阴影里有个盗贼斥候正贴着木桩窥探四周,显然不打算让外来者轻易通过。" ), }, "古塔废墟": { "event_key": "arrival::ancient_tower", "story_text": ( "半坍塌的古塔在风里发出低鸣,残破石阶和裂墙间还能看到新鲜抓痕。\n" "塔门内飘着一团幽蓝冷火,游荡幽灵在尘灰间若隐若现,像是在警告你别再向前。" ), }, "废弃矿洞入口": { "event_key": "arrival::mine_entrance", "story_text": ( "矿洞入口被枯枝和碎石半堵着,腐朽的矿车轨道向黑暗深处延伸,铁锈和硫磺气味扑面而来。\n" "一具骷髅兵从废弃矿车后方立起,锈迹斑斑的武器指向你——矿洞里的东西不欢迎活人。" ), }, "溪边营地": { "event_key": "arrival::creek_camp", "story_text": ( "森林中一处难得的开阔地带,清澈的溪水从旁流过,树冠间的光在水面打出零碎金片。\n" "篝火余烬和被压平的草丛说明有人不久前在此扎营——这里适合短暂休息和搜寻遗留物资。" ), }, "精灵遗迹": { "event_key": "arrival::elf_ruins", "story_text": ( "石柱在藤蔓间若隐若现,精灵文字的刻痕随你的步伐明灭,像是感应到了来者的意图。\n" "一个穿着褪色绿袍的消瘦身影从石柱阴影里转出,用警惕的眼神审视着你——遗迹有它的守护者。" ), }, "森林深处": { "event_key": "arrival::deep_forest", "story_text": ( "古树盘根错节,荧光苔藓将深处映成幽蓝,腐朽与魔力的气息混杂难辨。\n" "远处传来低沉咆哮,树影间有成双眼睛移动——有什么东西已经感知到了你的闯入。" ), }, } @dataclass(slots=True) class BattleSnapshot: hp: int attack: int defense: int stamina: int hit_rate: float dodge_rate: float live_state_multiplier: float = 1.0 @property def power(self) -> float: return ( (self.attack * 0.6 + self.defense * 0.3 + self.stamina * 0.1) * self.live_state_multiplier ) BATTLE_ENCOUNTER_CONFIG: dict[tuple[str, str], dict[str, Any]] = { ("黑暗森林入口", "哥布林"): { "enemy_snapshot": BattleSnapshot( hp=45, attack=8, defense=3, stamina=28, hit_rate=0.82, dodge_rate=0.08, live_state_multiplier=1.0, ), "defeated_flag": "encounter::dark_forest_gate_goblin_defeated", "reward_items": ["黑暗森林地图"], "quest_objectives": ["击败森林中的怪物"], }, ("山麓盗贼营", "盗贼斥候"): { "enemy_snapshot": BattleSnapshot( hp=52, attack=9, defense=4, stamina=30, hit_rate=0.84, dodge_rate=0.12, live_state_multiplier=1.0, ), "defeated_flag": "encounter::bandit_scout_defeated", "reward_items": ["山麓地图"], "quest_objectives": [], }, ("古塔废墟", "游荡幽灵"): { "enemy_snapshot": BattleSnapshot( hp=58, attack=10, defense=5, stamina=32, hit_rate=0.86, dodge_rate=0.14, live_state_multiplier=1.0, ), "defeated_flag": "encounter::ancient_tower_wraith_defeated", "reward_items": ["古塔地图"], "quest_objectives": [], }, ("废弃矿洞入口", "骷髅兵"): { "enemy_snapshot": BattleSnapshot( hp=38, attack=7, defense=4, stamina=22, hit_rate=0.75, dodge_rate=0.05, live_state_multiplier=1.0, ), "defeated_flag": "encounter::mine_skeleton_defeated", "reward_items": ["骷髅碎骨"], "quest_objectives": ["前往废弃矿洞调查"], }, ("黑暗森林入口", "野狼"): { "enemy_snapshot": BattleSnapshot( hp=58, attack=11, defense=5, stamina=36, hit_rate=0.82, dodge_rate=0.14, live_state_multiplier=1.0, ), "defeated_flag": "", "reward_items": [], "quest_objectives": [], }, ("森林深处", "森林巨魔"): { "enemy_snapshot": BattleSnapshot( hp=120, attack=15, defense=10, stamina=130, hit_rate=0.88, dodge_rate=0.1, live_state_multiplier=1.0, ), "defeated_flag": FOREST_TROLL_DEFEATED_FLAG, "reward_items": [], "quest_id": MAIN_QUEST_TROLL_ID, "quest_objectives": [FOREST_TROLL_BOSS_OBJECTIVE], }, } def action_time_cost_minutes(action_type: str) -> int: return ACTION_TIME_COSTS.get(str(action_type or "").upper(), 10) def resolve_battle( player: BattleSnapshot, enemy: BattleSnapshot, *, player_unarmed: bool = False, ) -> dict[str, Any]: enemy_power = max(enemy.power, 1.0) player_power = player.power if player_unarmed and player.attack <= enemy.defense: player_power *= 0.5 ratio = player_power / enemy_power if ratio < 0.6: outcome = "forced_retreat" elif ratio < 1.0: outcome = "pyrrhic_win" elif ratio < 1.5: outcome = "normal_win" else: outcome = "dominant_win" return { "player_power": round(player_power, 2), "enemy_power": round(enemy_power, 2), "ratio": round(ratio, 3), "outcome": outcome, } def resolve_trade( game_state, *, merchant_name: str, item_name: str, confirm: bool, ) -> dict[str, Any]: npc = game_state.world.npcs.get(merchant_name) if npc is None or not npc.can_trade: return {"applied": False, "reason": "invalid_merchant"} if npc.location != game_state.player.location: return {"applied": False, "reason": "merchant_not_here"} if item_name not in npc.shop_inventory: return {"applied": False, "reason": "item_not_sold_here"} item_info = game_state.world.item_registry.get(item_name) if item_info is None: return {"applied": False, "reason": "unknown_item"} if not confirm: return { "applied": False, "reason": "awaiting_confirmation", "price": item_info.value, } if game_state.player.gold < item_info.value: return {"applied": False, "reason": "insufficient_gold", "price": item_info.value} game_state.player.gold -= item_info.value game_state.player.inventory.append(item_name) game_state.last_recent_gain = item_name follow_up_actions = build_contextual_actions(game_state, recent_gain=item_name) return { "applied": True, "reason": "purchased", "price": item_info.value, "item_name": item_name, "follow_up_actions": follow_up_actions, } def get_battle_encounter(location_name: str, enemy_name: str) -> dict[str, Any] | None: return BATTLE_ENCOUNTER_CONFIG.get((str(location_name), str(enemy_name))) def _has_status(game_state, keyword: str) -> bool: keyword = str(keyword or "") return any(keyword in effect.name for effect in game_state.player.status_effects) def _has_map(game_state) -> bool: owned_items = set(game_state.player.inventory) | { str(item) for item in game_state.player.equipment.values() if item } return any("地图" in item for item in owned_items) def _has_named_map(game_state, map_name: str) -> bool: owned_items = set(game_state.player.inventory) | { str(item) for item in game_state.player.equipment.values() if item } return str(map_name) in owned_items def _is_accessible_destination(game_state, destination: str) -> bool: target_location = game_state.world.locations.get(destination) if target_location is None: return False if target_location.is_accessible: return True required_item = str(target_location.required_item or "") owned_items = set(game_state.player.inventory) | { str(item) for item in game_state.player.equipment.values() if item } return bool(required_item) and required_item in owned_items def _is_route_visible(game_state, destination: str) -> bool: if destination == "河边渡口": return bool(game_state.world.global_flags.get(FERRY_ROUTE_UNLOCKED_FLAG)) return True def _is_forest_troll_hunt_active(game_state) -> bool: quest = game_state.world.quests.get(MAIN_QUEST_TROLL_ID) if quest is None or quest.status != "active": return False return not bool(game_state.world.global_flags.get(FOREST_TROLL_DEFEATED_FLAG)) def _is_main_quest_report_pending(game_state) -> bool: quest = game_state.world.quests.get(MAIN_QUEST_ID) if quest is None or quest.status != "active": return False if not game_state.world.global_flags.get(FOREST_TROLL_TRACKS_FOUND_FLAG): return False return not bool(quest.objectives.get(REPORT_TO_CHIEF_OBJECTIVE)) def _find_encounter_location(enemy_name: str) -> str | None: enemy_name = str(enemy_name or "") if not enemy_name: return None for (location_name, encounter_enemy), _config in BATTLE_ENCOUNTER_CONFIG.items(): if str(encounter_enemy) == enemy_name: return str(location_name) return None def _move_action( target: str, *, priority: int, text: str | None = None, preserve_text: bool = False, ) -> dict[str, Any]: action = _make_action( action_type="MOVE", target=target, text=text or f"前往{target}", priority=priority, ) if preserve_text: action["preserve_text"] = True return action def _make_action( *, action_type: str, text: str, target: Any = None, priority: int = 50, ) -> dict[str, Any]: return { "id": 0, "text": text, "action_type": action_type, "target": target, "priority": priority, } def _dedupe_actions(actions: list[dict[str, Any]]) -> list[dict[str, Any]]: deduped: list[dict[str, Any]] = [] seen: set[tuple[str, str]] = set() for action in sorted( actions, key=lambda item: int(item.get("priority", 0) or 0), reverse=True, ): normalized = dict(action) normalized.setdefault("priority", 0) key = ( str(normalized.get("action_type")), str(normalized.get("target")), ) if key in seen: continue seen.add(key) normalized["id"] = len(deduped) + 1 deduped.append(normalized) return deduped def _first_incomplete_objective(game_state): active_quests = [ quest for quest in game_state.world.quests.values() if quest.status == "active" ] active_quests.sort(key=lambda quest: (quest.quest_type != "main", quest.quest_id)) for quest in active_quests: for objective, completed in quest.objectives.items(): if not completed: return quest, objective return None, None def _extract_dialogue_target(objective: str) -> str | None: suffixes = ("对话", "交谈", "确认情报", "了解情况") text = str(objective or "") if not text.startswith("与"): return None candidate = text[1:] for separator in ("对话", "交谈"): if separator in candidate: candidate = candidate.split(separator, 1)[0] break else: for suffix in suffixes: if candidate.endswith(suffix): candidate = candidate[: -len(suffix)] break candidate = candidate.strip() return candidate or None def _extract_location_target(objective: str) -> str | None: text = str(objective or "") for prefix in ("前往",): if text.startswith(prefix): candidate = text[len(prefix):] for suffix in ("调查", "探索", "查看", "侦察"): if candidate.endswith(suffix): candidate = candidate[: -len(suffix)] break return candidate or None return None def _find_next_step(game_state, destination: str) -> str | None: if destination == game_state.player.location: return destination visited = {game_state.player.location} queue: deque[tuple[str, list[str]]] = deque([(game_state.player.location, [])]) while queue: current, path = queue.popleft() current_loc = game_state.world.locations.get(current) if current_loc is None: continue for neighbor in current_loc.connected_to: if neighbor in visited: continue visited.add(neighbor) new_path = path + [neighbor] if neighbor == destination: return new_path[0] queue.append((neighbor, new_path)) return None def build_village_chief_follow_up_actions(game_state) -> list[dict[str, Any]]: blacksmith_text = "前往村庄铁匠铺准备武器" path_text = None if _is_forest_troll_hunt_active(game_state): blacksmith_text = "前往村庄铁匠铺准备武器和防具" path_text = "沿村口小路赶赴森林深处" return _dedupe_actions( [ _make_action( action_type="VIEW_MAP", text="查看地图", priority=120, ), _move_action( "森林深处" if path_text else "村口小路", priority=116, text=path_text, preserve_text=bool(path_text), ), _move_action( "村庄杂货铺", priority=112, text="前往村庄杂货铺准备火把", preserve_text=True, ), _move_action( "村庄铁匠铺", priority=108, text=blacksmith_text, preserve_text=True, ), ] ) def build_map_actions(game_state) -> list[dict[str, Any]]: if not _has_map(game_state): return [] current_location = game_state.world.locations.get(game_state.player.location) if current_location is None: return [] special_route_order = { "村庄广场": ["村庄铁匠铺", "村庄旅店", "村口小路", "村庄杂货铺"], "黑暗森林入口": ["村口小路", "溪边营地", "森林深处"], "河边渡口": ["废弃矿洞入口", "山麓盗贼营", "村口小路"], "山麓盗贼营": ["精灵遗迹", "河边渡口"], "古塔废墟": ["村口小路"], } route_order = special_route_order.get( current_location.name, list(current_location.connected_to), ) actions: list[dict[str, Any]] = [] for index, destination in enumerate(route_order): # 不显示"前往当前场景"的无效选项 if destination == game_state.player.location: continue if destination not in current_location.connected_to: continue if not _is_accessible_destination(game_state, destination): continue required_map = LOCATION_MAP_REQUIREMENTS.get(destination) if required_map and not _has_named_map(game_state, required_map): continue if not _is_route_visible(game_state, destination): continue if ( destination == "森林深处" and game_state.world.global_flags.get(DEEP_FOREST_BARRIER_SEEN_FLAG) and "森林之钥" not in game_state.player.inventory ): continue actions.append( _move_action( destination, priority=120 - index * 4, ) ) return _dedupe_actions(actions) def build_shop_menu_actions(game_state, merchant_name: str) -> list[dict[str, Any]]: npc = game_state.world.npcs.get(merchant_name) if npc is None or not npc.can_trade: return [] actions: list[dict[str, Any]] = [] player_gold = int(getattr(game_state.player, "gold", 0)) for index, item_name in enumerate(npc.shop_inventory): item_info = game_state.world.item_registry.get(item_name) price = int(item_info.value) if item_info else 0 affordable = player_gold >= price if affordable: label = f"【可购买】购买{item_name}({price}金币)" priority = 130 - index * 4 else: label = f"【金币不足】购买{item_name}({price}金币,当前{player_gold})" priority = 90 - index * 2 actions.append( _make_action( action_type="TRADE", target={"merchant": merchant_name, "item": item_name, "confirm": False}, text=label, priority=priority, ) ) actions.append( _make_action( action_type="SCENE_OPTIONS", target=npc.location, text="暂不购买,先离开柜台", priority=60, ) ) return _dedupe_actions(actions) def build_scene_actions(game_state, location_name: str | None = None) -> list[dict[str, Any]]: current_name = str(location_name or game_state.player.location) actions: list[dict[str, Any]] = [] if current_name == "村庄广场": actions.append( _make_action( action_type="TALK", target="村长老伯", text="与村长老伯对话", priority=120, ) ) actions.append( _make_action( action_type="RUMOR", target={"source": "布告栏", "topic": "rumor_menu"}, text="查看布告栏上的异闻", priority=108, ) ) if _has_named_map(game_state, "村庄地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=104, ) ) actions.append(_move_action("村庄铁匠铺", priority=100)) actions.append(_move_action("村庄旅店", priority=96)) actions.append(_move_action("村口小路", priority=92)) actions.append(_move_action("村庄杂货铺", priority=88)) return _dedupe_actions(actions) if current_name == "村口小路": traveler_available = ( game_state.world.global_flags.get(TRAVELER_RUMOR_HEARD_FLAG) and game_state.world.npcs.get("神秘旅人") and game_state.world.npcs["神秘旅人"].location == "村口小路" ) if traveler_available: actions.append( _make_action( action_type="TALK", target="神秘旅人", text="与神秘旅人交谈", priority=120, ) ) if game_state.world.global_flags.get(FERRY_ROUTE_UNLOCKED_FLAG): actions.append(_move_action("河边渡口", priority=112)) if _has_named_map(game_state, "村庄地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=108, ) ) if _is_main_quest_report_pending(game_state): actions.append( _move_action( "村庄广场", priority=120, text="回村向村长汇报发现", preserve_text=True, ) ) else: actions.append(_move_action("村庄广场", priority=112)) if _has_named_map(game_state, "村庄地图"): actions.append(_move_action("黑暗森林入口", priority=104)) return _dedupe_actions(actions) if current_name == "村庄铁匠铺": actions.append( _make_action( action_type="TALK", target="铁匠格林", text="与铁匠格林对话", priority=120, ) ) if _has_named_map(game_state, "村庄地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=104, ) ) actions.append(_move_action("村庄广场", priority=96)) return _dedupe_actions(actions) if current_name == "村庄旅店": actions.append( _make_action( action_type="TALK", target="旅店老板娘莉娜", text="与旅店老板娘莉娜对话", priority=120, ) ) actions.append( _make_action( action_type="RUMOR", target={"source": "旅店老板娘莉娜", "topic": "rumor_menu"}, text="向莉娜打听最近的异状", priority=116, ) ) actions.append( _make_action( action_type="REST", text="在旅店休息片刻", priority=110, ) ) if _has_named_map(game_state, "村庄地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=104, ) ) actions.append(_move_action("村庄广场", priority=96)) return _dedupe_actions(actions) if current_name == "村庄杂货铺": actions.append( _make_action( action_type="TALK", target="杂货商人阿尔", text="与杂货商人阿尔对话", priority=120, ) ) if _has_named_map(game_state, "村庄地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=104, ) ) actions.append(_move_action("村庄广场", priority=96)) return _dedupe_actions(actions) if current_name == "黑暗森林入口": goblin_defeated = game_state.world.global_flags.get(FOREST_GOBLIN_DEFEATED_FLAG) tracks_found = game_state.world.global_flags.get(FOREST_TROLL_TRACKS_FOUND_FLAG) barrier_seen = game_state.world.global_flags.get(DEEP_FOREST_BARRIER_SEEN_FLAG) troll_hunt_active = _is_forest_troll_hunt_active(game_state) if not goblin_defeated: actions.append( _make_action( action_type="ATTACK", target="哥布林", text="与哥布林战斗", priority=120, ) ) else: if not tracks_found: actions.append( _make_action( action_type="EXPLORE", target="黑暗森林入口", text="调查哥布林留下的痕迹", priority=120, ) ) elif troll_hunt_active and "森林之钥" in game_state.player.inventory: actions.append( _move_action( "森林深处", priority=120, text="前往森林深处探索", preserve_text=True, ) ) elif not barrier_seen: actions.append(_move_action("森林深处", priority=120)) elif _is_main_quest_report_pending(game_state): actions.append( _move_action( "村口小路", priority=120, text="返回村庄向村长汇报", preserve_text=True, ) ) if _has_named_map(game_state, "黑暗森林地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=112 if tracks_found else 108, ) ) actions.append(_move_action("溪边营地", priority=104)) if tracks_found and not barrier_seen and "森林之钥" in game_state.player.inventory: actions.append( _move_action( "森林深处", priority=116, text="前往森林深处探索" if troll_hunt_active else None, preserve_text=troll_hunt_active, ) ) if _is_main_quest_report_pending(game_state) and not barrier_seen and not troll_hunt_active: actions.append( _move_action( "村口小路", priority=114, text="先返回村庄汇报情况", preserve_text=True, ) ) elif not barrier_seen or troll_hunt_active: actions.append(_move_action("村口小路", priority=96)) return _dedupe_actions(actions) if current_name == "河边渡口": actions.append( _make_action( action_type="TALK", target="渡口老渔夫", text="与渡口老渔夫对话", priority=120, ) ) if _has_named_map(game_state, "山麓地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=116, ) ) actions.append(_move_action("废弃矿洞入口", priority=112)) actions.append(_move_action("山麓盗贼营", priority=108)) actions.append(_move_action("村口小路", priority=104)) return _dedupe_actions(actions) if current_name == "山麓盗贼营": if not game_state.world.global_flags.get("encounter::bandit_scout_defeated"): actions.append( _make_action( action_type="ATTACK", target="盗贼斥候", text="与盗贼斥候战斗", priority=120, ) ) if _has_named_map(game_state, "山麓地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=112, ) ) actions.append(_move_action("河边渡口", priority=108)) return _dedupe_actions(actions) if current_name == "古塔废墟": if not game_state.world.global_flags.get("encounter::ancient_tower_wraith_defeated"): actions.append( _make_action( action_type="ATTACK", target="游荡幽灵", text="与游荡幽灵战斗", priority=120, ) ) if _has_named_map(game_state, "古塔地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=108, ) ) actions.append(_move_action("村口小路", priority=104)) return _dedupe_actions(actions) if current_name == "废弃矿洞入口": if not game_state.world.global_flags.get("encounter::mine_skeleton_defeated"): actions.append( _make_action( action_type="ATTACK", target="骷髅兵", text="与骷髅兵战斗", priority=120, ) ) else: actions.append( _make_action( action_type="EXPLORE", target="废弃矿洞入口", text="搜查矿洞入口的遗留痕迹", priority=120, ) ) if _has_named_map(game_state, "山麓地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=108, ) ) actions.append(_move_action("河边渡口", priority=104)) return _dedupe_actions(actions) if current_name == "溪边营地": actions.append( _make_action( action_type="REST", text="在营地休息恢复体力", priority=120, ) ) actions.append( _make_action( action_type="EXPLORE", target="溪边营地", text="搜寻营地遗留的物资线索", priority=112, ) ) if _has_named_map(game_state, "黑暗森林地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=104, ) ) actions.append(_move_action("黑暗森林入口", priority=96)) return _dedupe_actions(actions) if current_name == "精灵遗迹": actions.append( _make_action( action_type="TALK", target="遗迹守护者", text="与遗迹守护者对话", priority=120, ) ) if _has_named_map(game_state, "山麓地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=108, ) ) actions.append(_move_action("山麓盗贼营", priority=100)) return _dedupe_actions(actions) if current_name == "森林深处": if game_state.world.global_flags.get(FOREST_TROLL_HOARD_PENDING_FLAG): actions.append( _make_action( action_type="CLAIM_REWARD", target={"source": "forest_troll_hoard"}, text="确认拾取洞穴中的战利品", priority=124, ) ) elif ( _is_forest_troll_hunt_active(game_state) and game_state.world.global_flags.get(FOREST_TROLL_INTRO_SEEN_FLAG) ): actions.append( _make_action( action_type="ATTACK", target="森林巨魔", text="与森林巨魔战斗", priority=120, ) ) else: actions.append( _make_action( action_type="EXPLORE", target="森林深处", text="深入调查森林异变的根源", priority=120, ) ) if _has_named_map(game_state, "黑暗森林地图"): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=108, ) ) actions.append(_move_action("黑暗森林入口", priority=100)) return _dedupe_actions(actions) return build_adjacent_actions(game_state) def build_arrival_event(game_state, location_name: str) -> dict[str, Any] | None: config = ARRIVAL_EVENT_CONFIG.get(str(location_name)) if config is None: return None return { "event_key": config["event_key"], "story_text": config["story_text"], "options": build_scene_actions(game_state, location_name), } def build_goal_directed_actions(game_state) -> list[dict[str, Any]]: quest, objective = _first_incomplete_objective(game_state) if not quest or not objective: return [] actions: list[dict[str, Any]] = [] inventory = set(game_state.player.inventory) current_location = game_state.world.locations.get(game_state.player.location) dialogue_target = _extract_dialogue_target(objective) if dialogue_target: npc = game_state.world.npcs.get(dialogue_target) if npc is None: npc = next( ( candidate for candidate in game_state.world.npcs.values() if dialogue_target in candidate.name or candidate.name in objective ), None, ) if npc and npc.location == game_state.player.location: actions.append( _make_action( action_type="TALK", target=npc.name, text=f"与{npc.name}对话", priority=120, ) ) elif npc: next_step = _find_next_step(game_state, npc.location) if next_step: actions.append( _make_action( action_type="MOVE", target=next_step, text=f"前往{next_step}", priority=112, ) ) location_target = _extract_location_target(objective) if location_target: if any("地图" in item for item in inventory): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=110, ) ) if ( location_target == "森林深处" and _is_forest_troll_hunt_active(game_state) and "森林之钥" in inventory and game_state.player.location != "森林深处" ): actions.append( _make_action( action_type="MOVE", target="森林深处", text="赶赴森林深处", priority=105, ) ) else: next_step = _find_next_step(game_state, location_target) if next_step and next_step != game_state.player.location: actions.append( _make_action( action_type="MOVE", target=next_step, text=f"前往{next_step}", priority=105, ) ) if location_target == "黑暗森林入口" and game_state.player.location == "村庄广场": if "火把" not in inventory: actions.append( _make_action( action_type="MOVE", target="村庄杂货铺", text="前往村庄杂货铺准备火把", priority=104, ) ) if ( not game_state.player.equipment.get("weapon") and "铁剑" not in inventory and "短剑" not in inventory ): actions.append( _make_action( action_type="MOVE", target="村庄铁匠铺", text="前往村庄铁匠铺准备武器", priority=103, ) ) if "击败" in str(objective): enemy_name = str(objective).replace("击败", "").replace("森林中的", "").replace("矿洞中的", "").replace("的怪物", "").strip() encounter_location = _find_encounter_location(enemy_name) if enemy_name in {"怪物", "敌人"} and current_location: local_enemies = list(current_location.enemies or []) if local_enemies: enemy_name = local_enemies[0] encounter_location = _find_encounter_location(enemy_name) else: hunt_step = next( ( neighbor for neighbor in current_location.connected_to if game_state.world.locations.get(neighbor) and game_state.world.locations[neighbor].enemies ), None, ) if hunt_step: actions.append( _make_action( action_type="MOVE", target=hunt_step, text=f"前往{hunt_step}搜索怪物", priority=108, ) ) enemy_name = "" if enemy_name and encounter_location and encounter_location != game_state.player.location: next_step = _find_next_step(game_state, encounter_location) if next_step and next_step != game_state.player.location: actions.append( _make_action( action_type="MOVE", target=next_step, text=f"前往{next_step}", priority=108, ) ) elif enemy_name: actions.append( _make_action( action_type="ATTACK", target=enemy_name, text=f"与{enemy_name}战斗", priority=100, ) ) if "调查" in str(objective) or "找到" in str(objective): if ( str(objective) == FOREST_CAUSE_OBJECTIVE and game_state.player.location == "黑暗森林入口" and game_state.world.global_flags.get(FOREST_GOBLIN_DEFEATED_FLAG) ): actions.append( _make_action( action_type="EXPLORE", target="黑暗森林入口", text="调查哥布林留下的痕迹", priority=118, ) ) return _dedupe_actions(actions) actions.append( _make_action( action_type="EXPLORE", target=game_state.player.location, text=f"围绕“{objective}”继续调查", priority=92, ) ) return _dedupe_actions(actions) def build_adjacent_actions(game_state) -> list[dict[str, Any]]: current_location = game_state.world.locations.get(game_state.player.location) if current_location is None: return [] actions: list[dict[str, Any]] = [] owned_items = set(game_state.player.inventory) | { str(item) for item in game_state.player.equipment.values() if item } for neighbor in current_location.connected_to: # 不显示"前往当前场景"的无效选项 if neighbor == game_state.player.location: continue target_location = game_state.world.locations.get(neighbor) if target_location is None: continue if not target_location.is_accessible: required_item = str(target_location.required_item or "") if not required_item or required_item not in owned_items: continue actions.append( _make_action( action_type="MOVE", target=neighbor, text=f"前往{neighbor}", priority=78 if not target_location.is_discovered else 72, ) ) for npc_name in current_location.npcs_present: if npc_name not in game_state.world.npcs: continue actions.append( _make_action( action_type="TALK", target=npc_name, text=f"与{npc_name}对话", priority=68, ) ) return _dedupe_actions(actions) def merge_demo_options( base_options: list[dict[str, Any]], *extra_option_groups: list[dict[str, Any]], limit: int = 3, ) -> list[dict[str, Any]]: merged: list[dict[str, Any]] = [option for option in base_options if isinstance(option, dict)] for group in extra_option_groups: merged.extend(group) deduped = _dedupe_actions(merged) return deduped[:limit] def build_contextual_actions( game_state, *, recent_gain: str | None = None, ) -> list[dict[str, Any]]: actions: list[dict[str, Any]] = [] inventory = set(game_state.player.inventory) light_level = str(game_state.world.light_level) location = game_state.world.locations.get(game_state.player.location) time_of_day = str(game_state.world.time_of_day) if recent_gain: item_info = game_state.world.item_registry.get(recent_gain) if item_info and item_info.item_type in {"weapon", "armor", "accessory"}: slot = "weapon" if item_info.item_type == "weapon" else "armor" if game_state.player.equipment.get(slot) != recent_gain: actions.append( _make_action( action_type="EQUIP", target=recent_gain, text=f"装备{recent_gain}", priority=100, ) ) if "地图" in str(recent_gain): actions.append( _make_action( action_type="VIEW_MAP", text="查看地图", priority=95, ) ) in_dark_area = ( light_level in {"黑暗", "昏暗", "幽暗", "漆黑"} or (location is not None and location.location_type == "dungeon") or ( location is not None and location.location_type in {"wilderness", "special"} and time_of_day in {"夜晚", "深夜"} ) ) if "火把" in inventory and in_dark_area and not _has_status(game_state, "火把"): actions.append( _make_action( action_type="USE_ITEM", target="火把", text="使用火把照明", priority=90, ) ) if game_state.player.hp < max(1, game_state.player.max_hp // 2): for potion_name in ("小型治疗药水", "治疗药水"): if potion_name in inventory: actions.append( _make_action( action_type="USE_ITEM", target=potion_name, text=f"使用{potion_name}", priority=85, ) ) break if game_state.player.hunger < 50: for food_name in ("面包", "烤肉", "麦酒", "草药包"): if food_name in inventory: actions.append( _make_action( action_type="USE_ITEM", target=food_name, text=f"食用{food_name}", priority=80, ) ) break if ( game_state.player.location in OVERNIGHT_REST_LOCATIONS and hasattr(game_state, "can_overnight_rest") and game_state.can_overnight_rest() ): actions.append( _make_action( action_type="OVERNIGHT_REST", text="在此处过夜", priority=88, ) ) return _dedupe_actions(actions)