docs: 为游戏地图模块添加详细的代码注释
为游戏地图模块的脚本文件添加全面的注释,说明每个组件的职责、关键设计、依赖关系和使用方式。注释覆盖了英雄信息面板、技能卡槽位管理器、排行榜弹窗、卡牌控制器、背景滚动组件等核心功能模块,提高了代码的可读性和维护性。 同时修复了英雄预制体的激活状态和技能效果预制体的尺寸参数。
This commit is contained in:
@@ -1,3 +1,24 @@
|
||||
/**
|
||||
* @file CardComp.ts
|
||||
* @description 单张卡牌槽位组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理单个卡牌槽位的 **显示** 和 **交互**(触摸拖拽 / 点击使用 / 锁定切换)。
|
||||
* 2. 接收来自 MissionCardComp 分发的 CardConfig 数据,渲染对应卡面(英雄 / 技能 / 特殊卡)。
|
||||
* 3. 在玩家触发"使用卡牌"操作时,执行 **费用扣除 → 动画表现 → 效果分发** 的完整流程。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 槽位可 **锁定**:锁定后刷新卡池不会覆盖旧卡,由 `isLocked` 控制。
|
||||
* - 卡牌使用支持 **上划手势** 与 **点击** 两种触发方式。
|
||||
* - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;技能 / 特殊卡使用 SpriteAtlas 静态图标。
|
||||
* - 通过 `iconVisualToken` 防止异步加载资源回调与当前显示不一致(竞态保护)。
|
||||
*
|
||||
* 依赖:
|
||||
* - CardConfig / CardType / CKind 等卡牌数据结构(CardSet)
|
||||
* - HeroInfo(heroSet)、SkillSet(SkillSet)—— 用于渲染卡面信息
|
||||
* - GameEvent 事件总线 —— 分发 CallHero / UseSkillCard / UseSpecialCard 等效果
|
||||
* - smc.vmdata.mission_data —— 读写局内金币
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources, Light } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
@@ -14,55 +35,99 @@ import { UIID } from "../common/config/GameUIConfig";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* CardComp —— 单张卡牌槽位视图组件
|
||||
*
|
||||
* 挂载在每个卡牌槽位节点上,由 MissionCardComp 统一管理 4 个实例。
|
||||
* 负责单卡的渲染、交互、动画以及使用逻辑。
|
||||
*/
|
||||
@ccclass('CardComp')
|
||||
@ecs.register('CardComp', false)
|
||||
export class CardComp extends CCComp {
|
||||
/** 是否开启调试日志 */
|
||||
private debugMode: boolean = true;
|
||||
|
||||
// ======================== 编辑器绑定节点 ========================
|
||||
|
||||
/** 锁定态图标节点(显示时表示本槽位锁定) */
|
||||
@property(Node)
|
||||
Lock: Node = null!
|
||||
/** 解锁态图标节点(显示时表示本槽位未锁定,可点击上锁) */
|
||||
@property(Node)
|
||||
unLock: Node = null!
|
||||
/** 英雄卡信息面板(显示 AP / HP) */
|
||||
@property(Node)
|
||||
info_node=null!
|
||||
/** 非英雄卡信息面板(显示技能 / 特殊卡描述文本) */
|
||||
@property(Node)
|
||||
oinfo_node=null!
|
||||
/** 卡牌名称标签节点 */
|
||||
@property(Node)
|
||||
name_node=null!
|
||||
/** 卡牌图标节点(英雄动画 / 技能图标) */
|
||||
@property(Node)
|
||||
icon_node=null!
|
||||
/** 费用显示节点 */
|
||||
@property(Node)
|
||||
cost_node=null!
|
||||
/** 卡牌种类标识节点(如近战 / 远程 / 辅助等分类子节点的容器) */
|
||||
@property(Node)
|
||||
Ckind_node=null!
|
||||
/** 卡牌背景底框节点(按卡池等级切换子节点显示) */
|
||||
@property(Node)
|
||||
BG_node=null!
|
||||
/** 普通品质边框节点(hero_lv / card_lv <= 1 时使用) */
|
||||
@property(Node)
|
||||
NF_node=null!
|
||||
/** 高品质边框节点(hero_lv / card_lv > 1 时使用) */
|
||||
@property(Node)
|
||||
HF_node=null!
|
||||
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 当前卡牌的金币费用 */
|
||||
card_cost:number=0
|
||||
/** 当前卡牌类型(英雄 / 技能 / 特殊升级 / 特殊刷新) */
|
||||
card_type:CardType=CardType.Hero
|
||||
/** 当前卡牌的唯一标识 UUID */
|
||||
card_uuid:number=0
|
||||
/** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */
|
||||
private isLocked: boolean = false;
|
||||
/** 图标图集缓存(后续接图标资源时直接复用) */
|
||||
/** 图标图集缓存(首次加载后复用,避免重复 IO) */
|
||||
private uiconsAtlas: SpriteAtlas | null = null;
|
||||
/** 当前槽位承载的卡牌数据,null 表示空槽 */
|
||||
private cardData: CardConfig | null = null;
|
||||
/** 上划使用阈值(像素):拖拽距离 >= 此值视为"使用卡牌" */
|
||||
private readonly dragUseThreshold: number = 70;
|
||||
/** 触摸起始 Y 坐标,用于计算拖拽距离 */
|
||||
private touchStartY: number = 0;
|
||||
/** 当前是否正在拖拽 */
|
||||
private isDragging: boolean = false;
|
||||
/** 当前是否正在执行"使用"流程(防止重复触发) */
|
||||
private isUsing: boolean = false;
|
||||
/** 卡牌的静止位置(未拖拽时应在此位置) */
|
||||
private restPosition: Vec3 = new Vec3();
|
||||
/** 是否已缓存基准 Y/Z 坐标(首次 setSlotPosition 时确定) */
|
||||
private hasFixedBasePosition: boolean = false;
|
||||
/** 基准 Y 坐标(由场景布局决定) */
|
||||
private fixedBaseY: number = 0;
|
||||
/** 基准 Z 坐标 */
|
||||
private fixedBaseZ: number = 0;
|
||||
/** UIOpacity 组件引用,用于使用消失动画中的渐隐 */
|
||||
private opacityComp: UIOpacity | null = null;
|
||||
/**
|
||||
* 图标视觉令牌:每次更新图标时 +1,
|
||||
* 异步加载回调通过比对 token 判断是否仍为当前可见内容,
|
||||
* 防止快速切卡时旧回调错误覆盖新图标。
|
||||
*/
|
||||
private iconVisualToken: number = 0;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
/**
|
||||
* 组件加载时:绑定交互事件,初始化基础 UI 状态。
|
||||
* 此阶段不触发业务逻辑。
|
||||
*/
|
||||
onLoad() {
|
||||
/** 初始阶段只做UI状态准备,不触发业务逻辑 */
|
||||
this.bindEvents();
|
||||
@@ -74,36 +139,53 @@ export class CardComp extends CCComp {
|
||||
|
||||
}
|
||||
|
||||
/** 组件销毁时解绑所有事件,防止残留回调 */
|
||||
onDestroy() {
|
||||
this.unbindEvents();
|
||||
}
|
||||
|
||||
/** 外部初始化入口(由 MissionCardComp 调用) */
|
||||
init(){
|
||||
this.onMissionStart();
|
||||
}
|
||||
|
||||
/** 游戏开始初始化 */
|
||||
/** 游戏开始初始化(预留扩展) */
|
||||
onMissionStart() {
|
||||
|
||||
}
|
||||
|
||||
/** 游戏结束清理 */
|
||||
/** 游戏结束清理(预留扩展) */
|
||||
onMissionEnd() {
|
||||
|
||||
}
|
||||
|
||||
/** 节点启动时确保可见 */
|
||||
start() {
|
||||
/** 单卡节点常驻,由数据控制显示内容 */
|
||||
this.node.active = true;
|
||||
}
|
||||
|
||||
/** 兼容旧接口:外部通过该入口更新卡牌 */
|
||||
// ======================== 对外接口 ========================
|
||||
|
||||
/**
|
||||
* 兼容旧接口:外部通过该入口更新卡牌
|
||||
* @param card 卡牌节点引用(历史遗留参数,当前未使用)
|
||||
* @param data 卡牌配置数据
|
||||
*/
|
||||
updateCardInfo(card:Node, data: CardConfig){
|
||||
this.applyDrawCard(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新卡牌图标:先尝试从缓存图集获取,未缓存则异步加载图集后获取。
|
||||
* @param node 图标所在节点
|
||||
* @param iconId 图标在 SpriteAtlas 中的帧名称
|
||||
*/
|
||||
private updateIcon(node: Node, iconId: string) {
|
||||
if (!node || !iconId) return;
|
||||
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||||
if (!sprite) return;
|
||||
// 已缓存图集 → 直接获取帧
|
||||
if (this.uiconsAtlas) {
|
||||
const frame = this.uiconsAtlas.getSpriteFrame(iconId);
|
||||
if (frame) {
|
||||
@@ -113,6 +195,7 @@ export class CardComp extends CCComp {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 首次加载图集
|
||||
resources.load("gui/uicons", SpriteAtlas, (err, atlas) => {
|
||||
if (err || !atlas) {
|
||||
mLogger.log(this.debugMode, "CardComp", "load uicons atlas failed", err);
|
||||
@@ -128,25 +211,42 @@ export class CardComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) */
|
||||
/**
|
||||
* 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发)
|
||||
* @param index 槽位索引(0~3)
|
||||
* @param data 卡牌配置数据
|
||||
*/
|
||||
updateCardData(index: number, data: CardConfig) {
|
||||
this.applyDrawCard(data);
|
||||
}
|
||||
|
||||
/** 兼容按钮回调入口:触发单卡使用 */
|
||||
/**
|
||||
* 兼容按钮回调入口:触发单卡使用
|
||||
* @param e 事件对象
|
||||
* @param index 索引字符串(历史遗留参数)
|
||||
*/
|
||||
selectCard(e: any, index: string) {
|
||||
this.useCard();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 关闭界面
|
||||
*/
|
||||
/** 关闭界面(预留) */
|
||||
close() {
|
||||
|
||||
}
|
||||
|
||||
/** 抽卡分发入口:返回 true 表示本次已成功接收新卡 */
|
||||
// ======================== 核心业务方法 ========================
|
||||
|
||||
/**
|
||||
* 抽卡分发入口:由 MissionCardComp 调用以向本槽位放入新卡。
|
||||
*
|
||||
* 流程:
|
||||
* 1. 若本槽位已锁定且已有卡 → 跳过,返回 false。
|
||||
* 2. 更新 cardData 及派生字段 → 刷新 UI → 播放入场动画。
|
||||
*
|
||||
* @param data 要放入的卡牌数据,null 表示无卡可放
|
||||
* @returns true = 成功接收;false = 跳过(锁定 / 无数据)
|
||||
*/
|
||||
applyDrawCard(data: CardConfig | null): boolean {
|
||||
if (!data) return false;
|
||||
/** 锁定且已有旧卡时,跳过本次刷新,保持老卡 */
|
||||
@@ -169,10 +269,24 @@ export class CardComp extends CCComp {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前卡牌的核心逻辑。
|
||||
*
|
||||
* 完整流程:
|
||||
* 1. 前置校验:是否有卡、是否正在使用中。
|
||||
* 2. 金币检查:不够则 toast 提示 + 回弹动画。
|
||||
* 3. 英雄卡额外校验:通过 GameEvent.UseHeroCard 事件向 MissionCardComp 询问
|
||||
* 英雄上限是否允许(guard 模式:外部可设 cancel=true 阻止使用)。
|
||||
* 4. 扣除金币,同步 CoinAdd 事件。
|
||||
* 5. 播放消失动画 → 动画结束后清空槽位并分发卡牌效果。
|
||||
*
|
||||
* @returns 被使用的卡牌数据,若未成功使用则返回 null
|
||||
*/
|
||||
useCard(): CardConfig | null {
|
||||
if (!this.cardData || this.isUsing) return null;
|
||||
const cardCost = Math.max(0, Math.floor(this.cardData.cost ?? 0));
|
||||
const currentCoin = this.getMissionCoin();
|
||||
// 金币不足 → 提示并回弹
|
||||
if (currentCoin < cardCost) {
|
||||
oops.gui.toast(`金币不足,召唤需要${cardCost}`);
|
||||
this.playReboundAnim();
|
||||
@@ -184,6 +298,7 @@ export class CardComp extends CCComp {
|
||||
});
|
||||
return null;
|
||||
}
|
||||
// 英雄卡特殊校验:通过 guard 对象实现"可取消"模式
|
||||
if (this.cardData.type === CardType.Hero) {
|
||||
const guard = {
|
||||
cancel: false,
|
||||
@@ -198,11 +313,13 @@ export class CardComp extends CCComp {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// 扣除金币
|
||||
this.setMissionCoin(currentCoin - cardCost);
|
||||
oops.message.dispatchEvent(GameEvent.CoinAdd, {
|
||||
syncOnly: true,
|
||||
delta: -cardCost
|
||||
});
|
||||
// 标记使用中,阻止并发操作
|
||||
this.isUsing = true;
|
||||
const used = this.cardData;
|
||||
mLogger.log(this.debugMode, "CardComp", "use card", {
|
||||
@@ -211,6 +328,7 @@ export class CardComp extends CCComp {
|
||||
cost: cardCost,
|
||||
leftCoin: this.getMissionCoin()
|
||||
});
|
||||
// 播放消失动画 → 动画结束后清槽并分发效果
|
||||
this.playUseDisappearAnim(() => {
|
||||
this.clearAfterUse();
|
||||
this.isUsing = false;
|
||||
@@ -219,6 +337,14 @@ export class CardComp extends CCComp {
|
||||
return used;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据卡牌类型分发对应的游戏效果事件。
|
||||
* - 英雄卡 → CallHero
|
||||
* - 技能卡 → UseSkillCard
|
||||
* - 特殊升级 / 特殊刷新 → UseSpecialCard
|
||||
*
|
||||
* @param payload 被使用的卡牌数据
|
||||
*/
|
||||
private executeCardEffectEntry(payload: CardConfig) {
|
||||
switch (payload.type) {
|
||||
case CardType.Hero:
|
||||
@@ -239,7 +365,10 @@ export class CardComp extends CCComp {
|
||||
return !!this.cardData;
|
||||
}
|
||||
|
||||
/** 外部设置锁定态 */
|
||||
/**
|
||||
* 外部设置锁定态
|
||||
* @param value true=锁定(刷新时保留旧卡),false=解锁
|
||||
*/
|
||||
setLocked(value: boolean) {
|
||||
this.isLocked = value;
|
||||
this.updateLockUI();
|
||||
@@ -250,6 +379,11 @@ export class CardComp extends CCComp {
|
||||
return this.isLocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置槽位的水平位置(由 MissionCardComp 根据槽位数量计算布局后调用)。
|
||||
* 首次调用时会记录基准 Y/Z,后续只更新 X。
|
||||
* @param x 目标水平坐标
|
||||
*/
|
||||
setSlotPosition(x: number) {
|
||||
const current = this.node.position;
|
||||
if (!this.hasFixedBasePosition) {
|
||||
@@ -258,12 +392,16 @@ export class CardComp extends CCComp {
|
||||
this.hasFixedBasePosition = true;
|
||||
}
|
||||
this.restPosition = new Vec3(x, this.fixedBaseY, this.fixedBaseZ);
|
||||
// 拖拽/使用中不立即移动,等状态结束后归位
|
||||
if (!this.isDragging && !this.isUsing) {
|
||||
this.node.setPosition(this.restPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/** 系统清槽:用于任务开始/结束等强制重置场景 */
|
||||
/**
|
||||
* 系统清槽:用于任务开始/结束等强制重置场景。
|
||||
* 停止所有动画 → 重置全部状态 → 清空显示 → 隐藏节点。
|
||||
*/
|
||||
clearBySystem() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
@@ -284,7 +422,12 @@ export class CardComp extends CCComp {
|
||||
this.node.active = false;
|
||||
}
|
||||
|
||||
/** 卡牌被玩家使用后的清槽行为 */
|
||||
// ======================== 内部清理 ========================
|
||||
|
||||
/**
|
||||
* 卡牌被玩家使用后的清槽行为。
|
||||
* 与 clearBySystem 类似,但不重置锁定态。
|
||||
*/
|
||||
private clearAfterUse() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
if (this.opacityComp) {
|
||||
@@ -304,7 +447,9 @@ export class CardComp extends CCComp {
|
||||
this.node.active = false;
|
||||
}
|
||||
|
||||
/** 绑定触控:卡面点击使用,锁按钮点击切换锁定 */
|
||||
// ======================== 事件绑定 ========================
|
||||
|
||||
/** 绑定触控事件:卡面点击/ 拖拽使用,锁按钮点击切换锁定 */
|
||||
private bindEvents() {
|
||||
this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
|
||||
this.node.on(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
|
||||
@@ -324,12 +469,18 @@ export class CardComp extends CCComp {
|
||||
this.unLock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
|
||||
}
|
||||
|
||||
// ======================== 触摸交互 ========================
|
||||
|
||||
/** 触摸开始:记录起点 Y,进入拖拽状态 */
|
||||
private onCardTouchStart(event: EventTouch) {
|
||||
if (!this.cardData || this.isUsing) return;
|
||||
this.touchStartY = event.getUILocation().y;
|
||||
this.isDragging = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸移动:跟随手指向上偏移卡牌(仅允许向上拖拽,deltaY < 0 被 clamp 为 0)
|
||||
*/
|
||||
private onCardTouchMove(event: EventTouch) {
|
||||
if (!this.isDragging || !this.cardData || this.isUsing) return;
|
||||
const currentY = event.getUILocation().y;
|
||||
@@ -337,7 +488,11 @@ export class CardComp extends CCComp {
|
||||
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
|
||||
}
|
||||
|
||||
/** 上拉超过阈值才视为使用,否则回弹原位 */
|
||||
/**
|
||||
* 触摸结束:
|
||||
* - 上拉距离 >= dragUseThreshold → 视为"使用卡牌"
|
||||
* - 否则视为"点击",打开英雄信息弹窗(仅英雄卡)并回弹
|
||||
*/
|
||||
private onCardTouchEnd(event: EventTouch) {
|
||||
if (!this.isDragging || !this.cardData || this.isUsing) return;
|
||||
const endY = event.getUILocation().y;
|
||||
@@ -351,13 +506,17 @@ export class CardComp extends CCComp {
|
||||
this.playReboundAnim();
|
||||
}
|
||||
|
||||
/** 触摸取消:回弹至原位 */
|
||||
private onCardTouchCancel() {
|
||||
if (!this.isDragging || this.isUsing) return;
|
||||
this.isDragging = false;
|
||||
this.playReboundAnim();
|
||||
}
|
||||
|
||||
/** 点击锁控件:切换锁态;空槽不允许锁定 */
|
||||
/**
|
||||
* 点击锁控件:切换锁态(空槽不允许锁定)。
|
||||
* 阻止事件冒泡,避免触发卡面的点击使用。
|
||||
*/
|
||||
private onToggleLock(event?: EventTouch) {
|
||||
if (!this.cardData) return;
|
||||
this.isLocked = !this.isLocked;
|
||||
@@ -372,22 +531,38 @@ export class CardComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据锁态刷新 Lock / unLock 显示(Lock=可点击上锁,unLock=可点击解锁) */
|
||||
// ======================== UI 渲染 ========================
|
||||
|
||||
/**
|
||||
* 根据锁态刷新 Lock / unLock 节点显示。
|
||||
* 当前功能已注释(锁定 UI 暂未启用),保留接口以备后续启用。
|
||||
*/
|
||||
private updateLockUI() {
|
||||
// if (this.Lock) this.Lock.active = !this.isLocked;
|
||||
// if (this.unLock) this.unLock.active = this.isLocked;
|
||||
}
|
||||
|
||||
/** 根据当前 cardData 渲染卡面文字与图标 */
|
||||
/**
|
||||
* 根据当前 cardData 渲染卡面文字与图标。
|
||||
*
|
||||
* 渲染逻辑根据卡牌类型分三路:
|
||||
* 1. 英雄卡:显示英雄名(带星级后缀)、AP / HP 数值、idle 动画。
|
||||
* 2. 技能卡:显示技能名(带品质后缀)、描述文本、静态图标。
|
||||
* 3. 特殊卡:显示卡名(带品质后缀)、描述文本、静态图标。
|
||||
*
|
||||
* 同时根据 pool_lv 切换背景底框,根据 hero_lv / card_lv 切换普通/高级边框。
|
||||
*/
|
||||
private applyCardUI() {
|
||||
if (!this.cardData) {
|
||||
this.applyEmptyUI();
|
||||
return;
|
||||
}
|
||||
// 递增视觉令牌,用于异步加载竞态保护
|
||||
this.iconVisualToken += 1;
|
||||
if (this.opacityComp) this.opacityComp.opacity = 255;
|
||||
this.node.setPosition(this.restPosition);
|
||||
|
||||
// ---- 卡牌种类标识(近战 / 远程 / 辅助等) ----
|
||||
if (this.Ckind_node) {
|
||||
const kindName = CKind[this.cardData.kind];
|
||||
this.Ckind_node.children.forEach(child => {
|
||||
@@ -395,6 +570,7 @@ export class CardComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 背景底框(按卡池等级显示对应子节点) ----
|
||||
const cardLvStr = `lv${this.cardData.pool_lv}`;
|
||||
if (this.BG_node) {
|
||||
this.BG_node.children.forEach(child => {
|
||||
@@ -402,6 +578,7 @@ export class CardComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 品质边框(高级 vs 普通) ----
|
||||
const card_lv_val = this.cardData.card_lv ?? 1;
|
||||
const isHighLevel = (this.cardData.hero_lv ?? 0) > 1 || card_lv_val > 1;
|
||||
if (this.HF_node) this.HF_node.active = isHighLevel;
|
||||
@@ -415,7 +592,9 @@ export class CardComp extends CCComp {
|
||||
if(isHighLevel){activeFrameNode.getChildByName("light").active=true}
|
||||
}
|
||||
|
||||
// ---- 按卡牌类型渲染具体内容 ----
|
||||
if(this.card_type===CardType.Hero){
|
||||
// 英雄卡:显示英雄名 + 星级 + AP/HP
|
||||
const hero = HeroInfo[this.card_uuid];
|
||||
const heroLv = Math.max(1, Math.floor(this.cardData.hero_lv ?? hero?.lv ?? 1));
|
||||
const suffix = heroLv >= 2 ? "★".repeat(heroLv - 1) : "";
|
||||
@@ -425,6 +604,7 @@ export class CardComp extends CCComp {
|
||||
this.info_node.getChildByName("ap").getChildByName("val").getComponent(Label).string = `${(hero?.ap ?? 0) * heroLv}`;
|
||||
this.info_node.getChildByName("hp").getChildByName("val").getComponent(Label).string = `${(hero?.hp ?? 0) * heroLv}`;
|
||||
}else if(this.card_type===CardType.Skill){
|
||||
// 技能卡:显示技能名 + 品质后缀 + 描述
|
||||
const skill = SkillSet[this.card_uuid];
|
||||
const skillCard = CardPoolList.find(c => c.uuid === this.card_uuid);
|
||||
const card_lv = Math.max(1, Math.floor(this.cardData.card_lv ?? 1));
|
||||
@@ -434,6 +614,7 @@ export class CardComp extends CCComp {
|
||||
this.oinfo_node.active = true;
|
||||
this.oinfo_node.getChildByName("info").getComponent(Label).string = `${skillCard?.info || skill?.info || ""}`;
|
||||
}else{
|
||||
// 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述
|
||||
const specialCard = this.card_type === CardType.SpecialUpgrade
|
||||
? SpecialUpgradeCardList[this.card_uuid]
|
||||
: SpecialRefreshCardList[this.card_uuid];
|
||||
@@ -445,13 +626,17 @@ export class CardComp extends CCComp {
|
||||
this.oinfo_node.getChildByName("info").getComponent(Label).string = `${specialCard?.info || ""}`;
|
||||
}
|
||||
|
||||
// ---- 费用标签 ----
|
||||
this.setLabel(this.cost_node, `${this.card_cost}`);
|
||||
|
||||
// ---- 图标 ----
|
||||
const iconNode = this.icon_node as Node;
|
||||
if (this.card_type === CardType.Hero) {
|
||||
// 英雄卡使用 AnimationClip,加载 idle 动画
|
||||
this.updateHeroAnimation(iconNode, this.card_uuid, this.iconVisualToken);
|
||||
return;
|
||||
}
|
||||
// 非英雄卡使用静态图标
|
||||
this.clearIconAnimation(iconNode);
|
||||
const iconId = this.resolveCardIconId(this.card_type, this.card_uuid);
|
||||
if (iconId) {
|
||||
@@ -462,6 +647,9 @@ export class CardComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 动画表现 ========================
|
||||
|
||||
/** 卡牌入场动画:先缩小再弹大再回归正常比例 */
|
||||
private playRefreshAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
this.node.setPosition(this.restPosition);
|
||||
@@ -472,6 +660,7 @@ export class CardComp extends CCComp {
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 回弹动画:从当前位置平滑回到静止位并恢复缩放 */
|
||||
private playReboundAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
tween(this.node)
|
||||
@@ -482,6 +671,14 @@ export class CardComp extends CCComp {
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用消失动画:
|
||||
* - 节点向上移动 120px 并缩小至 0.8
|
||||
* - 同时 UIOpacity 渐隐至 0
|
||||
* - 动画完成后调用 onComplete 回调
|
||||
*
|
||||
* @param onComplete 动画完成后的回调
|
||||
*/
|
||||
private playUseDisappearAnim(onComplete: () => void) {
|
||||
const targetPos = new Vec3(this.restPosition.x, this.restPosition.y + 120, this.restPosition.z);
|
||||
Tween.stopAllByTarget(this.node);
|
||||
@@ -501,7 +698,10 @@ export class CardComp extends CCComp {
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 渲染空槽状态 */
|
||||
/**
|
||||
* 渲染空槽状态:
|
||||
* 清空名称、费用、信息面板、种类标识、背景底框、边框、图标。
|
||||
*/
|
||||
private applyEmptyUI() {
|
||||
this.iconVisualToken += 1;
|
||||
this.setLabel(this.name_node, "");
|
||||
@@ -523,7 +723,13 @@ export class CardComp extends CCComp {
|
||||
if (sprite) sprite.spriteFrame = null;
|
||||
}
|
||||
|
||||
/** 安全设置文本,兼容节点上或子节点上的 Label */
|
||||
// ======================== 工具方法 ========================
|
||||
|
||||
/**
|
||||
* 安全设置文本,兼容节点上或子节点上的 Label
|
||||
* @param node 标签所在节点
|
||||
* @param value 要设置的文本
|
||||
*/
|
||||
private setLabel(node: Node | null, value: string) {
|
||||
if (!node) return;
|
||||
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
|
||||
@@ -531,6 +737,12 @@ export class CardComp extends CCComp {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据卡牌类型和 UUID 解析出图标 ID(在 SpriteAtlas 中的帧名)。
|
||||
* @param type 卡牌类型
|
||||
* @param uuid 卡牌 UUID
|
||||
* @returns 图标帧名称
|
||||
*/
|
||||
private resolveCardIconId(type: CardType, uuid: number): string {
|
||||
if (type === CardType.Skill) {
|
||||
return SkillSet[uuid]?.icon || `${uuid}`;
|
||||
@@ -541,6 +753,10 @@ export class CardComp extends CCComp {
|
||||
return `${uuid}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开英雄信息弹窗(IBox)。
|
||||
* 仅当当前卡为英雄卡且 HeroInfo 有效时生效。
|
||||
*/
|
||||
private openHeroInfoIBox() {
|
||||
if (!this.cardData) return;
|
||||
if (this.cardData.type !== CardType.Hero) return;
|
||||
@@ -554,6 +770,14 @@ export class CardComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为英雄卡图标加载并播放 idle 动画。
|
||||
* 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。
|
||||
*
|
||||
* @param node 图标节点
|
||||
* @param uuid 英雄 UUID
|
||||
* @param token 当前视觉令牌
|
||||
*/
|
||||
private updateHeroAnimation(node: Node, uuid: number, token: number) {
|
||||
const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite);
|
||||
if (sprite) sprite.spriteFrame = null;
|
||||
@@ -567,6 +791,7 @@ export class CardComp extends CCComp {
|
||||
mLogger.log(this.debugMode, "CardComp", `load hero animation failed ${uuid}`, err);
|
||||
return;
|
||||
}
|
||||
// 竞态保护:若加载期间卡面已变更则丢弃
|
||||
if (token !== this.iconVisualToken || !this.cardData || this.card_type !== CardType.Hero || this.card_uuid !== uuid) {
|
||||
return;
|
||||
}
|
||||
@@ -576,6 +801,7 @@ export class CardComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 清除图标节点上的动画(停止播放并移除所有 clip) */
|
||||
private clearIconAnimation(node: Node) {
|
||||
const anim = node?.getComponent(Animation);
|
||||
if (!anim) return;
|
||||
@@ -583,24 +809,32 @@ export class CardComp extends CCComp {
|
||||
this.clearAnimationClips(anim);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 Animation 组件上的全部 AnimationClip。
|
||||
* 通过浅拷贝数组避免遍历时修改集合。
|
||||
*/
|
||||
private clearAnimationClips(anim: Animation) {
|
||||
const clips = (anim as any).clips as AnimationClip[] | undefined;
|
||||
if (!clips || clips.length === 0) return;
|
||||
[...clips].forEach(clip => anim.removeClip(clip, true));
|
||||
}
|
||||
|
||||
// ======================== 数据访问 ========================
|
||||
|
||||
/** 从全局单例获取当前局内金币数量 */
|
||||
private getMissionCoin(): number {
|
||||
const missionData = smc?.vmdata?.mission_data;
|
||||
return Math.max(0, Math.floor(missionData?.coin ?? 0));
|
||||
}
|
||||
|
||||
/** 设置当前局内金币数量(自动向下取整并 clamp 至 >= 0) */
|
||||
private setMissionCoin(value: number) {
|
||||
const missionData = smc?.vmdata?.mission_data;
|
||||
if (!missionData) return;
|
||||
missionData.coin = Math.max(0, Math.floor(value));
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时的释放钩子:销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/**
|
||||
* @file CardController.ts
|
||||
* @description 卡牌控制器组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 作为卡牌系统的顶层控制器容器节点对应的脚本。
|
||||
* 2. 管理 "任务主页" 与 "任务中" 两个子界面的激活切换。
|
||||
* 3. 在 ECS 实体挂载后隐藏 loading 遮罩,标志地图加载完毕。
|
||||
*
|
||||
* 设计说明:
|
||||
* - 本组件挂载在 CardController 预制体根节点上,
|
||||
* 子节点 `mission_home` 与 `mission` 分别对应主页和战斗界面。
|
||||
* - update 中检查全局暂停 / 结束标志,预留帧逻辑扩展点。
|
||||
*
|
||||
* 依赖:
|
||||
* - smc.map.MapView.scene.mapLayer —— 获取地图层以隐藏 loading 节点
|
||||
* - smc.vmdata —— 读取全局游戏状态(game_over / game_pause)
|
||||
*/
|
||||
import { _decorator,Button,EventHandler,EventTouch,Label,NodeEventType,resources,Sprite,SpriteAtlas,tween,UITransform,v3 } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -5,40 +23,74 @@ import { smc } from "../common/SingletonModuleComp";
|
||||
import { mLogger } from "../common/Logger";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* CardControllerComp —— 卡牌系统控制器视图组件
|
||||
*
|
||||
* 负责初始化卡牌界面的顶层布局切换(主页 vs 战斗),
|
||||
* 并在 ECS 实体加入场景时处理 loading 状态。
|
||||
*/
|
||||
@ccclass('CardControllerComp')
|
||||
@ecs.register('CardController', false)
|
||||
export class CardControllerComp extends CCComp {
|
||||
/** 是否启用调试日志 */
|
||||
@property({ tooltip: "是否启用调试日志" })
|
||||
private debugMode: boolean = false;
|
||||
|
||||
/** 触控计时器(预留,可用于长按等交互逻辑) */
|
||||
touch_time:number = 0
|
||||
/** 是否正在触控中(预留) */
|
||||
in_touch:boolean = false
|
||||
/** 底部背景引用(预留) */
|
||||
bbg:any=null
|
||||
/** 底部背景 Y 坐标(预留) */
|
||||
bbg_y:number=40
|
||||
/** 卡牌槽位 X 坐标数组(5 个位置,预留) */
|
||||
bbg_x:any=[-300,-150,0,150,300]
|
||||
|
||||
protected onLoad(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* start:组件启动时打印日志并初始化页面。
|
||||
* 默认显示任务主页(mission_home),隐藏战斗界面(mission)。
|
||||
*/
|
||||
start() {
|
||||
mLogger.log(this.debugMode, 'CardController', "CardControllerComp start",this.node)
|
||||
this.page_init()
|
||||
}
|
||||
|
||||
/**
|
||||
* onAdded:当本组件对应的 ECS 实体被挂载到场景后触发。
|
||||
* 主要作用:关闭地图层上的 loading 遮罩。
|
||||
* @param args ECS 实体附加参数
|
||||
*/
|
||||
onAdded(args:any){
|
||||
mLogger.log(this.debugMode, 'CardController', "CardControllerComp onAdded",args)
|
||||
smc.map.MapView.scene.mapLayer.node.getChildByName("loading").active=false;
|
||||
}
|
||||
|
||||
/**
|
||||
* update:每帧更新。
|
||||
* 若全局标记 game_over 或 game_pause 时直接跳过(预留扩展位)。
|
||||
*/
|
||||
protected update(dt: number): void {
|
||||
if(smc.vmdata.game_over||smc.vmdata.game_pause){
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面初始化:
|
||||
* - 显示任务主页(mission_home)
|
||||
* - 隐藏任务战斗界面(mission)
|
||||
*/
|
||||
page_init(){
|
||||
this.node.getChildByName("mission_home").active=true;
|
||||
this.node.getChildByName("mission").active=false;
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ControllerComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/**
|
||||
* @file GameMap.ts
|
||||
* @description 游戏地图 ECS 实体
|
||||
*
|
||||
* 职责:
|
||||
* 1. 作为地图模块的 **ECS 根实体**,聚合 MapModelComp(数据层)与 MapViewComp(视图层)。
|
||||
* 2. 在 init 阶段仅注册 MapModelComp(纯数据,不依赖节点)。
|
||||
* 3. 显式调用 load() 时,异步加载地图预制(Prefab),实例化后将 MapViewComp 挂载到实体上。
|
||||
*
|
||||
* 设计说明:
|
||||
* - 视图层的挂载延后到 load() 完成后,确保 Prefab 加载成功且节点树就绪。
|
||||
* - 实例化的地图节点被添加至 oops.game.root,作为场景显示层的子节点。
|
||||
* - MapViewComp 从预制体中名为 "map" 的子节点上获取。
|
||||
*
|
||||
* 依赖:
|
||||
* - MapModelComp(model/MapModelComp.ts)—— 存放地图资源路径等数据配置
|
||||
* - MapViewComp(view/MapViewComp.ts)—— 地图视图逻辑
|
||||
* - oops.res —— 资源加载
|
||||
* - oops.game.root —— 全局显示根节点
|
||||
*/
|
||||
/*
|
||||
* @Author: dgflash
|
||||
* @Date: 2022-02-12 11:02:21
|
||||
@@ -10,18 +30,29 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
|
||||
import { MapModelComp } from "./model/MapModelComp";
|
||||
import { MapViewComp } from "./view/MapViewComp";
|
||||
|
||||
/** 游戏地图 */
|
||||
/** 游戏地图 ECS 实体 */
|
||||
@ecs.register(`GameMap`)
|
||||
export class GameMap extends ecs.Entity {
|
||||
/** 地图数据模型组件(通过 ECS 自动注入) */
|
||||
MapModel!: MapModelComp;
|
||||
/** 地图视图组件(通过 ECS 自动注入,load() 完成后可用) */
|
||||
MapView!: MapViewComp;
|
||||
|
||||
/**
|
||||
* ECS 实体初始化:注册数据层组件。
|
||||
* 视图层在 load() 中异步挂载。
|
||||
*/
|
||||
protected init(): void {
|
||||
this.addComponents<ecs.Comp>(
|
||||
MapModelComp);
|
||||
}
|
||||
|
||||
/** 加载地图显示资源 */
|
||||
/**
|
||||
* 加载地图显示资源:
|
||||
* 1. 通过 MapModel.resPrefab 路径加载 Prefab。
|
||||
* 2. 实例化预制体并挂载到全局根节点。
|
||||
* 3. 从预制体中提取 MapViewComp 并通过 this.add() 注册到实体。
|
||||
*/
|
||||
load() {
|
||||
oops.res.load(this.MapModel.resPrefab, Prefab, (err: Error | null, res: Prefab) => {
|
||||
if (err) {
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
/**
|
||||
* @file HInfoComp.ts
|
||||
* @description 场上英雄信息卡片组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 显示单个场上英雄的实时属性信息(AP / HP / 边框品质 / idle 动画)。
|
||||
* 2. 由 MissionCardComp.ensureHeroInfoPanel() 在英雄上场时实例化并绑定数据。
|
||||
* 3. 支持点击打开英雄详情弹窗(IBox)、出售英雄等交互(当前已注释)。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 通过 bindData(eid, model) 将组件与某个英雄 ECS 实体绑定。
|
||||
* - refresh() 被 MissionCardComp 定时调用以同步实时属性变化。
|
||||
* - iconVisualToken 机制与 CardComp 一致,用于异步动画加载的竞态保护。
|
||||
* - isModelAlive() 检测绑定的英雄实体是否仍存活(ECS ent 引用是否有效)。
|
||||
*
|
||||
* 依赖:
|
||||
* - HeroAttrsComp —— 英雄属性数据模型
|
||||
* - HeroInfo(heroSet)—— 英雄静态配置
|
||||
* - Hero —— 英雄 ECS 实体类(用于出售删除)
|
||||
* - UIID.IBox —— 英雄详情弹窗 ID
|
||||
*/
|
||||
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -10,37 +31,55 @@ import { mLogger } from "../common/Logger";
|
||||
|
||||
const {property, ccclass } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* HInfoComp —— 场上英雄信息面板视图组件
|
||||
*
|
||||
* 每个实例对应一个出战英雄,在卡牌面板下方横向排列。
|
||||
* 实时显示英雄的 AP / HP 和 idle 动画图标。
|
||||
*/
|
||||
@ccclass('HInfoComp')
|
||||
@ecs.register('HInfoComp', false)
|
||||
export class HInfoComp extends CCComp {
|
||||
/** 英雄 idle 动画图标节点 */
|
||||
@property(Node)
|
||||
icon_node=null!
|
||||
/** 出售按钮节点(预留,当前交互已注释) */
|
||||
@property(Node)
|
||||
sell_node=null!
|
||||
|
||||
/** 普通品质边框 */
|
||||
@property(Node)
|
||||
NF_node=null!
|
||||
/** 高品质边框 */
|
||||
@property(Node)
|
||||
HF_node=null!
|
||||
|
||||
|
||||
/** 绑定的英雄 ECS 实体 ID */
|
||||
private eid: number = 0;
|
||||
/** 绑定的英雄属性数据模型引用 */
|
||||
private model: HeroAttrsComp | null = null;
|
||||
/** AP 标签缓存引用 */
|
||||
private apLabel: Label | null = null;
|
||||
/** HP 标签缓存引用 */
|
||||
private hpLabel: Label | null = null;
|
||||
/** 图标视觉令牌(异步加载竞态保护) */
|
||||
private iconVisualToken: number = 0;
|
||||
/** 当前显示的英雄 UUID(避免相同 UUID 重复加载动画) */
|
||||
private iconHeroUuid: number = 0;
|
||||
/** 调试日志开关 */
|
||||
private debugMode: boolean = true;
|
||||
|
||||
onLoad() {
|
||||
this.cacheLabels();
|
||||
// this.bindEvents();
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// this.unbindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定英雄数据:关联实体 ID 和属性模型,并立即刷新显示。
|
||||
* @param eid 英雄 ECS 实体 ID
|
||||
* @param model 英雄属性组件引用
|
||||
*/
|
||||
bindData(eid: number, model: HeroAttrsComp) {
|
||||
this.eid = eid;
|
||||
this.model = model;
|
||||
@@ -48,13 +87,21 @@ export class HInfoComp extends CCComp {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新显示:
|
||||
* 1. 根据英雄等级切换高级 / 普通边框。
|
||||
* 2. 若英雄 UUID 发生变化,重新加载 idle 动画。
|
||||
* 3. 更新 AP / HP 数值标签。
|
||||
*/
|
||||
refresh() {
|
||||
if (!this.model) return;
|
||||
|
||||
// ---- 品质边框切换 ----
|
||||
const isHighLevel = (this.model.lv ?? 0) > 1;
|
||||
if (this.HF_node) this.HF_node.active = isHighLevel;
|
||||
if (this.NF_node) this.NF_node.active = !isHighLevel;
|
||||
|
||||
// 按卡池等级显示对应子节点
|
||||
const activeFrameNode = isHighLevel ? this.HF_node : this.NF_node;
|
||||
if (activeFrameNode) {
|
||||
const cardLvStr = `lv${this.model.pool_lv ?? 1}`;
|
||||
@@ -63,12 +110,15 @@ export class HInfoComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 图标动画(仅在 UUID 变化时重新加载) ----
|
||||
const heroUuid = this.model.hero_uuid ?? 0;
|
||||
if (heroUuid !== this.iconHeroUuid) {
|
||||
this.iconHeroUuid = heroUuid;
|
||||
this.iconVisualToken += 1;
|
||||
this.updateHeroAnimation(this.icon_node, heroUuid, this.iconVisualToken);
|
||||
}
|
||||
|
||||
// ---- 数值标签 ----
|
||||
if (this.apLabel) {
|
||||
this.apLabel.string = `${Math.max(0, Math.floor(this.model.ap ?? 0))}`;
|
||||
}
|
||||
@@ -77,10 +127,17 @@ export class HInfoComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测绑定的英雄实体是否仍存活。
|
||||
* 通过检查 model 上的 ent 引用判断 ECS 实体是否已被回收。
|
||||
*/
|
||||
isModelAlive(): boolean {
|
||||
return !!(this.model as any)?.ent;
|
||||
}
|
||||
|
||||
// ======================== 内部工具方法 ========================
|
||||
|
||||
/** 缓存 AP / HP Label 引用,避免每次刷新都遍历节点树 */
|
||||
private cacheLabels() {
|
||||
if (!this.apLabel) {
|
||||
this.apLabel = this.findLabelByPath(["ap", "val"]);
|
||||
@@ -90,6 +147,11 @@ export class HInfoComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按节点路径查找 Label 组件
|
||||
* @param path 从当前节点开始的子节点名称路径数组
|
||||
* @returns 找到的 Label 组件,或 null
|
||||
*/
|
||||
private findLabelByPath(path: string[]): Label | null {
|
||||
let current: Node | null = this.node;
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
@@ -99,8 +161,14 @@ export class HInfoComp extends CCComp {
|
||||
return current.getComponent(Label) || current.getComponentInChildren(Label);
|
||||
}
|
||||
|
||||
// ======================== 英雄动画 ========================
|
||||
|
||||
|
||||
/**
|
||||
* 为英雄图标加载并播放 idle 动画(带竞态保护)。
|
||||
* @param node 图标节点
|
||||
* @param uuid 英雄 UUID
|
||||
* @param token 视觉令牌
|
||||
*/
|
||||
private updateHeroAnimation(node: Node, uuid: number, token: number) {
|
||||
if (!node) return;
|
||||
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||||
@@ -115,6 +183,7 @@ export class HInfoComp extends CCComp {
|
||||
const path = `game/heros/hero/${hero.path}/idle`;
|
||||
resources.load(path, AnimationClip, (err, clip) => {
|
||||
if (err || !clip) return;
|
||||
// 竞态保护
|
||||
if (token !== this.iconVisualToken || !this.model || this.model.hero_uuid !== uuid) {
|
||||
return;
|
||||
}
|
||||
@@ -124,6 +193,7 @@ export class HInfoComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 停止并清除图标节点上的动画 */
|
||||
private clearIconAnimation(node: Node) {
|
||||
const anim = node?.getComponent(Animation);
|
||||
if (!anim) return;
|
||||
@@ -131,12 +201,15 @@ export class HInfoComp extends CCComp {
|
||||
this.clearAnimationClips(anim);
|
||||
}
|
||||
|
||||
/** 移除 Animation 组件上的全部 AnimationClip */
|
||||
private clearAnimationClips(anim: Animation) {
|
||||
const clips = (anim as any).clips as AnimationClip[] | undefined;
|
||||
if (!clips || clips.length === 0) return;
|
||||
[...clips].forEach(clip => anim.removeClip(clip, true));
|
||||
}
|
||||
|
||||
// ======================== 交互(当前已注释) ========================
|
||||
|
||||
// private bindEvents() {
|
||||
// this.sell_node?.on(Button.EventType.CLICK, this.onSellHero, this);
|
||||
// this.node.on(NodeEventType.TOUCH_END, this.onOpenIBox, this);
|
||||
@@ -147,6 +220,10 @@ export class HInfoComp extends CCComp {
|
||||
// this.node.off(NodeEventType.TOUCH_END, this.onOpenIBox, this);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 点击面板时打开英雄详情弹窗(IBox)。
|
||||
* 传入英雄 UUID、等级和技能列表。
|
||||
*/
|
||||
private onOpenIBox() {
|
||||
if (!this.model) return;
|
||||
if (!this.isModelAlive()) return;
|
||||
@@ -161,6 +238,10 @@ export class HInfoComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 出售英雄:通过 Hero.removeByEid 移除 ECS 实体,
|
||||
* 并关闭详情弹窗。
|
||||
*/
|
||||
private onSellHero(event?: Event) {
|
||||
if (!this.eid) return;
|
||||
const removed = Hero.removeByEid(this.eid);
|
||||
@@ -173,7 +254,7 @@ export class HInfoComp extends CCComp {
|
||||
oops.gui.remove(UIID.IBox);
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时的释放钩子:清理动画资源并销毁节点 */
|
||||
reset() {
|
||||
this.clearIconAnimation(this.icon_node);
|
||||
this.iconVisualToken = 0;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/**
|
||||
* @file HlistComp.ts
|
||||
* @description 英雄列表轮播组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 在主页界面以 **5 格轮播** 形式展示英雄图鉴。
|
||||
* 2. 支持左右切换,用 tween 动画平滑过渡节点位置。
|
||||
* 3. 点击切换时自动更新当前选中英雄的名称、AP、HP 和技能信息。
|
||||
*
|
||||
* 关键设计:
|
||||
* - carouselNodes[0..4] 对应 5 个展示位(最左-2 到最右+2),
|
||||
* 中间位 [2] 为当前选中英雄。
|
||||
* - 切换时:将即将移出屏幕的节点瞬间跳转到另一端,再用 tween 滑入。
|
||||
* - 切换完成后重排 carouselNodes 数组保持逻辑顺序。
|
||||
* - iconVisualTokens 按节点独立管理竞态令牌,防止异步动画回调错乱。
|
||||
*
|
||||
* 依赖:
|
||||
* - HeroInfo / HeroList(heroSet)—— 英雄静态配置与全量英雄 UUID 列表
|
||||
* - SkillSet / IType(SkillSet)—— 技能配置与类型枚举
|
||||
*/
|
||||
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources, 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";
|
||||
@@ -8,53 +28,83 @@ import { mLogger } from "../common/Logger";
|
||||
|
||||
const {property, ccclass } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* HListComp —— 英雄图鉴轮播视图组件
|
||||
*
|
||||
* 在任务主页展示所有可用英雄,玩家可左右滑动查看:
|
||||
* - 中间位显示完整信息(名称 / AP / HP / 技能列表)
|
||||
* - 两侧位显示缩略 idle 动画
|
||||
*/
|
||||
@ccclass('HListComp')
|
||||
@ecs.register('HListComp', false)
|
||||
export class HListComp extends CCComp {
|
||||
// ======================== 编辑器绑定节点 ========================
|
||||
|
||||
/** 中间位英雄 idle 图标节点 */
|
||||
@property(Node)
|
||||
hero_icon=null!
|
||||
/** 左侧第 1 位英雄图标 */
|
||||
@property(Node)
|
||||
phero_icon=null!
|
||||
/** 右侧第 1 位英雄图标 */
|
||||
@property(Node)
|
||||
nhero_icon=null!
|
||||
/** 左侧第 2 位英雄图标(最远) */
|
||||
@property(Node)
|
||||
phero1_icon=null!
|
||||
/** 右侧第 2 位英雄图标(最远) */
|
||||
@property(Node)
|
||||
nhero1_icon=null!
|
||||
/** 攻击力标签节点 */
|
||||
@property(Node)
|
||||
ap_node=null!
|
||||
/** 生命值标签节点 */
|
||||
@property(Node)
|
||||
hp_node=null!
|
||||
/** 技能信息容器节点(包含 Line1~Line5 子节点) */
|
||||
@property(Node)
|
||||
info_node=null!
|
||||
/** 英雄名称标签节点 */
|
||||
@property(Node)
|
||||
name_node=null!
|
||||
|
||||
/** 向左切换按钮 */
|
||||
@property(Node)
|
||||
pre_btn=null!
|
||||
/** 向右切换按钮 */
|
||||
@property(Node)
|
||||
next_btn=null!
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 当前选中英雄在 HeroList 中的索引 */
|
||||
huuid:number=null!
|
||||
/** 当前选中英雄在 HeroList 数组中的下标 */
|
||||
private currentIndex: number = 0;
|
||||
/** 各图标节点的视觉令牌映射(防止异步动画竞态) */
|
||||
private iconVisualTokens: Map<Node, number> = new Map();
|
||||
/** 是否正在播放切换动画(防止快速连点) */
|
||||
private isAnimating: boolean = false;
|
||||
/** 轮播节点数组,顺序为 [左2, 左1, 中, 右1, 右2] */
|
||||
private carouselNodes: Node[] = [];
|
||||
/** 5 个固定位置坐标(从场景中读取初始值) */
|
||||
private fixedPositions: Vec3[] = [];
|
||||
/** 调试日志开关 */
|
||||
debugMode: boolean = false;
|
||||
|
||||
onLoad() {
|
||||
// 绑定左右切换按钮事件
|
||||
this.pre_btn?.on(NodeEventType.TOUCH_END, this.onPreClick, this);
|
||||
this.next_btn?.on(NodeEventType.TOUCH_END, this.onNextClick, this);
|
||||
}
|
||||
|
||||
start() {
|
||||
// 初始化轮播节点数组和固定位置
|
||||
if (this.phero1_icon && this.phero_icon && this.hero_icon && this.nhero_icon && this.nhero1_icon) {
|
||||
this.carouselNodes = [this.phero1_icon, this.phero_icon, this.hero_icon, this.nhero_icon, this.nhero1_icon];
|
||||
this.fixedPositions = this.carouselNodes.map(n => n.position.clone());
|
||||
}
|
||||
|
||||
// 设置初始选中并加载所有位置的英雄动画
|
||||
if (HeroList && HeroList.length > 0) {
|
||||
this.currentIndex = 0;
|
||||
this.initAllNodes();
|
||||
@@ -62,6 +112,15 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 切换逻辑 ========================
|
||||
|
||||
/**
|
||||
* 向左切换(查看上一个英雄):
|
||||
* 1. currentIndex 左移。
|
||||
* 2. 最右节点 n4 瞬间跳到最左位置,加载新英雄动画。
|
||||
* 3. 所有节点 tween 向右滑动一格。
|
||||
* 4. 动画完成后重排 carouselNodes 数组。
|
||||
*/
|
||||
private onPreClick() {
|
||||
if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return;
|
||||
this.isAnimating = true;
|
||||
@@ -70,22 +129,31 @@ export class HListComp extends CCComp {
|
||||
|
||||
const [n0, n1, n2, n3, n4] = this.carouselNodes;
|
||||
|
||||
// n4 instantly jumps from rightmost to leftmost position to get ready to slide in
|
||||
// n4 瞬间跳至最左位置,准备滑入
|
||||
n4.setPosition(new Vec3(this.fixedPositions[0].x, n4.position.y, n4.position.z));
|
||||
this.updateNodeAnimationByOffset(n4, -2);
|
||||
this.updateHeroInfo();
|
||||
|
||||
// 所有节点向右滑动一格
|
||||
tween(n0).to(0.2, { position: new Vec3(this.fixedPositions[1].x, n0.position.y, n0.position.z) }).start();
|
||||
tween(n1).to(0.2, { position: new Vec3(this.fixedPositions[2].x, n1.position.y, n1.position.z) }).start();
|
||||
tween(n2).to(0.2, { position: new Vec3(this.fixedPositions[3].x, n2.position.y, n2.position.z) }).start();
|
||||
tween(n3).to(0.2, { position: new Vec3(this.fixedPositions[4].x, n3.position.y, n3.position.z) })
|
||||
.call(() => {
|
||||
// 重排数组:n4 成为新的最左节点
|
||||
this.carouselNodes = [n4, n0, n1, n2, n3];
|
||||
this.isAnimating = false;
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向右切换(查看下一个英雄):
|
||||
* 1. currentIndex 右移。
|
||||
* 2. 最左节点 n0 瞬间跳到最右位置,加载新英雄动画。
|
||||
* 3. 所有节点 tween 向左滑动一格。
|
||||
* 4. 动画完成后重排 carouselNodes 数组。
|
||||
*/
|
||||
private onNextClick() {
|
||||
if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return;
|
||||
this.isAnimating = true;
|
||||
@@ -94,32 +162,47 @@ export class HListComp extends CCComp {
|
||||
|
||||
const [n0, n1, n2, n3, n4] = this.carouselNodes;
|
||||
|
||||
// n0 instantly jumps from leftmost to rightmost position to get ready to slide in
|
||||
// n0 瞬间跳至最右位置,准备滑入
|
||||
n0.setPosition(new Vec3(this.fixedPositions[4].x, n0.position.y, n0.position.z));
|
||||
this.updateNodeAnimationByOffset(n0, 2);
|
||||
this.updateHeroInfo();
|
||||
|
||||
// 所有节点向左滑动一格
|
||||
tween(n1).to(0.2, { position: new Vec3(this.fixedPositions[0].x, n1.position.y, n1.position.z) }).start();
|
||||
tween(n2).to(0.2, { position: new Vec3(this.fixedPositions[1].x, n2.position.y, n2.position.z) }).start();
|
||||
tween(n3).to(0.2, { position: new Vec3(this.fixedPositions[2].x, n3.position.y, n3.position.z) }).start();
|
||||
tween(n4).to(0.2, { position: new Vec3(this.fixedPositions[3].x, n4.position.y, n4.position.z) })
|
||||
.call(() => {
|
||||
// 重排数组:n0 成为新的最右节点
|
||||
this.carouselNodes = [n1, n2, n3, n4, n0];
|
||||
this.isAnimating = false;
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
// ======================== 数据查询 ========================
|
||||
|
||||
/**
|
||||
* 根据偏移量获取英雄 UUID。
|
||||
* @param offset 相对于当前选中英雄的偏移(-2, -1, 0, 1, 2)
|
||||
* @returns HeroList 中对应位置的英雄 UUID
|
||||
*/
|
||||
private getHeroUuid(offset: number): number {
|
||||
const len = HeroList.length;
|
||||
return HeroList[(this.currentIndex + offset + len * 5) % len];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按偏移量更新指定节点的英雄动画。
|
||||
* @param node 目标图标节点
|
||||
* @param offset 偏移量
|
||||
*/
|
||||
private updateNodeAnimationByOffset(node: Node, offset: number) {
|
||||
const uuid = this.getHeroUuid(offset);
|
||||
this.updateHeroAnimation(node, uuid);
|
||||
}
|
||||
|
||||
/** 更新当前选中英雄的详细信息(名称、AP、HP、技能列表) */
|
||||
private updateHeroInfo() {
|
||||
this.huuid = this.getHeroUuid(0);
|
||||
const hero = HeroInfo[this.huuid];
|
||||
@@ -131,6 +214,7 @@ export class HListComp extends CCComp {
|
||||
this.updateSkillInfo(hero);
|
||||
}
|
||||
|
||||
/** 初始化 5 个轮播位的英雄动画 */
|
||||
private initAllNodes() {
|
||||
if (this.carouselNodes.length < 5) return;
|
||||
this.updateNodeAnimationByOffset(this.carouselNodes[0], -2);
|
||||
@@ -140,6 +224,13 @@ export class HListComp extends CCComp {
|
||||
this.updateNodeAnimationByOffset(this.carouselNodes[4], 2);
|
||||
}
|
||||
|
||||
// ======================== UI 工具 ========================
|
||||
|
||||
/**
|
||||
* 安全设置 Label 文本
|
||||
* @param node 标签所在节点
|
||||
* @param text 文本内容
|
||||
*/
|
||||
private setLabelText(node: Node, text: string) {
|
||||
if (!node) return;
|
||||
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
|
||||
@@ -148,6 +239,15 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 英雄动画 ========================
|
||||
|
||||
/**
|
||||
* 为指定节点加载并播放英雄 idle 动画。
|
||||
* 使用 iconVisualTokens 做节点级竞态保护。
|
||||
*
|
||||
* @param node 图标节点
|
||||
* @param uuid 英雄 UUID
|
||||
*/
|
||||
private updateHeroAnimation(node: Node, uuid: number) {
|
||||
if (!node) return;
|
||||
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||||
@@ -159,6 +259,7 @@ export class HListComp extends CCComp {
|
||||
const anim = node.getComponent(Animation) || node.addComponent(Animation);
|
||||
this.clearAnimationClips(anim);
|
||||
|
||||
// 递增该节点的视觉令牌
|
||||
let token = (this.iconVisualTokens.get(node) || 0) + 1;
|
||||
this.iconVisualTokens.set(node, token);
|
||||
const path = `game/heros/hero/${hero.path}/idle`;
|
||||
@@ -168,6 +269,7 @@ export class HListComp extends CCComp {
|
||||
mLogger.log(this.debugMode, "HListComp", `load hero animation failed ${uuid}`, err);
|
||||
return;
|
||||
}
|
||||
// 竞态保护:令牌不匹配则丢弃
|
||||
if (token !== this.iconVisualTokens.get(node)) {
|
||||
return;
|
||||
}
|
||||
@@ -177,6 +279,7 @@ export class HListComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 移除 Animation 上的全部 clip(倒序遍历避免索引偏移) */
|
||||
private clearAnimationClips(anim: Animation) {
|
||||
const clips = anim.clips;
|
||||
if (clips && clips.length > 0) {
|
||||
@@ -187,6 +290,11 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找子节点(按名称)
|
||||
* @param root 起始节点
|
||||
* @param name 目标节点名
|
||||
*/
|
||||
private findNodeByName(root: Node, name: string): Node | null {
|
||||
if (!root) return null;
|
||||
if (root.name === name) return root;
|
||||
@@ -197,6 +305,15 @@ export class HListComp extends CCComp {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ======================== 技能信息 ========================
|
||||
|
||||
/**
|
||||
* 更新技能信息面板:
|
||||
* 遍历英雄的技能列表,为 Line1~Line5 节点填充技能名、等级、CD、描述。
|
||||
* 同时根据技能类型(近战 / 远程 / 辅助)切换对应图标。
|
||||
*
|
||||
* @param hero 英雄配置数据(含 skills 字段)
|
||||
*/
|
||||
private updateSkillInfo(hero: any) {
|
||||
if (!this.info_node) return;
|
||||
|
||||
@@ -212,6 +329,7 @@ export class HListComp extends CCComp {
|
||||
const skillId = skill.uuid;
|
||||
const config = SkillSet[skillId];
|
||||
|
||||
// 拼接技能信息文本
|
||||
const text = config ? `${config.name} Lv.${skill.lv} CD:${skill.cd}s ${config.info}` : `未知技能 CD:${skill.cd}s`;
|
||||
|
||||
const noteNode = this.findNodeByName(line, "note");
|
||||
@@ -220,6 +338,7 @@ export class HListComp extends CCComp {
|
||||
label.string = text;
|
||||
}
|
||||
|
||||
// 切换技能类型图标
|
||||
this.updateLineTypeIcon(line, config?.IType);
|
||||
} else {
|
||||
line.active = false;
|
||||
@@ -227,6 +346,15 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新技能行的类型图标(互斥显示):
|
||||
* - Melee → 近战图标
|
||||
* - remote → 远程图标
|
||||
* - support → 辅助图标
|
||||
*
|
||||
* @param line 技能行节点
|
||||
* @param iType 技能类型枚举
|
||||
*/
|
||||
private updateLineTypeIcon(line: Node, iType?: IType) {
|
||||
const meleeNode = this.findNodeByName(line, "Melee");
|
||||
const remoteNode = this.findNodeByName(line, "remote");
|
||||
@@ -236,7 +364,7 @@ export class HListComp extends CCComp {
|
||||
if (supportNode) supportNode.active = iType === IType.support;
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
/**
|
||||
* @file IBoxComp.ts
|
||||
* @description 英雄信息弹窗组件(IBox,UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 作为全局弹窗,展示某个英雄的 **详细技能信息**。
|
||||
* 2. 通过 onAdded(args) 接收英雄 UUID、等级和运行时技能数据。
|
||||
* 3. 自动计算技能行数并动态调整弹窗背景高度和名称位置。
|
||||
* 4. 点击弹窗任意区域关闭自身。
|
||||
*
|
||||
* 关键设计:
|
||||
* - Line1~Line5 为预设的 5 行技能节点,按需显示/隐藏。
|
||||
* - 每行包含技能名、等级、CD、描述文本和类型图标(近战/远程/辅助)。
|
||||
* - 背景高度 = baseHeight + (行数 - 1) × extraLineHeight。
|
||||
* - 若传入 args.skills(运行时技能),优先使用;否则回退到英雄静态配置。
|
||||
*
|
||||
* 依赖:
|
||||
* - HeroInfo(heroSet)—— 英雄静态配置
|
||||
* - SkillSet / IType(SkillSet)—— 技能静态配置
|
||||
* - UIID —— 在 oops.gui 系统中注册的弹窗 ID
|
||||
*/
|
||||
import { _decorator, Label, Node, NodeEventType, UITransform } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -6,9 +27,16 @@ import { IType, SkillSet } from "../common/config/SkillSet";
|
||||
import { oops } from "db://oops-framework/core/Oops";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* IBoxComp —— 英雄信息弹窗视图组件
|
||||
*
|
||||
* 通过 oops.gui.open(UIID.IBox, { heroUuid, heroLv, skills? }) 打开。
|
||||
* 展示英雄技能列表,支持最多 5 行。
|
||||
*/
|
||||
@ccclass('IBoxComp')
|
||||
@ecs.register('IBoxComp', false)
|
||||
export class IBoxComp extends CCComp {
|
||||
// ======================== 编辑器绑定节点(5 行技能) ========================
|
||||
@property(Node)
|
||||
Line1: Node = null!
|
||||
@property(Node)
|
||||
@@ -19,11 +47,23 @@ export class IBoxComp extends CCComp {
|
||||
Line4: Node = null!
|
||||
@property(Node)
|
||||
Line5: Node = null!
|
||||
|
||||
// ======================== 布局常量 ========================
|
||||
/** 弹窗背景基础高度(只有 1 行技能时的高度) */
|
||||
private readonly baseHeight: number = 100;
|
||||
/** 每增加一行技能,背景增加的高度 */
|
||||
private readonly extraLineHeight: number = 50;
|
||||
/** 英雄名称标签的基准 Y 坐标 */
|
||||
private readonly nameBaseY: number = 50;
|
||||
/** 每增加一行技能,名称标签额外上移的 Y 偏移 */
|
||||
private readonly nameExtraLineOffsetY: number = 25;
|
||||
|
||||
/**
|
||||
* ECS 实体挂载回调:接收外部传入的英雄参数并渲染。
|
||||
* @param args.heroUuid 英雄 UUID
|
||||
* @param args.heroLv 英雄当前等级
|
||||
* @param args.skills (可选)运行时技能数据,若无则使用英雄静态配置
|
||||
*/
|
||||
onAdded(args: {
|
||||
heroUuid?: number;
|
||||
heroLv?: number;
|
||||
@@ -32,18 +72,32 @@ export class IBoxComp extends CCComp {
|
||||
this.renderHeroInfo(args);
|
||||
}
|
||||
|
||||
/** 绑定点击关闭事件 */
|
||||
onLoad() {
|
||||
this.node.on(NodeEventType.TOUCH_END, this.onTapClose, this);
|
||||
}
|
||||
|
||||
/** 解绑点击事件 */
|
||||
onDestroy() {
|
||||
this.node.off(NodeEventType.TOUCH_END, this.onTapClose, this);
|
||||
}
|
||||
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
// ======================== 渲染逻辑 ========================
|
||||
|
||||
/**
|
||||
* 主渲染方法:解析英雄数据并填充技能行。
|
||||
*
|
||||
* 流程:
|
||||
* 1. 从 args 中取英雄 UUID 和等级。
|
||||
* 2. 优先使用运行时技能数据(args.skills),否则回退英雄静态配置。
|
||||
* 3. 将每个技能映射为 { text, iType },过滤无效项。
|
||||
* 4. 调用 applyLineData 渲染到 Line1~Line5。
|
||||
*/
|
||||
private renderHeroInfo(args: {
|
||||
heroUuid?: number;
|
||||
heroLv?: number;
|
||||
@@ -58,13 +112,18 @@ export class IBoxComp extends CCComp {
|
||||
return;
|
||||
}
|
||||
this.setHeroName(hero.name);
|
||||
|
||||
// 运行时技能 vs 静态配置
|
||||
const runtimeSkills = args?.skills ? Object.values(args.skills) : [];
|
||||
const sourceSkills = runtimeSkills.length > 0 ? runtimeSkills : Object.values(hero.skills ?? {});
|
||||
|
||||
// 将每个技能转换为显示数据
|
||||
const lineData = sourceSkills.map(skill => {
|
||||
const skillId = Math.floor(skill?.uuid ?? 0);
|
||||
if (!skillId) return null;
|
||||
const config = SkillSet[skillId];
|
||||
if (!config) return null;
|
||||
// 运行时技能直接使用其等级;静态配置的技能等级需叠加英雄等级修正
|
||||
const runtimeLv = runtimeSkills.length > 0 ? Math.max(0, Math.floor(skill.lv ?? 0)) : Math.max(0, Math.floor((skill.lv ?? 1) + heroLv - 2));
|
||||
const cd = Number(skill?.cd ?? 0);
|
||||
return {
|
||||
@@ -72,9 +131,18 @@ export class IBoxComp extends CCComp {
|
||||
iType: config.IType
|
||||
};
|
||||
}).filter(item => !!item) as Array<{ text: string; iType: IType }>;
|
||||
|
||||
this.applyLineData(lineData.length > 0 ? lineData : [{ text: "暂无技能信息" }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将技能数据应用到 Line1~Line5:
|
||||
* 1. 按顺序填充文本和类型图标。
|
||||
* 2. 超出的行隐藏。
|
||||
* 3. 根据显示行数调整弹窗高度和名称位置。
|
||||
*
|
||||
* @param skillLines 技能行数据数组
|
||||
*/
|
||||
private applyLineData(skillLines: Array<{ text: string; iType?: IType }>) {
|
||||
const lines = [this.Line1, this.Line2, this.Line3, this.Line4, this.Line5];
|
||||
const showCount = Math.max(1, Math.min(lines.length, skillLines.length));
|
||||
@@ -86,16 +154,24 @@ export class IBoxComp extends CCComp {
|
||||
if (!active) continue;
|
||||
const data = skillLines[i];
|
||||
const text = data?.text ?? "";
|
||||
// 查找技能文本节点并设置内容
|
||||
const noteNode = line.getChildByName("note");
|
||||
const label = noteNode?.getComponent(Label) || noteNode?.getComponentInChildren(Label) || line.getComponentInChildren(Label);
|
||||
if (label) label.string = text;
|
||||
// 更新类型图标
|
||||
this.updateLineTypeIcon(line, data?.iType);
|
||||
}
|
||||
// 动态调整弹窗背景高度
|
||||
const targetHeight = this.baseHeight + Math.max(0, showCount - 1) * this.extraLineHeight;
|
||||
this.updateIBoxHeight(targetHeight);
|
||||
// 动态调整名称位置
|
||||
this.updateNamePosition(showCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置弹窗背景 Bg 节点的高度
|
||||
* @param height 目标高度
|
||||
*/
|
||||
private updateIBoxHeight(height: number) {
|
||||
const bgNode = this.node.getChildByName("Bg");
|
||||
const bgTransform = bgNode?.getComponent(UITransform);
|
||||
@@ -104,6 +180,10 @@ export class IBoxComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置英雄名称标签
|
||||
* @param name 英雄名称
|
||||
*/
|
||||
private setHeroName(name: string) {
|
||||
const bgNode = this.node.getChildByName("Bg");
|
||||
const nameNode = bgNode?.getChildByName("name");
|
||||
@@ -114,6 +194,12 @@ export class IBoxComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据显示行数调整名称节点的 Y 坐标。
|
||||
* 行数越多,名称越往上移,保持视觉居中。
|
||||
*
|
||||
* @param showCount 当前显示的技能行数
|
||||
*/
|
||||
private updateNamePosition(showCount: number) {
|
||||
const bgNode = this.node.getChildByName("Bg");
|
||||
const nameNode = bgNode?.getChildByName("name");
|
||||
@@ -123,6 +209,11 @@ export class IBoxComp extends CCComp {
|
||||
nameNode.setPosition(current.x, targetY, current.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新技能行的类型图标(互斥显示)。
|
||||
* @param line 技能行节点
|
||||
* @param iType 技能类型(近战 / 远程 / 辅助)
|
||||
*/
|
||||
private updateLineTypeIcon(line: Node, iType?: IType) {
|
||||
const meleeNode = line.getChildByName("Melee");
|
||||
const remoteNode = line.getChildByName("remote");
|
||||
@@ -132,6 +223,7 @@ export class IBoxComp extends CCComp {
|
||||
if (supportNode) supportNode.active = iType === IType.support;
|
||||
}
|
||||
|
||||
/** 点击弹窗任意区域关闭自身 */
|
||||
private onTapClose() {
|
||||
oops.gui.removeByNode(this.node);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/**
|
||||
* @file MissSkillsComp.ts
|
||||
* @description 场上技能卡槽位管理器组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理场上已使用的 **技能卡** 的可视化槽位(最多 10 个)。
|
||||
* 2. 监听 UseSkillCard 事件,当玩家使用技能卡时实例化 SkillBoxComp 并放入空闲槽位。
|
||||
* 3. 监听 RemoveSkillBox 事件,当技能生效完毕后回收槽位并重新排列。
|
||||
*
|
||||
* 关键设计:
|
||||
* - slots 数组预定义了 10 个固定坐标位(2 行 × 5 列),
|
||||
* 每个槽位记录是否占用及对应节点引用。
|
||||
* - 当某个 SkillBox 销毁时,触发 rearrangeSlots 将剩余节点
|
||||
* 紧凑地重排到前置槽位,避免视觉空洞。
|
||||
* - SkillBox 的实例化使用 skill_box Prefab,在编辑器中绑定。
|
||||
*
|
||||
* 依赖:
|
||||
* - SkillBoxComp(SkillBoxComp.ts)—— 单个技能卡的效果控制组件
|
||||
* - GameEvent.UseSkillCard —— 技能卡使用事件
|
||||
* - GameEvent.RemoveSkillBox —— 技能卡移除事件
|
||||
* - smc.map.MapView.scene.entityLayer —— 技能节点的父容器(SKILL 节点)
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Node, Prefab, instantiate, Vec3 } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
@@ -8,21 +30,39 @@ import { GameEvent } from "../common/config/GameEvent";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 技能槽位数据结构 */
|
||||
interface SkillBoxSlot {
|
||||
/** 该槽位的固定 X 坐标 */
|
||||
x: number;
|
||||
/** 该槽位的固定 Y 坐标 */
|
||||
y: number;
|
||||
/** 是否已被占用 */
|
||||
used: boolean;
|
||||
/** 占用该槽位的节点引用 */
|
||||
node: Node | null;
|
||||
}
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* MissSkillsComp —— 场上技能卡槽位管理器
|
||||
*
|
||||
* 在战斗场景中管理已激活的技能卡显示位置。
|
||||
* 2 行 × 5 列 = 10 个槽位,不足时提示已满。
|
||||
*/
|
||||
@ccclass('MissSkillsComp')
|
||||
@ecs.register('MissSkillsComp', false)
|
||||
export class MissSkillsComp extends CCComp {
|
||||
/** 调试日志开关 */
|
||||
private debugMode: boolean = true;
|
||||
|
||||
/** 技能卡 Prefab(在编辑器中赋值) */
|
||||
@property({type: Prefab})
|
||||
private skill_box: Prefab = null;
|
||||
|
||||
/**
|
||||
* 预定义的 10 个槽位坐标(2 行 × 5 列):
|
||||
* 第 1 行 y=240:x = -320, -240, -160, -80, 0
|
||||
* 第 2 行 y=320:x = -320, -240, -160, -80, 0
|
||||
*/
|
||||
private slots: SkillBoxSlot[] = [
|
||||
{ x: -320, y: 240, used: false, node: null },
|
||||
{ x: -240, y: 240, used: false, node: null },
|
||||
@@ -36,16 +76,26 @@ export class MissSkillsComp extends CCComp {
|
||||
{ x: 0, y: 320, used: false, node: null },
|
||||
];
|
||||
|
||||
/** 注册事件监听 */
|
||||
onLoad() {
|
||||
oops.message.on(GameEvent.UseSkillCard, this.onUseSkillCard, this);
|
||||
oops.message.on(GameEvent.RemoveSkillBox, this.onRemoveSkillBox, this);
|
||||
}
|
||||
|
||||
/** 移除事件监听 */
|
||||
onDestroy() {
|
||||
oops.message.off(GameEvent.UseSkillCard, this.onUseSkillCard, this);
|
||||
oops.message.off(GameEvent.RemoveSkillBox, this.onRemoveSkillBox, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理技能卡移除事件:
|
||||
* 1. 在 slots 中找到对应节点并释放。
|
||||
* 2. 调用 rearrangeSlots 紧凑重排。
|
||||
*
|
||||
* @param event 事件名
|
||||
* @param args 要移除的节点引用
|
||||
*/
|
||||
private onRemoveSkillBox(event: string, args: any) {
|
||||
const node = args as Node;
|
||||
let removed = false;
|
||||
@@ -62,7 +112,12 @@ export class MissSkillsComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 紧凑重排:将所有有效节点按顺序移到前置槽位。
|
||||
* 确保视觉上不会出现中间空洞。
|
||||
*/
|
||||
private rearrangeSlots() {
|
||||
// 收集所有有效节点
|
||||
const validNodes: Node[] = [];
|
||||
for (let i = 0; i < this.slots.length; i++) {
|
||||
if (this.slots[i].used && this.slots[i].node && this.slots[i].node.isValid) {
|
||||
@@ -71,7 +126,7 @@ export class MissSkillsComp extends CCComp {
|
||||
this.slots[i].used = false;
|
||||
this.slots[i].node = null;
|
||||
}
|
||||
|
||||
// 按顺序重新分配
|
||||
for (let i = 0; i < validNodes.length; i++) {
|
||||
if (i < this.slots.length) {
|
||||
this.slots[i].used = true;
|
||||
@@ -81,6 +136,11 @@ export class MissSkillsComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理使用技能卡事件:提取 uuid 和 card_lv 后调用 addSkill。
|
||||
* @param event 事件名
|
||||
* @param args 卡牌数据(含 uuid、card_lv)
|
||||
*/
|
||||
private onUseSkillCard(event: string, args: any) {
|
||||
const payload = args ?? event;
|
||||
const uuid = Number(payload?.uuid ?? 0);
|
||||
@@ -93,7 +153,17 @@ export class MissSkillsComp extends CCComp {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 在场上添加一个技能卡:
|
||||
* 1. 在 slots 中查找空闲位。
|
||||
* 2. 实例化 skill_box Prefab 并放置在空闲位坐标。
|
||||
* 3. 获取或添加 SkillBoxComp 并初始化。
|
||||
*
|
||||
* @param uuid 技能 UUID
|
||||
* @param card_lv 技能卡等级
|
||||
*/
|
||||
addSkill(uuid: number, card_lv: number) {
|
||||
// 技能节点的父容器
|
||||
var parent = smc.map.MapView.scene.entityLayer!.node!.getChildByName("SKILL")!;
|
||||
|
||||
if (!this.skill_box) {
|
||||
@@ -101,12 +171,14 @@ export class MissSkillsComp extends CCComp {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找空闲槽位
|
||||
const emptyIndex = this.slots.findIndex(slot => !slot.used);
|
||||
if (emptyIndex === -1) {
|
||||
mLogger.warn(this.debugMode, "MissSkillsComp", "skill_box slots are full");
|
||||
return;
|
||||
}
|
||||
|
||||
// 实例化并放入槽位
|
||||
const node = instantiate(this.skill_box);
|
||||
node.parent = parent;
|
||||
node.setPosition(new Vec3(this.slots[emptyIndex].x, this.slots[emptyIndex].y, 0));
|
||||
@@ -114,11 +186,12 @@ export class MissSkillsComp extends CCComp {
|
||||
this.slots[emptyIndex].used = true;
|
||||
this.slots[emptyIndex].node = node;
|
||||
|
||||
// 初始化技能效果组件
|
||||
const comp = node.getComponent(SkillBoxComp) || node.addComponent(SkillBoxComp);
|
||||
comp.init(uuid, card_lv);
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
/**
|
||||
* @file MissionCardComp.ts
|
||||
* @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. **卡牌分发管理** —— 从卡池抽取 4 张卡,分发到 4 个 CardComp 槽位。
|
||||
* 2. **卡池升级** —— 消耗金币提升卡池等级(poolLv),解锁更高稀有度的卡牌。
|
||||
* 3. **金币费用管理** —— 抽卡费用(refreshCost)、升级费用(CardsUpSet)、
|
||||
* 波次折扣(CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE)的计算与扣除。
|
||||
* 4. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许
|
||||
* 再召唤英雄(含合成后腾位的特殊判断 canUseHeroCardByMerge)。
|
||||
* 5. **场上英雄信息面板(HInfoComp 列表)同步** ——
|
||||
* 英雄上场时实例化面板,死亡时移除,定时刷新属性显示。
|
||||
* 6. **特殊卡执行** —— 处理英雄升级卡(SpecialUpgrade)和英雄刷新卡(SpecialRefresh)。
|
||||
* 7. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 4 个 CardComp 通过 cacheCardComps() 映射为有序数组 cardComps[],
|
||||
* 之后所有分发、清空操作均通过此数组进行。
|
||||
* - buildDrawCards() 保证每次抽取 4 张,不足时循环补齐。
|
||||
* - 英雄上限校验(onUseHeroCard)采用 guard/cancel 模式:
|
||||
* CardComp 发出 UseHeroCard 事件并传入 guard 对象,
|
||||
* 本组件可通过 guard.cancel=true 阻止使用。
|
||||
* - ensureHeroInfoPanel() 建立 EID → HInfoComp 的 Map 映射,
|
||||
* 支持英雄合成升级后面板热更新。
|
||||
*
|
||||
* 依赖:
|
||||
* - CardComp —— 单卡槽位
|
||||
* - HInfoComp —— 英雄信息面板
|
||||
* - CardSet 模块 —— 卡池配置、抽卡规则、特殊卡数据
|
||||
* - HeroAttrsComp —— 英雄属性(合成校验 / 升级)
|
||||
* - MissionHeroCompComp —— 获取合成规则(needCount / maxLv)
|
||||
* - smc.vmdata.mission_data —— 局内数据(coin / hero_num / hero_max_num)
|
||||
*/
|
||||
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";
|
||||
@@ -18,7 +52,12 @@ import { MissionHeroCompComp } from "./MissionHeroComp";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* MissionCardComp —— 卡牌系统核心控制器
|
||||
*
|
||||
* 管理 4 个卡牌槽位的抽卡分发、卡池升级、金币费用、
|
||||
* 英雄上限校验、场上英雄信息面板同步以及特殊卡执行。
|
||||
*/
|
||||
@ccclass('MissionCardComp')
|
||||
@ecs.register('MissionCard', false)
|
||||
export class MissionCardComp extends CCComp {
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
/**
|
||||
* @file MissionComp.ts
|
||||
* @description 任务(关卡)核心控制组件(UI + 逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理单局游戏的 **完整生命周期**:初始化 → 准备阶段 → 战斗阶段 → 结算。
|
||||
* 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。
|
||||
* 3. 管理怪物数量阈值(暂停 / 恢复刷怪的上下限)。
|
||||
* 4. 处理新一波事件(NewWave),进入准备阶段并发放金币奖励。
|
||||
* 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。
|
||||
* 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。
|
||||
*
|
||||
* 关键设计:
|
||||
* - mission_start() 初始化所有游戏数据 → 进入准备阶段 → 显示 loading。
|
||||
* - 准备阶段(enterPreparePhase):停止刷怪,显示开始按钮。
|
||||
* - 战斗阶段(to_fight):开始刷怪,隐藏按钮,由 update 驱动。
|
||||
* - 怪物数量管理采用 max/resume 双阈值:
|
||||
* * 超过 max → 暂停刷怪(stop_spawn_mon=true)
|
||||
* * 降至 resume 以下 → 恢复刷怪
|
||||
* - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。
|
||||
* - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。
|
||||
*
|
||||
* 依赖:
|
||||
* - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等)
|
||||
* - smc.vmdata.mission_data —— 局内数据(金币 / 波数 / 怪物数量等)
|
||||
* - FightSet —— 战斗常量配置
|
||||
* - CardInitCoins —— 初始金币数
|
||||
* - UIID.Victory —— 结算弹窗
|
||||
*/
|
||||
import { _decorator, Vec3,Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -19,7 +48,12 @@ const { ccclass, property } = _decorator;
|
||||
|
||||
//@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* MissionComp —— 任务(关卡)核心控制器
|
||||
*
|
||||
* 驱动单局游戏的完整流程:准备 → 战斗 → 结算。
|
||||
* 管理战斗计时、怪物数量控制、英雄全灭检测和金币奖励发放。
|
||||
*/
|
||||
@ccclass('MissionComp')
|
||||
@ecs.register('MissionComp', false)
|
||||
export class MissionComp extends CCComp {
|
||||
@@ -28,66 +62,105 @@ export class MissionComp extends CCComp {
|
||||
@property({ tooltip: "是否显示战斗内存观测面板" })
|
||||
private showMemoryPanel: boolean = false;
|
||||
|
||||
|
||||
// ======================== 配置参数 ========================
|
||||
|
||||
/** 怪物数量上限(超过后暂停刷怪) */
|
||||
private maxMonsterCount: number = 5;
|
||||
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
|
||||
private resumeMonsterCount: number = 3;
|
||||
/** 新一波金币奖励基础值 */
|
||||
private prepareBaseCoinReward: number = 100;
|
||||
/** 每一波金币增长值 */
|
||||
private prepareCoinWaveGrow: number = 1;
|
||||
/** 金币奖励上限 */
|
||||
private prepareCoinRewardCap: number = 500;
|
||||
|
||||
// VictoryComp:any = null;
|
||||
// reward:number = 0;
|
||||
// reward_num:number = 0;
|
||||
// ======================== 编辑器绑定节点 ========================
|
||||
|
||||
/** 开始战斗按钮 */
|
||||
@property(Node)
|
||||
start_btn:Node = null!
|
||||
/** 时间/波数显示节点 */
|
||||
@property(Node)
|
||||
time_node:Node = null!
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 战斗倒计时(秒) */
|
||||
FightTime:number = FightSet.FiIGHT_TIME
|
||||
/** 剩余复活次数 */
|
||||
revive_times: number = 1;
|
||||
/** 掉落奖励列表 */
|
||||
rewards:any[]=[]
|
||||
/** 累计游戏数据 */
|
||||
game_data:any={
|
||||
exp:0,
|
||||
gold:0,
|
||||
diamond:0
|
||||
}
|
||||
|
||||
/** 上一次显示的时间字符串(避免重复设置) */
|
||||
private lastTimeStr: string = "";
|
||||
/** 上一次显示的秒数(避免重复计算) */
|
||||
private lastTimeSecond: number = -1;
|
||||
/** 性能监控面板 Label 引用 */
|
||||
private memoryLabel: Label | null = null;
|
||||
/** 性能监控刷新计时器 */
|
||||
private memoryRefreshTimer: number = 0;
|
||||
/** 上一次性能文本(避免重复渲染) */
|
||||
private lastMemoryText: string = "";
|
||||
/** 帧间隔累加(用于计算平均 FPS) */
|
||||
private perfDtAcc: number = 0;
|
||||
/** 帧数计数 */
|
||||
private perfFrameCount: number = 0;
|
||||
/** 初始堆内存基准值(MB) */
|
||||
private heapBaseMB: number = -1;
|
||||
/** 堆内存峰值(MB) */
|
||||
private heapPeakMB: number = 0;
|
||||
/** 堆内存增长趋势(MB/分钟) */
|
||||
private heapTrendPerMinMB: number = 0;
|
||||
/** 趋势计算计时器 */
|
||||
private heapTrendTimer: number = 0;
|
||||
/** 趋势计算基准(MB) */
|
||||
private heapTrendBaseMB: number = -1;
|
||||
/** 怪物数量同步计时器(降低同步频率) */
|
||||
private monsterCountSyncTimer: number = 0;
|
||||
/** 当前波数 */
|
||||
private currentWave: number = 0;
|
||||
/** 上一次发放金币奖励的波数(防止重复发放) */
|
||||
private lastPrepareCoinWave: number = 0;
|
||||
|
||||
// ======================== ECS 查询匹配器(预缓存) ========================
|
||||
|
||||
/** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */
|
||||
private readonly heroViewMatcher = ecs.allOf(HeroViewComp);
|
||||
/** 匹配拥有 SkillView 的实体(技能视图) */
|
||||
private readonly skillViewMatcher = ecs.allOf(SkillView);
|
||||
/** 匹配拥有 HeroAttrsComp 的实体(英雄/怪物属性) */
|
||||
private readonly heroAttrsMatcher = ecs.allOf(HeroAttrsComp);
|
||||
|
||||
// 记录已触发的特殊刷怪索引
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad(){
|
||||
this.showMemoryPanel = false
|
||||
// 注册生命周期事件
|
||||
this.on(GameEvent.MissionStart,this.mission_start,this)
|
||||
// this.on(GameEvent.HeroDead,this.do_hero_dead,this)
|
||||
// this.on(GameEvent.FightEnd,this.fight_end,this)
|
||||
this.on(GameEvent.MissionEnd,this.mission_end,this)
|
||||
this.on(GameEvent.NewWave,this.onNewWave,this)
|
||||
this.on(GameEvent.DO_AD_BACK,this.do_ad,this)
|
||||
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||||
this.removeMemoryPanel()
|
||||
}
|
||||
|
||||
onDestroy(){
|
||||
this.start_btn?.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* 帧更新:
|
||||
* - 非播放 / 暂停状态 → 跳过
|
||||
* - 战斗中 → 同步怪物状态、更新计时器
|
||||
*/
|
||||
protected update(dt: number): void {
|
||||
if(!smc.mission.play) return
|
||||
if(smc.mission.pause) return
|
||||
@@ -96,10 +169,13 @@ export class MissionComp extends CCComp {
|
||||
if(smc.mission.stop_mon_action) return
|
||||
smc.vmdata.mission_data.fight_time+=dt
|
||||
this.FightTime-=dt
|
||||
// 检查特殊刷怪时间
|
||||
this.update_time();
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 时间显示 ========================
|
||||
|
||||
/** 更新时间/波数显示(仅在秒数变化时更新以减少 Label 操作) */
|
||||
update_time(){
|
||||
const time = Math.max(0, this.FightTime);
|
||||
const remainSecond = Math.floor(time);
|
||||
@@ -115,12 +191,16 @@ export class MissionComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 奖励与广告 ========================
|
||||
|
||||
//奖励发放
|
||||
/** 奖励发放(预留) */
|
||||
do_reward(){
|
||||
// 奖励发放
|
||||
}
|
||||
|
||||
/**
|
||||
* 广告回调处理:
|
||||
* 成功 → 增加刷新次数;失败 → 分发失败事件。
|
||||
*/
|
||||
do_ad(){
|
||||
if(this.ad_back()){
|
||||
oops.message.dispatchEvent(GameEvent.AD_BACK_TRUE)
|
||||
@@ -129,15 +209,24 @@ export class MissionComp extends CCComp {
|
||||
oops.message.dispatchEvent(GameEvent.AD_BACK_FALSE)
|
||||
}
|
||||
}
|
||||
|
||||
/** 广告观看结果(预留,默认返回 true) */
|
||||
ad_back(){
|
||||
return true
|
||||
}
|
||||
|
||||
// ======================== 任务生命周期 ========================
|
||||
|
||||
/**
|
||||
* 任务开始:
|
||||
* 1. 取消上一局延迟回调。
|
||||
* 2. 清理残留实体。
|
||||
* 3. 初始化全部局内数据。
|
||||
* 4. 分发 FightReady 事件。
|
||||
* 5. 进入准备阶段并显示 loading。
|
||||
*/
|
||||
async mission_start(){
|
||||
// 防止上一局的 fight_end 延迟回调干扰新局
|
||||
this.unscheduleAllCallbacks();
|
||||
// 确保清理上一局的残留实体
|
||||
this.cleanComponents();
|
||||
this.node.active=true
|
||||
this.data_init()
|
||||
@@ -148,17 +237,29 @@ export class MissionComp extends CCComp {
|
||||
this.scheduleOnce(()=>{
|
||||
loading.active=false
|
||||
},0.5)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入战斗:
|
||||
* - 恢复刷怪
|
||||
* - 标记战斗中
|
||||
* - 隐藏开始按钮
|
||||
* - 分发 FightStart 事件
|
||||
*/
|
||||
to_fight(){
|
||||
smc.mission.stop_spawn_mon = false;
|
||||
smc.mission.in_fight=true
|
||||
smc.vmdata.mission_data.in_fight = true
|
||||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
|
||||
oops.message.dispatchEvent(GameEvent.FightStart) //GameSetMonComp 监听刷怪
|
||||
oops.message.dispatchEvent(GameEvent.FightStart)
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入准备阶段:
|
||||
* - 标记非战斗
|
||||
* - 暂停刷怪
|
||||
* - 显示开始按钮
|
||||
*/
|
||||
private enterPreparePhase() {
|
||||
smc.mission.in_fight = false;
|
||||
smc.vmdata.mission_data.in_fight = false
|
||||
@@ -166,6 +267,7 @@ export class MissionComp extends CCComp {
|
||||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = true;
|
||||
}
|
||||
|
||||
/** 开始战斗按钮点击回调 */
|
||||
private onStartFightBtnClick() {
|
||||
if (!smc.mission.play) return;
|
||||
if (smc.mission.pause) return;
|
||||
@@ -174,11 +276,16 @@ export class MissionComp extends CCComp {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 打开结算弹窗:
|
||||
* - 暂停游戏
|
||||
* - 打开 VictoryComp 弹窗
|
||||
*
|
||||
* @param e 事件对象(未使用)
|
||||
* @param is_hero_dead 是否因英雄全灭触发
|
||||
*/
|
||||
open_Victory(e:any,is_hero_dead: boolean = false){
|
||||
// 暂停游戏循环和怪物行为
|
||||
// smc.mission.play = false;
|
||||
smc.mission.pause = true;
|
||||
// oops.message.dispatchEvent(GameEvent.FightEnd,{victory:false})
|
||||
mLogger.log(this.debugMode, 'MissionComp', " open_Victory",is_hero_dead,this.revive_times)
|
||||
oops.gui.open(UIID.Victory,{
|
||||
victory:false,
|
||||
@@ -189,10 +296,8 @@ export class MissionComp extends CCComp {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 战斗结束:延迟清理组件和对象池 */
|
||||
fight_end(){
|
||||
// mLogger.log(this.debugMode, 'MissionComp', "任务结束")
|
||||
// 延迟0.5秒后执行任务结束逻辑
|
||||
this.scheduleOnce(() => {
|
||||
smc.mission.play=false
|
||||
this.cleanComponents()
|
||||
@@ -200,9 +305,14 @@ export class MissionComp extends CCComp {
|
||||
}, 0.5)
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务结束(完全退出关卡):
|
||||
* - 取消所有延迟回调
|
||||
* - 重置全局标志
|
||||
* - 清理组件和对象池
|
||||
* - 隐藏节点
|
||||
*/
|
||||
mission_end(){
|
||||
// mLogger.log(this.debugMode, 'MissionComp', " mission_end")
|
||||
// 合并 FightEnd 逻辑:清理组件、停止游戏循环
|
||||
this.unscheduleAllCallbacks();
|
||||
smc.mission.play=false
|
||||
smc.mission.pause = false;
|
||||
@@ -214,8 +324,14 @@ export class MissionComp extends CCComp {
|
||||
this.node.active=false
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全部局内数据:
|
||||
* - 全局运行标志
|
||||
* - 战斗时间 / 怪物数量 / 金币 / 波数
|
||||
* - 奖励列表 / 复活次数
|
||||
* - 性能监控基准值
|
||||
*/
|
||||
data_init(){
|
||||
//局内数据初始化 smc 数据初始化
|
||||
smc.mission.play = true;
|
||||
smc.mission.pause = false;
|
||||
smc.mission.stop_mon_action = false;
|
||||
@@ -227,8 +343,8 @@ export class MissionComp extends CCComp {
|
||||
smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount))
|
||||
this.currentWave = 1;
|
||||
this.FightTime=FightSet.FiIGHT_TIME
|
||||
this.rewards=[] // 改为数组,用于存储掉落物品列表
|
||||
this.revive_times = 1; // 每次任务开始重置复活次数
|
||||
this.rewards=[]
|
||||
this.revive_times = 1;
|
||||
this.lastTimeStr = "";
|
||||
this.lastTimeSecond = -1;
|
||||
this.memoryRefreshTimer = 0;
|
||||
@@ -243,15 +359,20 @@ export class MissionComp extends CCComp {
|
||||
this.monsterCountSyncTimer = 0;
|
||||
this.lastPrepareCoinWave = 0;
|
||||
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins));
|
||||
|
||||
// 重置全局属性加成和主角引用 (确保新一局数据干净)
|
||||
// smc.role = null;
|
||||
|
||||
// 重置英雄数据,确保新一局是初始状态
|
||||
|
||||
// mLogger.log(this.debugMode, 'MissionComp', "局内数据初始化",smc.vmdata.mission_data)
|
||||
}
|
||||
|
||||
// ======================== 波次管理 ========================
|
||||
|
||||
/**
|
||||
* 新一波事件回调:
|
||||
* 1. 进入准备阶段。
|
||||
* 2. 更新当前波数。
|
||||
* 3. 发放本波金币奖励。
|
||||
* 4. 刷新时间显示。
|
||||
*
|
||||
* @param event 事件名
|
||||
* @param data { wave: number }
|
||||
*/
|
||||
private onNewWave(event: string, data: any) {
|
||||
const wave = Number(data?.wave ?? 0);
|
||||
if (wave <= 0) return;
|
||||
@@ -263,6 +384,13 @@ export class MissionComp extends CCComp {
|
||||
this.update_time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按波数发放金币奖励:
|
||||
* reward = min(cap, base + (wave - 1) × grow)
|
||||
* 仅在波数首次到达时发放,防止重复。
|
||||
*
|
||||
* @param wave 当前波数
|
||||
*/
|
||||
private grantPrepareCoinByWave(wave: number) {
|
||||
if (wave <= 0) return;
|
||||
if (wave <= this.lastPrepareCoinWave) return;
|
||||
@@ -280,12 +408,26 @@ export class MissionComp extends CCComp {
|
||||
mLogger.log(this.debugMode, 'MissionComp', "prepare coin reward", { wave, reward, coin: smc.vmdata.mission_data.coin });
|
||||
}
|
||||
|
||||
// ======================== 怪物数量管理 ========================
|
||||
|
||||
/**
|
||||
* 获取怪物数量阈值配置。
|
||||
* @returns { max: 刷怪上限, resume: 恢复刷怪阈值 }
|
||||
*/
|
||||
private getMonsterThresholds(): { max: number; resume: number } {
|
||||
const max = Math.max(1, Math.floor(this.maxMonsterCount));
|
||||
const resume = Math.min(max - 1, Math.max(0, Math.floor(this.resumeMonsterCount)));
|
||||
return { max, resume };
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步怪物刷新状态(降频执行,每 0.2 秒一次):
|
||||
* 1. 遍历所有 HeroAttrsComp 实体,统计怪物和英雄数量。
|
||||
* 2. 检测英雄全灭。
|
||||
* 3. 根据 max/resume 阈值切换 stop_spawn_mon 状态。
|
||||
*
|
||||
* @param dt 帧间隔
|
||||
*/
|
||||
private syncMonsterSpawnState(dt: number) {
|
||||
this.monsterCountSyncTimer += dt;
|
||||
if (dt > 0 && this.monsterCountSyncTimer < 0.2) return;
|
||||
@@ -309,12 +451,18 @@ export class MissionComp extends CCComp {
|
||||
smc.vmdata.mission_data.mon_max = max;
|
||||
const stopSpawn = !!smc.mission.stop_spawn_mon;
|
||||
if (stopSpawn) {
|
||||
// 降至恢复阈值以下 → 恢复刷怪
|
||||
if (monsterCount <= resume) smc.mission.stop_spawn_mon = false;
|
||||
return;
|
||||
}
|
||||
// 超过上限 → 暂停刷怪
|
||||
if (monsterCount >= max) smc.mission.stop_spawn_mon = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 英雄全灭检测:若场上无存活英雄且处于战斗中,触发结算弹窗。
|
||||
* @param heroCount 当前存活英雄数量
|
||||
*/
|
||||
private handleHeroWipe(heroCount: number) {
|
||||
if (heroCount > 0) return;
|
||||
if (!smc.mission.play || smc.mission.pause) return;
|
||||
@@ -324,6 +472,9 @@ export class MissionComp extends CCComp {
|
||||
this.open_Victory(null, true);
|
||||
}
|
||||
|
||||
// ======================== 清理 ========================
|
||||
|
||||
/** 清理所有英雄和技能 ECS 实体 */
|
||||
private cleanComponents() {
|
||||
const heroEntities: ecs.Entity[] = [];
|
||||
ecs.query(this.heroViewMatcher).forEach(entity => {
|
||||
@@ -341,6 +492,7 @@ export class MissionComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 回收所有战斗对象池(Monster / Skill / Tooltip)并清理场景节点 */
|
||||
private clearBattlePools() {
|
||||
Monster.clearPools();
|
||||
Skill.clearPools();
|
||||
@@ -348,6 +500,7 @@ export class MissionComp extends CCComp {
|
||||
this.clearBattleSceneNodes();
|
||||
}
|
||||
|
||||
/** 清理战斗场景中的 HERO 和 SKILL 根节点下的所有子节点 */
|
||||
private clearBattleSceneNodes() {
|
||||
const scene = smc.map?.MapView?.scene;
|
||||
const layer = scene?.entityLayer?.node;
|
||||
@@ -366,6 +519,7 @@ export class MissionComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取战斗层的英雄和技能节点数量(用于性能监控) */
|
||||
private getBattleLayerNodeCount() {
|
||||
const scene = smc.map?.MapView?.scene;
|
||||
const layer = scene?.entityLayer?.node;
|
||||
@@ -378,8 +532,11 @@ export class MissionComp extends CCComp {
|
||||
};
|
||||
}
|
||||
|
||||
// ======================== 性能监控面板 ========================
|
||||
|
||||
/** 性能监控相关代码 */
|
||||
|
||||
/** 初始化性能监控面板:在 time_node 下创建 Label */
|
||||
private initMemoryPanel() {
|
||||
if (!this.showMemoryPanel || !this.time_node) return;
|
||||
let panel = this.time_node.getChildByName("mem_panel");
|
||||
@@ -397,6 +554,7 @@ export class MissionComp extends CCComp {
|
||||
this.memoryLabel = label;
|
||||
}
|
||||
|
||||
/** 移除性能监控面板 */
|
||||
private removeMemoryPanel() {
|
||||
const panel = this.time_node?.getChildByName("mem_panel");
|
||||
if (panel) {
|
||||
@@ -407,6 +565,10 @@ export class MissionComp extends CCComp {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新性能监控面板内容(每 0.5 秒一次):
|
||||
* 显示 堆内存 / 增长趋势 / 帧率 / 实体数量 / 对象池状态 等信息。
|
||||
*/
|
||||
private updateMemoryPanel(dt: number) {
|
||||
if (!this.showMemoryPanel || !this.memoryLabel) return;
|
||||
this.perfDtAcc += dt;
|
||||
@@ -466,7 +628,7 @@ export class MissionComp extends CCComp {
|
||||
|
||||
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
/**
|
||||
* @file MissionHeroComp.ts
|
||||
* @description 英雄召唤与合成管理组件(逻辑层 + 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 处理 **英雄召唤**:接收 CallHero 事件 → 通过串行队列执行召唤。
|
||||
* 2. 处理 **英雄合成**:检测同 UUID 同等级英雄是否达到合成条件 →
|
||||
* 执行合成动画 → 销毁素材 → 生成高一级英雄。
|
||||
* 3. 支持 **链式合成**:合成完成后自动检测更高等级是否也满足合成条件。
|
||||
* 4. 管理英雄的出生点和掉落动画。
|
||||
*
|
||||
* 关键设计:
|
||||
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**,
|
||||
* 避免同帧并发导致合成判断错误。
|
||||
* - handleSingleSummon() 在每次召唤后检测是否触发合成。
|
||||
* - mergeGroupHeroes() 执行完整合成流程:
|
||||
* 聚合属性 → 向出生点汇聚动画 → 爆点特效 → 生成高级英雄。
|
||||
* - merge_need_count 控制合成所需数量(2 合 1 或 3 合 1)。
|
||||
* - merge_max_lv 控制合成上限等级。
|
||||
*
|
||||
* 依赖:
|
||||
* - Hero(hero/Hero.ts)—— 英雄 ECS 实体类
|
||||
* - HeroAttrsComp —— 英雄属性组件
|
||||
* - HeroInfo / HeroPos / HType(heroSet)—— 英雄静态配置
|
||||
* - FightSet —— 战斗常量(MERGE_NEED / MERGE_MAX)
|
||||
* - oneCom —— 一次性特效组件(控制爆点特效生命周期)
|
||||
*/
|
||||
import { _decorator, instantiate, Prefab, v3, 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";
|
||||
@@ -12,16 +39,26 @@ import { FacSet, FightSet } from "../common/config/GameSet";
|
||||
import { oneCom } from "../skill/oncend";
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* MissionHeroCompComp —— 英雄召唤与合成管理器
|
||||
*
|
||||
* 管理英雄的召唤请求队列、出生动画和合成系统。
|
||||
* 合成支持 2 合 1 或 3 合 1,且可链式合成至上限等级。
|
||||
*/
|
||||
@ccclass('MissionHeroCompComp')
|
||||
@ecs.register('MissionHeroComp', false)
|
||||
export class MissionHeroCompComp extends CCComp {
|
||||
/** 英雄出生时的掉落高度,用于表现从空中落地 */
|
||||
// ======================== 常量 ========================
|
||||
|
||||
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
||||
private static readonly HERO_DROP_HEIGHT = 260
|
||||
/** 近战起始出生 X */
|
||||
/** 近战英雄起始出生 X 坐标 */
|
||||
private static readonly HERO_SPAWN_START_MELEE_X = -280
|
||||
/** 远程(含中程)起始出生 X */
|
||||
/** 远程(含中程)英雄起始出生 X 坐标 */
|
||||
private static readonly HERO_SPAWN_START_RANGED_X = -280
|
||||
|
||||
// ======================== 运行时属性 ========================
|
||||
|
||||
/** 预留计时器 */
|
||||
timer:Timer=new Timer(2)
|
||||
/** 预留状态:友方是否全部死亡 */
|
||||
@@ -30,29 +67,32 @@ export class MissionHeroCompComp extends CCComp {
|
||||
current_hero_uuid:number=0
|
||||
/** 当前英雄数量缓存 */
|
||||
current_hero_num:number=-1
|
||||
/** 合成规则:2 合 1 或 3 合 1 */
|
||||
/** 合成规则:需要几个同级英雄才能合成(2 或 3) */
|
||||
merge_need_count:number=FightSet.MERGE_NEED
|
||||
/** 允许合成的最高等级 */
|
||||
/** 允许合成的最高等级(合成产物不超过此等级) */
|
||||
merge_max_lv:number=FightSet.MERGE_MAX
|
||||
/** 是否正在执行一次合成流程 */
|
||||
/** 是否正在执行一次合成流程(防止并发) */
|
||||
is_merging:boolean=false
|
||||
/** 是否正在消费召唤队列,防止并发处理 */
|
||||
/** 是否正在消费召唤队列(防止并发) */
|
||||
is_processing_queue:boolean=false
|
||||
/** 召唤请求队列,保证召唤与合成串行 */
|
||||
/** 召唤请求队列:保证召唤与合成按顺序串行执行 */
|
||||
summon_queue:{ uuid: number; hero_lv: number; pool_lv: number }[]=[]
|
||||
/** 预留英雄列表 */
|
||||
heros:any=[]
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad(){
|
||||
/** 节点事件监听 */
|
||||
// 注册节点级事件
|
||||
this.on(GameEvent.FightReady,this.fight_ready,this)
|
||||
this.on(GameEvent.Zhaohuan,this.zhao_huan,this)
|
||||
this.on(GameEvent.MissionEnd,this.clear_heros,this)
|
||||
/** 全局消息监听 */
|
||||
// 注册全局消息
|
||||
oops.message.on(GameEvent.CallHero,this.call_hero,this)
|
||||
}
|
||||
|
||||
onDestroy(){
|
||||
/** 清理监听,避免节点销毁后仍响应消息 */
|
||||
// 清理全部监听
|
||||
oops.message.off(GameEvent.CallHero,this.call_hero,this)
|
||||
oops.message.off(GameEvent.FightReady,this.fight_ready,this)
|
||||
oops.message.off(GameEvent.Zhaohuan,this.zhao_huan,this)
|
||||
@@ -60,29 +100,35 @@ export class MissionHeroCompComp extends CCComp {
|
||||
}
|
||||
|
||||
start() {
|
||||
// this.test_call()
|
||||
}
|
||||
/** 关卡结束时,清理全部存活英雄 */
|
||||
|
||||
// ======================== 事件处理 ========================
|
||||
|
||||
/** 关卡结束时清理全部存活英雄 ECS 实体 */
|
||||
clear_heros(){
|
||||
const heroes = this.getAliveHeroes();
|
||||
for (let i = 0; i < heroes.length; i++) {
|
||||
heroes[i].destroy();
|
||||
}
|
||||
}
|
||||
/** 战斗准备阶段重置出战英雄计数 */
|
||||
|
||||
/** 战斗准备阶段:重置出战英雄计数 */
|
||||
fight_ready(){
|
||||
smc.vmdata.mission_data.hero_num=0
|
||||
}
|
||||
// protected update(dt: number): void {
|
||||
|
||||
// }
|
||||
|
||||
/** 预留:召唤事件扩展入口 */
|
||||
private zhao_huan(event: string, args: any){
|
||||
|
||||
}
|
||||
|
||||
/** 召唤请求入口:归一化参数并进入串行队列 */
|
||||
/**
|
||||
* 召唤请求入口:
|
||||
* 从事件参数中提取 uuid / hero_lv / pool_lv,放入串行队列。
|
||||
*
|
||||
* @param event 事件名
|
||||
* @param args { uuid, hero_lv, pool_lv }
|
||||
*/
|
||||
private async call_hero(event: string, args: any){
|
||||
const payload = args ?? event;
|
||||
const uuid = Number(payload?.uuid ?? 1001);
|
||||
@@ -91,7 +137,19 @@ export class MissionHeroCompComp extends CCComp {
|
||||
this.summon_queue.push({ uuid, hero_lv, pool_lv });
|
||||
this.processSummonQueue();
|
||||
}
|
||||
/** 添加英雄:固定出生点上方生成,再落至落点 */
|
||||
|
||||
// ======================== 英雄生成 ========================
|
||||
|
||||
/**
|
||||
* 生成一个英雄 ECS 实体:
|
||||
* - 计算出生点(空中)和落点(地面)。
|
||||
* - 调用 hero.load() 初始化并播放掉落动画。
|
||||
*
|
||||
* @param uuid 英雄 UUID
|
||||
* @param hero_lv 英雄等级
|
||||
* @param pool_lv 卡池等级
|
||||
* @returns 创建的 Hero 实体
|
||||
*/
|
||||
private addHero(uuid:number=1001,hero_lv:number=1, pool_lv:number=1) {
|
||||
console.log("addHero uuid:",uuid)
|
||||
let hero = ecs.getEntity<Hero>(Hero);
|
||||
@@ -102,6 +160,13 @@ export class MissionHeroCompComp extends CCComp {
|
||||
return hero;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算英雄落点位置。
|
||||
* Y 坐标来自 HeroPos 配置,X 坐标根据英雄类型(近战/远程)决定。
|
||||
*
|
||||
* @param uuid 英雄 UUID
|
||||
* @returns 落点 Vec3
|
||||
*/
|
||||
private resolveHeroLandingPos(uuid: number): Vec3 {
|
||||
const hero_pos = 0;
|
||||
const baseY = HeroPos[hero_pos].pos.y;
|
||||
@@ -109,6 +174,11 @@ export class MissionHeroCompComp extends CCComp {
|
||||
return v3(startX, baseY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据英雄类型决定出生 X 坐标。
|
||||
* @param uuid 英雄 UUID
|
||||
* @returns 近战 or 远程的起始 X
|
||||
*/
|
||||
private resolveSpawnStartX(uuid: number): number {
|
||||
const heroType = HeroInfo[uuid]?.type;
|
||||
return heroType === HType.Melee
|
||||
@@ -116,7 +186,16 @@ export class MissionHeroCompComp extends CCComp {
|
||||
: MissionHeroCompComp.HERO_SPAWN_START_RANGED_X;
|
||||
}
|
||||
|
||||
/** 添加合成后的新英雄,并覆盖为聚合后的属性 */
|
||||
/**
|
||||
* 生成合成后的高级英雄,并覆盖为聚合后的属性。
|
||||
*
|
||||
* @param uuid 英雄 UUID
|
||||
* @param hero_lv 合成后等级
|
||||
* @param pool_lv 卡池等级
|
||||
* @param ap 聚合后攻击力
|
||||
* @param hp_max 聚合后最大生命值
|
||||
* @returns 实际生成的英雄等级
|
||||
*/
|
||||
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number): number {
|
||||
const hero = this.addHero(uuid, hero_lv, pool_lv);
|
||||
const model = hero.get(HeroAttrsComp);
|
||||
@@ -128,7 +207,9 @@ export class MissionHeroCompComp extends CCComp {
|
||||
return model.lv;
|
||||
}
|
||||
|
||||
/** 获取当前全部存活友方英雄 */
|
||||
// ======================== 英雄查询 ========================
|
||||
|
||||
/** 获取当前全部存活友方英雄 ECS 实体列表 */
|
||||
private getAliveHeroes(): Hero[] {
|
||||
const heroes: Hero[] = [];
|
||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||||
@@ -141,7 +222,15 @@ export class MissionHeroCompComp extends CCComp {
|
||||
return heroes;
|
||||
}
|
||||
|
||||
/** 挑选可参与本次合成的英雄组 */
|
||||
/**
|
||||
* 从存活英雄中挑选可参与本次合成的英雄组。
|
||||
*
|
||||
* @param aliveHeroes 存活英雄列表
|
||||
* @param uuid 目标英雄 UUID
|
||||
* @param hero_lv 目标等级
|
||||
* @param needCount 合成需要数量
|
||||
* @returns 匹配的英雄数组(长度 = needCount 或不足)
|
||||
*/
|
||||
private pickMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number, needCount: number = 3): Hero[] {
|
||||
const mergeHeroes: Hero[] = [];
|
||||
for (let i = 0; i < aliveHeroes.length; i++) {
|
||||
@@ -155,7 +244,7 @@ export class MissionHeroCompComp extends CCComp {
|
||||
return mergeHeroes;
|
||||
}
|
||||
|
||||
/** 统计满足同 uuid、同等级的可合成英雄数量 */
|
||||
/** 统计满足同 UUID 同等级的可合成英雄数量 */
|
||||
private countMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number): number {
|
||||
let count = 0;
|
||||
for (let i = 0; i < aliveHeroes.length; i++) {
|
||||
@@ -168,17 +257,32 @@ export class MissionHeroCompComp extends CCComp {
|
||||
return count;
|
||||
}
|
||||
|
||||
/** 读取当前合成需要数量,仅支持 2 或 3 */
|
||||
// ======================== 合成规则 ========================
|
||||
|
||||
/**
|
||||
* 读取合成所需数量(仅支持 2 或 3)。
|
||||
* 由 FightSet.MERGE_NEED 配置。
|
||||
*/
|
||||
private getMergeNeedCount(): number {
|
||||
return this.merge_need_count === 2 ? 2 : 3;
|
||||
}
|
||||
|
||||
/** 判断该等级是否还能继续向上合成 */
|
||||
/**
|
||||
* 判断该等级是否还能继续向上合成。
|
||||
* @param hero_lv 当前等级
|
||||
* @returns true = 可以合成(未达上限)
|
||||
*/
|
||||
private canMergeLevel(hero_lv: number): boolean {
|
||||
return hero_lv < Math.max(1, this.merge_max_lv);
|
||||
}
|
||||
|
||||
/** 串行消费召唤队列,避免同帧并发触发多次合成导致状态错乱 */
|
||||
// ======================== 召唤队列 ========================
|
||||
|
||||
/**
|
||||
* 串行消费召唤队列:
|
||||
* 使用 is_processing_queue 标志防止同帧多次调用。
|
||||
* 逐个取出队列中的请求并处理。
|
||||
*/
|
||||
private async processSummonQueue() {
|
||||
if (this.is_processing_queue) return;
|
||||
this.is_processing_queue = true;
|
||||
@@ -193,7 +297,16 @@ export class MissionHeroCompComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理单次召唤:先生成,再检测是否触发合成,再尝试链式合成 */
|
||||
/**
|
||||
* 处理单次召唤:
|
||||
* 1. 生成英雄。
|
||||
* 2. 检测是否满足合成条件。
|
||||
* 3. 满足则执行合成 + 链式合成。
|
||||
*
|
||||
* @param uuid 英雄 UUID
|
||||
* @param hero_lv 英雄等级
|
||||
* @param pool_lv 卡池等级
|
||||
*/
|
||||
private async handleSingleSummon(uuid: number, hero_lv: number, pool_lv: number = 1) {
|
||||
this.addHero(uuid, hero_lv, pool_lv);
|
||||
if (!this.canMergeLevel(hero_lv)) return;
|
||||
@@ -210,7 +323,15 @@ export class MissionHeroCompComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/** 将一组合成素材向出生点汇聚并销毁,全部完成后返回 */
|
||||
// ======================== 合成动画 ========================
|
||||
|
||||
/**
|
||||
* 将一组合成素材英雄向出生点汇聚并销毁。
|
||||
* 所有素材动画完成后 Promise resolve。
|
||||
*
|
||||
* @param mergeHeroes 合成素材英雄数组
|
||||
* @param spawnPos 汇聚目标位置
|
||||
*/
|
||||
private mergeDestroyAtBirth(mergeHeroes: Hero[], spawnPos: Vec3): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let doneCount = 0;
|
||||
@@ -231,7 +352,12 @@ export class MissionHeroCompComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 播放合成爆点特效,使用 oneCom 控制特效生命周期 */
|
||||
/**
|
||||
* 播放合成爆点特效(使用 oneCom 控制生命周期)。
|
||||
* 延迟 0.4 秒后 resolve。
|
||||
*
|
||||
* @param worldPos 特效播放位置
|
||||
*/
|
||||
private playMergeBoomFx(worldPos: Vec3): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const scene = smc.map?.MapView?.scene;
|
||||
@@ -257,8 +383,21 @@ export class MissionHeroCompComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 执行一次完整合成:聚合属性、销毁素材、播放特效、生成高一级英雄 */
|
||||
/**
|
||||
* 执行一次完整合成流程:
|
||||
* 1. 聚合素材的 AP 和 HP。
|
||||
* 2. 将素材向出生点汇聚并销毁。
|
||||
* 3. 播放爆点特效。
|
||||
* 4. 生成高一级英雄(属性为聚合值)。
|
||||
*
|
||||
* @param mergeHeroes 合成素材
|
||||
* @param uuid 英雄 UUID
|
||||
* @param hero_lv 素材等级
|
||||
* @param pool_lv 卡池等级
|
||||
* @returns 合成产物的实际等级
|
||||
*/
|
||||
private async mergeGroupHeroes(mergeHeroes: Hero[], uuid: number, hero_lv: number, pool_lv: number): Promise<number> {
|
||||
// 聚合属性
|
||||
let sumAp = 0;
|
||||
let sumHpMax = 0;
|
||||
for (let i = 0; i < mergeHeroes.length; i++) {
|
||||
@@ -267,14 +406,23 @@ export class MissionHeroCompComp extends CCComp {
|
||||
sumAp += model.ap;
|
||||
sumHpMax += model.hp_max;
|
||||
}
|
||||
// 计算出生点
|
||||
const landingPos = this.resolveHeroLandingPos(uuid);
|
||||
const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
|
||||
// 汇聚 → 特效 → 生成
|
||||
await this.mergeDestroyAtBirth(mergeHeroes, spawnPos);
|
||||
await this.playMergeBoomFx(spawnPos);
|
||||
return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax);
|
||||
}
|
||||
|
||||
/** 链式合成:当前等级合成完成后,继续尝试更高等级,直到条件不满足 */
|
||||
/**
|
||||
* 链式合成:合成完成后继续检测更高等级是否也满足条件。
|
||||
* 最多循环 20 次作为安全上限。
|
||||
*
|
||||
* @param uuid 英雄 UUID
|
||||
* @param startLv 起始检测等级
|
||||
* @param pool_lv 卡池等级
|
||||
*/
|
||||
private async tryChainMerge(uuid: number, startLv: number, pool_lv: number) {
|
||||
let checkLv = Math.max(1, startLv);
|
||||
const needCount = this.getMergeNeedCount();
|
||||
@@ -300,7 +448,7 @@ export class MissionHeroCompComp extends CCComp {
|
||||
|
||||
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件时触发,用于自定义释放逻辑 */
|
||||
/** ECS 组件移除时触发(当前不销毁节点,保留引用) */
|
||||
reset() {
|
||||
// this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/**
|
||||
* @file MissionHomeComp.ts
|
||||
* @description 任务主页组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 作为玩家进入游戏后第一个看到的界面(主菜单/大厅)。
|
||||
* 2. 提供"开始任务"按钮,触发 GameEvent.MissionStart 进入战斗。
|
||||
* 3. 提供"排行榜"按钮,打开 RanksComp 弹窗。
|
||||
* 4. 监听 MissionEnd 事件,任务结束后自动切回主页。
|
||||
*
|
||||
* 关键设计:
|
||||
* - start_mission() 分发 MissionStart 事件并隐藏自身节点。
|
||||
* - mission_end() 响应后重新显示主页。
|
||||
* - isWxClient() 检测是否运行在微信小游戏环境。
|
||||
*
|
||||
* 依赖:
|
||||
* - GameEvent.MissionStart / MissionEnd —— 游戏生命周期事件
|
||||
* - UIID.Ranks —— 排行榜弹窗 ID
|
||||
*/
|
||||
import { _decorator, instantiate, Prefab, resources, Sprite, SpriteAtlas, UITransform ,Node} from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -10,61 +29,88 @@ import { UIID } from "../common/config/GameUIConfig";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* MissionHomeComp —— 任务主页视图组件
|
||||
*
|
||||
* 游戏大厅界面,提供开始战斗和查看排行的入口。
|
||||
*/
|
||||
@ccclass('MissionHomeComp')
|
||||
@ecs.register('MissionHome', false)
|
||||
export class MissionHomeComp extends CCComp {
|
||||
/** 调试日志开关 */
|
||||
debugMode: boolean = false;
|
||||
|
||||
|
||||
/** 主页按钮节点(预留) */
|
||||
@property(Node)
|
||||
home_btn=null!
|
||||
/** 英雄图鉴按钮节点(预留) */
|
||||
@property(Node)
|
||||
hero_btn=null!
|
||||
/** 排行榜按钮节点 */
|
||||
@property(Node)
|
||||
rank_btn=null!
|
||||
|
||||
|
||||
/** 注册任务结束事件 */
|
||||
protected onLoad(): void {
|
||||
this.on(GameEvent.MissionEnd,this.mission_end,this)
|
||||
}
|
||||
/** 视图层逻辑代码分离演示 */
|
||||
|
||||
/** 启动时显示主页 */
|
||||
start() {
|
||||
this.home_active()
|
||||
}
|
||||
|
||||
onEnable(){
|
||||
}
|
||||
|
||||
update(dt:number){
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始任务:
|
||||
* 1. 打印日志。
|
||||
* 2. 分发 MissionStart 事件,驱动 MissionComp / MissionCardComp 初始化战斗。
|
||||
* 3. 隐藏主页节点。
|
||||
*/
|
||||
start_mission() {
|
||||
mLogger.log(this.debugMode, 'MissionHomeComp', "start_mission")
|
||||
oops.message.dispatchEvent(GameEvent.MissionStart, {})
|
||||
this.node.active=false;
|
||||
}
|
||||
|
||||
/** 打开排行榜弹窗 */
|
||||
openRanks(){
|
||||
oops.gui.open(UIID.Ranks)
|
||||
}
|
||||
|
||||
/** 任务结束回调:重新显示主页 */
|
||||
mission_end(){
|
||||
mLogger.log(this.debugMode, 'MissionHomeComp', "[MissionHomeComp]=>mission_end")
|
||||
this.home_active()
|
||||
}
|
||||
|
||||
/** 激活主页显示:刷新数据并显示节点 */
|
||||
home_active(){
|
||||
this.uodate_data()
|
||||
this.node.active=true
|
||||
}
|
||||
|
||||
/** 更新主页显示数据(预留) */
|
||||
uodate_data(){
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否运行在微信小游戏环境。
|
||||
* @returns true = 微信小游戏环境
|
||||
*/
|
||||
isWxClient(){
|
||||
return typeof wx !== 'undefined' && typeof (wx as any).getSystemInfoSync === 'function';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
/**
|
||||
* @file MissionMonComp.ts
|
||||
* @description 怪物(Monster)波次刷新管理组件(逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 分配怪物到固定槽位。
|
||||
* 2. 管理 6 个固定刷怪槽位的占用状态,支持 Boss 占 2 格。
|
||||
* 3. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。
|
||||
* 4. 自动推进波次:当前波所有怪物被清除后自动进入下一波。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 全场固定 6 个槽位(索引 0-5),每个槽位占固定 X 坐标。
|
||||
* - Boss 占 2 个连续槽位,只能放在 0、2、4 号位。
|
||||
* - slotOccupiedEids 记录每个槽位占用的怪物 ECS 实体 ID。
|
||||
* - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。
|
||||
* - refreshSlotOccupancy() 定期检查槽位占用的实体是否仍存活,清除已死亡的占用。
|
||||
* - tryAdvanceWave() 在所有怪物死亡后自动推进波次。
|
||||
*
|
||||
* 怪物属性计算公式:
|
||||
* ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias)
|
||||
* hp = floor((base_hp + stage × grow_hp) × SpawnPowerBias)
|
||||
* 其中 stage = currentWave - 1
|
||||
*
|
||||
* 依赖:
|
||||
* - RogueConfig —— 怪物类型、成长值、波次配置
|
||||
* - Monster(hero/Mon.ts)—— 怪物 ECS 实体类
|
||||
* - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置)
|
||||
* - HeroAttrsComp / MoveComp —— 怪物属性和移动组件
|
||||
* - BoxSet.GAME_LINE —— 地面基准 Y 坐标
|
||||
*/
|
||||
import { _decorator, v3, Vec3 } from "cc";
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
@@ -13,34 +43,63 @@ import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { MoveComp } from "../hero/MoveComp";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* MissionMonCompComp —— 怪物波次刷新管理器
|
||||
*
|
||||
* 每波开始时根据 WaveSlotConfig 配置分配怪物到固定槽位,
|
||||
* 战斗中监控槽位状态,所有怪物消灭后自动推进到下一波。
|
||||
*/
|
||||
@ccclass('MissionMonCompComp')
|
||||
@ecs.register('MissionMonComp', false)
|
||||
export class MissionMonCompComp extends CCComp {
|
||||
// ======================== 常量 ========================
|
||||
|
||||
/** Boss 的渲染优先级偏移(确保 Boss 始终渲染在最前) */
|
||||
private static readonly BOSS_RENDER_PRIORITY = 1000000;
|
||||
/** 第一个槽位的 X 坐标起点 */
|
||||
private static readonly MON_SLOT_START_X = 30;
|
||||
/** 槽位间的 X 间距 */
|
||||
private static readonly MON_SLOT_X_INTERVAL = 60;
|
||||
/** 怪物出生掉落高度 */
|
||||
private static readonly MON_DROP_HEIGHT = 280;
|
||||
/** 最大槽位数 */
|
||||
private static readonly MAX_SLOTS = 6;
|
||||
|
||||
// ======================== 编辑器属性 ========================
|
||||
|
||||
@property({ tooltip: "是否启用调试日志" })
|
||||
private debugMode: boolean = false;
|
||||
|
||||
// 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪)
|
||||
// 约定:队列里的怪会优先于常规刷新处理
|
||||
// ======================== 插队刷怪队列 ========================
|
||||
|
||||
/**
|
||||
* 刷怪队列(优先于常规配置处理):
|
||||
* 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。
|
||||
*/
|
||||
private MonQueue: Array<{
|
||||
/** 怪物 UUID */
|
||||
uuid: number,
|
||||
/** 怪物等级 */
|
||||
level: number,
|
||||
}> = [];
|
||||
|
||||
private static readonly MAX_SLOTS = 6;
|
||||
private slotOccupiedEids: Array<number | null> = Array(6).fill(null);
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 全局生成顺序计数器,用于层级管理(预留) */
|
||||
/** 槽位占用状态:记录每个槽位当前占用的怪物 ECS 实体 ID,null 表示空闲 */
|
||||
private slotOccupiedEids: Array<number | null> = Array(6).fill(null);
|
||||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||||
private globalSpawnOrder: number = 0;
|
||||
/** 插队刷怪处理计时器 */
|
||||
private queueTimer: number = 0;
|
||||
/** 当前波数 */
|
||||
private currentWave: number = 0;
|
||||
/** 当前波的目标怪物总数 */
|
||||
private waveTargetCount: number = 0;
|
||||
/** 当前波已生成的怪物数量 */
|
||||
private waveSpawnedCount: number = 0;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad(){
|
||||
this.on(GameEvent.FightReady,this.fight_ready,this)
|
||||
this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this);
|
||||
@@ -48,8 +107,30 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收特殊刷怪事件并入队
|
||||
* 事件数据最小结构:{ uuid, level }
|
||||
* 帧更新:
|
||||
* 1. 检查游戏是否运行中。
|
||||
* 2. 刷新槽位占用状态(清除已死亡怪物的占用)。
|
||||
* 3. 尝试推进波次(所有怪物清除后自动进入下一波)。
|
||||
* 4. 处理插队刷怪队列。
|
||||
*/
|
||||
protected update(dt: number): void {
|
||||
if(!smc.mission.play) return
|
||||
if(smc.mission.pause) return
|
||||
if(smc.mission.stop_mon_action) return;
|
||||
if(!smc.mission.in_fight) return;
|
||||
this.refreshSlotOccupancy();
|
||||
this.tryAdvanceWave();
|
||||
if(!smc.mission.in_fight) return;
|
||||
if(smc.mission.stop_spawn_mon) return;
|
||||
this.updateSpecialQueue(dt);
|
||||
}
|
||||
|
||||
// ======================== 事件处理 ========================
|
||||
|
||||
/**
|
||||
* 接收特殊刷怪事件并入队。
|
||||
* @param event 事件名
|
||||
* @param args { uuid: number, level: number }
|
||||
*/
|
||||
private onSpawnSpecialMonster(event: string, args: any) {
|
||||
if (!args) return;
|
||||
@@ -58,13 +139,16 @@ export class MissionMonCompComp extends CCComp {
|
||||
uuid: args.uuid,
|
||||
level: args.level,
|
||||
});
|
||||
// 让队列在下一帧附近尽快消费,提升事件响应感
|
||||
// 加速队列消费
|
||||
this.queueTimer = 1.0;
|
||||
}
|
||||
|
||||
start() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 战斗准备:重置所有运行时状态并开始第一波。
|
||||
*/
|
||||
fight_ready(){
|
||||
smc.vmdata.mission_data.mon_num=0
|
||||
smc.mission.stop_spawn_mon = false
|
||||
@@ -78,18 +162,14 @@ export class MissionMonCompComp extends CCComp {
|
||||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
|
||||
}
|
||||
|
||||
protected update(dt: number): void {
|
||||
if(!smc.mission.play) return
|
||||
if(smc.mission.pause) return
|
||||
if(smc.mission.stop_mon_action) return;
|
||||
if(!smc.mission.in_fight) return;
|
||||
this.refreshSlotOccupancy();
|
||||
this.tryAdvanceWave();
|
||||
if(!smc.mission.in_fight) return;
|
||||
if(smc.mission.stop_spawn_mon) return;
|
||||
this.updateSpecialQueue(dt);
|
||||
}
|
||||
// ======================== 插队刷怪 ========================
|
||||
|
||||
/**
|
||||
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
|
||||
* 1. 判断怪物是否为 Boss(决定占用 1 格还是 2 格)。
|
||||
* 2. 在空闲槽位中查找合适位置。
|
||||
* 3. 找到后从队列中移除并生成怪物。
|
||||
*/
|
||||
private updateSpecialQueue(dt: number) {
|
||||
if (this.MonQueue.length <= 0) return;
|
||||
this.queueTimer += dt;
|
||||
@@ -99,9 +179,10 @@ export class MissionMonCompComp extends CCComp {
|
||||
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].includes(item.uuid);
|
||||
const slotsPerMon = isBoss ? 2 : 1;
|
||||
|
||||
// 查找空闲槽位
|
||||
let slotIndex = -1;
|
||||
if (slotsPerMon === 2) {
|
||||
// Boss 只能放在 0, 2, 4
|
||||
// Boss 只能放在 0, 2, 4(需要连续 2 格空闲)
|
||||
for (const idx of [0, 2, 4]) {
|
||||
if (!this.slotOccupiedEids[idx] && !this.slotOccupiedEids[idx + 1]) {
|
||||
slotIndex = idx;
|
||||
@@ -109,6 +190,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 普通怪找第一个空闲格
|
||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
||||
if (!this.slotOccupiedEids[i]) {
|
||||
slotIndex = i;
|
||||
@@ -125,11 +207,20 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 波次管理 ========================
|
||||
|
||||
/**
|
||||
* 开始下一波:
|
||||
* 1. 波数 +1 并更新全局数据。
|
||||
* 2. 重置槽位并根据配置生成本波所有怪物。
|
||||
* 3. 分发 NewWave 事件。
|
||||
*/
|
||||
private startNextWave() {
|
||||
this.currentWave += 1;
|
||||
smc.vmdata.mission_data.level = this.currentWave;
|
||||
this.resetSlotSpawnData(this.currentWave);
|
||||
|
||||
// 检查本波是否有 Boss
|
||||
let hasBoss = false;
|
||||
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
||||
for (const slot of config) {
|
||||
@@ -145,6 +236,10 @@ export class MissionMonCompComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试推进波次:
|
||||
* 条件:队列为空 + 所有槽位无活怪 + 全局怪物数为 0。
|
||||
*/
|
||||
private tryAdvanceWave() {
|
||||
if (this.MonQueue.length > 0) return;
|
||||
if (this.hasActiveSlotMonster()) return;
|
||||
@@ -152,16 +247,25 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.startNextWave();
|
||||
}
|
||||
|
||||
/** 获取当前阶段(stage = wave - 1,用于属性成长计算) */
|
||||
private getCurrentStage(): number {
|
||||
return Math.max(0, this.currentWave - 1);
|
||||
}
|
||||
|
||||
// ======================== 随机选取 ========================
|
||||
|
||||
/** 随机选取一种成长类型 */
|
||||
private getRandomUpType(): UpType {
|
||||
const keys = Object.keys(StageGrow).map(v => Number(v) as UpType);
|
||||
const index = Math.floor(Math.random() * keys.length);
|
||||
return keys[index] ?? UpType.AP1_HP1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据怪物类型从对应池中随机选取 UUID。
|
||||
* @param monType 怪物类型(MonType 枚举值)
|
||||
* @returns 怪物 UUID
|
||||
*/
|
||||
private getRandomUuidByType(monType: number): number {
|
||||
const pool = (MonList as any)[monType] || MonList[MonType.Melee];
|
||||
if (!pool || pool.length === 0) return 6001;
|
||||
@@ -169,19 +273,38 @@ export class MissionMonCompComp extends CCComp {
|
||||
return pool[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物属性成长值对。
|
||||
* Boss 在普通成长基础上叠加 StageBossGrow。
|
||||
*
|
||||
* @param upType 成长类型
|
||||
* @param isBoss 是否为 Boss
|
||||
* @returns [AP 成长值, HP 成长值]
|
||||
*/
|
||||
private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] {
|
||||
// 普通怪基础成长:StageGrow
|
||||
const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1];
|
||||
if (!isBoss) return [grow[0], grow[1]];
|
||||
// Boss 额外成长:StageBossGrow(在普通成长上叠加)
|
||||
const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1];
|
||||
return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]];
|
||||
}
|
||||
|
||||
/** 获取全局刷怪强度系数 */
|
||||
private getSpawnPowerBias(): number {
|
||||
return SpawnPowerBias;
|
||||
}
|
||||
|
||||
// ======================== 槽位管理 ========================
|
||||
|
||||
/**
|
||||
* 重置槽位并生成本波所有怪物:
|
||||
* 1. 读取波次配置(WaveSlotConfig 或 DefaultWaveSlot)。
|
||||
* 2. 将 Boss 和普通怪分类。
|
||||
* 3. Boss 优先分配到 0, 2, 4 号位(占 2 格)。
|
||||
* 4. 普通怪填充剩余空闲格。
|
||||
* 5. 立即实例化所有怪物。
|
||||
*
|
||||
* @param wave 当前波数
|
||||
*/
|
||||
private resetSlotSpawnData(wave: number = 1) {
|
||||
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
|
||||
this.slotOccupiedEids = Array(MissionMonCompComp.MAX_SLOTS).fill(null);
|
||||
@@ -189,6 +312,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
let bosses: any[] = [];
|
||||
let normals: any[] = [];
|
||||
|
||||
// 按类型分类
|
||||
for (const slot of config) {
|
||||
const slotsPerMon = slot.slotsPerMon || 1;
|
||||
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss;
|
||||
@@ -207,7 +331,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.waveTargetCount = bosses.length + normals.length;
|
||||
this.waveSpawnedCount = 0;
|
||||
|
||||
// Boss 只能放在 0, 2, 4 (即 1, 3, 5 号位)
|
||||
// Boss 优先分配(只能放在 0, 2, 4)
|
||||
let bossAllowedIndices = [0, 2, 4];
|
||||
let assignedSlots = new Array(MissionMonCompComp.MAX_SLOTS).fill(null);
|
||||
|
||||
@@ -216,7 +340,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
for (const idx of bossAllowedIndices) {
|
||||
if (!assignedSlots[idx] && !assignedSlots[idx + 1]) {
|
||||
assignedSlots[idx] = boss;
|
||||
assignedSlots[idx + 1] = "occupied"; // 占位
|
||||
assignedSlots[idx + 1] = "occupied"; // 占位标记
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
@@ -226,6 +350,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// 普通怪填充剩余空位
|
||||
for (const normal of normals) {
|
||||
let placed = false;
|
||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
||||
@@ -249,6 +374,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查是否有任何槽位仍被活着的怪物占用 */
|
||||
private hasActiveSlotMonster() {
|
||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
||||
if (this.slotOccupiedEids[i]) return true;
|
||||
@@ -256,6 +382,11 @@ export class MissionMonCompComp extends CCComp {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有槽位的占用状态:
|
||||
* 检查每个占用的 ECS 实体是否仍存在且 HP > 0,
|
||||
* 已失效的清除占用标记。
|
||||
*/
|
||||
private refreshSlotOccupancy() {
|
||||
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
|
||||
const eid = this.slotOccupiedEids[i];
|
||||
@@ -272,6 +403,23 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 怪物生成 ========================
|
||||
|
||||
/**
|
||||
* 在指定槽位生成一个怪物:
|
||||
* 1. 计算出生坐标(多格时居中)。
|
||||
* 2. 创建 Monster ECS 实体。
|
||||
* 3. 标记槽位占用。
|
||||
* 4. 设置渲染排序(Boss 优先级更高)。
|
||||
* 5. 根据阶段和成长类型计算最终 AP / HP。
|
||||
*
|
||||
* @param slotIndex 槽位索引(0-5)
|
||||
* @param uuid 怪物 UUID
|
||||
* @param isBoss 是否为 Boss
|
||||
* @param upType 属性成长类型
|
||||
* @param monLv 怪物等级
|
||||
* @param slotsPerMon 占用格数
|
||||
*/
|
||||
private addMonsterBySlot(
|
||||
slotIndex: number,
|
||||
uuid: number = 1001,
|
||||
@@ -282,7 +430,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
) {
|
||||
let mon = ecs.getEntity<Monster>(Monster);
|
||||
let scale = -1;
|
||||
// 如果占用了多个格子,出生坐标居中处理
|
||||
// 多格占用时居中出生点
|
||||
const centerXOffset = (slotsPerMon - 1) * MissionMonCompComp.MON_SLOT_X_INTERVAL / 2;
|
||||
const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL + centerXOffset;
|
||||
const landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0);
|
||||
@@ -291,18 +439,22 @@ export class MissionMonCompComp extends CCComp {
|
||||
|
||||
mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv);
|
||||
|
||||
// 将它占用的所有格子都标记为这个 eid
|
||||
// 标记槽位占用
|
||||
for (let j = 0; j < slotsPerMon; j++) {
|
||||
if (slotIndex + j < MissionMonCompComp.MAX_SLOTS) {
|
||||
this.slotOccupiedEids[slotIndex + j] = mon.eid;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置渲染排序
|
||||
const move = mon.get(MoveComp);
|
||||
if (move) {
|
||||
move.spawnOrder = isBoss
|
||||
? MissionMonCompComp.BOSS_RENDER_PRIORITY + this.globalSpawnOrder
|
||||
: this.globalSpawnOrder;
|
||||
}
|
||||
|
||||
// 计算最终属性
|
||||
const model = mon.get(HeroAttrsComp);
|
||||
const base = HeroInfo[uuid];
|
||||
if (!model || !base) return;
|
||||
@@ -314,7 +466,7 @@ export class MissionMonCompComp extends CCComp {
|
||||
model.hp = model.hp_max;
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时触发(当前不销毁节点) */
|
||||
reset() {
|
||||
// this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/**
|
||||
* @file RanksComp.ts
|
||||
* @description 排行榜弹窗组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 展示排行榜界面,包含 Top1~Top3 特殊位和通用列表区域。
|
||||
* 2. 提供关闭排行榜弹窗的按钮回调。
|
||||
*
|
||||
* 关键设计:
|
||||
* - top1_node / top2_node / top3_node 用于展示前三名玩家的特殊样式。
|
||||
* - lists_node 为滚动列表的容器节点。
|
||||
* - list_prefab / melist_prefab 分别为普通排名项和"我的排名"项的预制体。
|
||||
* - 当前 onLoad / onAdded 未实现具体逻辑,预留后期接入排行数据。
|
||||
*
|
||||
* 依赖:
|
||||
* - UIID.Ranks —— 在 oops.gui 系统中注册的弹窗 ID
|
||||
*/
|
||||
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources, Prefab } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -10,37 +27,54 @@ import { mLogger } from "../common/Logger";
|
||||
|
||||
const {property, ccclass } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* RanksComp —— 排行榜视图组件
|
||||
*
|
||||
* 通过 oops.gui.open(UIID.Ranks) 打开。
|
||||
* 展示 Top3 + 通用列表 + 我的排名。
|
||||
*/
|
||||
@ccclass('RanksComp')
|
||||
@ecs.register('RanksComp', false)
|
||||
export class RanksComp extends CCComp {
|
||||
/** 第 1 名展示节点 */
|
||||
@property(Node)
|
||||
top1_node=null!
|
||||
/** 第 2 名展示节点 */
|
||||
@property(Node)
|
||||
top2_node=null!
|
||||
/** 第 3 名展示节点 */
|
||||
@property(Node)
|
||||
top3_node=null!
|
||||
/** 排名列表容器节点 */
|
||||
@property(Node)
|
||||
lists_node=null!
|
||||
/** 普通排名项预制体 */
|
||||
@property(Prefab)
|
||||
list_prefab=null!
|
||||
/** "我的排名"项预制体 */
|
||||
@property(Prefab)
|
||||
melist_prefab=null!
|
||||
|
||||
/** 预留:加载排行数据 */
|
||||
onLoad() {
|
||||
|
||||
}
|
||||
|
||||
/** 预留:弹窗打开时接收参数 */
|
||||
onAdded(args: any) {
|
||||
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
|
||||
}
|
||||
|
||||
/** 关闭排行榜弹窗 */
|
||||
closeRanks(){
|
||||
oops.gui.remove(UIID.Ranks)
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
|
||||
this.node.destroy();
|
||||
|
||||
@@ -1,74 +1,142 @@
|
||||
/**
|
||||
* @file RogueConfig.ts
|
||||
* @description Roguelike 关卡配置 —— 怪物类型、成长值、波次刷怪方案
|
||||
*
|
||||
* 职责:
|
||||
* 1. 定义怪物属性成长类型(UpType)和每种类型的 AP / HP 每阶段成长值。
|
||||
* 2. 定义怪物分类(MonType)和对应的怪物 UUID 池。
|
||||
* 3. 定义每一波(Wave)的怪物占位配置(WaveSlotConfig / DefaultWaveSlot)。
|
||||
* 4. 提供全局刷怪强度偏差系数(SpawnPowerBias)。
|
||||
*
|
||||
* 设计说明:
|
||||
* - 战场固定 6 个占位槽(索引 0-5)。
|
||||
* - Boss 占 2 个槽位,只能放在 0、2、4 号位(确保有连续 2 格)。
|
||||
* - MissionMonComp 在每波开始时读取本配置,决定刷怪组合。
|
||||
*
|
||||
* 注意:
|
||||
* - StageGrow / StageBossGrow 的索引 [0] 为 AP 成长,[1] 为 HP 成长。
|
||||
* - 实际计算公式:base_stat + stage × grow_value × SpawnPowerBias。
|
||||
*/
|
||||
|
||||
// ======================== 属性成长类型枚举 ========================
|
||||
|
||||
/** 怪物属性成长类型 */
|
||||
export enum UpType {
|
||||
AP1_HP1 = 0, //平衡
|
||||
HP2 = 1, //强hp
|
||||
AP2 = 2 //强ap
|
||||
/** 平衡型:AP 和 HP 均匀成长 */
|
||||
AP1_HP1 = 0,
|
||||
/** 强 HP 型:以血量为主成长 */
|
||||
HP2 = 1,
|
||||
/** 强 AP 型:以攻击力为主成长 */
|
||||
AP2 = 2
|
||||
}
|
||||
// 普通关卡成长值:第一项为攻击力成长,第二项为血量成长
|
||||
|
||||
// ======================== 普通怪成长配置 ========================
|
||||
|
||||
/**
|
||||
* 普通怪每阶段成长值:[AP 成长, HP 成长]
|
||||
* 每经历一波(stage +1),怪物的 base_ap / base_hp 增加对应值。
|
||||
*/
|
||||
export const StageGrow = {
|
||||
[UpType.AP1_HP1]: [4,10], // 平衡型:攻4 血10
|
||||
[UpType.HP2]: [2,20], // 强HP型:攻2 血20
|
||||
[UpType.AP2]: [8,0], // 强AP型:攻8 血0
|
||||
[UpType.AP1_HP1]: [4,10], // 平衡型:每波攻击+4 血量+10
|
||||
[UpType.HP2]: [2,20], // 强HP型:每波攻击+2 血量+20
|
||||
[UpType.AP2]: [8,0], // 强AP型:每波攻击+8 血量+0
|
||||
}
|
||||
|
||||
// Boss关卡成长值:同上,数值更高
|
||||
// ======================== Boss 额外成长配置 ========================
|
||||
|
||||
/**
|
||||
* Boss 在普通怪成长基础上的 **额外** 成长值:[AP 增量, HP 增量]
|
||||
* 实际 Boss 成长 = StageGrow + StageBossGrow。
|
||||
*/
|
||||
export const StageBossGrow = {
|
||||
[UpType.AP1_HP1]: [3,16], // 平衡型:攻3 血16
|
||||
[UpType.HP2]: [1,24], // 强HP型:攻1 血24
|
||||
[UpType.AP2]: [10,4], // 强AP型:攻10 血4
|
||||
[UpType.AP1_HP1]: [3,16], // 平衡型 Boss:额外攻击+3 血量+16
|
||||
[UpType.HP2]: [1,24], // 强HP型 Boss:额外攻击+1 血量+24
|
||||
[UpType.AP2]: [10,4], // 强AP型 Boss:额外攻击+10 血量+4
|
||||
}
|
||||
|
||||
// ======================== 怪物类型定义 ========================
|
||||
|
||||
/** 怪物类型常量(用于 WaveSlotConfig 中引用) */
|
||||
export const MonType = {
|
||||
Melee: 0, // 近战高功
|
||||
Long: 1, // 高速贴近
|
||||
Support: 2, // 支持怪
|
||||
MeleeBoss: 3, // boss怪
|
||||
LongBoss: 4, // boss怪
|
||||
/** 近战普通怪 */
|
||||
Melee: 0,
|
||||
/** 远程普通怪 */
|
||||
Long: 1,
|
||||
/** 辅助怪(支持类) */
|
||||
Support: 2,
|
||||
/** 近战 Boss */
|
||||
MeleeBoss: 3,
|
||||
/** 远程 Boss */
|
||||
LongBoss: 4,
|
||||
}
|
||||
|
||||
// ======================== 怪物 UUID 池 ========================
|
||||
|
||||
/** 各类型对应的怪物 UUID 列表(随机抽取) */
|
||||
export const MonList = {
|
||||
[MonType.Melee]: [6001,6002,6003], // 近战怪
|
||||
[MonType.Long]: [6004,6005], // 远程怪
|
||||
[MonType.Support]: [6005], // 辅助怪
|
||||
[MonType.MeleeBoss]:[6006,6015], // 近战boss
|
||||
[MonType.LongBoss]:[6104], // 远程boss
|
||||
[MonType.Melee]: [6001,6002,6003], // 近战怪池
|
||||
[MonType.Long]: [6004,6005], // 远程怪池
|
||||
[MonType.Support]: [6005], // 辅助怪池
|
||||
[MonType.MeleeBoss]:[6006,6015], // 近战 Boss 池
|
||||
[MonType.LongBoss]:[6104], // 远程 Boss 池
|
||||
}
|
||||
/*
|
||||
*** 全局刷怪强度配置,后期根据玩家强度动态调整
|
||||
*/
|
||||
|
||||
// ======================== 全局刷怪强度系数 ========================
|
||||
|
||||
/**
|
||||
* 全局刷怪强度偏差系数。
|
||||
* 所有怪物的最终 AP / HP 会乘以此系数。
|
||||
* 后期可根据玩家强度动态调整以实现自适应难度。
|
||||
*/
|
||||
export const SpawnPowerBias = 1
|
||||
|
||||
// ======================== 波次占位配置数据结构 ========================
|
||||
|
||||
/** 单条波次占位配置 */
|
||||
export interface IWaveSlot {
|
||||
type: number; // 对应 MonType
|
||||
count: number; // 占位数量
|
||||
slotsPerMon?: number; // 每个怪占用几个位置,默认 1
|
||||
/** 怪物类型(参考 MonType) */
|
||||
type: number;
|
||||
/** 该类型的怪物数量 */
|
||||
count: number;
|
||||
/** (可选)每个怪物占用几个槽位,默认 1;大型 Boss 设为 2 */
|
||||
slotsPerMon?: number;
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// 【每波怪物占位与刷怪配置说明】
|
||||
// 1. 字段说明:
|
||||
//
|
||||
// 字段说明:
|
||||
// - type: 怪物类型 (参考 MonType,如近战 0,远程 1,Boss 3 等)。
|
||||
// - count: 该类型的怪在场上同时存在几个。
|
||||
// - slotsPerMon: (可选) 单个怪物体积占用几个占位坑,默认为 1。如果是大型 Boss 可设为 2,它会跨占位降落。
|
||||
//
|
||||
// 【注意】:
|
||||
// 全场固定 6 个槽位(索引 0-5)。
|
||||
// Boss 固定占用 2 个位置,且只能出现在 1、3、5 号位(对应索引 0, 2, 4)。
|
||||
// 每波怪物总槽位占用不能超过 6。不再支持排队刷怪。
|
||||
// - slotsPerMon: (可选) 单个怪物体积占用几个占位坑,默认为 1。
|
||||
// 大型 Boss 设为 2,它会跨占位降落。
|
||||
//
|
||||
// 【规则约束】:
|
||||
// - 全场固定 6 个槽位(索引 0-5)。
|
||||
// - Boss 固定占用 2 个位置,且只能出现在 1、3、5 号位(对应索引 0, 2, 4)。
|
||||
// - 每波怪物总槽位占用不能超过 6。
|
||||
// =========================================================================================
|
||||
|
||||
/** 各波次的怪物占位配置(key = 波次编号) */
|
||||
export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = {
|
||||
/** 第 1 波:3 近战 + 3 远程 */
|
||||
1: [
|
||||
{ type: MonType.Melee, count: 3 },
|
||||
{ type: MonType.Long, count: 3 }
|
||||
],
|
||||
/** 第 2 波:2 近战 + 2 远程 + 2 辅助 */
|
||||
2: [
|
||||
{ type: MonType.Melee, count: 2 },
|
||||
{ type: MonType.Long, count: 2 },
|
||||
{ type: MonType.Support, count: 2 }
|
||||
],
|
||||
/** 第 3 波:2 近战 + 1 近战Boss(占2格) + 2 远程 */
|
||||
3: [
|
||||
{ type: MonType.Melee, count: 2 },
|
||||
{ type: MonType.MeleeBoss, count: 1, slotsPerMon: 2 },
|
||||
{ type: MonType.Long, count: 2 }
|
||||
],
|
||||
/** 第 4 波:2 近战 + 2 远程 + 1 远程Boss(占2格) */
|
||||
4: [
|
||||
{ type: MonType.Melee, count: 2 },
|
||||
{ type: MonType.Long, count: 2 },
|
||||
@@ -76,7 +144,11 @@ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = {
|
||||
],
|
||||
}
|
||||
|
||||
// 默认占位配置 (如果在 WaveSlotConfig 中找不到波次,则使用此配置)
|
||||
/**
|
||||
* 默认占位配置:
|
||||
* 当 WaveSlotConfig 中找不到对应波次时使用此兜底配置。
|
||||
* 默认 3 近战 + 3 远程。
|
||||
*/
|
||||
export const DefaultWaveSlot: IWaveSlot[] = [
|
||||
{ type: MonType.Melee, count: 3 },
|
||||
{ type: MonType.Long, count: 3 }
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/**
|
||||
* @file SIconComp.ts
|
||||
* @description 技能图标组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 根据技能 UUID 显示对应的技能图标。
|
||||
* 2. 从预加载的资源中获取 SpriteFrame 并赋给子节点 "icon" 的 Sprite。
|
||||
*
|
||||
* 使用方式:
|
||||
* 挂载在需要显示技能图标的节点上,外部调用 update_data(s_uuid) 传入技能 UUID。
|
||||
*
|
||||
* 依赖:
|
||||
* - SkillSet(SkillSet.ts)—— 技能静态配置(含 icon 字段)
|
||||
* - oops.res —— 已预加载的资源管理器
|
||||
*/
|
||||
import { _decorator, Sprite, SpriteFrame } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -6,22 +21,29 @@ import { oops } from "db://oops-framework/core/Oops";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* SIconCompComp —— 技能图标视图组件
|
||||
*
|
||||
* 从 SkillSet 查询技能配置获取 icon 路径,
|
||||
* 通过 oops.res.get 获取已预加载的 SpriteFrame 并显示。
|
||||
*/
|
||||
@ccclass('SIconCompComp')
|
||||
@ecs.register('SIconComp', false)
|
||||
export class SIconCompComp extends CCComp {
|
||||
/** 视图层逻辑代码分离演示 */
|
||||
start() {
|
||||
// var entity = this.ent as ecs.Entity; // ecs.Entity 可转为当前模块的具体实体对象
|
||||
// this.on(ModuleEvent.Cmd, this.onHandler, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新技能图标显示。
|
||||
* @param s_uuid 技能的唯一标识 UUID
|
||||
*/
|
||||
update_data(s_uuid:number){
|
||||
let skill_data = SkillSet[s_uuid]
|
||||
// 从预加载的资源中获取对应图标的 SpriteFrame 并赋值
|
||||
this.node.getChildByName("icon").getComponent(Sprite).spriteFrame = oops.res.get("game/heros/cards/"+skill_data.icon, SpriteFrame)
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
/**
|
||||
* @file SkillBoxComp.ts
|
||||
* @description 单个技能卡效果控制组件(UI 视图层 + 逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 表示一张已使用的技能卡在战场上的 **可视化实体**。
|
||||
* 2. 管理技能的 **触发逻辑**:即时触发 vs 定时触发(战斗中按间隔触发)。
|
||||
* 3. 显示技能图标和剩余触发次数。
|
||||
* 4. 触发结束后自动销毁。
|
||||
*
|
||||
* 关键设计:
|
||||
* - is_instant=true(即时技能):init 时立即触发一次,播放后延迟销毁。
|
||||
* - is_instant=false(持续技能):战斗中每隔 trigger_interval 秒触发一次,
|
||||
* 共触发 trigger_times 次后销毁。
|
||||
* - 新一波(NewWave)时如果持续技能的次数已用完则销毁。
|
||||
* - 销毁时通过 GameEvent.RemoveSkillBox 通知 MissSkillsComp 回收槽位。
|
||||
*
|
||||
* 触发技能的方式:
|
||||
* - 通过 GameEvent.TriggerSkill 事件,将技能 UUID、卡牌等级、
|
||||
* 触发位置等信息分发给技能系统。
|
||||
*
|
||||
* 依赖:
|
||||
* - CardPoolList(CardSet)—— 查询技能卡的触发配置(t_times / t_inv / is_inst)
|
||||
* - SkillSet —— 技能静态配置(icon 字段)
|
||||
* - GameEvent —— 各类游戏事件
|
||||
* - smc.mission —— 游戏运行状态
|
||||
*/
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { _decorator, Node, Prefab, Sprite, Label, Vec3, resources, SpriteAtlas } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
@@ -9,27 +36,53 @@ import { GameEvent } from "../common/config/GameEvent";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* SkillBoxComp —— 单个技能卡效果视图 + 逻辑组件
|
||||
*
|
||||
* 由 MissSkillsComp.addSkill() 实例化并初始化。
|
||||
* 在战场上以图标 + 剩余次数的形式呈现。
|
||||
*/
|
||||
@ccclass('SkillBoxComp')
|
||||
@ecs.register('SkillBoxComp', false)
|
||||
export class SkillBoxComp extends CCComp {
|
||||
/** 调试日志开关 */
|
||||
private debugMode: boolean = true;
|
||||
|
||||
/** 技能图标节点 */
|
||||
@property({type: Node})
|
||||
private icon_node:Node= null;
|
||||
|
||||
/** 剩余次数标签 */
|
||||
@property(Label)
|
||||
private info_label: Label = null;
|
||||
|
||||
// ======================== 技能配置 ========================
|
||||
|
||||
/** 技能 UUID */
|
||||
private s_uuid: number = 0;
|
||||
/** 卡牌等级 */
|
||||
private card_lv: number = 1;
|
||||
/** 是否为即时技能(true=使用后立即触发,false=战斗中定时触发) */
|
||||
private is_instant: boolean = true;
|
||||
/** 总触发次数 */
|
||||
private trigger_times: number = 1;
|
||||
/** 触发间隔(秒,仅持续技能有效) */
|
||||
private trigger_interval: number = 0;
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 已触发次数 */
|
||||
private current_trigger_times: number = 0;
|
||||
/** 当前计时器(秒) */
|
||||
private timer: number = 0;
|
||||
/** 是否处于战斗中(仅战斗中持续技能才计时) */
|
||||
private in_combat: boolean = false;
|
||||
/** 是否已初始化 */
|
||||
private initialized: boolean = false;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
/** 注册战斗开始、任务结束、新一波等事件 */
|
||||
onLoad() {
|
||||
oops.message.on(GameEvent.FightStart, this.onFightStart, this);
|
||||
oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this);
|
||||
@@ -37,19 +90,30 @@ export class SkillBoxComp extends CCComp {
|
||||
oops.message.on(GameEvent.NewWave, this.onNewWaveGlobal, this);
|
||||
}
|
||||
|
||||
/** 销毁时移除所有事件监听并通知槽位管理器回收 */
|
||||
onDestroy() {
|
||||
oops.message.off(GameEvent.FightStart, this.onFightStart, this);
|
||||
oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this);
|
||||
this.node.off(GameEvent.NewWave, this.onNewWave, this);
|
||||
oops.message.off(GameEvent.NewWave, this.onNewWaveGlobal, this);
|
||||
// 通知 MissSkillsComp 回收该节点占用的槽位
|
||||
oops.message.dispatchEvent(GameEvent.RemoveSkillBox, this.node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化技能卡效果:
|
||||
* 1. 从 CardPoolList 查询技能卡的触发配置。
|
||||
* 2. 更新 UI 显示(图标 + 次数)。
|
||||
* 3. 即时技能立即触发一次;若次数已满则延迟销毁。
|
||||
*
|
||||
* @param uuid 技能 UUID
|
||||
* @param card_lv 技能卡等级
|
||||
*/
|
||||
init(uuid: number, card_lv: number) {
|
||||
// this.node.parent=smc.map.MapView.scene.entityLayer!.node!
|
||||
this.s_uuid = uuid;
|
||||
this.card_lv = card_lv;
|
||||
|
||||
// 查询触发配置
|
||||
const config = CardPoolList.find(c => c.uuid === uuid);
|
||||
if (config) {
|
||||
this.is_instant = config.is_inst ?? true;
|
||||
@@ -64,18 +128,25 @@ export class SkillBoxComp extends CCComp {
|
||||
this.updateUI();
|
||||
|
||||
if (this.is_instant) {
|
||||
// 即时起效:立即触发
|
||||
// 即时技能:立即触发
|
||||
this.triggerSkill();
|
||||
this.current_trigger_times++;
|
||||
if (this.current_trigger_times >= this.trigger_times) {
|
||||
// 次数已满 → 延迟 1 秒后销毁(保留短暂视觉反馈)
|
||||
this.scheduleOnce(() => {
|
||||
this.node.destroy();
|
||||
}, 1.0); // 稍微延迟销毁,保证表现
|
||||
}, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 UI:
|
||||
* - 图标:从 uicons 图集获取。
|
||||
* - 剩余次数:持续技能显示剩余数字,即时技能不显示。
|
||||
*/
|
||||
updateUI() {
|
||||
// 加载技能图标
|
||||
if (this.icon_node) {
|
||||
const iconId = SkillSet[this.s_uuid]?.icon || `${this.s_uuid}`;
|
||||
resources.load("gui/uicons", SpriteAtlas, (err, atlas) => {
|
||||
@@ -88,6 +159,7 @@ export class SkillBoxComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
// 更新剩余次数标签
|
||||
if (this.info_label) {
|
||||
if (!this.is_instant) {
|
||||
const remain = Math.max(0, this.trigger_times - this.current_trigger_times);
|
||||
@@ -98,40 +170,56 @@ export class SkillBoxComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 战斗状态事件 ========================
|
||||
|
||||
/** 战斗开始:标记进入战斗状态,持续技能开始计时 */
|
||||
private onFightStart() {
|
||||
if (!this.initialized) return;
|
||||
this.in_combat = true;
|
||||
|
||||
if (!this.is_instant) {
|
||||
// 战斗开始时,计时归0,重新计时
|
||||
this.timer = 0;
|
||||
this.timer = 0; // 重置计时器
|
||||
}
|
||||
}
|
||||
|
||||
/** 节点级新一波事件处理 */
|
||||
private onNewWave() {
|
||||
this.handleNewWave();
|
||||
}
|
||||
|
||||
/** 全局级新一波事件处理 */
|
||||
private onNewWaveGlobal() {
|
||||
this.handleNewWave();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新一波:退出战斗状态。
|
||||
* 持续技能:若总次数已用完则销毁。
|
||||
*/
|
||||
private handleNewWave() {
|
||||
if (!this.initialized) return;
|
||||
this.in_combat = false;
|
||||
|
||||
if (!this.is_instant) {
|
||||
// 每回合不再重置次数,由全局次数进行控制
|
||||
if (this.current_trigger_times >= this.trigger_times) {
|
||||
this.node.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 任务结束:强制销毁 */
|
||||
private onMissionEnd() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
// ======================== 帧更新 ========================
|
||||
|
||||
/**
|
||||
* 每帧更新(仅对持续技能生效):
|
||||
* - 累加计时器,达到 trigger_interval 时触发一次技能。
|
||||
* - 触发后重置计时器并更新 UI。
|
||||
* - 总次数用完后延迟销毁。
|
||||
*/
|
||||
update(dt: number) {
|
||||
if (!this.initialized || !this.in_combat || this.is_instant) return;
|
||||
if (!smc.mission.play || smc.mission.pause) return;
|
||||
@@ -139,14 +227,13 @@ export class SkillBoxComp extends CCComp {
|
||||
if (this.current_trigger_times < this.trigger_times) {
|
||||
this.timer += dt;
|
||||
if (this.timer >= this.trigger_interval) {
|
||||
this.timer = 0; // 触发后重新计时
|
||||
this.timer = 0;
|
||||
this.triggerSkill();
|
||||
this.current_trigger_times++;
|
||||
this.updateUI(); // 触发后更新界面显示的剩余次数
|
||||
this.updateUI();
|
||||
|
||||
// 如果在战斗中就达到触发次数上限,则可以在此回合战斗结束或者立即销毁
|
||||
// 次数用完 → 延迟销毁
|
||||
if (this.current_trigger_times >= this.trigger_times) {
|
||||
// 可以选择直接销毁,不等到下一回合
|
||||
this.scheduleOnce(() => {
|
||||
if (this.node.isValid) this.node.destroy();
|
||||
}, 0.5);
|
||||
@@ -155,10 +242,14 @@ export class SkillBoxComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 技能触发 ========================
|
||||
|
||||
/**
|
||||
* 触发技能效果:
|
||||
* - 计算触发位置(节点局部坐标 + 父节点偏移)。
|
||||
* - 通过 GameEvent.TriggerSkill 事件将技能数据分发给技能系统。
|
||||
*/
|
||||
private triggerSkill() {
|
||||
// 获取自身在父节点下的局部坐标
|
||||
// UI 的局部坐标在 2D 相机中和实际的游戏逻辑坐标存在偏移关系,
|
||||
// 可以结合自身局部坐标做一次偏移,此处直接读取自身的 localPosition 加上父节点的偏移
|
||||
let targetPos = new Vec3();
|
||||
const localPos = this.node.position;
|
||||
const parentPos = this.node.parent ? this.node.parent.position : new Vec3(0, 0, 0);
|
||||
@@ -166,13 +257,13 @@ export class SkillBoxComp extends CCComp {
|
||||
|
||||
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
|
||||
s_uuid: this.s_uuid,
|
||||
isCardSkill: true,
|
||||
isCardSkill: true, // 标记为卡牌技能(区别于英雄自身技能)
|
||||
card_lv: this.card_lv,
|
||||
targetPos: targetPos
|
||||
});
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/**
|
||||
* @file TopComp.ts
|
||||
* @description 顶部 UI 栏组件
|
||||
*
|
||||
* 职责:
|
||||
* 1. 监听 GOLD_UPDATE 事件,当金币数量变化时播放数字弹跳动画。
|
||||
* 2. 该组件挂载在顶部 HUD 节点上,管理金币图标和数值的视觉反馈。
|
||||
*
|
||||
* 关键设计:
|
||||
* - 使用 tween 对金币数字节点执行 scale 弹跳动画(放大 → 回缩),
|
||||
* 提供视觉提示告知玩家金币变化。
|
||||
*
|
||||
* 依赖:
|
||||
* - GameEvent.GOLD_UPDATE —— 全局金币更新事件
|
||||
* - oops.message —— 全局事件总线
|
||||
*/
|
||||
import { _decorator, Component, Label, Node, tween, v3 } from 'cc';
|
||||
import { GameEvent } from '../common/config/GameEvent';
|
||||
import { smc } from '../common/SingletonModuleComp';
|
||||
@@ -5,14 +21,30 @@ import { BoxSet, NumberFormatter } from '../common/config/GameSet';
|
||||
import { oops } from 'db://oops-framework/core/Oops';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* topComp —— 顶部 UI 栏
|
||||
*
|
||||
* 监听金币变化事件,对金币数值标签播放弹跳反馈动画。
|
||||
*/
|
||||
@ccclass('topComp')
|
||||
export class topComp extends Component {
|
||||
/** 注册金币更新事件监听 */
|
||||
protected onLoad(): void {
|
||||
oops.message.on(GameEvent.GOLD_UPDATE,this.onGoldUpdate,this);
|
||||
}
|
||||
|
||||
start() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 金币更新事件回调:
|
||||
* 对 bar/gold/num 节点播放 scale 弹跳动画,
|
||||
* 先放大到 1.2 再回缩到 1.0。
|
||||
*
|
||||
* @param event 事件名
|
||||
* @param data 事件数据(当前未使用)
|
||||
*/
|
||||
onGoldUpdate(event:string,data:any){
|
||||
tween(this.node.getChildByName("bar").getChildByName("gold").getChildByName("num").getComponent(Label).node)
|
||||
.to(0.1,{scale:v3(1.2,1.2,1)})
|
||||
@@ -20,6 +52,7 @@ export class topComp extends Component {
|
||||
.start()
|
||||
}
|
||||
|
||||
/** 销毁时移除事件监听 */
|
||||
onDestroy(){
|
||||
oops.message.off(GameEvent.GOLD_UPDATE,this.onGoldUpdate,this);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/**
|
||||
* @file VictoryComp.ts
|
||||
* @description 战斗结算弹窗组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 在战斗结束时弹出,展示结算信息(得分、奖励)。
|
||||
* 2. 根据传入参数判断是否可复活,切换"下一步"或"复活"按钮。
|
||||
* 3. 计算单局总分并存储到 smc.vmdata.scores.score。
|
||||
* 4. 提供"重新开始"和"退出"两个操作入口。
|
||||
*
|
||||
* 关键设计:
|
||||
* - onAdded(args) 接收战斗结果参数(victory / rewards / game_data / can_revive)。
|
||||
* - calculateTotalScore() 根据 ScoreWeights 配置加权计算各项得分。
|
||||
* - restart() 和 victory_end() 通过分发 MissionEnd / MissionStart 事件驱动游戏状态切换。
|
||||
*
|
||||
* 依赖:
|
||||
* - smc.vmdata.scores —— 全局战斗统计数据
|
||||
* - ScoreWeights(ScoreSet)—— 得分权重配置
|
||||
* - GameEvent.MissionEnd / MissionStart —— 游戏生命周期事件
|
||||
*/
|
||||
import { _decorator, instantiate, Label ,Prefab,Node} from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -14,31 +34,48 @@ import { mLogger } from "../common/Logger";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* VictoryComp —— 战斗结算弹窗视图组件
|
||||
*
|
||||
* 通过 oops.gui.open(UIID.Victory, args) 打开。
|
||||
* 展示战斗结果,计算总分,并提供重开 / 退出操作。
|
||||
*/
|
||||
@ccclass('VictoryComp')
|
||||
@ecs.register('Victory', false)
|
||||
export class VictoryComp extends CCComp {
|
||||
/** 调试日志开关 */
|
||||
debugMode: boolean = false;
|
||||
/** 奖励等级(预留) */
|
||||
reward_lv:number=1
|
||||
/** 奖励数量(预留) */
|
||||
reward_num:number=2
|
||||
/** 掉落奖励列表 */
|
||||
rewards:any[]=[]
|
||||
/** 累计游戏数据(经验 / 金币 / 钻石) */
|
||||
game_data:any={
|
||||
exp:0,
|
||||
gold:0,
|
||||
diamond:0
|
||||
}
|
||||
|
||||
// 复活相关配置
|
||||
private canRevive: boolean = false; // 是否可以复活(由MissionComp传入)
|
||||
// private reviveCount: number = 0; // 已复活次数 - 移交 MissionComp 管理
|
||||
// ======================== 复活相关 ========================
|
||||
|
||||
/** 视图层逻辑代码分离演示 */
|
||||
/** 是否可以复活(由 MissionComp 传入,取决于剩余复活次数) */
|
||||
private canRevive: boolean = false;
|
||||
|
||||
/** 加载时隐藏 loading 遮罩 */
|
||||
protected onLoad(): void {
|
||||
this.node.getChildByName("loading").active=false
|
||||
// this.canRevive = true;
|
||||
// this.reviveCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗打开时的回调:接收战斗结果参数。
|
||||
*
|
||||
* @param args.victory 是否胜利(当前仅用于标识)
|
||||
* @param args.rewards 掉落奖励列表
|
||||
* @param args.game_data 累计数据 { exp, gold, diamond }
|
||||
* @param args.can_revive 是否可复活
|
||||
*/
|
||||
onAdded(args: any) {
|
||||
this.node.getChildByName("loading").active=false
|
||||
mLogger.log(this.debugMode, 'VictoryComp', "[VictoryComp] onAdded",args)
|
||||
@@ -46,25 +83,24 @@ export class VictoryComp extends CCComp {
|
||||
this.game_data=args.game_data
|
||||
}
|
||||
|
||||
// // 接收复活参数
|
||||
// if (args.can_revive !== undefined) {
|
||||
// this.canRevive = args.can_revive;
|
||||
// } else {
|
||||
// this.canRevive = false; // 默认不可复活
|
||||
// }
|
||||
|
||||
// 根据是否可复活决定按钮显示
|
||||
this.node.getChildByName("btns").getChildByName("next").active=!args.can_revive
|
||||
// this.node.getChildByName("btns").getChildByName("alive").active=args.can_revive
|
||||
|
||||
// 只有在不能复活(彻底结算)时才计算总分
|
||||
// if (!this.canRevive) {
|
||||
|
||||
// }
|
||||
// 计算总分
|
||||
this.calculateTotalScore();
|
||||
}
|
||||
|
||||
// ======================== 得分计算 ========================
|
||||
|
||||
/**
|
||||
* 计算单局总分并更新到 smc.scores.score
|
||||
* 计算单局总分并更新到 smc.vmdata.scores.score。
|
||||
*
|
||||
* 得分维度:
|
||||
* 1. 战斗行为分 —— 暴击次数、连击触发、闪避、格挡、眩晕、冻结
|
||||
* 2. 伤害转化分 —— 总伤害、平均伤害、反伤、暴击伤害
|
||||
* 3. 击杀得分 —— 近战击杀、远程击杀、精英击杀、Boss 击杀
|
||||
* 4. 生存得分 —— 治疗量、吸血量
|
||||
* 5. 资源得分 —— 经验获取、金币获取
|
||||
*/
|
||||
private calculateTotalScore() {
|
||||
const s = smc.vmdata.scores;
|
||||
@@ -98,31 +134,44 @@ export class VictoryComp extends CCComp {
|
||||
totalScore += s.exp_total * ScoreWeights.EXP_FACTOR;
|
||||
totalScore += s.gold_total * ScoreWeights.GOLD_FACTOR;
|
||||
|
||||
// 更新总分(向下取整)
|
||||
// 取整并存储
|
||||
s.score = Math.floor(totalScore);
|
||||
mLogger.log(this.debugMode, 'VictoryComp', `[VictoryComp] 结算总分: ${s.score}`);
|
||||
}
|
||||
|
||||
// ======================== 操作入口 ========================
|
||||
|
||||
/** 退出战斗:清理数据 → 触发任务结束 → 关闭弹窗 */
|
||||
victory_end(){
|
||||
this.clear_data()
|
||||
oops.message.dispatchEvent(GameEvent.MissionEnd)
|
||||
oops.gui.removeByNode(this.node)
|
||||
}
|
||||
|
||||
/** 清理运行时数据:解除暂停标志 */
|
||||
clear_data(){
|
||||
smc.mission.pause=false
|
||||
}
|
||||
//看广告双倍
|
||||
|
||||
/** 看广告双倍奖励(预留) */
|
||||
watch_ad(){
|
||||
return true
|
||||
}
|
||||
|
||||
/** 双倍奖励发放(预留) */
|
||||
double_reward(){
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新开始:
|
||||
* 1. 清理数据。
|
||||
* 2. 触发 MissionEnd 事件重置状态。
|
||||
* 3. 显示 loading 遮罩,延迟 0.5 秒后触发 MissionStart。
|
||||
* 4. 关闭弹窗。
|
||||
*/
|
||||
restart(){
|
||||
this.clear_data()
|
||||
// 确保游戏结束事件被触发,以便重置状态
|
||||
oops.message.dispatchEvent(GameEvent.MissionEnd)
|
||||
this.node.getChildByName("loading").active=true
|
||||
this.scheduleOnce(()=>{
|
||||
@@ -131,6 +180,8 @@ export class VictoryComp extends CCComp {
|
||||
oops.gui.removeByNode(this.node)
|
||||
},0.5)
|
||||
}
|
||||
|
||||
/** 物品展示回调(预留) */
|
||||
item_show(e:any,val:any){
|
||||
mLogger.log(this.debugMode, 'VictoryComp', "item_show",val)
|
||||
}
|
||||
@@ -138,7 +189,8 @@ export class VictoryComp extends CCComp {
|
||||
protected onDestroy(): void {
|
||||
mLogger.log(this.debugMode, 'VictoryComp', "释放胜利界面");
|
||||
}
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy()
|
||||
}
|
||||
|
||||
@@ -1,36 +1,86 @@
|
||||
/**
|
||||
* @file move.ts
|
||||
* @description 地图背景横向循环滚动组件
|
||||
*
|
||||
* 职责:
|
||||
* 1. 使挂载的节点在 [minX, maxX] 范围内沿水平方向持续移动。
|
||||
* 2. 到达边界后停止并发出事件,等待下一轮触发(配合另一个同类组件实现无缝循环)。
|
||||
* 3. 支持动态改变移动方向。
|
||||
*
|
||||
* 关键设计:
|
||||
* - sc(direction):1 = 从左到右,-1 = 从右到左。
|
||||
* - 采用事件驱动的接力机制:
|
||||
* * 当一个 move 组件到达终点后,发出 MAP_MOVE_END_LEFT 或 MAP_MOVE_END_RIGHT,
|
||||
* 同方向的另一个 move 组件收到后从起点开始移动,形成无缝衔接。
|
||||
* - isMove 控制当前是否处于移动中。
|
||||
*
|
||||
* 使用场景:
|
||||
* 通常挂载在地图背景层的两个重叠节点上,通过交替移动实现无限滚动效果。
|
||||
*
|
||||
* 依赖:
|
||||
* - GameEvent.MAP_MOVE_END_LEFT —— 左方向移动结束事件
|
||||
* - GameEvent.MAP_MOVE_END_RIGHT —— 右方向移动结束事件
|
||||
*/
|
||||
import { _decorator, CCBoolean, CCInteger, Component, Node } from 'cc';
|
||||
import { oops } from 'db://oops-framework/core/Oops';
|
||||
import { GameEvent } from '../common/config/GameEvent';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* move —— 背景循环滚动组件
|
||||
*
|
||||
* 挂载在背景节点上,配合另一个同方向实例实现无缝横向滚动。
|
||||
*/
|
||||
@ccclass('move')
|
||||
export class move extends Component {
|
||||
/** 移动速度(像素/秒) */
|
||||
@property({ type: CCInteger })
|
||||
speed: number = 2;
|
||||
|
||||
/** 水平移动右边界 */
|
||||
@property({ type: CCInteger })
|
||||
maxX: number = 640;
|
||||
|
||||
/** 水平移动左边界 */
|
||||
@property({ type: CCInteger })
|
||||
minX: number = -640;
|
||||
|
||||
/**
|
||||
* 移动方向:
|
||||
* 1 = 从左到右
|
||||
* -1 = 从右到左
|
||||
*/
|
||||
@property({ type: CCInteger })
|
||||
sc: number = 1; // 1: 从左到右, -1: 从右到左
|
||||
sc: number = 1;
|
||||
|
||||
/** 当前是否正在移动 */
|
||||
@property
|
||||
isMove:boolean=false
|
||||
|
||||
/** 注册地图移动结束事件 */
|
||||
protected onLoad(): void {
|
||||
oops.message.on(GameEvent.MAP_MOVE_END_LEFT, this.onMapMoveEndLeft, this);
|
||||
oops.message.on(GameEvent.MAP_MOVE_END_RIGHT, this.onMapMoveEndRight, this);
|
||||
}
|
||||
|
||||
start() {
|
||||
// 根据移动方向设置初始位置
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到"左方向移动结束"事件:
|
||||
* 仅从右到左(sc==-1)的实例响应 → 重置到起点并开始移动。
|
||||
*/
|
||||
onMapMoveEndLeft() {
|
||||
if(this.sc==-1){
|
||||
this.isMove=true
|
||||
this.setInitialPosition()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到"右方向移动结束"事件:
|
||||
* 仅从左到右(sc==1)的实例响应 → 重置到起点并开始移动。
|
||||
*/
|
||||
onMapMoveEndRight() {
|
||||
if(this.sc==1){
|
||||
this.isMove=true
|
||||
@@ -38,47 +88,53 @@ export class move extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/** 销毁时移除事件监听 */
|
||||
onDestroy() {
|
||||
oops.message.off(GameEvent.MAP_MOVE_END_LEFT, this.onMapMoveEndLeft, this);
|
||||
oops.message.off(GameEvent.MAP_MOVE_END_RIGHT, this.onMapMoveEndRight, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据移动方向设置初始位置
|
||||
* 根据移动方向设置初始位置:
|
||||
* - 从左到右:起点为 minX
|
||||
* - 从右到左:起点为 maxX
|
||||
*/
|
||||
setInitialPosition() {
|
||||
if (this.sc > 0) {
|
||||
// 从左到右移动,起点为 minX
|
||||
this.node.setPosition(this.minX, this.node.position.y);
|
||||
} else if (this.sc < 0) {
|
||||
// 从右到左移动,起点为 maxX
|
||||
this.node.setPosition(this.maxX, this.node.position.y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 帧更新:按速度和方向更新节点位置。
|
||||
* 移动中才执行,到达边界后检查是否需要停止。
|
||||
*/
|
||||
update(dt: number) {
|
||||
// 更新位置
|
||||
if(this.isMove){
|
||||
const newX = this.node.position.x + dt * this.speed * this.sc;
|
||||
this.node.setPosition(newX, this.node.position.y);
|
||||
// 检查边界并重置位置
|
||||
this.checkBoundaries();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查边界并重置位置
|
||||
* 检查边界并处理到达终点:
|
||||
* - 从左到右:到达 maxX → 停止移动,发出 MAP_MOVE_END_LEFT
|
||||
* - 从右到左:到达 minX → 停止移动,发出 MAP_MOVE_END_RIGHT
|
||||
*
|
||||
* 注意:事件名意味着"该方向的移动已结束",
|
||||
* 接收端的另一个实例会据此开始下一段移动。
|
||||
*/
|
||||
checkBoundaries() {
|
||||
if (this.sc > 0) {
|
||||
// 从左到右移动,到达右边界后回到左边界
|
||||
if (this.node.position.x >= this.maxX) {
|
||||
this.node.setPosition(this.minX, this.node.position.y);
|
||||
this.isMove=false
|
||||
oops.message.dispatchEvent(GameEvent.MAP_MOVE_END_LEFT)
|
||||
}
|
||||
} else if (this.sc < 0) {
|
||||
// 从右到左移动,到达左边界后回到右边界
|
||||
if (this.node.position.x <= this.minX) {
|
||||
this.node.setPosition(this.maxX, this.node.position.y);
|
||||
this.isMove=false
|
||||
@@ -88,8 +144,8 @@ export class move extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态改变移动方向
|
||||
* @param direction 1: 从左到右, -1: 从右到左
|
||||
* 动态改变移动方向并重置到对应起点。
|
||||
* @param direction 1 = 从左到右,-1 = 从右到左
|
||||
*/
|
||||
changeDirection(direction: number) {
|
||||
this.sc = direction;
|
||||
|
||||
Reference in New Issue
Block a user