refactor conversation

This commit is contained in:
bridge
2025-12-18 21:52:00 +08:00
parent 3ca5333246
commit acf7d9dd35
36 changed files with 109 additions and 51 deletions

View File

@@ -18,8 +18,9 @@ if TYPE_CHECKING:
@cooldown_action
class Assassinate(InstantAction):
COMMENT = "暗杀目标,失败则变为战斗"
DOABLES_REQUIREMENTS = "任何时候都可以执行;需要冷却"
ACTION_NAME = "暗杀"
DESC = "暗杀目标,失败则变为战斗"
DOABLES_REQUIREMENTS = "无限制;需要冷却"
PARAMS = {"avatar_name": "AvatarName"}
ACTION_CD_MONTHS = 12

View File

@@ -11,8 +11,9 @@ from src.classes.death_reason import DeathReason
from src.classes.kill_and_grab import kill_and_grab
class Attack(InstantAction):
COMMENT = "攻击目标,进行对战"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "发起战斗"
DESC = "攻击目标,进行对战"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"avatar_name": "AvatarName"}
# 提供用于故事生成的提示词:不出现血量/伤害等数值描述
STORY_PROMPT: str | None = (

View File

@@ -25,7 +25,8 @@ class Breakthrough(TimedAction):
失败时按 `CultivationProgress.get_breakthrough_fail_reduce_lifespan()` 减少寿元(年)。
"""
COMMENT = "尝试突破境界(成功增加寿元上限,失败折损寿元上限;境界越高,成功率越低。)"
ACTION_NAME = "突破"
DESC = "尝试突破境界(成功增加寿元上限,失败折损寿元上限;境界越高,成功率越低。)"
DOABLES_REQUIREMENTS = "角色处于瓶颈时;不能连续执行"
PARAMS = {}
# 冷却突破应当有CD避免连刷

View File

@@ -23,7 +23,8 @@ class Catch(TimedAction):
- 按动物境界映射成功率尝试捕捉,成功则成为灵兽(覆盖旧灵兽)。
"""
COMMENT = "尝试驯服一只灵兽,成为自身灵兽。只能有一只灵兽,但是可以高级替换低级。"
ACTION_NAME = "御兽"
DESC = "尝试驯服一只灵兽,成为自身灵兽。只能有一只灵兽,但是可以高级替换低级。"
DOABLES_REQUIREMENTS = "仅百兽宗;在有动物的普通区域;目标动物境界不高于角色"
PARAMS = {}

View File

@@ -7,7 +7,7 @@ def cooldown_action(cls: type) -> type:
- 仅当类定义了 ACTION_CD_MONTHS 且 >0 时生效
- 在 can_start 前置检查冷却;在 finish 后记录冷却开始月戳
- 冷却记录存放于 avatar._action_cd_last_months[ClassName]
- 同时在 COMMENT 中追加“冷却X月”便于 UI 显示
- 同时在 DESC 中追加“冷却X月”便于 UI 显示
"""
cd = int(getattr(cls, "ACTION_CD_MONTHS", 0) or 0)

View File

@@ -11,7 +11,8 @@ class Cultivate(TimedAction):
修炼动作,可以增加修仙进度。
"""
COMMENT = "修炼,增进修为。在修炼区域(洞府)且灵气匹配时效果最佳,否则效果很差。"
ACTION_NAME = "修炼"
DESC = "修炼,增进修为。在修炼区域(洞府)且灵气匹配时效果最佳,否则效果很差。"
DOABLES_REQUIREMENTS = "角色未到瓶颈;若在洞府区域,则该洞府需无主或归自己所有。"
PARAMS = {}

View File

@@ -10,7 +10,8 @@ class DevourMortals(TimedAction):
吞噬凡人:需持有万魂幡,吞噬魂魄可较多增加战力。
"""
COMMENT = "吞噬凡人,较多增加战力"
ACTION_NAME = "吞噬凡人"
DESC = "吞噬凡人,较多增加战力"
DOABLES_REQUIREMENTS = "持有万魂幡"
PARAMS = {}

View File

@@ -14,8 +14,9 @@ class Escape(InstantAction):
失败:抢占并进入 Attack。
"""
COMMENT = "逃离对方(基于成功率判定)"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "逃离"
DESC = "逃离对方(基于成功率判定)"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"avatar_name": "AvatarName"}
def _find_avatar_by_name(self, name: str) -> "Avatar|None":

View File

