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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
/** 卡牌大类定义 */
export enum CardType {
Hero = 1,
Skill = 2,
Potion = 3,
Special = 4,
Buff = 5,
Debuff = 6,
}
/** 卡池等级定义 */
export enum CardKind {
LV1 = 1,
LV2 = 2,
LV3 = 3,
LV4 = 4,
LV5 = 5,
LV6 = 6,
}
/** 通用卡牌配置 */
export interface CardConfig {
uuid: number
type: CardType
cost: number
weight: number
lv: CardKind
}
/** 特殊卡效果类型 */
export enum SpecialEffectType {
DrawHero = 1,
RepeatNextUse = 2,
}
/** 特殊卡效果参数 */
export interface SpecialCardEffect {
type: SpecialEffectType
drawHeroCount?: number
drawHeroLv?: CardKind
repeatNextUseTimes?: number
}
/** 特殊卡完整配置 */
export interface SpecialCardConfig extends CardConfig {
effect: SpecialCardEffect
}
/** 卡池默认初始等级 */
export const CARD_POOL_INIT_LEVEL = CardKind.LV1
/** 卡池等级上限 */
export const CARD_POOL_MAX_LEVEL = CardKind.LV6
/** 基础卡池英雄、技能、Buff、Debuff */
export const CardPoolList: CardConfig[] = [
{ uuid: 5001, type: CardType.Hero, cost: 3, weight: 20, lv: 1 },
{ uuid: 5003, type: CardType.Hero, cost: 3, weight: 20, lv: 1 },
{ uuid: 5002, type: CardType.Hero, cost: 3, weight: 25, lv: 2 },
{ uuid: 5005, type: CardType.Hero, cost: 3, weight: 25, lv: 2 },
{ uuid: 5004, type: CardType.Hero, cost: 3, weight: 30, lv: 3 },
{ uuid: 5006, type: CardType.Hero, cost: 3, weight: 35, lv: 4 },
{ uuid: 5007, type: CardType.Hero, cost: 3, weight: 40, lv: 5 },
{ uuid: 6001, type: CardType.Skill, cost: 1, weight: 20, lv: 1 },
{ uuid: 6002, type: CardType.Skill, cost: 1, weight: 20, lv: 1 },
{ uuid: 6003, type: CardType.Skill, cost: 2, weight: 25, lv: 2 },
{ uuid: 6100, type: CardType.Skill, cost: 4, weight: 25, lv: 2 },
{ uuid: 6004, type: CardType.Skill, cost: 3, weight: 30, lv: 3 },
{ uuid: 6102, type: CardType.Skill, cost: 4, weight: 35, lv: 4 },
{ uuid: 6101, type: CardType.Skill, cost: 5, weight: 40, lv: 5 },
{ uuid: 6103, type: CardType.Skill, cost: 6, weight: 45, lv: 6 },
{ uuid: 10001, type: CardType.Buff, cost: 2, weight: 30, lv: 1 },
{ uuid: 10101, type: CardType.Buff, cost: 3, weight: 26, lv: 2 },
{ uuid: 10011, type: CardType.Buff, cost: 3, weight: 24, lv: 3 },
{ uuid: 10311, type: CardType.Buff, cost: 4, weight: 20, lv: 4 },
{ uuid: 10302, type: CardType.Buff, cost: 5, weight: 18, lv: 5 },
{ uuid: 10201, type: CardType.Debuff, cost: 3, weight: 24, lv: 2 },
{ uuid: 10211, type: CardType.Debuff, cost: 4, weight: 20, lv: 3 },
{ uuid: 10312, type: CardType.Debuff, cost: 4, weight: 18, lv: 4 },
{ uuid: 20001, type: CardType.Debuff, cost: 5, weight: 14, lv: 5 },
{ uuid: 20011, type: CardType.Debuff, cost: 6, weight: 12, lv: 6 },
]
/** 特殊卡定义表 */
export const SpecialCardList: Record<number, SpecialCardConfig> = {
7001: {
uuid: 7001,
type: CardType.Special,
cost: 6,
weight: 20,
lv: CardKind.LV3,
effect: {
type: SpecialEffectType.DrawHero,
drawHeroCount: 4,
drawHeroLv: CardKind.LV3,
},
},
7002: {
uuid: 7002,
type: CardType.Special,
cost: 5,
weight: 20,
lv: CardKind.LV4,
effect: {
type: SpecialEffectType.RepeatNextUse,
repeatNextUseTimes: 1,
},
},
}
/** 规范等级到合法区间 [LV1, LV6] */
const clampCardLv = (lv: number): CardKind => {
const value = Math.floor(lv)
if (value < CARD_POOL_INIT_LEVEL) return CARD_POOL_INIT_LEVEL
if (value > CARD_POOL_MAX_LEVEL) return CARD_POOL_MAX_LEVEL
return value as CardKind
}
/** 单次按权重抽取一张卡 */
const weightedPick = (cards: CardConfig[]): CardConfig | null => {
if (cards.length === 0) return null
const totalWeight = cards.reduce((total, card) => total + card.weight, 0)
let random = Math.random() * totalWeight
for (const card of cards) {
random -= card.weight
if (random <= 0) return card
}
return cards[cards.length - 1]
}
/** 连续抽取 count 张卡,允许重复 */
const pickCards = (cards: CardConfig[], count: number): CardConfig[] => {
if (cards.length === 0 || count <= 0) return []
const selected: CardConfig[] = []
while (selected.length < count) {
const pick = weightedPick(cards)
if (!pick) break
selected.push(pick)
}
return selected
}
/** 获取指定等级可出现的基础卡池(英雄+技能) */
export const getCardPoolByLv = (lv: number): CardConfig[] => {
const cardLv = clampCardLv(lv)
return CardPoolList.filter(card => card.lv <= cardLv)
}
/** 常规发牌:前 2 英雄 + 后 2 其他 */
export const getCardsByLv = (lv: number): CardConfig[] => {
const pool = getCardPoolByLv(lv)
const heroPool = pool.filter(card => card.type === CardType.Hero)
const otherPool = pool.filter(card => card.type !== CardType.Hero)
const heroes = pickCards(heroPool, 2)
const others = pickCards(otherPool, 2)
return [...heroes, ...others]
}

