4 Commits

Author SHA1 Message Date
pan
e76cba7933 feat(map): 新增固定波次技能三选一弹窗系统
1.  新增MSkillBoxComp弹窗组件,实现固定波次触发的技能卡选择功能
2.  新增SkillBoxCardConfig配置与SkillBoxPool技能池,支持按波次配置技能
3.  重构MissionCardComp,将技能卡抽取改为固定波次弹窗触发
4.  扩展SingletonModuleComp与MissionComp,添加技能刷新次数持久化逻辑
5.  优化MissSkillsComp,新增SkillBox专属技能加载流程
6.  修复SkillBoxComp,支持自定义技能参数覆盖
7.  调整UIConfig与CardSet配置,适配新的技能卡流程
2026-06-03 16:36:22 +08:00
pan
1871551fca feat(ui,card): 添加技能卡牌系统UI配置与显示逻辑
1. 新增SkillBox UI界面配置到GameUIConfig
2. 为CardComp组件添加技能描述文本渲染功能
3. 实现卡牌节点标签缓存与统一UI样式配置
4. 修复不同类型卡牌切换时的文本残留问题
2026-06-03 15:12:36 +08:00
pan
a3a8c61b74 fix(map): adjust card info node display logic
set info node hidden for hero cards and visible for skill cards
2026-06-03 14:45:35 +08:00
pan
55c277016d refactor: 清理废弃资源并优化卡牌相关逻辑
1.  删除废弃的notify.prefab和TalentSet.ts.meta文件
2.  新增MSkillBox预制体资源
3.  优化MissionCardComp抽卡按钮显示逻辑
4.  格式化CardComp.ts代码格式与变量声明
5.  优化MissionComp.ts代码格式与事件绑定
6.  更新mission.prefab布局添加技能槽位
2026-06-03 14:39:31 +08:00
16 changed files with 5550 additions and 1598 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "053b0528-ec69-4ff8-85d7-99f128440121",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "MSkillBox"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,753 +0,0 @@
[
{
"__type__": "cc.Prefab",
"_name": "notify",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "notify",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 16
}
],
"_active": true,
"_components": [
{
"__id__": 26
},
{
"__id__": 28
},
{
"__id__": 30
},
{
"__id__": 32
}
],
"_prefab": {
"__id__": 34
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 365,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "popup",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 3
}
],
"_active": true,
"_components": [
{
"__id__": 9
},
{
"__id__": 11
},
{
"__id__": 13
}
],
"_prefab": {
"__id__": 15
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "UI_Social_Chat_02",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 2
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 4
},
{
"__id__": 6
}
],
"_prefab": {
"__id__": 8
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -273.146,
"y": 66.71,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1.4,
"y": 1.4,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": {
"__id__": 5
},
"_contentSize": {
"__type__": "cc.Size",
"width": 62,
"height": 52
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "8ef/z8CxNF5ovlgfNhCsbG"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": {
"__id__": 7
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@f67e4",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "378RDROUtFXLvCWnit6NVv"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "02OWPoHNdItqTNJdj8BAHB",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 10
},
"_contentSize": {
"__type__": "cc.Size",
"width": 680,
"height": 120
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "f1F5aeubtIh4vPjIyM/hBn"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 12
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@091ff",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "11UvQ+qU5EA5SfGlH10ds3"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 14
},
"_alignFlags": 40,
"_target": null,
"_left": 10,
"_right": 10,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 428,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "fckVe8qU5PTaaYD87gPIGO"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "583RDgRFFMB4HdtU7jAQMx",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "lab_content",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 17
},
{
"__id__": 19
},
{
"__id__": 21
},
{
"__id__": 23
}
],
"_prefab": {
"__id__": 25
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 15,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 16
},
"_enabled": true,
"__prefab": {
"__id__": 18
},
"_contentSize": {
"__type__": "cc.Size",
"width": 610,
"height": 70
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "99X9v0jUlJNZIh0XrXacci"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 16
},
"_enabled": true,
"__prefab": {
"__id__": 20
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_string": "信息",
"_horizontalAlign": 0,
"_verticalAlign": 1,
"_actualFontSize": 31,
"_fontSize": 30,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 2,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": false,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 1,
"_enableOutline": true,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "16EtbFHdZJE6XKV6ws1G8+"
},
{
"__type__": "110c8vEd5NEPL/N9meGQnaX",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 16
},
"_enabled": true,
"__prefab": {
"__id__": 22
},
"_params": [],
"_dataID": "",
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "e0rVPeXXFGD6gzXeFEDtBF"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 16
},
"_enabled": true,
"__prefab": {
"__id__": 24
},
"_alignFlags": 45,
"_target": null,
"_left": 60,
"_right": 30,
"_top": 40,
"_bottom": 40,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 600,
"_originalHeight": 54.4,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "44Zwb3Q1FBg6aZ5M45F97+"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "ddsPDrtFpNTpbV/TewBYOo",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 27
},
"_contentSize": {
"__type__": "cc.Size",
"width": 700,
"height": 150
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "51DJ7yv09KnoNkqSbMfoJS"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 29
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": null,
"_type": 1,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "23CbWnuJdPJYsCIbhHA6dc"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 31
},
"_alignFlags": 1,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 200,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "a2zw+Or45DsIUs9Ufp1kbN"
},
{
"__type__": "9a391lhQiBIG6lwpx34Nsvf",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 33
},
"lab_content": {
"__id__": 19
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "59S+ZqGoRPtZH794iLrIQn"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "4fj6jVgO1KzKob1u6D65Ik",
"instance": null,
"targetOverrides": null
}
]

View File

@@ -1 +0,0 @@
{"ver":"1.1.50","importer":"prefab","imported":true,"uuid":"4f87bee5-d246-4e72-827a-3747a9b5bfd7","files":[".json"],"subMetas":{},"userData":{"syncNodeName":"notify"}}

View File

