Files
pixelheros/assets/script/game/map/MissionCardComp.ts
walkpan 4e7c580936 fix: 修复新波次开始后游戏阶段状态错误的问题
- 在 MissionComp 中新增波次时调用 enterPreparePhase 进入准备阶段
- 在 MissionMonComp 中检查战斗状态,避免波次结束后继续生成怪物
- 在 MissionCardComp 中监听新波次事件并进入准备阶段
2026-03-27 08:39:07 +08:00

663 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/** 四个插卡槽位固定顺序分发1~4 */
@property(Node)
cards_node:Node = null!
@property({ tooltip: "战斗阶段卡牌面板下移隐藏距离" })
cardsBattleHideOffsetY: number = 1280;
@property({ tooltip: "卡牌面板位移动画时长" })
cardsPanelMoveDuration: number = 0.2;
@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 cardsShowPos: Vec3 = new Vec3();
private cardsHidePos: Vec3 = new Vec3();
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.updatePoolLvUI();
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);
this.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.updatePoolLvUI();
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.updatePoolLvUI();
this.playCoinChangeAnim(v > 0);
}
private onFightStart() {
this.enterBattlePhase();
}
private onNewWave() {
this.enterPreparePhase();
}
/** 解除按钮监听,避免节点销毁后回调泄漏 */
private unbindEvents() {
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() {
mLogger.log(this.debugMode, "MissionCardComp", "click draw", {
poolLv: this.poolLv
});
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.updatePoolLvUI();
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.updatePoolLvUI();
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;
const pos = this.cards_node.position;
const parentScaleY = Math.max(0.001, Math.abs(this.cards_node.parent?.scale?.y ?? 1));
const localOffsetY = Math.abs(this.cardsBattleHideOffsetY) / parentScaleY;
this.cardsShowPos = new Vec3(pos.x, pos.y, pos.z);
this.cardsHidePos = new Vec3(pos.x, pos.y - localOffsetY, pos.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.setPosition(this.cardsShowPos);
}
private enterBattlePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
this.cards_node.active = true;
Tween.stopAllByTarget(this.cards_node);
tween(this.cards_node)
.to(this.cardsPanelMoveDuration, {
position: this.cardsHidePos
})
.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) {
if (!node || !node.isValid) return;
Tween.stopAllByTarget(node);
tween(node)
.to(0.06, {
scale: new Vec3(this.buttonPressScale, this.buttonPressScale, 1)
})
.start();
}
private playButtonClickAnim(node: Node | null, onComplete: () => void) {
if (!node || !node.isValid) {
onComplete();
return;
}
Tween.stopAllByTarget(node);
tween(node)
.to(0.05, {
scale: new Vec3(this.buttonClickScale, this.buttonClickScale, 1)
})
.call(onComplete)
.to(0.08, {
scale: new Vec3(this.buttonNormalScale, this.buttonNormalScale, 1)
})
.start();
}
private playButtonResetAnim(node: Node | null) {
if (!node || !node.isValid) return;
Tween.stopAllByTarget(node);
tween(node)
.to(0.08, {
scale: new Vec3(this.buttonNormalScale, this.buttonNormalScale, 1)
})
.start();
}
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 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 playCoinChangeAnim(isIncrease: boolean) {
if (!this.coins_node || !this.coins_node.isValid) return;
const target = this.coins_node.getChildByName("num") || this.coins_node;
if (!target || !target.isValid) return;
const peak = isIncrease ? 1.16 : 1.08;
Tween.stopAllByTarget(target);
target.setScale(1, 1, 1);
tween(target)
.to(0.08, { scale: new Vec3(peak, peak, 1) })
.to(0.1, { scale: new Vec3(1, 1, 1) })
.start();
}
private getUpgradeCost(lv: number): number {
return CardsUpSet[lv] ?? 0;
}
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.16);
this.playHeroNumNodePop(numNode, 1.16);
}
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.1);
this.playHeroNumNodePop(numNode, 1.1);
}
private playHeroNumNodePop(node: Node | null, scalePeak: number) {
if (!node || !node.isValid) return;
Tween.stopAllByTarget(node);
node.setScale(1, 1, 1);
tween(node)
.to(0.08, { scale: new Vec3(scalePeak, scalePeak, 1) })
.to(0.1, { 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();
}
}