Compare commits
5 Commits
40723174c9
...
24eb72ef99
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24eb72ef99 | ||
|
|
d135aa9905 | ||
|
|
16996b9805 | ||
|
|
399e25c3a6 | ||
|
|
07cc167881 |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,12 @@
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "101841f1-5f9a-4115-964f-5a6ca92a0e46",
|
||||
"uuid": "f2f9a3e0-d0f5-46f4-84bd-dd54c2248470",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "equip-001"
|
||||
"syncNodeName": "cHero"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1456,7 +1456,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 306.634,
|
||||
"y": 451.513,
|
||||
"y": 651.253,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -1975,7 +1975,7 @@
|
||||
"_left": -8.317000000000007,
|
||||
"_right": -6.6340000000000146,
|
||||
"_top": 89.63,
|
||||
"_bottom": 396.513,
|
||||
"_bottom": 596.253,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
@@ -5906,7 +5906,7 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "cards",
|
||||
"_name": "cbox",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
@@ -5931,7 +5931,7 @@
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0,
|
||||
"y": 296.948,
|
||||
"y": 280,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -5971,8 +5971,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 519.4,
|
||||
"height": 300
|
||||
"width": 720,
|
||||
"height": 340
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -5983,7 +5983,7 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "d52ar8fGRCcJCkEGKYqLmS"
|
||||
"fileId": "78ZkzaYgNLrq5b2cza8aKb"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Widget",
|
||||
@@ -5997,14 +5997,14 @@
|
||||
"__prefab": {
|
||||
"__id__": 352
|
||||
},
|
||||
"_alignFlags": 42,
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
"_left": 100.30000000000001,
|
||||
"_right": 100.30000000000001,
|
||||
"_top": -491.51300000000003,
|
||||
"_bottom": 491.51300000000003,
|
||||
"_left": 0,
|
||||
"_right": 0,
|
||||
"_top": 0,
|
||||
"_bottom": 110,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 71.94799999999998,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
"_isAbsRight": true,
|
||||
"_isAbsTop": true,
|
||||
@@ -6012,14 +6012,14 @@
|
||||
"_isAbsHorizontalCenter": true,
|
||||
"_isAbsVerticalCenter": true,
|
||||
"_originalWidth": 100,
|
||||
"_originalHeight": 300,
|
||||
"_originalHeight": 100,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "4dDc0+apdNPrjk0bxsb5yf"
|
||||
"fileId": "6d6l1qXbRM3alP2Uq/Ylu4"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Layout",
|
||||
@@ -6029,11 +6029,11 @@
|
||||
"node": {
|
||||
"__id__": 348
|
||||
},
|
||||
"_enabled": false,
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 354
|
||||
},
|
||||
"_resizeMode": 1,
|
||||
"_resizeMode": 0,
|
||||
"_layoutType": 1,
|
||||
"_cellSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -6041,11 +6041,11 @@
|
||||
"height": 40
|
||||
},
|
||||
"_startAxis": 0,
|
||||
"_paddingLeft": 0,
|
||||
"_paddingRight": 0,
|
||||
"_paddingLeft": 10,
|
||||
"_paddingRight": 10,
|
||||
"_paddingTop": 0,
|
||||
"_paddingBottom": 0,
|
||||
"_spacingX": 4.7,
|
||||
"_spacingX": 5,
|
||||
"_spacingY": 0,
|
||||
"_verticalDirection": 1,
|
||||
"_horizontalDirection": 0,
|
||||
@@ -6057,7 +6057,7 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "81yCxsR/BPCro5/jgovbJu"
|
||||
"fileId": "986NWAVihCn4rQg8WZ0eck"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
@@ -6067,7 +6067,7 @@
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "f2mrMK3fBJT4tB+8JEKln0",
|
||||
"fileId": "52iIpQHPNPd56azk49eqsK",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
@@ -18390,7 +18390,7 @@
|
||||
"__id__": 348
|
||||
},
|
||||
"cardPrefab": {
|
||||
"__uuid__": "8aaff904-8c27-4173-9716-7161893b336e",
|
||||
"__uuid__": "f2f9a3e0-d0f5-46f4-84bd-dd54c2248470",
|
||||
"__expectedType__": "cc.Prefab"
|
||||
},
|
||||
"cards_chou": {
|
||||
@@ -18405,8 +18405,12 @@
|
||||
"showHeros": {
|
||||
"__id__": 924
|
||||
},
|
||||
"herosPanNode": null,
|
||||
"herosBoxNode": null,
|
||||
"herosPanNode": {
|
||||
"__id__": 227
|
||||
},
|
||||
"herosBoxNode": {
|
||||
"__id__": 247
|
||||
},
|
||||
"heroPrefab": null,
|
||||
"showEquips": {
|
||||
"__id__": 953
|
||||
@@ -18424,9 +18428,16 @@
|
||||
"showSkills": {
|
||||
"__id__": 985
|
||||
},
|
||||
"skillPanNode": null,
|
||||
"skillBoxNode": null,
|
||||
"skillPrefab": null,
|
||||
"skillPanNode": {
|
||||
"__id__": 621
|
||||
},
|
||||
"skillBoxNode": {
|
||||
"__id__": 662
|
||||
},
|
||||
"skillPrefab": {
|
||||
"__uuid__": "78a5b2d1-a34b-4190-895d-7d0aa22037cc",
|
||||
"__expectedType__": "cc.Prefab"
|
||||
},
|
||||
"showShop": {
|
||||
"__id__": 1014
|
||||
},
|
||||
|
||||
596
assets/script/game/map/CHeroComp.ts
Normal file
596
assets/script/game/map/CHeroComp.ts
Normal file
@@ -0,0 +1,596 @@
|
||||
/**
|
||||
* @file CHeroComp.ts
|
||||
* @description 英雄卡池单个卡槽组件(UI 视图层 + 购买/召唤逻辑)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 渲染单个英雄卡的显示信息(名称、等级、描述、图标、费用),
|
||||
* 展示方式与 SCardComp 一致(图集静态图标 + ★等级 + 描述词条)。
|
||||
* 同时支持动态升级卡(SpecialUpgrade + target_hero_eid)按英雄卡样式渲染。
|
||||
* 2. 处理购买点击 → UseHeroCard 上限校验(guard) → 扣费 → 派发 CallHero 召唤英雄。
|
||||
* 特殊卡(升级/刷新)则派发 UseSpecialCard。
|
||||
* 3. 使用成功后派发 CardUsed 事件,通知 MissionCardComp 刷新英雄卡池。
|
||||
*
|
||||
* 与 SCardComp 的差异:
|
||||
* - 数据源为 HeroInfo(heroSet),而非 SkillCardList / SkillSet。
|
||||
* - 购买前需通过 GameEvent.UseHeroCard 做英雄数量上限校验。
|
||||
* - 英雄卡效果事件为 GameEvent.CallHero,而非 UseSkillCard。
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, Tween, tween, UIOpacity } 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 { CardConfig, CardType } from "../common/config/CardSet";
|
||||
import { HeroInfo } from "../common/config/heroSet";
|
||||
import { oops } from "db://oops-framework/core/Oops";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { FieldSkillType } from "../common/config/SkillSet";
|
||||
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
||||
import { MissionEconomy } from "./MissionEconomy";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* CHeroComp —— 英雄卡池单个英雄卡项
|
||||
*
|
||||
* 由 MissionCardComp.cacheCardComps() 实例化。
|
||||
* 以图标 + 名称 + 描述词条 + 购买按钮的形式呈现。
|
||||
*/
|
||||
@ccclass('CHeroComp')
|
||||
@ecs.register('CHeroComp', false)
|
||||
export class CHeroComp extends CCComp {
|
||||
private debugMode: boolean = true;
|
||||
|
||||
// ======================== 编辑器绑定节点 ========================
|
||||
|
||||
/** 英雄图标节点 */
|
||||
@property({ type: Node })
|
||||
private icon_node: Node = null;
|
||||
|
||||
/** 背景节点 */
|
||||
@property({ type: Node })
|
||||
private bg_node: Node = null;
|
||||
|
||||
/** 英雄名称 */
|
||||
@property(Label)
|
||||
private name_label: Label = null;
|
||||
|
||||
/** 描述词条 1 */
|
||||
@property(Label)
|
||||
private fied1: Label = null;
|
||||
/** 描述词条 2 */
|
||||
@property(Label)
|
||||
private fied2: Label = null;
|
||||
/** 描述词条 3 */
|
||||
@property(Label)
|
||||
private fied3: Label = null;
|
||||
|
||||
/** 等级标签 */
|
||||
@property(Label)
|
||||
private level_label: Label = null;
|
||||
|
||||
/** 购买按钮节点 */
|
||||
@property({ type: Node })
|
||||
private buy_node: Node = null;
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 当前槽位承载的卡牌配置,null 表示空槽 */
|
||||
private cardData: CardConfig | null = null;
|
||||
/** 是否已购买 */
|
||||
private isPurchased: boolean = false;
|
||||
/** 是否已缓存基准 Y/Z 坐标(首次 setSlotPosition 时确定) */
|
||||
private hasFixedBasePosition: boolean = false;
|
||||
/** 槽位基准 Y 坐标 */
|
||||
private fixedBaseY: number = 0;
|
||||
/** 槽位基准 Z 坐标 */
|
||||
private fixedBaseZ: number = 0;
|
||||
/** 静止位置(由 MissionCardComp 布局决定) */
|
||||
private restPosition: Vec3 = new Vec3();
|
||||
/** 透明度组件(用于淡入淡出) */
|
||||
private opacityComp: UIOpacity | null = null;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad() {
|
||||
mLogger.log(this.debugMode, "CHeroComp", "onLoad bindings", {
|
||||
nodeName: this.node?.name,
|
||||
nodeUuid: this.node?.uuid,
|
||||
hasCardData: !!this.cardData,
|
||||
cardUuid: this.cardData?.uuid ?? null,
|
||||
icon_node: !!this.icon_node,
|
||||
bg_node: !!this.bg_node,
|
||||
name_label: !!this.name_label,
|
||||
level_label: !!this.level_label,
|
||||
fied1: !!this.fied1,
|
||||
fied2: !!this.fied2,
|
||||
fied3: !!this.fied3,
|
||||
buy_node: !!this.buy_node
|
||||
});
|
||||
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
|
||||
// 缓存透明度组件(用于淡入淡出动画)
|
||||
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
|
||||
|
||||
// 补偿首次渲染:addComponent 当帧 onLoad 尚未触发,
|
||||
// 若 MissionCardComp 在 onLoad 前已 applyCardData,此处绑定就绪后重刷一次
|
||||
if (this.cardData) {
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.buy_node && this.buy_node.isValid) {
|
||||
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 数据初始化 ========================
|
||||
|
||||
/**
|
||||
* 初始化英雄卡数据并渲染 UI
|
||||
*
|
||||
* 若槽位已有旧卡且处于显示状态,先播放旧卡"逝去"动画(缩小淡出),
|
||||
* 动画结束后再切换为新卡数据并播放入场动画。
|
||||
* 首次赋值(node 未激活)直接渲染,无逝去动画。
|
||||
*
|
||||
* @param data 卡牌配置,传 null 时清空显示
|
||||
*/
|
||||
applyCardData(data: CardConfig | null): void {
|
||||
mLogger.log(this.debugMode, "CHeroComp", "applyCardData", {
|
||||
nodeName: this.node?.name,
|
||||
nodeUuid: this.node?.uuid,
|
||||
uuid: data?.uuid ?? null,
|
||||
type: data?.type ?? null,
|
||||
name: data?.name ?? null,
|
||||
card_lv: data?.card_lv ?? null,
|
||||
hero_lv: data?.hero_lv ?? null,
|
||||
cost: data?.cost ?? null,
|
||||
icon: data?.icon ?? null,
|
||||
nodeActiveBefore: this.node?.active
|
||||
});
|
||||
|
||||
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
|
||||
if (this.isPurchased) {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
if (data) {
|
||||
this.node.active = true;
|
||||
this.updateUI();
|
||||
} else {
|
||||
this.applyEmptyUI();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 旧卡存在且当前显示中 → 播逝去动画,结束后再应用新卡
|
||||
if (this.cardData && this.node.active) {
|
||||
this.playRefreshExitAnim(() => {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
if (data) {
|
||||
this.node.active = true;
|
||||
this.updateUI();
|
||||
} else {
|
||||
this.applyEmptyUI();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次赋值或槽位为空 → 直接渲染
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
if (data) {
|
||||
this.node.active = true;
|
||||
this.updateUI();
|
||||
} else {
|
||||
this.applyEmptyUI();
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空 UI 显示 */
|
||||
private applyEmptyUI() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
this.opacityComp.opacity = 255;
|
||||
}
|
||||
this.node.setScale(1, 1, 1);
|
||||
if (this.name_label) this.name_label.string = "";
|
||||
if (this.level_label) this.level_label.string = "";
|
||||
const fieldLabels = [this.fied1, this.fied2, this.fied3];
|
||||
for (const label of fieldLabels) {
|
||||
if (label) {
|
||||
label.string = "";
|
||||
label.node.active = false;
|
||||
}
|
||||
}
|
||||
if (this.icon_node) {
|
||||
const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite);
|
||||
if (sprite) sprite.spriteFrame = null;
|
||||
}
|
||||
if (this.buy_node) {
|
||||
this.buy_node.active = false;
|
||||
}
|
||||
this.node.active = false;
|
||||
}
|
||||
|
||||
// ======================== UI 渲染 ========================
|
||||
|
||||
updateUI() {
|
||||
if (!this.cardData) {
|
||||
mLogger.log(this.debugMode, "CHeroComp", "updateUI skip: no cardData", { nodeName: this.node?.name });
|
||||
return;
|
||||
}
|
||||
|
||||
// 动态升级卡(SpecialUpgrade + target_hero_eid)按英雄卡样式渲染:
|
||||
// 通过 eid 反查场上实体得到 hero_uuid;实体不存在时降级为本卡 uuid
|
||||
const isUpgradeCard = this.cardData.type === CardType.SpecialUpgrade && !!this.cardData.target_hero_eid;
|
||||
const renderUuid = isUpgradeCard
|
||||
? (this.queryHeroUuidByEid(this.cardData.target_hero_eid as number) ?? this.cardData.uuid)
|
||||
: this.cardData.uuid;
|
||||
const hero = HeroInfo[renderUuid];
|
||||
|
||||
mLogger.log(this.debugMode, "CHeroComp", "updateUI resolve", {
|
||||
nodeName: this.node?.name,
|
||||
uuid: this.cardData.uuid,
|
||||
type: this.cardData.type,
|
||||
isUpgradeCard,
|
||||
renderUuid,
|
||||
heroFound: !!hero,
|
||||
heroName: hero?.name ?? null,
|
||||
heroPath: hero?.path ?? null,
|
||||
heroInfo: hero?.info ?? null
|
||||
});
|
||||
|
||||
// 名称
|
||||
if (this.name_label) {
|
||||
const finalName = hero?.name || this.cardData.name || "";
|
||||
this.name_label.string = finalName;
|
||||
mLogger.log(this.debugMode, "CHeroComp", "name_label assigned", {
|
||||
nodeName: this.node?.name,
|
||||
finalName,
|
||||
labelStringAfter: this.name_label.string,
|
||||
labelNodeActive: this.name_label.node?.active,
|
||||
labelNodeParentActive: this.name_label.node?.parent?.active,
|
||||
labelNodeParentName: this.name_label.node?.parent?.name
|
||||
});
|
||||
}
|
||||
|
||||
// 等级标签(★ 表示卡牌等级;升级卡显示英雄目标等级)
|
||||
if (this.level_label) {
|
||||
if (isUpgradeCard) {
|
||||
const heroLv = Math.max(1, this.cardData.hero_lv ?? 1);
|
||||
this.level_label.string = heroLv > 1 ? `Lv.${heroLv - 1} -> Lv.${heroLv}` : `Lv.${heroLv}`;
|
||||
} else {
|
||||
const lv = this.cardData.card_lv ?? 1;
|
||||
this.level_label.string = lv >= 2 ? "★".repeat(lv - 1) : "";
|
||||
}
|
||||
}
|
||||
|
||||
// 描述词条(最多显示 3 条,对应 fied1/fied2/fied3)
|
||||
const fieldLabels = [this.fied1, this.fied2, this.fied3];
|
||||
const desc = hero?.info || this.cardData.info || "";
|
||||
for (let i = 0; i < fieldLabels.length; i++) {
|
||||
if (!fieldLabels[i]) continue;
|
||||
fieldLabels[i].string = i === 0 ? desc : "";
|
||||
fieldLabels[i].node.active = i === 0 && !!desc;
|
||||
}
|
||||
|
||||
// 图标
|
||||
if (this.icon_node) {
|
||||
const iconId = this.getIconId(renderUuid);
|
||||
if (iconId) {
|
||||
this.updateIcon(this.icon_node, iconId);
|
||||
}
|
||||
}
|
||||
|
||||
// 购买按钮费用显示
|
||||
if (this.buy_node) {
|
||||
this.buy_node.active = true;
|
||||
const costLabel = this.buy_node.getChildByName("num")?.getComponent(Label)
|
||||
|| this.buy_node.getComponentInChildren(Label);
|
||||
if (costLabel) {
|
||||
costLabel.string = `${this.cardData.cost ?? 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 入场动画:从静止位置下方升起 + 淡入
|
||||
this.playEnterAnim();
|
||||
}
|
||||
|
||||
// ======================== 动画效果 ========================
|
||||
|
||||
/** 入场动画时长(秒) */
|
||||
private readonly enterDuration: number = 0.25;
|
||||
/** 购买动画时长(秒) */
|
||||
private readonly purchaseDuration: number = 0.2;
|
||||
|
||||
/**
|
||||
* 播放入场动画:由小变大(scale 0→1)+ 淡入。
|
||||
* 不依赖位置,与 Widget 布局完全解耦。
|
||||
*/
|
||||
private playEnterAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
this.opacityComp.opacity = 0;
|
||||
}
|
||||
this.node.setScale(0, 0, 1);
|
||||
tween(this.node)
|
||||
.to(this.enterDuration, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.enterDuration, { opacity: 255 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放购买动画:卡牌放大(scale 1→1.1)+ 淡出,结束后隐藏节点。
|
||||
* 动画结束由 onBuyClick 的后续逻辑(CardUsed 刷新卡池)接管。
|
||||
*/
|
||||
private playPurchaseAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.purchaseDuration, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.purchaseDuration, { opacity: 0 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放普通刷新"逝去"动画:旧卡缩小(scale 1→0)+ 淡出,结束后回调切换新卡。
|
||||
* 与购买动画区分:购买是放大逝去,普通刷新是缩小逝去。
|
||||
* @param onComplete 动画结束回调,用于应用新卡数据
|
||||
*/
|
||||
private playRefreshExitAnim(onComplete: () => void) {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.purchaseDuration, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.purchaseDuration, { opacity: 0 })
|
||||
.call(onComplete)
|
||||
.start();
|
||||
} else {
|
||||
// 无透明度组件时直接延迟回调
|
||||
this.scheduleOnce(onComplete, this.purchaseDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 按钮动画 ========================
|
||||
|
||||
/** 按钮正常缩放 */
|
||||
private readonly btnNormalScale: number = 1;
|
||||
/** 按钮按下缩放 */
|
||||
private readonly btnPressScale: number = 0.92;
|
||||
/** 按钮点击回弹峰值缩放 */
|
||||
private readonly btnClickScale: number = 1.08;
|
||||
|
||||
/** 购买按钮按下:缩小反馈 */
|
||||
private onBuyTouchStart() {
|
||||
this.playBtnScaleTo(this.btnPressScale, 0.06);
|
||||
}
|
||||
|
||||
/** 购买按钮取消:恢复缩放 */
|
||||
private onBuyTouchCancel() {
|
||||
this.playBtnScaleTo(this.btnNormalScale, 0.08);
|
||||
}
|
||||
|
||||
/** 按钮缩放动画(通用) */
|
||||
private playBtnScaleTo(scale: number, duration: number) {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(duration, { scale: new Vec3(scale, scale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
|
||||
private playBtnClickAnim(onComplete?: () => void) {
|
||||
if (!this.buy_node || !this.buy_node.isValid) {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
|
||||
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
|
||||
.call(() => onComplete?.())
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 eid 反查场上英雄实体的 hero_uuid。
|
||||
* 用于动态升级卡渲染:升级卡绑定的是 eid,但显示需要 HeroInfo[uuid]。
|
||||
* @returns 找到则返回 hero_uuid,找不到(实体已销毁等)返回 null
|
||||
*/
|
||||
private queryHeroUuidByEid(eid: number): number | null {
|
||||
if (!eid) return null;
|
||||
const entity = ecs.getEntityByEid(eid);
|
||||
if (!entity) return null;
|
||||
const model = entity.get(HeroAttrsComp);
|
||||
if (!model) return null;
|
||||
return model.hero_uuid ?? null;
|
||||
}
|
||||
|
||||
/** 获取英雄图标 id:优先卡牌自定义 icon,其次英雄 path,兜底 uuid */
|
||||
private getIconId(renderUuid: number): string {
|
||||
if (!this.cardData) return "";
|
||||
if (this.cardData.icon) return this.cardData.icon;
|
||||
const hero = HeroInfo[renderUuid];
|
||||
return hero?.path || `${renderUuid}`;
|
||||
}
|
||||
|
||||
/** 更新图标精灵 */
|
||||
private updateIcon(node: Node, iconId: string) {
|
||||
if (!node || !iconId) return;
|
||||
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||||
const frame = smc.uiconsAtlas ? smc.uiconsAtlas.getSpriteFrame(iconId) : null;
|
||||
mLogger.log(this.debugMode, "CHeroComp", "updateIcon", {
|
||||
nodeName: this.node?.name,
|
||||
iconId,
|
||||
spriteFound: !!sprite,
|
||||
atlasReady: !!smc.uiconsAtlas,
|
||||
frameFound: !!frame
|
||||
});
|
||||
if (!sprite) return;
|
||||
if (smc.uiconsAtlas) {
|
||||
sprite.spriteFrame = frame || null;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 购买逻辑 ========================
|
||||
|
||||
/**
|
||||
* 购买点击处理:
|
||||
* 1. 播放按钮点击回弹动画。
|
||||
* 2. 英雄卡先通过 UseHeroCard 事件做数量上限校验(guard 模式,可被取消)。
|
||||
* 3. 扣除金币(失败则提示并中止)。
|
||||
* 4. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard。
|
||||
* 5. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池。
|
||||
* 6. 标记已购买,隐藏购买按钮。
|
||||
*/
|
||||
private onBuyClick() {
|
||||
if (!this.cardData || this.isPurchased) return;
|
||||
|
||||
// 播放按钮点击回弹动画(视觉反馈,不阻塞后续逻辑)
|
||||
this.playBtnClickAnim();
|
||||
|
||||
oops.audio.playEffect("music/button");
|
||||
|
||||
// 英雄卡数量上限校验:guard 模式,MissionCardComp 可设 cancel=true 阻止使用
|
||||
if (this.cardData.type === CardType.Hero) {
|
||||
const guard = {
|
||||
cancel: false,
|
||||
reason: "",
|
||||
uuid: this.cardData.uuid,
|
||||
hero_lv: this.cardData.hero_lv ?? 1,
|
||||
card_lv: this.cardData.base_pool_lv ?? this.cardData.pool_lv ?? 1
|
||||
};
|
||||
oops.message.dispatchEvent(GameEvent.UseHeroCard, guard);
|
||||
if (guard.cancel) return;
|
||||
}
|
||||
|
||||
// 扣费(英雄卡享受驻场"购买优惠"折扣)
|
||||
let cost = this.cardData.cost ?? 0;
|
||||
if (this.cardData.type === CardType.Hero) {
|
||||
const discount = FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.BuyDiscount);
|
||||
cost = Math.max(0, cost - discount);
|
||||
}
|
||||
const success = MissionEconomy.spendCoin(Math.floor(cost));
|
||||
if (!success) {
|
||||
oops.message.dispatchEvent(GameEvent.ShowSmallTip, "buy_coin");
|
||||
mLogger.log(this.debugMode, "CHeroComp", "purchase failed: not enough coin", {
|
||||
uuid: this.cardData.uuid,
|
||||
cost
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
smc.vmdata.scores.refresh_hit_count++;
|
||||
|
||||
const used = this.cardData;
|
||||
|
||||
// 按卡牌类型分发效果事件
|
||||
switch (used.type) {
|
||||
case CardType.Hero:
|
||||
oops.message.dispatchEvent(GameEvent.CallHero, used);
|
||||
break;
|
||||
case CardType.SpecialUpgrade:
|
||||
case CardType.SpecialRefresh:
|
||||
oops.message.dispatchEvent(GameEvent.UseSpecialCard, used);
|
||||
break;
|
||||
}
|
||||
|
||||
// 播放购买动画(缩小淡出),刷新卡池由 CardUsed 事件触发
|
||||
this.playPurchaseAnim();
|
||||
|
||||
// 标记已购买
|
||||
this.isPurchased = true;
|
||||
if (this.buy_node) {
|
||||
this.buy_node.active = false;
|
||||
}
|
||||
|
||||
// 通知 MissionCardComp:本槽位卡牌已使用,刷新英雄卡池
|
||||
oops.message.dispatchEvent(GameEvent.CardUsed, this);
|
||||
|
||||
mLogger.log(this.debugMode, "CHeroComp", "hero card purchased", {
|
||||
uuid: used.uuid,
|
||||
type: used.type,
|
||||
cost
|
||||
});
|
||||
}
|
||||
|
||||
/** 标记为已购买状态 */
|
||||
setPurchased(): void {
|
||||
this.isPurchased = true;
|
||||
if (this.buy_node) {
|
||||
this.buy_node.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收 MissionCardComp 分发的卡牌并刷新显示。
|
||||
* @param data 卡牌配置,null 时清空显示
|
||||
* @returns 是否成功接收(true = 接收并刷新 UI)
|
||||
*/
|
||||
applyDrawCard(data: CardConfig | null): boolean {
|
||||
this.applyCardData(data);
|
||||
return !!data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置槽位的水平位置(由 MissionCardComp 布局计算后调用)。
|
||||
* 首次调用时缓存基准 Y/Z,后续仅更新 X。
|
||||
* @param x 目标水平坐标
|
||||
*/
|
||||
setSlotPosition(x: number) {
|
||||
const current = this.node.position;
|
||||
if (!this.hasFixedBasePosition) {
|
||||
this.fixedBaseY = current.y;
|
||||
this.fixedBaseZ = current.z;
|
||||
this.hasFixedBasePosition = true;
|
||||
}
|
||||
this.restPosition = new Vec3(x, this.fixedBaseY, this.fixedBaseZ);
|
||||
this.node.setPosition(this.restPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统清槽:任务开始/结束时强制重置。
|
||||
* 清空数据与 UI 并隐藏节点。
|
||||
*/
|
||||
clearBySystem() {
|
||||
this.cardData = null;
|
||||
this.isPurchased = false;
|
||||
this.node.setPosition(this.restPosition);
|
||||
this.applyEmptyUI();
|
||||
this.node.active = false;
|
||||
}
|
||||
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
if (this.node && this.node.isValid) {
|
||||
this.node.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/script/game/map/CHeroComp.ts.meta
Normal file
9
assets/script/game/map/CHeroComp.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "62e81154-704c-4ce5-ab9d-19c12d539517",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1038,9 +1038,18 @@ export class CardComp extends CCComp {
|
||||
if (token !== this.iconVisualToken) {
|
||||
return;
|
||||
}
|
||||
this.clearAnimationClips(anim);
|
||||
anim.addClip(clip);
|
||||
anim.play("idle");
|
||||
// 节点可能已在异步加载期间被销毁(如 cacheCardComps 重新实例化)
|
||||
if (!node || !node.isValid) {
|
||||
return;
|
||||
}
|
||||
// 重新获取 Animation 组件,防止原组件已被移除
|
||||
const animComp = node.getComponent(Animation);
|
||||
if (!animComp) {
|
||||
return;
|
||||
}
|
||||
this.clearAnimationClips(animComp);
|
||||
animComp.addClip(clip);
|
||||
animComp.play("idle");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* EquipBoxComp 按 Field 类型被动驻场,属性加成由 FieldSkillHelper 全局聚合。
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType } from "cc";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, UIOpacity, tween, Tween } 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 { CardConfig } from "../common/config/CardSet";
|
||||
@@ -74,17 +74,28 @@ export class EquipListComp extends CCComp {
|
||||
private cardData: CardConfig | null = null;
|
||||
/** 是否已购买 */
|
||||
private isPurchased: boolean = false;
|
||||
/** 透明度组件(用于淡入淡出动画) */
|
||||
private opacityComp: UIOpacity | null = null;
|
||||
/** 入场动画时长(秒) */
|
||||
private readonly enterDuration: number = 0.25;
|
||||
/** 购买/逝去动画时长(秒) */
|
||||
private readonly exitDuration: number = 0.2;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad() {
|
||||
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.buy_node && this.buy_node.isValid) {
|
||||
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +104,33 @@ export class EquipListComp extends CCComp {
|
||||
/**
|
||||
* 初始化装备卡数据并渲染 UI
|
||||
*
|
||||
* 若槽位已有旧卡且显示中,先播普通逝去动画(缩小淡出),结束后切新卡;
|
||||
* 刚购买的槽位跳过逝去动画直接应用新卡(购买动画已播放大淡出)。
|
||||
*
|
||||
* @param data 装备卡配置(必须为 trigger_type=Field 的装备卡)
|
||||
*/
|
||||
applyCardData(data: CardConfig): void {
|
||||
if (!data) return;
|
||||
|
||||
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
|
||||
if (this.isPurchased) {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
this.updateUI();
|
||||
return;
|
||||
}
|
||||
|
||||
// 旧卡存在且显示中 → 播普通逝去动画,结束后切新卡
|
||||
if (this.cardData && this.node.active) {
|
||||
this.playRefreshExitAnim(() => {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
this.updateUI();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次赋值 → 直接渲染
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
this.updateUI();
|
||||
@@ -150,6 +184,107 @@ export class EquipListComp extends CCComp {
|
||||
costLabel.string = `${this.cardData.cost ?? 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放入场动画(由小变大 + 淡入)
|
||||
this.playEnterAnim();
|
||||
}
|
||||
|
||||
// ======================== 动画 ========================
|
||||
|
||||
/** 播放入场动画:卡牌由小变大(scale 0→1)+ 淡入 */
|
||||
private playEnterAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
this.node.setScale(0, 0, 1);
|
||||
if (this.opacityComp) {
|
||||
this.opacityComp.opacity = 0;
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.enterDuration, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.enterDuration, { opacity: 255 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/** 播放购买动画:卡牌放大(scale 1→1.1)+ 淡出 */
|
||||
private playPurchaseAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.exitDuration, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.exitDuration, { opacity: 0 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放普通刷新"逝去"动画:旧卡缩小(scale 1→0)+ 淡出,结束后回调切换新卡。
|
||||
* @param onComplete 动画结束回调,用于应用新卡数据
|
||||
*/
|
||||
private playRefreshExitAnim(onComplete: () => void) {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.exitDuration, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.exitDuration, { opacity: 0 })
|
||||
.call(onComplete)
|
||||
.start();
|
||||
} else {
|
||||
this.scheduleOnce(onComplete, this.exitDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 按钮动画 ========================
|
||||
|
||||
/** 按钮正常缩放 */
|
||||
private readonly btnNormalScale: number = 1;
|
||||
/** 按钮按下缩放 */
|
||||
private readonly btnPressScale: number = 0.92;
|
||||
/** 按钮点击回弹峰值缩放 */
|
||||
private readonly btnClickScale: number = 1.08;
|
||||
|
||||
/** 购买按钮按下:缩小反馈 */
|
||||
private onBuyTouchStart() {
|
||||
this.playBtnScaleTo(this.btnPressScale, 0.06);
|
||||
}
|
||||
|
||||
/** 购买按钮取消:恢复缩放 */
|
||||
private onBuyTouchCancel() {
|
||||
this.playBtnScaleTo(this.btnNormalScale, 0.08);
|
||||
}
|
||||
|
||||
/** 按钮缩放动画(通用) */
|
||||
private playBtnScaleTo(scale: number, duration: number) {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(duration, { scale: new Vec3(scale, scale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
|
||||
private playBtnClickAnim() {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
|
||||
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 更新图标精灵 */
|
||||
@@ -178,6 +313,9 @@ export class EquipListComp extends CCComp {
|
||||
private onBuyClick() {
|
||||
if (!this.cardData || this.isPurchased) return;
|
||||
|
||||
// 播放按钮点击回弹动画
|
||||
this.playBtnClickAnim();
|
||||
|
||||
oops.audio.playEffect("music/button");
|
||||
|
||||
// 扣费
|
||||
@@ -201,6 +339,9 @@ export class EquipListComp extends CCComp {
|
||||
this.buy_node.active = false;
|
||||
}
|
||||
|
||||
// 播放购买动画(卡牌放大 + 淡出)
|
||||
this.playPurchaseAnim();
|
||||
|
||||
mLogger.log(this.debugMode, "EquipListComp", "equipment purchased", {
|
||||
uuid: this.cardData.uuid,
|
||||
cost
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 本组件同时服务于装备商店和商品商店,通过 cardData 的 trigger_type / kind 自动判断类型。
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType } from "cc";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, UIOpacity, tween, Tween } 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 { CardConfig, CardTriggerType, CKind } from "../common/config/CardSet";
|
||||
@@ -76,17 +76,28 @@ export class ItemListComp extends CCComp {
|
||||
private cardData: CardConfig | null = null;
|
||||
/** 是否已购买 */
|
||||
private isPurchased: boolean = false;
|
||||
/** 透明度组件(用于淡入淡出动画) */
|
||||
private opacityComp: UIOpacity | null = null;
|
||||
/** 入场动画时长(秒) */
|
||||
private readonly enterDuration: number = 0.25;
|
||||
/** 购买/逝去动画时长(秒) */
|
||||
private readonly exitDuration: number = 0.2;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad() {
|
||||
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.buy_node && this.buy_node.isValid) {
|
||||
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +106,33 @@ export class ItemListComp extends CCComp {
|
||||
/**
|
||||
* 初始化商品卡数据并渲染 UI
|
||||
*
|
||||
* 若槽位已有旧卡且显示中,先播普通逝去动画(缩小淡出),结束后切新卡;
|
||||
* 刚购买的槽位跳过逝去动画直接应用新卡(购买动画已播放大淡出)。
|
||||
*
|
||||
* @param data 商品/装备卡配置
|
||||
*/
|
||||
applyCardData(data: CardConfig): void {
|
||||
if (!data) return;
|
||||
|
||||
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
|
||||
if (this.isPurchased) {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
this.updateUI();
|
||||
return;
|
||||
}
|
||||
|
||||
// 旧卡存在且显示中 → 播普通逝去动画,结束后切新卡
|
||||
if (this.cardData && this.node.active) {
|
||||
this.playRefreshExitAnim(() => {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
this.updateUI();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次赋值 → 直接渲染
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
this.updateUI();
|
||||
@@ -196,6 +230,107 @@ export class ItemListComp extends CCComp {
|
||||
costLabel.string = `${this.cardData.cost ?? 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放入场动画(由小变大 + 淡入)
|
||||
this.playEnterAnim();
|
||||
}
|
||||
|
||||
// ======================== 动画 ========================
|
||||
|
||||
/** 播放入场动画:卡牌由小变大(scale 0→1)+ 淡入 */
|
||||
private playEnterAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
this.node.setScale(0, 0, 1);
|
||||
if (this.opacityComp) {
|
||||
this.opacityComp.opacity = 0;
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.enterDuration, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.enterDuration, { opacity: 255 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/** 播放购买动画:卡牌放大(scale 1→1.1)+ 淡出 */
|
||||
private playPurchaseAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.exitDuration, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.exitDuration, { opacity: 0 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放普通刷新"逝去"动画:旧卡缩小(scale 1→0)+ 淡出,结束后回调切换新卡。
|
||||
* @param onComplete 动画结束回调,用于应用新卡数据
|
||||
*/
|
||||
private playRefreshExitAnim(onComplete: () => void) {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.exitDuration, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.exitDuration, { opacity: 0 })
|
||||
.call(onComplete)
|
||||
.start();
|
||||
} else {
|
||||
this.scheduleOnce(onComplete, this.exitDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 按钮动画 ========================
|
||||
|
||||
/** 按钮正常缩放 */
|
||||
private readonly btnNormalScale: number = 1;
|
||||
/** 按钮按下缩放 */
|
||||
private readonly btnPressScale: number = 0.92;
|
||||
/** 按钮点击回弹峰值缩放 */
|
||||
private readonly btnClickScale: number = 1.08;
|
||||
|
||||
/** 购买按钮按下:缩小反馈 */
|
||||
private onBuyTouchStart() {
|
||||
this.playBtnScaleTo(this.btnPressScale, 0.06);
|
||||
}
|
||||
|
||||
/** 购买按钮取消:恢复缩放 */
|
||||
private onBuyTouchCancel() {
|
||||
this.playBtnScaleTo(this.btnNormalScale, 0.08);
|
||||
}
|
||||
|
||||
/** 按钮缩放动画(通用) */
|
||||
private playBtnScaleTo(scale: number, duration: number) {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(duration, { scale: new Vec3(scale, scale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
|
||||
private playBtnClickAnim() {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
|
||||
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 更新图标精灵 */
|
||||
@@ -222,6 +357,9 @@ export class ItemListComp extends CCComp {
|
||||
private onBuyClick() {
|
||||
if (!this.cardData || this.isPurchased) return;
|
||||
|
||||
// 播放按钮点击回弹动画
|
||||
this.playBtnClickAnim();
|
||||
|
||||
oops.audio.playEffect("music/button");
|
||||
|
||||
// 扣费
|
||||
@@ -269,6 +407,9 @@ export class ItemListComp extends CCComp {
|
||||
if (this.buy_node) {
|
||||
this.buy_node.active = false;
|
||||
}
|
||||
|
||||
// 播放购买动画(卡牌放大 + 淡出)
|
||||
this.playPurchaseAnim();
|
||||
}
|
||||
|
||||
/** 标记为已购买状态(用于跨波次保持购买记录) */
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. **卡牌分发管理** —— 从卡池抽取 3 张卡,分发到 3 个 CardComp 槽位。
|
||||
* 抽卡规则:场上已有英雄时按权重混合"对应英雄的升级卡" + "其他英雄卡" + "刷新卡";
|
||||
* 场上无英雄时仅抽取英雄卡和刷新卡。
|
||||
* 1. **卡牌分发管理** —— 从卡池抽取 3 张英雄卡,分发到 3 个 CHeroComp 槽位。
|
||||
* 抽卡规则:从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复;
|
||||
* 场上已召唤的英雄不再刷出。
|
||||
* 2. **金币费用管理** —— 抽卡费用(refreshCost)的扣除。
|
||||
* 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。
|
||||
* 4. **场上英雄信息面板(HInfoComp 列表)同步** ——
|
||||
@@ -15,20 +15,20 @@
|
||||
* 6. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 3 个 CardComp 由 cardPrefab 动态实例化生成,通过 cacheCardComps() 映射为有序数组 cardComps[],
|
||||
* - 3 个 CHeroComp 由 cardPrefab 动态实例化生成,通过 cacheCardComps() 映射为有序数组 cardComps[],
|
||||
* 之后所有分发、清空操作均通过此数组进行。
|
||||
* - buildDrawCards() 动态合并升级卡池和基础卡池后抽取 3 张,不足时循环补齐。
|
||||
* - buildDrawCards() 从基础英雄卡池按权重抽取 3 张,不足时循环补齐。
|
||||
* - 英雄上限校验(onUseHeroCard)采用 guard/cancel 模式:
|
||||
* CardComp 发出 UseHeroCard 事件并传入 guard 对象,
|
||||
* CHeroComp 发出 UseHeroCard 事件并传入 guard 对象,
|
||||
* 本组件可通过 guard.cancel=true 阻止使用。
|
||||
*
|
||||
* 历史:
|
||||
* 旧版本曾包含"卡池等级(poolLv)"机制和"三合一合成腾位"判断,已全部移除:
|
||||
* - 卡牌不再分级,所有英雄卡统一 lv1。
|
||||
* - 英雄升级仅通过升级卡(SpecialUpgrade)触发,按 UUID 精确升级场上对应英雄。
|
||||
* - 升级卡机制已移除,英雄卡池只出普通英雄卡;英雄升级仅通过升级卡(SpecialUpgrade)触发。
|
||||
*
|
||||
* 依赖:
|
||||
* - CardComp —— 单卡槽位
|
||||
* - CHeroComp —— 单卡槽位
|
||||
* - HInfoComp —— 英雄信息面板
|
||||
* - CardSet 模块 —— 卡池配置、抽卡规则、特殊卡数据
|
||||
* - HeroAttrsComp —— 英雄属性(升级)
|
||||
@@ -39,11 +39,11 @@ import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, Tween, twe
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
|
||||
import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType } from "../common/config/CardSet";
|
||||
import { drawSkillCards } from "../common/config/SCardSet";
|
||||
import { drawEquipCards } from "../common/config/EquipSet";
|
||||
import { drawItemCards } from "../common/config/ICardSet";
|
||||
import { CardComp } from "./CardComp";
|
||||
import { CHeroComp } from "./CHeroComp";
|
||||
import { SCardComp } from "./SCardComp";
|
||||
import { EquipListComp } from "./EquipListComp";
|
||||
import { oops } from "db://oops-framework/core/Oops";
|
||||
@@ -70,8 +70,6 @@ const { ccclass, property } = _decorator;
|
||||
export class MissionCardComp extends CCComp {
|
||||
/** 是否启用调试日志 */
|
||||
private debugMode: boolean = false;
|
||||
/** 卡牌槽位宽度(像素),用于水平等距布局 */
|
||||
private readonly cardWidth: number = 175;
|
||||
/** 按钮正常缩放 */
|
||||
private readonly buttonNormalScale: number = 1;
|
||||
/** 按钮按下缩放 */
|
||||
@@ -188,8 +186,8 @@ export class MissionCardComp extends CCComp {
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 三个槽位对应的 CardComp 控制器缓存(有序数组) */
|
||||
private cardComps: CardComp[] = [];
|
||||
/** 三个槽位对应的 CHeroComp 控制器缓存(有序数组) */
|
||||
private cardComps: CHeroComp[] = [];
|
||||
/** 技能卡槽控制器缓存 */
|
||||
private skillCardComps: SCardComp[] = [];
|
||||
/** 是否已召唤过英雄(用于控制其他面板按钮是否可点击) */
|
||||
@@ -217,14 +215,6 @@ export class MissionCardComp extends CCComp {
|
||||
private cardsShowScale: Vec3 = new Vec3(1, 1, 1);
|
||||
/** 卡牌面板收起态缩放(scale=0 隐藏) */
|
||||
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
|
||||
/** 卡牌原始定位点 */
|
||||
private cardsPos = [-220, 0, 220]
|
||||
/**
|
||||
* 必出升级卡的轮询索引:每次刷新按场上可升级英雄 UUID 升序轮询选 1 张。
|
||||
* 每次 buildDrawCards 取模更新,保证多次刷新时升级卡依次切换。
|
||||
*/
|
||||
private upgradeRotationIndex: number = 0;
|
||||
|
||||
/** 卡牌面板显示位置 Y 坐标 */
|
||||
private readonly cardPanelShowY: number = 0;
|
||||
/** 卡牌面板隐藏位置 Y 坐标(向下 700) */
|
||||
@@ -237,7 +227,7 @@ export class MissionCardComp extends CCComp {
|
||||
/**
|
||||
* 组件加载:
|
||||
* 1. 绑定生命周期事件和按钮交互事件。
|
||||
* 2. 缓存 3 个 CardComp 子控制器引用。
|
||||
* 2. 缓存 3 个 CHeroComp 子控制器引用。
|
||||
* 3. 计算并设置槽位水平布局。
|
||||
* 4. 初始化卡牌面板缩放参数。
|
||||
* 5. 初始化面板轮播布局。
|
||||
@@ -319,6 +309,10 @@ export class MissionCardComp extends CCComp {
|
||||
this.node.active = true;
|
||||
}
|
||||
const cards = this.buildDrawCards();
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "onMissionStart buildDrawCards", {
|
||||
cardsLength: cards.length,
|
||||
cards: cards.map(c => ({ uuid: c.uuid, type: c.type, name: c.name }))
|
||||
});
|
||||
this.dispatchCardsToSlots(cards);
|
||||
|
||||
// 重置购买记录并填充装备商店
|
||||
@@ -763,7 +757,7 @@ export class MissionCardComp extends CCComp {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用英雄卡的 guard 校验(由 CardComp 通过 UseHeroCard 事件调用):
|
||||
* 使用英雄卡的 guard 校验(由 CHeroComp 通过 UseHeroCard 事件调用):
|
||||
* - 当前英雄数 < 上限 → 允许使用。
|
||||
* - 已满 → 阻止使用(cancel=true),弹 toast。
|
||||
*
|
||||
@@ -797,8 +791,6 @@ export class MissionCardComp extends CCComp {
|
||||
if (!uuid) return;
|
||||
let success = false;
|
||||
if (type === CardType.SpecialUpgrade) {
|
||||
const template = SpecialUpgradeCardList[uuid];
|
||||
if (!template) return;
|
||||
const targetHeroEid = Number(payload?.target_hero_eid ?? 0);
|
||||
success = this.tryUpgradeHeroByEid(targetHeroEid);
|
||||
if (!success) {
|
||||
@@ -819,7 +811,7 @@ export class MissionCardComp extends CCComp {
|
||||
|
||||
/**
|
||||
* 单张卡牌被成功使用后的整池刷新回调:
|
||||
* - payload 为该卡槽组件实例(CardComp 或 SCardComp)。
|
||||
* - payload 为该卡槽组件实例(CHeroComp 或 SCardComp)。
|
||||
* - 命中英雄卡槽 → 重新构建 3 张英雄池卡牌并分发到所有英雄槽。
|
||||
* - 命中技能卡槽 → 重新构建 3 张技能卡并分发到所有技能槽。
|
||||
* - 未命中任何槽位 → 忽略。
|
||||
@@ -827,7 +819,7 @@ export class MissionCardComp extends CCComp {
|
||||
* Why: 购买一张卡后立即刷新整个卡池(不扣金币),让玩家持续从新池中挑选;
|
||||
* 英雄卡池与技能卡池遵循同一逻辑。
|
||||
* SpecialRefresh 自身效果(onUseSpecialCard 中的 tryRefreshHeroCards)
|
||||
* 已是整池刷新,因此 CardComp 对其不派发 CardUsed,避免双重刷新。
|
||||
* 已是整池刷新,因此 CHeroComp 对其不派发 CardUsed,避免双重刷新。
|
||||
*/
|
||||
private onCardUsed(event: string, args: any) {
|
||||
const source = args;
|
||||
@@ -1171,8 +1163,19 @@ export class MissionCardComp extends CCComp {
|
||||
this.populateShopItems();
|
||||
}
|
||||
|
||||
/** 将 cardPrefab 实例化为 3 个卡槽并映射为 CardComp,形成固定顺序控制数组 */
|
||||
/** 将 cardPrefab 实例化为 3 个卡槽并映射为 CHeroComp,形成固定顺序控制数组(幂等:已缓存则不重复实例化) */
|
||||
private cacheCardComps() {
|
||||
// 幂等保护:卡槽已存在且节点有效时,仅刷新 skillCardComps 缓存,不重复实例化英雄卡槽
|
||||
const hasValidHeroComps = this.cardComps.length === 3
|
||||
&& this.cardComps.every(c => c && c.node && c.node.isValid);
|
||||
if (hasValidHeroComps) {
|
||||
const skillNodes = [this.skill_card1, this.skill_card2, this.skill_card3];
|
||||
this.skillCardComps = skillNodes
|
||||
.map(node => node?.getComponent(SCardComp))
|
||||
.filter((comp): comp is SCardComp => !!comp);
|
||||
return;
|
||||
}
|
||||
|
||||
// 清理旧卡槽(如果存在)
|
||||
for (const comp of this.cardComps) {
|
||||
if (comp && comp.node && comp.node.isValid) {
|
||||
@@ -1186,9 +1189,25 @@ export class MissionCardComp extends CCComp {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const node = instantiate(this.cardPrefab);
|
||||
this.cards_node.addChild(node);
|
||||
const comp = node.getComponent(CardComp) || node.addComponent(CardComp);
|
||||
const comp = node.getComponent(CHeroComp) || node.addComponent(CHeroComp);
|
||||
this.cardComps.push(comp);
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "cacheCardComps instantiate", {
|
||||
index: i,
|
||||
nodeName: node.name,
|
||||
nodeActive: node.active,
|
||||
nodeWorldPos: node.worldPosition,
|
||||
parentName: this.cards_node.name,
|
||||
parentActive: this.cards_node.active,
|
||||
parentWorldPos: this.cards_node.worldPosition,
|
||||
cardPrefabValid: this.cardPrefab.isValid
|
||||
});
|
||||
}
|
||||
} else {
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "cacheCardComps failed", {
|
||||
cardPrefabValid: this.cardPrefab?.isValid,
|
||||
cards_nodeValid: this.cards_node?.isValid,
|
||||
cards_nodeActive: this.cards_node?.active
|
||||
});
|
||||
}
|
||||
|
||||
const skillNodes = [this.skill_card1, this.skill_card2, this.skill_card3];
|
||||
@@ -1269,9 +1288,28 @@ export class MissionCardComp extends CCComp {
|
||||
if (this.cardPanNode && this.cardPanNode.isValid) {
|
||||
this.cardPanNode.active = true;
|
||||
this.cardPanNode.setPosition(0, this.cardPanelShowY, 0);
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "enterPreparePhase cardPanNode", {
|
||||
active: this.cardPanNode.active,
|
||||
position: this.cardPanNode.position,
|
||||
worldPos: this.cardPanNode.worldPosition,
|
||||
parentName: this.cardPanNode.parent?.name,
|
||||
parentActive: this.cardPanNode.parent?.active,
|
||||
parentWorldPos: this.cardPanNode.parent?.worldPosition,
|
||||
cards_nodeActive: this.cards_node?.active,
|
||||
cards_nodeWorldPos: this.cards_node?.worldPosition,
|
||||
cards_nodeChildren: this.cards_node?.children.length
|
||||
});
|
||||
}
|
||||
// 滑动到英雄面板
|
||||
this.slideToPanel(0);
|
||||
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "enterPreparePhase herosPanNode", {
|
||||
herosPanNodeActive: this.herosPanNode?.active,
|
||||
herosPanNodePos: this.herosPanNode?.position,
|
||||
herosPanNodeWorldPos: this.herosPanNode?.worldPosition,
|
||||
currentPanelIndex: this.currentPanelIndex,
|
||||
panelSlideWidth: this.panelSlideWidth
|
||||
});
|
||||
}
|
||||
|
||||
private enterBattlePhase() {
|
||||
@@ -1287,123 +1325,42 @@ export class MissionCardComp extends CCComp {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建本次抽卡结果,保证最终可分发 3 条数据。
|
||||
* 构建本次抽卡结果,返回 3 张英雄卡。
|
||||
*
|
||||
* 抽卡规则:
|
||||
* 1. **必出规则**:当场上有可升级英雄时,每次刷新必出 1 张升级卡,并放在返回数组首位(对应卡池最左侧槽位)。
|
||||
* 多次刷新时,按场上英雄 target_hero_eid 升序循环切换升级卡(upgradeRotationIndex)。
|
||||
* 2. 其余 2 张从混合池(剩余升级卡 + 基础英雄卡)按权重抽取。
|
||||
* 规则:
|
||||
* 1. 从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复。
|
||||
* 2. 过滤掉场上已召唤的英雄(已召唤的英雄不再刷出,仅通过升级卡升级)。
|
||||
* 3. 不足 3 张时循环补齐(允许重复)。
|
||||
*
|
||||
* 特殊规则:当场存活英雄数已达 HERO_MAX_NUM 时,全部出升级卡(若全部已满级则降级回混合池)。
|
||||
* 注:升级卡机制已移除,英雄卡池只出普通英雄卡。
|
||||
*/
|
||||
private buildDrawCards(): CardConfig[] {
|
||||
const upgradeCards = this.buildHeroUpgradeCards();
|
||||
const aliveHeroCount = this.getAliveHeroCount();
|
||||
const heroMax = this.getMissionHeroMaxNum();
|
||||
|
||||
// 英雄已满员且有可升级的英雄 → 只出升级卡
|
||||
if (aliveHeroCount >= heroMax && upgradeCards.length > 0) {
|
||||
const picked = this.pickMixedCards(upgradeCards, 3, upgradeCards);
|
||||
if (picked.length >= 3) return picked.slice(0, 3);
|
||||
/** 兜底:不足 3 张时循环补齐 */
|
||||
const filled = [...picked];
|
||||
while (filled.length < 3) {
|
||||
filled.push(upgradeCards[filled.length % upgradeCards.length]);
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero);
|
||||
// 过滤掉场上已召唤英雄的普通英雄卡:已召唤的英雄只能通过升级卡升级,不再刷出普通卡
|
||||
const aliveHeroUuids = this.getAliveHeroUuids();
|
||||
const availableHeroCards = heroCards.filter(c => !aliveHeroUuids.has(c.uuid));
|
||||
|
||||
// 英雄池只刷英雄卡(含动态升级卡 + 未召唤英雄的普通卡)
|
||||
const mixedPool: CardConfig[] = [...upgradeCards, ...availableHeroCards];
|
||||
if (mixedPool.length === 0) return [];
|
||||
if (availableHeroCards.length === 0) return [];
|
||||
|
||||
const result: CardConfig[] = [];
|
||||
const usedUpgradeTargets = new Set<number>();
|
||||
const usedHeroUuids = new Set<number>();
|
||||
const usedUuids = new Set<number>();
|
||||
|
||||
// 必出 1 张升级卡,按 UUID 升序轮询,放在返回数组首位(对应最左侧卡槽)
|
||||
if (upgradeCards.length > 0) {
|
||||
const idx = this.upgradeRotationIndex % upgradeCards.length;
|
||||
this.upgradeRotationIndex = (this.upgradeRotationIndex + 1) % upgradeCards.length;
|
||||
const mandatory = upgradeCards[idx];
|
||||
result.push(mandatory);
|
||||
usedUpgradeTargets.add(mandatory.target_hero_eid ?? 0);
|
||||
}
|
||||
|
||||
// 剩余张数从混合池抽取,排除已选升级卡 target_hero_eid 和已选英雄卡 uuid
|
||||
const remainingCount = 3 - result.length;
|
||||
if (remainingCount > 0) {
|
||||
const remainingPool = mixedPool.filter(c => {
|
||||
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
|
||||
return !usedUpgradeTargets.has(c.target_hero_eid);
|
||||
}
|
||||
if (c.type === CardType.Hero) {
|
||||
return !usedHeroUuids.has(c.uuid);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const rest = this.pickMixedCards(remainingPool, remainingCount, upgradeCards);
|
||||
rest.forEach(c => {
|
||||
result.push(c);
|
||||
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
|
||||
usedUpgradeTargets.add(c.target_hero_eid);
|
||||
}
|
||||
if (c.type === CardType.Hero) {
|
||||
usedHeroUuids.add(c.uuid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 兜底:不足 3 张时从未使用的英雄卡中补齐,避免出现重复英雄
|
||||
// 按权重抽取,同一次抽卡内英雄不重复
|
||||
while (result.length < 3) {
|
||||
const fillCard = availableHeroCards.find(c => !usedHeroUuids.has(c.uuid));
|
||||
if (!fillCard) break;
|
||||
result.push(fillCard);
|
||||
usedHeroUuids.add(fillCard.uuid);
|
||||
const pool = availableHeroCards.filter(c => !usedUuids.has(c.uuid));
|
||||
if (pool.length === 0) break;
|
||||
const pick = this.weightedPick(pool);
|
||||
if (!pick) break;
|
||||
result.push(pick);
|
||||
usedUuids.add(pick.uuid);
|
||||
}
|
||||
|
||||
// 兜底:不足 3 张时循环补齐(允许重复)
|
||||
while (result.length < 3) {
|
||||
result.push(availableHeroCards[result.length % availableHeroCards.length]);
|
||||
}
|
||||
return result.slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从混合池中按权重抽取 n 张卡。
|
||||
* 升级卡之间强制 unique(同一个 target_hero_eid 只能出现一次)。
|
||||
* 英雄卡之间强制 unique(同一个 uuid 只能出现一次),避免同一次发牌出现重复英雄。
|
||||
*/
|
||||
private pickMixedCards(pool: CardConfig[], count: number, upgradeCards: CardConfig[]): CardConfig[] {
|
||||
if (pool.length === 0 || count <= 0) return [];
|
||||
const selected: CardConfig[] = [];
|
||||
const usedUpgradeTargets = new Set<number>();
|
||||
const usedHeroUuids = new Set<number>();
|
||||
|
||||
while (selected.length < count) {
|
||||
const available = pool.filter(c => {
|
||||
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
|
||||
return !usedUpgradeTargets.has(c.target_hero_eid);
|
||||
}
|
||||
if (c.type === CardType.Hero) {
|
||||
return !usedHeroUuids.has(c.uuid);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (available.length === 0) break;
|
||||
const pick = this.weightedPick(available);
|
||||
if (!pick) break;
|
||||
selected.push(pick);
|
||||
if (pick.type === CardType.SpecialUpgrade && pick.target_hero_eid) {
|
||||
usedUpgradeTargets.add(pick.target_hero_eid);
|
||||
}
|
||||
if (pick.type === CardType.Hero) {
|
||||
usedHeroUuids.add(pick.uuid);
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
/** 单次按权重抽取一张卡 */
|
||||
private weightedPick(cards: CardConfig[]): CardConfig | null {
|
||||
if (cards.length === 0) return null;
|
||||
@@ -1416,38 +1373,6 @@ export class MissionCardComp extends CCComp {
|
||||
return cards[cards.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描场上存活英雄,为每个英雄实体生成一张升级卡(按 eid 绑定)。
|
||||
* - 已达 HERO_MAX_LV 的英雄不出卡。
|
||||
* - 同 eid 只生成一张(eid 本就唯一,不会重复)。
|
||||
* - cost = 模板 cost + (当前等级 - 1) * BASE_COST,等级越高升级越贵。
|
||||
*/
|
||||
private buildHeroUpgradeCards(): CardConfig[] {
|
||||
const template = SpecialUpgradeCardList[7001];
|
||||
if (!template) return [];
|
||||
|
||||
const actors = this.queryAliveHeroActors();
|
||||
if (actors.length === 0) return [];
|
||||
|
||||
const result: CardConfig[] = [];
|
||||
for (const actor of actors) {
|
||||
const eid = actor.eid;
|
||||
const lv = actor.model.lv;
|
||||
if (lv >= FightSet.HERO_MAX_LV) continue; // 已达上限不再出升级卡
|
||||
const dynamicCost = template.cost + (lv - 1) * FightSet.BASE_COST;
|
||||
result.push({
|
||||
...template,
|
||||
cost: dynamicCost,
|
||||
weight: template.weight,
|
||||
target_hero_eid: eid,
|
||||
hero_lv: lv + 1,
|
||||
});
|
||||
}
|
||||
// 按 target_hero_eid 升序排序,保证返回顺序稳定(便于 buildDrawCards 按顺序轮询)
|
||||
result.sort((a, b) => (a.target_hero_eid ?? 0) - (b.target_hero_eid ?? 0));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建技能卡抽卡结果,返回 3 张。
|
||||
*
|
||||
@@ -1485,16 +1410,23 @@ export class MissionCardComp extends CCComp {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 全量分发给 3 槽;每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
|
||||
/** 全量分发给 3 槽;每个槽位是否接收由 CHeroComp 自己判断(锁定可跳过) */
|
||||
private dispatchCardsToSlots(cards: CardConfig[]) {
|
||||
if (!this.cardComps) return;
|
||||
for (let i = 0; i < this.cardComps.length; i++) {
|
||||
if (this.cardComps[i]) {
|
||||
const accepted = this.cardComps[i].applyDrawCard(cards[i] ?? null);
|
||||
const node = this.cardComps[i].node;
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "dispatch card", {
|
||||
index: i,
|
||||
card: cards[i]?.uuid ?? 0,
|
||||
accepted
|
||||
accepted,
|
||||
nodeActive: node?.active,
|
||||
nodeWorldPos: node?.worldPosition,
|
||||
nodeScale: node?.scale,
|
||||
parentName: node?.parent?.name,
|
||||
parentActive: node?.parent?.active,
|
||||
parentWorldPos: node?.parent?.worldPosition
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1514,18 +1446,7 @@ export class MissionCardComp extends CCComp {
|
||||
}
|
||||
|
||||
private layoutCardSlots() {
|
||||
if (!this.cardComps) return;
|
||||
const count = this.cardComps.length;
|
||||
if (count === 0) return;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (this.cardComps[i]) {
|
||||
this.cardComps[i].setSlotPosition(this.cardsPos[i]);
|
||||
}
|
||||
}
|
||||
mLogger.log(this.debugMode, "MissionCardComp", "layout card slots", {
|
||||
count,
|
||||
cardWidth: this.cardWidth
|
||||
});
|
||||
// 布局由 cHero.prefab 根节点 Widget 约束控制,代码不再覆盖位置
|
||||
}
|
||||
|
||||
private playButtonPressAnim(node: Node | null) {
|
||||
@@ -1670,7 +1591,7 @@ export class MissionCardComp extends CCComp {
|
||||
|
||||
/**
|
||||
* 获取场上所有存活英雄的 hero_uuid 集合。
|
||||
* 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出,只能通过升级卡升级。
|
||||
* 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出。
|
||||
*/
|
||||
private getAliveHeroUuids(): Set<number> {
|
||||
const uuids = new Set<number>();
|
||||
@@ -1805,8 +1726,8 @@ export class MissionCardComp extends CCComp {
|
||||
this.resetButtonScale(this.cards_chou);
|
||||
|
||||
// 关键:在 reset/销毁 时将 Map 置空,彻底切断引用
|
||||
this.cardComps = [] as any;
|
||||
this.skillCardComps = [] as any;
|
||||
this.cardComps = [];
|
||||
this.skillCardComps = [];
|
||||
this.purchasedEquipUuids.clear();
|
||||
this.purchasedItemUuids.clear();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* 由 MissSkillsComp 接收并创建 SBox 实体 / SkillBoxComp 执行技能逻辑。
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType } from "cc";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, UIOpacity, tween, Tween } 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 { CardConfig } from "../common/config/CardSet";
|
||||
@@ -73,17 +73,28 @@ export class SCardComp extends CCComp {
|
||||
private cardData: CardConfig | null = null;
|
||||
/** 是否已购买 */
|
||||
private isPurchased: boolean = false;
|
||||
/** 透明度组件(用于淡入淡出动画) */
|
||||
private opacityComp: UIOpacity | null = null;
|
||||
/** 入场动画时长(秒) */
|
||||
private readonly enterDuration: number = 0.25;
|
||||
/** 购买/逝去动画时长(秒) */
|
||||
private readonly exitDuration: number = 0.2;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad() {
|
||||
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.buy_node && this.buy_node.isValid) {
|
||||
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +103,41 @@ export class SCardComp extends CCComp {
|
||||
/**
|
||||
* 初始化技能卡数据并渲染 UI
|
||||
*
|
||||
* 若槽位已有旧卡且显示中,先播普通逝去动画(缩小淡出),结束后切新卡;
|
||||
* 刚购买的槽位跳过逝去动画直接应用新卡(购买动画已播放大淡出)。
|
||||
*
|
||||
* @param data 技能卡配置,传 null 时清空显示
|
||||
*/
|
||||
applyCardData(data: CardConfig | null): void {
|
||||
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
|
||||
if (this.isPurchased) {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
if (data) {
|
||||
this.node.active = true;
|
||||
this.updateUI();
|
||||
} else {
|
||||
this.applyEmptyUI();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 旧卡存在且显示中 → 播普通逝去动画,结束后切新卡
|
||||
if (this.cardData && this.node.active) {
|
||||
this.playRefreshExitAnim(() => {
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
if (data) {
|
||||
this.node.active = true;
|
||||
this.updateUI();
|
||||
} else {
|
||||
this.applyEmptyUI();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次赋值或空槽 → 直接渲染
|
||||
this.cardData = data;
|
||||
this.isPurchased = false;
|
||||
if (data) {
|
||||
@@ -107,6 +150,14 @@ export class SCardComp extends CCComp {
|
||||
|
||||
/** 清空 UI 显示 */
|
||||
private applyEmptyUI() {
|
||||
// 清理动画状态
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
this.opacityComp.opacity = 255;
|
||||
}
|
||||
this.node.setScale(1, 1, 1);
|
||||
|
||||
if (this.name_label) this.name_label.string = "";
|
||||
if (this.level_label) this.level_label.string = "";
|
||||
const fieldLabels = [this.fied1, this.fied2, this.fied3];
|
||||
@@ -171,6 +222,107 @@ export class SCardComp extends CCComp {
|
||||
costLabel.string = `${this.cardData.cost ?? 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放入场动画(由小变大 + 淡入)
|
||||
this.playEnterAnim();
|
||||
}
|
||||
|
||||
// ======================== 动画 ========================
|
||||
|
||||
/** 播放入场动画:卡牌由小变大(scale 0→1)+ 淡入 */
|
||||
private playEnterAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
this.node.setScale(0, 0, 1);
|
||||
if (this.opacityComp) {
|
||||
this.opacityComp.opacity = 0;
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.enterDuration, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.enterDuration, { opacity: 255 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/** 播放购买动画:卡牌放大(scale 1→1.1)+ 淡出 */
|
||||
private playPurchaseAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.exitDuration, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.exitDuration, { opacity: 0 })
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放普通刷新"逝去"动画:旧卡缩小(scale 1→0)+ 淡出,结束后回调切换新卡。
|
||||
* @param onComplete 动画结束回调,用于应用新卡数据
|
||||
*/
|
||||
private playRefreshExitAnim(onComplete: () => void) {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
Tween.stopAllByTarget(this.opacityComp);
|
||||
}
|
||||
tween(this.node)
|
||||
.to(this.exitDuration, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
|
||||
.start();
|
||||
if (this.opacityComp) {
|
||||
tween(this.opacityComp)
|
||||
.to(this.exitDuration, { opacity: 0 })
|
||||
.call(onComplete)
|
||||
.start();
|
||||
} else {
|
||||
this.scheduleOnce(onComplete, this.exitDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 按钮动画 ========================
|
||||
|
||||
/** 按钮正常缩放 */
|
||||
private readonly btnNormalScale: number = 1;
|
||||
/** 按钮按下缩放 */
|
||||
private readonly btnPressScale: number = 0.92;
|
||||
/** 按钮点击回弹峰值缩放 */
|
||||
private readonly btnClickScale: number = 1.08;
|
||||
|
||||
/** 购买按钮按下:缩小反馈 */
|
||||
private onBuyTouchStart() {
|
||||
this.playBtnScaleTo(this.btnPressScale, 0.06);
|
||||
}
|
||||
|
||||
/** 购买按钮取消:恢复缩放 */
|
||||
private onBuyTouchCancel() {
|
||||
this.playBtnScaleTo(this.btnNormalScale, 0.08);
|
||||
}
|
||||
|
||||
/** 按钮缩放动画(通用) */
|
||||
private playBtnScaleTo(scale: number, duration: number) {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(duration, { scale: new Vec3(scale, scale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
|
||||
private playBtnClickAnim() {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
|
||||
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 获取技能描述:驻场技能取 FieldSkillSet,其余取技能/卡牌配置 */
|
||||
@@ -214,6 +366,9 @@ export class SCardComp extends CCComp {
|
||||
private onBuyClick() {
|
||||
if (!this.cardData || this.isPurchased) return;
|
||||
|
||||
// 播放按钮点击回弹动画
|
||||
this.playBtnClickAnim();
|
||||
|
||||
oops.audio.playEffect("music/button");
|
||||
|
||||
// 扣费
|
||||
@@ -239,6 +394,9 @@ export class SCardComp extends CCComp {
|
||||
this.buy_node.active = false;
|
||||
}
|
||||
|
||||
// 播放购买动画(卡牌放大 + 淡出)
|
||||
this.playPurchaseAnim();
|
||||
|
||||
mLogger.log(this.debugMode, "SCardComp", "skill card purchased", {
|
||||
uuid: this.cardData.uuid,
|
||||
cost
|
||||
|
||||
Reference in New Issue
Block a user