Spaces:
Runtime error
Runtime error
PPP commited on
Commit ·
ed566a9
1
Parent(s): 67cdcb8
feat: add environment UI and tighten trade, state, and action-time rules
Browse files- app.py +107 -21
- state_manager.py +614 -260
- story_engine.py +217 -44
app.py
CHANGED
|
@@ -66,6 +66,7 @@ def _build_state_snapshot(gs: GameState) -> dict:
|
|
| 66 |
active_quests = []
|
| 67 |
effective_stats = gs.get_effective_player_stats()
|
| 68 |
equipment_bonuses = gs.get_equipment_stat_bonuses()
|
|
|
|
| 69 |
for quest in gs.world.quests.values():
|
| 70 |
if quest.status == "active":
|
| 71 |
active_quests.append(
|
|
@@ -85,6 +86,8 @@ def _build_state_snapshot(gs: GameState) -> dict:
|
|
| 85 |
"day": gs.world.day_count,
|
| 86 |
"time_of_day": gs.world.time_of_day,
|
| 87 |
"weather": gs.world.weather,
|
|
|
|
|
|
|
| 88 |
"player": {
|
| 89 |
"name": gs.player.name,
|
| 90 |
"level": gs.player.level,
|
|
@@ -816,6 +819,8 @@ def _format_status_panel(gs: GameState) -> str:
|
|
| 816 |
w = gs.world
|
| 817 |
effective_stats = gs.get_effective_player_stats()
|
| 818 |
equipment_bonuses = gs.get_equipment_stat_bonuses()
|
|
|
|
|
|
|
| 819 |
|
| 820 |
# 属性进度条
|
| 821 |
hp_bar = _progress_bar(p.hp, p.max_hp, "HP")
|
|
@@ -843,6 +848,13 @@ def _format_status_panel(gs: GameState) -> str:
|
|
| 843 |
if bonus_value < 0:
|
| 844 |
return f"{label}: {effective_value} <span style='color:#b44;'>({bonus_value} 装备)</span>"
|
| 845 |
return f"{label}: {base_value}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
|
| 847 |
# 状态效果
|
| 848 |
if p.status_effects:
|
|
@@ -855,8 +867,65 @@ def _format_status_panel(gs: GameState) -> str:
|
|
| 855 |
# 背包
|
| 856 |
if p.inventory:
|
| 857 |
inventory_text = "<br>".join(p.inventory)
|
| 858 |
-
else:
|
| 859 |
-
inventory_text = "空"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 860 |
|
| 861 |
# 活跃任务(完整展示:描述、子目标、奖励、来源)
|
| 862 |
active_quests = [q for q in w.quests.values() if q.status == "active"]
|
|
@@ -949,30 +1018,47 @@ def _format_status_panel(gs: GameState) -> str:
|
|
| 949 |
</span>
|
| 950 |
</div>
|
| 951 |
|
| 952 |
-
<div>
|
| 953 |
-
<h4 style="margin:4px 0 2px 0;">🎒 背包</h4>
|
| 954 |
-
<span style="font-size:0.85em;">
|
| 955 |
-
{inventory_text}
|
| 956 |
-
</span>
|
| 957 |
-
</div>
|
| 958 |
-
|
| 959 |
-
<div style="grid-column: 1 / -1;">
|
| 960 |
-
<h4 style="margin:4px 0 2px 0;">
|
| 961 |
-
<
|
| 962 |
-
{
|
| 963 |
-
|
| 964 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
|
| 966 |
<div>
|
| 967 |
<h4 style="margin:4px 0 2px 0;">🌍 世界信息</h4>
|
| 968 |
<span style="font-size:0.85em;">
|
| 969 |
位置: {w.current_scene}<br>
|
| 970 |
-
时间: 第{w.day_count}天 {w.time_of_day}<br>
|
| 971 |
-
天气: {w.weather}<br>
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
</
|
|
|
|
| 976 |
|
| 977 |
</div>
|
| 978 |
</div>"""
|
|
|
|
| 66 |
active_quests = []
|
| 67 |
effective_stats = gs.get_effective_player_stats()
|
| 68 |
equipment_bonuses = gs.get_equipment_stat_bonuses()
|
| 69 |
+
environment_snapshot = gs.get_environment_snapshot(limit=3)
|
| 70 |
for quest in gs.world.quests.values():
|
| 71 |
if quest.status == "active":
|
| 72 |
active_quests.append(
|
|
|
|
| 86 |
"day": gs.world.day_count,
|
| 87 |
"time_of_day": gs.world.time_of_day,
|
| 88 |
"weather": gs.world.weather,
|
| 89 |
+
"light_level": gs.world.light_level,
|
| 90 |
+
"environment": _json_safe(environment_snapshot),
|
| 91 |
"player": {
|
| 92 |
"name": gs.player.name,
|
| 93 |
"level": gs.player.level,
|
|
|
|
| 819 |
w = gs.world
|
| 820 |
effective_stats = gs.get_effective_player_stats()
|
| 821 |
equipment_bonuses = gs.get_equipment_stat_bonuses()
|
| 822 |
+
env_snapshot = gs.get_environment_snapshot(limit=3)
|
| 823 |
+
scene_summary = gs.get_scene_summary().replace("\n", "<br>")
|
| 824 |
|
| 825 |
# 属性进度条
|
| 826 |
hp_bar = _progress_bar(p.hp, p.max_hp, "HP")
|
|
|
|
| 848 |
if bonus_value < 0:
|
| 849 |
return f"{label}: {effective_value} <span style='color:#b44;'>({bonus_value} 装备)</span>"
|
| 850 |
return f"{label}: {base_value}"
|
| 851 |
+
|
| 852 |
+
def badge(text: str, bg: str, fg: str = "#1f2937") -> str:
|
| 853 |
+
return (
|
| 854 |
+
f"<span style='display:inline-block;margin:0 6px 6px 0;padding:3px 10px;"
|
| 855 |
+
f"border-radius:999px;background:{bg};color:{fg};font-size:0.8em;"
|
| 856 |
+
f"font-weight:600;'>{text}</span>"
|
| 857 |
+
)
|
| 858 |
|
| 859 |
# 状态效果
|
| 860 |
if p.status_effects:
|
|
|
|
| 867 |
# 背包
|
| 868 |
if p.inventory:
|
| 869 |
inventory_text = "<br>".join(p.inventory)
|
| 870 |
+
else:
|
| 871 |
+
inventory_text = "空"
|
| 872 |
+
|
| 873 |
+
weather_colors = {
|
| 874 |
+
"晴朗": "#fef3c7",
|
| 875 |
+
"多云": "#e5e7eb",
|
| 876 |
+
"小雨": "#dbeafe",
|
| 877 |
+
"浓雾": "#e0e7ff",
|
| 878 |
+
"暴风雨": "#c7d2fe",
|
| 879 |
+
"大雪": "#f3f4f6",
|
| 880 |
+
}
|
| 881 |
+
light_colors = {
|
| 882 |
+
"明亮": "#fde68a",
|
| 883 |
+
"柔和": "#fcd34d",
|
| 884 |
+
"昏暗": "#cbd5e1",
|
| 885 |
+
"幽暗": "#94a3b8",
|
| 886 |
+
"漆黑": "#334155",
|
| 887 |
+
}
|
| 888 |
+
danger_level = int(env_snapshot.get("danger_level", 0))
|
| 889 |
+
if danger_level >= 7:
|
| 890 |
+
danger_badge = badge(f"危险 {danger_level}/10", "#fecaca", "#7f1d1d")
|
| 891 |
+
elif danger_level >= 4:
|
| 892 |
+
danger_badge = badge(f"危险 {danger_level}/10", "#fed7aa", "#9a3412")
|
| 893 |
+
else:
|
| 894 |
+
danger_badge = badge(f"危险 {danger_level}/10", "#dcfce7", "#166534")
|
| 895 |
+
|
| 896 |
+
env_badges = "".join(
|
| 897 |
+
[
|
| 898 |
+
badge(f"天气 {w.weather}", weather_colors.get(w.weather, "#e5e7eb")),
|
| 899 |
+
badge(
|
| 900 |
+
f"光照 {w.light_level}",
|
| 901 |
+
light_colors.get(w.light_level, "#e5e7eb"),
|
| 902 |
+
"#0f172a" if w.light_level not in {"幽暗", "漆黑"} else "#f8fafc",
|
| 903 |
+
),
|
| 904 |
+
danger_badge,
|
| 905 |
+
badge(f"场景 {env_snapshot.get('location_type', 'unknown')}", "#ede9fe", "#4c1d95"),
|
| 906 |
+
]
|
| 907 |
+
)
|
| 908 |
+
|
| 909 |
+
recent_env_events = env_snapshot.get("recent_events", [])
|
| 910 |
+
if recent_env_events:
|
| 911 |
+
latest_event = recent_env_events[-1]
|
| 912 |
+
latest_event_html = (
|
| 913 |
+
f"<div style='padding:8px 10px;border-radius:10px;background:#f8fafc;"
|
| 914 |
+
f"border:1px solid #dbeafe;margin-bottom:6px;'>"
|
| 915 |
+
f"<b>{latest_event.get('title', '环境事件')}</b>"
|
| 916 |
+
f"<br><span style='font-size:0.82em;color:#475569;'>{latest_event.get('description', '')}</span>"
|
| 917 |
+
f"</div>"
|
| 918 |
+
)
|
| 919 |
+
recent_event_lines = "<br>".join(
|
| 920 |
+
f"- {event.get('title', '环境事件')}"
|
| 921 |
+
for event in reversed(recent_env_events[-3:])
|
| 922 |
+
)
|
| 923 |
+
else:
|
| 924 |
+
latest_event_html = (
|
| 925 |
+
"<div style='padding:8px 10px;border-radius:10px;background:#f8fafc;"
|
| 926 |
+
"border:1px dashed #cbd5e1;color:#64748b;'>本回合暂无显式环境事件</div>"
|
| 927 |
+
)
|
| 928 |
+
recent_event_lines = "无"
|
| 929 |
|
| 930 |
# 活跃任务(完整展示:描述、子目标、奖励、来源)
|
| 931 |
active_quests = [q for q in w.quests.values() if q.status == "active"]
|
|
|
|
| 1018 |
</span>
|
| 1019 |
</div>
|
| 1020 |
|
| 1021 |
+
<div>
|
| 1022 |
+
<h4 style="margin:4px 0 2px 0;">🎒 背包</h4>
|
| 1023 |
+
<span style="font-size:0.85em;">
|
| 1024 |
+
{inventory_text}
|
| 1025 |
+
</span>
|
| 1026 |
+
</div>
|
| 1027 |
+
|
| 1028 |
+
<div style="grid-column: 1 / -1;">
|
| 1029 |
+
<h4 style="margin:4px 0 2px 0;">🌦️ 环境态势</h4>
|
| 1030 |
+
<div style="font-size:0.85em;">
|
| 1031 |
+
{env_badges}
|
| 1032 |
+
{latest_event_html}
|
| 1033 |
+
<span style="color:#475569;">最���环境事件: {recent_event_lines}</span>
|
| 1034 |
+
</div>
|
| 1035 |
+
</div>
|
| 1036 |
+
|
| 1037 |
+
<div style="grid-column: 1 / -1;">
|
| 1038 |
+
<h4 style="margin:4px 0 2px 0;">🧭 场景摘要</h4>
|
| 1039 |
+
<div style="font-size:0.85em;line-height:1.5;padding:8px 10px;border-radius:10px;background:#fff7ed;border:1px solid #fed7aa;">
|
| 1040 |
+
{scene_summary}
|
| 1041 |
+
</div>
|
| 1042 |
+
</div>
|
| 1043 |
+
|
| 1044 |
+
<div style="grid-column: 1 / -1;">
|
| 1045 |
+
<h4 style="margin:4px 0 2px 0;">📜 任务</h4>
|
| 1046 |
+
<span style="font-size:0.85em;">
|
| 1047 |
+
{quest_text}
|
| 1048 |
+
</span>
|
| 1049 |
+
</div>
|
| 1050 |
|
| 1051 |
<div>
|
| 1052 |
<h4 style="margin:4px 0 2px 0;">🌍 世界信息</h4>
|
| 1053 |
<span style="font-size:0.85em;">
|
| 1054 |
位置: {w.current_scene}<br>
|
| 1055 |
+
时间: 第{w.day_count}天 {w.time_of_day}<br>
|
| 1056 |
+
天气: {w.weather}<br>
|
| 1057 |
+
光照: {w.light_level}<br>
|
| 1058 |
+
季节: {w.season}<br>
|
| 1059 |
+
回合: {gs.turn}
|
| 1060 |
+
</span>
|
| 1061 |
+
</div>
|
| 1062 |
|
| 1063 |
</div>
|
| 1064 |
</div>"""
|
state_manager.py
CHANGED
|
@@ -16,11 +16,12 @@ state_manager.py - StoryWeaver 状态管理器
|
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
-
import copy
|
| 20 |
-
import logging
|
| 21 |
-
import
|
| 22 |
-
|
| 23 |
-
from
|
|
|
|
| 24 |
|
| 25 |
from utils import clamp
|
| 26 |
|
|
@@ -288,7 +289,7 @@ class PlayerState(BaseModel):
|
|
| 288 |
# ============================================================
|
| 289 |
|
| 290 |
|
| 291 |
-
class WorldState(BaseModel):
|
| 292 |
"""
|
| 293 |
世界状态容器
|
| 294 |
|
|
@@ -299,11 +300,13 @@ class WorldState(BaseModel):
|
|
| 299 |
- rumors / active_threats 丰富 NPC 对话内容
|
| 300 |
- faction_relations 支持阵营间动态关系
|
| 301 |
"""
|
| 302 |
-
current_scene: str = "村庄广场" # 当前场景名称
|
| 303 |
-
time_of_day: str = "清晨" # 清晨 / 上午 / 正午 / 下午 / 黄昏 / 夜晚 / 深夜
|
| 304 |
-
day_count: int = 1 # 当前天数
|
| 305 |
-
weather: str = "晴朗" # 晴朗 / 多云 / 小雨 / 暴风雨 / 大雪 / 浓雾
|
| 306 |
-
|
|
|
|
|
|
|
| 307 |
|
| 308 |
# --- 地图 ---
|
| 309 |
locations: dict[str, LocationInfo] = Field(default_factory=dict)
|
|
@@ -319,11 +322,12 @@ class WorldState(BaseModel):
|
|
| 319 |
item_registry: dict[str, ItemInfo] = Field(default_factory=dict)
|
| 320 |
|
| 321 |
# --- 全局标记 ---
|
| 322 |
-
global_flags: dict[str, bool] = Field(default_factory=dict)
|
| 323 |
-
world_events: list[str] = Field(default_factory=list)
|
| 324 |
-
# 已发生的全局事件
|
| 325 |
-
|
| 326 |
-
|
|
|
|
| 327 |
rumors: list[str] = Field(default_factory=list)
|
| 328 |
# 流传的传闻
|
| 329 |
faction_relations: dict[str, dict[str, str]] = Field(default_factory=dict)
|
|
@@ -335,7 +339,7 @@ class WorldState(BaseModel):
|
|
| 335 |
# ============================================================
|
| 336 |
|
| 337 |
|
| 338 |
-
class GameEvent(BaseModel):
|
| 339 |
"""
|
| 340 |
事件日志模型(一致性维护的关键)
|
| 341 |
|
|
@@ -357,9 +361,27 @@ class GameEvent(BaseModel):
|
|
| 357 |
state_changes: dict = Field(default_factory=dict)
|
| 358 |
# 状态变更快照
|
| 359 |
player_action: str = "" # 触发该事件的玩家操作
|
| 360 |
-
consequence_tags: list[str] = Field(default_factory=list)
|
| 361 |
-
# 后果标签
|
| 362 |
-
is_reversible: bool = True # 是否可逆
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
|
| 365 |
# ============================================================
|
|
@@ -383,11 +405,11 @@ class GameState:
|
|
| 383 |
- check_consistency() 在生成前检测可能的矛盾
|
| 384 |
"""
|
| 385 |
|
| 386 |
-
def __init__(self, player_name: str = "旅人"):
|
| 387 |
-
"""初始化游戏状态,创建默认的起
|
| 388 |
-
self.player = PlayerState(name=player_name)
|
| 389 |
-
self.world = WorldState()
|
| 390 |
-
self.event_log: list[GameEvent] = []
|
| 391 |
self.turn: int = 0
|
| 392 |
self.game_mode: str = "exploration" # exploration / combat / dialogue / cutscene / game_over
|
| 393 |
self.difficulty: str = "normal" # easy / normal / hard
|
|
@@ -395,9 +417,11 @@ class GameState:
|
|
| 395 |
self.ending_flags: dict[str, bool] = {} # 结局条件追踪
|
| 396 |
self.combat_log: list[str] = [] # 最近战斗记录
|
| 397 |
self.achievement_list: list[str] = [] # 已解锁成就
|
| 398 |
-
|
| 399 |
-
# 初始化起始世界
|
| 400 |
-
self._init_starting_world()
|
|
|
|
|
|
|
| 401 |
|
| 402 |
def _init_starting_world(self):
|
| 403 |
"""
|
|
@@ -590,7 +614,7 @@ class GameState:
|
|
| 590 |
race="矮人",
|
| 591 |
occupation="铁匠",
|
| 592 |
can_trade=True,
|
| 593 |
-
shop_inventory=["铁剑", "皮甲", "木盾"],
|
| 594 |
relationship_level=0,
|
| 595 |
schedule={"清晨": "村庄铁匠铺", "正午": "村庄铁匠铺", "夜晚": "村庄旅店"},
|
| 596 |
backstory="曾经是王都的御用铁匠,因一场变故隐居此地。对远方的怪物有独到的了解。",
|
|
@@ -756,7 +780,9 @@ class GameState:
|
|
| 756 |
|
| 757 |
# --- 初始物品注册表 ---
|
| 758 |
self.world.item_registry = {
|
| 759 |
-
"
|
|
|
|
|
|
|
| 760 |
"皮甲": ItemInfo(name="皮甲", item_type="armor", description="硬化皮革制成的轻甲,兼顾防护与灵活性。", rarity="common", stat_bonus={"defense": 3}, value=25),
|
| 761 |
"木盾": ItemInfo(name="木盾", item_type="armor", description="坚硬橡木制成的盾牌,可以抵挡基础攻击。", rarity="common", stat_bonus={"defense": 2}, value=15),
|
| 762 |
"面包": ItemInfo(name="面包", item_type="consumable", description="新鲜烤制的面包,香气扑鼻。", usable=True, use_effect="恢复 10 饱食度", value=5),
|
|
@@ -791,21 +817,71 @@ class GameState:
|
|
| 791 |
"有人在精灵遗迹附近见到过一个穿绿袍的身影,不知是人是鬼。",
|
| 792 |
]
|
| 793 |
|
| 794 |
-
# ---
|
| 795 |
-
self.
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 809 |
|
| 810 |
# ============================================================
|
| 811 |
# 核心方法
|
|
@@ -846,28 +922,31 @@ class GameState:
|
|
| 846 |
changes = _filtered
|
| 847 |
|
| 848 |
# --- 玩家属性变更 ---
|
| 849 |
-
if "hp_change" in changes:
|
| 850 |
-
old_hp = self.player.hp
|
| 851 |
-
self.player.hp = clamp(
|
| 852 |
-
self.player.hp + int(changes["hp_change"]),
|
| 853 |
-
0,
|
| 854 |
-
self.player.max_hp,
|
| 855 |
-
)
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
self.player.mp
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
|
|
|
|
|
|
|
|
|
| 871 |
|
| 872 |
if "exp_change" in changes:
|
| 873 |
old_exp = self.player.experience
|
|
@@ -878,48 +957,53 @@ class GameState:
|
|
| 878 |
self._level_up()
|
| 879 |
change_log.append(f"升级!当前等级: {self.player.level}")
|
| 880 |
|
| 881 |
-
if "morale_change" in changes:
|
| 882 |
-
old_morale = self.player.morale
|
| 883 |
-
self.player.morale = clamp(
|
| 884 |
-
self.player.morale + int(changes["morale_change"]),
|
| 885 |
-
0, 100,
|
| 886 |
-
)
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
self.player.sanity
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
if new_loc not in
|
| 919 |
-
self.
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
|
| 924 |
# --- 物品变更 ---
|
| 925 |
# 货币关键词列表:这些物品不进背包,而是直接转换为金币
|
|
@@ -1004,33 +1088,42 @@ class GameState:
|
|
| 1004 |
change_log.append(f"移除状态: {name}")
|
| 1005 |
|
| 1006 |
# --- NPC 相关变更 ---
|
| 1007 |
-
if "npc_changes" in changes:
|
| 1008 |
-
for npc_name, npc_data in changes["npc_changes"].items():
|
| 1009 |
-
if npc_name in self.world.npcs:
|
| 1010 |
-
npc = self.world.npcs[npc_name]
|
| 1011 |
-
if "attitude" in npc_data:
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
if npc.
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
|
| 1035 |
# --- 任务变更 ---
|
| 1036 |
if "quest_updates" in changes:
|
|
@@ -1054,30 +1147,45 @@ class GameState:
|
|
| 1054 |
status_cn = _QUEST_STATUS_CN.get(quest.status, quest.status)
|
| 1055 |
change_log.append(f"任务【{quest.title}】进展至\"{status_cn}\"")
|
| 1056 |
|
| 1057 |
-
# --- 世界状态变更 ---
|
| 1058 |
-
if "weather_change" in changes:
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
|
| 1082 |
# --- 装备变更 ---
|
| 1083 |
if "equip" in changes:
|
|
@@ -1298,16 +1406,19 @@ class GameState:
|
|
| 1298 |
if self.world.rumors:
|
| 1299 |
rumors_desc = "\n【流传的传闻】\n" + "\n".join(f" - {r}" for r in self.world.rumors[-3:])
|
| 1300 |
|
| 1301 |
-
# 8.
|
| 1302 |
-
|
| 1303 |
-
if
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
f"
|
| 1307 |
-
f"
|
| 1308 |
-
f"
|
| 1309 |
-
|
| 1310 |
-
|
|
|
|
|
|
|
|
|
|
| 1311 |
|
| 1312 |
# 9. 一致性约束指令
|
| 1313 |
consistency_rules = (
|
|
@@ -1339,13 +1450,13 @@ class GameState:
|
|
| 1339 |
scene_desc,
|
| 1340 |
player_desc,
|
| 1341 |
npc_desc,
|
| 1342 |
-
quest_desc,
|
| 1343 |
-
move_desc,
|
| 1344 |
-
event_desc,
|
| 1345 |
-
rumors_desc,
|
| 1346 |
-
|
| 1347 |
-
consistency_rules,
|
| 1348 |
-
])
|
| 1349 |
|
| 1350 |
return full_prompt
|
| 1351 |
|
|
@@ -1419,20 +1530,21 @@ class GameState:
|
|
| 1419 |
f"矛盾: 物品 '{item}' 不是消耗品,使用后不应消失。请将其从 items_lost 中移除。"
|
| 1420 |
)
|
| 1421 |
|
| 1422 |
-
# 检测3: 位置移动是否合法
|
| 1423 |
-
if "new_location" in proposed_changes:
|
| 1424 |
-
target = str(proposed_changes["new_location"])
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
-
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
if target_loc
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
|
|
|
| 1436 |
|
| 1437 |
# 检测4: 金币是否足够(如果是消费操作)
|
| 1438 |
if "gold_change" in proposed_changes:
|
|
@@ -1589,64 +1701,296 @@ class GameState:
|
|
| 1589 |
return True
|
| 1590 |
return False
|
| 1591 |
|
| 1592 |
-
def tick_time(self) -> list[str]:
|
| 1593 |
-
"""
|
| 1594 |
-
推进游戏时间
|
| 1595 |
-
|
| 1596 |
-
设计思路:
|
| 1597 |
-
- 回合数递增
|
| 1598 |
-
- 时间
|
| 1599 |
-
-
|
| 1600 |
-
-
|
| 1601 |
-
-
|
| 1602 |
-
-
|
| 1603 |
-
|
| 1604 |
-
|
| 1605 |
-
|
| 1606 |
-
|
| 1607 |
-
|
| 1608 |
-
|
| 1609 |
-
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
|
| 1615 |
-
|
| 1616 |
-
|
| 1617 |
-
|
| 1618 |
-
|
| 1619 |
-
|
| 1620 |
-
|
| 1621 |
-
|
| 1622 |
-
|
| 1623 |
-
|
| 1624 |
-
|
| 1625 |
-
|
| 1626 |
-
|
| 1627 |
-
|
| 1628 |
-
|
| 1629 |
-
|
| 1630 |
-
|
| 1631 |
-
|
| 1632 |
-
|
| 1633 |
-
|
| 1634 |
-
|
| 1635 |
-
|
| 1636 |
-
|
| 1637 |
-
|
| 1638 |
-
self.
|
| 1639 |
-
|
| 1640 |
-
|
| 1641 |
-
|
| 1642 |
-
|
| 1643 |
-
|
| 1644 |
-
|
| 1645 |
-
|
| 1646 |
-
|
| 1647 |
-
|
| 1648 |
-
|
| 1649 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1650 |
|
| 1651 |
Returns:
|
| 1652 |
effect_log: 状态效果结算引起的变化描述列表
|
|
@@ -1709,17 +2053,8 @@ class GameState:
|
|
| 1709 |
|
| 1710 |
return effect_log
|
| 1711 |
|
| 1712 |
-
def
|
| 1713 |
-
"""
|
| 1714 |
-
import random
|
| 1715 |
-
self.pending_random_event = None
|
| 1716 |
-
if self.random_event_pool and random.random() < 0.25:
|
| 1717 |
-
event = random.choice(self.random_event_pool)
|
| 1718 |
-
self.pending_random_event = event
|
| 1719 |
-
logger.info(f"随机事件触发: {event}")
|
| 1720 |
-
|
| 1721 |
-
def _check_quest_deadlines(self):
|
| 1722 |
-
"""检查限时任务是否过期"""
|
| 1723 |
for quest in self.world.quests.values():
|
| 1724 |
if quest.status == "active" and quest.turns_remaining > 0:
|
| 1725 |
quest.turns_remaining -= 1
|
|
@@ -1920,3 +2255,22 @@ class GameState:
|
|
| 1920 |
if self.player.sanity < 100:
|
| 1921 |
filtered["sanity_change"] = base_recovery["sanity_change"]
|
| 1922 |
return filtered
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
+
import copy
|
| 20 |
+
import logging
|
| 21 |
+
import random
|
| 22 |
+
import re
|
| 23 |
+
from typing import Any, Optional
|
| 24 |
+
from pydantic import BaseModel, Field
|
| 25 |
|
| 26 |
from utils import clamp
|
| 27 |
|
|
|
|
| 289 |
# ============================================================
|
| 290 |
|
| 291 |
|
| 292 |
+
class WorldState(BaseModel):
|
| 293 |
"""
|
| 294 |
世界状态容器
|
| 295 |
|
|
|
|
| 300 |
- rumors / active_threats 丰富 NPC 对话内容
|
| 301 |
- faction_relations 支持阵营间动态关系
|
| 302 |
"""
|
| 303 |
+
current_scene: str = "村庄广场" # 当前场景名称
|
| 304 |
+
time_of_day: str = "清晨" # 清晨 / 上午 / 正午 / 下午 / 黄昏 / 夜晚 / 深夜
|
| 305 |
+
day_count: int = 1 # 当前天数
|
| 306 |
+
weather: str = "晴朗" # 晴朗 / 多云 / 小雨 / 暴风雨 / 大雪 / 浓雾
|
| 307 |
+
light_level: str = "明亮" # 明亮 / 柔和 / 昏暗 / 幽暗 / 漆黑
|
| 308 |
+
time_progress_units: int = 0 # 当前时段内累积的动作耗时点数
|
| 309 |
+
season: str = "春" # 春 / 夏 / 秋 / 冬
|
| 310 |
|
| 311 |
# --- 地图 ---
|
| 312 |
locations: dict[str, LocationInfo] = Field(default_factory=dict)
|
|
|
|
| 322 |
item_registry: dict[str, ItemInfo] = Field(default_factory=dict)
|
| 323 |
|
| 324 |
# --- 全局标记 ---
|
| 325 |
+
global_flags: dict[str, bool] = Field(default_factory=dict)
|
| 326 |
+
world_events: list[str] = Field(default_factory=list)
|
| 327 |
+
# 已发生的全局事件
|
| 328 |
+
recent_environment_events: list["EnvironmentEvent"] = Field(default_factory=list)
|
| 329 |
+
active_threats: list[str] = Field(default_factory=list)
|
| 330 |
+
# 当前全局威胁
|
| 331 |
rumors: list[str] = Field(default_factory=list)
|
| 332 |
# 流传的传闻
|
| 333 |
faction_relations: dict[str, dict[str, str]] = Field(default_factory=dict)
|
|
|
|
| 339 |
# ============================================================
|
| 340 |
|
| 341 |
|
| 342 |
+
class GameEvent(BaseModel):
|
| 343 |
"""
|
| 344 |
事件日志模型(一致性维护的关键)
|
| 345 |
|
|
|
|
| 361 |
state_changes: dict = Field(default_factory=dict)
|
| 362 |
# 状态变更快照
|
| 363 |
player_action: str = "" # 触发该事件的玩家操作
|
| 364 |
+
consequence_tags: list[str] = Field(default_factory=list)
|
| 365 |
+
# 后果标签
|
| 366 |
+
is_reversible: bool = True # 是否可逆
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
class EnvironmentEvent(BaseModel):
|
| 370 |
+
"""Structured environment event used by UI, logs, and prompt injection."""
|
| 371 |
+
event_id: str
|
| 372 |
+
category: str = "environment" # weather / light / environment
|
| 373 |
+
title: str = ""
|
| 374 |
+
description: str = ""
|
| 375 |
+
location: str = ""
|
| 376 |
+
time_of_day: str = ""
|
| 377 |
+
weather: str = ""
|
| 378 |
+
light_level: str = ""
|
| 379 |
+
severity: str = "low" # low / medium / high
|
| 380 |
+
state_changes: dict[str, Any] = Field(default_factory=dict)
|
| 381 |
+
prompt_hint: str = ""
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
WorldState.model_rebuild()
|
| 385 |
|
| 386 |
|
| 387 |
# ============================================================
|
|
|
|
| 405 |
- check_consistency() 在生成前检测可能的矛盾
|
| 406 |
"""
|
| 407 |
|
| 408 |
+
def __init__(self, player_name: str = "旅人"):
|
| 409 |
+
"""初始化游戏状态,创建默认的起��世界"""
|
| 410 |
+
self.player = PlayerState(name=player_name)
|
| 411 |
+
self.world = WorldState()
|
| 412 |
+
self.event_log: list[GameEvent] = []
|
| 413 |
self.turn: int = 0
|
| 414 |
self.game_mode: str = "exploration" # exploration / combat / dialogue / cutscene / game_over
|
| 415 |
self.difficulty: str = "normal" # easy / normal / hard
|
|
|
|
| 417 |
self.ending_flags: dict[str, bool] = {} # 结局条件追踪
|
| 418 |
self.combat_log: list[str] = [] # 最近战斗记录
|
| 419 |
self.achievement_list: list[str] = [] # 已解锁成就
|
| 420 |
+
|
| 421 |
+
# 初始化起始世界
|
| 422 |
+
self._init_starting_world()
|
| 423 |
+
self.pending_environment_event: EnvironmentEvent | None = None
|
| 424 |
+
self.world.light_level = self._determine_light_level()
|
| 425 |
|
| 426 |
def _init_starting_world(self):
|
| 427 |
"""
|
|
|
|
| 614 |
race="矮人",
|
| 615 |
occupation="铁匠",
|
| 616 |
can_trade=True,
|
| 617 |
+
shop_inventory=["小刀", "短剑", "铁剑", "皮甲", "木盾"],
|
| 618 |
relationship_level=0,
|
| 619 |
schedule={"清晨": "村庄铁匠铺", "正午": "村庄铁匠铺", "夜晚": "村庄旅店"},
|
| 620 |
backstory="曾经是王都的御用铁匠,因一场变故隐居此地。对远方的怪物有独到的了解。",
|
|
|
|
| 780 |
|
| 781 |
# --- 初始物品注册表 ---
|
| 782 |
self.world.item_registry = {
|
| 783 |
+
"小刀": ItemInfo(name="小刀", item_type="weapon", description="一把朴素但实用的小刀,便于近身防身。", rarity="common", stat_bonus={"attack": 2}, value=5),
|
| 784 |
+
"短剑": ItemInfo(name="短剑", item_type="weapon", description="一把适合新手携带的短剑,轻便易用。", rarity="common", stat_bonus={"attack": 3}, value=10),
|
| 785 |
+
"铁剑": ItemInfo(name="铁剑", item_type="weapon", description="一把标准的铁制长剑,刀锋锐利。", rarity="common", stat_bonus={"attack": 5}, value=30),
|
| 786 |
"皮甲": ItemInfo(name="皮甲", item_type="armor", description="硬化皮革制成的轻甲,兼顾防护与灵活性。", rarity="common", stat_bonus={"defense": 3}, value=25),
|
| 787 |
"木盾": ItemInfo(name="木盾", item_type="armor", description="坚硬橡木制成的盾牌,可以抵挡基础攻击。", rarity="common", stat_bonus={"defense": 2}, value=15),
|
| 788 |
"面包": ItemInfo(name="面包", item_type="consumable", description="新鲜烤制的面包,香气扑鼻。", usable=True, use_effect="恢复 10 饱食度", value=5),
|
|
|
|
| 817 |
"有人在精灵遗迹附近见到过一个穿绿袍的身影,不知是人是鬼。",
|
| 818 |
]
|
| 819 |
|
| 820 |
+
# --- 显式环境事件模板 ---
|
| 821 |
+
self.environment_event_pool: list[dict[str, Any]] = [
|
| 822 |
+
{
|
| 823 |
+
"event_id": "lanterns_dim",
|
| 824 |
+
"category": "light",
|
| 825 |
+
"title": "灯火忽暗",
|
| 826 |
+
"description": "屋内的灯火突然暗了一截,墙角的影子被拉得细长。",
|
| 827 |
+
"location_types": ["town", "shop"],
|
| 828 |
+
"time_slots": ["黄昏", "夜晚", "深夜"],
|
| 829 |
+
"severity": "medium",
|
| 830 |
+
"state_changes": {"sanity_change": -2},
|
| 831 |
+
"prompt_hint": "环境光线明显变暗,叙事里要体现角色对阴影和氛围变化的反应。",
|
| 832 |
+
},
|
| 833 |
+
{
|
| 834 |
+
"event_id": "cold_gust",
|
| 835 |
+
"category": "environment",
|
| 836 |
+
"title": "冷风穿林",
|
| 837 |
+
"description": "一阵带着湿意的冷风从林间掠过,让人本能地绷紧肩背。",
|
| 838 |
+
"location_types": ["wilderness", "dungeon"],
|
| 839 |
+
"time_slots": ["下午", "黄昏", "夜晚", "深夜"],
|
| 840 |
+
"severity": "low",
|
| 841 |
+
"state_changes": {"morale_change": -3},
|
| 842 |
+
"prompt_hint": "风声和体感温度都发生了变化,适合让玩家察觉环境压力正在上升。",
|
| 843 |
+
},
|
| 844 |
+
{
|
| 845 |
+
"event_id": "forest_rustle",
|
| 846 |
+
"category": "environment",
|
| 847 |
+
"title": "林影骚动",
|
| 848 |
+
"description": "黑暗树影间传来急促的窸窣声,仿佛刚有什么东西贴着边缘掠过。",
|
| 849 |
+
"location_types": ["wilderness", "dungeon"],
|
| 850 |
+
"time_slots": ["黄昏", "夜晚", "深夜"],
|
| 851 |
+
"min_danger": 3,
|
| 852 |
+
"severity": "medium",
|
| 853 |
+
"state_changes": {"sanity_change": -3},
|
| 854 |
+
"prompt_hint": "这是偏悬疑的环境扰动,至少一个后续选项应允许玩家追查或回避。",
|
| 855 |
+
},
|
| 856 |
+
{
|
| 857 |
+
"event_id": "fireplace_relief",
|
| 858 |
+
"category": "environment",
|
| 859 |
+
"title": "火光回暖",
|
| 860 |
+
"description": "炉火和热气驱散了紧绷感,让呼吸也慢慢平稳下来。",
|
| 861 |
+
"location_types": ["shop", "town"],
|
| 862 |
+
"requires_rest_available": True,
|
| 863 |
+
"time_slots": ["黄昏", "夜晚", "深夜"],
|
| 864 |
+
"severity": "low",
|
| 865 |
+
"state_changes": {"morale_change": 4, "sanity_change": 2},
|
| 866 |
+
"prompt_hint": "这是偏正向的氛围事件,叙事里可以体现安全感和短暂放松。",
|
| 867 |
+
},
|
| 868 |
+
{
|
| 869 |
+
"event_id": "fog_pressures_in",
|
| 870 |
+
"category": "environment",
|
| 871 |
+
"title": "雾气压近",
|
| 872 |
+
"description": "潮湿的雾从地面漫上来,视野被一点点吞没,声音也变得含混。",
|
| 873 |
+
"location_types": ["wilderness", "dungeon"],
|
| 874 |
+
"time_slots": ["清晨", "黄昏", "夜晚", "深夜"],
|
| 875 |
+
"weathers": ["浓雾", "小雨"],
|
| 876 |
+
"severity": "medium",
|
| 877 |
+
"state_changes": {"sanity_change": -2},
|
| 878 |
+
"prompt_hint": "视野受限且不安感上升,叙事里应弱化远景、强调近距离感官细节。",
|
| 879 |
+
},
|
| 880 |
+
]
|
| 881 |
+
|
| 882 |
+
# --- 玩家初始装备 ---
|
| 883 |
+
self.player.inventory = ["面包", "面包", "小型治疗药水"]
|
| 884 |
+
self.player.location = self.world.current_scene
|
| 885 |
|
| 886 |
# ============================================================
|
| 887 |
# 核心方法
|
|
|
|
| 922 |
changes = _filtered
|
| 923 |
|
| 924 |
# --- 玩家属性变更 ---
|
| 925 |
+
if "hp_change" in changes:
|
| 926 |
+
old_hp = self.player.hp
|
| 927 |
+
self.player.hp = clamp(
|
| 928 |
+
self.player.hp + int(changes["hp_change"]),
|
| 929 |
+
0,
|
| 930 |
+
self.player.max_hp,
|
| 931 |
+
)
|
| 932 |
+
if self.player.hp != old_hp:
|
| 933 |
+
change_log.append(f"HP: {old_hp} → {self.player.hp}")
|
| 934 |
+
|
| 935 |
+
if "mp_change" in changes:
|
| 936 |
+
old_mp = self.player.mp
|
| 937 |
+
self.player.mp = clamp(
|
| 938 |
+
self.player.mp + int(changes["mp_change"]),
|
| 939 |
+
0,
|
| 940 |
+
self.player.max_mp,
|
| 941 |
+
)
|
| 942 |
+
if self.player.mp != old_mp:
|
| 943 |
+
change_log.append(f"MP: {old_mp} → {self.player.mp}")
|
| 944 |
+
|
| 945 |
+
if "gold_change" in changes:
|
| 946 |
+
old_gold = self.player.gold
|
| 947 |
+
self.player.gold = max(0, self.player.gold + int(changes["gold_change"]))
|
| 948 |
+
if self.player.gold != old_gold:
|
| 949 |
+
change_log.append(f"金币: {old_gold} → {self.player.gold}")
|
| 950 |
|
| 951 |
if "exp_change" in changes:
|
| 952 |
old_exp = self.player.experience
|
|
|
|
| 957 |
self._level_up()
|
| 958 |
change_log.append(f"升级!当前等级: {self.player.level}")
|
| 959 |
|
| 960 |
+
if "morale_change" in changes:
|
| 961 |
+
old_morale = self.player.morale
|
| 962 |
+
self.player.morale = clamp(
|
| 963 |
+
self.player.morale + int(changes["morale_change"]),
|
| 964 |
+
0, 100,
|
| 965 |
+
)
|
| 966 |
+
if self.player.morale != old_morale:
|
| 967 |
+
change_log.append(f"士气: {old_morale} → {self.player.morale}")
|
| 968 |
+
|
| 969 |
+
if "sanity_change" in changes:
|
| 970 |
+
old_sanity = self.player.sanity
|
| 971 |
+
self.player.sanity = clamp(
|
| 972 |
+
self.player.sanity + int(changes["sanity_change"]),
|
| 973 |
+
0, 100,
|
| 974 |
+
)
|
| 975 |
+
if self.player.sanity != old_sanity:
|
| 976 |
+
change_log.append(f"理智: {old_sanity} → {self.player.sanity}")
|
| 977 |
+
|
| 978 |
+
if "hunger_change" in changes:
|
| 979 |
+
old_hunger = self.player.hunger
|
| 980 |
+
self.player.hunger = clamp(
|
| 981 |
+
self.player.hunger + int(changes["hunger_change"]),
|
| 982 |
+
0, 100,
|
| 983 |
+
)
|
| 984 |
+
if self.player.hunger != old_hunger:
|
| 985 |
+
change_log.append(f"饱食度: {old_hunger} → {self.player.hunger}")
|
| 986 |
+
|
| 987 |
+
if "karma_change" in changes:
|
| 988 |
+
old_karma = self.player.karma
|
| 989 |
+
self.player.karma += int(changes["karma_change"])
|
| 990 |
+
if self.player.karma != old_karma:
|
| 991 |
+
change_log.append(f"善恶值: {old_karma} → {self.player.karma}")
|
| 992 |
+
|
| 993 |
+
# --- 位置变更 ---
|
| 994 |
+
if "new_location" in changes:
|
| 995 |
+
old_loc = self.player.location
|
| 996 |
+
new_loc = str(changes["new_location"])
|
| 997 |
+
if new_loc.strip().lower() not in ("", "none", "null") and new_loc != old_loc:
|
| 998 |
+
self.player.location = new_loc
|
| 999 |
+
self.world.current_scene = new_loc
|
| 1000 |
+
change_log.append(f"位置: {old_loc} → {new_loc}")
|
| 1001 |
+
# 发现新地点
|
| 1002 |
+
if new_loc not in self.world.discovered_locations:
|
| 1003 |
+
self.world.discovered_locations.append(new_loc)
|
| 1004 |
+
change_log.append(f"发现新地点: {new_loc}")
|
| 1005 |
+
if new_loc in self.world.locations:
|
| 1006 |
+
self.world.locations[new_loc].is_discovered = True
|
| 1007 |
|
| 1008 |
# --- 物品变更 ---
|
| 1009 |
# 货币关键词列表:这些物品不进背包,而是直接转换为金币
|
|
|
|
| 1088 |
change_log.append(f"移除状态: {name}")
|
| 1089 |
|
| 1090 |
# --- NPC 相关变更 ---
|
| 1091 |
+
if "npc_changes" in changes:
|
| 1092 |
+
for npc_name, npc_data in changes["npc_changes"].items():
|
| 1093 |
+
if npc_name in self.world.npcs:
|
| 1094 |
+
npc = self.world.npcs[npc_name]
|
| 1095 |
+
if "attitude" in npc_data:
|
| 1096 |
+
new_attitude = str(npc_data["attitude"])
|
| 1097 |
+
if npc.attitude != new_attitude:
|
| 1098 |
+
npc.attitude = new_attitude
|
| 1099 |
+
change_log.append(f"NPC {npc_name} 态度变为: {npc.attitude}")
|
| 1100 |
+
if "is_alive" in npc_data:
|
| 1101 |
+
new_is_alive = bool(npc_data["is_alive"])
|
| 1102 |
+
was_alive = npc.is_alive
|
| 1103 |
+
if npc.is_alive != new_is_alive:
|
| 1104 |
+
npc.is_alive = new_is_alive
|
| 1105 |
+
if was_alive and not npc.is_alive:
|
| 1106 |
+
change_log.append(f"NPC {npc_name} 已死亡")
|
| 1107 |
+
if "relationship_change" in npc_data:
|
| 1108 |
+
old_rel = npc.relationship_level
|
| 1109 |
+
npc.relationship_level = clamp(
|
| 1110 |
+
npc.relationship_level + int(npc_data["relationship_change"]),
|
| 1111 |
+
-100, 100,
|
| 1112 |
+
)
|
| 1113 |
+
if npc.relationship_level != old_rel:
|
| 1114 |
+
change_log.append(
|
| 1115 |
+
f"NPC {npc_name} 好感度: {old_rel} → {npc.relationship_level}"
|
| 1116 |
+
)
|
| 1117 |
+
if "hp_change" in npc_data:
|
| 1118 |
+
old_hp = npc.hp
|
| 1119 |
+
npc.hp = max(0, npc.hp + int(npc_data["hp_change"]))
|
| 1120 |
+
if npc.hp <= 0:
|
| 1121 |
+
npc.is_alive = False
|
| 1122 |
+
change_log.append(f"NPC {npc_name} 被击败")
|
| 1123 |
+
elif npc.hp != old_hp:
|
| 1124 |
+
change_log.append(f"NPC {npc_name} HP: {old_hp} → {npc.hp}")
|
| 1125 |
+
if "memory_add" in npc_data:
|
| 1126 |
+
npc.memory.append(str(npc_data["memory_add"]))
|
| 1127 |
|
| 1128 |
# --- 任务变更 ---
|
| 1129 |
if "quest_updates" in changes:
|
|
|
|
| 1147 |
status_cn = _QUEST_STATUS_CN.get(quest.status, quest.status)
|
| 1148 |
change_log.append(f"任务【{quest.title}】进展至\"{status_cn}\"")
|
| 1149 |
|
| 1150 |
+
# --- 世界状态变更 ---
|
| 1151 |
+
if "weather_change" in changes:
|
| 1152 |
+
valid_weathers = {"晴朗", "多云", "小雨", "暴风雨", "大雪", "浓雾"}
|
| 1153 |
+
new_weather = str(changes["weather_change"])
|
| 1154 |
+
if new_weather in valid_weathers:
|
| 1155 |
+
if self.world.weather != new_weather:
|
| 1156 |
+
self.world.weather = new_weather
|
| 1157 |
+
change_log.append(f"天气变为: {self.world.weather}")
|
| 1158 |
+
else:
|
| 1159 |
+
logger.warning(f"无效的 weather_change 值 '{new_weather}',已忽略。")
|
| 1160 |
+
|
| 1161 |
+
if "time_change" in changes:
|
| 1162 |
+
valid_times = ["清晨", "上午", "正午", "下午", "黄昏", "夜晚", "深夜"]
|
| 1163 |
+
new_time = str(changes["time_change"])
|
| 1164 |
+
if new_time in valid_times:
|
| 1165 |
+
old_time = self.world.time_of_day
|
| 1166 |
+
if new_time != old_time:
|
| 1167 |
+
self.world.time_of_day = new_time
|
| 1168 |
+
change_log.append(f"时间流逝: {old_time} → {self.world.time_of_day}")
|
| 1169 |
+
else:
|
| 1170 |
+
logger.warning(f"无效的 time_change 值 '{new_time}',已忽略。合法值: {valid_times}")
|
| 1171 |
+
|
| 1172 |
+
if "weather_change" in changes or "time_change" in changes:
|
| 1173 |
+
old_light = self.world.light_level
|
| 1174 |
+
self.world.light_level = self._determine_light_level()
|
| 1175 |
+
if old_light != self.world.light_level:
|
| 1176 |
+
change_log.append(f"光照变化: {old_light} → {self.world.light_level}")
|
| 1177 |
+
|
| 1178 |
+
if "global_flags_set" in changes:
|
| 1179 |
+
for flag, value in changes["global_flags_set"].items():
|
| 1180 |
+
self.world.global_flags[flag] = bool(value)
|
| 1181 |
+
# 全局标记仅内部使用,不展示给用户
|
| 1182 |
+
logger.info(f"全局标记设置: {flag} = {value}")
|
| 1183 |
+
|
| 1184 |
+
if "world_event" in changes:
|
| 1185 |
+
world_event = str(changes["world_event"])
|
| 1186 |
+
if not self.world.world_events or self.world.world_events[-1] != world_event:
|
| 1187 |
+
self.world.world_events.append(world_event)
|
| 1188 |
+
change_log.append(f"世界事件: {world_event}")
|
| 1189 |
|
| 1190 |
# --- 装备变更 ---
|
| 1191 |
if "equip" in changes:
|
|
|
|
| 1406 |
if self.world.rumors:
|
| 1407 |
rumors_desc = "\n【流传的传闻】\n" + "\n".join(f" - {r}" for r in self.world.rumors[-3:])
|
| 1408 |
|
| 1409 |
+
# 8. 显式环境事件
|
| 1410 |
+
environment_event_desc = ""
|
| 1411 |
+
if self.pending_environment_event:
|
| 1412 |
+
env = self.pending_environment_event
|
| 1413 |
+
environment_event_desc = (
|
| 1414 |
+
f"\n【本回合环境事件 —— 必须融入本次叙事】\n"
|
| 1415 |
+
f"[{env.category.upper()}|{env.severity}] {env.title}\n"
|
| 1416 |
+
f"{env.description}\n"
|
| 1417 |
+
f"{env.prompt_hint}\n"
|
| 1418 |
+
f"请将此事件自然地融入剧情描写中,作为本回合可感知的环境变化。"
|
| 1419 |
+
f"玩家可以选择回应、调查、规避或忽视它。至少一个选项应与此事件相关。"
|
| 1420 |
+
)
|
| 1421 |
+
self.pending_environment_event = None # 用后清除
|
| 1422 |
|
| 1423 |
# 9. 一致性约束指令
|
| 1424 |
consistency_rules = (
|
|
|
|
| 1450 |
scene_desc,
|
| 1451 |
player_desc,
|
| 1452 |
npc_desc,
|
| 1453 |
+
quest_desc,
|
| 1454 |
+
move_desc,
|
| 1455 |
+
event_desc,
|
| 1456 |
+
rumors_desc,
|
| 1457 |
+
environment_event_desc,
|
| 1458 |
+
consistency_rules,
|
| 1459 |
+
])
|
| 1460 |
|
| 1461 |
return full_prompt
|
| 1462 |
|
|
|
|
| 1530 |
f"矛盾: 物品 '{item}' 不是消耗品,使用后不应消失。请将其从 items_lost 中移除。"
|
| 1531 |
)
|
| 1532 |
|
| 1533 |
+
# 检测3: 位置移动是否合法
|
| 1534 |
+
if "new_location" in proposed_changes:
|
| 1535 |
+
target = str(proposed_changes["new_location"])
|
| 1536 |
+
if target.strip().lower() not in ("", "none", "null") and target != self.player.location:
|
| 1537 |
+
current_loc = self.world.locations.get(self.player.location)
|
| 1538 |
+
if current_loc and target not in current_loc.connected_to:
|
| 1539 |
+
contradictions.append(
|
| 1540 |
+
f"矛盾: 试图移动到不相邻的地点 '{target}'(当前位置: {self.player.location})"
|
| 1541 |
+
)
|
| 1542 |
+
target_loc = self.world.locations.get(target)
|
| 1543 |
+
if target_loc and not target_loc.is_accessible:
|
| 1544 |
+
if target_loc.required_item and target_loc.required_item not in self.player.inventory:
|
| 1545 |
+
contradictions.append(
|
| 1546 |
+
f"矛盾: 地点 '{target}' 被锁定,需要 '{target_loc.required_item}'"
|
| 1547 |
+
)
|
| 1548 |
|
| 1549 |
# 检测4: 金币是否足够(如果是消费操作)
|
| 1550 |
if "gold_change" in proposed_changes:
|
|
|
|
| 1701 |
return True
|
| 1702 |
return False
|
| 1703 |
|
| 1704 |
+
def tick_time(self, player_intent: Optional[dict] = None) -> list[str]:
|
| 1705 |
+
"""
|
| 1706 |
+
按动作消耗推进游戏时间。
|
| 1707 |
+
|
| 1708 |
+
设计思路:
|
| 1709 |
+
- 回合数递增
|
| 1710 |
+
- 不同动作消耗不同的时间点数
|
| 1711 |
+
- 累积点数达到阈值时,时间段按固定顺序轮转
|
| 1712 |
+
- 每过一个完整日夜循环,天数+1
|
| 1713 |
+
- 自动减少饱食度,模拟饥饿机制
|
| 1714 |
+
- 结算状态效果持续时间
|
| 1715 |
+
- 检查限时任务
|
| 1716 |
+
|
| 1717 |
+
Returns:
|
| 1718 |
+
tick_log: 本回合时间流逝引起的状态变化描述列表
|
| 1719 |
+
"""
|
| 1720 |
+
tick_log: list[str] = []
|
| 1721 |
+
self.turn += 1
|
| 1722 |
+
time_threshold = 2
|
| 1723 |
+
action_units = self._estimate_time_cost_units(player_intent)
|
| 1724 |
+
self.world.time_progress_units += action_units
|
| 1725 |
+
elapsed_segments = self.world.time_progress_units // time_threshold
|
| 1726 |
+
self.world.time_progress_units = self.world.time_progress_units % time_threshold
|
| 1727 |
+
|
| 1728 |
+
if elapsed_segments <= 0:
|
| 1729 |
+
return tick_log
|
| 1730 |
+
|
| 1731 |
+
time_sequence = ["清晨", "上午", "正午", "下午", "黄昏", "夜晚", "深夜"]
|
| 1732 |
+
for _ in range(elapsed_segments):
|
| 1733 |
+
current_idx = (
|
| 1734 |
+
time_sequence.index(self.world.time_of_day)
|
| 1735 |
+
if self.world.time_of_day in time_sequence
|
| 1736 |
+
else 0
|
| 1737 |
+
)
|
| 1738 |
+
next_idx = (current_idx + 1) % len(time_sequence)
|
| 1739 |
+
old_time = self.world.time_of_day
|
| 1740 |
+
self.world.time_of_day = time_sequence[next_idx]
|
| 1741 |
+
tick_log.append(f"时间流逝: {old_time} → {self.world.time_of_day}")
|
| 1742 |
+
|
| 1743 |
+
if next_idx == 0:
|
| 1744 |
+
self.world.day_count += 1
|
| 1745 |
+
tick_log.append(f"新的一天!第 {self.world.day_count} 天")
|
| 1746 |
+
logger.info(f"新的一天开始了!第 {self.world.day_count} 天")
|
| 1747 |
+
|
| 1748 |
+
old_hunger = self.player.hunger
|
| 1749 |
+
self.player.hunger = max(0, self.player.hunger - (3 * elapsed_segments))
|
| 1750 |
+
if old_hunger != self.player.hunger:
|
| 1751 |
+
tick_log.append(f"饱食度自然衰减: {old_hunger} → {self.player.hunger}")
|
| 1752 |
+
if self.player.hunger <= 0:
|
| 1753 |
+
tick_log.append("极度饥饿!属性受到惩罚")
|
| 1754 |
+
logger.info("玩家非常饥饿,属性受到惩罚")
|
| 1755 |
+
|
| 1756 |
+
for _ in range(elapsed_segments):
|
| 1757 |
+
effect_log = self._apply_status_effects()
|
| 1758 |
+
tick_log.extend(effect_log)
|
| 1759 |
+
|
| 1760 |
+
# 检查限时任务
|
| 1761 |
+
self._check_quest_deadlines()
|
| 1762 |
+
|
| 1763 |
+
# 更新 NPC 位置(根据时间表)
|
| 1764 |
+
self._update_npc_schedules()
|
| 1765 |
+
|
| 1766 |
+
# 显式环境系统:光照 / 天气 / 环境扰动
|
| 1767 |
+
self._update_environment_cycle(tick_log)
|
| 1768 |
+
|
| 1769 |
+
return tick_log
|
| 1770 |
+
|
| 1771 |
+
def _estimate_time_cost_units(self, player_intent: Optional[dict] = None) -> int:
|
| 1772 |
+
"""Estimate how much in-world time a player action should consume."""
|
| 1773 |
+
if not isinstance(player_intent, dict):
|
| 1774 |
+
return 2
|
| 1775 |
+
|
| 1776 |
+
intent = str(player_intent.get("intent", "")).upper()
|
| 1777 |
+
raw_input = str(player_intent.get("raw_input", "") or "")
|
| 1778 |
+
details = str(player_intent.get("details", "") or "")
|
| 1779 |
+
target = str(player_intent.get("target", "") or "")
|
| 1780 |
+
full_text = f"{raw_input} {details} {target}"
|
| 1781 |
+
|
| 1782 |
+
base_costs = {
|
| 1783 |
+
"TALK": 0,
|
| 1784 |
+
"TRADE": 1,
|
| 1785 |
+
"USE_ITEM": 1,
|
| 1786 |
+
"EQUIP": 1,
|
| 1787 |
+
"SKILL": 1,
|
| 1788 |
+
"MOVE": 2,
|
| 1789 |
+
"EXPLORE": 2,
|
| 1790 |
+
"QUEST": 2,
|
| 1791 |
+
"ATTACK": 2,
|
| 1792 |
+
"REST": 4,
|
| 1793 |
+
"WAIT": 4,
|
| 1794 |
+
}
|
| 1795 |
+
cost = base_costs.get(intent, 1)
|
| 1796 |
+
|
| 1797 |
+
lightweight_markers = (
|
| 1798 |
+
"检查",
|
| 1799 |
+
"查看",
|
| 1800 |
+
"确认",
|
| 1801 |
+
"翻看",
|
| 1802 |
+
"端详",
|
| 1803 |
+
"研究",
|
| 1804 |
+
"询问",
|
| 1805 |
+
"交谈",
|
| 1806 |
+
"背包",
|
| 1807 |
+
"物品",
|
| 1808 |
+
"装备",
|
| 1809 |
+
"地图",
|
| 1810 |
+
)
|
| 1811 |
+
if intent in {"EXPLORE", "QUEST"} and any(marker in full_text for marker in lightweight_markers):
|
| 1812 |
+
return 0
|
| 1813 |
+
|
| 1814 |
+
return max(0, cost)
|
| 1815 |
+
|
| 1816 |
+
def _determine_light_level(self) -> str:
|
| 1817 |
+
"""Derive current light level from time of day and weather."""
|
| 1818 |
+
base_levels = {
|
| 1819 |
+
"清晨": "柔和",
|
| 1820 |
+
"上午": "明亮",
|
| 1821 |
+
"正午": "明亮",
|
| 1822 |
+
"下午": "柔和",
|
| 1823 |
+
"黄昏": "昏暗",
|
| 1824 |
+
"夜晚": "幽暗",
|
| 1825 |
+
"深夜": "漆黑",
|
| 1826 |
+
}
|
| 1827 |
+
ordered_levels = ["明亮", "柔和", "昏暗", "幽暗", "漆黑"]
|
| 1828 |
+
weather_penalty = {
|
| 1829 |
+
"晴朗": 0,
|
| 1830 |
+
"多云": 0,
|
| 1831 |
+
"小雨": 1,
|
| 1832 |
+
"大雪": 1,
|
| 1833 |
+
"浓雾": 1,
|
| 1834 |
+
"暴风雨": 2,
|
| 1835 |
+
}
|
| 1836 |
+
|
| 1837 |
+
base_level = base_levels.get(self.world.time_of_day, "柔和")
|
| 1838 |
+
current_index = ordered_levels.index(base_level)
|
| 1839 |
+
darker_by = weather_penalty.get(self.world.weather, 0)
|
| 1840 |
+
next_index = min(len(ordered_levels) - 1, current_index + darker_by)
|
| 1841 |
+
return ordered_levels[next_index]
|
| 1842 |
+
|
| 1843 |
+
def _update_environment_cycle(self, tick_log: list[str]):
|
| 1844 |
+
"""Advance light/weather and roll explicit environment events."""
|
| 1845 |
+
self.pending_environment_event = None
|
| 1846 |
+
self._update_light_level_event(tick_log)
|
| 1847 |
+
self._maybe_shift_weather(tick_log)
|
| 1848 |
+
self._update_light_level_event(tick_log)
|
| 1849 |
+
self._roll_environment_event(tick_log)
|
| 1850 |
+
|
| 1851 |
+
def _update_light_level_event(self, tick_log: list[str]):
|
| 1852 |
+
old_light = self.world.light_level
|
| 1853 |
+
new_light = self._determine_light_level()
|
| 1854 |
+
if new_light == old_light:
|
| 1855 |
+
return
|
| 1856 |
+
|
| 1857 |
+
self.world.light_level = new_light
|
| 1858 |
+
event = EnvironmentEvent(
|
| 1859 |
+
event_id=f"light-{self.turn}",
|
| 1860 |
+
category="light",
|
| 1861 |
+
title=f"光照转为{new_light}",
|
| 1862 |
+
description=f"随着时间与天气变化,周围环境现在呈现出{new_light}的光照状态。",
|
| 1863 |
+
location=self.player.location,
|
| 1864 |
+
time_of_day=self.world.time_of_day,
|
| 1865 |
+
weather=self.world.weather,
|
| 1866 |
+
light_level=new_light,
|
| 1867 |
+
severity="medium" if new_light in {"幽暗", "漆黑"} else "low",
|
| 1868 |
+
prompt_hint="请在叙事中体现能见度、阴影和角色主观感受的变化。",
|
| 1869 |
+
)
|
| 1870 |
+
self._register_environment_event(
|
| 1871 |
+
event,
|
| 1872 |
+
tick_log,
|
| 1873 |
+
inject_prompt=new_light in {"昏暗", "幽暗", "漆黑"},
|
| 1874 |
+
)
|
| 1875 |
+
|
| 1876 |
+
def _maybe_shift_weather(self, tick_log: list[str]):
|
| 1877 |
+
"""Occasionally shift weather to keep the environment dynamic."""
|
| 1878 |
+
chance = 0.18 if self.world.time_of_day in {"清晨", "黄昏"} else 0.08
|
| 1879 |
+
if random.random() >= chance:
|
| 1880 |
+
return
|
| 1881 |
+
|
| 1882 |
+
weather_transitions = {
|
| 1883 |
+
"晴朗": ["多云", "小雨"],
|
| 1884 |
+
"多云": ["晴朗", "���雨", "浓雾"],
|
| 1885 |
+
"小雨": ["多云", "暴风雨", "浓雾"],
|
| 1886 |
+
"浓雾": ["多云", "小雨", "晴朗"],
|
| 1887 |
+
"暴风雨": ["小雨", "多云"],
|
| 1888 |
+
"大雪": ["多云"],
|
| 1889 |
+
}
|
| 1890 |
+
next_candidates = weather_transitions.get(self.world.weather, ["晴朗", "多云"])
|
| 1891 |
+
new_weather = random.choice(next_candidates)
|
| 1892 |
+
if new_weather == self.world.weather:
|
| 1893 |
+
return
|
| 1894 |
+
|
| 1895 |
+
loc = self.world.locations.get(self.player.location)
|
| 1896 |
+
weather_effects: dict[str, Any] = {"weather_change": new_weather}
|
| 1897 |
+
if loc and loc.location_type in {"wilderness", "dungeon"}:
|
| 1898 |
+
if new_weather == "暴风雨":
|
| 1899 |
+
weather_effects.update({"morale_change": -3, "sanity_change": -2})
|
| 1900 |
+
elif new_weather == "浓雾":
|
| 1901 |
+
weather_effects.update({"sanity_change": -1})
|
| 1902 |
+
elif new_weather == "晴朗" and self.world.weather in {"小雨", "浓雾", "暴风雨"}:
|
| 1903 |
+
weather_effects.update({"morale_change": 2})
|
| 1904 |
+
|
| 1905 |
+
event = EnvironmentEvent(
|
| 1906 |
+
event_id=f"weather-{self.turn}",
|
| 1907 |
+
category="weather",
|
| 1908 |
+
title=f"天气转为{new_weather}",
|
| 1909 |
+
description=f"周围的天象正在变化,空气与视野都随着天气转向{new_weather}。",
|
| 1910 |
+
location=self.player.location,
|
| 1911 |
+
time_of_day=self.world.time_of_day,
|
| 1912 |
+
weather=new_weather,
|
| 1913 |
+
light_level=self.world.light_level,
|
| 1914 |
+
severity="medium" if new_weather in {"暴风雨", "浓雾"} else "low",
|
| 1915 |
+
state_changes=weather_effects,
|
| 1916 |
+
prompt_hint="请把天气变化作为当前回合的重要氛围来源,影响角色观察和选择。",
|
| 1917 |
+
)
|
| 1918 |
+
self._register_environment_event(event, tick_log, inject_prompt=True)
|
| 1919 |
+
self.world.light_level = self._determine_light_level()
|
| 1920 |
+
|
| 1921 |
+
def _roll_environment_event(self, tick_log: list[str]):
|
| 1922 |
+
"""Roll a structured environment event using explicit template filters."""
|
| 1923 |
+
loc = self.world.locations.get(self.player.location)
|
| 1924 |
+
if loc is None or not self.environment_event_pool:
|
| 1925 |
+
return
|
| 1926 |
+
|
| 1927 |
+
chance = 0.08
|
| 1928 |
+
if loc.danger_level >= 3:
|
| 1929 |
+
chance += 0.08
|
| 1930 |
+
if self.world.light_level in {"幽暗", "漆黑"}:
|
| 1931 |
+
chance += 0.06
|
| 1932 |
+
if self.world.weather in {"暴风雨", "浓雾"}:
|
| 1933 |
+
chance += 0.04
|
| 1934 |
+
if random.random() >= chance:
|
| 1935 |
+
return
|
| 1936 |
+
|
| 1937 |
+
candidates: list[dict[str, Any]] = []
|
| 1938 |
+
for template in self.environment_event_pool:
|
| 1939 |
+
if template.get("location_types") and loc.location_type not in template["location_types"]:
|
| 1940 |
+
continue
|
| 1941 |
+
if template.get("time_slots") and self.world.time_of_day not in template["time_slots"]:
|
| 1942 |
+
continue
|
| 1943 |
+
if template.get("weathers") and self.world.weather not in template["weathers"]:
|
| 1944 |
+
continue
|
| 1945 |
+
if template.get("requires_rest_available") and not loc.rest_available:
|
| 1946 |
+
continue
|
| 1947 |
+
if loc.danger_level < int(template.get("min_danger", 0)):
|
| 1948 |
+
continue
|
| 1949 |
+
candidates.append(template)
|
| 1950 |
+
|
| 1951 |
+
if not candidates:
|
| 1952 |
+
return
|
| 1953 |
+
|
| 1954 |
+
template = random.choice(candidates)
|
| 1955 |
+
event = EnvironmentEvent(
|
| 1956 |
+
event_id=f"{template['event_id']}-{self.turn}",
|
| 1957 |
+
category=str(template.get("category", "environment")),
|
| 1958 |
+
title=str(template.get("title", "环境异动")),
|
| 1959 |
+
description=str(template.get("description", "")),
|
| 1960 |
+
location=self.player.location,
|
| 1961 |
+
time_of_day=self.world.time_of_day,
|
| 1962 |
+
weather=self.world.weather,
|
| 1963 |
+
light_level=self.world.light_level,
|
| 1964 |
+
severity=str(template.get("severity", "low")),
|
| 1965 |
+
state_changes=copy.deepcopy(template.get("state_changes", {})),
|
| 1966 |
+
prompt_hint=str(template.get("prompt_hint", "")),
|
| 1967 |
+
)
|
| 1968 |
+
self._register_environment_event(event, tick_log, inject_prompt=True)
|
| 1969 |
+
|
| 1970 |
+
def _register_environment_event(
|
| 1971 |
+
self,
|
| 1972 |
+
event: EnvironmentEvent,
|
| 1973 |
+
tick_log: list[str],
|
| 1974 |
+
*,
|
| 1975 |
+
inject_prompt: bool,
|
| 1976 |
+
):
|
| 1977 |
+
"""Persist an environment event, optionally inject it into the next prompt, and apply effects."""
|
| 1978 |
+
self.world.recent_environment_events.append(event)
|
| 1979 |
+
self.world.recent_environment_events = self.world.recent_environment_events[-8:]
|
| 1980 |
+
if inject_prompt:
|
| 1981 |
+
self.pending_environment_event = event
|
| 1982 |
+
|
| 1983 |
+
if event.category == "light":
|
| 1984 |
+
tick_log.append(f"光照变化: {event.title}")
|
| 1985 |
+
|
| 1986 |
+
changes = copy.deepcopy(event.state_changes)
|
| 1987 |
+
changes.setdefault("world_event", event.title)
|
| 1988 |
+
change_log = self.apply_changes(changes)
|
| 1989 |
+
tick_log.extend(change_log)
|
| 1990 |
+
logger.info("环境事件触发: %s", event.title)
|
| 1991 |
+
|
| 1992 |
+
def _apply_status_effects(self) -> list[str]:
|
| 1993 |
+
"""每回合结算状态效果:应用修正、递减持续时间、移除过期效果
|
| 1994 |
|
| 1995 |
Returns:
|
| 1996 |
effect_log: 状态效果结算引起的变化描述列表
|
|
|
|
| 2053 |
|
| 2054 |
return effect_log
|
| 2055 |
|
| 2056 |
+
def _check_quest_deadlines(self):
|
| 2057 |
+
"""检查限时任务是否过期"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2058 |
for quest in self.world.quests.values():
|
| 2059 |
if quest.status == "active" and quest.turns_remaining > 0:
|
| 2060 |
quest.turns_remaining -= 1
|
|
|
|
| 2255 |
if self.player.sanity < 100:
|
| 2256 |
filtered["sanity_change"] = base_recovery["sanity_change"]
|
| 2257 |
return filtered
|
| 2258 |
+
|
| 2259 |
+
def get_environment_snapshot(self, limit: int = 3) -> dict[str, Any]:
|
| 2260 |
+
"""Return a compact environment summary for UI and logs."""
|
| 2261 |
+
loc = self.world.locations.get(self.player.location)
|
| 2262 |
+
recent_events = [
|
| 2263 |
+
event.model_dump()
|
| 2264 |
+
for event in self.world.recent_environment_events[-limit:]
|
| 2265 |
+
]
|
| 2266 |
+
return {
|
| 2267 |
+
"weather": self.world.weather,
|
| 2268 |
+
"light_level": self.world.light_level,
|
| 2269 |
+
"time_of_day": self.world.time_of_day,
|
| 2270 |
+
"season": self.world.season,
|
| 2271 |
+
"location_type": loc.location_type if loc else "unknown",
|
| 2272 |
+
"danger_level": loc.danger_level if loc else 0,
|
| 2273 |
+
"rest_available": bool(loc.rest_available) if loc else False,
|
| 2274 |
+
"shop_available": bool(loc.shop_available) if loc else False,
|
| 2275 |
+
"recent_events": recent_events,
|
| 2276 |
+
}
|
story_engine.py
CHANGED
|
@@ -525,7 +525,7 @@ class StoryEngine:
|
|
| 525 |
# 2. apply_changes 的 change_log 与状态栏显示一致
|
| 526 |
# 3. 不会出现“文本说100、状态栏显示97”的同步Bug
|
| 527 |
# ============================================
|
| 528 |
-
tick_log = self.game_state.tick_time()
|
| 529 |
|
| 530 |
# ============================================
|
| 531 |
# 第一阶段:生成剧情大纲
|
|
@@ -1032,49 +1032,222 @@ class StoryEngine:
|
|
| 1032 |
),
|
| 1033 |
}
|
| 1034 |
|
| 1035 |
-
def _sanitize_state_changes(self, changes: dict, event_type: str = "") -> tuple[dict, list[str]]:
|
| 1036 |
-
"""
|
| 1037 |
-
清理状态变更
|
| 1038 |
-
|
| 1039 |
-
规则:
|
| 1040 |
-
- 交易(TRADE)、赠送、丢弃等行为可以移除任何物品
|
| 1041 |
-
- 其他行为(使用、探索、对话等)只能移除消耗品
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
Returns:
|
| 1048 |
-
(清理后的changes, 警告列表)
|
| 1049 |
-
"""
|
| 1050 |
-
warnings = []
|
| 1051 |
-
if "items_lost" not in changes:
|
| 1052 |
-
return changes, warnings
|
| 1053 |
-
|
| 1054 |
-
# 交易/赠送行为可以移除任何物品
|
| 1055 |
-
trade_events = {"TRADE", "GIVE", "DROP"}
|
| 1056 |
-
if event_type.upper() in trade_events:
|
| 1057 |
-
return changes, warnings
|
| 1058 |
-
|
| 1059 |
-
# 其他行为:只允许移除消耗品
|
| 1060 |
-
changes = dict(changes) # 浅拷贝,避免修改原始数据
|
| 1061 |
-
sanitized_items_lost = []
|
| 1062 |
-
for item_name in changes["items_lost"]:
|
| 1063 |
-
item_str = str(item_name)
|
| 1064 |
-
if self.game_state.is_item_consumable(item_str):
|
| 1065 |
-
sanitized_items_lost.append(item_name)
|
| 1066 |
-
else:
|
| 1067 |
-
warnings.append(
|
| 1068 |
-
f"物品 '{item_str}' 不是消耗品,使用后仍保留在背包中"
|
| 1069 |
-
)
|
| 1070 |
-
logger.warning(f"阻止移除非消耗品: {item_str}")
|
| 1071 |
-
|
| 1072 |
-
if sanitized_items_lost:
|
| 1073 |
-
changes["items_lost"] = sanitized_items_lost
|
| 1074 |
-
else:
|
| 1075 |
-
changes.pop("items_lost", None)
|
| 1076 |
|
| 1077 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1078 |
|
| 1079 |
def _fill_missing_consumable_effects(
|
| 1080 |
self,
|
|
@@ -1396,7 +1569,7 @@ class StoryEngine:
|
|
| 1396 |
logger.info(f"[流式/合并] 生成故事响应,玩家意图: {player_intent}")
|
| 1397 |
|
| 1398 |
# 推进时间
|
| 1399 |
-
tick_log = self.game_state.tick_time()
|
| 1400 |
|
| 1401 |
# 构建合并 Prompt
|
| 1402 |
system_prompt = MERGED_SYSTEM_PROMPT_TEMPLATE.format(
|
|
|
|
| 525 |
# 2. apply_changes 的 change_log 与状态栏显示一致
|
| 526 |
# 3. 不会出现“文本说100、状态栏显示97”的同步Bug
|
| 527 |
# ============================================
|
| 528 |
+
tick_log = self.game_state.tick_time(player_intent)
|
| 529 |
|
| 530 |
# ============================================
|
| 531 |
# 第一阶段:生成剧情大纲
|
|
|
|
| 1032 |
),
|
| 1033 |
}
|
| 1034 |
|
| 1035 |
+
def _sanitize_state_changes(self, changes: dict, event_type: str = "") -> tuple[dict, list[str]]:
|
| 1036 |
+
"""
|
| 1037 |
+
清理状态变更,避免非预期的高风险副作用进入正式状态。
|
| 1038 |
+
|
| 1039 |
+
规则:
|
| 1040 |
+
- 交易(TRADE)、赠送、丢弃等行为可以移除任何物品
|
| 1041 |
+
- 其他行为(使用、探索、对话等)只能移除消耗品
|
| 1042 |
+
- 非战斗事件不能直接造成 NPC 死亡
|
| 1043 |
+
|
| 1044 |
+
Args:
|
| 1045 |
+
changes: 状态变更字典
|
| 1046 |
+
event_type: 事件类型(来自大纲)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1047 |
|
| 1048 |
+
Returns:
|
| 1049 |
+
(清理后的changes, 警告列表)
|
| 1050 |
+
"""
|
| 1051 |
+
warnings = []
|
| 1052 |
+
changes = dict(changes)
|
| 1053 |
+
|
| 1054 |
+
if "items_lost" in changes:
|
| 1055 |
+
# 交易/赠送行为可以移除任何物品
|
| 1056 |
+
trade_events = {"TRADE", "GIVE", "DROP"}
|
| 1057 |
+
if event_type.upper() not in trade_events:
|
| 1058 |
+
# 其他行为:只允许移除消耗品
|
| 1059 |
+
sanitized_items_lost = []
|
| 1060 |
+
for item_name in changes["items_lost"]:
|
| 1061 |
+
item_str = str(item_name)
|
| 1062 |
+
if self.game_state.is_item_consumable(item_str):
|
| 1063 |
+
sanitized_items_lost.append(item_name)
|
| 1064 |
+
else:
|
| 1065 |
+
warnings.append(
|
| 1066 |
+
f"物品 '{item_str}' 不是消耗品,使用后仍保留在背包中"
|
| 1067 |
+
)
|
| 1068 |
+
logger.warning(f"阻止移除非消耗品: {item_str}")
|
| 1069 |
+
|
| 1070 |
+
if sanitized_items_lost:
|
| 1071 |
+
changes["items_lost"] = sanitized_items_lost
|
| 1072 |
+
else:
|
| 1073 |
+
changes.pop("items_lost", None)
|
| 1074 |
+
|
| 1075 |
+
changes, item_warnings = self._sanitize_controlled_item_changes(changes, event_type)
|
| 1076 |
+
warnings.extend(item_warnings)
|
| 1077 |
+
changes, npc_warnings = self._sanitize_noncombat_npc_changes(changes, event_type)
|
| 1078 |
+
warnings.extend(npc_warnings)
|
| 1079 |
+
|
| 1080 |
+
return changes, warnings
|
| 1081 |
+
|
| 1082 |
+
def _sanitize_controlled_item_changes(
|
| 1083 |
+
self,
|
| 1084 |
+
changes: dict,
|
| 1085 |
+
event_type: str = "",
|
| 1086 |
+
) -> tuple[dict, list[str]]:
|
| 1087 |
+
"""Restrict item gain/equip to registered items and paid trade outcomes."""
|
| 1088 |
+
sanitized_changes = dict(changes)
|
| 1089 |
+
warnings: list[str] = []
|
| 1090 |
+
registered_items = set(self.game_state.world.item_registry.keys())
|
| 1091 |
+
inventory_items = set(self.game_state.player.inventory)
|
| 1092 |
+
equipped_items = {
|
| 1093 |
+
str(item)
|
| 1094 |
+
for item in self.game_state.player.equipment.values()
|
| 1095 |
+
if item and str(item).lower() not in {"none", "null", ""}
|
| 1096 |
+
}
|
| 1097 |
+
allowed_trade_items = {
|
| 1098 |
+
str(item_name)
|
| 1099 |
+
for npc in self.game_state.world.npcs.values()
|
| 1100 |
+
if npc.can_trade
|
| 1101 |
+
for item_name in npc.shop_inventory
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
original_items_gained = [
|
| 1105 |
+
str(item)
|
| 1106 |
+
for item in sanitized_changes.get("items_gained", [])
|
| 1107 |
+
]
|
| 1108 |
+
valid_items_gained: list[str] = []
|
| 1109 |
+
for item_name in original_items_gained:
|
| 1110 |
+
if item_name not in registered_items:
|
| 1111 |
+
warning = f"移除未注册物品获取: {item_name}"
|
| 1112 |
+
warnings.append(warning)
|
| 1113 |
+
logger.warning(warning)
|
| 1114 |
+
continue
|
| 1115 |
+
if event_type.upper() == "TRADE" and allowed_trade_items and item_name not in allowed_trade_items:
|
| 1116 |
+
warning = f"移除不在商店配置中的交易物品: {item_name}"
|
| 1117 |
+
warnings.append(warning)
|
| 1118 |
+
logger.warning(warning)
|
| 1119 |
+
continue
|
| 1120 |
+
valid_items_gained.append(item_name)
|
| 1121 |
+
|
| 1122 |
+
if valid_items_gained:
|
| 1123 |
+
sanitized_changes["items_gained"] = valid_items_gained
|
| 1124 |
+
else:
|
| 1125 |
+
sanitized_changes.pop("items_gained", None)
|
| 1126 |
+
|
| 1127 |
+
if event_type.upper() == "TRADE":
|
| 1128 |
+
gold_change = sanitized_changes.get("gold_change")
|
| 1129 |
+
items_lost = sanitized_changes.get("items_lost", [])
|
| 1130 |
+
is_paid_trade = (
|
| 1131 |
+
isinstance(gold_change, (int, float)) and gold_change < 0
|
| 1132 |
+
) or bool(items_lost)
|
| 1133 |
+
if not is_paid_trade:
|
| 1134 |
+
if "items_gained" in sanitized_changes:
|
| 1135 |
+
warning = "移除未支付的交易物品获取"
|
| 1136 |
+
warnings.append(warning)
|
| 1137 |
+
logger.warning(warning)
|
| 1138 |
+
sanitized_changes.pop("items_gained", None)
|
| 1139 |
+
if "equip" in sanitized_changes:
|
| 1140 |
+
warning = "移除未支付的交易装备变更"
|
| 1141 |
+
warnings.append(warning)
|
| 1142 |
+
logger.warning(warning)
|
| 1143 |
+
sanitized_changes.pop("equip", None)
|
| 1144 |
+
|
| 1145 |
+
if "equip" in sanitized_changes and isinstance(sanitized_changes["equip"], dict):
|
| 1146 |
+
valid_equippable = inventory_items | equipped_items | set(
|
| 1147 |
+
sanitized_changes.get("items_gained", [])
|
| 1148 |
+
)
|
| 1149 |
+
sanitized_equip: dict[str, str] = {}
|
| 1150 |
+
for slot, item_name in sanitized_changes["equip"].items():
|
| 1151 |
+
item_str = str(item_name)
|
| 1152 |
+
if item_str.lower() in {"none", "null", ""}:
|
| 1153 |
+
sanitized_equip[str(slot)] = item_name
|
| 1154 |
+
continue
|
| 1155 |
+
if item_str not in registered_items:
|
| 1156 |
+
warning = f"移除未注册装备变更: {slot} -> {item_str}"
|
| 1157 |
+
warnings.append(warning)
|
| 1158 |
+
logger.warning(warning)
|
| 1159 |
+
continue
|
| 1160 |
+
if event_type.upper() == "TRADE" and allowed_trade_items and item_str not in allowed_trade_items:
|
| 1161 |
+
warning = f"移除不在商店配置中的交易装备: {slot} -> {item_str}"
|
| 1162 |
+
warnings.append(warning)
|
| 1163 |
+
logger.warning(warning)
|
| 1164 |
+
continue
|
| 1165 |
+
if item_str not in valid_equippable:
|
| 1166 |
+
warning = f"移除未持有装备变更: {slot} -> {item_str}"
|
| 1167 |
+
warnings.append(warning)
|
| 1168 |
+
logger.warning(warning)
|
| 1169 |
+
continue
|
| 1170 |
+
sanitized_equip[str(slot)] = item_str
|
| 1171 |
+
|
| 1172 |
+
if sanitized_equip:
|
| 1173 |
+
sanitized_changes["equip"] = sanitized_equip
|
| 1174 |
+
else:
|
| 1175 |
+
sanitized_changes.pop("equip", None)
|
| 1176 |
+
|
| 1177 |
+
return sanitized_changes, warnings
|
| 1178 |
+
|
| 1179 |
+
def _sanitize_noncombat_npc_changes(
|
| 1180 |
+
self,
|
| 1181 |
+
changes: dict,
|
| 1182 |
+
event_type: str = "",
|
| 1183 |
+
) -> tuple[dict, list[str]]:
|
| 1184 |
+
"""移除非战斗事件中意外出现的 NPC 致死变更。"""
|
| 1185 |
+
npc_changes = changes.get("npc_changes")
|
| 1186 |
+
if not isinstance(npc_changes, dict):
|
| 1187 |
+
return changes, []
|
| 1188 |
+
|
| 1189 |
+
noncombat_events = {
|
| 1190 |
+
"TRADE",
|
| 1191 |
+
"GIVE",
|
| 1192 |
+
"DROP",
|
| 1193 |
+
"TALK",
|
| 1194 |
+
"DIALOGUE",
|
| 1195 |
+
"MOVE",
|
| 1196 |
+
"EXPLORE",
|
| 1197 |
+
"REST",
|
| 1198 |
+
"DISCOVERY",
|
| 1199 |
+
}
|
| 1200 |
+
if event_type.upper() not in noncombat_events:
|
| 1201 |
+
return changes, []
|
| 1202 |
+
|
| 1203 |
+
sanitized_changes = dict(changes)
|
| 1204 |
+
sanitized_npc_changes: dict[str, dict] = {}
|
| 1205 |
+
warnings: list[str] = []
|
| 1206 |
+
|
| 1207 |
+
for npc_name, npc_data in npc_changes.items():
|
| 1208 |
+
if not isinstance(npc_data, dict):
|
| 1209 |
+
sanitized_npc_changes[str(npc_name)] = npc_data
|
| 1210 |
+
continue
|
| 1211 |
+
|
| 1212 |
+
cleaned_data = dict(npc_data)
|
| 1213 |
+
removed_keys: list[str] = []
|
| 1214 |
+
|
| 1215 |
+
if cleaned_data.get("is_alive") is False:
|
| 1216 |
+
cleaned_data.pop("is_alive", None)
|
| 1217 |
+
removed_keys.append("is_alive")
|
| 1218 |
+
|
| 1219 |
+
hp_delta = cleaned_data.get("hp_change")
|
| 1220 |
+
npc = self.game_state.world.npcs.get(str(npc_name))
|
| 1221 |
+
try:
|
| 1222 |
+
hp_delta_value = int(hp_delta) if hp_delta is not None else None
|
| 1223 |
+
except (TypeError, ValueError):
|
| 1224 |
+
hp_delta_value = None
|
| 1225 |
+
|
| 1226 |
+
if (
|
| 1227 |
+
hp_delta_value is not None
|
| 1228 |
+
and npc is not None
|
| 1229 |
+
and npc.hp + hp_delta_value <= 0
|
| 1230 |
+
):
|
| 1231 |
+
cleaned_data.pop("hp_change", None)
|
| 1232 |
+
removed_keys.append("hp_change")
|
| 1233 |
+
|
| 1234 |
+
if removed_keys:
|
| 1235 |
+
warning = (
|
| 1236 |
+
f"移除非战斗事件中的 NPC 致死变更: {npc_name} -> "
|
| 1237 |
+
f"{', '.join(removed_keys)}"
|
| 1238 |
+
)
|
| 1239 |
+
warnings.append(warning)
|
| 1240 |
+
logger.warning(warning)
|
| 1241 |
+
|
| 1242 |
+
if cleaned_data:
|
| 1243 |
+
sanitized_npc_changes[str(npc_name)] = cleaned_data
|
| 1244 |
+
|
| 1245 |
+
if sanitized_npc_changes:
|
| 1246 |
+
sanitized_changes["npc_changes"] = sanitized_npc_changes
|
| 1247 |
+
else:
|
| 1248 |
+
sanitized_changes.pop("npc_changes", None)
|
| 1249 |
+
|
| 1250 |
+
return sanitized_changes, warnings
|
| 1251 |
|
| 1252 |
def _fill_missing_consumable_effects(
|
| 1253 |
self,
|
|
|
|
| 1569 |
logger.info(f"[流式/合并] 生成故事响应,玩家意图: {player_intent}")
|
| 1570 |
|
| 1571 |
# 推进时间
|
| 1572 |
+
tick_log = self.game_state.tick_time(player_intent)
|
| 1573 |
|
| 1574 |
# 构建合并 Prompt
|
| 1575 |
system_prompt = MERGED_SYSTEM_PROMPT_TEMPLATE.format(
|