1. add shield_duration translation and override config entry 2. add warning tip for shield duration setting with usage notes 3. control tip visibility based on whether shield_duration is configured
373 lines
18 KiB
Vue
373 lines
18 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/debuff 型覆写(timed_buff_id/timed_debuff_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>
|
||
<!-- 选中 proc_skill 后的触发语义提示:施法出手即判定,与命中/弹道数无关 -->
|
||
<el-alert
|
||
v-if="hasProcSkill"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
class="hint-alert"
|
||
style="margin-bottom: 6px"
|
||
title="proc_skill 为施法后触发"
|
||
description="出手即判定概率,一次施法只判定一次(多弹道/多召唤技能不会按实体数重复判定),不再依赖命中;即使技能落空也会触发。"
|
||
/>
|
||
<!-- 选中 debuff 后的取值语义提示:依 BuffList 配置(tick/modifier/易伤/控制)说明 debuff_value 的正负与生效方式 -->
|
||
<el-alert
|
||
v-if="debuffHint"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
class="hint-alert"
|
||
style="margin-bottom: 6px"
|
||
:title="debuffHint.title"
|
||
:description="debuffHint.desc"
|
||
/>
|
||
<!-- 选中护盾时长后的取值语义提示:仅护盾技能生效,0=永久易踩坑 -->
|
||
<el-alert
|
||
v-if="shieldDurationHint"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
class="hint-alert"
|
||
style="margin-bottom: 6px"
|
||
title="shield_duration 仅护盾技能(kind=Shield,如 6301)生效"
|
||
description="取值为秒数:>0 持续对应秒数(底座 6301 默认 7 秒);0 表示永久护盾(不是立即消失);不配置则沿用底座值。护盾数值由 ap 覆写控制(施法者最大生命 ap%)。"
|
||
/>
|
||
<!-- 选中 buff(增益)后的取值语义提示:缺省挂施法者自身;tick 类正=回血/负=自伤,modifier 类正=增强 -->
|
||
<el-alert
|
||
v-if="buffHint"
|
||
type="success"
|
||
:closable="false"
|
||
show-icon
|
||
class="hint-alert"
|
||
style="margin-bottom: 6px"
|
||
:title="buffHint.title"
|
||
:description="buffHint.desc"
|
||
/>
|
||
<el-alert
|
||
v-if="showEnemyBuffWarn"
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
class="hint-alert"
|
||
style="margin-bottom: 6px"
|
||
title="buff/debuff 配置提示"
|
||
description="buff 为增益语义:缺省挂施法者自身(如吸血/攻击后强化),可用 buff_target 改为全体友方;debuff 为对敌减益语义:缺省挂被命中者(目标不可覆写),对敌技能请用 timed_debuff_id。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'))
|
||
|
||
// 是否配置了 proc_skill(施法后概率触发主动技能):控制触发语义提示显隐
|
||
const hasProcSkill = computed(() => rows.value.some((r) => r.key === 'proc_skill'))
|
||
|
||
// 是否配置了 shield_duration:控制护盾时长取值语义提示显隐(0=永久易踩坑)
|
||
const shieldDurationHint = computed(() => rows.value.some((r) => r.key === 'shield_duration'))
|
||
|
||
// 易伤 buff id(与游戏侧 BuffSet.VULNERABLE_BUFF_ID 对应),取值语义特殊需单独提示
|
||
const VULNERABLE_ID = 7002
|
||
|
||
// 当前选中的 debuff 配置(timed_debuff_id 行)
|
||
const selectedDebuff = computed(() => {
|
||
const row = rows.value.find((r) => r.key === 'timed_debuff_id')
|
||
const id = typeof row?.value === 'number' ? row.value : Number(row?.value)
|
||
if (!id || isNaN(id)) return null
|
||
return props.buffOptions.find((b) => b.id === id) ?? null
|
||
})
|
||
|
||
// 依 BuffList 配置推导 debuff_value 取值语义提示(正负号 / 生效方式 / 叠层)
|
||
const debuffHint = computed(() => {
|
||
const buff = selectedDebuff.value
|
||
if (!buff) return null
|
||
const stackTxt = buff.max_stack === 0 ? '无限叠层' : buff.max_stack > 1 ? `可叠 ${buff.max_stack} 层` : '不可叠层'
|
||
// 控制类:无数值语义
|
||
if (buff.is_control) {
|
||
return { title: `「${buff.name}」为控制类 debuff`, desc: `无需配置 debuff_value(不生效)。${stackTxt},持续 ${buff.duration}s。` }
|
||
}
|
||
// 易伤:正值,被打时按层累加附加伤害,可被易伤抗性减免
|
||
if (buff.id === VULNERABLE_ID) {
|
||
return { title: `「${buff.name}」debuff_value 用正值`, desc: `含义=每层被击中时附加的固定伤害。取值 10 → 目标每次被打多掉 10 血,${stackTxt}时按层累加(N 层=N×value),可被目标易伤抗性减免。负值无效(被拦截为 0)。结算在暴击后、护盾前,不吃暴击。` }
|
||
}
|
||
// tick 类(DoT/HoT):负=伤害 正=治疗,每 interval 结算,可按 ap 缩放
|
||
if (buff.tick) {
|
||
const scaleTxt = buff.tick.scale_by_ap_pct > 0 ? `,并按施法者 ap 缩放(每100 ap +${buff.tick.scale_by_ap_pct}%)` : ',不随 ap 缩放'
|
||
return { title: `「${buff.name}」为周期伤害 debuff,debuff_value 用负值表伤害`, desc: `含义=每 ${buff.tick.interval}s 一次的基础伤害(负值=掉血,正值=回血)。取值 -10 → 每跳约 10 点伤害${scaleTxt}。${stackTxt},持续 ${buff.duration}s。` }
|
||
}
|
||
// modifier 类(属性修饰):正负表增减,直接改属性
|
||
if (buff.modifiers?.length) {
|
||
const mod = buff.modifiers[0]
|
||
return { title: `「${buff.name}」为属性修饰 debuff`, desc: `debuff_value 覆写修饰数值(负=削减,正=增强),作用于属性 ${mod.attr}(${mod.op === 1 ? '百分比' : '固定值'})。${stackTxt},持续 ${buff.duration}s。` }
|
||
}
|
||
return { title: `「${buff.name}」`, desc: `${stackTxt},持续 ${buff.duration}s。` }
|
||
})
|
||
|
||
// 当前选中的 buff 配置(timed_buff_id 行)
|
||
const selectedBuff = computed(() => {
|
||
const row = rows.value.find((r) => r.key === 'timed_buff_id')
|
||
const id = typeof row?.value === 'number' ? row.value : Number(row?.value)
|
||
if (!id || isNaN(id)) return null
|
||
return props.buffOptions.find((b) => b.id === id) ?? null
|
||
})
|
||
|
||
// buff(增益)取值语义提示:缺省挂施法者自身;tick 类正=回血/负=自伤,modifier 类正=增强
|
||
const buffHint = computed(() => {
|
||
const buff = selectedBuff.value
|
||
if (!buff) return null
|
||
const stackTxt = buff.max_stack === 0 ? '无限叠层' : buff.max_stack > 1 ? `可叠 ${buff.max_stack} 层` : '不可叠层'
|
||
const targetTxt = '缺省挂施法者自身,可用 buff_target 改全体友方'
|
||
if (buff.is_control) {
|
||
return { title: `「${buff.name}」为控制类`, desc: `无需配置 buff_value(不生效)。${targetTxt}。${stackTxt},持续 ${buff.duration}s。` }
|
||
}
|
||
if (buff.tick) {
|
||
const scaleTxt = buff.tick.scale_by_ap_pct > 0 ? `,并按自身 ap 缩放(每100 ap +${buff.tick.scale_by_ap_pct}%)` : ',不随 ap 缩放'
|
||
return { title: `「${buff.name}」为周期效果 buff`, desc: `buff_value 正值=每 ${buff.tick.interval}s 回血,负值=自伤(如自残出血)。取值 10 → 每跳约回 10 血${scaleTxt}。${targetTxt}。${stackTxt},持续 ${buff.duration}s。` }
|
||
}
|
||
if (buff.modifiers?.length) {
|
||
const mod = buff.modifiers[0]
|
||
return { title: `「${buff.name}」为属性修饰 buff`, desc: `buff_value 覆写修饰数值(正=增强,负=削弱),作用于属性 ${mod.attr}(${mod.op === 1 ? '百分比' : '固定值'})。${targetTxt}。${stackTxt},持续 ${buff.duration}s。` }
|
||
}
|
||
return { title: `「${buff.name}」`, desc: `${targetTxt}。${stackTxt},持续 ${buff.duration}s。` }
|
||
})
|
||
|
||
// 本地行数组,允许 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, item.key === 'timed_debuff_id')
|
||
}
|
||
|
||
// 挂载 timed_buff_id / timed_debuff_id 后自动补齐配套三键(已存在的不重复添加):
|
||
// 数值预填 BuffList 默认值(modifiers[0].value / tick.damage_or_heal / duration),避免导出 0 值覆盖 buff 语义
|
||
// isDebuff=true 时补 debuff_* 键,否则补 buff_* 键
|
||
function ensureBuffCompanionRows(buffId, isDebuff = false) {
|
||
const buff = typeof buffId === 'number' ? props.buffOptions.find((b) => b.id === buffId) : null
|
||
const prefix = isDebuff ? 'debuff' : 'buff'
|
||
const has = (key) => rows.value.some((r) => r.key === key)
|
||
if (!has(`${prefix}_value`)) {
|
||
rows.value.push({ key: `${prefix}_value`, value: buff?.modifiers?.[0]?.value ?? buff?.tick?.damage_or_heal ?? 0 })
|
||
}
|
||
if (!has(`${prefix}_chance`)) {
|
||
rows.value.push({ key: `${prefix}_chance`, value: 100 })
|
||
}
|
||
if (!has(`${prefix}_duration`)) {
|
||
rows.value.push({ key: `${prefix}_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); }
|
||
/* debuff 取值提示:压缩内边距并用 12px 小字,与编辑器其他辅助文本一致 */
|
||
.hint-alert { padding: 4px 8px; }
|
||
.hint-alert :deep(.el-alert__title) { font-size: 12px; line-height: 1.4; }
|
||
.hint-alert :deep(.el-alert__description) { font-size: 12px; line-height: 1.4; margin-top: 2px; }
|
||
.hint-alert :deep(.el-alert__icon) { font-size: 14px; width: 14px; }
|
||
</style>
|