feat(skill): 新增召唤目标选择模式与技能优化

1. 新增SummonTMode枚举,支持同一敌人/随机敌人/去重三种召唤目标模式
2. 为多个技能prefab新增召唤参数配置字段
3. 重构索敌逻辑,支持按配置模式选择召唤目标
4. 优化技能伤害与动画配置,新增maxm7技能与动画资源
5. 修复部分技能的碰撞次数与偏移参数
This commit is contained in:
pan
2026-09-22 15:56:27 +08:00
parent fa58f699d9
commit f92502f4f5
16 changed files with 1622 additions and 37 deletions

View File

@@ -1,8 +1,8 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { Vec3, Prefab, instantiate, tween, Node } from "cc";
import { Vec3, Prefab, instantiate, tween, Node, UITransform } from "cc";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { DTType, RType, SkillConfig, SkillKind, SkillSet, TGroup, SkillOverrides, mergeSkillParams } from "../common/config/SkillSet";
import { DTType, RType, SkillConfig, SkillKind, SkillSet, SummonTMode, TGroup, SkillOverrides, mergeSkillParams } from "../common/config/SkillSet";
import { Skill } from "../skill/Skill";
import { SkillView } from "../skill/SkillView";
import { smc } from "../common/SingletonModuleComp";
@@ -20,12 +20,14 @@ import { BuffList } from "../common/config/BuffSet";
import { CameraFX } from "../common/CameraFX";
import { ScreenShake } from "../common/ScreenShake";
/** 召唤参数:数量 / XY 步进偏移 / 生成间隔,均由技能 prefab 上的 SkillView 配置 */
/** 召唤参数:数量 / XY 步进偏移 / 生成间隔 / 目标选择模式,均由技能 prefab 上的 SkillView 配置 */
export interface SummonParams {
count: number;
x_offset: number;
y_offset: number;
interval: number;
/** 多次施放的目标选择模式(同一敌人/随机敌人/避免相同敌人) */
target_mode: SummonTMode;
}
/**
@@ -137,12 +139,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
mockAttrs.type = HType.Long; // 假定为远程,拥有较长索敌范围
mockAttrs.dis = 2000; // 给予全屏以上的索敌范围
// 伪造一个 view 供找敌逻辑使用,位置为 spawnPos(random/unique 模式实时索敌也复用)
const mockView = {
node: { position: spawnPos }
} as any;
let targetPos: Vec3 | null = null;
if (!isFriendly) {
// 伪造一个 view 供找敌逻辑使用,位置为 spawnPos
const mockView = {
node: { position: spawnPos }
} as any;
// 获取全屏索敌范围
const maxRange = this.resolveMaxCastRange(mockAttrs, mockAttrs.type as HType);
const target = this.findNearestEnemyInRange(mockAttrs, mockView, maxRange);
@@ -157,6 +159,8 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}
mLogger.log(this.debugMode, "SCastSystem", `[卡牌施法] 释放技能「${config.name}」(s_uuid:${s_uuid}) rarity:${rarity} castTimes:${castTimes} targetPos:${targetPos ? `(${targetPos.x.toFixed(0)},${targetPos.y.toFixed(0)})` : "none"} isFriendly:${isFriendly}`);
// unique 模式去重记录:本次施法序列内已锁定过的敌人 eid
const usedTargetEids = new Set<number>();
for (let i = 0; i < castTimes; i++) {
// 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*castInterval(召唤间隔由 prefab 配置)
const exec = () => {
@@ -167,7 +171,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
if (friendlyTargets.length === 0) return;
this.applyFriendlySkillEffects(s_uuid, rarity, config, null as any, mockAttrs, friendlyTargets, spawnPos);
} else {
const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i, summon);
const enemyTargetPos = this.resolveSummonCastTargetPos(config, mockAttrs, mockView, targetPos, i, summon, usedTargetEids);
this.createSkillEntityForCard(s_uuid, rarity, mockAttrs, spawnPos, enemyTargetPos, i, overrides, summon);
}
};
@@ -214,14 +218,14 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
/**
* 解析单次施法的召唤参数。
* 数量优先级:合并后配置上的 summon_count(heroSet overrides 可覆写) > prefab 上 SkillView.summon_count;
* XY 偏移与间隔仅由 prefab 配置。
* XY 偏移、间隔与目标选择模式仅由 prefab 配置。
*/
private resolveSummonParams(config: SkillConfig): SummonParams {
const fromPrefab = this.readSummonParamsFromPrefab(config.sp_name);
const count = typeof config.summon_count === "number"
? Math.max(1, Math.floor(config.summon_count))
: fromPrefab.count;
return { count, x_offset: fromPrefab.x_offset, y_offset: fromPrefab.y_offset, interval: fromPrefab.interval };
return { count, x_offset: fromPrefab.x_offset, y_offset: fromPrefab.y_offset, interval: fromPrefab.interval, target_mode: fromPrefab.target_mode };
}
/**
@@ -232,7 +236,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const cached = this.summonParamsCache.get(sp_name);
if (cached) return cached;
const path = `game/skill/atk/${sp_name}`;
let params: SummonParams = { count: 1, x_offset: 0, y_offset: 0, interval: 0 };
let params: SummonParams = { count: 1, x_offset: 0, y_offset: 0, interval: 0, target_mode: SummonTMode.same };
const prefab = oops.res.get(path, Prefab) as Prefab | null;
if (prefab) {
const view = prefab.data?.getComponent(SkillView);
@@ -242,6 +246,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
x_offset: view.summon_x_offset,
y_offset: view.summon_y_offset,
interval: view.summon_interval,
target_mode: view.summon_target_mode ?? SummonTMode.same,
};
}
this.summonParamsCache.set(sp_name, params);
@@ -355,6 +360,8 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}
heroView.skill_name(val, s_uuid)
mLogger.log(this.debugMode, "SCastSystem", `[触发施法] ${heroAttrs.hero_name}(uuid:${heroAttrs.hero_uuid}) 触发类型:${triggerType ?? "none"} 释放技能「${config.name}」(s_uuid:${s_uuid}) lv:${skillLv} ${isFriendly ? "友方" : "对敌"} castTimes:${castTimes}`);
// unique 模式去重记录:本次施法序列内已锁定过的敌人 eid
const usedTargetEids = new Set<number>();
for (let i = 0; i < castTimes; i++) {
if (!heroView.node || !heroView.node.isValid) return;
// 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*castInterval(召唤间隔由 prefab 配置)
@@ -367,7 +374,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
return;
}
const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i, summon);
const enemyTargetPos = this.resolveSummonCastTargetPos(config, heroAttrs, heroView, targetPos, i, summon, usedTargetEids);
this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides, summon);
}, i * castInterval);
}
@@ -469,6 +476,8 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 注意:这里仍然是基于时间的延迟,受帧率波动影响。
// 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。
const delay = config.ready > 0 ? config.ready : FightSet.SKILL_CAST_DELAY;
// unique 模式去重记录:本次施法序列内已锁定过的敌人 eid
const usedTargetEids = new Set<number>();
heroView.scheduleOnce(() => {
if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return;
@@ -484,7 +493,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
return;
}
const enemyTargetPos = this.resolveRepeatCastTargetPos(castPlan.targetPos, i, summon);
const enemyTargetPos = this.resolveSummonCastTargetPos(config, heroAttrs, heroView, castPlan.targetPos, i, summon, usedTargetEids);
this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides, summon);
}, i * castInterval);
}
@@ -537,6 +546,71 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
return new Vec3(targetPos.x + offset.x, targetPos.y + offset.y, targetPos.z);
}
/**
* 解析第 castIndex 次施放的对敌目标点(召唤目标选择模式入口)。
* - same(默认):沿用施法时锁定的目标点并按召唤偏移对称铺开(原行为);
* - random:每次出手时从射程内敌人中随机锁定一个(错峰施放时实时索敌,目标状态最新鲜);
* - unique:优先锁定本轮未选过的敌人,敌人不足一轮时重置去重记录再来一轮;
* 实时索敌失败(敌人死亡/移动出射程)时回退施法时锁定的目标点,保证技能不落空。
*/
private resolveSummonCastTargetPos(config: SkillConfig, heroAttrs: HeroAttrsComp, heroView: HeroViewComp | null, baseTargetPos: Vec3 | null, castIndex: number, summon: SummonParams | undefined, usedEids: Set<number>): Vec3 | null {
if (!summon || summon.count <= 1 || summon.target_mode === SummonTMode.same) {
return this.resolveRepeatCastTargetPos(baseTargetPos, castIndex, summon);
}
const maxRange = this.resolveMaxCastRange(heroAttrs, heroAttrs.type as HType);
const enemies = this.collectEnemiesInRange(heroAttrs, heroView, maxRange);
const picked = enemies.length > 0 ? this.pickEnemyByTargetMode(enemies, summon.target_mode, usedEids) : null;
if (picked?.node) {
mLogger.log(this.debugMode, "SCastSystem", `[召唤索敌] 「${config.name}」第${castIndex + 1}个实体锁定目标 eid:${picked.ent?.eid} 模式:${SummonTMode[summon.target_mode]}`);
// 与 resolveEnemyCastTargetPos 保持一致:贝塞尔取脚底原始坐标,其余补半身高命中身体中心
const pos = config.RType === RType.bezier ? picked.node.position.clone() : this.buildEnemyCastTargetPos(picked);
// 召唤偏移铺开仍生效:random 模式重复锁定同一敌人时,避免实体完全重叠
const offset = this.resolveCastOffset(castIndex, summon);
pos.x += offset.x;
pos.y += offset.y;
return pos;
}
// 实时索敌失败:回退施法时锁定的目标点
return this.resolveRepeatCastTargetPos(baseTargetPos, castIndex, summon);
}
/** 按目标模式从射程内候选敌人中挑选本次施放的锁定目标,并写入去重记录 */
private pickEnemyByTargetMode(enemies: HeroViewComp[], mode: SummonTMode, usedEids: Set<number>): HeroViewComp | null {
let pool = enemies;
if (mode === SummonTMode.unique) {
const unused = enemies.filter(view => !view.ent || !usedEids.has(view.ent.eid));
if (unused.length > 0) {
pool = unused;
} else {
// 敌人不足一轮:重置已选记录,重新开始去重,保证尽量分散
usedEids.clear();
}
}
const picked = pool[Math.floor(Math.random() * pool.length)];
if (picked?.ent) usedEids.add(picked.ent.eid);
return picked ?? null;
}
/** 收集施法者射程内的全部敌方目标(随机/去重目标模式实时索敌用),网格遍历与 findNearestEnemyByGrid 同源 */
private collectEnemiesInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp | null, maxRange: number): HeroViewComp[] {
const result: HeroViewComp[] = [];
if (!heroView?.node) return result;
const grid = heroAttrs.fac === FacSet.HERO ? smc.mission.monGrid : smc.mission.heroGrid;
const currentX = heroView.node.position.x;
for (const eid of grid) {
if (eid < 0) continue;
const entity = ecs.getEntityByEid(eid);
if (!entity) continue;
const model = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!model || model.is_dead || model.is_reviving || !view?.node) continue;
if (Math.abs(currentX - view.node.position.x) <= maxRange) {
result.push(view);
}
}
return result;
}
/** 解析第 castIndex 个实体的偏移:召唤技能按 prefab 配置对称交替铺开(0, +1, -1, +2, -2... 档 × 步进值);多发回落纵向扇形档位 */
private resolveCastOffset(castIndex: number, summon?: SummonParams): { x: number; y: number } {
if (summon && summon.count > 1) {
@@ -883,7 +957,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
return nearestEnemy.node?.position.clone() ?? null;
}
if (config.DTType !== DTType.range) {
return this.buildEnemyCastTargetPos(heroView, nearestEnemy, maxRange);
return this.buildEnemyCastTargetPos(nearestEnemy);
}
const frontEnemy = this.findFrontEnemyInRange(heroAttrs, heroView, maxRange, nearestEnemy);
if (!frontEnemy?.node) return null;
@@ -897,19 +971,10 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
return heroAttrs.dis;
}
/** 生成沿目标方向的施法目标坐标 */
private buildEnemyCastTargetPos(caster: HeroViewComp, target: HeroViewComp, castRange: number): Vec3 {
// 直接返回目标的真实坐标,保留其 Y 轴信息,确保能向目标真实所在位置发射
// 考虑到目前角色的 y 坐标都是脚底(碰撞体底部),为了命中身体中心,给目标 y 加上高度的一半
let halfHeight = 0;
if (target.node) {
const transform = target.node.getComponent('cc.UITransform') as any;
if (transform) {
halfHeight = transform.height / 2;
} else {
halfHeight = 40; // 如果没有 UITransform,给一个默认高度偏移
}
}
/** 生成对敌施法目标坐标:直接返回目标的真实坐标;y 为脚底(碰撞体底部),补半身高以命中身体中心 */
private buildEnemyCastTargetPos(target: HeroViewComp): Vec3 {
const transform = target.node?.getComponent(UITransform);
const halfHeight = transform ? transform.height / 2 : 40; // 无 UITransform 时给默认高度偏移
const pos = target.node.position.clone();
pos.y += halfHeight;