feat(卡牌系统): 实现任务卡牌抽卡与锁定功能

- 新增 MissionCardComp 作为卡牌面板控制器,管理四个固定卡槽
- 实现抽卡按钮逻辑,根据卡池等级抽取并分发卡牌到四个槽位
- 实现卡池升级按钮,提升抽卡品质但不影响已锁定卡牌
- 新增 CardComp 作为单卡控制器,支持卡牌使用与槽位锁定功能
- 锁定状态下卡槽将跳过抽卡更新,保持原有卡牌
- 添加任务开始/结束时的卡槽清理与界面显隐控制
- 修复预制体字段缺失问题,补充 instance 和 targetOverrides 字段
This commit is contained in:
walkpan
2026-03-14 09:18:45 +08:00
parent dbe376033d
commit d0e824e93b
3 changed files with 265 additions and 43 deletions

View File

@@ -7076,6 +7076,8 @@
"__id__": 0
},
"fileId": "5aMCdIWc5OmJF+7Y1vMDAV",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -7270,6 +7272,8 @@
"__id__": 0
},
"fileId": "57ozFIFb9ETJnSg6jZ4keY",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -7407,6 +7411,8 @@
"__id__": 0
},
"fileId": "80R6KCqF1MUotJRCKCOAB1",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -7564,6 +7570,8 @@
"__id__": 0
},
"fileId": "4b2ngPxLNPTLyKGNy8mqMw",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -7645,6 +7653,8 @@
"__id__": 0
},
"fileId": "658QGyYfxEyJvkxOrsGTX4",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -7802,6 +7812,8 @@
"__id__": 0
},
"fileId": "8eSy9TOKJMi4sjqRA6RoQk",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -7841,6 +7853,8 @@
"__id__": 0
},
"fileId": "63FHyGP9BOaqAuH4RtPSZ0",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -7954,6 +7968,8 @@
"__id__": 0
},
"fileId": "46VgrHm5VNH5UNmq7bgAi+",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -8817,7 +8833,9 @@
"cards_chou": {
"__id__": 288
},
"cards_up": null,
"cards_up": {
"__id__": 237
},
"_id": ""
},
{

View File

@@ -1,28 +1,19 @@
import { mLogger } from "../common/Logger";
import { _decorator, Label, Node, tween, Vec3, Color, Sprite, Tween, SpriteAtlas, resources } from "cc";
import { _decorator, Label, Node, NodeEventType, Sprite, 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 { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { CardType } from "../common/config/CardSet";
import { CardConfig, CardType } from "../common/config/CardSet";
const { ccclass, property } = _decorator;
interface ICardEvent {
type?: CardType;
level?: number;
}
/** 视图层对象 */
@ccclass('CardComp')
@ecs.register('CardComp', false)
export class CardComp extends CCComp {
private debugMode: boolean = true;
/** 视图层逻辑代码分离演示 */
/** 锁定态图标节点(显示时表示本槽位锁定) */
@property(Node)
Lock: Node = null!
@property(Node)
@@ -41,18 +32,23 @@ export class CardComp extends CCComp {
card_cost:number=0
card_type:CardType=CardType.Hero
card_uuid:number=0
// 是否处于锁定状态
private isLocked: boolean = true;
// 图标图集缓存
/** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */
private isLocked: boolean = false;
/** 图标图集缓存(后续接图标资源时直接复用) */
private uiconsAtlas: SpriteAtlas | null = null;
/** 当前槽位承载的卡牌数据null 表示空槽 */
private cardData: CardConfig | null = null;
onLoad() {
/** 初始阶段只做UI状态准备不触发业务逻辑 */
this.bindEvents();
this.updateLockUI();
this.applyEmptyUI();
}
onDestroy() {
this.unbindEvents();
}
init(){
this.onMissionStart();
@@ -68,24 +64,27 @@ export class CardComp extends CCComp {
}
start() {
// 初始隐藏或显示逻辑
this.node.active = false;
/** 单卡节点常驻,由数据控制显示内容 */
this.node.active = true;
}
updateCardInfo(card:Node, data: any){
/** 兼容旧接口:外部通过该入口更新卡牌 */
updateCardInfo(card:Node, data: CardConfig){
this.applyDrawCard(data);
}
private updateIcon(node: Node, iconId: string) {
}
updateCardData(index: number, data: any) {
/** 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) */
updateCardData(index: number, data: CardConfig) {
this.applyDrawCard(data);
}
/** 兼容按钮回调入口:触发单卡使用 */
selectCard(e: any, index: string) {
this.useCard();
}
@@ -96,6 +95,132 @@ export class CardComp extends CCComp {
}
/** 抽卡分发入口:返回 true 表示本次已成功接收新卡 */
applyDrawCard(data: CardConfig | null): boolean {
if (!data) return false;
/** 锁定且已有旧卡时,跳过本次刷新,保持老卡 */
if (this.isLocked && this.cardData) {
mLogger.log(this.debugMode, "CardComp", "slot locked, skip update", this.card_uuid);
return false;
}
this.cardData = data;
this.card_uuid = data.uuid;
this.card_type = data.type;
this.card_cost = data.cost;
this.node.active = true;
this.applyCardUI();
return true;
}
/** 使用当前卡牌仅做UI层清空不触发效果事件下一步再接 */
useCard(): CardConfig | null {
if (!this.cardData) return null;
const used = this.cardData;
this.clearAfterUse();
return used;
}
/** 查询槽位是否有卡 */
hasCard(): boolean {
return !!this.cardData;
}
/** 外部设置锁定态 */
setLocked(value: boolean) {
this.isLocked = value;
this.updateLockUI();
}
/** 外部读取当前锁定态 */
isSlotLocked(): boolean {
return this.isLocked;
}
/** 系统清槽:用于任务开始/结束等强制重置场景 */
clearBySystem() {
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.card_type = CardType.Hero;
this.isLocked = false;
this.updateLockUI();
this.applyEmptyUI();
}
/** 卡牌被玩家使用后的清槽行为 */
private clearAfterUse() {
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.card_type = CardType.Hero;
this.isLocked = false;
this.updateLockUI();
this.applyEmptyUI();
}
/** 绑定触控:卡面点击使用,锁按钮点击切换锁定 */
private bindEvents() {
this.node.on(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.Lock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
this.unLock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
/** 解绑触控,防止节点销毁后残留回调 */
private unbindEvents() {
this.node.off(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.Lock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
this.unLock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
/** 点击卡面执行单卡使用仅UI变化 */
private onCardTouchEnd() {
this.useCard();
}
/** 点击锁控件:切换锁态;空槽不允许锁定 */
private onToggleLock(event?: Event) {
if (!this.cardData) return;
this.isLocked = !this.isLocked;
this.updateLockUI();
event?.stopPropagation();
}
/** 根据锁态刷新 Lock / unLock 显示 */
private updateLockUI() {
if (this.Lock) this.Lock.active = this.isLocked;
if (this.unLock) this.unLock.active = !this.isLocked;
}
/** 根据当前 cardData 渲染卡面文字与图标 */
private applyCardUI() {
if (!this.cardData) {
this.applyEmptyUI();
return;
}
this.setLabel(this.name_node, `${CardType[this.card_type]}-${this.card_uuid}`);
this.setLabel(this.cost_node, `${this.card_cost}`);
if (this.ap_node) this.ap_node.active = false;
if (this.hp_node) this.hp_node.active = false;
this.updateIcon(this.icon_node, `${this.card_uuid}`);
}
/** 渲染空槽状态 */
private applyEmptyUI() {
this.setLabel(this.name_node, "");
this.setLabel(this.cost_node, "");
if (this.ap_node) this.ap_node.active = false;
if (this.hp_node) this.hp_node.active = false;
const sprite = this.icon_node?.getComponent(Sprite);
if (sprite) sprite.spriteFrame = null;
}
/** 安全设置文本,兼容节点上或子节点上的 Label */
private setLabel(node: Node | null, value: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
if (label) label.string = value;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.node.destroy();

View File

@@ -1,14 +1,10 @@
import { mLogger } from "../common/Logger";
import { _decorator, Label, Node, tween, Vec3, Color, Sprite, Tween, SpriteAtlas, resources } from "cc";
import { _decorator, Label, Node, NodeEventType, SpriteAtlas } 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 { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { CardType } from "../common/config/CardSet";
import { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CardConfig, getCardsByLv } from "../common/config/CardSet";
import { CardComp } from "./CardComp";
const { ccclass, property } = _decorator;
@@ -18,7 +14,7 @@ const { ccclass, property } = _decorator;
@ecs.register('MissionCard', false)
export class MissionCardComp extends CCComp {
private debugMode: boolean = true;
/** 视图层逻辑代码分离演示 */
/** 四个插卡槽位固定顺序分发1~4 */
@property(Node)
card1:Node = null!
@property(Node)
@@ -32,27 +28,39 @@ export class MissionCardComp extends CCComp {
@property(Node)
cards_up:Node = null!
/** 预留图集缓存(后续接入按钮/卡面图标时复用) */
private uiconsAtlas: SpriteAtlas | null = null;
/** 四个槽位对应的单卡控制器缓存 */
private cardComps: CardComp[] = [];
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
private poolLv: number = CARD_POOL_INIT_LEVEL;
onLoad() {
/** 绑定事件 -> 缓存子控制器 -> 初始化UI状态 */
this.bindEvents();
this.cacheCardComps();
this.onMissionStart();
}
onDestroy() {
this.unbindEvents();
}
init(){
this.onMissionStart();
}
/** 游戏开始初始化 */
/** 任务开始时重置卡池等级、清空4槽、显示面板 */
onMissionStart() {
this.poolLv = CARD_POOL_INIT_LEVEL;
this.clearAllCards();
this.updatePoolLvUI();
this.node.active = true;
}
/** 游戏结束清理 */
/** 任务结束时清空4槽并隐藏面板 */
onMissionEnd() {
this.clearAllCards();
this.node.active = false;
}
start() {
@@ -61,11 +69,82 @@ export class MissionCardComp extends CCComp {
/**
* 关闭界面
*/
/** 关闭面板(不销毁数据模型,仅隐藏) */
close() {
this.node.active = false;
}
/** 只处理UI层事件不做卡牌效果分发 */
private bindEvents() {
/** 生命周期事件 */
this.on(GameEvent.MissionStart, this.onMissionStart, this);
this.on(GameEvent.MissionEnd, this.onMissionEnd, this);
/** 按钮事件:抽卡与卡池升级 */
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onClickDraw, this);
this.cards_up?.on(NodeEventType.TOUCH_END, this.onClickUpgrade, this);
}
/** 解除按钮监听,避免节点销毁后回调泄漏 */
private unbindEvents() {
this.cards_chou?.off(NodeEventType.TOUCH_END, this.onClickDraw, this);
this.cards_up?.off(NodeEventType.TOUCH_END, this.onClickUpgrade, this);
}
/** 将四个卡槽节点映射为 CardComp形成固定顺序控制数组 */
private cacheCardComps() {
const nodes = [this.card1, this.card2, this.card3, this.card4];
this.cardComps = nodes
.map(node => node?.getComponent(CardComp))
.filter((comp): comp is CardComp => !!comp);
}
/** 抽卡按钮每次固定抽4张然后顺序分发给4个单卡脚本 */
private onClickDraw() {
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
}
/** 升级按钮:仅提升卡池等级,卡槽是否更新由下一次抽卡触发 */
private onClickUpgrade() {
if (this.poolLv >= CARD_POOL_MAX_LEVEL) return;
this.poolLv += 1;
this.updatePoolLvUI();
mLogger.log(this.debugMode, "MissionCardComp", "pool level up", this.poolLv);
}
/** 构建本次抽卡结果保证最终可分发4条数据 */
private buildDrawCards(): CardConfig[] {
const cards = getCardsByLv(this.poolLv);
/** 正常情况下直接取前4 */
if (cards.length >= 4) return cards.slice(0, 4);
/** 兜底当返回不足4张时循环补齐保证分发不缺位 */
const filled = [...cards];
while (filled.length < 4) {
const fallback = getCardsByLv(this.poolLv);
if (fallback.length === 0) break;
filled.push(fallback[filled.length % fallback.length]);
}
return filled;
}
/** 全量分发给4槽每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
private dispatchCardsToSlots(cards: CardConfig[]) {
for (let i = 0; i < this.cardComps.length; i++) {
this.cardComps[i].applyDrawCard(cards[i] ?? null);
}
}
/** 系统清空4槽用于任务切换 */
private clearAllCards() {
this.cardComps.forEach(comp => comp.clearBySystem());
}
/** 更新升级按钮上的等级文案,反馈当前卡池层级 */
private updatePoolLvUI() {
if (!this.cards_up) return;
const label = this.cards_up.getComponentInChildren(Label);
if (!label) return;
label.string = `卡池Lv.${this.poolLv}`;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */