@@ -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 = summonC ount > 1 ? 0 : Math.min ( 2 , Math . max ( 0 , Math . floor ( overrides ? . num ? ? 0 ) ) ) ;
const castTimes = Math . max ( summonC ount , 1 + cNum ) ;
const summon = this . resolveSummonParams ( config ) ;
const cNum = summon. c ount > 1 ? 0 : Math.min ( 2 , Math . max ( 0 , Math . floor ( overrides ? . num ? ? 0 ) ) ) ;
const castTimes = Math . max ( summon. c ount , 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 summonCount Cache : Map < string , n umber > = new Map ( ) ;
/** 技能 prefab 召唤参 数缓存: sp_name -> 召唤参数(数量/XY偏移/间隔) */
private summonParams Cache : Map < string , S ummonParams > = 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 . summonCount Cache . set ( config . sp_name, count ) ;
this . summonParams Cache . 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 = summonC ount > 1 ? 0 : Math.min ( 2 , Math . max ( 0 , Math . floor ( overrides ? . num ? ? 0 ) ) ) ;
const castTimes = Math . max ( summonC ount , 1 + cNum ) ;
const summon = this . resolveSummonParams ( config ) ;
const cNum = summon. c ount > 1 ? 0 : Math.min ( 2 , Math . max ( 0 , Math . floor ( overrides ? . num ? ? 0 ) ) ) ;
const castTimes = Math . max ( summon. c ount , 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 = summonC ount > 1 ;
const summon = this . resolveSummonParams ( config ) ;
const isSummonSkill = summon. c ount > 1 ;
const cNum = isSummonSkill ? 0 : Math.min ( 2 , Math . max ( 0 , Math . floor ( overrides ? . num ? ? 0 ) ) ) ;
const castTimes = Math . max ( summonC ount , 1 + cNum ) ;
const castTimes = Math . max ( summon. c ount , 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 ) ;
}
/**