1. 为OverrideEditor新增baseSkillUuid参数,支持对敌技能debuff配置校验 2. 优化proc_skill和timed_buff_id的编辑体验,自动补全关联配置项 3. 更新常量注释与英雄技能配置,修正overrides字段配置 4. 新增对敌技能buff配置警示提示,引导正确配置方式
250 lines
10 KiB
Vue
250 lines
10 KiB
Vue
<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>
|