2 Commits

Author SHA1 Message Date
pan
ba3810d00a feat(skill-editor): 新增普攻lv2强化档编辑模式
1. 为SkillEditor新增basicLv2属性,实现独立的普攻lv2档位编辑逻辑
2. 锁定lv和SkillLv值,禁用档位增删操作
3. 调整界面文案,适配普攻lv2模式的编辑提示
4. 自动规范化lv_entries数据,确保仅存在一个标准lv2档位
2026-09-04 16:39:41 +08:00
pan
e97de40d2e feat: 新增技能图标英雄语境覆写功能
1. 扩展SkillOverrides接口,新增icon字段支持自定义图标
2. 前端常量与编辑器适配:新增字符串型覆写类型与图标候选选择器
3. 新增getHeroSkillIcon工具函数,实现英雄专属图标优先级加载逻辑
4. 修复并优化技能图标展示逻辑,支持同底座技能不同英雄差异化图标
5. 重构部分英雄配置格式,统一代码风格
2026-09-04 16:25:57 +08:00
9 changed files with 139 additions and 22 deletions

View File

@@ -188,6 +188,9 @@ export interface SkillOverrides {
revive_hp?: number;
/** 复活次数上限,配合 revive_hp 使用(仅 dead 触发有效) */
revive_count?: number;
/** 英雄语境专属图标(图鉴面板/技能弹窗展示用):配置后覆盖 SkillSet.icon,
* 实现"同一底座、不同英雄/档位不同外观"(如 5002 普攻与冲刺斩同 6004 但图标不同) */
icon?: string;
}
/**

View File

@@ -324,6 +324,64 @@ export function getFieldGroupName(hero: heroInfo | undefined): string {
return firstUuid ? (FieldSkillSet[firstUuid]?.name ?? "") : "";
}
/**
* 解析技能的对外展示图标(英雄语境):英雄/触发引用侧通过 overrides.icon 覆写时优先,
* 缺省回退 SkillSet 底座 icon;驻场光环词条统一取 FieldSkillSet 底座 icon。
* Why: 图标是"对外展示"维度(与数值覆写同一套注入机制),实现同一底座不同英雄/档位不同外观。
* @param hero 英雄配置
* @param skillUuid 技能 uuid(skills key / 触发组 s_uuid / FieldSkillSet uuid)
* @param slot 技能所属槽位(可空):指定时仅在该槽位查找;缺省按 普攻/大招→触发组 顺序定位
* @param isGroup 整组模式:true 时取组内首个 s_uuid 的图标(slot 必填,光环取首个词条)
* @returns 图标ID;未找到返回 undefined
*/
export function getHeroSkillIcon(hero: heroInfo | undefined, skillUuid: number, slot?: SkillTriggerType, isGroup: boolean = false): string | undefined {
if (!hero) return undefined;
// 整组模式:组内首个 s_uuid/词条的图标
if (isGroup && slot !== undefined) {
if (slot === SkillTriggerType.Field) {
const firstEntry = hero.field?.[0]?.uuids?.[0];
return firstEntry ? FieldSkillSet[firstEntry.uuid]?.icon : undefined;
}
const grouped = hero[slot] as TriggerGrouped | undefined;
if (!grouped) return undefined;
for (const key in grouped) {
const icon = resolveTriggerSkillIcon(grouped[Number(key)]?.[0], Number(key));
if (icon) return icon;
}
return undefined;
}
// 普攻/大招(slot 缺省时优先定位):lv_entries 首档 icon > 基础 overrides.icon > 底座
const hSkill = hero.skills?.[skillUuid];
if (hSkill && slot === undefined) {
return hSkill.lv_entries?.[0]?.overrides?.icon
?? hSkill.overrides?.icon
?? SkillSet[skillUuid]?.icon;
}
// 指定触发槽位:该组首个 s_uuid 首档 icon
if (slot !== undefined && slot !== SkillTriggerType.Field) {
const grouped = hero[slot] as TriggerGrouped | undefined;
const entry = grouped?.[skillUuid]?.[0];
if (entry) return resolveTriggerSkillIcon(entry, skillUuid);
return undefined;
}
// 自动定位:再各触发组,最后光环
for (const type of [SkillTriggerType.Call, SkillTriggerType.Dead, SkillTriggerType.FStart, SkillTriggerType.Atking, SkillTriggerType.Atked]) {
const grouped = hero[type] as TriggerGrouped | undefined;
const entry = grouped?.[skillUuid]?.[0];
if (entry) return resolveTriggerSkillIcon(entry, skillUuid);
}
if (hero.field?.some(t => t.uuids.some(u => u.uuid === skillUuid))) {
return FieldSkillSet[skillUuid]?.icon;
}
return undefined;
}
/** 触发技能档位图标:档位 overrides.icon 优先,缺省回退该 s_uuid 的底座 icon */
function resolveTriggerSkillIcon(entry: LvTriggerEntry | undefined, skillUuid: number): string | undefined {
if (!entry) return undefined;
return entry.overrides?.icon ?? SkillSet[skillUuid]?.icon;
}
/**
* 复活已纳入 dead 触发组:通过 SkillOverrides.revive_hp / revive_count 配置,
* 不再需要独立的 revive 字段和解析函数。
@@ -504,7 +562,7 @@ export const HeroInfo: Record<number, heroInfo> = {
},
atked: {
6301: [
{ lv: 4, s_lv: SkillLv.Lv1, name: "全体圣盾", t_num: 3, overrides: {"TGroup":null,"ap":10,"summon_count":1}, desc: "全队获得本英雄生命最大值{ap}%的护盾" },
{ lv: 4, s_lv: SkillLv.Lv1, name: "全体圣盾", t_num: 3, overrides: {"TGroup":null,"ap":10,"icon":"Stat_UnitSummonIncrease_01"}, desc: "全队获得本英雄生命最大值{ap}%的护盾" },
],
},
info: "",

View File

@@ -6,7 +6,7 @@ import { CardTriggerType } from "../common/config/CardSet";
import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import {
FieldEntry, HeroInfo, TriggerGrouped, SkillTriggerType,
getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName,
getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName, getHeroSkillIcon,
} from "../common/config/heroSet";
import { UIID } from "../common/config/GameUIConfig";
import { smc } from "../common/SingletonModuleComp";
@@ -234,15 +234,19 @@ export class SSkillBoxComp extends CCComp {
// 优先使用卡牌自定义 icon;未定义则按图鉴模式 / 装备卡 trigger_type 自动取
let iconId: string | undefined = this.card_icon;
if (!iconId) {
if (this.hero_icon_source === "field") {
// 英雄图鉴:驻场光环词条图标取 FieldSkillSet
if (this.hero_uuid > 0) {
// 英雄图鉴:英雄语境图标优先(overrides.icon 覆写),底座 icon 兜底
iconId = getHeroSkillIcon(HeroInfo[this.hero_uuid], this.s_uuid, this.hero_slot, this.hero_group);
}
if (!iconId && this.hero_icon_source === "field") {
// 光环词条图标取 FieldSkillSet
iconId = this.s_uuid ? FieldSkillSet[this.s_uuid]?.icon : undefined;
} else if (this.hero_icon_source === "skill") {
// 英雄图鉴:大招/触发技能图标取 SkillSet
} else if (!iconId && this.hero_icon_source === "skill") {
// 大招/触发技能图标取 SkillSet
iconId = this.s_uuid ? SkillSet[this.s_uuid]?.icon : undefined;
} else if (this.trigger_type === CardTriggerType.Interval) {
} else if (!iconId && this.trigger_type === CardTriggerType.Interval) {
iconId = this.s_uuid ? SkillSet[this.s_uuid]?.icon : undefined;
} else if (this.trigger_type === CardTriggerType.Field) {
} else if (!iconId && this.trigger_type === CardTriggerType.Field) {
const fieldUuid = this.field?.[0]?.uuid;
iconId = fieldUuid ? FieldSkillSet[fieldUuid]?.icon : undefined;
}

View File

@@ -35,7 +35,7 @@ import { oops } from "db://oops-framework/core/Oops";
import { CardTriggerType, CardConfig } from "../common/config/CardSet";
import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import { SkillCardList } from "../common/config/SCardSet";
import { FieldEntry, SkillTriggerType } from "../common/config/heroSet";
import { FieldEntry, SkillTriggerType, HeroInfo, getHeroSkillIcon } from "../common/config/heroSet";
import { buildCardDescRich } from "../common/config/CardSkillText";
import { buildHeroSkillTierLines, buildHeroGroupTierLines, HeroSkillTierLine } from "../common/HeroSkillText";
import { smc } from "../common/SingletonModuleComp";
@@ -372,6 +372,10 @@ export class SinfoComp extends CCComp {
/** 解析图标ID */
private resolveIconId(p: SInfoParams): string | undefined {
if (p.card_icon) return p.card_icon;
// 英雄图鉴模式:英雄语境图标优先(overrides.icon 覆写),底座 icon 兜底
if (p.hero_uuid) {
return getHeroSkillIcon(HeroInfo[p.hero_uuid], p.uuid, p.hero_slot, p.hero_group);
}
// 技能卡:从 SkillCardList 查 icon
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard?.icon) return skillCard.icon;

