Files
pixelheros/assets/script/game/map/MissionCardComp.ts
panw 0ce80dd42a refactor(game): 重构属性卡逻辑以使用 Buff 系统
将属性卡的效果应用方式从直接修改全局属性改为通过 addBuff 添加永久 Buff。
这样可以统一属性加成处理逻辑,利用现有的 Buff 系统进行管理,提高代码的可维护性和扩展性。
2026-02-03 16:21:36 +08:00

704 lines
30 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 { mLogger } from "../common/Logger";
import { _decorator, Label, Node, tween, Vec3, Color, Sprite, Tween, SpriteAtlas, resources } 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 { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
import { CardType, FightSet, CardKind } from "../common/config/GameSet";
import { getCardOptions, ICardInfo } from "../common/config/CardSet";
import { TalComp } from "../hero/TalComp";
import { HeroSkillsComp } from "../hero/HeroSkills";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { BuffConf } from "../common/config/SkillSet";
import { BType } from "../common/config/HeroAttrs";
import { AttrCards, PotionCards } from "../common/config/AttrSet";
import { HeroMasterComp } from "../hero/HeroMasterComp";
const { ccclass, property } = _decorator;
interface ICardEvent {
type?: CardType;
level?: number;
}
/** 视图层对象 */
@ccclass('MissionCardComp')
@ecs.register('MissionCard', false)
export class MissionCardComp extends CCComp {
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = true;
/** 视图层逻辑代码分离演示 */
@property(Node)
card1:Node = null!
@property(Node)
card2:Node = null!
@property(Node)
card3:Node = null!
@property(Node)
card4:Node = null!
@property(Node)
btnClose: Node = null!
@property(Node)
Lock: Node = null!
@property(Node)
unLock: Node = null!
@property(Node)
noStop: Node = null!
card1_data: ICardInfo = null!
card2_data: ICardInfo = null!
card3_data: ICardInfo = null!
card4_data: ICardInfo = null!
// 当前卡片类型 (用于特殊获取模式)
curCardType: CardType | null = null;
// 是否处于锁定状态
private isLocked: boolean = true;
// 是否永久解锁(本局)
private isAdUnlocked: boolean = false;
// 图标图集缓存
private uiconsAtlas: SpriteAtlas | null = null;
onLoad() {
if (this.btnClose) {
this.btnClose.on(Node.EventType.TOUCH_END, this.onGiveUp, this);
}
oops.message.on(GameEvent.TalentSelect, this.onTalentSelect, this);
oops.message.on(GameEvent.AttrSelect, this.onAttrSelect, this);
oops.message.on(GameEvent.HeroSkillSelect, this.onHeroSkillSelect, this);
oops.message.on(GameEvent.ShopOpen, this.onShopOpen, this);
oops.message.on(GameEvent.MissionStart, this.onMissionStart, this);
oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this);
oops.message.on(GameEvent.ToCallFriend, this.onCallFriend, this);
}
onDestroy() {
if (this.btnClose) {
this.btnClose.off(Node.EventType.TOUCH_END, this.onGiveUp, this);
}
oops.message.off(GameEvent.TalentSelect, this.onTalentSelect, this);
oops.message.off(GameEvent.AttrSelect, this.onAttrSelect, this);
oops.message.off(GameEvent.HeroSkillSelect, this.onHeroSkillSelect, this);
oops.message.off(GameEvent.ShopOpen, this.onShopOpen, this);
oops.message.off(GameEvent.MissionStart, this.onMissionStart, this);
oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this);
oops.message.off(GameEvent.ToCallFriend, this.onCallFriend, this);
this.ent.destroy();
}
init(){
this.onMissionStart();
}
/** 游戏开始初始化 */
onMissionStart() {
this.isLocked = true;
this.isAdUnlocked = false;
this.noStop.active = false;
if (this.Lock) this.Lock.active = false; // 初始不显示,等待 showCardType
if(this.unLock) this.unLock.active=false
this.eventQueue = [];
}
/** 游戏结束清理 */
onMissionEnd() {
this.eventQueue = [];
this.node.active = false;
this.hasSelected = false;
// 停止所有卡片动画
const cards = [this.card1, this.card2, this.card3, this.card4];
cards.forEach(card => {
if (card) {
Tween.stopAllByTarget(card);
const selected = card.getChildByName("selected");
if (selected) Tween.stopAllByTarget(selected);
}
});
}
start() {
// 初始隐藏或显示逻辑
this.node.active = false;
this.resetCardStates();
}
private resetCardStates() {
const cards = [this.card1, this.card2, this.card3, this.card4];
cards.forEach(card => {
if (card) {
const selected = card.getChildByName("selected");
if (selected) selected.active = false;
// 恢复缩放和颜色
card.setScale(1, 1, 1);
const sprite = card.getComponent(Sprite);
if (sprite) sprite.color = new Color(255, 255, 255);
}
});
}
// 是否已经选择了天赋
private hasSelected: boolean = false;
// 事件队列
private eventQueue: ICardEvent[] = [];
private onShopOpen(event: string, args: any) {
this.eventQueue.push({ type: CardType.Potion });
this.checkQueue();
}
private onAttrSelect(event: string, args: any) {
this.eventQueue.push({ type: CardType.Attr });
this.checkQueue();
}
private onTalentSelect(event: string, args: any) {
this.eventQueue.push({ type: CardType.Talent });
this.checkQueue();
}
private onHeroSkillSelect(event: string, args: any) {
this.eventQueue.push({ type: CardType.Skill });
this.checkQueue();
}
private onCallFriend(event: string, args: any) {
this.eventQueue.push({ type: CardType.Partner });
this.checkQueue();
}
private checkQueue() {
if (this.node.active) return;
if (this.eventQueue.length === 0) return;
const event = this.eventQueue.shift();
if (event) {
if (event.type !== undefined) {
this.showCardType(event.type);
}
}
}
/**
* 显示指定类型的卡牌(特殊获取模式)
*/
private showCardType(type: CardType) {
this.curCardType = type;
// 获取当前英雄等级作为参考或者默认1级
const level = smc.vmdata.hero.lv || 1;
this.fetchCards(level, type);
this.openUI();
}
private openUI() {
this.node.active = true;
this.hasSelected = false;
// 根据锁定状态显示 Lock 节点 (仅在特殊模式下可能需要锁定?或者统一逻辑)
// 原逻辑Lock.active = this.isLocked
if (this.Lock) {
this.Lock.active = this.isLocked;
}
// 显示 noStop 节点
if (this.noStop) {
this.noStop.active = true;
this.checkNoStop()
}
// 如果没有开启 noStop则暂停怪物行动
if (!smc.data.noStop) {
smc.mission.stop_mon_action = true;
}
this.resetCardStates();
this.playShowAnimation();
}
checkNoStop(){
this.noStop.getChildByName("no").active=!smc.data.noStop
// 更新暂停状态
if (this.node.active) {
smc.mission.stop_mon_action = !smc.data.noStop;
}
}
switchNoStop(){
smc.data.noStop=!smc.data.noStop
this.checkNoStop()
}
private playShowAnimation() {
const cards = [this.card1, this.card2, this.card3, this.card4];
cards.forEach((card, index) => {
if (card) {
card.setScale(Vec3.ZERO);
tween(card)
.delay(index * 0.1)
.to(0.4, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
.start();
}
});
}
/**
* 专门的获取卡牌方法
* @param level 等级
* @param forcedType 强制类型 (可选)
*/
fetchCards(level: number, forcedType?: CardType){
// 使用 CardSet 的 getCardOptions 获取卡牌
// 这里我们要获取 4 张卡牌
const options = getCardOptions(level, 4, [], forcedType);
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 获取到的卡牌选项: ${JSON.stringify(options)}`);
// 更新卡片数据
if (options.length > 0) this.updateCardData(1, options[0]);
if (options.length > 1) this.updateCardData(2, options[1]);
if (options.length > 2) this.updateCardData(3, options[2]);
if (options.length > 3) this.updateCardData(4, options[3]);
// 如果获取不足4张隐藏多余的卡片节点 (UI可能需要处理空数据)
if (options.length < 4 && this.card4) this.card4.active = false;
if (options.length < 3 && this.card3) this.card3.active = false;
if (options.length < 2 && this.card2) this.card2.active = false;
if (options.length < 1 && this.card1) this.card1.active = false;
}
updateCardInfo(card:Node, data: ICardInfo){
if(!card) return
card.active = true;
// 隐藏选中状态
const selected = card.getChildByName("selected");
if(selected) selected.active = false;
let name = card.getChildByName("name")
if(name){
name.getComponent(Label)!.string = data.name
}
let info = card.getChildByName("info")?.getChildByName("Label")
if(info){
// ICardInfo 已经标准化了 desc直接使用
info.getComponent(Label)!.string = data.desc || "";
}
// 先隐藏所有类型标识
const typeNodes = ["Atk", "Atked", "Buff", "Attr", "Skill", "Hp", "Dead", "Partner"];
// 1. 处理 card 直接子节点
typeNodes.forEach(nodeName => {
const node = card.getChildByName(nodeName);
if (node) node.active = false;
});
// 2. 处理 card/type 下的子节点
const typeContainer = card.getChildByName("type");
if (typeContainer) {
typeNodes.forEach(nodeName => {
const node = typeContainer.getChildByName(nodeName);
if (node) node.active = false;
});
}
// 根据 kind 激活对应节点
let activeNodeName = "";
switch (data.kind) {
case CardKind.Atk:
activeNodeName = "Atk";
break;
case CardKind.Atted:
activeNodeName = "Atked";
break;
case CardKind.Buff:
activeNodeName = "Buff";
break;
case CardKind.Attr:
activeNodeName = "Attr";
break;
case CardKind.Skill:
activeNodeName = "Skill";
break;
case CardKind.Hp:
activeNodeName = "Hp";
break;
case CardKind.Dead:
activeNodeName = "Dead";
break;
case CardKind.Partner:
activeNodeName = "Partner";
break;
}
if (activeNodeName) {
// 激活 card 下的节点
const activeNode = card.getChildByName(activeNodeName);
if (activeNode) activeNode.active = true;
// 激活 card/type 下的节点
if (typeContainer) {
const activeTypeNode = typeContainer.getChildByName(activeNodeName);
if (activeTypeNode) activeTypeNode.active = true;
}
}
// 更新图标 (如果存在 icon 节点)
// 注意:根据 Prefab 分析icon 可能在 card/Mask/icon 路径下,也可能在各个分类节点下
// 这里尝试统一查找 icon 节点
let iconNode: Node | null = null;
// 1. 尝试查找通用的 mask/icon (根据之前 card.prefab 分析,有个 Mask/icon 节点)
const maskNode = card.getChildByName("Mask");
if (maskNode) {
iconNode = maskNode.getChildByName("icon");
}
if (iconNode && data.icon) {
this.updateIcon(iconNode, data.icon);
}
}
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite);
if (!sprite) return;
if (this.uiconsAtlas) {
const frame = this.uiconsAtlas.getSpriteFrame(iconId);
if (frame) {
sprite.spriteFrame = frame;
}
} else {
// 加载图集
resources.load("gui/uicons", SpriteAtlas, (err, atlas) => {
if (err) {
mLogger.error(this.debugMode, 'MissionCard', "[MissionCardComp] Failed to load uicons atlas", err);
return;
}
this.uiconsAtlas = atlas;
const frame = atlas.getSpriteFrame(iconId);
if (frame) {
sprite.spriteFrame = frame;
}
});
}
}
updateCardData(index: number, data: ICardInfo) {
// 使用动态属性访问
(this as any)[`card${index}_data`] = data;
this.updateCardInfo((this as any)[`card${index}`], data);
}
selectCard(e: any, index: string) {
mLogger.log(this.debugMode, 'MissionCard', "selectCard", index)
let _index = parseInt(index);
// 如果已经选择过,则不再处理
if (this.hasSelected) return;
// 动态获取数据和节点
let selectedData: ICardInfo = (this as any)[`card${_index}_data`];
let selectedCardNode: Node | null = (this as any)[`card${_index}`];
if (selectedData && selectedCardNode) {
this.hasSelected = true;
mLogger.log(this.debugMode, 'MissionCard', "选择卡片:", selectedData.name, "类型:", selectedData.type);
// 未选中的卡片缩小
const cards = [this.card1, this.card2, this.card3, this.card4];
cards.forEach(card => {
if (card && card !== selectedCardNode) {
tween(card).to(0.2, { scale: Vec3.ZERO }).start();
}
});
// 显示当前选中的 selected 节点
const selected = selectedCardNode.getChildByName("selected");
if(selected) {
selected.active = true;
selected.setScale(Vec3.ZERO);
tween(selected).to(0.2, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' }).start();
}
// 选中卡片动效后触发逻辑
tween(selectedCardNode)
.to(0.1, { scale: new Vec3(1.1, 1.1, 1.1) })
.to(0.1, { scale: new Vec3(1, 1, 1) })
.delay(0.5)
.call(() => {
// 使用 HeroMasterComp 查找主角实体
// @ts-ignore
const entities = ecs.query(ecs.allOf(HeroMasterComp));
let role = entities.length > 0 ? entities[0] : null;
if (!role) {
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 未找到挂载 HeroMasterComp 的主角实体`);
} else {
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 成功定位主角实体: ${role.eid}`);
}
if (role) {
switch (selectedData.type) {
case CardType.Talent:
smc.addTalentRecord(selectedData.uuid);
// 直接调用 TalComp 添加天赋
const talComp = role.get(TalComp);
if (talComp) {
const beforeCount = Object.keys(talComp.Tals).length;
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Talent Before: Count=${beforeCount}, Tals=${JSON.stringify(talComp.Tals)}`);
talComp.addTal(selectedData.uuid);
const afterCount = Object.keys(talComp.Tals).length;
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Talent After: Count=${afterCount}, Added=${selectedData.uuid}, Tals=${JSON.stringify(talComp.Tals)}`);
}
break;
case CardType.Skill:
smc.addSkillRecord(selectedData.uuid);
// 直接调用 HeroSkillsComp 添加技能
const skillComp = role.get(HeroSkillsComp);
if (skillComp) {
const beforeCount = Object.keys(skillComp.skills).length;
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Skill Before: Count=${beforeCount}, Skills=${JSON.stringify(skillComp.skills)}`);
skillComp.addSkill(selectedData.uuid);
const afterCount = Object.keys(skillComp.skills).length;
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Skill After: Count=${afterCount}, Added=${selectedData.uuid}, Skills=${JSON.stringify(skillComp.skills)}`);
}
break;
case CardType.Partner:
// 伙伴是召唤新实体,依然适合用事件,或者直接调用 summon 方法
oops.message.dispatchEvent(GameEvent.CallFriend, { uuid: selectedData.uuid });
break;
case CardType.Potion:
// 药水直接作用于 HeroAttrsComp
const attrsComp = role.get(HeroAttrsComp);
if (attrsComp) {
const potion = PotionCards[selectedData.uuid];
if (potion) {
const beforeVal = attrsComp.Attrs[potion.attr] || 0;
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Potion Before: Attr[${potion.attr}]=${beforeVal}, Attrs=${JSON.stringify(attrsComp.Attrs)}`);
const buffConf: BuffConf = {
buff: potion.attr,
value: potion.value,
BType: BType.RATIO,
time: potion.duration,
chance: 1,
};
attrsComp.addBuff(buffConf);
smc.updateHeroInfo(attrsComp);
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Potion Applied: ${potion.desc}, Value=${potion.value}, Attrs=${JSON.stringify(attrsComp.Attrs)}`);
oops.gui.toast(potion.desc);
}
}
break;
case CardType.Attr:
// 属性卡:使用 addBuff 添加永久属性加成
const attrCard = AttrCards[selectedData.uuid];
if (attrCard) {
const attrsComp = role.get(HeroAttrsComp);
if (attrsComp) {
// 记录变更前状态
const roleBefore = attrsComp.Attrs[attrCard.attr] || 0;
// 构造永久 Buff (time: 0)
const buffConf: BuffConf = {
buff: attrCard.attr,
value: attrCard.value,
// 注意:这里需要根据属性类型决定是 VALUE 还是 RATIO
// 通常 AttrCards 里的 value 如果是百分比属性(如暴击率),配置值可能是 2 (代表2%)
// HeroAttrsComp.addBuff 会根据 BType 累加
// 为了安全,我们可以查一下 AttrsType或者默认属性卡都是 VALUE (直接加数值) 或 RATIO (百分比)
// 根据 AttrSet.tsAttrCards 的 value 是直接数值。
// 而 HeroAttrsComp.recalculateSingleAttr 中RATIO 类型的属性也是直接加 totalRatio
// 所以这里关键是 buffConf.BType。
// 如果我们希望属性卡是"基础值加成"(VALUE) 还是 "百分比加成"(RATIO)
// 查看 AttrSet.ts: 2003 DEF value: 2 desc: "防御力 +2%" -> 看起来是百分比
// 2001 AP value: 10 desc: "攻击力 +10" -> 看起来是数值
// 所以需要根据 attrCard.attr 的类型来决定 BType或者在 AttrCards 配置中增加 BType 字段
// 目前 AttrInfo 接口没有 BType。
// 我们可以引入 AttrsType 来判断默认类型,或者全部作为 VALUE (如果是百分比属性VALUE也是加到数值上的)
// 修正HeroAttrsComp.recalculateSingleAttr 中:
// isRatioAttr = AttrsType[attrIndex] === BType.RATIO
// 如果是 RATIO 属性totalValue + totalRatio
// 如果是 VALUE 属性totalValue * (1 + totalRatio/100)
// 策略:
// 如果是数值型属性如HP, AP属性卡通常是加固定值 -> BType.VALUE
// 如果是百分比型属性(如暴击率),属性卡加的是点数 -> BType.VALUE (因为最终计算是 totalValue + totalRatio对于百分比属性 totalValue 就是基础点数)
// 等等recalculateSingleAttr 对 RATIO 属性是 totalValue + totalRatio
// 如果暴击率基础是 0加 2%暴击率。
// 如果用 BType.VALUE, totalValue += 2。 结果 = 2 + 0 = 2。正确。
// 如果用 BType.RATIO, totalRatio += 2。 结果 = 0 + 2 = 2。也正确。
// 但是对于数值型属性如AP
// 攻击力 +10。
// 用 BType.VALUE, totalValue += 10。 结果 = (Base+10) * (1+Ratio)。正确。
// 用 BType.RATIO, totalRatio += 10。 结果 = Base * (1+10/100) = Base * 1.1。这是加10%不是加10点。
// 结论AttrCards 中的 value 看起来都是"点数"或"绝对值",所以应该统一使用 BType.VALUE。
// 即使是 "防御力 +2%" (2003),如果 DEF 是 RATIO 类型AttrSet.ts里定义为RATIO? 不DEF通常是混合但这里AttrsType定义DEF是RATIO? 让我们查一下)
// 查 AttrsType: [Attrs.DEF]: BType.RATIO
// 如果 DEF 是 RATIO那 +2% 就是 +2 数值。
// 所以统一用 BType.VALUE 是安全的,代表"增加属性面板数值"。
BType: BType.VALUE,
time: 0,
chance: 1,
};
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Adding Buff: Attr=${attrCard.attr}, Val=${attrCard.value}, Type=VALUE`);
attrsComp.addBuff(buffConf);
// addBuff 内部会自动调用 recalculateSingleAttr 和 updateHeroInfo
const roleAfter = attrsComp.Attrs[attrCard.attr] || 0;
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Attr After: Hero=${roleAfter} (Change: ${roleAfter - roleBefore})`);
oops.gui.toast(attrCard.desc);
}
} else {
console.warn(`[MissionCard] 未找到属性卡配置: UUID=${selectedData.uuid}`);
}
break;
}
} else {
mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 主角实体无效,无法应用卡牌效果`);
}
// 记录已获取的卡牌
oops.message.dispatchEvent(GameEvent.UpdateMissionGet, {
uuid: selectedData.uuid,
icon: selectedData.icon,
kind: selectedData.kind
});
this.close();
})
.start();
}
}
/** 看广告关闭 Lock */
watchAdCloseLock() {
// TODO: 此处接入 IAA 广告 SDK
mLogger.log(this.debugMode, 'MissionCard', "播放激励视频广告...");
// 模拟广告播放成功回调
this.isLocked = false;
this.isAdUnlocked = true;
if (this.Lock) {
this.Lock.active = false;
oops.gui.toast("解锁成功");
}
this.closeUnLock();
}
coinCloseLock(){
let cost = smc.vmdata.mission_data.unlockCoin;
if (smc.vmdata.gold >= cost) {
// 扣除金币
if (smc.updateGold(-cost)) {
this.isLocked = false;
if (this.Lock) {
this.Lock.active = false;
}
oops.gui.toast("解锁成功");
this.closeUnLock();
} else {
oops.gui.toast("交易失败");
}
} else {
oops.gui.toast("金币不足");
}
}
showUnLock(){
if (this.unLock) {
this.unLock.active = true;
this.unLock.setScale(Vec3.ZERO);
tween(this.unLock)
.to(0.2, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
.start();
}
}
closeUnLock(){
if (this.unLock && this.unLock.active) {
tween(this.unLock)
.to(0.2, { scale: Vec3.ZERO }, { easing: 'backIn' })
.call(() => {
this.unLock.active = false;
})
.start();
}
}
/** 放弃选择 */
onGiveUp() {
if (this.hasSelected) return;
this.hasSelected = true;
// 隐藏关闭按钮
if (this.btnClose) {
this.btnClose.active = false;
}
const cards = [this.card1, this.card2, this.card3, this.card4];
let delayTime = 0.2;
cards.forEach(card => {
if (card && card.active) {
tween(card).to(delayTime, { scale: Vec3.ZERO }).start();
}
});
// 动画结束后关闭
this.scheduleOnce(() => {
this.close();
}, delayTime);
}
/**
* 关闭界面
*/
close() {
this.node.active = false;
// 恢复游戏运行状态(取消暂停)
smc.mission.stop_mon_action = false;
// 关闭时隐藏按钮,避免下次打开其他类型时闪烁
if (this.btnClose) {
this.btnClose.active = false;
}
// 关闭时隐藏 Lock 节点
if (this.Lock) {
this.Lock.active = false;
}
// 关闭时隐藏 noStop 节点
if (this.noStop) {
this.noStop.active = false;
}
// 恢复锁定状态(如果没有永久解锁)
if (!this.isAdUnlocked) {
this.isLocked = true;
}
this.checkQueue();
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.node.destroy();
}
}