add emoji to frontend

This commit is contained in:
bridge
2026-01-01 15:08:09 +08:00
parent f301d67493
commit 561f1efe21
32 changed files with 141 additions and 2 deletions

View File

@@ -69,6 +69,8 @@ class Action(ABC):
"""
return str(self.__class__.__name__)
EMOJI: str = ""
def get_save_data(self) -> dict:
"""获取需要存档的运行时数据"""
return {}

View File

@@ -19,6 +19,7 @@ if TYPE_CHECKING:
@cooldown_action
class Assassinate(InstantAction):
ACTION_NAME = "暗杀"
EMOJI = "🗡️"
DESC = "暗杀目标,失败则变为战斗"
DOABLES_REQUIREMENTS = "无限制;需要冷却"
PARAMS = {"avatar_name": "AvatarName"}

View File

@@ -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"}

View File

@@ -26,6 +26,7 @@ class Breakthrough(TimedAction):
"""
ACTION_NAME = "突破"
EMOJI = ""
DESC = "尝试突破境界(成功增加寿元上限,失败折损寿元上限;境界越高,成功率越低。)"
DOABLES_REQUIREMENTS = "角色处于瓶颈时;不能连续执行"
PARAMS = {}

View File

@@ -24,6 +24,7 @@ class Catch(TimedAction):
"""
ACTION_NAME = "御兽"
EMOJI = "🕸️"
DESC = "尝试驯服一只灵兽,成为自身灵兽。只能有一只灵兽,但是可以高级替换低级。"
DOABLES_REQUIREMENTS = "仅百兽宗;在有动物的普通区域;目标动物境界不高于角色"
PARAMS = {}

View File

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

View File

@@ -11,6 +11,7 @@ class DevourMortals(TimedAction):
"""
ACTION_NAME = "吞噬凡人"
EMOJI = "🩸"
DESC = "吞噬凡人,较多增加战力"
DOABLES_REQUIREMENTS = "持有万魂幡"
PARAMS = {}

View File

@@ -15,6 +15,7 @@ class Escape(InstantAction):
"""
ACTION_NAME = "逃离"
EMOJI = "💨"
DESC = "逃离对方(基于成功率判定)"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {"avatar_name": "AvatarName"}

View File

@@ -13,6 +13,7 @@ class Harvest(TimedAction):
"""
ACTION_NAME = "采集"
EMOJI = "🌾"
DESC = "在当前区域采集植物,获取植物材料"
DOABLES_REQUIREMENTS = "在有植物的普通区域且avatar的境界必须大于等于植物的境界"
PARAMS = {}

View File

@@ -13,6 +13,7 @@ class HelpMortals(TimedAction):
"""
ACTION_NAME = "帮助凡人"
EMOJI = "🤝"
DESC = "在城镇帮助凡人,消耗少量灵石"
DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘正’,并且灵石足够"
PARAMS = {}

View File

@@ -13,6 +13,7 @@ class Hunt(TimedAction):
"""
ACTION_NAME = "狩猎"
EMOJI = "🏹"
DESC = "在当前区域狩猎动物,获取动物材料"
DOABLES_REQUIREMENTS = "在有动物的普通区域且avatar的境界必须大于等于动物的境界"
PARAMS = {}

View File

@@ -10,6 +10,7 @@ class Move(DefineAction, ChunkActionMixin):
"""
ACTION_NAME = "移动"
EMOJI = "🏃"
DESC = "移动到某个相对位置"
PARAMS = {"delta_x": "int", "delta_y": "int"}

View File

@@ -11,6 +11,7 @@ class NurtureWeapon(TimedAction):
"""
ACTION_NAME = "温养兵器"
EMOJI = ""
DESC = "温养兵器,增加兵器熟练度"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {}

View File

@@ -10,6 +10,7 @@ class Play(TimedAction):
"""
ACTION_NAME = "消遣"
EMOJI = "🪁"
DESC = "消遣,放松身心"
DOABLES_REQUIREMENTS = "无限制"
PARAMS = {}

View File

@@ -13,6 +13,7 @@ class PlunderMortals(TimedAction):
"""
ACTION_NAME = "搜刮凡人"
EMOJI = "💀"
DESC = "在城镇搜刮凡人,获取少量灵石"
DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘邪’"
PARAMS = {}

View File

@@ -12,6 +12,7 @@ class SelfHeal(TimedAction):
"""
ACTION_NAME = "疗伤"
EMOJI = "💚"
DESC = "在宗门总部静养疗伤回满HP"
DOABLES_REQUIREMENTS = "自己是宗门弟子且位于本宗门总部区域且当前HP未满"
PARAMS = {}

View File

@@ -14,6 +14,7 @@ class SellItems(InstantAction):
"""
ACTION_NAME = "出售物品"
EMOJI = "💰"
DESC = "在城镇出售持有的某类物品的全部"
DOABLES_REQUIREMENTS = "在城镇且背包非空"
PARAMS = {"item_name": "str"}

View File

@@ -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)

View File

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

View File

@@ -26,6 +26,7 @@ class Conversation(MutualAction):
"""
ACTION_NAME = "交谈"
EMOJI = "🗣️"
DESC = "与对方进行一段交流对话"
DOABLES_REQUIREMENTS = "目标在交互范围内"
PARAMS = {"target_avatar": "AvatarName"}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"}

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ class Talk(MutualAction):
"""
ACTION_NAME = "攀谈"
EMOJI = "👋"
DESC = "向对方发起攀谈"
DOABLES_REQUIREMENTS = "目标在交互范围内"
PARAMS = {"target_avatar": "AvatarName"}

View File

@@ -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)
})

View File

@@ -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 })
}
</script>
<template>
@@ -141,6 +199,22 @@ function handlePointerTap() {
@render="drawFallback"
/>
<!-- Emoji Bubble -->
<container
v-if="avatar.action_emoji"
:x="tileSize * 0.6"
:y="(getTexture() ? -tileSize * 3.5 : -tileSize * 1.2) + emojiBob"
:z-index="100"
>
<graphics @render="drawEmojiBg" />
<text
:text="avatar.action_emoji"
:style="emojiStyle"
:anchor="0.5"
:scale="1.0"
/>
</container>
<text
:text="avatar.name"
:style="nameStyle"

View File

@@ -76,6 +76,11 @@ async function handleClearObjective() {
</div>
<div class="content-scroll">
<!-- Action State Banner -->
<div v-if="!data.is_dead && data.action_state" class="action-banner">
{{ data.action_state }}
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<StatItem label="境界" :value="data.realm" :sub-value="data.level" />
@@ -226,6 +231,17 @@ async function handleClearObjective() {
border: 1px solid #7a2a2a;
}
.action-banner {
background: rgba(23, 125, 220, 0.15);
color: #aaddff;
padding: 8px;
border-radius: 4px;
text-align: center;
font-size: 13px;
margin-bottom: 8px;
border: 1px solid rgba(23, 125, 220, 0.3);
}
.content-scroll {
flex: 1;
overflow-y: auto;

View File

@@ -36,6 +36,7 @@ export interface Item extends EffectEntity {
export interface AvatarSummary extends EntityBase, Coordinates {
action?: string;
action_emoji?: string;
gender?: string;
pic_id?: number;
is_dead?: boolean;
@@ -49,6 +50,7 @@ export interface AvatarDetail extends EntityBase {
nickname?: string;
appearance: string; // 外貌描述
is_dead?: boolean;
action_state?: string; // 当前正在进行的动作描述
death_info?: {
time: number;
reason: string;