From 284d124486682ed211adf9711d19756af1f0ed02 Mon Sep 17 00:00:00 2001 From: panFD Date: Sun, 16 Aug 2026 18:56:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(skill):=20=E6=96=B0=E5=A2=9E=E5=8F=AC?= =?UTF-8?q?=E5=94=A4=E6=8A=80=E8=83=BD=E9=85=8D=E7=BD=AE=E4=B8=8E=E9=94=99?= =?UTF-8?q?=E5=B3=B0=E5=8F=91=E5=B0=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 为技能prefab新增summon_count、forceStart坐标与height_offset配置项 2. 新增全局SUMMON_INTERVAL配置用于控制召唤物发射间隔 3. 重构施法系统,支持按配置召唤多个技能实体并错峰执行 4. 为多发/召唤技能添加纵向偏移档位,优化弹道分布 5. 屏蔽召唤技能的风怒额外触发逻辑,修复普攻链计入问题 --- assets/resources/game/skill/atk/fb-1.prefab | 3 + assets/script/game/common/config/GameSet.ts | 2 + assets/script/game/hero/SCastSystem.ts | 131 ++++++++++++++------ assets/script/game/skill/Skill.ts | 3 +- assets/script/game/skill/SkillView.ts | 8 ++ 5 files changed, 107 insertions(+), 40 deletions(-) diff --git a/assets/resources/game/skill/atk/fb-1.prefab b/assets/resources/game/skill/atk/fb-1.prefab index 2eecb443..c595ee57 100644 --- a/assets/resources/game/skill/atk/fb-1.prefab +++ b/assets/resources/game/skill/atk/fb-1.prefab @@ -258,6 +258,9 @@ "speed": 720, "time": 0, "distance": 0, + "forceStartX": -1, + "forceStartY": -1, + "summon_count": 3, "_id": "" }, { diff --git a/assets/script/game/common/config/GameSet.ts b/assets/script/game/common/config/GameSet.ts index 6f67bb4d..390601fa 100644 --- a/assets/script/game/common/config/GameSet.ts +++ b/assets/script/game/common/config/GameSet.ts @@ -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 点伤害) */ diff --git a/assets/script/game/hero/SCastSystem.ts b/assets/script/game/hero/SCastSystem.ts index cb16d7cc..3eeb6bdf 100644 --- a/assets/script/game/hero/SCastSystem.ts +++ b/assets/script/game/hero/SCastSystem.ts @@ -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 = 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, 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); } /** diff --git a/assets/script/game/skill/Skill.ts b/assets/script/game/skill/Skill.ts index 28e466fc..c50b0d32 100644 --- a/assets/script/game/skill/Skill.ts +++ b/assets/script/game/skill/Skill.ts @@ -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; diff --git a/assets/script/game/skill/SkillView.ts b/assets/script/game/skill/SkillView.ts index e12b8ef3..ae43898d 100644 --- a/assets/script/game/skill/SkillView.ts +++ b/assets/script/game/skill/SkillView.ts @@ -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;