View File

@@ -22,24 +22,6 @@ export enum BoxSet {
//攻击距离
}
export enum CardType {
Talent = 1,
Skill = 2,
Potion = 3,
Partner = 4,
Attr = 5,
}
export enum CardKind {
Atk = 1,
Atted = 2,
Buff = 3,
Attr = 4,
Skill = 5,
Hp = 6,
Dead = 7,
Partner = 8,
}
export enum FacSet {

View File

@@ -232,7 +232,7 @@ export const SkillSet: Record<number, SkillConfig> = {
ready:0,EAnm:0,DAnm:9001,RType:RType.fixed,EType:EType.animationEnd,
buffs:[],debuffs:[],info:"对前方目标造成150%攻击的伤害",
},
// ========== 基础buff ========== 6100-6199
//============================= ====== 基础buff ====== ========================== 6100-6199
6100: {
uuid:6100,name:"治疗",sp_name:"buff_wind",icon:"1292",TGroup:TGroup.Self,TType:TType.LowestHP,act:"atk",DTType:DTType.single,
ap:30,hit_num:1,hit:1,hitcd:0.2,speed:720,with:0,

View File

@@ -0,0 +1,103 @@
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 { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { 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)
unLock: Node = null!
@property(Node)
ap_node=null!
@property(Node)
hp_node=null!
@property(Node)
name_node=null!
@property(Node)
icon_node=null!
@property(Node)
cost_node=null!
card_cost:number=0
card_type:CardType=CardType.Hero
card_uuid:number=0
// 是否处于锁定状态
private isLocked: boolean = true;
// 图标图集缓存
private uiconsAtlas: SpriteAtlas | null = null;
onLoad() {
}
onDestroy() {
}
init(){
this.onMissionStart();
}
/** 游戏开始初始化 */
onMissionStart() {
}
/** 游戏结束清理 */
onMissionEnd() {
}
start() {
// 初始隐藏或显示逻辑
this.node.active = false;
}
updateCardInfo(card:Node, data: any){
}
private updateIcon(node: Node, iconId: string) {
}
updateCardData(index: number, data: any) {
}
selectCard(e: any, index: string) {
}
/**
* 关闭界面
*/
close() {
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.node.destroy();
}
}

View File

@@ -0,0 +1 @@
{"ver":"4.0.24","importer":"typescript","imported":true,"uuid":"c4842465-1171-41b1-85ab-66922e63d734","files":[],"subMetas":{},"userData":{}}

View File

@@ -1,11 +1,10 @@
import { _decorator, Animation, AnimationClip, Component, instantiate, Label, Node, Prefab, resources, Sprite, SpriteFrame, v3, tween, Vec3, ProgressBar, SpriteAtlas } from 'cc';
import { oops } from 'db://oops-framework/core/Oops';
import { getHeroList, getPreAttr, HeroConf, HeroInfo, HType, HTypeName } from '../common/config/heroSet';
import { smc } from '../common/SingletonModuleComp';
import { GameEvent } from '../common/config/GameEvent';
import { _decorator, instantiate, Label, Node, Prefab, UITransform, v3, Vec3 } from 'cc';
import { CCComp } from 'db://oops-framework/module/common/CCComp';
import { ecs } from 'db://oops-framework/libs/ecs/ECS';
import { SkillSet } from '../common/config/SkillSet';
import { BoxSet, FacSet } from '../common/config/GameSet';
import { smc } from '../common/SingletonModuleComp';
import { Skill } from '../skill/Skill';
import { mLogger } from '../common/Logger';
const { ccclass, property } = _decorator;
@@ -13,46 +12,185 @@ const { ccclass, property } = _decorator;
@ecs.register('HSkillComp', false)
export class HSkillComp extends CCComp {
debugMode: boolean = false;
private readonly panelName: string = 'skill_debug_panel';
private readonly panelWidth: number = 680;
private readonly panelHeight: number = 1180;
private readonly colCount: number = 4;
private readonly cellWidth: number = 160;
private readonly cellHeight: number = 68;
private readonly startX: number = -240;
private readonly startY: number = 560;
@property(Prefab)
btnPrefab: Prefab | null = null;
h_uuid:number=0
private uiconsAtlas: SpriteAtlas | null = null;
private panelNode: Node | null = null;
private buttonNodes: Node[] = [];
private mockCasterNode: Node | null = null;
private mockCasterView: any = null;
private currentSkill: Skill | null = null;
protected onLoad(): void {
this.ensurePanel();
}
start() {
this.renderSkillButtons();
}
start_test(){
this.node.active=true
this.node.parent.getChildByName("mission_home").active=false
start_test() {
this.node.active = true;
const home = this.node.parent?.getChildByName('mission_home');
if (home) {
home.active = false;
}
this.renderSkillButtons();
}
end_test(){
this.node.parent.getChildByName("mission_home").active=true
this.node.active=false
end_test() {
const home = this.node.parent?.getChildByName('mission_home');
if (home) {
home.active = true;
}
this.node.active = false;
}
update(deltaTime: number) {
}
update_data(uuid:number){
update_data(uuid: number) {
this.renderSkillButtons();
}
load_hui(uuid:number){
var path = "game/gui/hui";
var prefab: Prefab = oops.res.get(path, Prefab)!;
var node = instantiate(prefab);
// 将节点添加到父节点下
this.node.addChild(node);
// 设置节点位置
private ensurePanel() {
let panel = this.node.getChildByName(this.panelName);
if (!panel) {
panel = new Node(this.panelName);
panel.parent = this.node;
const transform = panel.addComponent(UITransform);
transform.setContentSize(this.panelWidth, this.panelHeight);
panel.setPosition(0, 640, 0);
}
this.panelNode = panel;
}
private renderSkillButtons() {
this.ensurePanel();
const prefab = this.getBtnPrefab();
if (!prefab || !this.panelNode || !this.panelNode.isValid) {
return;
}
this.clearButtons();
const skillIds = Object.keys(SkillSet).map(Number).sort((a, b) => a - b);
skillIds.forEach((skillId, index) => {
const btnNode = instantiate(prefab);
btnNode.parent = this.panelNode;
const row = Math.floor(index / this.colCount);
const col = index % this.colCount;
btnNode.setPosition(
this.startX + col * this.cellWidth,
this.startY - row * this.cellHeight,
0
);
const label = btnNode.getChildByName('Label')?.getComponent(Label);
if (label) {
const conf = SkillSet[skillId];
label.string = `${skillId} ${conf?.name ?? ''}`;
}
btnNode.on(Node.EventType.TOUCH_END, () => this.playDebugSkill(skillId), this);
this.buttonNodes.push(btnNode);
});
}
private clearButtons() {
this.buttonNodes.forEach(node => {
if (!node || !node.isValid) {
return;
}
node.off(Node.EventType.TOUCH_END);
node.destroy();
});
this.buttonNodes.length = 0;
}
private getBtnPrefab(): Prefab | null {
if (this.btnPrefab) {
return this.btnPrefab;
}
mLogger.error(this.debugMode, 'HSkillComp', '[HSkillComp] 未绑定 Btn 预制体,请在编辑器中设置 btnPrefab');
return null;
}
private getSkillParent(): Node {
const layer = smc.map?.MapView?.scene?.entityLayer?.node?.getChildByName('SKILL');
if (layer && layer.isValid) {
return layer;
}
return this.node;
}
private ensureMockCaster(parent: Node, startPos: Vec3): any {
if (!this.mockCasterNode || !this.mockCasterNode.isValid) {
this.mockCasterNode = new Node('debug_caster');
this.mockCasterNode.parent = parent;
this.mockCasterNode.setScale(v3(1, 1, 1));
this.mockCasterNode.active = false;
} else if (this.mockCasterNode.parent !== parent) {
this.mockCasterNode.parent = parent;
}
this.mockCasterNode.setPosition(startPos);
const mockAttrs = {
hero_name: '技能调试器',
ap: 100,
critical: 0,
critical_dmg: 50,
freeze_chance: 0,
stun_chance: 0,
back_chance: 0,
slow_chance: 0,
puncture: 0,
puncture_dmg: 0,
wfuny: 0,
fac: FacSet.HERO
};
const mockEntity = {
eid: -10086,
get: () => mockAttrs
};
this.mockCasterView = {
node: this.mockCasterNode,
box_group: BoxSet.HERO,
ent: mockEntity
};
return this.mockCasterView;
}
private playDebugSkill(skillId: number) {
const conf = SkillSet[skillId];
if (!conf) {
return;
}
this.destroyCurrentSkill();
const parent = this.getSkillParent();
const startPos = v3(-260, -40, 0);
const targetPos = v3(260, -40, 0);
const caster = this.ensureMockCaster(parent, startPos);
const skill = ecs.getEntity<Skill>(Skill);
skill.load(startPos.clone(), parent, skillId, targetPos, caster, 0);
this.currentSkill = skill;
}
private destroyCurrentSkill() {
if (!this.currentSkill) {
return;
}
this.currentSkill.destroy();
this.currentSkill = null;
}
reset() {
this.node.destroy()
this.destroyCurrentSkill();
this.clearButtons();
if (this.mockCasterNode && this.mockCasterNode.isValid) {
this.mockCasterNode.destroy();
this.mockCasterNode = null;
}
this.node.destroy();
}
}
}

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) 删除组件是触发组件处理自定义释放逻辑 */

View File

@@ -9,7 +9,7 @@ import { GameEvent } from "../common/config/GameEvent";
import { HeroViewComp } from "../hero/HeroViewComp";
import { UIID } from "../common/config/GameUIConfig";
import { SkillView } from "../skill/SkillView";
import { FightSet, CardType, FacSet } from "../common/config/GameSet";
import { FightSet } from "../common/config/GameSet";
import { mLogger } from "../common/Logger";
const { ccclass, property } = _decorator;