feat(card): 新增卡牌系统核心组件与配置

- 新增 CardComp 组件用于卡牌视图展示
- 新增 CardSet 配置文件,包含卡牌类型、种类枚举和完整卡池配置
- 重构 HSkillComp 组件,优化技能调试面板布局和交互逻辑
- 更新 MissionCardComp 组件,移除旧卡牌类型依赖
- 调整 GameSet 配置文件,移除 CardType 和 CardKind 枚举
- 更新卡牌预制体结构,优化 UI 布局和组件绑定
- 新增特殊卡牌效果系统,支持抽英雄和重复使用等特殊能力
- 实现卡牌按权重抽取算法和卡池等级管理机制
This commit is contained in:
walkpan
2026-03-13 23:15:21 +08:00
parent 45ba5b72f5
commit c8c3dde2e4
10 changed files with 1179 additions and 1367 deletions

View File

@@ -5,8 +5,8 @@ import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/modu
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 { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { CardType } from "../common/config/CardSet";
@@ -32,61 +32,20 @@ export class MissionCardComp extends CCComp {
@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();
@@ -94,569 +53,27 @@ export class MissionCardComp extends CCComp {
/** 游戏开始初始化 */
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){
// 获取主角已有的属性倾向 (已拥有的永久属性Buff)
// 使用 CardSet 的 getCardOptions 获取卡牌
// 这里我们要获取 4 张卡牌
// const options = getCardOptions(level, 4, [], forcedType, preferredAttrs);
// 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: any){
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: any) {
// 使用动态属性访问
(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: any;
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(() => {
// // @ts-ignore
// let role = entities.length > 0 ? entities[0] : null;
// if (!role) {
// } 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 类型
// // 如果属性本身是 RATIO 型如暴击率AttrCards 中的值如2应该作为 VALUE 添加(因为 recalculateSingleAttr 会把 VALUE 和 RATIO 相加)
// // 但如果属性本身是 VALUE 型如攻击力AttrCards 中的值是直接加数值,也应该作为 VALUE 添加
// // 结论无论属性类型如何AttrCards 中的配置都是"增加的点数",所以统一使用 BType.VALUE
// // 修正:虽然 AttrsType 定义了属性本身的类型,但在 addBuff 中BType.VALUE 表示"加法叠加"BType.RATIO 表示"乘法叠加"
// // 对于数值型属性如攻击力BType.VALUE 是 +10BType.RATIO 是 +10%
// // 对于百分比型属性如暴击率BType.VALUE 是 +2(%)BType.RATIO 是 +2%(即 *1.02,通常不这么用)
// // 所以AttrCards 配置的值应当被视为"绝对值增量",对应 BType.VALUE
// // 构造永久 Buff (time: 0)
// const buffConf: BuffConf = {
// buff: attrCard.attr,
// value: attrCard.value,
// BType: BType.RATIO, // 始终使用 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 {
// mLogger.warn(this.debugMode, 'MissionCard', `[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) 删除组件是触发组件处理自定义释放逻辑 */