View File

@@ -77,7 +77,7 @@
"overrides": {
"TGroup": null,
"ap": 10,
"summon_count": 1
"icon": "Stat_UnitSummonIncrease_01"
},
"desc": "全队获得本英雄生命最大值{ap}%的护盾"
}
@@ -764,4 +764,4 @@
"info": ""
}
]
}
}

View File

@@ -114,7 +114,7 @@
<!-- 普攻 -->
<div class="panel">
<h3>普攻 skills[0]</h3>
<SkillEditor v-if="basicSkill" :skill="basicSkill" :options="basicSkillOptions" />
<SkillEditor v-if="basicSkill" :skill="basicSkill" :options="basicSkillOptions" :basic-lv2="true" />
<el-button v-else size="small" type="primary" @click="addBasicSkill">+ 添加普攻</el-button>
</div>

View File

@@ -1,10 +1,23 @@
<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: 200px" filterable>
<el-select v-model="item.key" placeholder="字段" size="small" style="width: 220px" filterable>
<el-option v-for="k in availableKeys(idx)" :key="k.key" :label="k.label" :value="k.key" />
</el-select>
<el-input-number v-model="item.value" size="small" :controls="false" style="width: 120px" />
<!-- 字符串型覆写(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>
<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-button size="small" @click="add">+ 添加覆盖</el-button>
@@ -13,7 +26,7 @@
<script setup>
import { ref, watch } from 'vue'
import { OVERRIDE_KEYS } from '../constants'
import { OVERRIDE_KEYS, SKILL_ICON_OPTIONS } from '../constants'
const props = defineProps({ modelValue: { type: Object, default: () => ({}) } })
const emit = defineEmits(['update:modelValue'])
@@ -43,6 +56,11 @@ function availableKeys(idx) {
return OVERRIDE_KEYS.filter((k) => !used.has(k.key))
}
// 该覆写字段是否为字符串型(icon 取帧名,非数值)
function isStringKey(key) {
return OVERRIDE_KEYS.find((k) => k.key === key)?.type === 'string'
}
function add() {
rows.value.push({ key: '', value: 0 })
}

View File

@@ -28,19 +28,20 @@
<OverrideEditor v-model="skill.overrides" />
</el-form-item>
<template v-if="isUlt">
<el-divider content-position="left">大招 lv_entries 分档</el-divider>
<!-- 分档编辑:大招(is-ult)全档编辑;普攻(basic-lv2)仅 lv2 强化档(客户端独立技能图标消费) -->
<template v-if="isUlt || basicLv2">
<el-divider content-position="left">{{ basicLv2 ? '普攻 lv2 强化档(独立技能)' : '大招 lv_entries 分档' }}</el-divider>
<div v-for="(e, idx) in skill.lv_entries" :key="idx" class="skill-block">
<div class="head">
<span>档位 lv {{ e.lv }} (SkillLv.Lv{{ e.s_lv }})</span>
<el-button type="danger" size="small" text @click="skill.lv_entries.splice(idx, 1)">移除</el-button>
<el-button v-if="!basicLv2" type="danger" size="small" text @click="skill.lv_entries.splice(idx, 1)">移除</el-button>
</div>
<div class="form-grid">
<el-form-item label="等级 lv">
<el-input-number v-model="e.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
<el-input-number v-model="e.lv" :min="1" :max="20" :controls="false" :disabled="basicLv2" style="width: 100%" />
</el-form-item>
<el-form-item label="SkillLv">
<el-select v-model="e.s_lv">
<el-select v-model="e.s_lv" :disabled="basicLv2">
<el-option v-for="n in 5" :key="n" :label="`Lv${n}`" :value="n" />
</el-select>
</el-form-item>
@@ -52,8 +53,11 @@
<el-input v-model="e.desc" type="textarea" :rows="2" />
</el-form-item>
</div>
<el-button size="small" @click="addEntry">+ 添加档位(lv9/lv16)</el-button>
<span class="muted" style="margin-left: 8px">建议按 lv9 / lv16 两档</span>
<template v-if="!basicLv2">
<el-button size="small" @click="addEntry">+ 添加档位(lv9/lv16)</el-button>
<span class="muted" style="margin-left: 8px">建议按 lv9 / lv16 两档</span>
</template>
<span v-else class="muted">固定 lv=2 / SkillLv.Lv2,客户端图鉴按独立技能图标展示(名称/图标取 lv1,只显示 lv2 说明)</span>
</template>
</div>
</template>
@@ -67,8 +71,20 @@ const props = defineProps({
skill: { type: Object, required: true },
options: { type: Array, default: () => [] },
isUlt: { type: Boolean, default: false },
// 普攻 lv2 强化档模式:仅允许配置单个 lv2 档(lv=2 / s_lv=2 锁定,禁用增删)
basicLv2: { type: Boolean, default: false },
})
// 普攻 lv2 模式:确保 lv_entries 恰为一个 lv2 档(无则补,多则只保留首个并规范化)
if (props.basicLv2) {
if (!props.skill.lv_entries?.length) {
props.skill.lv_entries = [{ lv: 2, s_lv: 2, overrides: {}, desc: '' }]
} else {
props.skill.lv_entries.splice(1)
Object.assign(props.skill.lv_entries[0], { lv: 2, s_lv: 2 })
}
}
const cdSpeedKey = refFromCd(props.skill)
function refFromCd(skill) {

View File

@@ -117,4 +117,18 @@ export const OVERRIDE_KEYS = [
{ key: 'call_hero', label: 'call_hero 召唤英雄id' },
{ key: 'revive_hp', label: 'revive_hp 复活回血比例(%)' },
{ key: 'revive_count', label: 'revive_count 复活次数上限' },
// 字符串型覆写:value 为图标ID(uicons 图集帧名),非数值
{ key: 'icon', label: 'icon 英雄语境图标(覆写 SkillSet.icon)', type: 'string' },
]
// icon 覆写常用候选(SkillSet 各底座图标 + 通用图标),可在输入框直接键入其他帧名
export const SKILL_ICON_OPTIONS = [
'Stat_Attack_01', 'Stat_RangedAttack', 'Stat_Tripleshot',
'Sword-FA_WP_Main_Sword_003_PurpleSilver',
'Sword-FA_WP_Main_Sword_019_WoodSilver',
'Axe-FA_WP_Main_Axe_004_WoodGray',
'Sub_Item-FA_WP_Sub_MagicBall_001_Blue',
'Sub_Item-FA_WP_Sub_MagicBall_001_Purple',
'Sub_Item-FA_WP_Sub_Potion_001_Green',
'Sub_Item-FA_WP_Sub_Potion_001_Red',
]