@@ -12,7 +12,8 @@ class Harvest(TimedAction):
可以获得植物对应的物品
"""
COMMENT = "在当前区域采集植物,获取植物材料"
ACTION_NAME = "采集"
DESC = "在当前区域采集植物,获取植物材料"
DOABLES_REQUIREMENTS = "在有植物的普通区域且avatar的境界必须大于等于植物的境界"
PARAMS = {}

View File

@@ -12,7 +12,8 @@ class HelpMortals(TimedAction):
仅正阵营可执行。
"""
COMMENT = "在城镇帮助凡人,消耗少量灵石"
ACTION_NAME = "帮助凡人"
DESC = "在城镇帮助凡人,消耗少量灵石"
DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘正’,并且灵石足够"
PARAMS = {}
COST = 10

View File

@@ -12,7 +12,8 @@ class Hunt(TimedAction):
可以获得动物对应的物品
"""
COMMENT = "在当前区域狩猎动物,获取动物材料"
ACTION_NAME = "狩猎"
DESC = "在当前区域狩猎动物,获取动物材料"
DOABLES_REQUIREMENTS = "在有动物的普通区域且avatar的境界必须大于等于动物的境界"
PARAMS = {}

View File

@@ -9,7 +9,8 @@ class Move(DefineAction, ChunkActionMixin):
最基础的移动动作在tile之间进行切换。
"""
COMMENT = "移动到某个相对位置"
ACTION_NAME = "移动"
DESC = "移动到某个相对位置"
PARAMS = {"delta_x": "int", "delta_y": "int"}
def _execute(self, delta_x: int, delta_y: int) -> None:

View File

@@ -17,8 +17,9 @@ class MoveAwayFromAvatar(TimedAction):
- 任何时候都可以启动
"""
COMMENT = "持续远离指定角色"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "远离角色"
DESC = "持续远离指定角色"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"avatar_name": "AvatarName"}
def _find_avatar_by_name(self, name: str) -> "Avatar | None":

View File

@@ -8,8 +8,9 @@ from src.utils.distance import euclidean_distance
class MoveAwayFromRegion(InstantAction):
COMMENT = "离开指定区域"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "离开区域"
DESC = "离开指定区域"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"region": "RegionName"}
def _execute(self, region: str) -> None:

View File

@@ -13,8 +13,9 @@ class MoveToAvatar(DefineAction, ActualActionMixin):
朝另一个角色当前位置移动。
"""
COMMENT = "移动到某个角色所在位置"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "移动到角色"
DESC = "移动到某个角色所在位置"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"avatar_name": "str"}
def _get_target(self, avatar_name: str):

View File

@@ -57,8 +57,9 @@ class MoveToDirection(DefineAction, ActualActionMixin):
向某个方向移动探索固定时长6个月
"""
COMMENT = "向某个方向探索未知区域"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "移动探索"
DESC = "向某个方向探索未知区域"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"direction": "direction (North/South/East/West)"}
IS_MAJOR = False

View File

@@ -14,8 +14,9 @@ class MoveToRegion(DefineAction, ActualActionMixin):
移动到某个region
"""
COMMENT = "移动到某个区域"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "移动到区域"
DESC = "移动到某个区域"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"region": "region_name"}
def __init__(self, avatar, world):

View File

@@ -10,8 +10,9 @@ class NurtureWeapon(TimedAction):
温养兵器:花时间温养兵器,可以较多增加熟练度
"""
COMMENT = "温养兵器,增加兵器熟练度"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "温养兵器"
DESC = "温养兵器,增加兵器熟练度"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {}
duration_months = 3

View File

@@ -9,8 +9,9 @@ class Play(TimedAction):
消遣动作,持续半年时间
"""
COMMENT = "消遣,放松身心"
DOABLES_REQUIREMENTS = "任何时候都可以执行"
ACTION_NAME = "消遣"
DESC = "消遣,放松身心"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {}
duration_months = 6

View File

@@ -12,7 +12,8 @@ class PlunderMortals(TimedAction):
仅邪阵营可执行。
"""
COMMENT = "在城镇搜刮凡人,获取少量灵石"
ACTION_NAME = "搜刮凡人"
DESC = "在城镇搜刮凡人,获取少量灵石"
DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘邪’"
PARAMS = {}
GAIN = 20

View File

@@ -11,7 +11,8 @@ class SelfHeal(TimedAction):
单月动作执行后HP直接回满。
"""
COMMENT = "在宗门总部静养疗伤回满HP"
ACTION_NAME = "疗伤"
DESC = "在宗门总部静养疗伤回满HP"
DOABLES_REQUIREMENTS = "自己是宗门弟子且位于本宗门总部区域且当前HP未满"
PARAMS = {}

View File

