Files
pixelheros/assets/script/game/hero/SCastSystem.ts
walkpan 0f56591376 refactor(skill): 重构技能数据结构并支持技能等级
- 将 HeroAttrsComp 中的技能数组和独立 CD 映射重构为统一的 HSkillInfo 对象记录
- 在 SDataCom 中新增 skill_lv 字段,并在 Skill 加载时传入技能等级
- 更新 Hero 和 Monster 初始化逻辑以适配新的技能数据结构
- 修改 SCastSystem 以传递技能等级并影响技能效果
- 更新 heroSet 配置,将 skills 字段类型改为 Record<number, HSkillInfo>
2026-03-22 16:25:46 +08:00

279 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 { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { Vec3 } from "cc";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { BuffsList, DTType, SkillConfig, SkillKind, SkillSet, TGroup } from "../common/config/SkillSet";
import { Skill } from "../skill/Skill";
import { smc } from "../common/SingletonModuleComp";
import { GameConst } from "../common/config/GameConst";
import { HType } from "../common/config/heroSet";
/**
* ==================== 自动施法系统 ====================
*
* 职责:
* 1. 检测可施放的技能
* 2. 根据策略自动施法AI
* 3. 选择目标
* 4. 添加施法请求标记
*
* 设计理念:
* - 负责"何时施法"的决策
*/
@ecs.register('SCastSystem')
export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
debugMode: boolean = false; // 是否启用调试模式
private readonly emptyCastPlan = { skillId: 0, skillLv: 1, isFriendly: false, targetPos: null as Vec3 | null, targetEids: [] as number[] };
private readonly meleeCastRange = 64;
private heroMatcher: ecs.IMatcher | null = null;
private getHeroMatcher(): ecs.IMatcher {
if (!this.heroMatcher) {
this.heroMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
}
return this.heroMatcher;
}
filter(): ecs.IMatcher {
return this.getHeroMatcher();
}
update(e: ecs.Entity): void {
if(!smc.mission.play ) return;
if(smc.mission.pause) return
const heroAttrs = e.get(HeroAttrsComp);
const heroView = e.get(HeroViewComp);
if (!heroAttrs || !heroView || !heroView.node) return;
if (heroAttrs.is_dead || heroAttrs.is_reviving || heroAttrs.isFrost()) return;
heroAttrs.updateCD(this.dt);
heroView.cd_show();
const castPlan = this.pickCastSkill(heroAttrs, heroView);
if (!this.hasCastTarget(castPlan)) return;
this.castSkill(castPlan, heroAttrs, heroView);
}
private pickCastSkill(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): { skillId: number; skillLv: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[] } {
const type = heroAttrs.type as HType;
const maxRange = this.resolveMaxCastRange(heroAttrs, type);
const target = this.findNearestEnemyInRange(heroAttrs, heroView, maxRange);
const skillCandidates = this.buildSkillCandidates(heroAttrs.getSkillIds());
const selfEid = heroView.ent?.eid;
for (const s_uuid of skillCandidates) {
if (!s_uuid) continue;
const config = SkillSet[s_uuid];
if (!config) continue;
if (!heroAttrs.isSkillReady(s_uuid)) continue;
const skillLv = heroAttrs.getSkillLevel(s_uuid);
if (this.isSelfSkill(config.TGroup)) {
if (typeof selfEid !== "number") continue;
return { skillId: s_uuid, skillLv, isFriendly: true, targetPos: null, targetEids: [selfEid] };
}
if (this.isFriendlySkill(config.TGroup)) {
const includeSelf = config.TGroup === TGroup.Ally;
const friendlyEids = this.collectFriendlyTargetEids(heroAttrs.fac, selfEid, includeSelf);
if (friendlyEids.length === 0) continue;
return { skillId: s_uuid, skillLv, isFriendly: true, targetPos: null, targetEids: friendlyEids };
}
if (!target || !heroView.node || !target.node) continue;
const targetPos = this.resolveEnemyCastTargetPos(config, heroAttrs, heroView, target, maxRange);
if (!targetPos) continue;
return { skillId: s_uuid, skillLv, isFriendly: false, targetPos, targetEids: [] };
}
return this.emptyCastPlan;
}
private castSkill(castPlan: { skillId: number; skillLv: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[] }, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) {
const s_uuid = castPlan.skillId;
const skillLv = castPlan.skillLv;
const config = SkillSet[s_uuid];
if (!config) return;
//播放前摇技能动画
heroView.playReady(config.readyAnm);
//播放角色攻击动画
heroView.playSkillAnm(config.act);
// 优先使用技能配置的前摇时间,否则使用全局默认值
// 注意:这里仍然是基于时间的延迟,受帧率波动影响。
// 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。
const delay = config.ready > 0 ? config.ready : GameConst.Battle.SKILL_CAST_DELAY;
heroView.scheduleOnce(() => {
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
if (castPlan.isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(castPlan.targetEids, heroAttrs.fac);
if (friendlyTargets.length === 0) return;
this.applyPrimaryEffect(s_uuid, skillLv, config, heroView,heroAttrs, friendlyTargets, null);
this.applyExtraEffects(config, friendlyTargets);
return;
}
this.applyPrimaryEffect(s_uuid, skillLv, config, heroView,heroAttrs, [], castPlan.targetPos);
}, delay);
heroAttrs.triggerSkillCD(s_uuid);
}
private buildSkillCandidates(skillIds: number[]): number[] {
if (!skillIds || skillIds.length === 0) return [];
if (skillIds.length === 1) return [skillIds[0]];
return [...skillIds.slice(1), skillIds[0]];
}
private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp,cAttrsComp: HeroAttrsComp, targetPos: Vec3) {
if (!caster.node || !caster.node.isValid) return;
const parent = caster.node.parent;
if (!parent) return;
const skill = ecs.getEntity<Skill>(Skill);
skill.load(caster.node.position.clone(), parent, s_uuid, targetPos.clone(), caster, cAttrsComp, skillLv, 0);
}
private applyPrimaryEffect(s_uuid: number, skillLv: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp,targets: HeroViewComp[], targetPos: Vec3 | null) {
const kind = config.kind ?? SkillKind.Damage;
if (kind === SkillKind.Damage) {
if (config.ap <= 0 || !targetPos) return;
this.createSkillEntity(s_uuid, skillLv, heroView,cAttrsComp, targetPos);
return;
}
for (const target of targets) {
if (!target.ent) continue;
const model = target.ent.get(HeroAttrsComp);
if (!model || model.is_dead) continue;
if (kind === SkillKind.Heal && config.ap !== 0) {
let addHp = model.add_hp(config.ap, false);
target.health(addHp);
} else if (kind === SkillKind.Shield && config.ap !== 0) {
model.add_shield(config.ap, false);
}
}
}
//应用额外效果
private applyExtraEffects(config: SkillConfig, targets: HeroViewComp[]) {
for (const target of targets) {
if (!target.ent) continue;
const model = target.ent.get(HeroAttrsComp);
if (!model || model.is_dead) continue;
if (config.buffs) {
for (const buffId of config.buffs) {
const buffConf = BuffsList[buffId];
if (buffConf) {
model.addBuff(buffConf);
}
}
}
}
}
private resolveFriendlyTargets(targetEids: number[], fac: number): HeroViewComp[] {
const targets: HeroViewComp[] = [];
for (const eid of targetEids) {
const entity = ecs.getEntityByEid(eid);
if (!entity) continue;
const model = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!model || !view?.node) continue;
if (model.fac !== fac) continue;
if (model.is_dead || model.is_reviving) continue;
targets.push(view);
}
return targets;
}
private collectFriendlyTargetEids(fac: number, selfEid: number | undefined, includeSelf: boolean): number[] {
const eids: number[] = [];
ecs.query(this.getHeroMatcher()).forEach(entity => {
const model = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!model || !view?.node || !view.ent) return;
if (model.fac !== fac) return;
if (model.is_dead || model.is_reviving) return;
const eid = view.ent.eid;
if (!includeSelf && typeof selfEid === "number" && eid === selfEid) return;
eids.push(eid);
});
return eids;
}
private hasCastTarget(castPlan: { skillId: number; skillLv: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[] }): boolean {
if (castPlan.skillId === 0) return false;
if (castPlan.isFriendly) return castPlan.targetEids.length > 0;
return !!castPlan.targetPos;
}
private isFriendlySkill(group: TGroup): boolean {
return group === TGroup.Team || group === TGroup.Ally;
}
private isSelfSkill(group: TGroup): boolean {
return group === TGroup.Self;
}
private findNearestEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null {
if (!heroView.node) return null;
const currentX = heroView.node.position.x;
let nearest: HeroViewComp | null = null;
let minDist = Infinity;
ecs.query(this.getHeroMatcher()).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!attrs || !view?.node) return;
if (attrs.fac === heroAttrs.fac) return;
if (attrs.is_dead || attrs.is_reviving) return;
const dist = Math.abs(currentX - view.node.position.x);
if (dist > maxRange) return;
if (dist >= minDist) return;
minDist = dist;
nearest = view;
});
return nearest;
}
private findFrontEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number, nearestEnemy: HeroViewComp): HeroViewComp | null {
if (!heroView.node || !nearestEnemy.node) return null;
const currentX = heroView.node.position.x;
const direction = nearestEnemy.node.position.x >= currentX ? 1 : -1;
let frontEnemy: HeroViewComp | null = null;
let edgeX = direction > 0 ? Infinity : -Infinity;
ecs.query(this.getHeroMatcher()).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!attrs || !view?.node) return;
if (attrs.fac === heroAttrs.fac) return;
if (attrs.is_dead || attrs.is_reviving) return;
const enemyX = view.node.position.x;
const dist = Math.abs(currentX - enemyX);
if (dist > maxRange) return;
if (direction > 0) {
if (enemyX >= edgeX) return;
edgeX = enemyX;
} else {
if (enemyX <= edgeX) return;
edgeX = enemyX;
}
frontEnemy = view;
});
return frontEnemy;
}
private resolveEnemyCastTargetPos(config: SkillConfig, heroAttrs: HeroAttrsComp, heroView: HeroViewComp, nearestEnemy: HeroViewComp, maxRange: number): Vec3 | null {
if (config.DTType !== DTType.range) {
return this.buildEnemyCastTargetPos(heroView, nearestEnemy, maxRange);
}
const frontEnemy = this.findFrontEnemyInRange(heroAttrs, heroView, maxRange, nearestEnemy);
if (!frontEnemy?.node) return null;
return frontEnemy.node.position.clone();
}
private resolveMaxCastRange(heroAttrs: HeroAttrsComp, type: HType): number {
const cached = heroAttrs.getCachedMaxSkillDistance();
if (cached > 0) return cached;
if (type === HType.Long) return 720;
if (type === HType.Mid) return 360;
return this.meleeCastRange;
}
private buildEnemyCastTargetPos(caster: HeroViewComp, target: HeroViewComp, castRange: number): Vec3 {
const casterPos = caster.node.position;
const targetPos = target.node.position;
const direction = targetPos.x >= casterPos.x ? 1 : -1;
return new Vec3(casterPos.x + direction * castRange, casterPos.y, casterPos.z);
}
}