From 764095732576bd2c5fe7f05ec0d4910a8a11b6ac Mon Sep 17 00:00:00 2001 From: panFD Date: Mon, 14 Sep 2026 21:28:50 +0800 Subject: [PATCH] feat(skill): add multi-summon positioning and interval configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增技能召唤物XY轴步进偏移与召唤间隔可配置项 2. 重构召唤逻辑,支持按配置对称排布召唤物 3. 替换全局固定间隔为技能预制体可配置的错峰召唤间隔 4. 优化召唤参数缓存与解析逻辑,统一参数获取流程 5. 调整maxm3技能预制体的缩放与尺寸配置作为示例 --- assets/resources/game/skill/atk/maxm3.prefab | 10 +- assets/script/game/hero/SCastSystem.ts | 146 ++++++++++++------- assets/script/game/skill/SkillView.ts | 12 ++ 3 files changed, 111 insertions(+), 57 deletions(-) diff --git a/assets/resources/game/skill/atk/maxm3.prefab b/assets/resources/game/skill/atk/maxm3.prefab index 72599958..c866b267 100644 --- a/assets/resources/game/skill/atk/maxm3.prefab +++ b/assets/resources/game/skill/atk/maxm3.prefab @@ -108,8 +108,8 @@ }, "_lscale": { "__type__": "cc.Vec3", - "x": 0.9, - "y": 1, + "x": 1, + "y": 2, "z": 1 }, "_mobility": 0, @@ -136,8 +136,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 65, - "height": 330 + "width": 128, + "height": 128 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -173,7 +173,7 @@ "a": 255 }, "_spriteFrame": { - "__uuid__": "2842dc10-d89e-4ff6-8a11-6d09bf48cf20@5cbcb", + "__uuid__": "2842dc10-d89e-4ff6-8a11-6d09bf48cf20@55402", "__expectedType__": "cc.SpriteFrame" }, "_type": 0, diff --git a/assets/script/game/hero/SCastSystem.ts b/assets/script/game/hero/SCastSystem.ts index 18c2d335..b91bb6b4 100644 --- a/assets/script/game/hero/SCastSystem.ts +++ b/assets/script/game/hero/SCastSystem.ts @@ -20,6 +20,14 @@ import { BuffList } from "../common/config/BuffSet"; import { CameraFX } from "../common/CameraFX"; import { ScreenShake } from "../common/ScreenShake"; +/** 召唤参数:数量 / XY 步进偏移 / 生成间隔,均由技能 prefab 上的 SkillView 配置 */ +export interface SummonParams { + count: number; + x_offset: number; + y_offset: number; + interval: number; +} + /** * ==================== 自动施法系统 ==================== * @@ -93,9 +101,10 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate targetEids = this.collectFriendlyTargetEids(FacSet.HERO, undefined, true); // 获取所有英雄阵营的目标 } - const summonCount = this.resolveSummonCount(config); - const cNum = summonCount > 1 ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0))); - const castTimes = Math.max(summonCount, 1 + cNum); + const summon = this.resolveSummonParams(config); + const cNum = summon.count > 1 ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0))); + const castTimes = Math.max(summon.count, 1 + cNum); + const castInterval = this.resolveCastInterval(summon); // 构造一个模拟的 HeroAttrsComp 用于数值计算,只包含基础卡牌伤害计算所需的属性 const mockAttrs = new HeroAttrsComp(); @@ -149,7 +158,7 @@ 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}`); for (let i = 0; i < castTimes; i++) { - // 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*SUMMON_INTERVAL(卡牌无施法主体,用 setTimeout 并在回调内做效期校验) + // 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*castInterval(召唤间隔由 prefab 配置) const exec = () => { if (!smc.mission.play || smc.mission.pause) return; if (isFriendly) { @@ -158,14 +167,14 @@ 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); - this.createSkillEntityForCard(s_uuid, rarity, mockAttrs, spawnPos, enemyTargetPos, i, overrides); + const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i, summon); + this.createSkillEntityForCard(s_uuid, rarity, mockAttrs, spawnPos, enemyTargetPos, i, overrides, summon); } }; if (i === 0) { exec(); } else { - setTimeout(exec, i * FightSet.SUMMON_INTERVAL * 1000); + setTimeout(exec, i * castInterval * 1000); } } // 施法后概率触发额外技能(proc_skill,原命中触发改为出手即判定;卡牌技能无视 CD、无施法主体) @@ -173,7 +182,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate } /** 专用于卡牌施放的技能实体生成 */ - private createSkillEntityForCard(s_uuid: number, skillLv: number, mockAttrs: HeroAttrsComp, startPos: Vec3, targetPos: Vec3 | null, castIndex: number = 0, overrides?: SkillOverrides) { + private createSkillEntityForCard(s_uuid: number, skillLv: number, mockAttrs: HeroAttrsComp, startPos: Vec3, targetPos: Vec3 | null, castIndex: number = 0, overrides?: SkillOverrides, summon?: SummonParams) { const scene = smc.map.MapView.scene; const parent = scene.entityLayer?.node?.getChildByName("SKILL"); if (!parent || !targetPos) { @@ -182,7 +191,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate } const skill = ecs.getEntity(Skill); - const actualStartPos = this.resolveRepeatCastStartPos(startPos, castIndex); + const actualStartPos = this.resolveRepeatCastStartPos(startPos, castIndex, summon); // 伪造一个简单的 heroView 供 Skill 初始化使用,只包含方向信息 const mockView = { @@ -199,31 +208,50 @@ 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 召唤参数缓存:sp_name -> 召唤参数(数量/XY偏移/间隔) */ + private summonParamsCache: Map = new Map(); /** - * 解析单次施法的召唤数量。 - * 优先级:合并后配置上的 summon_count(heroSet overrides 可覆写) > prefab 上 SkillView.summon_count。 - * prefab 回退路径仅使用已缓存的资源(oops.res.get 同步命中),未加载时回退 1(单体),不引入异步等待。 + * 解析单次施法的召唤参数。 + * 数量优先级:合并后配置上的 summon_count(heroSet overrides 可覆写) > prefab 上 SkillView.summon_count; + * XY 偏移与间隔仅由 prefab 配置。 */ - private resolveSummonCount(config: SkillConfig): number { - if (typeof config.summon_count === "number") { - return Math.max(1, Math.floor(config.summon_count)); - } - const cached = this.summonCountCache.get(config.sp_name); - if (cached !== undefined) return cached; - const path = `game/skill/atk/${config.sp_name}`; - let count = 1; + 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 }; + } + + /** + * 读取 prefab 上 SkillView 的召唤参数(带缓存)。 + * prefab 回退路径仅使用已缓存的资源(oops.res.get 同步命中),未加载时回退默认值(单体、无偏移),不引入异步等待。 + */ + private readSummonParamsFromPrefab(sp_name: string): SummonParams { + 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 }; 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)); + params = { + count: Math.max(1, Math.floor(view.summon_count)), + x_offset: view.summon_x_offset, + y_offset: view.summon_y_offset, + interval: view.summon_interval, + }; } - this.summonCountCache.set(config.sp_name, count); + this.summonParamsCache.set(sp_name, params); } - return count; + return params; + } + + /** 解析错峰施法间隔:召唤技能优先 prefab 配置(>0),其余场景回退全局 SUMMON_INTERVAL */ + private resolveCastInterval(summon: SummonParams): number { + return summon.count > 1 && summon.interval > 0 ? summon.interval : FightSet.SUMMON_INTERVAL; } /** 获取英雄查询条件(包含属性与视图组件) */ @@ -317,9 +345,10 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate } } - const summonCount = this.resolveSummonCount(config); - const cNum = summonCount > 1 ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0))); - const castTimes = Math.max(summonCount, 1 + cNum); + const summon = this.resolveSummonParams(config); + const cNum = summon.count > 1 ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0))); + const castTimes = Math.max(summon.count, 1 + cNum); + const castInterval = this.resolveCastInterval(summon); let val = "" if (castTimes > 1) { val = "*" + castTimes.toString() @@ -328,7 +357,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate 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}`); for (let i = 0; i < castTimes; i++) { if (!heroView.node || !heroView.node.isValid) return; - // 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*SUMMON_INTERVAL + // 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*castInterval(召唤间隔由 prefab 配置) heroView.scheduleOnce(() => { if (!smc.mission.play || smc.mission.pause) return; if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return; @@ -338,9 +367,9 @@ 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); - this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides); - }, i * FightSet.SUMMON_INTERVAL); + const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i, summon); + this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides, summon); + }, i * castInterval); } // 施法后概率触发额外技能(proc_skill,原命中触发改为出手即判定,无视 CD) this.tryProcSkill(config, heroAttrs, heroView, triggerType); @@ -399,10 +428,11 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate config = mergeSkillParams(config, overrides); - const summonCount = this.resolveSummonCount(config); - const isSummonSkill = summonCount > 1; + const summon = this.resolveSummonParams(config); + const isSummonSkill = summon.count > 1; const cNum = isSummonSkill ? 0 : Math.min(2, Math.max(0, Math.floor(overrides?.num ?? 0))); - const castTimes = Math.max(summonCount, 1 + cNum); + const castTimes = Math.max(summon.count, 1 + cNum); + const castInterval = this.resolveCastInterval(summon); mLogger.log(this.debugMode, "SCastSystem", `[施法] ${heroAttrs.hero_name}(uuid:${heroAttrs.hero_uuid}) 释放技能「${config.name}」(s_uuid:${s_uuid}) lv:${skillLv} ${castPlan.isFriendly ? "友方" : "对敌"} castTimes:${castTimes}`); @@ -444,7 +474,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return; if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return; for (let i = 0; i < castTimes; i++) { - // 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*SUMMON_INTERVAL + // 多发/召唤按间隔错峰执行:i=0 立即,其余延迟 i*castInterval(召唤间隔由 prefab 配置) heroView.scheduleOnce(() => { if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return; if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return; @@ -454,9 +484,9 @@ 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); - this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides); - }, i * FightSet.SUMMON_INTERVAL); + const enemyTargetPos = this.resolveRepeatCastTargetPos(castPlan.targetPos, i, summon); + this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides, summon); + }, i * castInterval); } }, delay); @@ -497,14 +527,26 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate } } - /** 多发/召唤的纵向扇形偏移档位(像素),索引按 中/上/下/上上/下下... 循环 */ + /** 多发的纵向扇形偏移档位(像素),索引按 中/上/下/上上/下下... 循环;召唤技能偏移由 prefab 配置 */ 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 { + private resolveRepeatCastTargetPos(targetPos: Vec3 | null, castIndex: number, summon?: SummonParams): Vec3 | null { if (!targetPos) return null; - 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); + const offset = this.resolveCastOffset(castIndex, summon); + if (offset.x === 0 && offset.y === 0) return targetPos; + return new Vec3(targetPos.x + offset.x, targetPos.y + offset.y, targetPos.z); + } + + /** 解析第 castIndex 个实体的偏移:召唤技能按 prefab 配置对称交替铺开(0, +1, -1, +2, -2... 档 × 步进值);多发回落纵向扇形档位 */ + private resolveCastOffset(castIndex: number, summon?: SummonParams): { x: number; y: number } { + if (summon && summon.count > 1) { + // 对称交替:偶数索引(>=2)取负档,奇数索引取正档,使召唤物以原点为中心向两侧展开 + const level = Math.ceil(castIndex / 2); + const sign = castIndex % 2 === 0 ? -1 : 1; + const factor = castIndex === 0 ? 0 : sign * level; + return { x: factor * summon.x_offset, y: factor * summon.y_offset }; + } + return { x: 0, y: SCastSystem.REPEAT_Y_OFFSETS[castIndex % SCastSystem.REPEAT_Y_OFFSETS.length] }; } /** 构建技能尝试顺序:优先主动技能,其次普攻 */ @@ -518,12 +560,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate * 创建技能实体(投射物/范围体等)。 * 仅用于对敌伤害技能的实体化表现与碰撞伤害分发。 */ - private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3, castIndex: number = 0, overrides?: SkillOverrides) { + private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3, castIndex: number = 0, overrides?: SkillOverrides, summon?: SummonParams) { if (!caster.node || !caster.node.isValid) return; const parent = caster.node.parent; if (!parent) return; const skill = ecs.getEntity(Skill); - const startPos = this.resolveRepeatCastStartPos(caster.node.position, castIndex); + const startPos = this.resolveRepeatCastStartPos(caster.node.position, castIndex, summon); skill.load(startPos, parent, s_uuid, targetPos.clone(), caster, cAttrsComp, skillLv, 0, overrides); } @@ -531,17 +573,17 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate * 对敌技能效果处理。 * 当前职责:仅处理伤害技能并创建技能实体。 */ - private applyEnemySkillEffects(s_uuid: number, skillLv: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3 | null, castIndex: number = 0, overrides?: SkillOverrides) { + private applyEnemySkillEffects(s_uuid: number, skillLv: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3 | null, castIndex: number = 0, overrides?: SkillOverrides, summon?: SummonParams) { const kind = config.kind ?? SkillKind.Damage; if (kind !== SkillKind.Damage) return; if (config.ap <= 0 || !targetPos) return; - this.createSkillEntity(s_uuid, skillLv, heroView, cAttrsComp, targetPos, castIndex, overrides); + this.createSkillEntity(s_uuid, skillLv, heroView, cAttrsComp, targetPos, castIndex, overrides, summon); } - private resolveRepeatCastStartPos(startPos: Readonly, castIndex: number): Vec3 { - 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); + private resolveRepeatCastStartPos(startPos: Readonly, castIndex: number, summon?: SummonParams): Vec3 { + const offset = this.resolveCastOffset(castIndex, summon); + if (offset.x === 0 && offset.y === 0) return startPos.clone(); + return new Vec3(startPos.x + offset.x, startPos.y + offset.y, startPos.z); } /** diff --git a/assets/script/game/skill/SkillView.ts b/assets/script/game/skill/SkillView.ts index df383a7c..91c83852 100644 --- a/assets/script/game/skill/SkillView.ts +++ b/assets/script/game/skill/SkillView.ts @@ -54,6 +54,18 @@ export class SkillView extends CCComp { @property({ type: CCInteger, slide: true, range: [1, 8, 1], tooltip: "单次施法召唤的技能实体个数" }) summon_count: number = 1; + /** 召唤物 X 轴步进偏移(像素):summon_count>1 时以出生点/目标点为中心对称交替铺开(第1个=0,之后按 +1,-1,+2,-2... 档 × 该值,正=右) */ + @property({ type: CCFloat, slide: true, range: [-200, 200, 5], tooltip: "召唤物X轴步进偏移:以中心对称交替铺开(0,+1,-1,+2,-2...档×该值)" }) + summon_x_offset: number = 0; + + /** 召唤物 Y 轴步进偏移(像素):summon_count>1 时与 X 同档位对称交替铺开(正=上) */ + @property({ type: CCFloat, slide: true, range: [-200, 200, 5], tooltip: "召唤物Y轴步进偏移:与X同档位对称交替(0,+1,-1,+2,-2...档×该值)" }) + summon_y_offset: number = 0; + + /** 召唤间隔(秒):summon_count>1 时相邻两个实体的生成时间间隔(<=0 回退全局 FightSet.SUMMON_INTERVAL) */ + @property({ type: CCFloat, slide: true, range: [0, 2, 0.05], tooltip: "召唤间隔(秒):相邻实体生成间隔,0=用全局默认" }) + summon_interval: number = 0; + /** 抛射物高度增量(像素),贝塞尔弹道时叠加到 bezierMidHeight,控制抛物线顶点抬高 */ @property({ type: CCFloat, slide: true, range: [0, 500, 5], tooltip: "抛射物高度增量(叠加到贝塞尔弹道中点高度)" }) height_offset: number = 0;