@@ -14,7 +14,8 @@ class SellItems(InstantAction):
收益为 item_price * item_num动作耗时1个月。
"""
COMMENT = "在城镇出售持有的某类物品的全部"
ACTION_NAME = "出售物品"
DESC = "在城镇出售持有的某类物品的全部"
DOABLES_REQUIREMENTS = "在城镇且背包非空"
PARAMS = {"item_name": "str"}

View File

@@ -13,7 +13,8 @@ class SwitchWeapon(InstantAction):
熟练度重置为0。
"""
COMMENT = "切换到指定类型的凡品兵器或卸下兵器。当前兵器会丧失熟练度会重置为0。适用于想要更换兵器类型或从头修炼新兵器的情况。"
ACTION_NAME = "切换兵器"
DESC = "切换到指定类型的凡品兵器或卸下兵器。当前兵器会丧失熟练度会重置为0。适用于想要更换兵器类型或从头修炼新兵器的情况。"
DOABLES_REQUIREMENTS = "无前置条件"
PARAMS = {"weapon_type_name": "str"}

View File

@@ -15,8 +15,8 @@ ALL_ACTUAL_ACTION_NAMES = [cls.__name__ for cls in ALL_ACTUAL_ACTION_CLASSES]
def _build_action_info(action):
info = {
"comment": action.COMMENT,
"requirements": action.DOABLES_REQUIREMENTS,
"desc": action.DESC,
"require": action.DOABLES_REQUIREMENTS,
}
if action.PARAMS:
info["params"] = action.PARAMS

View File

@@ -156,3 +156,29 @@ class ActionMixin:
if to_sidebar:
self._pending_events.append(event)
def get_planned_actions_str(self: "Avatar") -> str:
"""
获取易读的计划动作列表字符串。
"""
if not self.planned_actions:
return ""
lines = []
for i, plan in enumerate(self.planned_actions, 1):
try:
action_cls = ActionRegistry.get(plan.action_name)
# 优先取 ACTION_NAME否则用类名
display_name = getattr(action_cls, "ACTION_NAME", plan.action_name)
except Exception:
display_name = plan.action_name
# 简化参数显示,只保留基本类型
simple_params = {k: v for k, v in plan.params.items() if isinstance(v, (str, int, float, bool))}
info = f"{i}. {display_name}"
if simple_params:
info += f" {simple_params}"
lines.append(info)
return "\n".join(lines)

View File

@@ -13,7 +13,7 @@ class MutualAttack(MutualAction):
"""攻击另一个NPC"""
ACTION_NAME = "攻击"
COMMENT = "对目标进行攻击。"
DESC = "对目标进行攻击。"
DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行"
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS = ["Escape", "Attack"]

View File

@@ -26,7 +26,7 @@ class Conversation(MutualAction):
"""
ACTION_NAME = "交谈"
COMMENT = "与对方进行一段交流对话"
DESC = "与对方进行一段交流对话"
DOABLES_REQUIREMENTS = "目标在交互范围内"
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS: list[str] = [] # Conversation 自动触发,不需要对方决策
@@ -51,12 +51,20 @@ class Conversation(MutualAction):
# 获取关系上下文
possible_new_relations, possible_cancel_relations = get_relation_change_context(self.avatar, target_avatar)
# 获取后续计划
p1 = self.avatar.get_planned_actions_str()
p2 = target_avatar.get_planned_actions_str()
planned_actions_str = {
avatar_name_1: p1,
avatar_name_2: p2,
}
return {
"avatar_infos": avatar_infos,
"avatar_name_1": avatar_name_1,
"avatar_name_2": avatar_name_2,
"possible_new_relations": possible_new_relations,
"possible_cancel_relations": possible_cancel_relations,
"planned_actions": planned_actions_str,
}
def _can_start(self, target: "Avatar") -> tuple[bool, str]:

View File

@@ -13,7 +13,7 @@ class DriveAway(MutualAction):
"""驱赶:试图让对方离开当前区域。"""
ACTION_NAME = "驱赶"
COMMENT = "以武力威慑对方离开此地。"
DESC = "以武力威慑对方离开此地。"
DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行"
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS = ["MoveAwayFromRegion", "Attack"]

View File

@@ -26,7 +26,7 @@ class DualCultivation(MutualAction):
"""
ACTION_NAME = "双修"
COMMENT = "以情入道的双修之术,仅合欢宗弟子可发起,对象可接受或拒绝"
DESC = "以情入道的双修之术,仅合欢宗弟子可发起,对象可接受或拒绝"
DOABLES_REQUIREMENTS = "发起者为合欢宗;目标在交互范围内;不能连续执行"
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS = ["Accept", "Reject"]

View File

