Files
pixelheros/assets/script/game/skill/SkillView.ts
panFD c65c8a36ab feat: 新增穿透伤害加成机制与相关配置
1. 新增全局穿透伤害衰减系数配置与英雄属性字段
2. 实现穿透伤害加成的计算逻辑与增益buff
3. 补全技能与英雄配置的相关变更
4. 新增穿透伤害相关的光环与技能配置
2026-08-11 21:44:50 +08:00

237 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { _decorator, Animation, CCInteger, Collider2D, Contact2DType, UITransform, v3, Vec3 } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { HeroViewComp } from "../hero/HeroViewComp";
import { DTType, EType, SkillConfig, SkillSet } from "../common/config/SkillSet";
import { SDataCom } from "./SDataCom";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { DamageQueueHelper } from "../hero/DamageQueueComp";
import { mLogger } from "../common/Logger";
import { FightSet } from "../common/config/GameSet";
import { Attrs } from "../common/config/HeroAttrs";
const { ccclass, property } = _decorator;
/** 技能视图组件:负责碰撞窗口、碰撞回调与伤害派发 */
@ccclass('SkillView')
@ecs.register('SkillView', false)
export class SkillView extends CCComp {
/** 视图层逻辑代码分离演示 */
@property({ type: CCInteger })
atk_x: number = 0
@property({ type: CCInteger })
atk_y: number = 0
private debugMode: boolean = false;
anim:Animation=null;
group:number=0;
SConf:SkillConfig=null;
sData:SDataCom=null;
s_uuid:number=1001
private collider: Collider2D = null; // 缓存碰撞体引用
private pendingDisableCollider: boolean = false;
private isDisposing: boolean = false;
// 生命周期保护标记,避免在销毁阶段重复处理碰撞
init() {
this.SConf = SkillSet[this.s_uuid]
this.sData = this.ent.get(SDataCom)
this.anim = this.node.getComponent(Animation)
this.node.active = true;
this.pendingDisableCollider = false;
this.isDisposing = false;
this.collider = this.getComponent(Collider2D);
if(this.collider) {
this.collider.group = this.group;
this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.enabled = this.SConf?.EType === EType.collision;
}
if(this.node.getComponent(Animation)){
let anim = this.node.getComponent(Animation);
mLogger.log(this.debugMode, 'SkillView', "[SkillCom]:has anim",anim)
anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this);
anim.on(Animation.EventType.FINISHED, this.onAnimationFinished, this);
// 对象池复用时,需要手动播放默认动画(因为 Play On Load 只在首次生效)
if (anim.defaultClip) {
anim.play(anim.defaultClip.name);
}
}
}
onBeginContact (seCol: Collider2D, oCol: Collider2D) {
if (!this.sData || !this.SConf) {
mLogger.warn(this.debugMode, 'SkillView', '[SkillView] onBeginContact 缺少 sData 或 SConf忽略此次碰撞');
return;
}
if (this.isDisposing) return;
if (!this.node || !this.node.activeInHierarchy) return;
const targetView = oCol.getComponent(HeroViewComp);
// 不是 HeroViewComp直接忽略
if (!targetView) return;
// 🔥 方案A防御性检查 - 在获取model前强制检查ent是否存在
if (!targetView.ent) {
mLogger.warn(this.debugMode, 'SkillView', '[SkillView] onBeginContact targetView.ent为空实体已销毁忽略此次碰撞');
return;
}
if (this.debugMode) {
const casterEid = this.sData.casterEid;
const targetName = targetView?.ent?.get(HeroAttrsComp)?.hero_name ?? '非英雄对象';
const targetEid = targetView?.ent?.eid ?? '未知EID';
mLogger.log(this.debugMode, 'SkillView', `[skillView] 碰撞1 [${this.sData.group}][eid:${casterEid}]的[${seCol.group}]:[${this.SConf.name}][${this.ent.eid}]碰撞了 [${oCol.group}]:[${targetName}][${targetEid}]`);
}
if (oCol.group === seCol.group) return;
if (this.pendingDisableCollider) return;
if (this.sData.hit_count >= this.sData.max_hit_count) {
this.handle_collision_limit();
return;
}
this.sData.hit_count++;
if (this.sData.hit_count >= this.sData.max_hit_count) {
// == 新增:穿透概率判定 ==
// 仅对单体伤害技能生效DTType.single且存在穿透概率
if (this.SConf.DTType === DTType.single) {
const punctureChance = this.sData.Attrs[Attrs.puncture_chance] || 0;
if (punctureChance > 0) {
const rand = Math.random() * 100;
if (rand < punctureChance) {
// 触发穿透:不销毁,增加 max_hit_count 允许继续穿透
this.sData.max_hit_count++;
// 概率衰减(减去固定的 PUNCTURE_DOWN 值,比如 50
this.sData.Attrs[Attrs.puncture_chance] = Math.max(0, punctureChance - FightSet.PUNCTURE_DOWN);
if (this.debugMode) {
mLogger.log(this.debugMode, 'SkillView', `[SkillView] 触发穿透!剩余概率: ${this.sData.Attrs[Attrs.puncture_chance]}%`);
}
} else {
// 未触发穿透,正常销毁
this.handle_collision_limit();
}
} else {
this.handle_collision_limit();
}
} else {
this.handle_collision_limit();
}
}
// 命中次数按碰撞事件统计:不依赖是否最终造成伤害
let model = targetView.ent.get(HeroAttrsComp);
if (this.debugMode) {
mLogger.log(this.debugMode, 'SkillView', `[skillView] 碰撞3`, oCol.group, seCol.group, model);
}
if (!model) return;
if (model.is_dead) return;
if (this.sData.fac == model.fac) return;
// 检查是否已经命中过这个目标(日志安全输出)
this.apply_damage(targetView, this.sData.hit_count)
}
onAnimationFinished(){
// animationEnd 类型:动画结束后再关闭碰撞并销毁
if(this.SConf.EType==EType.animationEnd){
this.disable_collider_now();
this.ent.destroy()
}
}
// 动画帧事件 atk仅负责开启一帧碰撞窗口不负责伤害与计数
public atk(args:any){
if(!this.SConf) return;
if(this.SConf.EType==EType.collision) return
if (this.enable_collider_safely()) {
mLogger.log(this.debugMode, 'SkillView', `[SkillView] [${this.SConf?.name}] 开启碰撞检测`);
this.scheduleOnce(() => {
if (!this.node || !this.node.isValid || this.isDisposing) return;
this.close_collider();
mLogger.log(this.debugMode, 'SkillView', `[SkillView] [${this.SConf?.name}] 关闭碰撞检测`);
}, 0);
}
}
/**
* 仅负责伤害派发,不处理命中次数与实体销毁
* @param target 目标英雄视图
* @param punctureIndex 当前是第几个被命中的目标1=首目标;>=2 视为穿透目标,单体技能按 FightSet.PUNCTURE_DMG_RATIO 折算伤害,范围技能不衰减)
*/
apply_damage(target: HeroViewComp, punctureIndex: number = 1) {
if (target == null) return;
// 安全检查:如果目标实体已不存在,直接返回
if (!target.ent) return;
if (!this.sData) return;
if (!this.SConf) return;
if (this.debugMode) {
const targetName = target.ent.get(HeroAttrsComp)?.hero_name ?? "未知目标";
mLogger.log(this.debugMode, 'SkillView', `[skillView] 伤害 [${this.group}][eid:${this.sData.casterEid}]的 [${this.SConf.name}]对 [${target.box_group}][${targetName}][${target.ent.eid}] 第${punctureIndex}目标`);
}
// 穿透伤害衰减仅单体伤害技能DTType.single且命中第2个及以后目标时按比例折算
// 乘法模型:最终穿透系数 = PUNCTURE_DMG_RATIO * (1 + puncture_dmg_bonus/100)
// 注puncture_dmg_bonus 单位为百分点30 = +30%),与项目其他概率/加成属性保持一致
const punctureDmgBonus = this.sData.Attrs[Attrs.puncture_dmg_bonus] || 0;
const decayRatio = (this.SConf.DTType === DTType.single && punctureIndex >= 2)
? FightSet.PUNCTURE_DMG_RATIO * (1 + punctureDmgBonus / 100)
: 1;
const finalRatio = this.sData.dmg_ratio * decayRatio;
// 使用伤害队列系统处理伤害
DamageQueueHelper.addDamageToEntity(
target.ent,
this.sData.Attrs,
this.sData.casterEid,
this.sData.s_uuid,
this.sData.ext_dmg,
finalRatio,
);
}
close_collider(){
if (!this.collider) return;
if (this.pendingDisableCollider && !this.collider.enabled) return;
this.pendingDisableCollider = true;
if (this.collider.isValid) {
this.collider.enabled = false;
}
this.pendingDisableCollider = false;
}
private disable_collider_now() {
this.isDisposing = true;
this.close_collider();
}
// 碰撞上限收口先关碰撞collision 类型再延迟销毁实体
private handle_collision_limit() {
this.close_collider();
if (this.SConf?.EType !== EType.collision) return;
if (this.isDisposing) return;
this.isDisposing = true;
this.scheduleOnce(() => {
if (this.ent) {
this.ent.destroy();
}
}, 0);
}
private enable_collider_safely(): boolean {
if (!this.collider || !this.collider.isValid) return false;
if (this.isDisposing) return false;
if (!this.node || !this.node.isValid || !this.node.activeInHierarchy) return false;
this.pendingDisableCollider = false;
this.collider.group = this.group;
this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.enabled = true;
return true;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
// 清理碰撞体事件监听
if (this.collider && this.collider.isValid) {
this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
this.collider.enabled = false;
}
this.pendingDisableCollider = false;
this.isDisposing = false;
if (this.anim && this.anim.isValid) {
this.anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this);
}
// 取消所有定时器
this.unscheduleAllCallbacks();
if (this.node && this.node.isValid) {
this.node.active = false;
}
}
}