@@ -73,6 +73,9 @@ export class SingletonModuleComp extends ecs.Comp {
max_mission: 4,//最大关卡
coin: 0,
time: 15 * 60,//游戏时间
// 技能三选一弹窗:刷新次数(跨波次保留,广告可叠加)
skill_box_refresh_remain: 1, // 初始赠送的刷新次数
skill_box_ad_refresh_remain: 0, // 看广告累计的额外次数
},
scores: {
score: 0, // 基础得分

View File

@@ -2,9 +2,10 @@ import * as exp from "constants"
import { HeroInfo, HeroList, HType } from "./heroSet"
import { FightSet } from "./GameSet"
import { oops } from "db://oops-framework/core/Oops"
import { SkillOverrides } from "./SkillSet"
class I18nString {
constructor(private key: string, private params?: any[]) {}
constructor(private key: string, private params?: any[]) { }
private getTranslated(): string {
let str = oops.language.getLangByID(this.key) || this.key;
if (this.params && this.params.length > 0) {
@@ -52,9 +53,9 @@ export interface CardConfig {
kind: CKind
pool_lv: CardLV
hero_lv?: number
card_lv?:number
card_lv?: number
base_pool_lv?: number
// 技能卡扩展属性
name?: string // 卡牌名称
info?: string // 卡牌描述信息
@@ -62,6 +63,25 @@ export interface CardConfig {
t_times?: number // 触发次数
t_inv?: number // 触发间隔(秒)
}
/**
* 技能卡池专用卡牌配置(MSkillBoxComp 三选一弹窗使用)
*
* 在普通 CardConfig 基础上扩展:
* - s_uuid : 实际生效的 SkillSet 条目 uuid
* - t_num : 触发次数(可选,默认从 SkillSet 读取)
* - overrides : 参数覆盖,参考 heroSet 中触发技能的 overrides 字段格式
*
* uuid 范围 9000+ 用于和普通 CardPoolList 区分,MissSkillsComp 据此识别。
*/
export interface SkillBoxCardConfig extends CardConfig {
/** 实际生效的 SkillSet 技能 uuid */
s_uuid: number
/** 触发次数(可选,默认 1) */
t_num?: number
/** 技能参数覆盖,参考 SkillOverrides */
overrides?: SkillOverrides
}
export const CardsUpSet: Record<number, number> = {
1: 50,
2: 100,
@@ -88,28 +108,28 @@ export const CardPoolList: CardConfig[] = [];
HeroList.forEach(uuid => {
const hero = HeroInfo[uuid];
if (!hero) return;
const basePoolLv = hero.pool_lv || 1;
const baseHeroLv = hero.lv || 1;
const baseCost = 5;
const baseWeight = 25;
// 生成从 basePoolLv 到 CARD_POOL_MAX_LEVEL 的卡牌
for (let pLv = basePoolLv; pLv <= CARD_POOL_MAX_LEVEL; pLv++) {
const offset = pLv - basePoolLv;
const targetHeroLv = baseHeroLv + offset;
// 英雄的最高等级 是MERGE_MAX-1
if (targetHeroLv > FightSet.MERGE_MAX - 1) {
break;
}
// cost = baseCost * 3^(lv-1): Lv1=5, Lv2=15, Lv3=45
let cost = baseCost;
if (targetHeroLv > 1) {
cost = baseCost * Math.pow(FightSet.MERGE_NEED, targetHeroLv - 1);
}
CardPoolList.push({
uuid: hero.uuid,
type: CardType.Hero,
@@ -164,22 +184,27 @@ export interface SpecialRefreshCardConfig extends CardConfig {
export const SpecialUpgradeCardList: Record<number, SpecialUpgradeCardConfig> = {
7001: { uuid: 7001,type: CardType.SpecialUpgrade,cost: 10,weight: 16,pool_lv: CardLV.LV1,kind:CKind.Card,name:t("scard_name_7001"),info:t("scard_info_7001"),
7001: {
uuid: 7001, type: CardType.SpecialUpgrade, cost: 10, weight: 16, pool_lv: CardLV.LV1, kind: CKind.Card, name: t("scard_name_7001"), info: t("scard_info_7001"),
currentLv: 1, targetLv: 2,
},
7002: { uuid: 7002,type: CardType.SpecialUpgrade,cost: 28,weight: 14,pool_lv: CardLV.LV2,kind:CKind.Card,name:t("scard_name_7002"),info:t("scard_info_7002"),
7002: {
uuid: 7002, type: CardType.SpecialUpgrade, cost: 28, weight: 14, pool_lv: CardLV.LV2, kind: CKind.Card, name: t("scard_name_7002"), info: t("scard_info_7002"),
currentLv: 2, targetLv: 3,
},
}
export const SpecialRefreshCardList: Record<number, SpecialRefreshCardConfig> = {
7101: { uuid: 7101,type: CardType.SpecialRefresh,cost: 3,weight: 14,pool_lv: CardLV.LV1,kind:CKind.Card,name:t("scard_name_7101"),info:t("scard_info_7101"),
7101: {
uuid: 7101, type: CardType.SpecialRefresh, cost: 3, weight: 14, pool_lv: CardLV.LV1, kind: CKind.Card, name: t("scard_name_7101"), info: t("scard_info_7101"),
refreshLv: 0, refreshHeroType: SpecialRefreshHeroType.Melee,
},
7102: { uuid: 7102,type: CardType.SpecialRefresh,cost: 3,weight: 14,pool_lv: CardLV.LV1,kind:CKind.Card,name:t("scard_name_7102"),info:t("scard_info_7102"),
7102: {
uuid: 7102, type: CardType.SpecialRefresh, cost: 3, weight: 14, pool_lv: CardLV.LV1, kind: CKind.Card, name: t("scard_name_7102"), info: t("scard_info_7102"),
refreshLv: 0, refreshHeroType: SpecialRefreshHeroType.Ranged,
},
7103: { uuid: 7103,type: CardType.SpecialRefresh,cost: 4,weight: 12,pool_lv: CardLV.LV2,kind:CKind.Card,name:t("scard_name_7103"),info:t("scard_info_7103"),
7103: {
uuid: 7103, type: CardType.SpecialRefresh, cost: 4, weight: 12, pool_lv: CardLV.LV2, kind: CKind.Card, name: t("scard_name_7103"), info: t("scard_info_7103"),
refreshLv: 3, refreshHeroType: SpecialRefreshHeroType.Any,
},
}
@@ -290,3 +315,172 @@ export const drawCardsByRule = (
const picked = pickCards(pool, count)
return picked
}
/**
* 固定波次弹出技能三选一弹窗(MissionCardComp 触发)
* 5 个波次:1 / 5 / 10 / 15 / 20
*/
export const SKILL_BOX_TRIGGER_WAVES: readonly number[] = [1, 5, 10, 15, 20] as const
/**
* 技能卡池(MSkillBoxComp 三选一)
*
* 5 个硬编码独立池,key 对应触发波次,value 是该波次可刷出的 SkillBoxCardConfig 列表。
* 每条配置:
* - uuid 唯一且 >= 9000,用于 MissSkillsComp 识别
* - s_uuid 指向 SkillSet 中实际生效的技能
* - overrides 参考 heroSet 触发技能格式,对技能参数做具体覆盖
* - cost 强制 0(免费领取)
* - is_inst / t_times / t_inv 控制技能使用后行为
*/
export const SkillBoxPool: Record<number, SkillBoxCardConfig[]> = {
// 第 1 波:基础技能(伤害/治疗/护盾基础数值)
1: [
{
uuid: 9001, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV1, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9001"), info: t("skillbox_info_9001"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6401, t_num: 1, overrides: { ap: 120 }
},
{
uuid: 9002, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV1, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9002"), info: t("skillbox_info_9002"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6402, t_num: 1, overrides: { ap: 150 }
},
{
uuid: 9003, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV1, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9003"), info: t("skillbox_info_9003"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6403, t_num: 1, overrides: { ap: 100, hit_count: 2 }
},
{
uuid: 9004, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV1, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9004"), info: t("skillbox_info_9004"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6404, t_num: 1, overrides: { ap: 110 }
},
],
// 第 5 波:中级技能(伤害提升,带附加效果)
5: [
{
uuid: 9005, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV2, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9005"), info: t("skillbox_info_9005"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6405, t_num: 1, overrides: { ap: 130, crt: 10 }
},
{
uuid: 9006, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV2, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9006"), info: t("skillbox_info_9006"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6406, t_num: 1, overrides: { ap: 140, hit_count: 2 }
},
{
uuid: 9007, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV2, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9007"), info: t("skillbox_info_9007"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6304, t_num: 1, overrides: { ap: 160 }
},
{
uuid: 9008, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV2, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9008"), info: t("skillbox_info_9008"),
is_inst: false, t_times: 3, t_inv: 2,
s_uuid: 6401, t_num: 3, overrides: { ap: 100 }
},
],
// 第 10 波:高级技能(高伤害/多段/持续)
10: [
{
uuid: 9009, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9009"), info: t("skillbox_info_9009"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6305, t_num: 1, overrides: { ap: 200 }
},
{
uuid: 9010, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9010"), info: t("skillbox_info_9010"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6405, t_num: 1, overrides: { ap: 220, hit_count: 3, crt: 15 }
},
{
uuid: 9011, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9011"), info: t("skillbox_info_9011"),
is_inst: false, t_times: 4, t_inv: 1.5,
s_uuid: 6402, t_num: 4, overrides: { ap: 130, frz: 10 }
},
{
uuid: 9012, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 1,
name: t("skillbox_name_9012"), info: t("skillbox_info_9012"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6406, t_num: 1, overrides: { ap: 240, bck: 20 }
},
],
// 第 15 波:精英技能(高额暴击/多段)
15: [
{
uuid: 9013, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 2,
name: t("skillbox_name_9013"), info: t("skillbox_info_9013"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6304, t_num: 1, overrides: { ap: 280, hit_count: 3 }
},
{
uuid: 9014, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 2,
name: t("skillbox_name_9014"), info: t("skillbox_info_9014"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6305, t_num: 1, overrides: { ap: 320, crt: 25 }
},
{
uuid: 9015, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 2,
name: t("skillbox_name_9015"), info: t("skillbox_info_9015"),
is_inst: false, t_times: 5, t_inv: 1.2,
s_uuid: 6406, t_num: 5, overrides: { ap: 200, hit_count: 2, bck: 30 }
},
],
// 第 20 波:终极技能(全屏/极限数值)
20: [
{
uuid: 9016, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 3,
name: t("skillbox_name_9016"), info: t("skillbox_info_9016"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6304, t_num: 1, overrides: { ap: 400, hit_count: 4, crt: 30 }
},
{
uuid: 9017, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 3,
name: t("skillbox_name_9017"), info: t("skillbox_info_9017"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6305, t_num: 1, overrides: { ap: 500, crt: 40, frz: 20 }
},
{
uuid: 9018, type: CardType.Skill, cost: 0, weight: 0, pool_lv: CardLV.LV3, kind: CKind.Skill, card_lv: 3,
name: t("skillbox_name_9018"), info: t("skillbox_info_9018"),
is_inst: true, t_times: 1, t_inv: 0,
s_uuid: 6406, t_num: 1, overrides: { ap: 600, hit_count: 5, bck: 50 }
},
],
}
/**
* 从指定波次的技能池中抽取 N 张卡(加权随机,去重,不足循环补齐)
*
* @param wave 触发波次(1/5/10/15/20),若该波次无池则返回空数组
* @param count 期望返回卡牌数(默认 3)
* @returns 抽中的 SkillBoxCardConfig 列表(长度 = count)
*/
export function getSkillBoxCards(wave: number, count: number = 3): SkillBoxCardConfig[] {
const pool = SkillBoxPool[wave] || [];
if (pool.length === 0) return [];
const result: SkillBoxCardConfig[] = [];
const usedIndexes = new Set<number>();
for (let i = 0; i < count; i++) {
const available: number[] = [];
for (let j = 0; j < pool.length; j++) {
if (!usedIndexes.has(j)) available.push(j);
}
// 池内卡片不足时,允许重复
const candidates = available.length > 0 ? available : Array.from({ length: pool.length }, (_, k) => k);
const pick = candidates[Math.floor(Math.random() * candidates.length)];
usedIndexes.add(pick);
result.push(pool[pick]);
}
return result;
}

View File

@@ -22,6 +22,8 @@ export enum UIID {
// Talents,
Mission,
HInfo,
/** 技能卡牌系统核心控制器 */
SkillBox,
}
/** 打开界面方式的配置数据 */
@@ -37,4 +39,5 @@ export var UIConfigData: { [key: number]: UIConfig } = {
// [UIID.Talents]: { layer: LayerType.UI, prefab: "gui/element/talents" },
[UIID.Mission]: { layer: LayerType.UI, prefab: "gui/element/mission" },
[UIID.HInfo]: { layer: LayerType.UI, prefab: "gui/element/hnode" },
[UIID.SkillBox]: { layer: LayerType.UI, prefab: "gui/element/MSkillBox" },
}

View File

@@ -37,6 +37,7 @@ import { FieldSkillType } from "../common/config/SkillSet";
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
import { getLvColor } from "../common/config/GameSet";
import { MissionEconomy } from "./MissionEconomy";
import { buildSkillDesc } from "../common/config/HeroSkillDesc";
@@ -67,19 +68,22 @@ export class CardComp extends CCComp {
// info_node=null!
/** 卡牌名称标签节点 */
@property(Node)
name_node=null!
name_node = null!
/** 卡牌图标节点(英雄动画 / 技能图标) */
@property(Node)
icon_node=null!
icon_node = null!
/** 费用显示节点 */
@property(Node)
cost_node=null!
cost_node = null!
/** 卡牌种类标识节点(如近战 / 远程 / 辅助等分类子节点的容器) */
@property(Node)
Ckind_node=null!
Ckind_node = null!
/** 卡牌背景底框节点(按卡池等级切换子节点显示) */
@property(Node)
BG_node=null!
BG_node = null!
/** 技能卡牌信息节点,显示技能信息*/
@property(Node)
info_node = null!
@@ -87,18 +91,21 @@ export class CardComp extends CCComp {
lvl_node: Label = null! //英雄本身的等级
@property(Node)
ap_node=null!
ap_node = null!
@property(Node)
hp_node=null!
hp_node = null!
/** 技能信息标签缓存引用(由 cacheLabels 在 onLoad 时初始化) */
private infoLabel: Label | null = null;
// ======================== 运行时状态 ========================
/** 当前卡牌的金币费用 */
card_cost:number=0
card_cost: number = 0
/** 当前卡牌类型(英雄 / 技能 / 特殊升级 / 特殊刷新) */
card_type:CardType=CardType.Hero
card_type: CardType = CardType.Hero
/** 当前卡牌的唯一标识 UUID */
card_uuid:number=0
card_uuid: number = 0
/** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */
private isLocked: boolean = false;
/** 当前槽位承载的卡牌数据null 表示空槽 */
@@ -141,6 +148,7 @@ export class CardComp extends CCComp {
onLoad() {
/** 初始阶段只做UI状态准备不触发业务逻辑 */
this.bindEvents();
this.cacheLabels();
this.restPosition = this.node.position.clone();
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
this.opacityComp.opacity = 255;
@@ -148,7 +156,7 @@ export class CardComp extends CCComp {
this.applyEmptyUI();
}
/** 组件销毁时解绑所有事件,防止残留回调 */
onDestroy() {
super.onDestroy();
@@ -156,18 +164,18 @@ export class CardComp extends CCComp {
}
/** 外部初始化入口(由 MissionCardComp 调用) */
init(){
init() {
this.onMissionStart();
}
/** 游戏开始初始化(预留扩展) */
onMissionStart() {
}
/** 游戏结束清理(预留扩展) */
onMissionEnd() {
}
/** 节点启动时确保可见 */
@@ -183,7 +191,7 @@ export class CardComp extends CCComp {
* @param card 卡牌节点引用(历史遗留参数,当前未使用)
* @param data 卡牌配置数据
*/
updateCardInfo(card:Node, data: CardConfig){
updateCardInfo(card: Node, data: CardConfig) {
this.applyDrawCard(data);
}
@@ -218,13 +226,13 @@ export class CardComp extends CCComp {
* @param index 索引字符串(历史遗留参数)
*/
selectCard(e: any, index: string) {
this.useCard();
this.useCard();
}
/** 关闭界面(预留) */
close() {
}
// ======================== 核心业务方法 ========================
@@ -285,7 +293,7 @@ export class CardComp extends CCComp {
useCard(): CardConfig | null {
if (!this.cardData || this.isUsing) return null;
const cardCost = this.card_cost;
// 英雄卡特殊校验:通过 guard 对象实现"可取消"模式
if (this.cardData.type === CardType.Hero) {
const guard = {
@@ -318,7 +326,7 @@ export class CardComp extends CCComp {
// 【评分系统 - 效率分】记录刷新后的选中卡次数(命中率分子)
smc.vmdata.scores.refresh_hit_count++;
// 标记使用中,阻止并发操作
this.isUsing = true;
const used = this.cardData;
@@ -359,7 +367,7 @@ export class CardComp extends CCComp {
break;
}
}
/** 查询槽位是否有卡 */
hasCard(): boolean {
return !!this.cardData;
@@ -519,7 +527,7 @@ export class CardComp extends CCComp {
this.isLongPressed = false;
oops.gui.remove(UIID.HInfo);
}
if (deltaY >= this.dragUseThreshold) {
const used = this.useCard();
if (!used) {
@@ -527,7 +535,7 @@ export class CardComp extends CCComp {
}
return;
}
this.playReboundAnim();
}
@@ -602,9 +610,9 @@ export class CardComp extends CCComp {
// 递增视觉令牌,用于异步加载竞态保护
this.iconVisualToken += 1;
if (this.opacityComp) this.opacityComp.opacity = 255;
this.node.setPosition(this.restPosition.x, this.restPosition.y, this.restPosition.z);
// ---- 卡牌种类标识(近战 / 远程 / 辅助等) ----
const kindName = CKind[this.cardData.kind];
@@ -633,7 +641,7 @@ export class CardComp extends CCComp {
const widget = this.node.getComponent(Widget);
if (widget) widget.updateAlignment();
}
if (this.BG_node) {
const bgTrans = this.BG_node.getComponent(UITransform);
if (bgTrans) {
@@ -650,7 +658,7 @@ export class CardComp extends CCComp {
}
});
}
const hbNode = this.node.getChildByName("HB");
if (hbNode) {
const hbTrans = hbNode.getComponent(UITransform);
@@ -660,7 +668,7 @@ export class CardComp extends CCComp {
if (widget) widget.updateAlignment();
}
}
// 触发布局刷新确保其所有子节点比如右上角的cost、名字等依赖 Widget 的节点重新对齐
this.node.children.forEach(child => {
const widget = child.getComponent(Widget);
@@ -680,7 +688,7 @@ export class CardComp extends CCComp {
if (widget) widget.updateAlignment();
}
if(this.card_type===CardType.Hero){
if (this.card_type === CardType.Hero) {
const hero = HeroInfo[this.card_uuid];
const heroLv = Math.max(1, this.cardData.hero_lv ?? hero?.lv ?? 1);
this.setLabel(this.name_node, `${hero?.name || ""}`);
@@ -691,11 +699,15 @@ export class CardComp extends CCComp {
}
this.ap_node.getChildByName("val").getComponent(Label).string = `${(hero?.ap ?? 0) * heroLv}`;
this.hp_node.getChildByName("val").getComponent(Label).string = `${(hero?.hp ?? 0) * heroLv}`;
this.ap_node.active = true;
this.hp_node.active = true;
}else if(this.card_type===CardType.Skill){
// 英雄卡:隐藏技能信息节点
if (this.info_node) this.info_node.active = false;
// 英雄卡:清空技能描述文本,避免切回时残留上一次的技能信息
if (this.infoLabel) this.infoLabel.string = "";
} else if (this.card_type === CardType.Skill) {
if (this.lvl_node) this.lvl_node.node.active = false;
// 技能卡:显示技能名 + 品质后缀 + 描述
const skill = SkillSet[this.card_uuid];
@@ -706,7 +718,21 @@ export class CardComp extends CCComp {
this.ap_node.active = false;
this.hp_node.active = false;
}else{
// 技能卡:显示技能信息节点
if (this.info_node) this.info_node.active = true;
// 技能卡:填充技能描述文本(沿用 HInfoComp.buildSkillDesc 模式)
// 注意:技能卡 uuid 来自 SkillSet 而非 HeroInfo故以 SkillConfig 作为数据源;
// 用 buildSkillDesc 兼容调用,结果为空时回退到 SkillConfig.info 字段
if (this.infoLabel) {
const skill = SkillSet[this.card_uuid];
if (skill) {
const desc = buildSkillDesc(skill as any) || skill.info || "";
this.infoLabel.string = desc;
} else {
this.infoLabel.string = "";
}
}
} else {
if (this.lvl_node) this.lvl_node.node.active = false;
// 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述
const specialCard = this.card_type === CardType.SpecialUpgrade
@@ -718,6 +744,8 @@ export class CardComp extends CCComp {
this.ap_node.active = false;
this.hp_node.active = false;
// 特殊卡:清空技能描述文本,避免切到特殊卡时残留上一次的技能信息
if (this.infoLabel) this.infoLabel.string = "";
}
// ---- 费用标签 ----
@@ -728,7 +756,7 @@ export class CardComp extends CCComp {
this.setLabel(numNode, `${this.card_cost}`);
}
}
if (this.name_node) {
const currentPos = this.name_node.position;
this.name_node.setPosition(currentPos.x, -70, currentPos.z);
@@ -841,7 +869,7 @@ export class CardComp extends CCComp {
if (widget) widget.updateAlignment();
}
}
this.node.children.forEach(child => {
const widget = child.getComponent(Widget);
if (widget) widget.updateAlignment();
@@ -899,6 +927,25 @@ export class CardComp extends CCComp {
if (label) label.string = value;
}
/**
* 缓存 info_node 子树中的 Label 引用。
* 与 HInfoComp.cacheLabels() 同源实现:若 info_node 下没有 Label 则动态创建,
* 并设置与 HInfoComp 一致的字号 / 行高 / 对齐 / 溢出策略。
*/
private cacheLabels() {
if (this.infoLabel || !this.info_node) return;
this.infoLabel = this.info_node.getComponentInChildren(Label);
if (!this.infoLabel) {
const child = this.info_node.children[0] || this.info_node;
this.infoLabel = child.getComponent(Label) || child.addComponent(Label);
this.infoLabel.fontSize = 20;
this.infoLabel.lineHeight = 28;
this.infoLabel.overflow = Label.Overflow.SHRINK;
this.infoLabel.horizontalAlign = Label.HorizontalAlign.LEFT;
this.infoLabel.verticalAlign = Label.VerticalAlign.TOP;
}
}
/**
* 根据卡牌类型和 UUID 解析出图标 ID在 SpriteAtlas 中的帧名)。

View File

@@ -0,0 +1,266 @@
/**
* @file MSkillBoxComp.ts
* @description 固定波次触发的「技能卡三选一」弹窗组件UI 视图层 + 流程控制)
*
* 职责:
* 1. 由 MissionCardComp 在 NewWave 事件且当前波次 ∈ {1, 5, 10, 15, 20} 时
* 通过 `oops.gui.open(UIID.SkillBox, { wave, poolLv })` 弹出。
* 2. 从 SkillBoxPool[wave] 抽 3 张卡,渲染到 card1 / card2 / card3 三个 CardComp 槽位。
* 3. 玩家点中其中一张卡后,直接以该卡为 payload 分发 UseSkillCard 事件,
* 由 MissSkillsComp 接管并实例化 SkillBoxComp(走 SkillBoxCardConfig 路径)。
* 4. 提供「刷新」按钮(扣除 refreshRemain 一次)和「看广告刷新」按钮(回调留空,
* 待接入广告 SDK 后会把 adRefreshRemain 累加 +3)。
*
* 关键设计:
* - **强制必选**:不绑定关闭按钮,玩家只能点中 3 张卡之一才能关闭弹窗。
* - **免费领取**:cost 强制 0,dispatchEvent(UseSkillCard) 不走 CoinAdd 扣费。
* - **次数持久化**:refreshRemain / adRefreshRemain 存于 smc.vmdata.mission_data,
* 跨波次保留,每局 mission 开始时由 MissionComp.data_init() 重置为 1/0。
*
* 依赖:
* - CardComp —— 复用其渲染/交互逻辑(免费版:card_cost=0)
* - SkillBoxPool / getSkillBoxCards (CardSet) —— 5 级硬编码技能池
* - GameEvent.UseSkillCard —— 技能使用事件
* - UIID.SkillBox (GameUIConfig) —— 弹窗 prefab 路径
* - smc.vmdata.mission_data —— 刷新次数持久化
*/
import { mLogger } from "../common/Logger";
import { _decorator, Label, Node, Button } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { GameEvent } from "../common/config/GameEvent";
import { oops } from "db://oops-framework/core/Oops";
import { smc } from "../common/SingletonModuleComp";
import { getSkillBoxCards, SkillBoxCardConfig } from "../common/config/CardSet";
import { UIID } from "../common/config/GameUIConfig";
import { CardComp } from "./CardComp";
const { ccclass, property } = _decorator;
/**
* MSkillBoxComp —— 技能三选一弹窗控制器
*
* 由 oops.gui.open(UIID.SkillBox, { wave, poolLv }) 实例化。
*/
@ccclass('MSkillBoxComp')
@ecs.register('MSkillBoxComp', false)
export class MSkillBoxComp extends CCComp {
/** 是否启用调试日志 */
private debugMode: boolean = true;
// ======================== 编辑器绑定节点 ========================
/** 卡牌槽位 1 节点(需挂 CardComp) */
@property(Node)
card1: Node = null!
/** 卡牌槽位 2 节点(需挂 CardComp) */
@property(Node)
card2: Node = null!
/** 卡牌槽位 3 节点(需挂 CardComp) */
@property(Node)
card3: Node = null!
/** 刷新按钮(消耗 1 次 refreshRemain 重抽 3 张) */
@property(Node)
refreshBtn: Node = null!
/** 看广告刷新按钮(回调留空,后续接入广告 SDK 后累加 adRefreshRemain +3) */
@property(Node)
adRefreshBtn: Node = null!
/** 刷新次数显示标签(可选,显示 "1/1" 形式) */
@property(Label)
refreshCountLabel: Label = null!
// ======================== 运行时状态 ========================
/** 当前波次(1/5/10/15/20) */
private currentWave: number = 0;
/** 当前卡池等级(预留扩展,目前未使用) */
private currentPoolLv: number = 1;
/** 3 个 CardComp 子控制器引用(有序) */
private cardComps: CardComp[] = [];
/** 是否已派发 UseSkillCard(防重复触发关闭) */
private hasPicked: boolean = false;
// ======================== 生命周期 ========================
onLoad() {
// 监听任务结束,自动关闭弹窗(避免玩家关游戏时残留)
oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this);
this.bindEvents();
}
onAdded(args: { wave?: number; poolLv?: number }) {
this.currentWave = Math.max(0, Math.floor(Number(args?.wave ?? 0)));
this.currentPoolLv = Math.max(1, Math.floor(Number(args?.poolLv ?? 1)));
this.hasPicked = false;
this.cacheCardComps();
this.rollAndRender();
this.refreshRefreshCountUI();
mLogger.log(this.debugMode, "MSkillBoxComp", "opened", {
wave: this.currentWave,
poolLv: this.currentPoolLv,
refreshRemain: this.getRefreshRemain(),
adRefreshRemain: this.getAdRefreshRemain(),
});
}
onDestroy() {
super.onDestroy();
this.unbindEvents();
oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this);
}
onMissionEnd() {
// 任务结束时强制关闭弹窗
oops.gui.remove(UIID.SkillBox);
}
init() {
// 弹窗组件无需额外 init,onAdded 阶段完成所有初始化
}
update(dt: number) {
// 弹窗无帧更新逻辑
}
reset() {
// ECS 组件移除时清理
}
// ======================== 内部:CardComp 缓存 ========================
/**
* 缓存 3 个卡槽上的 CardComp 引用。
* 与 MissionCardComp.cacheCardComps() 同源实现,确保即开即用。
*/
private cacheCardComps() {
this.cardComps = [];
const slots: (Node | null)[] = [this.card1, this.card2, this.card3];
for (let i = 0; i < slots.length; i++) {
const node = slots[i];
if (!node) continue;
const comp = node.getComponent(CardComp) || node.addComponent(CardComp);
this.cardComps.push(comp);
}
}
// ======================== 内部:抽卡与渲染 ========================
/**
* 从 SkillBoxPool[currentWave] 抽 3 张并渲染到 3 个卡槽。
* 渲染时强制 cost=0,触发 free 路径。
*/
private rollAndRender() {
const cards = getSkillBoxCards(this.currentWave, 3);
for (let i = 0; i < this.cardComps.length; i++) {
const comp = this.cardComps[i];
if (!comp) continue;
if (i < cards.length) {
comp.applyDrawCard(cards[i]);
comp.card_cost = 0; // 强制免费
} else {
comp.clearBySystem();
}
}
}
/** 重新抽 3 张(玩家点 refreshBtn) */
private reroll() {
if (this.hasPicked) return;
this.rollAndRender();
mLogger.log(this.debugMode, "MSkillBoxComp", "reroll", {
refreshRemain: this.getRefreshRemain(),
adRefreshRemain: this.getAdRefreshRemain(),
});
}
// ======================== 内部:刷新次数持久化 ========================
/** 读取当前总可用刷新次数(普通 + 广告奖励) */
private getRefreshRemain(): number {
const d = smc.vmdata?.mission_data;
if (!d) return 0;
return Math.max(0, Math.floor(Number(d.skill_box_refresh_remain ?? 0)));
}
/** 读取广告奖励次数(待接入 SDK 后使用) */
private getAdRefreshRemain(): number {
const d = smc.vmdata?.mission_data;
if (!d) return 0;
return Math.max(0, Math.floor(Number(d.skill_box_ad_refresh_remain ?? 0)));
}
/** 消耗一次刷新(优先用普通次数,再用广告奖励) */
private consumeRefresh(): boolean {
const d = smc.vmdata?.mission_data;
if (!d) return false;
const remain = this.getRefreshRemain();
const adRemain = this.getAdRefreshRemain();
if (remain + adRemain <= 0) return false;
if (remain > 0) {
d.skill_box_refresh_remain = remain - 1;
} else {
d.skill_box_ad_refresh_remain = adRemain - 1;
}
return true;
}
/** 同步刷新次数显示 */
private refreshRefreshCountUI() {
if (!this.refreshCountLabel) return;
const total = this.getRefreshRemain() + this.getAdRefreshRemain();
this.refreshCountLabel.string = `${total}`;
}
// ======================== 内部:玩家选择 ========================
/**
* 监听每个 CardComp 的 UseSkillCard 派发,以关闭弹窗。
* 由于 CardComp.useCard 内部已经 dispatchEvent(UseSkillCard, payload),
* 这里只需监听事件并在识别为本弹窗的卡时关闭。
*/
private onSkillCardUsed(event: string, args: any) {
if (this.hasPicked) return;
const payload = args ?? event;
if (!payload) return;
const uuid = Number(payload?.uuid ?? 0);
// 仅处理本弹窗抽出的 SkillBox 卡(uuid >= 9000)
if (uuid < 9000) return;
this.hasPicked = true;
mLogger.log(this.debugMode, "MSkillBoxComp", "player picked skill card", { uuid });
oops.gui.remove(UIID.SkillBox);
}
// ======================== 按钮事件 ========================
private bindEvents() {
this.refreshBtn?.on(Button.EventType.CLICK, this.onRefreshClick, this);
this.adRefreshBtn?.on(Button.EventType.CLICK, this.onAdRefreshClick, this);
oops.message.on(GameEvent.UseSkillCard, this.onSkillCardUsed, this);
}
private unbindEvents() {
this.refreshBtn?.off(Button.EventType.CLICK, this.onRefreshClick, this);
this.adRefreshBtn?.off(Button.EventType.CLICK, this.onAdRefreshClick, this);
oops.message.off(GameEvent.UseSkillCard, this.onSkillCardUsed, this);
}
private onRefreshClick() {
if (this.hasPicked) return;
if (this.getRefreshRemain() + this.getAdRefreshRemain() <= 0) {
oops.gui.toast("刷新次数已用完,请观看广告获取");
return;
}
if (!this.consumeRefresh()) return;
this.reroll();
this.refreshRefreshCountUI();
}
private onAdRefreshClick() {
// TODO: 接入广告 SDK 后,在广告播放成功回调中:
// smc.vmdata.mission_data.skill_box_ad_refresh_remain += 3;
// this.refreshRefreshCountUI();
// 当前为占位实现,仅打印日志提示
mLogger.log(this.debugMode, "MSkillBoxComp", "onAdRefreshClick", "TODO: 接入广告 SDK,成功后 adRefreshRemain += 3");
oops.gui.toast("广告功能即将上线");
}
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "76a850b1-5eda-481f-a6e1-8e12e41db6c8",
"uuid": "85b247fa-1f22-411a-b3a6-82e9797512e1",
"files": [],
"subMetas": {},
"userData": {}

View File

@@ -28,6 +28,7 @@ import { SkillBoxComp } from "./SkillBoxComp";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
import { SkillBoxCardConfig } from "../common/config/CardSet";
const { ccclass, property } = _decorator;
/** 技能槽位数据结构 */
@@ -55,7 +56,7 @@ export class MissSkillsComp extends CCComp {
private debugMode: boolean = true;
/** 技能卡 Prefab在编辑器中赋值 */
@property({type: Prefab})
@property({ type: Prefab })
private skill_box: Prefab = null;
/**
@@ -67,13 +68,13 @@ export class MissSkillsComp extends CCComp {
{ x: -320, y: 240, used: false, node: null },
{ x: -240, y: 240, used: false, node: null },
{ x: -160, y: 240, used: false, node: null },
{ x: -80, y: 240, used: false, node: null },
{ x: 0, y: 240, used: false, node: null },
{ x: -80, y: 240, used: false, node: null },
{ x: 0, y: 240, used: false, node: null },
{ x: -320, y: 320, used: false, node: null },
{ x: -240, y: 320, used: false, node: null },
{ x: -160, y: 320, used: false, node: null },
{ x: -80, y: 320, used: false, node: null },
{ x: 0, y: 320, used: false, node: null },
{ x: -80, y: 320, used: false, node: null },
{ x: 0, y: 320, used: false, node: null },
];
/** 注册事件监听 */
@@ -141,17 +142,36 @@ export class MissSkillsComp extends CCComp {
* 处理使用技能卡事件:提取 uuid 和 card_lv 后调用 addSkill。
* @param event 事件名
* @param args 卡牌数据(含 uuid、card_lv
*
* 兼容两种数据源:
* - 普通卡池uuid < 9000:走 addSkill(uuid, card_lv) 旧流程
* - SkillBox 卡池uuid >= 9000 且 payload 含 s_uuid:
* 视为 SkillBoxCardConfig,使用 addSkillByConfig 走新流程
*/
private onUseSkillCard(event: string, args: any) {
const payload = args ?? event;
const uuid = Number(payload?.uuid ?? 0);
const card_lv = Math.max(1, Math.floor(Number(payload?.card_lv ?? 1)));
if (!uuid) return;
this.addSkill(uuid, card_lv);
if (this.isSkillBoxPayload(payload)) {
this.addSkillByConfig(payload as SkillBoxCardConfig);
} else {
this.addSkill(uuid, card_lv);
}
}
/**
* 判断 payload 是否为 SkillBox 三选一弹窗的卡牌配置。
* 识别规则:uuid >= 9000 且 payload 含 s_uuid 字段。
*/
private isSkillBoxPayload(payload: any): boolean {
if (!payload) return false;
const uuid = Number(payload.uuid ?? 0);
return uuid >= 9000 && Number(payload.s_uuid ?? 0) > 0;
}
start() {
}
/**
@@ -166,7 +186,7 @@ export class MissSkillsComp extends CCComp {
addSkill(uuid: number, card_lv: number) {
// 技能节点的父容器
var parent = smc.map.MapView.scene.entityLayer!.node!.getChildByName("SKILL")!;
if (!this.skill_box) {
mLogger.error(this.debugMode, "MissSkillsComp", "skill_box prefab not set");
return;
@@ -183,7 +203,7 @@ export class MissSkillsComp extends CCComp {
const node = instantiate(this.skill_box);
node.parent = parent;
node.setPosition(new Vec3(this.slots[emptyIndex].x, this.slots[emptyIndex].y, 0));
this.slots[emptyIndex].used = true;
this.slots[emptyIndex].node = node;
@@ -192,6 +212,37 @@ export class MissSkillsComp extends CCComp {
comp.init(uuid, card_lv);
}
/**
* 在场上添加一个 SkillBox 弹窗产出的技能卡uuid >= 9000
* 流程与 addSkill 相同,但初始化走 initWithConfig 以支持 overrides / s_uuid。
*
* @param skillBoxCard SkillBoxCardConfig 完整卡牌配置
*/
addSkillByConfig(skillBoxCard: SkillBoxCardConfig) {
var parent = smc.map.MapView.scene.entityLayer!.node!.getChildByName("SKILL")!;
if (!this.skill_box) {
mLogger.error(this.debugMode, "MissSkillsComp", "skill_box prefab not set");
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));
this.slots[emptyIndex].used = true;
this.slots[emptyIndex].node = node;
const comp = node.getComponent(SkillBoxComp) || node.addComponent(SkillBoxComp);
comp.initWithConfig(skillBoxCard);
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy();

View File

@@ -37,7 +37,7 @@ import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, SpriteAtla
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { GameEvent } from "../common/config/GameEvent";
import { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE, CardConfig, CardType, CardsUpSet, drawCardsByRule, getCardsByLv, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
import { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE, CardConfig, CardType, CardsUpSet, drawCardsByRule, getCardsByLv, SKILL_BOX_TRIGGER_WAVES, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
import { CardComp } from "./CardComp";
import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
@@ -48,6 +48,7 @@ import { FacSet, FightSet } from "../common/config/GameSet";
import { MoveComp } from "../hero/MoveComp";
import { MissionHeroComp } from "./MissionHeroComp";
import { MissionEconomy } from "./MissionEconomy";
import { UIID } from "../common/config/GameUIConfig";
const { ccclass, property } = _decorator;
@@ -323,13 +324,35 @@ export class MissionCardComp extends CCComp {
}
/** 新一波:展开面板 → 刷新费用 UI → 重新抽卡分发 */
private onNewWave() {
private onNewWave(event?: string, args?: any) {
this.isBattlePhase = false;
this.enterPreparePhase();
this.updateCoinAndCostUI();
this.layoutCardSlots();
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
// 固定波次(1/5/10/15/20)弹出技能三选一弹窗
const wave = Number(args?.wave ?? 0);
if (this.isSkillBoxTriggerWave(wave)) {
this.openSkillBox(wave);
}
}
/** 判断 wave 是否属于技能弹窗触发波次 */
private isSkillBoxTriggerWave(wave: number): boolean {
if (!wave || wave <= 0) return false;
return SKILL_BOX_TRIGGER_WAVES.includes(wave);
}
/** 打开技能三选一弹窗(MSkillBoxComp) */
private openSkillBox(wave: number) {
if (smc.map?.MapView) {
oops.gui.open(UIID.SkillBox, { wave, poolLv: this.poolLv });
mLogger.log(this.debugMode, "MissionCardComp", "openSkillBox", { wave, poolLv: this.poolLv });
} else {
mLogger.warn(this.debugMode, "MissionCardComp", "openSkillBox skipped, smc.map.MapView not ready");
}
}
/** 解除按钮监听,避免节点销毁后回调泄漏 */
@@ -603,13 +626,17 @@ export class MissionCardComp extends CCComp {
this.cardsHideScale = new Vec3(0, 0, this.cardsBaseScale.z);
}
/** 进入准备阶段:展开卡牌面板(立即恢复缩放,无动画) */
/** 进入准备阶段:展开卡牌面板(立即恢复缩放,无动画)+ 显示抽卡按钮 */
private enterPreparePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
this.cards_node.active = true;
Tween.stopAllByTarget(this.cards_node);
this.cards_node.setScale(this.cardsShowScale);
// 准备阶段:显示抽卡按钮
if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.active = true;
}
}
private enterBattlePhase() {
@@ -625,16 +652,16 @@ export class MissionCardComp extends CCComp {
// }
// })
// .start();
// 战斗阶段:隐藏抽卡按钮
if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.active = false;
}
}
/** 构建本次抽卡结果保证最终可分发3条数据 */
private buildDrawCards(): CardConfig[] {
let targetType: CardType | CardType[] | undefined = undefined;
if (this.isBattlePhase) {
targetType = CardType.Skill;
} else {
targetType = [CardType.Hero, CardType.SpecialRefresh];
}
// 技能卡已不再通过常规刷新分发,统一走 MSkillBoxComp 固定波次弹窗
const targetType: CardType[] = [CardType.Hero, CardType.SpecialRefresh];
const cards = getCardsByLv(this.poolLv, targetType);
/** 正常情况下直接取前3 */

View File

@@ -27,7 +27,7 @@
* - CardInitCoins —— 初始金币数
* - UIID.Victory —— 结算弹窗
*/
import { _decorator, Vec3,Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label, CCInteger, tween, v3, Tween, Widget, UIOpacity } from "cc";
import { _decorator, Vec3, Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label, CCInteger, tween, v3, Tween, Widget, UIOpacity } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { smc } from "../common/SingletonModuleComp";
@@ -97,17 +97,17 @@ export class MissionComp extends CCComp {
/** 卡池升级波次配置:达到对应波次时,推送卡池升级事件 */
@property({ type: [CCInteger], tooltip: "卡池升级波次配置,例如 [10, 20] 表示第10波升到2级第20波升到3级" })
cardPoolUpgradeWaves: number[] = [5, 10];
// ======================== 编辑器绑定节点 ========================
/** 开始战斗按钮 */
@property(Node)
start_btn:Node = null!
start_btn: Node = null!
/** 时间/波数显示节点 */
@property(Node)
time_node:Node = null!
time_node: Node = null!
@property(Node)
tooltip:Node = null!
tooltip: Node = null!
/** 阶段名称映射表(用于 UI 显示) */
private static readonly PhaseNameMap: Record<MissionPhase, string> = {
@@ -124,19 +124,19 @@ export class MissionComp extends CCComp {
// ======================== 运行时状态 ========================
/** 战斗已耗时(秒),正向计时 */
clearTime:number = 0
clearTime: number = 0
/** 剩余复活次数 */
revive_times: number = 1;
/** 掉落奖励列表 */
rewards:any[]=[]
rewards: any[] = []
/** 累计游戏数据 */
game_data:any={
exp:0,
gold:0,
diamond:0
game_data: any = {
exp: 0,
gold: 0,
diamond: 0
}
/**秒计时 */
PhaseTime:Timer= new Timer(1)
PhaseTime: Timer = new Timer(1)
/** 上一次显示的时间字符串(避免重复设置) */
private lastTimeStr: string = "";
/** 上一次显示的秒数(避免重复计算) */
@@ -180,18 +180,18 @@ export class MissionComp extends CCComp {
private skillViewMatcher: any = null;
/** 匹配拥有 HeroAttrsComp 的实体(英雄/怪物属性) */
private heroAttrsMatcher: any = null;
// ======================== 生命周期 ========================
onLoad(){
onLoad() {
this.heroViewMatcher = ecs.allOf(HeroViewComp);
this.skillViewMatcher = ecs.allOf(SkillView);
this.heroAttrsMatcher = ecs.allOf(HeroAttrsComp);
this.showMemoryPanel = false
// 注册生命周期事件
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.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()
}
@@ -206,7 +206,7 @@ export class MissionComp extends CCComp {
smc.map.MapView.scene.mapLayer.stopAnimations();
}
onDestroy(){
onDestroy() {
smc.map.MapView.scene.mapLayer.playAnimations()
super.onDestroy();
if (this.start_btn && this.start_btn.isValid) {
@@ -220,10 +220,10 @@ export class MissionComp extends CCComp {
* - 战斗中 → 同步怪物状态、更新计时器
*/
protected update(dt: number): void {
if(!smc.mission.play) return
if (!smc.mission.play) return
// 如果是暂停状态,且不在 BattleEnd 阶段(全灭时需要播放完 fend 技能动画并自动流转),才真正停止 update 逻辑
if(smc.mission.pause && this.currentPhase !== MissionPhase.BattleEnd) return
if (smc.mission.pause && this.currentPhase !== MissionPhase.BattleEnd) return
// 处理过渡阶段的计时
if (this.currentPhase === MissionPhase.PrepareStart ||
@@ -235,10 +235,10 @@ export class MissionComp extends CCComp {
}
}
if(this.currentPhase === MissionPhase.Battle){
if (this.currentPhase === MissionPhase.Battle) {
this.syncMonsterSpawnState(dt)
if(smc.mission.stop_mon_action) return
smc.vmdata.mission_data.fight_time+=dt
if (smc.mission.stop_mon_action) return
smc.vmdata.mission_data.fight_time += dt
this.clearTime += dt
this.update_time();
}
@@ -247,14 +247,14 @@ export class MissionComp extends CCComp {
// ======================== 时间显示 ========================
/** 更新时间/波数显示(仅在秒数变化时更新以减少 Label 操作) */
update_time(){
update_time() {
const remainSecond = Math.floor(this.clearTime);
if (remainSecond === this.lastTimeSecond) return;
this.lastTimeSecond = remainSecond;
let m = Math.floor(remainSecond / 60);
let s = remainSecond % 60;
let str = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
if(str != this.lastTimeStr){
if (str != this.lastTimeStr) {
if (this.time_node && this.time_node.isValid) {
const timeChild = this.time_node.getChildByName("time");
if (timeChild) {
@@ -269,24 +269,24 @@ export class MissionComp extends CCComp {
// ======================== 奖励与广告 ========================
/** 奖励发放(预留) */
do_reward(){
do_reward() {
}
/**
* 广告回调处理:
* 成功 → 增加刷新次数;失败 → 分发失败事件。
*/
do_ad(){
if(this.ad_back()){
do_ad() {
if (this.ad_back()) {
oops.message.dispatchEvent(GameEvent.AD_BACK_TRUE)
smc.vmdata.mission_data.refresh_count+=FightSet.MORE_RC
}else{
smc.vmdata.mission_data.refresh_count += FightSet.MORE_RC
} else {
oops.message.dispatchEvent(GameEvent.AD_BACK_FALSE)
}
}
/** 广告观看结果(预留,默认返回 true */
ad_back(){
ad_back() {
return true
}
@@ -300,18 +300,18 @@ export class MissionComp extends CCComp {
* 4. 分发 FightReady 事件。
* 5. 进入准备阶段并显示 loading。
*/
async mission_start(){
async mission_start() {
this.unscheduleAllCallbacks();
this.cleanComponents();
this.data_init()
oops.message.dispatchEvent(GameEvent.FightReady)
this.changePhase(MissionPhase.Prepare)
let loading=this.node.getChildByName("loading")
let loading = this.node.getChildByName("loading")
if (loading) {
loading.active=true
this.scheduleOnce(()=>{
loading.active=false
},0.5)
loading.active = true
this.scheduleOnce(() => {
loading.active = false
}, 0.5)
}
}
@@ -389,8 +389,8 @@ export class MissionComp extends CCComp {
const phaseName = MissionComp.PhaseNameMap[targetPhase] || "未知";
// 播放状态切换提示栏动效(过滤掉 None、Prepare 准备阶段、Battle 战斗中阶段)
if (targetPhase !== MissionPhase.None &&
targetPhase !== MissionPhase.Prepare &&
if (targetPhase !== MissionPhase.None &&
targetPhase !== MissionPhase.Prepare &&
targetPhase !== MissionPhase.Battle) {
this.playTooltipAnim(phaseName);
}
@@ -404,7 +404,7 @@ export class MissionComp extends CCComp {
const wave = Math.max(1, this.currentWave || (smc.vmdata && smc.vmdata.mission_data ? smc.vmdata.mission_data.level : 1) || 1);
label.string = `${wave}/15 波`;
}
// 阶段切换动感表现:只在进入战斗阶段跳动一下,让流程充满心流体验
if (targetPhase === MissionPhase.BattleStart) {
Tween.stopAllByTarget(this.time_node);
@@ -449,6 +449,10 @@ export class MissionComp extends CCComp {
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);
break;
@@ -462,7 +466,7 @@ export class MissionComp extends CCComp {
smc.vmdata.scores.wave_win_count++;
// 【评分系统 - 战绩分】记录每回合结束时场上留存的敌人数量(扣分项)
smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num;
let allAlive = true;
let hasHero = false;
ecs.query(this.heroAttrsMatcher).forEach(entity => {
@@ -485,7 +489,7 @@ export class MissionComp extends CCComp {
// 触发战斗结束技能fend
this.triggerHeroBattleSkills(false);
// 战斗结束阶段给予所有英雄恢复70%血量的技能效果
this.healAllHeroes();
break;
@@ -496,7 +500,7 @@ export class MissionComp extends CCComp {
smc.mission.stop_spawn_mon = true;
// 不隐藏开始按钮
break;
case MissionPhase.None:
smc.mission.in_fight = false;
smc.vmdata.mission_data.in_fight = false;
@@ -529,7 +533,7 @@ export class MissionComp extends CCComp {
this.changePhase(MissionPhase.PrepareStart);
} else {
this.changePhase(MissionPhase.Settle);
// 此时已经经过了 2s可以真正执行结算弹窗或清理逻辑
if (smc.mission.play) {
// 如果游戏还在运行中,说明是通过 open_Victory 进来的
@@ -559,7 +563,7 @@ export class MissionComp extends CCComp {
* - 分发 FightStart 事件
* - 触发英雄战斗开始技能
*/
to_fight(){
to_fight() {
this.changePhase(MissionPhase.PrepareEnd);
}
@@ -609,14 +613,14 @@ export class MissionComp extends CCComp {
const attrs = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!attrs || !view || attrs.fac !== FacSet.HERO) return;
// 计算恢复量:基于配置的百分比(如 70%)的最大生命值
const healAmount = Math.floor(attrs.hp_max * finalHealRate);
// 应用恢复量,不超过最大生命值
attrs.hp = Math.min(attrs.hp_max, attrs.hp + healAmount);
attrs.dirty_hp = true;
// 重置复活次数,使得下回合可以继续复活
attrs.revived_count = 0;
@@ -642,7 +646,7 @@ export class MissionComp extends CCComp {
* @param e 事件对象(未使用)
* @param is_hero_dead 是否因英雄全灭触发
*/
open_Victory(e:any,is_hero_dead: boolean = false){
open_Victory(e: any, is_hero_dead: boolean = false) {
// 战斗失败或胜利,标记暂停状态以切断波次流转逻辑
smc.mission.pause = true;
// 直接切入 BattleEnd触发 fend 表现
@@ -652,9 +656,9 @@ export class MissionComp extends CCComp {
/** 战斗结束:延迟清理组件和对象池 */
fight_end(){
fight_end() {
// 这里只是强制清理关卡,为了防止重复弹窗,标记 play = false
smc.mission.play=false
smc.mission.play = false
this.changePhase(MissionPhase.BattleEnd);
}
@@ -665,9 +669,9 @@ export class MissionComp extends CCComp {
* - 清理组件和对象池
* - 隐藏节点
*/
mission_end(){
mission_end() {
this.unscheduleAllCallbacks();
smc.mission.play=false
smc.mission.play = false
smc.mission.pause = false;
this.changePhase(MissionPhase.None);
this.cleanComponents()
@@ -682,7 +686,7 @@ export class MissionComp extends CCComp {
* - 奖励列表 / 复活次数
* - 性能监控基准值
*/
data_init(){
data_init() {
if (!this.PhaseTime) {
this.PhaseTime = new Timer(1);
}
@@ -690,16 +694,16 @@ export class MissionComp extends CCComp {
smc.mission.pause = false;
smc.mission.stop_mon_action = false;
smc.mission.stop_spawn_mon = false;
smc.vmdata.mission_data.in_fight=false
smc.vmdata.mission_data.fight_time=0
smc.vmdata.mission_data.in_fight = false
smc.vmdata.mission_data.fight_time = 0
this.clearTime = 0
smc.vmdata.mission_data.mon_num=0
smc.vmdata.mission_data.mon_num = 0
smc.vmdata.mission_data.level = 1
smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount))
this.currentPhase = MissionPhase.None;
this.currentWave = 1;
this.isBossWave = false;
this.rewards=[]
this.rewards = []
this.revive_times = 1;
this.lastTimeStr = "";
this.lastTimeSecond = -1;
@@ -714,15 +718,18 @@ export class MissionComp extends CCComp {
this.heapTrendBaseMB = -1;
this.monsterCountSyncTimer = 0;
this.lastPrepareCoinWave = 0;
spawningEngine.reset();
// 重置所有的战局得分数据,防止上一局的数据污染
smc.resetScores();
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins));
// 【评分系统 - 效率分】记录初始获得的金币收入
smc.vmdata.scores.gold_earned += smc.vmdata.mission_data.coin;
// 技能三选一弹窗:每局重置 1 次普通刷新 + 清零广告累计次数
smc.vmdata.mission_data.skill_box_refresh_remain = 1;
smc.vmdata.mission_data.skill_box_ad_refresh_remain = 0;
}
// ======================== 波次管理 ========================
@@ -740,9 +747,9 @@ export class MissionComp extends CCComp {
private onNewWave(event: string, data: any) {
const wave = Number(data?.wave ?? 0);
if (wave <= 0) return;
this.isBossWave = !!data?.bossWave;
if (wave > 1) {
// 在新一波到来时,先进入 BattleEnd触发上一波的战斗结束技能 (fend)2秒后自动进入下一波的准备阶段
this.changePhase(MissionPhase.BattleEnd);
@@ -773,7 +780,7 @@ export class MissionComp extends CCComp {
if (upgradeIndex !== -1) {
// 根据配置的索引,计算目标等级(初始等级 + index + 1
// 例如 index=0对应等级为2index=1对应等级为3
const targetLv = upgradeIndex + 2;
const targetLv = upgradeIndex + 2;
oops.message.dispatchEvent(GameEvent.CardPoolUpgrade, { wave, targetLv });
mLogger.log(this.debugMode, 'MissionComp', "card pool upgrade event pushed", { wave, targetLv });
}
@@ -836,7 +843,7 @@ export class MissionComp extends CCComp {
}
});
this.handleHeroWipe(heroCount);
// 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,且待刷新的怪物队列也为空,直接结束战斗进入下一波的准备阶段
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
@@ -844,7 +851,7 @@ export class MissionComp extends CCComp {
// 如果能获取当前已部署英雄数最好,这里简化处理,大于 4 个就算高存活
heroesAliveRatio = Math.min(1.0, heroCount / 4.0);
spawningEngine.updateAdaptive(heroesAliveRatio, this.clearTime);
if (this.currentWave >= 15) {
// 15 波通关
this.open_Victory(null, false);
@@ -853,7 +860,7 @@ export class MissionComp extends CCComp {
}
return;
}
smc.vmdata.mission_data.mon_num = monsterCount;
const { max, resume } = this.getMonsterThresholds();
smc.vmdata.mission_data.mon_max = max;
@@ -938,7 +945,7 @@ export class MissionComp extends CCComp {
skillNodes: skillRoot?.children.length || 0
};
}
// ======================== 性能监控面板 ========================
/** 性能监控相关代码 */

View File

@@ -29,8 +29,8 @@ 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";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { CardPoolList } from "../common/config/CardSet";
import { SkillSet } from "../common/config/SkillSet";
import { CardPoolList, SkillBoxCardConfig } from "../common/config/CardSet";
import { SkillOverrides, SkillSet } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
@@ -49,8 +49,8 @@ export class SkillBoxComp extends CCComp {
private debugMode: boolean = true;
/** 技能图标节点 */
@property({type: Node})
private icon_node:Node= null;
@property({ type: Node })
private icon_node: Node = null;
/** 剩余次数标签 */
@property(Label)
@@ -68,6 +68,8 @@ export class SkillBoxComp extends CCComp {
private trigger_times: number = 1;
/** 触发间隔(秒,仅持续技能有效) */
private trigger_interval: number = 0;
/** 技能参数覆盖(来自 SkillBoxCardConfig.overrides,触发时随事件派发) */
private skill_overrides: SkillOverrides | null = null;
// ======================== 运行时状态 ========================
@@ -115,7 +117,7 @@ export class SkillBoxComp extends CCComp {
init(uuid: number, card_lv: number) {
this.s_uuid = uuid;
this.card_lv = card_lv;
// 查询触发配置
const config = CardPoolList.find(c => c.uuid === uuid);
if (config) {
@@ -123,6 +125,7 @@ export class SkillBoxComp extends CCComp {
this.trigger_times = config.t_times ?? 1;
this.trigger_interval = config.t_inv ?? 0;
}
this.skill_overrides = null;
this.current_trigger_times = 0;
this.timer = 0;
@@ -143,6 +146,39 @@ export class SkillBoxComp extends CCComp {
}
}
/**
* 使用 SkillBoxCardConfig 初始化MSkillBoxComp 三选一弹窗场景)
*
* 与 init(uuid, card_lv) 的区别:
* - 直接以 s_uuid 字段作为实际生效技能
* - 触发参数is_inst / t_times / t_inv取自 SkillBoxCardConfig
* - overrides 在 triggerSkill 时随事件一起派发,由技能执行系统按需应用
*/
initWithConfig(skillBoxCard: SkillBoxCardConfig) {
this.s_uuid = skillBoxCard.s_uuid;
this.card_lv = Math.max(1, Math.floor(skillBoxCard.card_lv ?? 1));
this.is_instant = skillBoxCard.is_inst ?? true;
this.trigger_times = skillBoxCard.t_num ?? skillBoxCard.t_times ?? 1;
this.trigger_interval = skillBoxCard.t_inv ?? 0;
this.skill_overrides = skillBoxCard.overrides ?? null;
this.current_trigger_times = 0;
this.timer = 0;
this.initialized = true;
this.updateUI();
if (this.is_instant) {
this.triggerSkill();
this.current_trigger_times++;
if (this.current_trigger_times >= this.trigger_times) {
this.scheduleOnce(() => {
this.node.destroy();
}, 1.0);
}
}
}
/**
* 更新 UI
* - 图标:从 uicons 图集获取。
@@ -160,7 +196,7 @@ export class SkillBoxComp extends CCComp {
}
}
}
// 更新剩余次数标签
if (this.info_label) {
if (!this.is_instant) {
@@ -178,7 +214,7 @@ export class SkillBoxComp extends CCComp {
private onFightStart() {
if (!this.initialized) return;
this.in_combat = true;
if (!this.is_instant) {
this.timer = 0; // 重置计时器
}
@@ -201,7 +237,7 @@ export class SkillBoxComp extends CCComp {
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();
@@ -233,7 +269,7 @@ export class SkillBoxComp extends CCComp {
this.triggerSkill();
this.current_trigger_times++;
this.updateUI();
// 次数用完 → 延迟销毁
if (this.current_trigger_times >= this.trigger_times) {
this.scheduleOnce(() => {
@@ -256,12 +292,14 @@ export class SkillBoxComp extends CCComp {
const localPos = this.node.position;
const parentPos = this.node.parent ? this.node.parent.position : new Vec3(0, 0, 0);
targetPos.set(parentPos.x + localPos.x, parentPos.y + localPos.y, 0);
// 将 SkillBoxCardConfig 的 overrides 一起派发,技能执行系统可按需合并到基础 SkillSet 配置
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: this.s_uuid,
isCardSkill: true, // 标记为卡牌技能(区别于英雄自身技能)
card_lv: this.card_lv,
targetPos: targetPos
targetPos: targetPos,
overrides: this.skill_overrides ?? undefined
});
}