- 将 playButtonPressAnim、playButtonClickAnim 和 playButtonResetAnim 中的动画逻辑提取为通用方法 playNodeScaleTo 和 playNodeScalePop - 调整 playCoinChangeAnim 以使用新的动画方法,并分别对图标和数字应用动画 - 清理属性声明顺序,移除已弃用的 tooltip 注释
708 lines
25 KiB
TypeScript
708 lines
25 KiB
TypeScript
import { mLogger } from "../common/Logger";
|
||
import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, SpriteAtlas, Tween, tween, Vec3 } 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 { GameEvent } from "../common/config/GameEvent";
|
||
import { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CardConfig, CardsUpSet, getCardsByLv } from "../common/config/CardSet";
|
||
import { CardComp } from "./CardComp";
|
||
import { oops } from "db://oops-framework/core/Oops";
|
||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||
import { HInfoComp } from "./HInfoComp";
|
||
import { smc } from "../common/SingletonModuleComp";
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
|
||
/** 视图层对象 */
|
||
@ccclass('MissionCardComp')
|
||
@ecs.register('MissionCard', false)
|
||
export class MissionCardComp extends CCComp {
|
||
private debugMode: boolean = true;
|
||
private readonly cardWidth: number = 175;
|
||
private readonly buttonNormalScale: number = 1;
|
||
private readonly buttonPressScale: number = 0.94;
|
||
private readonly buttonClickScale: number = 1.06;
|
||
refreshCost: number = 1;
|
||
cardsPanelMoveDuration: number = 0.2;
|
||
|
||
/** 四个插卡槽位(固定顺序分发:1~4) */
|
||
@property(Node)
|
||
cards_node:Node = null!
|
||
@property(Node)
|
||
card1:Node = null!
|
||
@property(Node)
|
||
card2:Node = null!
|
||
@property(Node)
|
||
card3:Node = null!
|
||
@property(Node)
|
||
card4:Node = null!
|
||
@property(Node)
|
||
cards_chou:Node = null!
|
||
@property(Node)
|
||
cards_up:Node = null!
|
||
@property(Node)
|
||
coins_node:Node = null!
|
||
@property(Node)
|
||
pool_lv_node:Node = null!
|
||
@property(Node)
|
||
hero_info_node:Node = null! //场上英雄信息面板所在节点
|
||
@property(Prefab)
|
||
hero_info_prefab:Prefab=null! //场上英雄信息面板Prefab
|
||
@property(Node)
|
||
hero_num_node:Node=null!
|
||
|
||
/** 预留图集缓存(后续接入按钮/卡面图标时复用) */
|
||
private uiconsAtlas: SpriteAtlas | null = null;
|
||
/** 四个槽位对应的单卡控制器缓存 */
|
||
private cardComps: CardComp[] = [];
|
||
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
|
||
private poolLv: number = CARD_POOL_INIT_LEVEL;
|
||
private readonly heroInfoItemGap: number = 86;
|
||
private heroInfoSyncTimer: number = 0;
|
||
private hasCachedCardsBaseScale: boolean = false;
|
||
private cardsBaseScale: Vec3 = new Vec3(1, 1, 1);
|
||
private cardsShowScale: Vec3 = new Vec3(1, 1, 1);
|
||
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
|
||
private heroInfoItems: Map<number, {
|
||
node: Node,
|
||
model: HeroAttrsComp,
|
||
comp: HInfoComp
|
||
}> = new Map();
|
||
onLoad() {
|
||
/** 绑定事件 -> 缓存子控制器 -> 初始化UI状态 */
|
||
this.bindEvents();
|
||
this.cacheCardComps();
|
||
this.layoutCardSlots();
|
||
this.initCardsPanelPos();
|
||
this.onMissionStart();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "onLoad init", {
|
||
slots: this.cardComps.length,
|
||
poolLv: this.poolLv
|
||
});
|
||
}
|
||
|
||
onDestroy() {
|
||
this.unbindEvents();
|
||
this.clearHeroInfoPanels();
|
||
}
|
||
init(){
|
||
this.onMissionStart();
|
||
}
|
||
|
||
/** 任务开始时:重置卡池等级、清空4槽、显示面板 刷新一次卡池*/
|
||
onMissionStart() {
|
||
this.enterPreparePhase();
|
||
this.poolLv = CARD_POOL_INIT_LEVEL;
|
||
const missionData = this.getMissionData();
|
||
if (missionData) {
|
||
missionData.coin = Math.max(0, Math.floor(missionData.coin ?? 0));
|
||
missionData.hero_num = 0;
|
||
missionData.hero_max_num = 5;
|
||
missionData.hero_extend_max_num = 6;
|
||
}
|
||
this.clearHeroInfoPanels();
|
||
this.layoutCardSlots();
|
||
this.clearAllCards();
|
||
if (this.cards_up) {
|
||
this.cards_up.active = true;
|
||
}
|
||
this.resetButtonScale(this.cards_chou);
|
||
this.resetButtonScale(this.cards_up);
|
||
this.updateCoinAndCostUI();
|
||
this.updateHeroNumUI(false, false);
|
||
this.node.active = true;
|
||
const cards = this.buildDrawCards();
|
||
this.dispatchCardsToSlots(cards);
|
||
mLogger.log(this.debugMode, "MissionCardComp", "mission start", {
|
||
poolLv: this.poolLv
|
||
});
|
||
}
|
||
|
||
/** 任务结束时:清空4槽并隐藏面板 */
|
||
onMissionEnd() {
|
||
this.clearAllCards();
|
||
this.clearHeroInfoPanels();
|
||
this.node.active = false;
|
||
}
|
||
start() {
|
||
}
|
||
|
||
update(dt: number) {
|
||
this.heroInfoSyncTimer += dt;
|
||
if (this.heroInfoSyncTimer < 0.15) return;
|
||
this.heroInfoSyncTimer = 0;
|
||
this.refreshHeroInfoPanels();
|
||
}
|
||
|
||
|
||
|
||
/** 关闭面板(不销毁数据模型,仅隐藏) */
|
||
close() {
|
||
this.node.active = false;
|
||
}
|
||
|
||
/** 只处理UI层事件,不做卡牌效果分发 */
|
||
private bindEvents() {
|
||
/** 生命周期事件 */
|
||
this.on(GameEvent.MissionStart, this.onMissionStart, this);
|
||
this.on(GameEvent.MissionEnd, this.onMissionEnd, this);
|
||
this.on(GameEvent.NewWave, this.onNewWave, this);
|
||
this.on(GameEvent.FightStart, this.onFightStart, this);
|
||
oops.message.on(GameEvent.CoinAdd, this.onCoinAdd, this);
|
||
oops.message.on(GameEvent.MasterCalled, this.onMasterCalled, this);
|
||
oops.message.on(GameEvent.HeroDead, this.onHeroDead, this);
|
||
oops.message.on(GameEvent.UseHeroCard, this.onUseHeroCard, this);
|
||
|
||
/** 按钮事件:抽卡与卡池升级 */
|
||
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
||
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
|
||
this.cards_chou?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
|
||
this.cards_up?.on(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
|
||
this.cards_up?.on(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
|
||
this.cards_up?.on(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
|
||
}
|
||
private onCoinAdd(args:any){
|
||
if (args?.syncOnly) {
|
||
this.updateCoinAndCostUI();
|
||
this.playCoinChangeAnim((args?.delta ?? 0) > 0);
|
||
return;
|
||
}
|
||
const v = typeof args === 'number' ? args : (args?.delta ?? args?.value ?? 0);
|
||
if (v === 0) return;
|
||
this.setMissionCoin(this.getMissionCoin() + v);
|
||
this.updateCoinAndCostUI();
|
||
this.playCoinChangeAnim(v > 0);
|
||
}
|
||
|
||
private onFightStart() {
|
||
this.enterBattlePhase();
|
||
}
|
||
|
||
private onNewWave() {
|
||
this.enterPreparePhase();
|
||
this.layoutCardSlots();
|
||
const cards = this.buildDrawCards();
|
||
this.dispatchCardsToSlots(cards);
|
||
}
|
||
|
||
/** 解除按钮监听,避免节点销毁后回调泄漏 */
|
||
private unbindEvents() {
|
||
oops.message.off(GameEvent.CoinAdd, this.onCoinAdd, this);
|
||
oops.message.off(GameEvent.MasterCalled, this.onMasterCalled, this);
|
||
oops.message.off(GameEvent.HeroDead, this.onHeroDead, this);
|
||
oops.message.off(GameEvent.UseHeroCard, this.onUseHeroCard, this);
|
||
this.cards_chou?.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
||
this.cards_chou?.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
|
||
this.cards_chou?.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
|
||
this.cards_up?.off(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
|
||
this.cards_up?.off(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
|
||
this.cards_up?.off(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
|
||
}
|
||
|
||
private onMasterCalled(event: string, args: any) {
|
||
const payload = args ?? event;
|
||
const eid = Number(payload?.eid ?? 0);
|
||
const model = payload?.model as HeroAttrsComp | undefined;
|
||
if (!eid || !model) return;
|
||
const before = this.getAliveHeroCount();
|
||
this.ensureHeroInfoPanel(eid, model);
|
||
const after = this.getAliveHeroCount();
|
||
this.updateHeroNumUI(true, after > before);
|
||
}
|
||
|
||
private onHeroDead() {
|
||
this.refreshHeroInfoPanels();
|
||
this.updateHeroNumUI(true, false);
|
||
}
|
||
|
||
private onUseHeroCard(event: string, args: any) {
|
||
const payload = args ?? event;
|
||
if (!payload) return;
|
||
const current = this.getAliveHeroCount();
|
||
this.syncMissionHeroData(current);
|
||
const heroMax = this.getMissionHeroMaxNum();
|
||
if (current >= heroMax) {
|
||
payload.cancel = true;
|
||
payload.reason = "hero_limit";
|
||
oops.gui.toast(`英雄已满 (${current}/${heroMax})`);
|
||
this.playHeroNumDeniedAnim();
|
||
}
|
||
}
|
||
|
||
private onDrawTouchStart() {
|
||
this.playButtonPressAnim(this.cards_chou);
|
||
}
|
||
|
||
private onDrawTouchEnd() {
|
||
this.playButtonClickAnim(this.cards_chou, () => this.onClickDraw());
|
||
}
|
||
|
||
private onDrawTouchCancel() {
|
||
this.playButtonResetAnim(this.cards_chou);
|
||
}
|
||
|
||
private onUpgradeTouchStart() {
|
||
this.playButtonPressAnim(this.cards_up);
|
||
}
|
||
|
||
private onUpgradeTouchEnd() {
|
||
this.playButtonClickAnim(this.cards_up, () => this.onClickUpgrade());
|
||
}
|
||
|
||
private onUpgradeTouchCancel() {
|
||
this.playButtonResetAnim(this.cards_up);
|
||
}
|
||
|
||
/** 将四个卡槽节点映射为 CardComp,形成固定顺序控制数组 */
|
||
private cacheCardComps() {
|
||
const nodes = [this.card1, this.card2, this.card3, this.card4];
|
||
this.cardComps = nodes
|
||
.map(node => node?.getComponent(CardComp))
|
||
.filter((comp): comp is CardComp => !!comp);
|
||
}
|
||
|
||
/** 抽卡按钮:每次固定抽4张,然后顺序分发给4个单卡脚本 */
|
||
private onClickDraw() {
|
||
const cost = this.getRefreshCost();
|
||
const currentCoin = this.getMissionCoin();
|
||
if (currentCoin < cost) {
|
||
oops.gui.toast(`金币不足,刷新需要${cost}`);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "draw coin not enough", {
|
||
currentCoin,
|
||
cost
|
||
});
|
||
return;
|
||
}
|
||
this.setMissionCoin(currentCoin - cost);
|
||
this.playCoinChangeAnim(false);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "click draw", {
|
||
poolLv: this.poolLv,
|
||
cost,
|
||
leftCoin: this.getMissionCoin()
|
||
});
|
||
this.layoutCardSlots();
|
||
const cards = this.buildDrawCards();
|
||
this.dispatchCardsToSlots(cards);
|
||
}
|
||
|
||
/** 升级按钮:仅提升卡池等级,卡槽是否更新由下一次抽卡触发 */
|
||
private onClickUpgrade() {
|
||
if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool already max", this.poolLv);
|
||
return;
|
||
}
|
||
const cost = this.getUpgradeCost(this.poolLv);
|
||
const currentCoin = this.getMissionCoin();
|
||
if (currentCoin < cost) {
|
||
oops.gui.toast(`金币不足,升级需要${cost}`);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool upgrade coin not enough", {
|
||
poolLv: this.poolLv,
|
||
currentCoin,
|
||
cost
|
||
});
|
||
return;
|
||
}
|
||
this.setMissionCoin(currentCoin - cost);
|
||
this.poolLv += 1;
|
||
this.playCoinChangeAnim(false);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool level up", {
|
||
poolLv: this.poolLv,
|
||
cost,
|
||
leftCoin: this.getMissionCoin()
|
||
});
|
||
}
|
||
|
||
private initCardsPanelPos() {
|
||
if (!this.cards_node || !this.cards_node.isValid) return;
|
||
if (!this.hasCachedCardsBaseScale) {
|
||
const scale = this.cards_node.scale;
|
||
this.cardsBaseScale = new Vec3(scale.x, scale.y, scale.z);
|
||
this.hasCachedCardsBaseScale = true;
|
||
}
|
||
this.cardsShowScale = new Vec3(this.cardsBaseScale.x, this.cardsBaseScale.y, this.cardsBaseScale.z);
|
||
this.cardsHideScale = new Vec3(0, 0, this.cardsBaseScale.z);
|
||
}
|
||
|
||
private enterPreparePhase() {
|
||
if (!this.cards_node || !this.cards_node.isValid) return;
|
||
this.initCardsPanelPos();
|
||
this.cards_node.active = true;
|
||
Tween.stopAllByTarget(this.cards_node);
|
||
this.cards_node.setScale(this.cardsShowScale);
|
||
}
|
||
|
||
private enterBattlePhase() {
|
||
if (!this.cards_node || !this.cards_node.isValid) return;
|
||
this.initCardsPanelPos();
|
||
this.cards_node.active = true;
|
||
Tween.stopAllByTarget(this.cards_node);
|
||
this.cards_node.setScale(this.cardsShowScale);
|
||
tween(this.cards_node)
|
||
.to(this.cardsPanelMoveDuration, {
|
||
scale: this.cardsHideScale
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/** 构建本次抽卡结果,保证最终可分发4条数据 */
|
||
private buildDrawCards(): CardConfig[] {
|
||
const cards = getCardsByLv(this.poolLv);
|
||
/** 正常情况下直接取前4 */
|
||
if (cards.length >= 4) return cards.slice(0, 4);
|
||
/** 兜底:当返回不足4张时循环补齐,保证分发不缺位 */
|
||
const filled = [...cards];
|
||
while (filled.length < 4) {
|
||
const fallback = getCardsByLv(this.poolLv);
|
||
if (fallback.length === 0) break;
|
||
filled.push(fallback[filled.length % fallback.length]);
|
||
}
|
||
return filled;
|
||
}
|
||
|
||
/** 全量分发给4槽;每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
|
||
private dispatchCardsToSlots(cards: CardConfig[]) {
|
||
for (let i = 0; i < this.cardComps.length; i++) {
|
||
const accepted = this.cardComps[i].applyDrawCard(cards[i] ?? null);
|
||
mLogger.log(this.debugMode, "MissionCardComp", "dispatch card", {
|
||
index: i,
|
||
card: cards[i]?.uuid ?? 0,
|
||
accepted
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 系统清空4槽(用于任务切换) */
|
||
private clearAllCards() {
|
||
this.cardComps.forEach(comp => comp.clearBySystem());
|
||
}
|
||
|
||
private layoutCardSlots() {
|
||
const count = this.cardComps.length;
|
||
if (count === 0) return;
|
||
const startX = -((count - 1) * this.cardWidth) / 2;
|
||
for (let i = 0; i < count; i++) {
|
||
const x = startX + i * this.cardWidth;
|
||
this.cardComps[i].setSlotPosition(x);
|
||
}
|
||
mLogger.log(this.debugMode, "MissionCardComp", "layout card slots", {
|
||
count,
|
||
cardWidth: this.cardWidth,
|
||
startX
|
||
});
|
||
}
|
||
|
||
private playButtonPressAnim(node: Node | null) {
|
||
this.playNodeScaleTo(node, this.buttonPressScale, 0.06);
|
||
}
|
||
|
||
private playButtonClickAnim(node: Node | null, onComplete: () => void) {
|
||
if (!node || !node.isValid) {
|
||
onComplete();
|
||
return;
|
||
}
|
||
this.playNodeScalePop(node, this.buttonClickScale, 0.05, 0.08, onComplete);
|
||
}
|
||
|
||
private playButtonResetAnim(node: Node | null) {
|
||
this.playNodeScaleTo(node, this.buttonNormalScale, 0.08);
|
||
}
|
||
|
||
private resetButtonScale(node: Node | null) {
|
||
if (!node || !node.isValid) return;
|
||
Tween.stopAllByTarget(node);
|
||
node.setScale(this.buttonNormalScale, this.buttonNormalScale, 1);
|
||
}
|
||
private canUpPool() {
|
||
if (this.poolLv >= CARD_POOL_MAX_LEVEL) return false;
|
||
const currentCoin = this.getMissionCoin();
|
||
return currentCoin >= this.getUpgradeCost(this.poolLv);
|
||
}
|
||
|
||
private canDrawCards() {
|
||
return this.getMissionCoin() >= this.getRefreshCost();
|
||
}
|
||
/** 更新升级按钮上的等级文案,反馈当前卡池层级 */
|
||
private updatePoolLvUI() {
|
||
if (!this.cards_up) return;
|
||
const nobg = this.cards_up.getChildByName("nobg");
|
||
if (nobg) {
|
||
nobg.active = !this.canUpPool();
|
||
}
|
||
const label = this.cards_up.getChildByName("coin").getChildByName("num").getComponent(Label);
|
||
if (!label) return;
|
||
if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
|
||
label.string = `0`;
|
||
} else {
|
||
label.string = `${this.getUpgradeCost(this.poolLv)}`;
|
||
}
|
||
if (this.pool_lv_node) {
|
||
for (let i = 1; i <= CARD_POOL_MAX_LEVEL; i++) {
|
||
const n = this.pool_lv_node.getChildByName(`lv${i}`);
|
||
if (n) n.active = i === this.poolLv;
|
||
}
|
||
}
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool lv ui update", {
|
||
poolLv: this.poolLv,
|
||
cost: this.getUpgradeCost(this.poolLv)
|
||
});
|
||
}
|
||
|
||
private updateDrawCostUI() {
|
||
if (!this.cards_chou) return;
|
||
const nobg = this.cards_chou.getChildByName("nobg");
|
||
if (nobg) {
|
||
nobg.active = !this.canDrawCards();
|
||
}
|
||
const coinNode = this.cards_chou.getChildByName("coin");
|
||
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
|
||
if (numLabel) {
|
||
numLabel.string = `${this.getRefreshCost()}`;
|
||
}
|
||
}
|
||
|
||
private updateCoinAndCostUI() {
|
||
this.updatePoolLvUI();
|
||
this.updateDrawCostUI();
|
||
}
|
||
|
||
private playCoinChangeAnim(isIncrease: boolean) {
|
||
if (!this.coins_node || !this.coins_node.isValid) return;
|
||
const icon = this.coins_node.getChildByName("icon");
|
||
if (!icon || !icon.isValid ) return;
|
||
const peak = isIncrease ? 1.2 : 1.2;
|
||
this.playHeroNumNodePop(icon, peak);
|
||
const num= this.coins_node.getChildByName("num");
|
||
if (!num || !num.isValid) return;
|
||
this.playHeroNumNodePop(num, peak);
|
||
}
|
||
|
||
private getUpgradeCost(lv: number): number {
|
||
return CardsUpSet[lv] ?? 0;
|
||
}
|
||
|
||
private getRefreshCost(): number {
|
||
return Math.max(0, Math.floor(this.refreshCost));
|
||
}
|
||
|
||
private ensureHeroInfoPanel(eid: number, model: HeroAttrsComp) {
|
||
if (!this.hero_info_node || !this.hero_info_prefab) return;
|
||
this.hero_info_node.active = true;
|
||
const current = this.heroInfoItems.get(eid);
|
||
if (current) {
|
||
current.model = model;
|
||
current.comp.bindData(eid, model);
|
||
this.updateHeroInfoPanel(current);
|
||
return;
|
||
}
|
||
const node = instantiate(this.hero_info_prefab);
|
||
node.parent = this.hero_info_node;
|
||
node.active = true;
|
||
const comp = node.getComponent(HInfoComp);
|
||
if (!comp) {
|
||
node.destroy();
|
||
return;
|
||
}
|
||
const item = {
|
||
node,
|
||
model,
|
||
comp
|
||
};
|
||
comp.bindData(eid, model);
|
||
this.heroInfoItems.set(eid, item);
|
||
this.relayoutHeroInfoPanels();
|
||
this.updateHeroInfoPanel(item);
|
||
}
|
||
|
||
private refreshHeroInfoPanels() {
|
||
const removeKeys: number[] = [];
|
||
this.heroInfoItems.forEach((item, eid) => {
|
||
if (!item.node || !item.node.isValid) {
|
||
removeKeys.push(eid);
|
||
return;
|
||
}
|
||
if (item.model?.is_dead) {
|
||
if (item.node.isValid) item.node.destroy();
|
||
removeKeys.push(eid);
|
||
return;
|
||
}
|
||
if (!item.comp.isModelAlive()) {
|
||
if (item.node.isValid) item.node.destroy();
|
||
removeKeys.push(eid);
|
||
return;
|
||
}
|
||
this.updateHeroInfoPanel(item);
|
||
});
|
||
for (let i = 0; i < removeKeys.length; i++) {
|
||
this.heroInfoItems.delete(removeKeys[i]);
|
||
}
|
||
if (removeKeys.length > 0) {
|
||
this.relayoutHeroInfoPanels();
|
||
}
|
||
this.updateHeroNumUI(false, false);
|
||
}
|
||
|
||
private updateHeroInfoPanel(item: {
|
||
node: Node,
|
||
model: HeroAttrsComp,
|
||
comp: HInfoComp
|
||
}) {
|
||
item.comp.refresh();
|
||
}
|
||
|
||
private relayoutHeroInfoPanels() {
|
||
let index = 0;
|
||
this.heroInfoItems.forEach(item => {
|
||
if (!item.node || !item.node.isValid) return;
|
||
const pos = item.node.position;
|
||
item.node.setPosition(pos.x, -index * this.heroInfoItemGap, pos.z);
|
||
index += 1;
|
||
});
|
||
}
|
||
|
||
private clearHeroInfoPanels() {
|
||
this.heroInfoItems.forEach(item => {
|
||
if (item.node && item.node.isValid) {
|
||
item.node.destroy();
|
||
}
|
||
});
|
||
this.heroInfoItems.clear();
|
||
if (this.hero_info_node && this.hero_info_node.isValid) {
|
||
for (let i = this.hero_info_node.children.length - 1; i >= 0; i--) {
|
||
const child = this.hero_info_node.children[i];
|
||
if (child && child.isValid) child.destroy();
|
||
}
|
||
}
|
||
this.heroInfoSyncTimer = 0;
|
||
this.syncMissionHeroData(0);
|
||
this.updateHeroNumUI(false, false);
|
||
}
|
||
|
||
public setHeroMaxCount(max: number) {
|
||
const missionData = this.getMissionData();
|
||
if (!missionData) return;
|
||
const min = 5;
|
||
const limit = Math.max(min, missionData.hero_extend_max_num ?? 6);
|
||
const next = Math.max(min, Math.min(limit, Math.floor(max || min)));
|
||
if (next === missionData.hero_max_num) return;
|
||
missionData.hero_max_num = next;
|
||
this.updateHeroNumUI(true, false);
|
||
}
|
||
|
||
public tryExpandHeroMax(add: number = 1): boolean {
|
||
const missionData = this.getMissionData();
|
||
if (!missionData) return false;
|
||
const before = this.getMissionHeroMaxNum();
|
||
const next = before + Math.max(0, Math.floor(add));
|
||
this.setHeroMaxCount(next);
|
||
return this.getMissionHeroMaxNum() > before;
|
||
}
|
||
|
||
public canUseHeroCard(): boolean {
|
||
return this.getAliveHeroCount() < this.getMissionHeroMaxNum();
|
||
}
|
||
|
||
private getAliveHeroCount(): number {
|
||
let count = 0;
|
||
this.heroInfoItems.forEach(item => {
|
||
if (!item?.node || !item.node.isValid) return;
|
||
if (!item.comp?.isModelAlive()) return;
|
||
if (item.model?.is_dead) return;
|
||
count += 1;
|
||
});
|
||
return count;
|
||
}
|
||
|
||
private updateHeroNumUI(animate: boolean, isIncrease: boolean) {
|
||
this.syncMissionHeroData();
|
||
if (!animate || !isIncrease) return;
|
||
this.playHeroNumGainAnim();
|
||
}
|
||
|
||
private playHeroNumGainAnim() {
|
||
if (!this.hero_num_node || !this.hero_num_node.isValid) return;
|
||
const iconNode = this.hero_num_node.getChildByName("icon");
|
||
const numNode = this.hero_num_node.getChildByName("num");
|
||
this.playHeroNumNodePop(iconNode, 1.2);
|
||
this.playHeroNumNodePop(numNode, 1.2);
|
||
}
|
||
|
||
private playHeroNumDeniedAnim() {
|
||
if (!this.hero_num_node || !this.hero_num_node.isValid) return;
|
||
const iconNode = this.hero_num_node.getChildByName("icon");
|
||
const numNode = this.hero_num_node.getChildByName("num");
|
||
this.playHeroNumNodePop(iconNode, 1.2);
|
||
this.playHeroNumNodePop(numNode, 1.2);
|
||
}
|
||
|
||
private playHeroNumNodePop(node: Node | null, scalePeak: number) {
|
||
this.playNodeScalePop(node, scalePeak, 0.08, 0.1);
|
||
}
|
||
|
||
private playNodeScaleTo(node: Node | null, scale: number, duration: number) {
|
||
if (!node || !node.isValid) return;
|
||
Tween.stopAllByTarget(node);
|
||
tween(node)
|
||
.to(duration, {
|
||
scale: new Vec3(scale, scale, 1)
|
||
})
|
||
.start();
|
||
}
|
||
|
||
private playNodeScalePop(node: Node | null, scalePeak: number, toPeakDuration: number, toNormalDuration: number, onPeak?: () => void) {
|
||
if (!node || !node.isValid) return;
|
||
Tween.stopAllByTarget(node);
|
||
node.setScale(1, 1, 1);
|
||
const seq = tween(node)
|
||
.to(toPeakDuration, { scale: new Vec3(scalePeak, scalePeak, 1) });
|
||
if (onPeak) {
|
||
seq.call(onPeak);
|
||
}
|
||
seq.to(toNormalDuration, { scale: new Vec3(1, 1, 1) })
|
||
.start();
|
||
}
|
||
|
||
private getMissionData(): any {
|
||
return smc?.vmdata?.mission_data ?? null;
|
||
}
|
||
|
||
private getMissionHeroNum(): number {
|
||
const missionData = this.getMissionData();
|
||
return Math.max(0, Math.floor(missionData?.hero_num ?? 0));
|
||
}
|
||
|
||
private getMissionCoin(): number {
|
||
const missionData = this.getMissionData();
|
||
return Math.max(0, Math.floor(missionData?.coin ?? 0));
|
||
}
|
||
|
||
private setMissionCoin(value: number) {
|
||
const missionData = this.getMissionData();
|
||
if (!missionData) return;
|
||
missionData.coin = Math.max(0, Math.floor(value));
|
||
}
|
||
|
||
private getMissionHeroMaxNum(): number {
|
||
const missionData = this.getMissionData();
|
||
return Math.max(5, Math.floor(missionData?.hero_max_num ?? 5));
|
||
}
|
||
|
||
private syncMissionHeroData(count?: number) {
|
||
const missionData = this.getMissionData();
|
||
if (!missionData) return;
|
||
const safeCount = Math.max(0, Math.floor(count ?? this.getAliveHeroCount()));
|
||
missionData.hero_num = safeCount;
|
||
}
|
||
|
||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||
reset() {
|
||
this.clearHeroInfoPanels();
|
||
this.resetButtonScale(this.cards_chou);
|
||
this.resetButtonScale(this.cards_up);
|
||
this.node.destroy();
|
||
}
|
||
}
|