Files
pixelheros/assets/script/game/skill/Skill.ts
pan 799b93817d refactor: 重构技能触发逻辑,将proc_skill从命中触发改为施法后触发
1.  调整proc_skill触发时机为施法出手时判定,一次施法仅判定一次,与命中/弹道数无关
2.  移除HeroAtkSystem中旧的proc_skill触发逻辑,新增SCastSystem的tryProcSkill方法
3.  更新技能配置注释、编辑器文案与文档说明,同步调整服务端技能描述显示
4.  全局设置Element Plus组件为small尺寸,优化编辑器UI布局
5.  清理冗余的proc_skill快照传递逻辑,移除unused的reviveOptions计算属性
2026-09-08 17:22:41 +08:00

292 lines
12 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 { BoxCollider2D, instantiate, Node, Prefab, v3, Vec3, NodePool } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { EType, SkillSet, SkillOverrides, mergeSkillParams } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { Attrs } from "../common/config/HeroAttrs";
import { SkillView } from "./SkillView";
import { SDataCom } from "./SDataCom";
import { SMoveDataComp } from "../skill/SMoveComp";
import { StimeDataComp } from "./STimeComp";
import { HeroViewComp } from "../hero/HeroViewComp";
import { smc } from "../common/SingletonModuleComp";
import { mLogger } from "../common/Logger";
/** Skill 模块 */
@ecs.register(`Skill`)
export class Skill extends ecs.Entity {
private debugMode: boolean = false;
/** 多键对象池:Map<prefabPath, NodePool> */
static pools: Map<string, NodePool> = new Map();
static readonly MAX_POOL_SIZE: number = 48;
static readonly MAX_POOL_TOTAL: number = 192;
private static totalPoolSize(): number {
let total = 0;
this.pools.forEach((pool) => {
total += pool.size();
});
return total;
}
static getFromPool(path: string): Node | null {
if (this.pools.has(path)) {
const pool = this.pools.get(path)!;
while (pool.size() > 0) {
const node = pool.get();
if (node && node.isValid) {
return node;
}
}
}
return null;
}
static putToPool(path: string, node: Node) {
if (!node || !node.isValid) return;
if (!this.pools.has(path)) {
this.pools.set(path, new NodePool());
}
const pool = this.pools.get(path)!;
if (pool.size() >= this.MAX_POOL_SIZE || this.totalPoolSize() >= this.MAX_POOL_TOTAL) {
node.destroy();
return;
}
pool.put(node);
}
static clearPools() {
this.pools.forEach((pool) => {
while (pool.size() > 0) {
const node = pool.get();
if (node && node.isValid) {
node.destroy();
}
}
pool.clear();
});
this.pools.clear();
}
static getPoolStats() {
let total = 0;
this.pools.forEach((pool) => {
total += pool.size();
});
return {
paths: this.pools.size,
total,
maxPerPath: this.MAX_POOL_SIZE,
maxTotal: this.MAX_POOL_TOTAL
};
}
/** ---------- 数据层 ---------- */
SDataCom!: SDataCom;
SMoveCom!: SMoveDataComp
/** 当前技能的预制体路径(用于对象池回收) */
private prefabPath: string = "";
private skillNode: Node | null = null; // 持有节点引用以便销毁时回收
/** ---------- 业务层 ---------- */
// SkillBll!: SkillBllComp;
/** ---------- 视图层 ---------- */
SView!: SkillView;
/** 实始添加的数据层组件 */
protected init() {
this.addComponents<SDataCom>(SDataCom);
this.addComponents<SMoveDataComp>(SMoveDataComp);
}
load(startPos: Vec3, parent: Node, s_uuid: number, targetPos: Vec3,
caster: HeroViewComp, cAttrsComp: HeroAttrsComp, skill_lv: number = 0, ext_dmg: number = 0, overrides?: SkillOverrides) {
let config = SkillSet[s_uuid];
if (!config) {
mLogger.error(this.debugMode, 'Skill', "[Skill] 技能配置不存在:", s_uuid);
return;
}
config = mergeSkillParams(config, overrides);
const path = `game/skill/atk/${config.sp_name}`;
const initSkillNode = (prefab: Prefab) => {
const node: Node = Skill.getFromPool(path) || instantiate(prefab);
if (!node || !node.isValid) {
mLogger.error(this.debugMode, 'Skill', "[Skill] 节点无效:", path);
return;
}
this.prefabPath = path;
this.skillNode = node;
let skillParent: Node | null = null;
if (smc.map && smc.map.MapView && smc.map.MapView.scene && smc.map.MapView.scene.entityLayer && smc.map.MapView.scene.entityLayer.node) {
skillParent = smc.map.MapView.scene.entityLayer.node.getChildByName("SKILL");
}
if (!skillParent || !skillParent.isValid) {
skillParent = parent;
}
if (!skillParent || !skillParent.isValid) {
mLogger.error(this.debugMode, 'Skill', "[Skill] 父节点无效");
if (node.isValid) node.destroy();
return;
}
node.parent = skillParent;
node.active = true;
// 设置节点属性
let face = caster.node.scale.x < 0 ? -1 : 1
node.setScale(v3(Math.abs(node.scale.x) * face, node.scale.y, 1))
// 初始视图
const SView = node.getComponent(SkillView);
if (!SView) {
mLogger.error(this.debugMode, 'Skill', "[Skill] SkillView 组件缺失:", path);
if (node.isValid) node.destroy();
return;
}
if (SView.endType != EType.collision) {
const collider = node.getComponent(BoxCollider2D);
if (collider) {
collider.enabled = false
}
}
// 只设置必要的运行时属性,配置信息通过 SkillSet[uuid] 访问
// 核心标识
SView.s_uuid = s_uuid
SView.group = caster.box_group
this.add(SView);
// 强制出生点:-1=不强制使用 atk_x/atk_y 偏移,>=0=强制世界坐标
if (SView.forceStartX >= 0) {
startPos.x = SView.forceStartX;
} else {
startPos.x = startPos.x + SView.atk_x * face;
}
if (SView.forceStartY >= 0) {
startPos.y = SView.forceStartY;
} else {
startPos.y = startPos.y + SView.atk_y;
}
node.setPosition(startPos);
// 初始化移动组件 - 从SkillView获取移动参数
let sMoveCom = this.get(SMoveDataComp);
if (!sMoveCom) {
sMoveCom = this.add(SMoveDataComp);
}
sMoveCom.reset(); // 复用组件时重置状态
sMoveCom.startPos.set(startPos);
sMoveCom.targetPos.set(targetPos);
sMoveCom.s_uuid = s_uuid;
sMoveCom.scale = caster.node.scale.x < 0 ? -1 : 1;
// runType/endType/speed/time 由 prefab 的 SkillView @property 决定,配置表同名字段仅作兜底
sMoveCom.runType = SView.runType;
sMoveCom.endType = SView.endType;
sMoveCom.speed = SView.speed > 0 ? SView.speed : config.speed;
sMoveCom.bezierStartHeight = config.bezier_start_y ?? sMoveCom.bezierStartHeight;
// 抛射物高度增量:叠加视图层 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;
sMoveCom.atk_y = SView.atk_y;
sMoveCom.distance = SView.distance;
if (SView.endType === EType.timeEnd) {
let sTimeCom = this.get(StimeDataComp);
if (!sTimeCom) sTimeCom = this.add(StimeDataComp);
sTimeCom.reset();
sTimeCom.s_uuid = s_uuid;
sTimeCom.totalTime = Math.max(1, SView.time > 0 ? SView.time : (config.time ?? 0));
sTimeCom.hitInterval = Math.max(0.5, config.hitcd || 0);
} else {
const sTimeCom = this.get(StimeDataComp);
if (sTimeCom) this.remove(StimeDataComp);
}
// 初始化数据组件
let sDataCom = this.get(SDataCom);
if (!sDataCom) {
sDataCom = this.add(SDataCom);
}
sDataCom.reset();
sDataCom.group = caster.box_group
sDataCom.casterEid = caster.ent.eid
sDataCom.Attrs = {};
// 技能成长数值已由 SkillConfig/SkillOverrides 提供(crt/frz/stun/bck/ap/hit_count),无需再叠加 SkillUpList
const sCrt = config.crt ?? 0;
const sFrz = config.frz ?? 0;
const sStun = config.stun ?? 0;
const sPara = config.para ?? 0;
const sBck = config.bck ?? 0;
const sAp = config.ap;
const sHit = config.hit_count;
sDataCom.Attrs[Attrs.ap] = Math.floor(cAttrsComp.getFinalAp() * sAp / 100); //技能的ap是百分值 需要/100 而且需要再最终计算总ap时再/100,不然会出现ap为90%变0
sDataCom.Attrs[Attrs.critical] = cAttrsComp.getFinalCritical() + sCrt;
sDataCom.Attrs[Attrs.critical_damage] = cAttrsComp.getFinalCritDamage();
sDataCom.Attrs[Attrs.freeze_chance] = cAttrsComp.getFinalFreezeChance() + sFrz;
sDataCom.Attrs[Attrs.stun_chance] = cAttrsComp.getFinalStunChance() + sStun;
sDataCom.Attrs[Attrs.paralyze_chance] = cAttrsComp.getFinalParalyzeChance() + sPara;
sDataCom.Attrs[Attrs.knockback_chance] = cAttrsComp.getFinalKnockbackChance() + sBck;
sDataCom.Attrs[Attrs.knockback_distance] = cAttrsComp.getFinalKnockbackDistance();
// 技能击退概率:取合并后配置的 bck_chance(heroSet overrides 可覆写),未配置视为 0 不击退
sDataCom.Attrs[Attrs.knockback_skill_chance] = config.bck_chance ?? 0;
sDataCom.Attrs[Attrs.puncture_chance] = cAttrsComp.getFinalPunctureChance(); // 初始化携带施法者的穿透概率
sDataCom.Attrs[Attrs.puncture_dmg_bonus] = cAttrsComp.getFinalPunctureDmgBonus(); // 初始化携带施法者的穿透伤害加成
sDataCom.Attrs[Attrs.accuracy] = cAttrsComp.getFinalAccuracy(); // 初始化携带施法者的命中率(含驻场光环与计时 buff)
sDataCom.s_uuid = s_uuid
sDataCom.skill_lv = Math.max(0, skill_lv);
sDataCom.fac = cAttrsComp.fac
sDataCom.ext_dmg = ext_dmg
sDataCom.hit_count = 0
sDataCom.max_hit_count = Math.max(1, sHit)
// proc_skill 已改为施法后触发(SCastSystem 判定),此处不再快照到伤害链
// 快照合并后的对敌 debuff 配置,命中时由 HeroAtkSystem 结算(heroSet overrides/lv_entries 注入的 debuff 依赖此透传)
sDataCom.timed_debuff = config.timed_debuff_id
? { id: config.timed_debuff_id, value: config.debuff_value, duration: config.debuff_duration, chance: config.debuff_chance }
: undefined;
SView.init();
};
let prefab: Prefab = oops.res.get(path, Prefab);
if (prefab) {
initSkillNode(prefab);
} else {
oops.res.loadAsync(path, Prefab).then((res: Prefab) => {
if (res) {
initSkillNode(res);
} else {
mLogger.error(this.debugMode, 'Skill', "[Skill] 预制体加载失败:", path);
}
}).catch((err) => {
mLogger.error(this.debugMode, 'Skill', "[Skill] 预制体加载异常:", path, err);
});
}
}
/** 模块资源释放 */
destroy() {
// 注: 自定义释放逻辑,视图层实现 ecs.IComp 接口的 ecs 组件需要手动释放
this.remove(SDataCom);
this.remove(SMoveDataComp)
this.remove(StimeDataComp)
// 移除组件会触发 reset,但我们在 reset 中移除了 destroy
this.remove(SkillView)
// 回收节点到对象池
if (this.skillNode && this.skillNode.isValid && this.prefabPath) {
Skill.putToPool(this.prefabPath, this.skillNode);
this.skillNode = null;
}
this.prefabPath = "";
super.destroy();
}
}