Files
pixelheros/put/web/src/components/OverrideEditor.vue
pan f5908a9adf feat(editor): 完善技能覆写编辑器功能,优化配置体验
1. 为OverrideEditor新增baseSkillUuid参数,支持对敌技能debuff配置校验
2. 优化proc_skill和timed_buff_id的编辑体验,自动补全关联配置项
3. 更新常量注释与英雄技能配置,修正overrides字段配置
4. 新增对敌技能buff配置警示提示,引导正确配置方式
2026-09-08 10:38:18 +08:00

250 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="override-editor">
<div v-for="(item, idx) in rows" :key="idx" class="override-row">
<el-select v-model="item.key" placeholder="字段" size="small" style="width: 220px" filterable @change="onKeyChange(item)">
<el-option v-for="k in availableKeys(idx)" :key="k.key" :label="k.label" :value="k.key" />
</el-select>
<!-- 字符串型覆写(icon):输入帧名,提供常用候选可筛选 -->
<el-select
v-if="isStringKey(item.key)"
v-model="item.value"
placeholder="图标帧名"
size="small"
style="width: 280px"
filterable
allow-create
default-first-option
>
<el-option v-for="opt in SKILL_ICON_OPTIONS" :key="opt" :label="opt" :value="opt" />
</el-select>
<!-- 枚举型覆写(buff_target/TGroup):下拉选枚举值 -->
<el-select
v-else-if="isEnumKey(item.key)"
v-model="item.value"
placeholder="枚举值"
size="small"
style="width: 220px"
>
<el-option v-for="opt in enumOptions(item.key)" :key="String(opt.value)" :label="opt.label" :value="opt.value" />
</el-select>
<!-- 对象型覆写(proc_skill):命中后概率触发额外技能,结构化编辑 s_uuid/chance/overrides -->
<template v-else-if="isObjectKey(item.key)">
<div class="proc-skill-editor">
<el-select
v-model="item.value.s_uuid"
placeholder="被触发技能 uuid"
size="small"
style="width: 220px"
filterable
@change="onProcSkillChange(item)"
>
<el-option v-for="s in skillOptions" :key="s.uuid" :label="`${s.uuid} ${s.name}`" :value="s.uuid" />
</el-select>
<el-input-number
v-model="item.value.chance"
:min="0"
:max="100"
:controls="false"
size="small"
placeholder="概率%"
style="width: 90px"
/>
<div class="proc-skill-overrides">
<span class="proc-skill-label">被触发技能覆写:</span>
<OverrideEditor v-model="item.value.overrides" :skill-options="skillOptions" :buff-options="buffOptions" :base-skill-uuid="item.value.s_uuid" />
</div>
</div>
</template>
<!-- buff 型覆写(timed_buff_id):BuffList 下拉,副文本展示 buff info;allow-create 兼容 meta 未加载时手输 id -->
<el-select
v-else-if="isBuffKey(item.key)"
v-model="item.value"
placeholder="选择 buff id"
size="small"
style="width: 280px"
filterable
allow-create
default-first-option
@change="onBuffValueChange(item)"
>
<el-option v-for="b in buffOptions" :key="b.id" :label="`${b.id} ${b.name}`" :value="b.id">
<span>{{ b.id }} {{ b.name }}</span>
<span class="muted" style="float: right">{{ b.info }}</span>
</el-option>
</el-select>
<el-input-number v-else v-model="item.value" size="small" :controls="false" style="width: 120px" />
<el-button type="danger" size="small" text @click="remove(idx)">删除</el-button>
</div>
<el-alert
v-if="showEnemyBuffWarn"
type="info"
:closable="false"
show-icon
style="margin-bottom: 6px"
title="对敌技能 debuff 配置提示"
description="对敌技能的命中附带 debuff 以 SkillSet 底座配置为准,此处 overrides 注入建议用于友方/辅助技能(可用 buff_target 分流挂敌);吸血类需 buff_target=1(施法者自身)。proc_skill 用于命中后概率触发主动技能,不是挂 buff 的方式。"
/>
<el-button size="small" @click="add">+ 添加覆盖</el-button>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { OVERRIDE_KEYS, SKILL_ICON_OPTIONS } from '../constants'
const props = defineProps({
modelValue: { type: Object, default: () => ({}) },
// 全部技能候选(proc_skill.s_uuid 下拉用),由 App 经 SkillEditor 透传
skillOptions: { type: Array, default: () => [] },
// 全部 buff 候选(timed_buff_id 下拉用),由 App 从 meta.BuffList 计算后透传
buffOptions: { type: Array, default: () => [] },
// 宿主技能 uuid:用于判定对敌技能(TGroup=Enemy)注入 timed_buff_id 的断链警示
baseSkillUuid: { type: Number, default: null },
})
const emit = defineEmits(['update:modelValue'])
// 宿主技能是否走对敌路径:有效 TGroup = overrides.TGroup 覆写值 ?? 底座 TGroup(Enemy=3;meta 未加载时不判定)
const isEnemyHost = computed(() => {
if (props.baseSkillUuid == null) return false
const base = props.skillOptions.find((s) => s.uuid === props.baseSkillUuid)
const tgroupRow = rows.value.find((r) => r.key === 'TGroup')
const effective = tgroupRow?.value ?? base?.TGroup
return effective === 3 || effective === 'Enemy'
})
// 对敌技能且注入了 timed_buff_id → 运行时断链不生效,警示引导改用 proc_skill / 底座表
const showEnemyBuffWarn = computed(() => isEnemyHost.value && rows.value.some((r) => r.key === 'timed_buff_id'))
// 本地行数组,允许 key 为空
const rows = ref(Object.entries(props.modelValue || {}).map(([key, value]) => ({ key, value })))
// 该行是否满足输出条件(proc_skill 需有合法 s_uuid,其余仅需有 key)
function isRowOutput(it) {
if (!it.key) return false
if (it.key === 'proc_skill') return !!it.value?.s_uuid
return true
}
// 外部 modelValue 变化时同步(如切换英雄):
// 仅当外部值与"当前可输出的行"不一致时才覆盖;proc_skill 编辑中(s_uuid 未填)的行不输出但需保留,
// 否则会因 modelValue 缺少 proc_skill 而把用户正在编辑的行整个清掉(表现为"添加无效")
watch(() => props.modelValue, (v) => {
const next = Object.entries(v || {}).map(([key, value]) => ({ key, value }))
const outputRows = rows.value.filter(isRowOutput)
if (JSON.stringify(next) !== JSON.stringify(outputRows)) {
// 保留编辑中(key 已选但未达输出条件)的行,追加在外部同步结果之后
const editing = rows.value.filter((r) => r.key && !isRowOutput(r))
rows.value = [...next, ...editing]
}
}, { deep: true })
// 每次行变化都尝试同步(过滤空 key;proc_skill 需有合法 s_uuid 才输出)
watch(rows, (arr) => {
const obj = {}
for (const it of arr) {
if (!isRowOutput(it)) continue
obj[it.key] = it.value
}
emit('update:modelValue', obj)
}, { deep: true })
// 已选中的 key 不再出现在其他行的下拉中
function availableKeys(idx) {
const used = new Set(rows.value.map((r, i) => (i === idx ? null : r.key)).filter(Boolean))
return OVERRIDE_KEYS.filter((k) => !used.has(k.key))
}
// 该覆写字段是否为字符串型(icon 取帧名,非数值)
function isStringKey(key) {
return OVERRIDE_KEYS.find((k) => k.key === key)?.type === 'string'
}
// 该覆写字段是否为对象型(proc_skill:{s_uuid, chance, overrides})
function isObjectKey(key) {
return OVERRIDE_KEYS.find((k) => k.key === key)?.type === 'object'
}
// 该覆写字段是否为枚举型(buff_target/TGroup 等)
function isEnumKey(key) {
return OVERRIDE_KEYS.find((k) => k.key === key)?.type === 'enum'
}
// 枚举型覆写的候选值列表
function enumOptions(key) {
return OVERRIDE_KEYS.find((k) => k.key === key)?.options || []
}
// 该覆写字段是否为 buff 型(timed_buff_id:值为 BuffList 配置 id)
function isBuffKey(key) {
return OVERRIDE_KEYS.find((k) => k.key === key)?.type === 'buff'
}
// 选中 proc_skill 被触发技能后,在其内嵌 overrides 自动补 TGroup 行(已存在不覆盖):
// 值取该技能底座的 TGroup,让内嵌编辑器能立刻按有效目标群体做对敌断链警示等判定,也便于直改释放对象
function onProcSkillChange(item) {
const overrides = item.value?.overrides
if (!overrides) return
if (overrides.TGroup !== undefined) return
const base = props.skillOptions.find((s) => s.uuid === item.value.s_uuid)
const name = TGROUP_NAMES[base?.TGroup]
if (name) overrides.TGroup = name
}
// 底座 TGroup 数值 → 字符串名(编辑器 TGroup 下拉 value 为字符串,导出时还原 TGroup.X 枚举表达式)
const TGROUP_NAMES = ['Self', 'Ally', 'Team', 'Enemy', 'All']
// allow-create 手输的 buff id 是字符串,归一化为数字,保证导出 TS 配置类型正确
function onBuffValueChange(item) {
if (typeof item.value === 'string' && item.value.trim() !== '' && !isNaN(Number(item.value))) {
item.value = Number(item.value)
}
ensureBuffCompanionRows(item.value)
}
// 挂载 timed_buff_id 后自动补齐配套三键(已存在的不重复添加):
// 数值预填 BuffList 默认值(modifiers[0].value / tick.damage_or_heal / duration),避免导出 0 值覆盖 buff 语义
function ensureBuffCompanionRows(buffId) {
const buff = typeof buffId === 'number' ? props.buffOptions.find((b) => b.id === buffId) : null
const has = (key) => rows.value.some((r) => r.key === key)
if (!has('buff_value')) {
rows.value.push({ key: 'buff_value', value: buff?.modifiers?.[0]?.value ?? buff?.tick?.damage_or_heal ?? 0 })
}
if (!has('buff_chance')) {
rows.value.push({ key: 'buff_chance', value: 100 })
}
if (!has('buff_duration')) {
rows.value.push({ key: 'buff_duration', value: buff?.duration ?? 5 })
}
}
// 对象型覆写行选中时初始化默认结构,避免 v-model 绑定到 undefined 子字段
function onKeyChange(item) {
if (isObjectKey(item.key)) {
item.value = { s_uuid: undefined, chance: 100, overrides: {} }
} else if (isStringKey(item.key)) {
item.value = ''
} else if (isEnumKey(item.key)) {
// 枚举默认取首个候选值
item.value = enumOptions(item.key)[0]?.value
} else if (isBuffKey(item.key)) {
item.value = undefined // buff id 必须显式选择,0 不是合法 id
} else {
item.value = 0
}
}
function add() {
rows.value.push({ key: '', value: 0 })
}
function remove(idx) {
rows.value.splice(idx, 1)
}
</script>
<style scoped>
.override-row { display: flex; gap: 8px; margin-bottom: 6px; align-items: center; }
/* proc_skill 结构化编辑:占满整行,子字段横排,内嵌覆写编辑器缩进展示 */
.proc-skill-editor { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap; flex: 1; }
.proc-skill-overrides { width: 100%; padding-left: 12px; border-left: 2px solid var(--el-border-color); }
.proc-skill-label { font-size: 12px; color: var(--el-text-color-secondary); }
</style>