@@ -21,7 +21,7 @@ class GiftSpiritStone(MutualAction):
"""
ACTION_NAME = "赠送灵石"
COMMENT = "向对方赠送灵石一次赠送100灵石"
DESC = "向对方赠送灵石一次赠送100灵石"
DOABLES_REQUIREMENTS = "发起者至少有100灵石目标在交互范围内"
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS = ["Accept", "Reject"]

View File

@@ -25,7 +25,7 @@ class Impart(MutualAction):
"""
ACTION_NAME = "传道"
COMMENT = "师傅向徒弟传授修炼经验,徒弟可获得大量修为"
DESC = "师傅向徒弟传授修炼经验,徒弟可获得大量修为"
DOABLES_REQUIREMENTS = "发起者是目标的师傅;师傅等级 > 徒弟等级 + 20目标在交互范围内不能连续执行"
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS = ["Accept", "Reject"]

View File

@@ -25,14 +25,14 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
互动动作A 对 B 发起动作B 可以给出反馈(由 LLM 决策)。
子类需要定义:
- ACTION_NAME: 当前动作名(给模板展示)
- COMMENT: 动作语义说明(给模板展示)
- DESC: 动作语义说明(给模板展示)
- FEEDBACK_ACTIONS: 反馈可选的 action name 列表(直接可执行)
- PARAMS: 参数,需要包含 target_avatar
- FEEDBACK_ACTIONS: 反馈可选的 action name 列表(直接可执行)
"""
ACTION_NAME: str = "MutualAction"
COMMENT: str = ""
DESC: str = ""
DOABLES_REQUIREMENTS: str = "交互范围内可互动"
PARAMS: dict = {"target_avatar": "Avatar"}
FEEDBACK_ACTIONS: list[str] = []
@@ -72,14 +72,14 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
}
feedback_actions = self.FEEDBACK_ACTIONS
comment = self.COMMENT
desc = self.DESC
action_name = self.ACTION_NAME
return {
"avatar_infos": avatar_infos,
"avatar_name_1": avatar_name_1,
"avatar_name_2": avatar_name_2,
"action_name": action_name,
"action_info": comment,
"action_info": desc,
"feedback_actions": feedback_actions,
}

View File

@@ -28,7 +28,7 @@ class Occupy(MutualAction):
对方拒绝则进入战斗,进攻方胜利则洞府易主。
"""
ACTION_NAME = "抢夺洞府"
COMMENT = "占据或抢夺洞府"
DESC = "占据或抢夺洞府"
PARAMS = {"region_name": "str"}
FEEDBACK_ACTIONS = ["Yield", "Reject"]
FEEDBACK_LABELS = {"Yield": "让步", "Reject": "拒绝"}

View File

@@ -21,7 +21,7 @@ class Spar(MutualAction):
切磋动作:双方切磋,不造成伤害,增加武器熟练度。
"""
ACTION_NAME = "切磋"
COMMENT = "与目标切磋武艺,点到为止(大幅增加武器熟练度,不造成伤害)"
DESC = "与目标切磋武艺,点到为止(大幅增加武器熟练度,不造成伤害)"
DOABLES_REQUIREMENTS = "交互范围内可互动;不能连续执行"
FEEDBACK_ACTIONS = ["Accept", "Reject"]

View File

@@ -19,7 +19,7 @@ class Talk(MutualAction):
"""
ACTION_NAME = "攀谈"
COMMENT = "向对方发起攀谈"
DESC = "向对方发起攀谈"
DOABLES_REQUIREMENTS = "目标在交互范围内"
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"]

View File

@@ -1,9 +1,11 @@
你是一个决策者这是一个仙侠世界你负责来生成两个NPC间的对话内容并决定两人是否会有关系的变化。
你是一个小说家这是一个仙侠世界你负责来生成两个NPC间的对话内容并决定两人是否会有关系的变化。
你需要进行决策的NPC的dict[AvatarName, info]为
{avatar_infos}
之后NPC的将要做行动为
{planned_actions}
{avatar_name_1}和{avatar_name_2}正在对话。这个对话可能是善意的也可能是恶意的也可能是闲聊。内容和性质取决于NPC特质性格、天赋等、正邪、关系等因素
{avatar_name_1}和{avatar_name_2}的对话可能是善意\恶意\闲聊。目的和内容参考NPC信息得出
两者可能进入的关系:{possible_new_relations}
两者可能取消的关系:{possible_cancel_relations}
@@ -14,7 +16,7 @@
{{
"{avatar_name_2}": {{
"thinking": ..., // 简单思考对话如何进行
"conversation_content": ... // 对话双方均为第三人称视角的对话100~150字仙侠语言风格。可以是聊天也可以是对话概括
"conversation_content": ... // 对话双方均为第三人称视角的对话100~300字有来有回的多轮对话
"analyze_relation": ... // 分析是否应该有关系的取消或者新增
"new_relation": ... // 如果你认为可以让两者产生某种身份关系则返回关系的中文名否则返回空str。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。
"cancel_relation": ... // 可选如果你认为可以让两者取消某种身份关系则返回关系的中文名否则返回空str。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。