feat(skill): 新增召唤技能配置与错峰发射逻辑
1. 为技能prefab新增summon_count、forceStart坐标与height_offset配置项 2. 新增全局SUMMON_INTERVAL配置用于控制召唤物发射间隔 3. 重构施法系统,支持按配置召唤多个技能实体并错峰执行 4. 为多发/召唤技能添加纵向偏移档位,优化弹道分布 5. 屏蔽召唤技能的风怒额外触发逻辑,修复普攻链计入问题
This commit is contained in:
@@ -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 配置的单次施法召唤数量。
|
||||
* 仅使用已缓存的 prefab(oops.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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user