Files
pixelheros/assets/script/game/hero/SCastSystem.ts
panw c3badecd71 fix: 修正技能AP百分比计算错误
修复技能AP百分比计算逻辑,移除重复除以100的操作。在Skill.ts中移除多余注释,在SCastSystem.ts中正确计算治疗量,确保技能AP作为百分比值正确处理。
2026-03-24 10:39:47 +08:00

399 lines
18 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 { DTType, SkillConfig, SkillKind, SkillSet, SkillUpList, 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";
import { Attrs } from "../common/config/HeroAttrs";
/**
* ==================== 自动施法系统 ====================
*
* 职责:
* 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;
/** 查询缓存:避免每帧重复创建 matcher */
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();
}
/**
* 每帧更新:
* 1. 战斗状态校验
* 2. 英雄施法CD更新与显示
* 3. 选取本帧可施放技能并执行施法
*/
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);
}
/**
* 选择当前应释放的技能。
* 选择顺序:技能候选列表顺序 + 条件过滤CD、目标可达、目标类型匹配
* 返回内容同时包含“对敌施法”和“友方施法”两种执行所需数据。
*/
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;
}
/**
* 执行施法:
* - 播放前摇与技能动作
* - 延迟到出手时机后,按技能目标类型分发到对敌/友方效果处理
* - 触发技能CD
*/
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];
const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid]:SkillUpList[1001];
const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0)));
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;
const castTimes = 1 + cNum;
for (let i = 0; i < castTimes; i++) {
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.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
continue;
}
const enemyTargetPos = this.resolveRepeatCastTargetPos(castPlan.targetPos, i);
this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i);
}
}, delay);
heroAttrs.triggerSkillCD(s_uuid);
}
private resolveRepeatCastTargetPos(targetPos: Vec3 | null, castIndex: number): Vec3 | null {
if (!targetPos) return null;
if (castIndex === 1) return new Vec3(targetPos.x, targetPos.y + 15, targetPos.z);
if (castIndex === 2) return new Vec3(targetPos.x, targetPos.y - 15, targetPos.z);
return targetPos;
}
/** 构建技能尝试顺序:优先主动技能,其次普攻 */
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, castIndex: number = 0) {
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);
skill.load(startPos, parent, s_uuid, targetPos.clone(), caster, cAttrsComp, skillLv, 0);
}
/**
* 对敌技能效果处理。
* 当前职责:仅处理伤害技能并创建技能实体。
*/
private applyEnemySkillEffects(s_uuid: number, skillLv: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3 | null, castIndex: number = 0) {
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);
}
private resolveRepeatCastStartPos(startPos: Readonly<Vec3>, castIndex: number): Vec3 {
if (castIndex === 1) return new Vec3(startPos.x, startPos.y + 15, startPos.z);
if (castIndex === 2) return new Vec3(startPos.x, startPos.y - 15, startPos.z);
return startPos.clone();
}
/**
* 友方技能效果处理。
* 当前职责:
* 1. 处理治疗与护盾
* 2. 处理 buffs 配置追加
* 3. 保留完整施法信息参数,便于后续扩展更多友方效果
*/
private applyFriendlySkillEffects(_s_uuid: number, _skillLv: number, config: SkillConfig, _heroView: HeroViewComp, _cAttrsComp: HeroAttrsComp, targets: HeroViewComp[], _targetPos: Vec3 | null) {
const kind = config.kind ?? SkillKind.Support;
const sUp = SkillUpList[_s_uuid] ?? SkillUpList[1001];
const sAp =config.ap+sUp.ap*_skillLv;
const sHit=config.hit_count+sUp.hit_count*_skillLv;
const selectedTargets = this.pickRandomFriendlyTargets(targets, sHit);
const applyTargets = kind === SkillKind.Heal ? this.sortTargetsByLowestHp(selectedTargets) : selectedTargets;
for (const target of applyTargets) {
if (!target.ent) continue;
const model = target.ent.get(HeroAttrsComp);
if (!model || model.is_dead) continue;
if (kind === SkillKind.Heal && sAp !== 0) {
const addHp = Math.floor(sAp*_cAttrsComp.ap/100);//技能的ap是百分值 需要/100
model.add_hp(addHp);
target.health(addHp);
} else if (kind === SkillKind.Shield && sAp !== 0) {
const addShield = Math.floor(sAp*_cAttrsComp.ap);
model.add_shield(addShield);
}
if (!config.buffs || config.buffs.length === 0) continue;
for (const buffConf of config.buffs) {
if (!buffConf) continue;
const sBuffAp=buffConf.value+sUp.buff_ap
const sBuffHp=buffConf.value+sUp.buff_hp
switch (buffConf.buff){
case Attrs.ap:
model.add_ap(sBuffAp)
//加工动画
break
case Attrs.hp_max:
model.add_hp_max(sBuffHp)
//加最大生命值动画
break
}
}
}
}
private pickRandomFriendlyTargets(targets: HeroViewComp[], hitCount: number): HeroViewComp[] {
if (!targets || targets.length === 0) return [];
const validHitCount = Math.max(1, Math.floor(hitCount));
if (validHitCount >= targets.length) return [...targets];
const pool = [...targets];
const selected: HeroViewComp[] = [];
while (selected.length < validHitCount && pool.length > 0) {
const index = Math.floor(Math.random() * pool.length);
const [target] = pool.splice(index, 1);
if (!target) continue;
selected.push(target);
}
return selected;
}
private sortTargetsByLowestHp(targets: HeroViewComp[]): HeroViewComp[] {
return [...targets].sort((a, b) => {
const aModel = a.ent?.get(HeroAttrsComp);
const bModel = b.ent?.get(HeroAttrsComp);
const aRatio = aModel && aModel.hp_max > 0 ? aModel.hp / aModel.hp_max : 1;
const bRatio = bModel && bModel.hp_max > 0 ? bModel.hp / bModel.hp_max : 1;
if (aRatio !== bRatio) return aRatio - bRatio;
const aHp = aModel?.hp ?? Number.MAX_SAFE_INTEGER;
const bHp = bModel?.hp ?? Number.MAX_SAFE_INTEGER;
return aHp - bHp;
});
}
/** 根据目标 eid 列表解析出有效友方视图目标 */
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;
}
/**
* 收集可作为友方技能目标的实体 eid。
* includeSelf 控制是否包含施法者自身。
*/
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;
}
/**
* 在施法距离内查找“最前排”敌人。
* 依据施法者面向方向选择 x 轴上更前的目标。
*/
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);
}
}