Files
heros/assets/script/game/hero/HeroViewComp.ts
walkpan 11f1f08c1d refactor(属性系统): 重构英雄属性系统,将属性定义移至HeroAttrs模块
- 将Attrs和DBuff相关定义从SkillSet迁移至HeroAttrs
- 新增NeAttrs枚举用于管理负面状态
- 重构HeroViewComp中的buff/debuff处理逻辑
- 优化属性分类和分组,增加新属性类型
- 移除旧的DBuff相关代码,改用统一的负面状态管理
2025-10-25 15:04:11 +08:00

669 lines
22 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 { Vec3, _decorator , v3,Collider2D,Contact2DType,Label ,Node,Prefab,instantiate,ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, Color, BoxCollider2D} 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 { HeroSpine } from "./HeroSpine";
import { BoxSet, FacSet } from "../common/config/BoxSet";
import { smc } from "../common/SingletonModuleComp";
import { Timer } from "../../../../extensions/oops-plugin-framework/assets/core/common/timer/Timer";
import { SkillSet,BuffConf,} from "../common/config/SkillSet";
import { BuffComp } from "./BuffComp";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { FightSet, TooltipTypes } from "../common/config/Mission";
import { RandomManager } from "db://oops-framework/core/common/random/RandomManager";
import { AttrSet, HeroInfo, HeroUpSet } from "../common/config/heroSet";
import { Attrs, AttrsType, BType, NeAttrs } from "../common/config/HeroAttrs";
const { ccclass, property } = _decorator;
/**
* ==================== BUFF 系统使用说明 ====================
*
* 1. 系统架构
* - BUFF_V/BUFFS_V: 数值型 buff持临时
* - BUFF_R/BUFFS_R: 百分比型 buff持临时
* - DBUFF_V/DBUFFS_V: 数值型 debuff持临时
* - DBUFF_R/DBUFFS_R: 百分比型 debuff持临时
*
* 2. 智能覆盖规则
* - 值更小:不添加(弱效果不覆盖强效果)
* - 值相同且临时:叠加时
* - 值更大:更新为新值(临时则更新值和时间
*
*/
/** 角色显示组件 */
export interface BuffInfo {
attr: Attrs;
value: number;
remainTime?: number;
}
@ccclass('HeroViewComp') // 定义Cocos Creator 组件
@ecs.register('HeroView', false) // 定义ECS 组件
export class HeroViewComp extends CCComp {
BUFFCOMP:BuffComp=null!
as: HeroSpine = null!
status:String = "idle"
hero_uuid:number = 1001;
hero_name : string = "hero";
lv:number =1;
scale: number = 1; /** 角色阵营 1hero -1 :mon */
type: number = 0; /**角色类型 0近战-需要贴1远程-保持距离 2辅助 */
fac:number=0; //阵营 0hero 1monster
box_group:number = BoxSet.HERO;
is_dead:boolean = false; //是否摧毁
is_count_dead:boolean = false; //是否计数死亡
is_stop:boolean = false;
is_atking:boolean = false;
is_boss:boolean = false;
is_big_boss:boolean = false;
is_master:boolean =false;
is_friend:boolean =false;
is_kalami:boolean =false;
skills:any=[]
mp: number = 100;
hp: number = 100; /** 血*/
shield:number=0; //当前护甲
/** 基础属有初始值的基础属后续Attrs 属性计算时用到*/
base_ap: number = 0; //基础攻击
base_map: number = 0;
base_def: number = 5;
base_hp: number = 100;
base_mp: number = 100;
base_speed: number = 100; /** 角色移动速度 */
base_dis: number = 100;
Attrs:any=[]
NeAttrs:any=[]
// Buff debuff 统一管理, value是正,debuff 是负数
// 结构: { [attrIndex: number]: { value: number, remainTime?: number } }
BUFF_V: Record<number, {value: number}> = {} // 持久型数buff
BUFF_R: Record<number, {value: number}> = {} // 持久型百分比 buff
BUFFS_V: Record<number, {value: number, remainTime: number}> = {} // 临时型数buff
BUFFS_R: Record<number, {value: number, remainTime: number}> = {} // 临时型百分比 buff
atk_count: number = 0;
atked_count: number = 0;
private damageQueue: Array<{
damage: number,
isCrit: boolean,
delay: number,
anm:string,
}> = [];
private isProcessingDamage: boolean = false;
private damageInterval: number = 0.01; // 伤害数字显示间隔
onLoad() {
this.as = this.getComponent(HeroSpine);
//console.log("[HeroViewComp]:hero view comp ",this.FIGHTCON)
this.on(GameEvent.FightEnd,this.do_fight_end,this)
const collider = this.node.getComponent(BoxCollider2D);
this.scheduleOnce(()=>{
if (collider) collider.enabled = true; // 先禁
},1)
// let anm = this.node.getChildByName("anm")
// anm.setScale(anm.scale.x*0.8,anm.scale.y*0.8);
}
/** 视图层逻辑代码分离演示 */
start () {
this.as.idle()
this.BUFFCOMP=this.node.getComponent(BuffComp);
// console.log("[HeroViewComp]:heroview"+this.hero_name,this.Attrs)
/** 方向 */
this.node.setScale(this.scale,1);
this.node.getChildByName("top").setScale(this.scale,1);
if(this.is_boss){
this.node.getChildByName("top").position=v3(this.node.position.x,this.node.position.y+100,0)
}
/* 显示角色血*/
this.node.getChildByName("top").getChildByName("hp").active = true;
this.node.getChildByName("top").getChildByName("pow").active = true;
}
// ==================== BUFF系统初始====================
/**
* 初始化角色的 buff debuff
* HeroInfo 读取初始配置,建立属性系
*/
initAttrs() {
// 清空现有 buff/debuff
this.BUFF_V = {
};
this.BUFFS_V = {
};
this.BUFF_R = {
};
this.BUFFS_R = {
};
// 获取英雄配置
const heroInfo = HeroInfo[this.hero_uuid];
if (!heroInfo) return;
// 1. 重置为基础
this.Attrs[Attrs.HP_MAX] = this.base_hp;
this.Attrs[Attrs.MP_MAX] = this.base_mp;
this.Attrs[Attrs.DEF] = this.base_def;
this.Attrs[Attrs.AP] = this.base_ap;
this.Attrs[Attrs.MAP] = this.base_map;
this.Attrs[Attrs.SPEED] = this.base_speed;
this.Attrs[Attrs.DIS] = this.base_dis;
// 2. 初始化其他属性(无初始值的
for (const attrKey in this.Attrs) {
const attrIndex = parseInt(attrKey);
if(
attrIndex !== Attrs.HP_MAX &&
attrIndex !== Attrs.MP_MAX&&
attrIndex !== Attrs.DEF &&
attrIndex !== Attrs.AP &&
attrIndex !== Attrs.MAP &&
attrIndex !== Attrs.SPEED &&
attrIndex !== Attrs.DIS
) {
this.Attrs[attrIndex] = 0;
}
}
// 加载初始 buff
if (heroInfo.buff && heroInfo.buff.length > 0) {
for (const buffConf of heroInfo.buff) {
this.addBuff(buffConf);
}
}
}
// ==================== BUFF管理 ====================
/**
* 添加 buff 效果(智能覆盖)
* @param buffConf buff 配置 (来自 SkillSet.BuffConf heroSet.buff)
*
* 智能覆盖规则
* 1. 值更小:不添
* 2. 值相同且都是临时:叠加时
* 3. 值更大:更新为新值(临时则更新值和时间
*/
addBuff(buffConf: BuffConf) {
const isValue = buffConf.BType === BType.VALUE;
const isPermanent = buffConf.time === 0;
const attrIndex = buffConf.buff;
// 根据类型选择对应buff 字典
const permanentBuffs = isValue ? this.BUFF_V : this.BUFF_R;
const temporaryBuffs = isValue ? this.BUFFS_V : this.BUFFS_R;
if (isPermanent) {
// 添加持久buff
const existing = permanentBuffs[attrIndex];
if (existing) {
// 值更小,不添
if (buffConf.value <= existing.value) {
return;
}
// 值更大,更新
existing.value = buffConf.value;
} else {
// 没有同类型,直接添加
permanentBuffs[attrIndex] = { value: buffConf.value };
}
} else {
// 添加临时buff
const existing = temporaryBuffs[attrIndex];
if (existing) {
if (buffConf.value < existing.value) {
// 值更小,不添
return;
} else if (buffConf.value === existing.value) {
// 值相同,叠加时间
existing.remainTime += buffConf.time;
return; // 时间叠加不需要重算属
} else {
// 值更大,更新值和时间
existing.value = buffConf.value;
existing.remainTime = buffConf.time;
}
} else {
// 没有同类型,直接添加
temporaryBuffs[attrIndex] = {
value: buffConf.value,
remainTime: buffConf.time
};
}
}
this.recalculateSingleAttr(buffConf.buff);
}
// ==================== 属性计算系====================
/**
* 重新计算单个属性
* @param attrIndex 属性索引
*
* 计算公式:
* - 数值型属性:最终值 = (基础值 + 数值型buff - 数值型debuff) × (1 + 百分比buff/100 - 百分比debuff/100)
* - 百分比型属性:最终值 = 基础值 + 数值型buff - 数值型debuff + 百分比buff - 百分比debuff
*/
private recalculateSingleAttr(attrIndex: number) {
// 1. 获取基础值
const baseValues: Record<number, number> = {
[Attrs.HP_MAX]: this.base_hp,
[Attrs.MP_MAX]: this.base_mp,
[Attrs.DEF]: this.base_def,
[Attrs.AP]: this.base_ap,
[Attrs.MAP]: this.base_map,
[Attrs.SPEED]: this.base_speed,
[Attrs.SHIELD_MAX]: 0
};
const baseVal = baseValues[attrIndex] !== undefined ? baseValues[attrIndex] : 0;
// 2. 收集所有数值型 buff/debuff
let totalValue = baseVal;
// Buff直接使用 Attrs 索引
if (this.BUFF_V[attrIndex]) {
totalValue += this.BUFF_V[attrIndex].value;
}
if (this.BUFFS_V[attrIndex]) {
totalValue += this.BUFFS_V[attrIndex].value;
}
// 3. 收集所有百分比型 buff/debuff
let totalRatio = 0; // 总百分比(可正可负)
// Buff直接使用 Attrs 索引
if (this.BUFF_R[attrIndex]) {
totalRatio += this.BUFF_R[attrIndex].value;
}
if (this.BUFFS_R[attrIndex]) {
totalRatio += this.BUFFS_R[attrIndex].value;
}
// 4. 根据属性类型计算最终值
const attrType = AttrsType[attrIndex];
const isRatioAttr = attrType === BType.RATIO;
if (isRatioAttr) {
// 百分比型属性:直接加减
this.Attrs[attrIndex] = totalValue + totalRatio;
} else {
// 数值型属性:(基础值+数值) × (1 + 百分比/100)
this.Attrs[attrIndex] = Math.floor(totalValue * (1 + totalRatio / 100));
}
// 5. 确保属性值合理
this.clampSingleAttr(attrIndex);
}
/**
* 确保单个属性值合
*/
private clampSingleAttr(attrIndex: number) {
switch(attrIndex) {
case Attrs.HP_MAX:
case Attrs.MP_MAX:
this.Attrs[attrIndex] = Math.max(1, this.Attrs[attrIndex]);
break;
case Attrs.DEF:
case Attrs.AP:
case Attrs.MAP:
this.Attrs[attrIndex] = Math.max(1, this.Attrs[attrIndex]);
break;
case Attrs.CRITICAL:
case Attrs.DODGE:
case Attrs.HIT:
this.Attrs[attrIndex] = Math.max(0, Math.min(AttrSet.ATTR_MAX, this.Attrs[attrIndex])); //AttrSet.ATTR_MAX =85
break;
}
}
// ==================== 临时 BUFF/DEBUFF 更新 ====================
/**
* 更新临时 buff/debuff 的剩余时
* 应在 update 中定期调
* @param dt 时间
*/
updateTemporaryBuffsDebuffs(dt: number) {
const affectedAttrs = new Set<number>();
// 更新临时型数buff
for (const attrIndex in this.BUFFS_V) {
const buff = this.BUFFS_V[attrIndex];
buff.remainTime -= dt;
if (buff.remainTime <= 0) {
delete this.BUFFS_V[attrIndex];
affectedAttrs.add(parseInt(attrIndex));
}
}
// 更新临时型百分比 buff
for (const attrIndex in this.BUFFS_R) {
const buff = this.BUFFS_R[attrIndex];
buff.remainTime -= dt;
if (buff.remainTime <= 0) {
delete this.BUFFS_R[attrIndex];
affectedAttrs.add(parseInt(attrIndex));
}
}
// 负面状态更新
for (const key in this.NeAttrs) {
const debuff = this.NeAttrs[key];
debuff.remainTime -= dt;
if (debuff.remainTime <= 0) {
debuff.remainTime = 0;
}
}
// 只重新计算受影响的属
affectedAttrs.forEach(attrIndex => {
this.recalculateSingleAttr(attrIndex);
});
}
public isStun() {
return this.NeAttrs[NeAttrs.IN_STUN].time > 0;
}
public isFrost() {
return this.NeAttrs[NeAttrs.IN_FROST].time > 0;
}
update(dt: number){
if(!smc.mission.play||smc.mission.pause) return
// if(this.is_dead) {
// this.ent.destroy();
// return
// }
this.BaseUp(dt)
// 更新所有按时间减少的buff和debuff
this.in_stop(dt);
// 处理伤害队列
this.processDamageQueue();
// 更新临时 buff/debuff 时间
this.updateTemporaryBuffsDebuffs(dt);
this.BUFFCOMP.hp_show(this.hp,this.Attrs[Attrs.HP_MAX])
this.BUFFCOMP.mp_show(this.mp,this.Attrs[Attrs.MP_MAX])
this.BUFFCOMP.show_shield(this.shield,this.Attrs[Attrs.SHIELD_MAX])
}
BaseUp(dt:number){
this.mp += HeroUpSet.MP*dt
this.hp += HeroUpSet.HP*dt
if(this.mp > this.Attrs[Attrs.MP_MAX]) this.mp = this.Attrs[Attrs.MP_MAX]
if(this.hp > this.Attrs[Attrs.HP_MAX]) this.hp = this.Attrs[Attrs.HP_MAX]
}
do_fight_end(){
this.as.do_buff()
}
get isActive() {
return this.ent.has(HeroViewComp) && this.node?.isValid;
}
//状态切
status_change(type:string){
this.status=type
if(type == "idle"){
this.as.idle()
// this.as.change_default("idle")
}
if(type == "move"){
this.as.move()
// this.as.change_default("move")
}
}
add_shield(shield:number){
this.shield = this.Attrs[Attrs.SHIELD_MAX] +=shield
if(this.shield>0) this.BUFFCOMP.show_shield(this.shield,this.Attrs[Attrs.SHIELD_MAX])
}
health(hp: number = 0,is_num:boolean=true) {
this.BUFFCOMP.heathed();
let real_hp=0
let hp_max=this.Attrs[Attrs.HP_MAX]
let lost_hp=hp_max-this.hp
if(is_num){
if(lost_hp > hp){
real_hp=Math.floor(hp);
}else{
real_hp=lost_hp;
}
}else{
if(lost_hp > hp/100*hp_max){
real_hp=Math.floor(hp/100*hp_max);
}else{
real_hp=lost_hp;
}
}
if(real_hp > 0){
this.hp+=real_hp;
this.BUFFCOMP.tooltip(TooltipTypes.health,real_hp.toFixed(0));
}
this.BUFFCOMP.hp_show(this.hp,this.Attrs[Attrs.HP_MAX])
// this.update_vm
}
/** 静止时间 */
in_stop (dt: number) {
}
count_atk_count(){ //主将攻击
if(this.fac==FacSet.MON) return
this.atk_count+=1
}
do_dead(){
this.do_dead_trigger()
//console.log("[HeroViewComp]:角色死亡",this.hero_uuid)
if(this.is_count_dead) return
this.is_count_dead=true
if(this.fac==FacSet.MON){
this.scheduleOnce(()=>{
this.do_drop()
},0.1)
}
if(this.fac==FacSet.HERO){
this.scheduleOnce(()=>{
oops.message.dispatchEvent(GameEvent.HeroDead,{hero_uuid:this.hero_uuid})
},0.1)
}
if(this.fac==FacSet.HERO){
//console.log("[HeroViewComp]:英雄死亡")
// oops.message.dispatchEvent(GameEvent.FightEnd,{victory:false})
}
}
do_drop(){
}
do_atked(remainingDamage:number,CAttrs:any,s_uuid:number){
let SConf=SkillSet[s_uuid]
this.do_atked_trigger()
if(this.check_dodge()) return
let is_crit = this.check_crit(CAttrs[Attrs.CRITICAL])
if(this == null) return;
let damage = this.count_damage(remainingDamage)
if(is_crit) {
damage = Math.floor(damage * (1 + (FightSet.CRIT_DAMAGE+CAttrs[Attrs.CRITICAL_DMG])/100))
}
// console.log(this.hero_name+"[HeroViewComp]:heroview :damage|hp|hp_max",damage,this.hp,this.Attrs[BuffAttr.HP_MAX])
damage=this.check_shield(damage)
if(damage <= 0) return
this.hp -= damage;
if(this.hp <= 0) {
if(this == null) return;
this.is_dead=true
if(this.BUFFCOMP){
this.BUFFCOMP.dead()
}
this.do_dead()
//console.log("[HeroViewComp]:dead,fac => "+(this.fac==FacSet.HERO?"hero":"monster"))
if(this.ent == null) return;
if(this.fac ==FacSet.HERO){
this.to_grave()
}else{
this.ent.destroy();
}
}
// this.update_vm
this.back()
this.showDamage(damage, is_crit,SConf.AtkedName);
}
//后退
back(){
if(this.fac==FacSet.MON) {
let tx=this.node.position.x+5
if(tx > 320) tx=320
tween(this.node).to(0.1, { position:v3(tx,this.node.position.y,0)}).start()
}
if(this.fac==FacSet.HERO) {
let tx=this.node.position.x-5
if(tx < -320) tx=-320
tween(this.node).to(0.1, { position:v3(tx,this.node.position.y,0)}).start()
}
}
//伤害计算 debuff 易伤
count_damage(remainingDamage:number){
return remainingDamage
}
check_shield(damage:number){
if(this.shield <= 0 ) return damage
if(this.shield >= damage){
this.shield -= damage
this.BUFFCOMP.tooltip(TooltipTypes.uskill,"*吸收*");
if(this.shield <= 0){
this.shield=this.Attrs[Attrs.SHIELD_MAX]=0
}
damage = 0
}
if(this.shield < damage){
damage=damage-this.shield
this.shield=0
this.Attrs[Attrs.SHIELD_MAX]=0
}
this.BUFFCOMP.show_shield(this.shield,this.Attrs[Attrs.SHIELD_MAX])
return damage
}
check_dodge(){
if(this.Attrs[Attrs.DODGE] > 0){
let random = Math.random()*100
if(random < this.Attrs[Attrs.DODGE]) {
this.BUFFCOMP.tooltip(TooltipTypes.uskill,"*闪避*");
return true
}
}
return false
}
check_crit(crit:number=0){
if(crit > 0){
let random = Math.random()*100
if(random < crit) {
//console.log("[HeroViewComp]:crit",crit,random)
return true
}
}
//console.log("[HeroViewComp]:crit",crit)
return false
}
do_dead_trigger(){ //死亡特殊处理
if(this.is_dead||this.fac==FacSet.MON) return
}
do_atked_trigger(){ //受伤特殊处理
if(this.is_dead||this.fac==FacSet.MON) return
}
to_grave(){
tween(this.node).to(0.5, { position:v3(-900,this.node.position.y+300,0)},{
onComplete: (target?: object) => {
this.node.setPosition(-900,this.node.position.y-300,0)
}
}).start()
}
// to_alive(){
// this.is_dead=false
// this.currentHp=this.currentHpMax*(100+this.hp_buff)/100
// this.BUFFCOMP.vmdata_update(true)
// this.node.setPosition(HeroPos[this.fight_pos].pos)
// this.BUFFCOMP.heathed()
// }
to_console(value:any,value2:any=null,value3:any=null){
//console.log("["+this.scale+this.hero_name+']'+value,value2,value3)
}
reset() {
this.is_dead = false;
const collider = this.getComponent(Collider2D);
if (collider) {
collider.off(Contact2DType.BEGIN_CONTACT);
}
this.scheduleOnce(() => {
this.node.destroy();
}, 0.1);
}
playSkillEffect(skill_id:number) {
let skill = SkillSet[skill_id]
switch(skill.act){
case "max":
this.as.max()
this.BUFFCOMP.tooltip(TooltipTypes.skill,skill.name,skill_id)
break
case "atk":
this.as.atk()
break
}
}
/** 显示伤害数字 */
showDamage(damage: number, isCrit: boolean,anm:string="atked") {
this.damageQueue.push({
damage,
isCrit,
delay: this.damageInterval,
anm
});
}
/** 处理伤害队列 */
private processDamageQueue() {
if (this.isProcessingDamage || this.damageQueue.length === 0) return;
this.isProcessingDamage = true;
const damageInfo = this.damageQueue.shift()!;
this.showDamageImmediate(damageInfo.damage, damageInfo.isCrit,damageInfo.anm);
// 设置延时处理下一个伤
this.scheduleOnce(() => {
this.isProcessingDamage = false;
}, this.damageInterval);
}
/** 立即显示伤害效果 */
private showDamageImmediate(damage: number, isCrit: boolean,anm:string="atked") {
this.BUFFCOMP.hp_show(this.hp,this.Attrs[Attrs.HP_MAX])
this.BUFFCOMP.in_atked(anm,this.fac==FacSet.HERO?1:-1)
this.atked_count++;
if (isCrit) {
this.BUFFCOMP.hp_tip(TooltipTypes.crit, damage.toFixed(0), damage);
// //console.log("暴击伤害 + damage);
} else {
this.BUFFCOMP.hp_tip(TooltipTypes.life, damage.toFixed(0), damage);
// //console.log("普通伤害:" + damage);
}
}
}