feat(skill): 新增召唤目标选择模式与技能优化
1. 新增SummonTMode枚举,支持同一敌人/随机敌人/去重三种召唤目标模式 2. 为多个技能prefab新增召唤参数配置字段 3. 重构索敌逻辑,支持按配置模式选择召唤目标 4. 优化技能伤害与动画配置,新增maxm7技能与动画资源 5. 修复部分技能的碰撞次数与偏移参数
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user