feat(skill): add multi-summon positioning and interval configuration
1. 新增技能召唤物XY轴步进偏移与召唤间隔可配置项 2. 重构召唤逻辑,支持按配置对称排布召唤物 3. 替换全局固定间隔为技能预制体可配置的错峰召唤间隔 4. 优化召唤参数缓存与解析逻辑,统一参数获取流程 5. 调整maxm3技能预制体的缩放与尺寸配置作为示例
This commit is contained in:
@@ -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>(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<string, number> = new Map();
|
||||
/** 技能 prefab 召唤参数缓存:sp_name -> 召唤参数(数量/XY偏移/间隔) */
|
||||
private summonParamsCache: Map<string, SummonParams> = 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>(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<Vec3>, 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<Vec3>, 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user