feat(skill): 新增召唤技能配置与错峰发射逻辑

1.  为技能prefab新增summon_count、forceStart坐标与height_offset配置项
2.  新增全局SUMMON_INTERVAL配置用于控制召唤物发射间隔
3.  重构施法系统,支持按配置召唤多个技能实体并错峰执行
4.  为多发/召唤技能添加纵向偏移档位,优化弹道分布
5.  屏蔽召唤技能的风怒额外触发逻辑,修复普攻链计入问题
This commit is contained in:
panFD
2026-08-16 18:56:15 +08:00
parent 8a506d9b83
commit 284d124486
5 changed files with 107 additions and 40 deletions

View File

@@ -258,6 +258,9 @@
"speed": 720,
"time": 0,
"distance": 0,
"forceStartX": -1,
"forceStartY": -1,
"summon_count": 3,
"_id": ""
},
{

View File

@@ -42,6 +42,8 @@ export enum FightSet {
FROST_TIME = 3,//冰冻时间
STUN_TIME = 2,//击晕时间
SKILL_CAST_DELAY = 0.15,
/** 多发/召唤技能每次实例化之间的间隔(秒),首个立即执行 */
SUMMON_INTERVAL = 0.12,
CSKILL_START_X = -340,
CSKILL_START_Y = 30,
/** 护盾数值上限数值减免型1 点护盾吸收 1 点伤害) */

View File

@@ -4,6 +4,7 @@ import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { DTType, RType, SkillConfig, SkillKind, SkillSet, TGroup, SkillOverrides, mergeSkillParams } from "../common/config/SkillSet";
import { Skill } from "../skill/Skill";
import { SkillView } from "../skill/SkillView";
import { smc } from "../common/SingletonModuleComp";
import { HeroDisVal, HeroInfo, HType } from "../common/config/heroSet";
import { Attrs } from "../common/config/HeroAttrs";
@@ -90,9 +91,11 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
targetEids = this.collectFriendlyTargetEids(FacSet.HERO, undefined, true); // 获取所有英雄阵营的目标
}
// 多发次数由 overrides.num 提供(技能卡差异化多发),限制在 [0, 2]
const cNum = Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0)));
const castTimes = 1 + cNum;
// 召唤数量由技能 prefab 的 SkillView.summon_count 决定;召唤类技能(>1默认不再参与风怒多发
const summonCount = this.resolveSummonCount(config);
// 多发次数由 overrides.num 提供(技能卡差异化多发),限制在 [0, 2];召唤类技能屏蔽风怒多发
const cNum = summonCount > 1 ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0)));
const castTimes = Math.max(summonCount, 1 + cNum);
// 构造一个模拟的 HeroAttrsComp 用于数值计算,只包含基础卡牌伤害计算所需的属性
const mockAttrs = new HeroAttrsComp();
@@ -145,14 +148,23 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
mLogger.log(this.debugMode, "SCastSystem", "forceCastCardSkill: casting skill", s_uuid, "castTimes", castTimes, "targetPos", targetPos, "isFriendly", isFriendly, "buff_value", config.buff_value, "timed_buff_id", config.timed_buff_id);
for (let i = 0; i < castTimes; i++) {
if (isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(targetEids, FacSet.HERO);
mLogger.log(this.debugMode, "SCastSystem", "friendlyTargets count =", friendlyTargets.length);
if (friendlyTargets.length === 0) continue;
this.applyFriendlySkillEffects(s_uuid, cardLv, config, null as any, mockAttrs, friendlyTargets, spawnPos);
// 多发/召唤按间隔错峰执行i=0 立即,其余延迟 i*SUMMON_INTERVAL卡牌无施法主体用 setTimeout 并在回调内做效期校验)
const exec = () => {
if (!smc.mission.play || smc.mission.pause) return;
if (isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(targetEids, FacSet.HERO);
mLogger.log(this.debugMode, "SCastSystem", "friendlyTargets count =", friendlyTargets.length);
if (friendlyTargets.length === 0) return;
this.applyFriendlySkillEffects(s_uuid, cardLv, config, null as any, mockAttrs, friendlyTargets, spawnPos);
} else {
const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i);
this.createSkillEntityForCard(s_uuid, cardLv, mockAttrs, spawnPos, enemyTargetPos, i, overrides);
}
};
if (i === 0) {
exec();
} else {
const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i);
this.createSkillEntityForCard(s_uuid, cardLv, mockAttrs, spawnPos, enemyTargetPos, i, overrides);
setTimeout(exec, i * FightSet.SUMMON_INTERVAL * 1000);
}
}
}
@@ -184,6 +196,29 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
/** 查询缓存:避免每帧重复创建 matcher */
private heroMatcher: ecs.IMatcher | null = null;
/** 技能 prefab 召唤数量缓存sp_name -> summon_count */
private summonCountCache: Map<string, number> = new Map();
/**
* 解析技能 prefab 上 SkillView.summon_count 配置的单次施法召唤数量。
* 仅使用已缓存的 prefaboops.res.get 同步命中),未加载时回退 1单体不引入异步等待。
*/
private resolveSummonCount(config: SkillConfig): number {
const cached = this.summonCountCache.get(config.sp_name);
if (cached !== undefined) return cached;
const path = `game/skill/atk/${config.sp_name}`;
let count = 1;
const prefab = oops.res.get(path, Prefab) as Prefab | null;
if (prefab) {
const view = prefab.data?.getComponent(SkillView);
if (view) {
count = Math.max(1, Math.floor(view.summon_count));
}
this.summonCountCache.set(config.sp_name, count);
}
return count;
}
/** 获取英雄查询条件(包含属性与视图组件) */
private getHeroMatcher(): ecs.IMatcher {
if (!this.heroMatcher) {
@@ -273,9 +308,11 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}
}
// 多发次数由 overrides.num 提供(技能卡差异化多发),限制在 [0, 2]
const cNum = Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0)));
const castTimes = 1 + cNum;
// 召唤数量由技能 prefab 的 SkillView.summon_count 决定;召唤类技能(>1默认不再参与风怒多发
const summonCount = this.resolveSummonCount(config);
// 多发次数由 overrides.num 提供(技能卡差异化多发),限制在 [0, 2];召唤类技能屏蔽风怒多发
const cNum = summonCount > 1 ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0)));
const castTimes = Math.max(summonCount, 1 + cNum);
let val = ""
if (castTimes > 1) {
val = "*" + castTimes.toString
@@ -283,14 +320,19 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
heroView.skill_name(val, s_uuid, triggerType)
for (let i = 0; i < castTimes; i++) {
if (!heroView.node || !heroView.node.isValid) return;
if (isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(targetEids, heroAttrs.fac);
if (friendlyTargets.length === 0) continue;
this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
} else {
// 多发/召唤按间隔错峰执行i=0 立即,其余延迟 i*SUMMON_INTERVAL
heroView.scheduleOnce(() => {
if (!smc.mission.play || smc.mission.pause) return;
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
if (isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(targetEids, heroAttrs.fac);
if (friendlyTargets.length === 0) return;
this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
return;
}
const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i);
this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides);
}
}, i * FightSet.SUMMON_INTERVAL);
}
}
@@ -343,12 +385,16 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const skillLv = castPlan.skillLv;
const overrides = castPlan.overrides;
let config = SkillSet[s_uuid];
// 多发次数由 overrides.num 提供(技能卡差异化多发),限制在 [0, 2]
const cNum = Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0)));
if (!config) return;
config = mergeSkillParams(config, overrides);
// 召唤数量由技能 prefab 的 SkillView.summon_count 决定;召唤类技能(>1默认不再触发风怒
const summonCount = this.resolveSummonCount(config);
const isSummonSkill = summonCount > 1;
// 多发次数由 overrides.num 提供(技能卡差异化多发),限制在 [0, 2];召唤类技能屏蔽风怒多发
const cNum = isSummonSkill ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0)));
//播放前摇技能动画
heroView.playReady(config.readyAnm);
//播放角色攻击动画
@@ -356,8 +402,9 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 因为 castSkill 只由 update 循环调用,处理的必然是 heroAttrs.skills 中的普通技能
// 特殊触发技能(call/dead/atking等)走的是 forceCastTriggerSkill不会调用 castSkill
// 召唤类技能summon_count>1不再计入普攻链避免触发 atking风怒类附加技能
const skillIds = heroAttrs.getSkillIds();
if (skillIds.includes(s_uuid)) {
if (!isSummonSkill && skillIds.includes(s_uuid)) {
heroAttrs.atk_count++;
this.checkAndTriggerAtkingSkills(heroAttrs, heroView);
}
@@ -370,18 +417,21 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
heroView.scheduleOnce(() => {
if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return;
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
const castTimes = 1 + cNum;
const castTimes = Math.max(summonCount, 1 + cNum);
for (let i = 0; i < castTimes; i++) {
if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return;
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
if (castPlan.isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(castPlan.targetEids, heroAttrs.fac);
if (friendlyTargets.length === 0) return;
this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
continue;
}
const enemyTargetPos = this.resolveRepeatCastTargetPos(castPlan.targetPos, i);
this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides);
// 多发/召唤按间隔错峰执行i=0 立即,其余延迟 i*SUMMON_INTERVAL
heroView.scheduleOnce(() => {
if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return;
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
if (castPlan.isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(castPlan.targetEids, heroAttrs.fac);
if (friendlyTargets.length === 0) return;
this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
return;
}
const enemyTargetPos = this.resolveRepeatCastTargetPos(castPlan.targetPos, i);
this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides);
}, i * FightSet.SUMMON_INTERVAL);
}
}, delay);
heroAttrs.triggerSkillCD(s_uuid);
@@ -392,11 +442,14 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
SkillTriggerHelper.trigger(SkillTriggerType.Atking, heroAttrs, heroView);
}
/** 多发/召唤的纵向扇形偏移档位(像素),索引按 中/上/下/上上/下下... 循环 */
private static readonly REPEAT_Y_OFFSETS: number[] = [0, 15, -15, 30, -30, 45, -45, 60, -60];
private resolveRepeatCastTargetPos(targetPos: Vec3 | null, castIndex: number): Vec3 | null {
if (!targetPos) return null;
if (castIndex === 1) return new Vec3(targetPos.x, targetPos.y + 15, targetPos.z);
if (castIndex === 2) return new Vec3(targetPos.x, targetPos.y - 15, targetPos.z);
return targetPos;
const offset = SCastSystem.REPEAT_Y_OFFSETS[castIndex % SCastSystem.REPEAT_Y_OFFSETS.length];
if (offset === 0) return targetPos;
return new Vec3(targetPos.x, targetPos.y + offset, targetPos.z);
}
/** 构建技能尝试顺序:优先主动技能,其次普攻 */
@@ -431,9 +484,9 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}
private resolveRepeatCastStartPos(startPos: Readonly<Vec3>, castIndex: number): Vec3 {
if (castIndex === 1) return new Vec3(startPos.x, startPos.y + 15, startPos.z);
if (castIndex === 2) return new Vec3(startPos.x, startPos.y - 15, startPos.z);
return startPos.clone();
const offset = SCastSystem.REPEAT_Y_OFFSETS[castIndex % SCastSystem.REPEAT_Y_OFFSETS.length];
if (offset === 0) return startPos.clone();
return new Vec3(startPos.x, startPos.y + offset, startPos.z);
}
/**

View File

@@ -188,7 +188,8 @@ export class Skill extends ecs.Entity {
sMoveCom.endType = SView.endType;
sMoveCom.speed = SView.speed > 0 ? SView.speed : config.speed;
sMoveCom.bezierStartHeight = config.bezier_start_y ?? sMoveCom.bezierStartHeight;
sMoveCom.bezierMidHeight = config.bezier_mid_y ?? sMoveCom.bezierMidHeight;
// 抛射物高度增量:叠加视图层 height_offset抬高贝塞尔弹道顶点
sMoveCom.bezierMidHeight = (config.bezier_mid_y ?? sMoveCom.bezierMidHeight) + Math.max(0, SView.height_offset);
sMoveCom.bezierArc = config.bezier_arc ?? sMoveCom.bezierArc;
// 从SkillView获取移动参数位置初始化由SMoveSystem统一处理
sMoveCom.atk_x = SView.atk_x;

View File

@@ -50,6 +50,14 @@ export class SkillView extends CCComp {
@property({ type: CCFloat, tooltip: "强制出生点Y-1=不强制,>=0=强制世界坐标Y" })
forceStartY: number = -1;
/** 召唤数量SCastSystem 单次施法实例化的技能实体个数(<=1 为单体;>1 时按召唤物语义,不再参与风怒等额外触发) */
@property({ type: CCInteger, slide: true, range: [1, 8, 1], tooltip: "单次施法召唤的技能实体个数" })
summon_count: number = 1;
/** 抛射物高度增量(像素),贝塞尔弹道时叠加到 bezierMidHeight控制抛物线顶点抬高 */
@property({ type: CCFloat, slide: true, range: [0, 500, 5], tooltip: "抛射物高度增量(叠加到贝塞尔弹道中点高度)" })
height_offset: number = 0;
private debugMode: boolean = false;
anim: Animation = null;