diff --git a/src/classes/action/action.py b/src/classes/action/action.py index ea70a62..04c4899 100644 --- a/src/classes/action/action.py +++ b/src/classes/action/action.py @@ -69,6 +69,8 @@ class Action(ABC): """ return str(self.__class__.__name__) + EMOJI: str = "" + def get_save_data(self) -> dict: """获取需要存档的运行时数据""" return {} diff --git a/src/classes/action/assassinate.py b/src/classes/action/assassinate.py index 0834894..d452a63 100644 --- a/src/classes/action/assassinate.py +++ b/src/classes/action/assassinate.py @@ -19,6 +19,7 @@ if TYPE_CHECKING: @cooldown_action class Assassinate(InstantAction): ACTION_NAME = "暗杀" + EMOJI = "🗡️" DESC = "暗杀目标,失败则变为战斗" DOABLES_REQUIREMENTS = "无限制;需要冷却" PARAMS = {"avatar_name": "AvatarName"} diff --git a/src/classes/action/attack.py b/src/classes/action/attack.py index 12beb95..c804637 100644 --- a/src/classes/action/attack.py +++ b/src/classes/action/attack.py @@ -12,6 +12,7 @@ from src.classes.kill_and_grab import kill_and_grab class Attack(InstantAction): ACTION_NAME = "发起战斗" + EMOJI = "⚔️" DESC = "攻击目标,进行对战" DOABLES_REQUIREMENTS = "无限制" PARAMS = {"avatar_name": "AvatarName"} diff --git a/src/classes/action/breakthrough.py b/src/classes/action/breakthrough.py index 2c3f982..7e90979 100644 --- a/src/classes/action/breakthrough.py +++ b/src/classes/action/breakthrough.py @@ -26,6 +26,7 @@ class Breakthrough(TimedAction): """ ACTION_NAME = "突破" + EMOJI = "⚡" DESC = "尝试突破境界(成功增加寿元上限,失败折损寿元上限;境界越高,成功率越低。)" DOABLES_REQUIREMENTS = "角色处于瓶颈时;不能连续执行" PARAMS = {} diff --git a/src/classes/action/catch.py b/src/classes/action/catch.py index c0b19a9..edcb016 100644 --- a/src/classes/action/catch.py +++ b/src/classes/action/catch.py @@ -24,6 +24,7 @@ class Catch(TimedAction): """ ACTION_NAME = "御兽" + EMOJI = "🕸️" DESC = "尝试驯服一只灵兽,成为自身灵兽。只能有一只灵兽,但是可以高级替换低级。" DOABLES_REQUIREMENTS = "仅百兽宗;在有动物的普通区域;目标动物境界不高于角色" PARAMS = {} diff --git a/src/classes/action/cultivate.py b/src/classes/action/cultivate.py index 10238a8..62fb7bf 100644 --- a/src/classes/action/cultivate.py +++ b/src/classes/action/cultivate.py @@ -12,6 +12,7 @@ class Cultivate(TimedAction): """ ACTION_NAME = "修炼" + EMOJI = "🧘" DESC = "修炼,增进修为。在修炼区域(洞府)且灵气匹配时效果最佳,否则效果很差。" DOABLES_REQUIREMENTS = "角色未到瓶颈;若在洞府区域,则该洞府需无主或归自己所有。" PARAMS = {} diff --git a/src/classes/action/devour_mortals.py b/src/classes/action/devour_mortals.py index 8f6457f..c5111f4 100644 --- a/src/classes/action/devour_mortals.py +++ b/src/classes/action/devour_mortals.py @@ -11,6 +11,7 @@ class DevourMortals(TimedAction): """ ACTION_NAME = "吞噬凡人" + EMOJI = "🩸" DESC = "吞噬凡人,较多增加战力" DOABLES_REQUIREMENTS = "持有万魂幡" PARAMS = {} diff --git a/src/classes/action/escape.py b/src/classes/action/escape.py index e87b75d..4b22ad7 100644 --- a/src/classes/action/escape.py +++ b/src/classes/action/escape.py @@ -15,6 +15,7 @@ class Escape(InstantAction): """ ACTION_NAME = "逃离" + EMOJI = "💨" DESC = "逃离对方(基于成功率判定)" DOABLES_REQUIREMENTS = "无限制" PARAMS = {"avatar_name": "AvatarName"} diff --git a/src/classes/action/harvest.py b/src/classes/action/harvest.py index 32cedd8..122879c 100644 --- a/src/classes/action/harvest.py +++ b/src/classes/action/harvest.py @@ -13,6 +13,7 @@ class Harvest(TimedAction): """ ACTION_NAME = "采集" + EMOJI = "🌾" DESC = "在当前区域采集植物,获取植物材料" DOABLES_REQUIREMENTS = "在有植物的普通区域,且avatar的境界必须大于等于植物的境界" PARAMS = {} diff --git a/src/classes/action/help_mortals.py b/src/classes/action/help_mortals.py index 4fffc4d..b9aa304 100644 --- a/src/classes/action/help_mortals.py +++ b/src/classes/action/help_mortals.py @@ -13,6 +13,7 @@ class HelpMortals(TimedAction): """ ACTION_NAME = "帮助凡人" + EMOJI = "🤝" DESC = "在城镇帮助凡人,消耗少量灵石" DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘正’,并且灵石足够" PARAMS = {} diff --git a/src/classes/action/hunt.py b/src/classes/action/hunt.py index 5b84320..dd56074 100644 --- a/src/classes/action/hunt.py +++ b/src/classes/action/hunt.py @@ -13,6 +13,7 @@ class Hunt(TimedAction): """ ACTION_NAME = "狩猎" + EMOJI = "🏹" DESC = "在当前区域狩猎动物,获取动物材料" DOABLES_REQUIREMENTS = "在有动物的普通区域,且avatar的境界必须大于等于动物的境界" PARAMS = {} diff --git a/src/classes/action/move.py b/src/classes/action/move.py index 360a1b5..dc1ce62 100644 --- a/src/classes/action/move.py +++ b/src/classes/action/move.py @@ -10,6 +10,7 @@ class Move(DefineAction, ChunkActionMixin): """ ACTION_NAME = "移动" + EMOJI = "🏃" DESC = "移动到某个相对位置" PARAMS = {"delta_x": "int", "delta_y": "int"} diff --git a/src/classes/action/nurture_weapon.py b/src/classes/action/nurture_weapon.py index 36db308..15139de 100644 --- a/src/classes/action/nurture_weapon.py +++ b/src/classes/action/nurture_weapon.py @@ -11,6 +11,7 @@ class NurtureWeapon(TimedAction): """ ACTION_NAME = "温养兵器" + EMOJI = "✨" DESC = "温养兵器,增加兵器熟练度" DOABLES_REQUIREMENTS = "无限制" PARAMS = {} diff --git a/src/classes/action/play.py b/src/classes/action/play.py index a73d207..a2370ae 100644 --- a/src/classes/action/play.py +++ b/src/classes/action/play.py @@ -10,6 +10,7 @@ class Play(TimedAction): """ ACTION_NAME = "消遣" + EMOJI = "🪁" DESC = "消遣,放松身心" DOABLES_REQUIREMENTS = "无限制" PARAMS = {} diff --git a/src/classes/action/plunder_mortals.py b/src/classes/action/plunder_mortals.py index c5156a7..435328d 100644 --- a/src/classes/action/plunder_mortals.py +++ b/src/classes/action/plunder_mortals.py @@ -13,6 +13,7 @@ class PlunderMortals(TimedAction): """ ACTION_NAME = "搜刮凡人" + EMOJI = "💀" DESC = "在城镇搜刮凡人,获取少量灵石" DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘邪’" PARAMS = {} diff --git a/src/classes/action/self_heal.py b/src/classes/action/self_heal.py index 9897d30..7bc8f1b 100644 --- a/src/classes/action/self_heal.py +++ b/src/classes/action/self_heal.py @@ -12,6 +12,7 @@ class SelfHeal(TimedAction): """ ACTION_NAME = "疗伤" + EMOJI = "💚" DESC = "在宗门总部静养疗伤,回满HP" DOABLES_REQUIREMENTS = "自己是宗门弟子,且位于本宗门总部区域,且当前HP未满" PARAMS = {} diff --git a/src/classes/action/sell.py b/src/classes/action/sell.py index fbca4d7..40867e5 100644 --- a/src/classes/action/sell.py +++ b/src/classes/action/sell.py @@ -14,6 +14,7 @@ class SellItems(InstantAction): """ ACTION_NAME = "出售物品" + EMOJI = "💰" DESC = "在城镇出售持有的某类物品的全部" DOABLES_REQUIREMENTS = "在城镇且背包非空" PARAMS = {"item_name": "str"} diff --git a/src/classes/avatar/info_presenter.py b/src/classes/avatar/info_presenter.py index 4500335..993bf23 100644 --- a/src/classes/avatar/info_presenter.py +++ b/src/classes/avatar/info_presenter.py @@ -111,6 +111,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict: "nickname_reason": avatar.nickname.reason if avatar.nickname else None, "is_dead": avatar.is_dead, "death_info": avatar.death_info, + "action_state": f"正在{getattr(avatar.current_action.action, 'ACTION_NAME', '思考')}" if hasattr(avatar, "current_action") and avatar.current_action and getattr(avatar.current_action, "action", None) else "思考" } # 1. 特质 (Personas) diff --git a/src/classes/mutual_action/attack.py b/src/classes/mutual_action/attack.py index 11fcbb1..2695188 100644 --- a/src/classes/mutual_action/attack.py +++ b/src/classes/mutual_action/attack.py @@ -13,6 +13,7 @@ class MutualAttack(MutualAction): """攻击另一个NPC""" ACTION_NAME = "攻击" + EMOJI = "⚔️" DESC = "对目标进行攻击。" DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} diff --git a/src/classes/mutual_action/conversation.py b/src/classes/mutual_action/conversation.py index 593cd76..f1d0c8f 100644 --- a/src/classes/mutual_action/conversation.py +++ b/src/classes/mutual_action/conversation.py @@ -26,6 +26,7 @@ class Conversation(MutualAction): """ ACTION_NAME = "交谈" + EMOJI = "🗣️" DESC = "与对方进行一段交流对话" DOABLES_REQUIREMENTS = "目标在交互范围内" PARAMS = {"target_avatar": "AvatarName"} diff --git a/src/classes/mutual_action/drive_away.py b/src/classes/mutual_action/drive_away.py index da3dc74..9900ccc 100644 --- a/src/classes/mutual_action/drive_away.py +++ b/src/classes/mutual_action/drive_away.py @@ -13,6 +13,7 @@ class DriveAway(MutualAction): """驱赶:试图让对方离开当前区域。""" ACTION_NAME = "驱赶" + EMOJI = "😤" DESC = "以武力威慑对方离开此地。" DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} diff --git a/src/classes/mutual_action/dual_cultivation.py b/src/classes/mutual_action/dual_cultivation.py index fe0003f..fd73425 100644 --- a/src/classes/mutual_action/dual_cultivation.py +++ b/src/classes/mutual_action/dual_cultivation.py @@ -26,6 +26,7 @@ class DualCultivation(MutualAction): """ ACTION_NAME = "双修" + EMOJI = "💕" DESC = "以情入道的双修之术,仅合欢宗弟子可发起,对象可接受或拒绝" DOABLES_REQUIREMENTS = "发起者为合欢宗;目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} diff --git a/src/classes/mutual_action/gift_spirit_stone.py b/src/classes/mutual_action/gift_spirit_stone.py index a44dda6..31cbb1e 100644 --- a/src/classes/mutual_action/gift_spirit_stone.py +++ b/src/classes/mutual_action/gift_spirit_stone.py @@ -21,6 +21,7 @@ class GiftSpiritStone(MutualAction): """ ACTION_NAME = "赠送灵石" + EMOJI = "🎁" DESC = "向对方赠送灵石,一次赠送100灵石" DOABLES_REQUIREMENTS = "发起者至少有100灵石;目标在交互范围内" PARAMS = {"target_avatar": "AvatarName"} diff --git a/src/classes/mutual_action/impart.py b/src/classes/mutual_action/impart.py index 5f73a6d..2a7df23 100644 --- a/src/classes/mutual_action/impart.py +++ b/src/classes/mutual_action/impart.py @@ -25,6 +25,7 @@ class Impart(MutualAction): """ ACTION_NAME = "传道" + EMOJI = "📖" DESC = "师傅向徒弟传授修炼经验,徒弟可获得大量修为" DOABLES_REQUIREMENTS = "发起者是目标的师傅;师傅等级 > 徒弟等级 + 20;目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index 2c69492..e8c4ba6 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -31,6 +31,7 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin): """ ACTION_NAME: str = "MutualAction" + EMOJI: str = "💬" DESC: str = "" DOABLES_REQUIREMENTS: str = "交互范围内可互动" PARAMS: dict = {"target_avatar": "Avatar"} diff --git a/src/classes/mutual_action/occupy.py b/src/classes/mutual_action/occupy.py index 1604841..0f730d5 100644 --- a/src/classes/mutual_action/occupy.py +++ b/src/classes/mutual_action/occupy.py @@ -28,6 +28,7 @@ class Occupy(MutualAction): 对方拒绝则进入战斗,进攻方胜利则洞府易主。 """ ACTION_NAME = "抢夺洞府" + EMOJI = "🚩" DESC = "占据或抢夺洞府" PARAMS = {"region_name": "str"} FEEDBACK_ACTIONS = ["Yield", "Reject"] diff --git a/src/classes/mutual_action/spar.py b/src/classes/mutual_action/spar.py index 3466b40..4de72b2 100644 --- a/src/classes/mutual_action/spar.py +++ b/src/classes/mutual_action/spar.py @@ -21,6 +21,7 @@ class Spar(MutualAction): 切磋动作:双方切磋,不造成伤害,增加武器熟练度。 """ ACTION_NAME = "切磋" + EMOJI = "🤺" DESC = "与目标切磋武艺,点到为止(大幅增加武器熟练度,不造成伤害)" DOABLES_REQUIREMENTS = "交互范围内可互动;不能连续执行" FEEDBACK_ACTIONS = ["Accept", "Reject"] diff --git a/src/classes/mutual_action/talk.py b/src/classes/mutual_action/talk.py index d030bac..3c0c70e 100644 --- a/src/classes/mutual_action/talk.py +++ b/src/classes/mutual_action/talk.py @@ -19,6 +19,7 @@ class Talk(MutualAction): """ ACTION_NAME = "攀谈" + EMOJI = "👋" DESC = "向对方发起攀谈" DOABLES_REQUIREMENTS = "目标在交互范围内" PARAMS = {"target_avatar": "AvatarName"} diff --git a/src/server/main.py b/src/server/main.py index 616fbeb..187e065 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -99,6 +99,21 @@ def resolve_avatar_pic_id(avatar) -> int: gender_val = getattr(getattr(avatar, "gender", None), "value", "male") return get_avatar_pic_id(str(getattr(avatar, "id", "")), gender_val or "male") +def resolve_avatar_action_emoji(avatar) -> str: + """获取角色当前动作的 Emoji""" + if not avatar: + return "" + curr = getattr(avatar, "current_action", None) + if not curr: + return "" + + # ActionInstance.action -> Action 实例 + act_instance = getattr(curr, "action", None) + if not act_instance: + return "" + + return getattr(act_instance, "EMOJI", "") + # 触发配置重载的标记 (technique.csv updated) # 简易的命令行参数检查 (不使用 argparse 以避免冲突和时序问题) @@ -380,7 +395,8 @@ async def game_loop(): "y": int(getattr(a, "pos_y", 0)), "gender": a.gender.value, "pic_id": resolve_avatar_pic_id(a), - "action": getattr(a, "current_action", {}).get("name", "发呆") if hasattr(a, "current_action") and a.current_action else "发呆", + "action": getattr(a, "current_action", {}).get("name", "思考") if hasattr(a, "current_action") and a.current_action else "思考", + "action_emoji": resolve_avatar_action_emoji(a), "is_dead": False }) @@ -409,7 +425,8 @@ async def game_loop(): avatar_updates.append({ "id": str(a.id), "x": int(getattr(a, "pos_x", 0)), - "y": int(getattr(a, "pos_y", 0)) + "y": int(getattr(a, "pos_y", 0)), + "action_emoji": resolve_avatar_action_emoji(a) }) count += 1 @@ -597,6 +614,7 @@ def get_state(): "x": ax, "y": ay, "action": str(aaction), + "action_emoji": resolve_avatar_action_emoji(a), "gender": str(a.gender.value), "pic_id": resolve_avatar_pic_id(a) }) diff --git a/web/src/components/game/AnimatedAvatar.vue b/web/src/components/game/AnimatedAvatar.vue index 9e3f5a3..92e669b 100644 --- a/web/src/components/game/AnimatedAvatar.vue +++ b/web/src/components/game/AnimatedAvatar.vue @@ -55,8 +55,15 @@ useSharedTicker((delta) => { } else { currentY.value = destY } + + // Emoji bobbing animation + emojiTime += delta * 0.05 + emojiBob.value = Math.sin(emojiTime) * 5 }) +let emojiTime = 0 +const emojiBob = ref(0) + function getTexture() { const gender = (props.avatar.gender || 'male').toLowerCase() let pid = props.avatar.pic_id @@ -117,6 +124,57 @@ function handlePointerTap() { name: props.avatar.name }) } + +const emojiStyle = { + fontFamily: '"Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif', + fontSize: 70, + align: 'center', +} as any + +const drawEmojiBg = (g: Graphics) => { + g.clear() + + const w = 80 + const h = 80 + const r = 16 + const halfW = w / 2 + const halfH = h / 2 + + // 1. Draw all fills first (to cover background) + g.beginPath() + g.roundRect(-halfW, -halfH, w, h, r) + g.fill({ color: 0xffffff, alpha: 1.0 }) + + // Tail fill + g.beginPath() + g.moveTo(-halfW + 10, halfH) // Start at bottom-left area of body + g.lineTo(-halfW - 10, halfH + 20) // Point pointing down-left + g.lineTo(-halfW, halfH - 10) // Back to left edge of body + g.closePath() + g.fill({ color: 0xffffff, alpha: 1.0 }) + + // 2. Draw Strokes (Outlines) + // We draw the bubble body stroke + g.roundRect(-halfW, -halfH, w, h, r) + g.stroke({ width: 3, color: 0x000000, alpha: 1.0 }) + + // We draw the tail stroke + g.beginPath() + g.moveTo(-halfW + 10, halfH) + g.lineTo(-halfW - 10, halfH + 20) + g.lineTo(-halfW, halfH - 10) + g.stroke({ width: 3, color: 0x000000, alpha: 1.0 }) + + // 3. Clean up the intersection with a white patch + // We fill a small polygon over the line where tail meets body + g.beginPath() + g.moveTo(-halfW + 8, halfH - 2) // Inside body, near bottom + g.lineTo(-halfW - 2, halfH - 12) // Inside body, near left + g.lineTo(-halfW - 8, halfH + 16) // Towards tail tip (but not all the way) + g.lineTo(-halfW + 8, halfH + 2) // Towards tail base + g.closePath() + g.fill({ color: 0xffffff, alpha: 1.0 }) +}