import { _decorator, BoxCollider2D, Color, find, isValid, Node, PhysicsSystem2D, sp, tween, Tween, UITransform, v3, Vec2, Vec3 } from 'cc'; import { SuperTrail } from 'db://assets/mx/components/SuperTrail'; import { Skill } from './WeaponSkill/Skill'; import { BattleType, ChapterDifficulty } from '../../manager/ChapterDataManager'; import { GBundle, UIID } from '../../game/ConfigRes'; import { MapManager } from './MapManager'; import { MonsterSpawner } from './MonsterSpawner'; import { Monster, MonsterTag } from './Monster'; import { PlayerController } from './PlayerController'; import { SkillController, WeaponState } from './WeaponSkill/SkillController'; import { DamageStatistics } from './DamageStatistics'; import { BattleDataManager } from './BattleDataManager'; import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager'; import { GEvent } from 'db://assets/mx/module/event/GEvent'; import { ConstEnum, ConstManager } from '../../manager/ConstManager'; import { MapAgent } from './MapAgent'; import { MapGrid } from './MapGrid'; import { sdkConfig } from '../../sdk/sdkConfig'; import { StatisticsType } from '../../game/GameData'; import { MapObstacle, ObstacleType } from './MapObstacle'; import { SkillSelectCore } from '../../game/SkillSelectCore'; import { ConstNumType, TetriType, TetriWeaponType } from '../../game/ConfigProjectData'; import { SkillCostType, TableNames } from '../../game/ConfigTableData'; import MTools from 'db://assets/mx/tools/MTools'; import { TargetTaskType } from '../../manager/TargetTaskManager'; import { ArrowFlowLine } from 'db://assets/mx/components/ArrowFlowLine'; import { tetriMap } from '../../tetriMap/tetriMap'; import { BattleDiagnostics } from '../../diagnostics/BattleDiagnostics'; import { tetriWeaponSystem } from '../../tetriMap/tetriWeaponSystem'; import { tetriNode } from '../../tetriCard/tetriNode'; import { jiujiuwoMap } from '../../jiujiuwoMap/jiujiuwoMap'; import { JIUJIUWOYA_INIT_WEAPON_ID, tetriWeaponCarrier } from '../../tetriCard/tetriWeaponCarrier'; import { role } from '../../tetriCard/role'; import { LuosiDing } from '../jiujiuwoyaGame/LuosiDing'; import { BattleResLoader } from '../../game/BattleResLoader'; const { ccclass, property } = _decorator; export type WeaponLaunchEventRunner = (battle: BattleCore) => void; export interface WeaponLaunchEvent { /**调试用:事件来源/类型 */ type: string; /**执行一次的回调(由载体决定怎么发射) */ run: WeaponLaunchEventRunner; } @ccclass('BattleCore') export class BattleCore { /**取消放置的距离(px) */ cancelDistancePx: number = 0; /**取消放置的时间(ms) */ cancelTimeMs: number = 500; //-------------------------初始化前的参数----------------------------- /**当前章节id */ ChapterId: number = 0; /**当前战斗类型 */ BattleType: BattleType = BattleType.Level; /**当前难度 */ Difficulty: ChapterDifficulty = ChapterDifficulty.Normal; /**当前地图id */ MapId: number = 0; /**地图名称 */ MapName: string = ''; /**当前章节通关奖励字符串 */ CurentChapterRewardStr: string = ''; /**技能核心 */ SkillCore: SkillSelectCore = null; //-------------------------初始化战斗数据----------------------------- /**当前章节配置 */ CurentConfigChapter: ITableChapter = null; /**第二章节配置(部分战斗模式需要使用到另一个配置) */ CurentConfigChapter2: ITableChapter = null; /**当前副本配置 */ CurentConfigReplica: ITableReplica = null; /**当前章节所有机制配置 */ CurentConfigMechanism: ITablePlaymode[] = []; /**当前章节所有障碍物配置 */ CurentConfigObstacle: ITableObstacle[] = []; /**最大章节id */ MaxChapterId: number = 0; /**当前章节怪物分组 */ MonsterGroup: number = 0; /**当前章节怪物血量系数 */ MonsterHpMultiple: number = 1; /**当前章节战力修正系数 */ MonsterPowerPara: number = 1; /**当前章节怪物总血量 */ MonsterTotalHp: number = 0; /**当前章节所有怪物数量 */ TotalMonsterNum: number = 0; /**总波数 */ TotalWaveNum: number = 0; /**波次刷新时间 */ WaveRefreshTime: number = 30; /***每波次敌人的数量累积 */ PerWaveSpawnNumCumulative: number[] = []; /**城墙最大血量 */ WallMaxHp: number = 0; /**怪物掉落银币加成系数 */ MonsterDropCoinAddRatio: number = 1; /**当前章节基础银币数 */ CurentChapterBaseCoin: number = 0; /**看广告增加银币数量 */ VideoAddCoin: number = 0; /**广告出现频率20秒*/ AdFrequency: number = 0; /**看广告增加银币次数 */ VideoAddCoinTimes: number = 0; /**技能卡片3选1免费刷新次数 */ rogueFreeRefreshTimes = 1; /**技能卡片3选1广告刷新次数 */ rogueADRefreshTimes = 1; /**技能卡片3选1免费全部都要次数 */ rogueFreeAllGetTimes = 1; /**技能卡片3选1广告全部都要次数 */ rogueADAllGetTimes = 1; /**当前失败复活剩余次数 */ curFailReviveLeftNum = 1; /**当前广告回血剩余次数 */ curAdAddHpLeftNum = 0; /**当前选技能卡片(白银宝箱)的次数 */ curRogueSkillTimes = 0; /**当前选武器卡片(黄金宝箱)的次数 */ curRogueWeaponTimes = 1; /**提示功能看视频次数 */ GuidePosADTimesArr: number[] = [1]; /**当前最高关卡的武器修正系数 */ WeaponPara = 1; /**弹武器宝箱次数组 */ OpenSkillSelectTypeArr: number[] = []; /**开启宝箱消耗银币数组 */ OpenSkillSelectCoinArr: number[] = []; /**锤子银币消耗 */ GameShovelCoin: number = 0; /**波次结算银币 */ waveGetCoin: number = 0; //-------------------------初始化战斗组件----------------------------- /**当前地图管理器 */ Map: MapManager = null; /**当前俄罗斯方块地图 */ TetraMap: tetriMap = null; /**救救我鸭地图*/ JiujiuwoMap: jiujiuwoMap = null; /**救救我鸭:场上直挂角色的武器载体(供 SkillController 解析发射点) */ jiujiuwoyaWeaponCarriers: tetriWeaponCarrier[] = []; /** 救救我鸭:技能三选一关闭面板后再播升级特效(避免被 UI 遮住) */ private _pendingJiujiuwoyaSkillUpgradeWeaponId = 0; /**tetriWeaponSystem 俄罗斯方块武器系统*/ TetriWeaponSystem: tetriWeaponSystem = null; /**怪物生成器 */ MonsterSpawner: MonsterSpawner = null; /**临时怪物父节点 */ TempMonsterParent: Node = null; /**玩家控制器 */ PlayerController: PlayerController = null; /**武器技能控制器 */ SkillController: SkillController = null; /**伤害统计 */ DamageStatistics: DamageStatistics = null; /**城墙目标节点 */ WallTargetNode: Node = null; /**tetriMap 终点缓存(wallTarget) */ private _tetraEndTargetNode: Node | null = null; /**伤害分数层 */ ScoreLayer: Node = null; /**画线层 */ GraphicsLineLayer: Node = null; /**顶部 spine 层 */ TopSpineLayer: Node = null; ui_mubanLayer: Node = null; ui_floorNode: Node = null; /** 已弹出螺丝的容器(滚动收集区) */ ui_luosiding: Node = null; //-------------------------初始化战斗状态----------------------------- /**是否是副本 */ IsReplica: boolean = false; /**是否有首次出现的机制 */ HaveNeverNewPlay: boolean = false; /**boss是否被击杀 */ finishKillBoss: boolean = false; /**是否使用锤子*/ IsUseShovel: boolean = false; //-------------------------战斗中的状态----------------------------- /**地图寻路对象是否可以移动 */ CanMove: boolean = true; /**是否是游戏暂停 */ IsGamePause: boolean = true; /**是否是机制编辑 */ IsEdit: boolean = false; /**是否游戏结束 */ IsGameOver: boolean = false; /**是否正在等待胜利结算(防止重复触发) */ private _isWinSettling: boolean = false; /**胜利结算延迟计时器 */ private _winSettleTimer: any = null; /**是否生成怪物 */ isSpwanMonster: boolean = false; /**是否暂停更新时间 */ isPauseUpdateTime: boolean = false; /**是否正在雷击 */ IsLaunchThunder: boolean = false; /**是否正在空降 */ IsLaunchParatroops: boolean = false; /**是否开启红月加速 */ IsOpenRedMoon: boolean = false; /**是否开启魔眼机制 */ IsOpenMagicEye: boolean = false; /**是否开启防护水晶 */ IsOpenProtectiveCrystal: boolean = false; /**该局游戏是否观看过广告 */ IsWatchAd: boolean = false //-------------------------战斗中的数据----------------------------- /**当前游戏时间 */ BattleTime: number = 0; /**当前波次时间 */ CurentWaveTime: number = 0; /**时间倍数缩放 */ TimeScale: number = 1; /**当前章节怪物出现数量 */ MonsterAppearNum: number = 0; /**怪物死亡数量 */ MonsterDeadNum: number = 0; //无尽模式的怪物死亡数量 InfiniteModeMonsterDeadNum: number = 0; /**当前银币数 */ CurentCoinNum = 0; /**当前波次 */ CurentWaveNum = 1; // /**无尽模式当前波次 */ InfiniteModeCurentWaveNum: number = 1; /**当前城墙血量 */ CurentWallHp: number = 0; /**当前使用的武器 */ CurUseWeaponArr: ITableWeapon2[] = []; /**当前编辑机制配置 */ CurEditMechanismConfig: ITablePlaymode = null; /**当前编辑障碍物配置 */ CurEditObstacleConfig: ITableObstacle = null; /**红月加速倍速 */ RedMoonMultiplier: number = 1; /**当前温度 */ CurentTemperature: number = 0; /**当前看视频增加银币次数 */ CurentVideoAddCoinTimes: number = 0; /**剩余显示推荐点个数 */ RemainGuidePointsNum: number = 0; /**提示功能看视频次数 */ //GuidePosADTimes: number = 0; /**编辑前的所有障碍物 */ OldObstacleArr: MapGrid[] = []; /**编辑后的所有障碍物 */ NewObstacleArr: MapGrid[] = []; /**本次编辑移除的障碍物 */ RemovedObstacleArr: MapGrid[] = []; /**本次编辑放置的障碍物 */ PlacedObstacleArr: MapGrid[] = []; /**当前武器DPS */ CurentDPS: number = 0; /**当前使用的技能 */ CurUseSkillArr: ITableSkill[] = []; /**武器攻击怪物标记最大时间 */ WeaponAtkMonsterMaxTime: number = 20; WeaponAtkMonsterCurrentTime: number = 20; /**丧尸牢笼抓住的丧尸id */ CaptureZombieId: number = 0; /**丧尸牢笼是否已经抓住了尸尸 */ isCaptureZombie = false /**默认障碍物 */ DefaultBarrier: string = ''; /** 抽卡模式 */ /** 抽卡模式障碍物数量 */ GachaModeObstacleNum: number = 0; /**抽卡模式铲子数量 */ GachaModeShovelNum: number = 0; /**抽卡模式下的抽卡次数 */ GachaModeChoujiangNum: number = 0; /**抽卡副本抽奖中 */ IsGachaModeChoujiang: boolean = false; /** 固定技能筛选品质 */ FixedQuality: number = 0; /**抽卡模式每个波次怪物数量 */ gachaWaveMonsterNums: number[] = []; /**看广告加成的属性 */ /** 宝箱副本增加攻击力 */ fubenBoxAddAtk: number = 0; /**宝箱副本技能冷却*/ fubenBoxSkillCoolDown: number = 0; /**宝箱副本增加暴击 */ fubenBoxAddCrit: number = 0; /**宝箱副本增伤 */ fubenBoxAddDamage: number = 0; /**boss减速 */ fubenBoxReduceBossSpeed: number = 0; /***引导使用榴莲 */ IsGuideUseLian: boolean = false; /**开始初始化战斗 */ /**是否是最大波次 */ isMaxWave: boolean = false; //摆放提示广告数 GuidePosADTimes: number = 0; /**画线路径重建最小间隔(毫秒) */ private _wallLineMinIntervalMs: number = 80; /**上次画线路径重建时间 */ private _lastWallLineBuildMs: number = 0; /**是否有待处理的画线重建请求 */ private _pendingWallLineBuild: boolean = false; /**画线分帧任务是否进行中 */ private _wallLineBuildActive: boolean = false; /**画线分帧任务每帧处理的出怪口数量 */ private _wallLineBuildBatchPerFrame: number = 2; /**画线分帧任务待处理出怪口 */ private _wallLineBuildSpawnPoints: Node[] = []; /**画线分帧任务当前索引 */ private _wallLineBuildIndex: number = 0; /**画线分帧任务累计路径 */ private _wallLineBuildPaths: Vec2[][] = []; /**编辑可达性检查短窗口缓存 */ private _lastReachabilityCheckMs: number = 0; private _lastReachabilityCheckKey: string = ""; private _lastReachabilityCheckResult: boolean = true; /**10米宝箱打开情况 */ is10mBoxOpen: boolean = false; /**30米宝箱打开情况 */ is30mBoxOpen: boolean = false; /**60米宝箱打开情况 */ is60mBoxOpen: boolean = false; /**100米宝箱打开情况 */ is100mBoxOpen: boolean = false; /**一次广告给锤子数目 */ onAddShovelNum = 1; /**锤子数目 初始给一个锤子*/ gameShovelNum = 1; /**神奇方块的刷新次数 次数刷完点击无效 */ onRefreshMagicalNum = 3; /** 方块武器map 方块名称:方块携带的武器id(只有一个武器) */ TetriWeaponMap: Map = new Map(); haveWeaponType = [] /**方块武器发射事件(用于按帧分发,削减多方块下的循环) */ WeaponLaunchEventQueue: WeaponLaunchEvent[] = []; /**每帧最多处理多少个发射事件 */ WeaponLaunchMaxPerFrame: number = 2; /**每隔多少帧才触发一次出队(1=每帧都出队) */ WeaponLaunchDispatchIntervalFrames: number = 2; private _weaponLaunchDispatchFrameAcc: number = 0; /** 俄罗斯方块堆当前高度(米,与左侧标尺一致:按 blockHeight 折算并四舍五入),由 UItetriGame 按刷新间隔写入 */ TetriStackHeightMeters: number = 0; /** 同上未取整的原始值(业务如需阈值判断可用) */ TetriStackHeightMetersRaw: number = 0; /**存放宝箱的数量*/ tetriSaveBoxNum = 0; /**3选1弹出次数 宝箱打开次数 */ IsOpenSkillSelectTimes: number = 0; /**转盘“再抽一次”引导:单场战斗仅触发一次 */ IsTurnTableAgainGuideShown: boolean = false; /**预警时间 */ tetriWarnTime = 0; /**预警倒计时剩余时间(运行时递减) */ tetriWarnTimeFix = 0; /**是否处于“波次预警倒计时” */ public IsWarnTimeActive: boolean = false; /** 波次预警开始时刻(用于超时兜底) */ private _warnActiveSinceMs = 0; /** 玩法说明叠层 pause 引用计数(连点开/关时对齐 pause/resume) */ private _tetriDocPauseDepth = 0; /**引导怪物数量 */ guideGuideMonster = 0 getFreeCoinNum = 0 IsGameOverBoxJump: boolean = true; /*** DC优化层 ***/ ui_skillTraillayer: Node = null; /**技能拖尾节点 */ ui_roleLayer: Node = null; ui_roleWeaponLayer: Node = null; private _trailPruneAcc = 0; /**橙武副本选择的方块类型 */ ModechooseTetriType: number[] = []; /***雷电副本爱心数量 */ loveHpNum = 0; /** 雷电副本:本局已因方块触碰「移除用地面」消耗的爱心段数(达到 loveHpNum 后城墙破灭不再弹复活) */ lightningLoveConsumed = 0; /** 救救我鸭:卡槽失败上限(逻辑消对后 remain.length >= 本值则失败;默认 4,看广告解锁 +1) */ luosiKacaoNum = 4 /** 救救我鸭:消除等级(ReplicaLevel 表下标,满级后不再涨经验) */ luosiXiaochuLevel = 0 /** 救救我鸭:当前等级已累计经验 */ luosiXiaochuExp = 0 /** 救救我鸭:由 ReplicaLevel.type 指定下次 SkillSelect 强制武器(1)/技能(2),0 不强制 */ luosiReplicaBoxForceType = 0 /** 波次预警 pause 不被 UI 弹窗覆盖 reason(关箱后 heal 每帧误判 orphan) */ private static readonly _UI_OVERLAY_PAUSE = new Set([ 'SkillSelect', 'TurnTable', 'BattlePause', 'BattleRevive', 'BattleFail', 'newMechanism', ]); /** 消除一组螺丝(一对)增加的经验 */ static readonly LUOSI_XIAOCHU_EXP_PER_PAIR = 5 /** 救救我鸭:ui_luosiding 按入槽顺序剩余的类型(成对消除后) */ luosiSlotRemainTypes: number[] = []; jiujiuwoyaSoltFullFail = false; /** 螺丝落入 ui_luosiding 时回调(由 UIjiujiuwoyaGame 注册,仅用于失败检测) */ onJiujiuwoyaLuosiEnteredSlot: ((luosi: LuosiDing) => void) | null = null; /** 槽内物理碰撞消对完成、节点销毁前(同步类型队列) */ onJiujiuwoyaLuosiPairRemovedFromSlot: ((a: LuosiDing, b: LuosiDing) => void) | null = null; /** 开 ReplicaLevel 宝箱前播放 UI 动效(由 UIjiujiuwoyaGame 注册,结束后再 openPanels) */ onJiujiuwoyaReplicaTreasureBeforeOpen: ((type: number, openPanels: () => void) => void) | null = null; /**木板生成完成 */ mubanSpawnFinished = false; /** 救救我鸭:螺丝已全部消除,待当前波清场后跳转最后一波 */ luosiReplicaPendingSkipToLastWave = false; /** 救救我鸭:本局已执行过「跳最后一波」 */ luosiReplicaSkipToLastWaveDone = false; /** 普通块 / floor 碰到会触发移除的地面(地图「地面」节点或 TetrisOutTable 落出区)时调用 */ tryApplyLightningLoveOnRemovalGround(): void { if (this.IsGameOver || this.BattleType !== BattleType.OrangeWeaponMode_Dian || this.loveHpNum <= 0) return; this.lightningLoveConsumed++; GEvent.Ins.emit(GEvent.LightningLoveDrop); } /** 救救我鸭:副本等级表(按 id 升序) */ getLuosiReplicaLevelRows(): ITableReplicaLevel[] { const list = gg.data.table.getTableList(TableNames.ReplicaLevel) ?? []; return list.slice().sort((a, b) => a.id - b.id); } /** 救救我鸭:当前卡槽失败阈值(与 luosiKacaoNum 同步,看广告解锁后会变大) */ getLuosiSlotFailLimit(): number { return Math.max(1, this.luosiKacaoNum); } /** 是否已达 ReplicaLevel 最高档(之后不涨经验、不弹宝箱) */ isLuosiReplicaLevelMax(): boolean { return this.luosiXiaochuLevel >= this.getLuosiReplicaLevelRows().length; } /** 当前消除升级所需经验;满级时返回最后一档 exp 供 UI 显示满条 */ getLuosiReplicaLevelNeedExp(): number { const rows = this.getLuosiReplicaLevelRows(); if (!rows.length) return 1; if (this.isLuosiReplicaLevelMax()) { return rows[rows.length - 1].exp; } const row = rows[this.luosiXiaochuLevel]; return row?.exp ?? rows[rows.length - 1].exp; } /** 消除一组螺丝后增加经验,满级则忽略;达标则按表 type 开宝箱并升档 */ addLuosiXiaochuExp(amount: number = BattleCore.LUOSI_XIAOCHU_EXP_PER_PAIR): void { if (this.BattleType !== BattleType.OrangeWeaponMode_Ya) return; if (this.IsGameOver || !this.mubanSpawnFinished) return; if (this.isLuosiReplicaLevelMax()) return; this.luosiXiaochuExp += amount; const row = this.getLuosiReplicaLevelRows()[this.luosiXiaochuLevel]; if (!row || this.luosiXiaochuExp < row.exp) return; this.luosiXiaochuExp -= row.exp; this.openLuosiReplicaTreasure(row.type); this.luosiXiaochuLevel++; if (this.isLuosiReplicaLevelMax()) { this.luosiXiaochuExp = 0; this.markLuosiReplicaSkipToLastWaveIfNeeded(); } } /** 螺丝经验满级(全部消除)且未到最后一波时,标记本波结束后跳最后一波 */ markLuosiReplicaSkipToLastWaveIfNeeded(): void { if (this.BattleType !== BattleType.OrangeWeaponMode_Ya) return; if (this.luosiReplicaSkipToLastWaveDone || this.luosiReplicaPendingSkipToLastWave) return; if (!this.isLuosiReplicaLevelMax()) return; if (this.CurentWaveNum >= this.TotalWaveNum) return; this.luosiReplicaPendingSkipToLastWave = true; console.log( '[jiujiuwoya] 螺丝已全部消除,第', this.CurentWaveNum, '波清场后将跳转最后一波', ); } /** 当前波清场后:若已标记则跳到最后一波并同步刷怪时间轴 */ tryApplyLuosiReplicaSkipToLastWave(): void { if (this.BattleType !== BattleType.OrangeWeaponMode_Ya) return; if (!this.luosiReplicaPendingSkipToLastWave || this.luosiReplicaSkipToLastWaveDone) return; this.luosiReplicaPendingSkipToLastWave = false; if (this.CurentWaveNum >= this.TotalWaveNum) return; const fromWaveNum = this.CurentWaveNum; this.luosiReplicaSkipToLastWaveDone = true; this.CurentWaveNum = this.TotalWaveNum; this.isMaxWave = true; this.syncLuosiReplicaMonsterCountAfterSkip(fromWaveNum); this.MonsterSpawner?.seekToWave(this.TotalWaveNum - 1); console.log( '[jiujiuwoya] 跳转最后一波', this.CurentWaveNum, '/', this.TotalWaveNum, 'dead=', this.MonsterDeadNum, 'total=', this.TotalMonsterNum, ); } /** * 跳波后同步怪物统计:扣除被跳过波次(fromWaveNum ~ TotalWaveNum-1)的怪物数, * 使 TotalMonsterNum / PerWaveSpawnNumCumulative 与实刷、实杀数量一致,结算可正常按击杀数通关。 */ private syncLuosiReplicaMonsterCountAfterSkip(fromWaveNum: number): void { const cum = this.PerWaveSpawnNumCumulative; if (!cum?.length || this.TotalWaveNum <= 1 || fromWaveNum >= this.TotalWaveNum) return; const completedIdx = fromWaveNum - 2; const lastSkippedIdx = this.TotalWaveNum - 2; if (completedIdx < 0 || lastSkippedIdx < completedIdx) return; const throughCompleted = cum[completedIdx] ?? 0; const throughLastSkipped = cum[lastSkippedIdx] ?? throughCompleted; const skippedCount = throughLastSkipped - throughCompleted; if (skippedCount <= 0) return; this.TotalMonsterNum = Math.max(this.MonsterDeadNum, this.TotalMonsterNum - skippedCount); for (let i = completedIdx + 1; i < cum.length; i++) { cum[i] = Math.max(throughCompleted, (cum[i] ?? 0) - skippedCount); } GEvent.Ins.emit(GEvent.UpdateDeadMonster); console.log( '[jiujiuwoya] 同步怪物数 跳过波次', fromWaveNum, '-', this.TotalWaveNum - 1, 'skipped=', skippedCount, 'dead=', this.MonsterDeadNum, 'total=', this.TotalMonsterNum, '末波累计=', cum[this.TotalWaveNum - 1], ); } /** ReplicaLevel.type:1 武器、2 技能、3 转盘 */ openLuosiReplicaTreasure(type: number): void { if (this.IsGameOver) return; const openPanels = () => this.doOpenLuosiReplicaTreasurePanels(type); if (this.onJiujiuwoyaReplicaTreasureBeforeOpen) { this.onJiujiuwoyaReplicaTreasureBeforeOpen(type, openPanels); } else { openPanels(); } } private doOpenLuosiReplicaTreasurePanels(type: number): void { switch (type) { case 1: this.luosiReplicaBoxForceType = 1; gg.ui.popPanel(UIID.SkillSelect); break; case 2: this.luosiReplicaBoxForceType = 2; gg.ui.popPanel(UIID.SkillSelect); break; case 3: gg.ui.popPanel(UIID.TurnTable, { data: 0 }); break; default: console.warn('[jiujiuwoya] 未知 ReplicaLevel.type', type); break; } } failGame(){ if (this.IsGameOver) { console.log('[jiujiuwoya] failGame 跳过: 已 IsGameOver'); return; } // 雷电副本:城墙已空时往往已由扣血分支走失败/弹窗,避免爱心动画结束再走失败流程叠界面 if (this.BattleType === BattleType.OrangeWeaponMode_Dian && this.CurentWallHp <= 0) { return; } if (this.BattleType === BattleType.OrangeWeaponMode_Ya) { let useRevive = this.curFailReviveLeftNum > 0; if(this.jiujiuwoyaSoltFullFail){ useRevive = true } console.log( '[jiujiuwoya] failGame 救救我鸭' + ` 逻辑remain=${JSON.stringify(this.luosiSlotRemainTypes)}` + ` luosiKacaoNum=${this.luosiKacaoNum} 失败阈值=${this.getLuosiSlotFailLimit()} 复活=${useRevive}`, ); this.showBattleSettlementPanel(useRevive); return; } if (this.lightningLoveConsumed == this.loveHpNum) { this.showBattleSettlementPanel(this.curFailReviveLeftNum > 0); } } /** 打开失败/复活结算(强制弹层,并关闭会挡住的交互弹窗) */ private showBattleSettlementPanel(useRevive: boolean) { const panelId = useRevive ? UIID.BattleRevive : UIID.BattleFail; if (useRevive) { this.pause('BattleRevive'); this.IsGameOver = true; } else { this.gameOver(false); } this._closeSettlementBlockers(); gg.ui.openBattleSettlementPanel(panelId); } /** 关闭会挡住战斗结算弹窗的交互层 */ private _closeSettlementBlockers(): void { for (const blockerId of [UIID.SkillSelect, UIID.TurnTable]) { if (gg.ui.panelIsShow(blockerId)) { void gg.ui.closePanel(blockerId); } } } /** 通关胜利:2s 后直连打开 BattleSuccess(不走 pop 队列,避免被转盘/技能箱挡住) */ private async _finishWinSettle(): Promise { this._winSettleTimer = null; this.gameOver(true); this._closeSettlementBlockers(); await gg.ui.openBattleSettlementPanel(UIID.BattleSuccess); gg.data.saveToServer(null, true); } /** 复活等:重置雷电爱心计数 */ resetLightningLoveState(): void { this.lightningLoveConsumed = 0; } enqueueWeaponLaunchEvent(evt: WeaponLaunchEvent) { if (!evt) return; this.WeaponLaunchEventQueue.push(evt); } _dequeueWeaponLaunchEvents(maxCount: number, everyNFrames: number = 1) { const interval = Math.max(1, everyNFrames | 0); if (interval > 1) { this._weaponLaunchDispatchFrameAcc++; if (this._weaponLaunchDispatchFrameAcc < interval) { return; } this._weaponLaunchDispatchFrameAcc = 0; } const n = Math.max(0, Math.min(maxCount | 0, this.WeaponLaunchEventQueue.length)); for (let i = 0; i < n; i++) { const evt = this.WeaponLaunchEventQueue.shift(); if (!evt) continue; try { evt.run?.(this); } catch (e) { console.warn('[WeaponLaunchEventQueue] run error', e); } } } init() { gg.ui.GameCamera.node.position = Vec3.ZERO; gg.ui.GameCamera.orthoHeight = gg.ui.height / 2; this.initData(); } /**初始化战斗数据 */ initData() { //确定当前战斗的章节ID和战斗类型 this.ChapterId = gg.game.CurentSelectChapterId; this.BattleType = gg.game.CurentSelectBattleType; this.Difficulty = gg.game.CurentSelectBattleDifficulty; this.CurentConfigReplica = gg.game.CurentSelectReplica; this.IsReplica = this.BattleType != BattleType.Level; //确定当前战斗使用的实际配表配置和地图信息 this.MaxChapterId = gg.data.project.getMaxChapterId(); this.CurentConfigChapter = gg.data.project.getChapterConfig(this.ChapterId); this.MapId = this.CurentConfigChapter.mapId; this.MapName = this.CurentConfigChapter.name; // 波次预警时间(配置)& 当前倒计时剩余 this.tetriWarnTime = 15//gg.data.table.getConst(ConstNumType.MonsterWarnTime); if (gg.data.doc.chapter == 1) { this.tetriWarnTime = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1209)?.constValue; } else { this.tetriWarnTime = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1210)?.constValue; } if(this.BattleType == BattleType.OrangeWeaponMode_Ya){ this.tetriWarnTime = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1214)?.constValue; } this.tetriWarnTimeFix = this.tetriWarnTime; this.OpenSkillSelectCoinArr = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1206)?.constString.split(',').map(x => parseInt(x)); this.GameShovelCoin = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1207)?.constValue; this.waveGetCoin = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1208)?.constValue; this.GuidePosADTimes = 0; this.isMaxWave = false; this.IsTurnTableAgainGuideShown = false; this.OpenSkillSelectTypeArr = gg.data.table.getConstArray(ConstNumType.BoxWaveDrop); if(this.BattleType == BattleType.OrangeWeaponMode_Dan){ this.OpenSkillSelectTypeArr = gg.data.table.getConstArray(ConstNumType.OrangeWeaponMode_DanBoxWaveDrop); } if(this.BattleType == BattleType.OrangeWeaponMode_Dian){ this.gameShovelNum = 0 } //确定当前战斗使用的实际配表配置和地图信息 this.MaxChapterId = gg.data.project.getMaxChapterId(); this.CurentConfigChapter = gg.data.project.getChapterConfig(this.ChapterId); this.MapId = this.CurentConfigChapter.mapId; this.MapName = this.CurentConfigChapter.name; if (this.BattleType == BattleType.BossMode) { this.waveGetCoin = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1213)?.constValue; void BattleResLoader.preloadBossSkillAssets(); } //如果是副本模式,并且需要随机章节地图的话,随机一个章节地图id if (this.CurentConfigReplica && this.CurentConfigReplica.mapID.length > 0) { let idRange = this.CurentConfigReplica.mapID; let minID = idRange[0]; let maxID = idRange[idRange.length - 1]; let id2 = Math.floor(Math.random() * (maxID - minID + 1)) + minID; this.CurentConfigChapter2 = gg.data.project.getChapterConfig(id2); if (this.CurentConfigChapter2) { this.MapId = this.CurentConfigChapter2.mapId; this.MapName = this.CurentConfigChapter2.name; } } if(this.CurentConfigReplica && this.BattleType == BattleType.OrangeWeaponMode_Dian){ this.loveHpNum = Number(this.CurentConfigReplica.parameter); } if (this.BattleType === BattleType.OrangeWeaponMode_Ya) { this.luosiKacaoNum = 4; this.luosiReplicaPendingSkipToLastWave = false; this.luosiReplicaSkipToLastWaveDone = false; const slotParam = Number(this.CurentConfigReplica?.parameter); if (Number.isFinite(slotParam) && slotParam > 0) { this.luosiKacaoNum = Math.floor(slotParam); } } this.lightningLoveConsumed = 0; //根据战斗类型和难度确定广告刷新次数 let adstrArr = this.CurentConfigChapter.advertise_limit.split("|"); let index = 0; let str = ""; let advertise_limit = []; if (this.BattleType == BattleType.Level && this.Difficulty == ChapterDifficulty.Hard) index = 1; if (this.BattleType == BattleType.Level && this.Difficulty == ChapterDifficulty.Hell) index = 2; if (index < adstrArr.length) str = adstrArr[index]; if (str != "") advertise_limit = str.split(',').map(x => parseInt(x)) this.rogueFreeRefreshTimes = 1// + gg.purchase.getFreeRefreshTimes(); this.rogueADRefreshTimes = advertise_limit[0] this.rogueFreeAllGetTimes = gg.data.doc.chapter == 1 ? 1 : 0 this.rogueADAllGetTimes = advertise_limit[1] this.VideoAddCoinTimes = advertise_limit[2] this.VideoAddCoinTimes = advertise_limit[2] this.curFailReviveLeftNum = advertise_limit[3] this.WeaponAtkMonsterMaxTime = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1204).constValue; this.GuidePosADTimesArr = gg.data.table.getConstArray(ConstNumType.GuidePosADTimes); // 获取 BattleCube 表:type==1 为基础武器方块,用 cubename 与 weaponid 建立映射 // console.log('TetriWeaponMap++++++++===', this.TetriWeaponMap) let CurentMaxChapterConfig = gg.data.table.getTableData(TableNames.Chapter, gg.data.doc.chapter); this.WeaponPara = CurentMaxChapterConfig.weapon_para; // gg.data.project.initChapterNewPlayInfo(); this.initTotalMonsterNum(); BattleDataManager.refreshWeaponFixedPropertyCache() // BattleDataManager.initBossTimeArr() //确定当前章节所有机制配置 // this.CurentConfigMechanism = gg.data.project.getChapterAllMechanismConfig(this.CurentConfigChapter2?.id || this.ChapterId); // let ids = gg.data.project.newPlayIdArr; // this.CurentConfigMechanism = this.CurentConfigMechanism.filter(x => ids.includes(x.id + "")); // 判断是否有“首次遇到”的机制: // 仅当该机制的首个出现章节 == 当前战斗章节,且玩家当前进度仍在该章节(通常代表首次挑战)时才弹。 this.HaveNeverNewPlay = false; const chapterList = gg.data.table.getTableList(TableNames.Chapter) .filter(x => x.type == BattleType.Level) .sort((a, b) => a.id - b.id); const curChapterId = this.CurentConfigChapter2?.id || this.ChapterId; const progressChapterId = gg.data.doc.chapter; for (let i = 0; i < this.CurentConfigMechanism.length; i++) { const data = this.CurentConfigMechanism[i]; let firstAppearChapterId = 0; for (let j = 0; j < chapterList.length; j++) { const chapterData = chapterList[j]; if (chapterData.new_play.indexOf(data.id + "") >= 0) { firstAppearChapterId = chapterData.id; break; } } if (firstAppearChapterId > 0 && firstAppearChapterId == curChapterId && progressChapterId <= curChapterId) { this.HaveNeverNewPlay = true; break; } } if (gg.game.playerSkillIdArray.length > 0) { let arr = gg.game.playerSkillIdArray; for (let i = 0; i < arr.length; i++) { let skillID = arr[i]; if (!skillID || skillID == "") continue; let skillData = gg.data.table.getTableData(TableNames.Skill, Number(skillID)); this.CurUseSkillArr.push(skillData); } } let difficultyIndex = this.CurentConfigChapter.difficult //确定当前章节怪物分组 let groupConfig = this.CurentConfigChapter2 || this.CurentConfigChapter; this.MonsterGroup = this.CurentConfigChapter.monster_group; this.MonsterHpMultiple = groupConfig.hp_multiple; this.MonsterPowerPara = this.CurentConfigChapter.power_para; if (this.BattleType != BattleType.Level && this.CurentConfigReplica) { this.MonsterHpMultiple = gg.data.getCurPower() / 100; if (this.CurentConfigReplica.hp > 0) { this.MonsterHpMultiple = this.MonsterHpMultiple * this.CurentConfigReplica.hp; } // if ([1, 2].includes(this.CurentConfigReplica.id)) { // this.MonsterHpMultiple = groupConfig.hp_multiple; // } } this.DefaultBarrier = groupConfig.barrier; this.CurentChapterRewardStr = gg.data.project.getChapterRewardStr(this.CurentConfigChapter.id, this.Difficulty); this.CurentChapterBaseCoin = this.CurentConfigChapter2?.base_coin || this.CurentConfigChapter.base_coin; if (gg.game.playerCoinNum > 0) { this.CurentChapterBaseCoin = gg.game.playerCoinNum; } //this.CurentChapterBaseCoin = 10000000 this.VideoAddCoin = gg.data.table.getConst(ConstNumType.VideoAddCoin); this.AdFrequency = gg.data.table.getConst(ConstNumType.AdFrequency); this.CurentVideoAddCoinTimes = this.VideoAddCoinTimes; this.CurentCoinNum = this.CurentChapterBaseCoin; //确定波次 let monsterStr = this.CurentConfigChapter2 || this.CurentConfigChapter; let monster1 = monsterStr.monster1.split('|') let monster2_1 = monsterStr.monster2_1.split('|') //选择长度最长的 let num = Math.max(monster1.length, monster2_1.length) // let num = Math.ceil(monster1.length / 3) // if (gg.game.CurentSelectBattleType == BattleType.GachaMode) { // num = Math.ceil(monster1.length / 3 / 2) // } this.TotalWaveNum = num; console.log('TotalWaveNum++++++++===', this.TotalWaveNum) // if (this.BattleType == BattleType.GachaMode) { // this.WaveRefreshTime = 60 // } else { // this.WaveRefreshTime = 30 // } if (gg.game.playerHp > 0) { this.WallMaxHp = gg.game.playerHp } else { this.WallMaxHp = gg.data.table.getConst(ConstNumType.BaseHp); } this.CurentWallHp = this.WallMaxHp; this.SkillCore = new SkillSelectCore(); this.CaptureZombieId = 0; this.isCaptureZombie = false //初始化战斗状态 this.initStatus(); //武器统计 gg.targetTaskManager.statisticalData(TargetTaskType.fightSpecWeapon, 1); if (this.BattleType != BattleType.Level) { //所有副本统计 gg.targetTaskManager.statisticalData(TargetTaskType.anyCopy, 1); } if (this.BattleType == BattleType.InfiniteMode) { //武器副本统计 gg.targetTaskManager.statisticalData(TargetTaskType.weaponCopy, 1); } else if (this.BattleType == BattleType.SpeedMode) { }else if(this.BattleType == BattleType.OrangeWeaponMode_Dan){ } this.TetriWeaponMap.clear(); const battleCubeList = gg.data.table.getTableList(TableNames.BattleCube); for (let i = 0; i < battleCubeList.length; i++) { const row = battleCubeList[i]; if (row.type !== TetriType.Tetris_Baseattk) continue; if (!row.cubename) continue; if (this.TetriWeaponMap.has(row.cubename)) continue; this.TetriWeaponMap.set(row.cubename, row.weaponid); //this.TetriWeaponMap.set(row.cubename,20); } if (this.BattleType != BattleType.Level) { //所有副本统计 gg.targetTaskManager.statisticalData(TargetTaskType.anyCopy, 1); } if (this.BattleType == BattleType.SpeedMode) { //武器副本统计 gg.targetTaskManager.statisticalData(TargetTaskType.weaponCopy, 1); } else if (this.BattleType == BattleType.InfiniteMode) { //金币副本统计 gg.targetTaskManager.statisticalData(TargetTaskType.goldCopy, 1); } } addTestSkill() { let skillId = [951] //1931,1932,1941,1942 let lists = gg.data.table.getTableList(TableNames.Skill); let skillArr = lists.filter(x => skillId.includes(x.id)); this.addCurHaveSkillArr(skillArr); } /**初始化当前章节怪物总数 */ private initTotalMonsterNum() { this.PerWaveSpawnNumCumulative = [] let config = this.CurentConfigChapter; if (this.CurentConfigChapter2) config = this.CurentConfigChapter2; const monsterNumScale = gg.game.monsterNumScale > 0 ? gg.game.monsterNumScale : 1 let getMonster1_2Num = (monster) => { if (monster == '' || monster == null) { return 0 } let num = 0 for (let i = 0; i < monster.length; i++) { if (monster[i] == '') { continue } let item = monster[i].split(';') for (let j = 0; j < item.length; j++) { let t = item[j].split(',') if (t.length != 3) { console.warn('怪物配置错误==', item); continue } // 与 MonsterSpawner.parseSpawnPointConfig 一致:数量列乘 GM 倍率后再参与统计 num += Number(t[1]) * monsterNumScale } if (this.PerWaveSpawnNumCumulative[i]) { this.PerWaveSpawnNumCumulative[i] = this.PerWaveSpawnNumCumulative[i] + Math.round(num) } else { this.PerWaveSpawnNumCumulative[i] = Math.round(num) } } // console.log('this.PerWaveSpawnNumCumulative=', this.PerWaveSpawnNumCumulative) return Math.round(num) } let allNum = 0 let monster1 = config.monster1.split('|') let monster2_1 = config.monster2_1.split('|') // let monster2_2 = config.monster2_2.split('|') // let monster2_3 = config.monster2_3.split('|') // let monster2_4 = config.monster2_4.split('|') // let monster2_5 = config.monster2_5.split('|') allNum += getMonster1_2Num(monster1) allNum += getMonster1_2Num(monster2_1) this.TotalMonsterNum = allNum; console.log('[TotalMonsterNum]', 'chapter=', this.ChapterId, 'diff=', this.Difficulty, 'total=', allNum, 'PerWaveSpawnNumCumulative:', this.PerWaveSpawnNumCumulative); } /**初始化战斗状态 */ initStatus() { BattleDiagnostics.resetBattle(); this.IsGamePause = true; BattleDiagnostics.onPause('battleInit'); } update(dt) { BattleDiagnostics.tick(this); this._trailPruneAcc += dt; if (this._trailPruneAcc >= 1) { this._trailPruneAcc = 0; this.pruneDetachedBattleLayers(); } this._tryAdvanceWarnTime(dt); this._healOrphanTetriDocPause(); this._healOrphanWaveWarnPause(); this._healOrphanSkillSelectPause(); this._healOrphanTurnTablePause(); // 波次预警倒计时:默认暂停战斗(pause),武器与出怪时间轴停住。 // Boss 模式:预警期间不 pause,且仍派发武器事件,便于继续攻击场上怪物;出怪时间仍在预警结束后再走(不推进 CurentWaveTime)。 const allowCombatWhileWaveWarn = this.BattleType == BattleType.BossMode && this.IsWarnTimeActive; if ( this.canUpdateFrame() && this.isSpwanMonster && (!this.IsWarnTimeActive || allowCombatWhileWaveWarn) ) { let dTime = dt * this.TimeScale; this.BattleTime += dTime; // console.log('canUpdateFrame', this.BattleTime ) // 按帧分发方块武器发射事件,避免多发射点带来的嵌套循环 this._dequeueWeaponLaunchEvents(this.WeaponLaunchMaxPerFrame * this.TimeScale, this.WeaponLaunchDispatchIntervalFrames / this.TimeScale); if (this.canUpdateFrameTime() && !this.IsWarnTimeActive) { //是不是可以更新波次时间了。和创建怪物时间有关 this.CurentWaveTime += dTime; } } } /**开始预警倒计时*/ startWarnTime() { if (this.IsGameOver || this.IsEdit) return; // Boss 模式:预警 UI 仍显示,但不进入全局暂停,武器可继续攻击怪物。 if (this.BattleType != BattleType.BossMode) { this.pause('waveWarn'); } this.IsWarnTimeActive = true; this._warnActiveSinceMs = Date.now(); // 使用 initData 初始化的配置时间;这里只重置剩余时间 this.tetriWarnTimeFix = this.tetriWarnTime; if (this.CurentWaveNum == 1) this.tetriWarnTimeFix = 0; BattleDiagnostics.onWarnStart(this.CurentWaveNum, this.tetriWarnTimeFix); //每波次 清空武器发射事件队列 this.WeaponLaunchEventQueue = []; gg.sdk.reportDY("inLevel", `章节${gg.game.CurentBattle.getReportDYChapterId()}_${gg.game.CurentBattle.CurentWaveNum}-进入`); // 通知技能/特效在倒计时阶段自清理(例如激光持续特效) GEvent.Ins.emit(GEvent.BattleWarnTimeStart); const waveIdx = this.CurentWaveNum - 1; this.MonsterSpawner?.preloadWaveMonsters(waveIdx); // 第一波 / 提前结束:无弹窗时可同帧进入战斗 if (this.tetriWarnTimeFix <= 0 && !this.havePauseLayer()) { this._finishWarnTimeAndStartGame(); } } /** * 提前结束预警(UI「提前」按钮):无 blocking 弹窗时立即开波。 */ tryFinishWarnTimeEarly(): void { if (!this.IsWarnTimeActive || this.IsGameOver) return; this.tetriWarnTimeFix = 0; this._tryFinishWarnIfUnblocked(); } /** * blocking 弹窗(技能箱/转盘等)关闭后:预警期间不能 resume,但需补开波检查。 */ onBlockingPanelClosed(): void { if (this.IsWarnTimeActive) { const reason = BattleDiagnostics.getPauseReason(); if (reason === 'SkillSelect' || reason === 'TurnTable') { BattleDiagnostics.onPause('waveWarn'); } this._tryFinishWarnIfUnblocked(); return; } this.resume(); } private _tryFinishWarnIfUnblocked(): void { if (!this.IsWarnTimeActive || this.IsGameOver || this.havePauseLayer()) return; if (this.tetriWarnTimeFix <= 0) { this._finishWarnTimeAndStartGame(); } } /** 推进波次预警倒计时;弹窗打开时冻结,关闭后继续;无弹窗且超时才强制开波 */ private _tryAdvanceWarnTime(dt: number): void { if (!this.IsWarnTimeActive || this.IsGameOver) return; if (this.havePauseLayer()) { // 技能箱/转盘等:冻结倒计时与强制开波超时(只计无弹窗停留时间) this._warnActiveSinceMs = Date.now(); return; } if (this._warnActiveSinceMs > 0) { const warnMs = Date.now() - this._warnActiveSinceMs; const maxWarnMs = Math.max((this.tetriWarnTime + 5) * 1000, 25000); if (warnMs >= maxWarnMs) { console.warn('[BattleCore] wave warn force finish', this.CurentWaveNum, warnMs); BattleDiagnostics.onWarnForceFinish(this.CurentWaveNum, warnMs); this._finishWarnTimeAndStartGame(); return; } } const dTime = dt * this.TimeScale; if (this.tetriWarnTimeFix > 0) { this.tetriWarnTimeFix -= dTime; } if (this.tetriWarnTimeFix <= 0) { this._finishWarnTimeAndStartGame(); } } /** 预警结束 → 开始刷怪/发射 */ private _finishWarnTimeAndStartGame(): void { if (!this.IsWarnTimeActive || this.IsGameOver) return; if (this.havePauseLayer()) return; this.tetriWarnTimeFix = 0; this.IsWarnTimeActive = false; this._warnActiveSinceMs = 0; const curWaveNum = this.CurentWaveNum; BattleDiagnostics.onWarnFinished(curWaveNum); // 先恢复战斗态,再埋点,避免 report 异常导致 IsGamePause 残留 this.startGame(); gg.sdk.reportDY("inLevel", `章节${this.getReportDYChapterId()}_${curWaveNum}-开始刷怪`); const length = this.curMap?.getTetriNodeCount?.() ?? 0; gg.sdk.reportDY("inLevel", `章节${this.getReportDYChapterId()}_${curWaveNum}-摆放方块数量` + length); gg.sdk.reportDY("inLevel", `章节${this.getReportDYChapterId()}_${curWaveNum}-使用宝箱数量` + this.IsOpenSkillSelectTimes); } /**开始战斗 */ startGame() { this.BattleTime = 0; this._tetriDocPauseDepth = 0; this.IsGamePause = false; this.CanMove = true; BattleDiagnostics.onResumeSuccess(); this.isSpwanMonster = true this.SkillController?.refreshDroneOrLaserLaunch?.(); GEvent.Ins.emit(GEvent.tetriGameStart); gg.game.CurentBattle?.curMap?.initState(); //增加波次银币 } havePauseLayer() { if (gg.ui.panelIsShow(UIID.newMechanism)) return true; if (gg.ui.panelIsShow(UIID.BattlePause)) return true; if (gg.ui.panelIsShow(UIID.BattleFail)) return true; if (gg.ui.panelIsShow(UIID.SkillSelect)) return true; if (gg.ui.panelIsOpening(UIID.SkillSelect)) return true; if (gg.ui.panelIsShow(UIID.TurnTable)) return true; if (gg.ui.panelIsOpening(UIID.TurnTable)) return true; if (gg.ui.panelIsShow(UIID.BattleRevive)) return true; if (gg.ui.panelIsShow(UIID.TetriDoc)) return true; if (gg.ui.panelIsOpening(UIID.TetriDoc)) return true; return false; } /**暂停战斗 */ pause(reason = 'unknown') { // 波次预警已 pause,玩法说明叠层不应覆盖 pause reason if (reason === 'TetriDoc' && this.IsWarnTimeActive) { return; } // 预警期间 UI 弹窗叠层:保持 pause,但不把 reason 从 waveWarn 改成 SkillSelect 等 if (this.IsWarnTimeActive && reason !== 'waveWarn' && BattleCore._UI_OVERLAY_PAUSE.has(reason)) { this.IsGamePause = true; this.CanMove = false; return; } this.IsGamePause = true; this.CanMove = false; BattleDiagnostics.onPause(reason); } /** 玩法说明打开:引用计数 pause,避免连点导致 resume 丢失 */ pauseForTetriDoc(): void { if (this.IsWarnTimeActive) return; this._tetriDocPauseDepth++; if (this._tetriDocPauseDepth === 1) { this.pause('TetriDoc'); } } /** 玩法说明关闭:引用计数 resume */ resumeForTetriDoc(): void { if (this._tetriDocPauseDepth <= 0) return; this._tetriDocPauseDepth--; if (this._tetriDocPauseDepth > 0) return; if (this.IsWarnTimeActive) return; if (gg.ui.panelIsShow(UIID.TetriDoc) || gg.ui.panelIsOpening(UIID.TetriDoc)) { this._tetriDocPauseDepth = 1; return; } this.resume(); } /** 无玩法说明面板但仍处于 TetriDoc pause 时自动恢复(连点竞态兜底) */ private _healOrphanTetriDocPause(): void { if (this.IsGameOver || this.IsWarnTimeActive) return; if (gg.ui.panelIsShow(UIID.TetriDoc) || gg.ui.panelIsOpening(UIID.TetriDoc)) return; if (this._tetriDocPauseDepth > 0) { this._tetriDocPauseDepth = 0; if (this.IsGamePause) { BattleDiagnostics.onTetriDocPauseHeal(); this.resume(); } return; } if (this.IsGamePause && BattleDiagnostics.getPauseReason() === 'TetriDoc') { BattleDiagnostics.onTetriDocPauseHeal(); this.resume(); } } /** 预警已结束但 waveWarn pause 未恢复(开波链路中断) */ private _healOrphanWaveWarnPause(): void { if (this.IsGameOver || this.IsWarnTimeActive) return; if (this.havePauseLayer()) return; if (!this.IsGamePause || BattleDiagnostics.getPauseReason() !== 'waveWarn') return; BattleDiagnostics.onWaveWarnPauseHeal(); this.startGame(); } /** 技能箱已关但仍处于 SkillSelect pause(打开失败 / onInit 异常 / 关箱未 resume) */ private _healOrphanSkillSelectPause(): void { if (this.IsGameOver) return; if (gg.ui.panelIsShow(UIID.SkillSelect) || gg.ui.panelIsOpening(UIID.SkillSelect)) return; if (!this.IsGamePause || BattleDiagnostics.getPauseReason() !== 'SkillSelect') return; if (this.IsWarnTimeActive) { BattleDiagnostics.onPause('waveWarn'); return; } const blocker = BattleDiagnostics.getResumeBlocker(); if (blocker) { if (blocker !== 'SkillSelect') { BattleDiagnostics.onPause(blocker); } return; } BattleDiagnostics.onSkillSelectPauseHeal(); this.resume(); } /** 转盘已关但仍处于 TurnTable pause(关盘未 resume / 预警叠层 reason 残留) */ private _healOrphanTurnTablePause(): void { if (this.IsGameOver) return; if (gg.ui.panelIsShow(UIID.TurnTable) || gg.ui.panelIsOpening(UIID.TurnTable)) return; if (!this.IsGamePause || BattleDiagnostics.getPauseReason() !== 'TurnTable') return; if (this.IsWarnTimeActive) { BattleDiagnostics.onPause('waveWarn'); return; } const blocker = BattleDiagnostics.getResumeBlocker(); if (blocker) { if (blocker !== 'TurnTable') { BattleDiagnostics.onPause(blocker); } return; } BattleDiagnostics.onTurnTablePauseHeal(); this.resume(); } /**恢复战斗 */ resume() { const blocker = BattleDiagnostics.getResumeBlocker(); if (blocker) { BattleDiagnostics.onResumeBlocked(blocker); return; } this.IsGamePause = false; this.CanMove = true; BattleDiagnostics.onResumeSuccess(); } /** * 拆层渲染节点的统一清理入口(方块移除 / 周期兜底)。 * @param blockRoot 指定方块时清 followTarget 绑定该方块(及子树)的 ui_roleLayer 角色 */ pruneDetachedBattleLayers(blockRoot: Node | null = null): void { if (blockRoot?.isValid) { role.destroyRoleLayerFollowersOf(blockRoot); } else { role.pruneOrphanRoleLayer(); } this.pruneSkillTrailLayer(); } /** * 方块移除时清理 ui_roleLayer 上仍绑定该方块的角色(落稳后角色已不在方块子树内)。 */ destroyRoleLayerFollowersOf(blockRoot: Node | null): void { role.destroyRoleLayerFollowersOf(blockRoot); } /** * 清理 ui_skillTraillayer 中无效或高度≥30m 后无需保留的拖尾节点。 */ pruneSkillTrailLayer(): void { const layer = this.ui_skillTraillayer; if (!layer?.isValid) return; const suppress = this.TetriStackHeightMeters >= 30; const trails = layer.getComponentsInChildren(SuperTrail); for (let i = 0; i < trails.length; i++) { const st = trails[i]; if (!st?.node?.isValid) continue; const ft = st.followTarget; let bulletSkill: Skill | null = null; if (ft?.isValid) { let cur: Node | null = ft; while (cur) { bulletSkill = cur.getComponent(Skill); if (bulletSkill) break; cur = cur.parent; } } const orphan = ft == null || !ft.isValid; const bulletDead = bulletSkill?.getIsDead?.() === true; if (suppress || orphan || bulletDead) { st.restoreOriginalPlacement(true); st.node.active = false; } } } /** 离开战斗前关闭 2D 物理并注销地图监听,避免销毁刚体时 Box2D 仍在 step */ shutdownBattlePhysicsForLeave(): void { this.TetraMap?.detachPhysics?.(); this.SkillController?.recycleSkillLayerBullets?.(); PhysicsSystem2D.instance.enable = false; } /** 离开战斗场景后的兜底:确保物理可被下局战斗重新开启 */ forceRestoreBattlePhysics(): void { PhysicsSystem2D.instance.enable = true; } /**是否可以更新帧逻辑 */ canUpdateFrame() { return !this.IsEdit && !this.IsGamePause && !this.IsGameOver; } canUpdateFrameMonster() { return !this.IsEdit && !this.IsGamePause && !this.IsGameOver && this.isSpwanMonster; } canUpdateFrameTime() { return !this.IsEdit && !this.IsGamePause && !this.IsGameOver && !this.isPauseUpdateTime; } /**进入机制编辑 */ enterEdit(obstacleConfig: ITableObstacle) { if (!this.PlayerController || !this.PlayerController || !this.SkillController) { return; } this.pause('mechanismEdit'); this.OldObstacleArr = this.Map.getAllCanRemoveObstacleGrids(); this.CurEditObstacleConfig = obstacleConfig; this.CurEditMechanismConfig = this.CurentConfigMechanism.find(x => x.id + "" == obstacleConfig.playMode + ""); this.SkillController.node.active = false; this.PlayerController.node.active = false; this.MonsterSpawner.node.opacity = 50; this.TopSpineLayer.opacity = 200; //this.showWallLinePath(); //this.Map.showRecommendPoints(); GEvent.Ins.emit(GEvent.EnterEdit); } /**退出机制编辑 */ exitEdit(save: boolean) { this.IsEdit = false; this.CurEditMechanismConfig = null; this.CurEditObstacleConfig = null; this.SkillController.node.active = true; this.PlayerController.node.active = true; this.MonsterSpawner.node.opacity = 255; this.TopSpineLayer.opacity = 255; this.GraphicsLineLayer.active = false; this.Map.hideRecommendPoints(); GEvent.Ins.emit(GEvent.EndEdit, save); // EndEdit 结束后再基于“最终状态”统计差异,避免取消编辑时按临时状态弹问号 this.NewObstacleArr = this.Map.getAllCanRemoveObstacleGrids(); this.RemovedObstacleArr = this.OldObstacleArr.filter(x => !this.NewObstacleArr.includes(x)); this.PlacedObstacleArr = this.NewObstacleArr.filter(x => !this.OldObstacleArr.includes(x)); if (save) { this.showMonsterWenHao(); } else { this.RemovedObstacleArr = []; this.PlacedObstacleArr = []; } // EndEdit 回滚后统一刷新地图可走性版本,确保寻路/引导不使用旧缓存 this.Map.notifyMapWalkStateChanged(); GEvent.Ins.emit(GEvent.ExitEdit, save); if (!save) { GEvent.Ins.emit(GEvent.UpdateCoin); } this.resume(); GEvent.Ins.emit(GEvent.updateAgentPath); //this.MonsterSpawner.node.children.map(x => x.getComponent(MapAgent)?.updatePath()); } /** * 生成普通怪物怪物 * @param monsterId 怪物id * @param spawnPoint 出怪口 */ async spawnMonster(monsterId: number, spawnPoint: number, monsterGroup: number) { let monsterData = gg.data.project.getMonsterDataByID(monsterId) if (!monsterData) { console.warn('怪物数据不存在monsterId', monsterId) this.onSpawnFailed(monsterId, spawnPoint, 'monsterDataMissing'); return null; } let posParent = gg.game.CurentBattle.curMap.spawnPointParent.find(`spawnPoint_${spawnPoint}`); if (!posParent) { console.warn('出怪口不存在spawnPoint', spawnPoint) this.onSpawnFailed(monsterId, spawnPoint, 'spawnPointMissing'); return null; } let areaWidth = posParent.getComponent(UITransform).width let areaHeight = posParent.getComponent(UITransform).height const spawnX = -areaWidth / 2 + Math.random() * areaWidth const spawnY = -areaHeight / 2 + Math.random() * areaHeight let pos = v3(posParent.worldPosition.x + spawnX, posParent.worldPosition.y + spawnY, 0); pos = this.fixSpawnPosIfBlocked(pos); let monsterNode = await this.createMonster(monsterData, pos, this.MonsterSpawner.node); if (!monsterNode) { this.onSpawnFailed(monsterId, spawnPoint, 'monsterPrefabMissing'); return null; } let hp = this.getMonsterHP(monsterData, monsterGroup); monsterNode.getComponent(Monster).setData(monsterData, hp); if (gg.game.IsPlayingGuideStory) { monsterNode.getComponent(Monster).playGuide(); } if (spawnPoint == 8) { monsterNode.getComponent(Monster).setSkinScale(-1); } // tetriMap:把地图引用给 MapAgent,由 MapAgent 自己挑 point 作为终点 const agent = monsterNode.getComponent(MapAgent); if (agent) { agent.curMap = this.curMap; agent.setSpawnPointId(spawnPoint); } let resultIndex = 0 for (let i = 1; i < this.MonsterSpawner.node.children.length; i++) { if (pos.y > this.MonsterSpawner.node.children[i].position.y && pos.y < this.MonsterSpawner.node.children[i - 1].y) { resultIndex = i break } } monsterNode.setSiblingIndex(resultIndex) if (gg.game.IsPlayingGuideStory) { } else { this.MonsterAppearNum++ } this.MonsterTotalHp += hp; //判断当前波次的怪物是不是已经全部出完,出完的话暂停更新怪物波次时间 let curWaveMonsterNum = this.PerWaveSpawnNumCumulative[this.CurentWaveNum - 1] if (this.MonsterAppearNum >= curWaveMonsterNum) { console.log('当前波次已经出怪完成暂停出怪时间') this.isPauseUpdateTime = true; } else { this.isPauseUpdateTime = false; } return monsterNode; } /**从 tetriMap 节点树中解析“终点/受击点” */ private getTetraEndTargetNode(): Node | null { if (this._tetraEndTargetNode && this._tetraEndTargetNode.isValid) return this._tetraEndTargetNode; const root = this.TetraMap?.node; if (!root || !root.isValid) return null; // 兼容两种层级: // 1) 目标点/wallTarget1/... // 2) wallTarget1/... const n1 = find('目标点/wallTarget1', root); if (n1 && n1.isValid) return (this._tetraEndTargetNode = n1); const n2 = find('wallTarget1', root); if (n2 && n2.isValid) return (this._tetraEndTargetNode = n2); const group = find('目标点', root); const fallback = group?.children?.[0] ?? null; if (fallback && fallback.isValid) return (this._tetraEndTargetNode = fallback); return null; } /**当出生点落在不可行走区域时,纠偏到附近可走且可达攻击点的格子 */ private fixSpawnPosIfBlocked(pos: Vec3): Vec3 { const map = this.Map; if (!map || !isValid(map)) return pos; const best = map.findNearestWalkableReachableGridForSpawn(pos); if (best) { const wp = best.node.worldPosition; return v3(wp.x, wp.y, pos.z); } return pos; } /** 生成失败时回退总数,避免卡在“差 1 只”无法通关 */ private onSpawnFailed(monsterId: number, spawnPoint: number, reason: string) { if (this.TotalMonsterNum > this.MonsterDeadNum) { this.TotalMonsterNum--; console.warn( '[SpawnFailedAdjust]', 'chapter=', this.ChapterId, 'diff=', this.Difficulty, 'reason=', reason, 'monsterId=', monsterId, 'spawnPoint=', spawnPoint, 'dead=', this.MonsterDeadNum, 'total=', this.TotalMonsterNum ); // 刷新击杀/总数展示,并立即复核是否已满足通关条件 GEvent.Ins.emit(GEvent.UpdateDeadMonster); this.checkWin(); } } setLayerMask(n: Node, layerMask: number) { n.layer = layerMask; for (let i = 0; i < n.children.length; i++) { if (n.children[i].isValid) { this.setLayerMask(n.children[i], layerMask); } } } /**创建怪物 */ async createMonster(monsterData: ITableMonster, pos: Vec3, parent: Node, mubanTemp = 'monsterTemplate') { let monsterNode = await gg.res.getNodeAsync('prefab/', mubanTemp, GBundle.BattleGameMonster); if (!monsterNode) { console.warn('[createMonster] template missing', mubanTemp); return null; } monsterNode.setParent(parent); // 宝箱模板不要用缩放入场:UI 上易被其它 tween/stop 打断,scale 会卡在 0 if (mubanTemp === 'monsterBoxTemplate' || monsterData.id == 40041) { const s = monsterData?.scale || 1; monsterNode.scale = v3(s, s, s); monsterNode.opacity = 255; } else { monsterNode.worldPosition = pos; monsterNode.scale = Vec3.ZERO; monsterNode.opacity = 0; Tween.stopAllByTarget(monsterNode); tween(monsterNode).to(0.5, { scale: v3(monsterData.scale, monsterData.scale, monsterData.scale), opacity: 255 }).start(); } let skinNode = monsterNode.find("skin"); let n = await gg.res.getNodeAsync('prefab/monster/', monsterData.model, GBundle.BattleGameMonster); if (n) { n.setParent(skinNode); let anim = n.find("anim").getComponent(sp.Skeleton); monsterNode.getComponent(Monster).anim = anim; Tween.stopAllByTarget(anim); anim.color = new Color(255, 255, 255, 255); if (monsterData.skin != "") { anim.setSkin(monsterData.skin); } // 宝箱副本的宝箱怪使用模板专用碰撞框(再乘实例缩放), // 避免通用 SkeletonBounds 在部分宝箱骨骼上产生偏移。 if (mubanTemp !== 'monsterBoxTemplate') { this.syncMonsterColliderToSkin(monsterNode, n, monsterData); } // setTimeout(() => { // }, 10); this.setLayerMask(monsterNode, parent.layer); } return monsterNode; } /** 采样碰撞体前优先切到待机类动作,避免 prefab 默认「走路」等姿势把 AABB 撑得过大 */ private _trySetColliderSampleAnim(anim: sp.Skeleton, monsterType?: number) { if (!anim?.skeletonData) return; const isBossLike = monsterType === MonsterType.Boss || monsterType === MonsterType.BigBoss; const names = isBossLike ? ['1待机', '待机', 'idle', '走路'] : ['待机', '走路', 'idle']; for (let i = 0; i < names.length; i++) { if (anim.findAnimation(names[i])) { anim.setAnimation(0, names[i], true); return; } } } /** * 让怪物碰撞体与 skin 的可视大小保持一致,避免模板固定碰撞框与不同模型不匹配。 * Boss 使用轴对齐矩形,Spine 全骨骼 AABB 会包含伸臂/跨步等空白区,需略收紧以免子弹提前命中。 */ private syncMonsterColliderToSkin(monsterNode: Node, skinModelNode: Node, monsterData?: ITableMonster) { const collider = monsterNode.getComponent(BoxCollider2D); if (!collider || !skinModelNode) return; const targetNode = skinModelNode.getChildByName("anim") || skinModelNode; const anim = targetNode.getComponent(sp.Skeleton); let widthLocal = 0; let heightLocal = 0; let centerLocalX = targetNode.position.x; let centerLocalY = targetNode.position.y; let hasSpineBounds = false; // 优先 Spine runtime 包围盒(真实显示包围) if (anim && (anim as any)._skeleton && (sp as any).spine?.SkeletonBounds) { try { this._trySetColliderSampleAnim(anim, monsterData?.type); anim.updateAnimation(0); const runtimeSkeleton = (anim as any)._skeleton; runtimeSkeleton.updateWorldTransform(); const bounds = new (sp as any).spine.SkeletonBounds(); bounds.update(runtimeSkeleton, true); const minX = bounds.minX; const minY = bounds.minY; const maxX = bounds.maxX; const maxY = bounds.maxY; if ([minX, minY, maxX, maxY].every(v => typeof v === "number")) { // SkeletonBounds 是 anim 的局部空间,先转成 monsterNode 本地空间 // 这里只需要考虑 targetNode 自身局部变换;monsterNode 的整体缩放对 collider 本地值不应重复乘。 const sx = targetNode.scale.x; const sy = targetNode.scale.y; widthLocal = Math.max(1, Math.abs((maxX - minX) * sx)); heightLocal = Math.max(1, Math.abs((maxY - minY) * sy)); centerLocalX = targetNode.position.x + ((minX + maxX) * 0.5) * sx; // Y 轴按“脚底在 targetNode.position.y”处理,避免中心点掉到脚下 centerLocalY = targetNode.position.y + heightLocal * 0.5; hasSpineBounds = true; } } catch (e) { console.warn("[BattleCore] syncMonsterColliderToSkin spine bounds failed:", e); } } if (widthLocal <= 0 || heightLocal <= 0) { const uiTransform = targetNode.getComponent(UITransform) || skinModelNode.getComponent(UITransform); if (!uiTransform) return; const contentSize = uiTransform.contentSize; const localScale = targetNode.scale; widthLocal = Math.max(1, contentSize.width * Math.abs(localScale.x)); heightLocal = Math.max(1, contentSize.height * Math.abs(localScale.y)); centerLocalX = targetNode.position.x; centerLocalY = targetNode.position.y + heightLocal * 0.5; } if (monsterData?.type === MonsterType.BigBoss) { // 收紧命中区:须保持脚底对齐,否则仅缩 size 不挪 center 会把框整体上移,直线弹从左侧打来会整段 miss const shrinkW = 0.45; const shrinkH = 0.55; const bottomY = centerLocalY - heightLocal * 0.5; widthLocal *= shrinkW; heightLocal *= shrinkH; centerLocalY = bottomY + heightLocal * 0.5; } // 兜底:若 Spine bounds 可用但中心仍异常偏低,强制脚底基准 if (hasSpineBounds && centerLocalY < targetNode.position.y + heightLocal * 0.25) { centerLocalY = targetNode.position.y + heightLocal * 0.5; } collider.size.set(widthLocal, heightLocal); collider.offset.set(centerLocalX, centerLocalY); collider.apply(); } /**获取怪物血量(包含所有加成计算后) */ getMonsterHP(monsterData: ITableMonster, tableId: number = this.MonsterGroup) { let power = gg.data.project.getRecommendPower(this.ChapterId, this.Difficulty) let baseHp = MonsterDataManager.getMonsterHpByDiffAndTime(tableId, this.MonsterGroup, power, this.MonsterPowerPara) let hp = Math.floor(monsterData.HP * this.MonsterHpMultiple * baseHp * this.WeaponPara); if (this.BattleType == BattleType.InfiniteMode && this.InfiniteModeCurentWaveNum > 20) { // baseHp = MonsterDataManager.getMonsterHpByDiffAndTime(20, this.MonsterGroup, power, this.MonsterPowerPara) hp = Math.floor(monsterData.HP * this.MonsterHpMultiple * baseHp * this.WeaponPara); let num = gg.data.table.getConst(ConstNumType.InfiniteModeHpBaseMultiple) hp = hp * Math.pow(num, this.InfiniteModeCurentWaveNum - 20); } // let dy_xishu = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1211)?.constValue; let wx_xishu = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1212)?.constValue; if (window["tt"]) { hp = hp * dy_xishu; } else if (window["wx"]) { hp = hp * wx_xishu; } const originHp = hp; const challengeCount = this.getEarlyChapterChallengeCount(); const hpCoef = this.getEarlyChapterMonsterHpCoef(); hp = Math.floor(hp * hpCoef); if (this.BattleType == BattleType.Level && this.ChapterId >= 1 && this.ChapterId <= 4) { console.log(`[前4关血量] 怪物${monsterData.id} 原始血量:${originHp} 变化后血量:${hp} 第${challengeCount}次挑战 系数:${hpCoef}`); } // console.log(`怪物ID:${monsterData.id}`); // console.log(`怪物表血量:${monsterData.HP}`); // console.log(`章节推荐战力:${power}`); // console.log(`实际战力:${gg.data.getCurPower()}`); // console.log(`当前章节怪物血量系数:${this.MonsterHpMultiple}`); // console.log(`血量基础值:${baseHp}`); // console.log(`当前最高关卡的武器修正系数:${this.WeaponPara}`); // console.log(`最终血量:${hp}`); return hp; } /**前4关重复挑战次数,默认1 */ getEarlyChapterChallengeCount(): number { const n = gg.data.getStatistics(StatisticsType.EarlyChapterChallengeCount); return n < 1 ? 1 : n; } /**前4关重复挑战怪物血量系数:常量表1215 constString,第N次挑战取第N个值,超出取最后一项 */ getEarlyChapterMonsterHpCoef(): number { if (this.BattleType != BattleType.Level) return 1; if (this.ChapterId < 1 || this.ChapterId > 4) return 1; const str = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1215)?.constString; if (!str) return 1; const arr = str.split(',').map(Number).filter(x => !isNaN(x)); if (arr.length == 0) return 1; const idx = Math.min(this.getEarlyChapterChallengeCount(), arr.length) - 1; const coef = arr[idx]; return coef > 0 ? coef : 1; } /**设置重开/退出、失败结算:1-4关且波次>=6时增加挑战次数 */ tryAddEarlyChapterChallengeCount(): void { if (this.BattleType != BattleType.Level) return; if (this.ChapterId < 1 || this.ChapterId > 4) return; if (this.CurentWaveNum < 6) return; const n = this.getEarlyChapterChallengeCount(); gg.data.setStatistics(StatisticsType.EarlyChapterChallengeCount, n + 1); console.log(`[前4关血量] 挑战次数+1: ${n} -> ${n + 1} 波次:${this.CurentWaveNum} 关卡:${this.ChapterId}`); } /**当前最大关卡胜利后重置前4关挑战次数为1 */ resetEarlyChapterChallengeCount(): void { gg.data.setStatistics(StatisticsType.EarlyChapterChallengeCount, 1); console.log(`[前4关血量] 当前最大关胜利,挑战次数重置为1 关卡:${this.ChapterId}`); } addStoryGuideMonster() { this.guideGuideMonster++; if (this.guideGuideMonster == 2) { gg.game.IsPlayingGuideStory = false; GEvent.Ins.emit(GEvent.PlayingGuideStoryEnd); } } /** * Boss 模式:刷怪已全部结束,且「计入清场统计」的怪为 0,但场上仍有存活怪(多为未计入的 Boss), * 且尚未标记击杀 Boss → 判负(与波次溢出 gameOver 互补;基地血量归零仍走 subWallHp 原逻辑)。 */ private _tryBossModeFailWhenBossAliveAfterSpawnsDone(): boolean { if (this.BattleType !== BattleType.BossMode || this.IsGameOver) { return false; } if (this.finishKillBoss) { return false; } const sp = this.MonsterSpawner; if (!sp?.isAllSpawnFinished()) { return false; } if (sp.getAliveCountForWin() > 0) { return false; } const n = sp.node; if (!n?.isValid) { return false; } let anyAlive = false; for (const c of n.children) { const m = c.getComponent(Monster); if (m && !m.IsDead) { anyAlive = true; break; } } if (!anyAlive) { return false; } console.log('boss模式失败 _tryBossModeFailWhenBossAliveAfterSpawnsDone'); this.showBattleSettlementPanel(false); return true; } /**怪物死亡 */ monsterDie(monster: Monster) { // 增加当前获得的银币 // let addNum = Math.floor(monster.Data.coin * this.MonsterDropCoinAddRatio); this.CurentCoinNum += addNum; this.MonsterDeadNum++; if (this.BattleType == BattleType.InfiniteMode) { this.InfiniteModeMonsterDeadNum++; } GEvent.Ins.emit(GEvent.UpdateDeadMonster); // } //检测当前波次的怪物是不是都已经死亡,如果全部死亡就更新波次 let curWaveMonsterNum = this.PerWaveSpawnNumCumulative[this.CurentWaveNum - 1] if (this.BattleType == BattleType.BossMode) { curWaveMonsterNum = curWaveMonsterNum - 1; } let totalMonsterNum = this.TotalMonsterNum; let canWinByClearAll = this.BattleType != BattleType.GachaMode && this.MonsterSpawner?.isAllSpawnFinished() && this.MonsterSpawner?.getAliveCountForWin() <= 0; // Boss 模式:清场胜利必须已击杀 Boss(部分 Boss 配表 id/类型不计入 getAliveCountForWin,会误判清场) if (this.BattleType == BattleType.BossMode) { canWinByClearAll = canWinByClearAll && this.finishKillBoss; } if ((this.MonsterDeadNum >= totalMonsterNum) || canWinByClearAll) { //游戏结束 this.IsGameOverBoxJump = false; } //console.log('this.MonsterDeadNum=', this.MonsterDeadNum, 'curWaveMonsterNum=', curWaveMonsterNum) this.checkWin(); if (this.IsGameOver) { return; } if (this._tryBossModeFailWhenBossAliveAfterSpawnsDone()) { return; } if (this.MonsterDeadNum >= curWaveMonsterNum) { this.CurentWaveNum++; if (this.CurentWaveNum > this.TotalWaveNum) { this.CurentWaveNum = this.TotalWaveNum; if (this.BattleType == BattleType.BossMode) { console.log('boss模式失败 monsterDie'); this.showBattleSettlementPanel(false); return; } } if (this.BattleType == BattleType.InfiniteMode) { this.InfiniteModeCurentWaveNum++; if (this.InfiniteModeCurentWaveNum % 20 == 1) { this.MonsterDeadNum = 0 this.CurentWaveTime = 0 this.CurentWaveNum = 1 this.isPauseUpdateTime = false; this.MonsterAppearNum = 0; GEvent.Ins.emit(GEvent.InfiniteModeWaveReset); //重置死亡怪物数量 } } if (this.BattleType === BattleType.OrangeWeaponMode_Ya) { this.tryApplyLuosiReplicaSkipToLastWave(); } //开始重新计时波次时间 console.log('开始重新计时波次时间this.CurentWaveNum++=', this.CurentWaveNum) this.isPauseUpdateTime = false; //发送波次刷新事件 GEvent.Ins.emit(GEvent.BattleWaveRefresh); // 下一波开始:进入预警倒计时,倒计时结束再 start() if (this.CurentWaveNum <= this.TotalWaveNum) { this.startWarnTime(); } } } checkWin() { if (this.IsGameOver) return; if (this.BattleType == BattleType.InfiniteMode) { return; } //调试用,击杀调试设置的怪物数量过关 let totalMonsterNum = this.TotalMonsterNum; if (sdkConfig.isDebugFastWinMonsterNum > 0) { totalMonsterNum = sdkConfig.isDebugFastWinMonsterNum; } let canWinByClearAll = this.BattleType != BattleType.GachaMode && this.MonsterSpawner?.isAllSpawnFinished() && this.MonsterSpawner?.getAliveCountForWin() <= 0; if (this.BattleType == BattleType.BossMode) { canWinByClearAll = canWinByClearAll && this.finishKillBoss; } const bossModeFinish = this.BattleType == BattleType.BossMode && this.finishKillBoss; if ((this.MonsterDeadNum >= totalMonsterNum) || canWinByClearAll || bossModeFinish) { if (this._isWinSettling) return; this._isWinSettling = true; // 最后一波清怪时,monsterDie() 里会先 CurentWaveNum++; // 结算显示/埋点应展示实际通关波次,不展示“下一波”。 this.CurentWaveNum = Math.min(this.CurentWaveNum, this.TotalWaveNum); this.pause('waveWin'); if (this.IsGameOver) return; this.IsGameOver = true; //播放胜利动画 gg.game.CurentBattle?.curMap?.showMeinvWinEffect(); this._winSettleTimer = setTimeout(() => { void this._finishWinSettle(); }, 2000); } } addWallHp(hpNum: number) { this.CurentWallHp += hpNum; if (this.CurentWallHp > this.WallMaxHp) { this.CurentWallHp = this.WallMaxHp; } GEvent.Ins.emit(GEvent.UpdateWallHp) } /**设置城墙血量 */ setCurentWallHp(hp: number) { this.CurentWallHp = hp; } /**减少城墙血量 */ subWallHp(attack: number) { if (this.IsGameOver) return; this.CurentWallHp -= attack; GEvent.Ins.emit(GEvent.UpdateWallHp); if (this.CurentWallHp <= 0) { this.CurentWallHp = 0; GEvent.Ins.emit(GEvent.UpdateWallHp); this.showBattleSettlementPanel(this.curFailReviveLeftNum > 0); } } /**游戏结束 */ gameOver(result: boolean) { if (this._winSettleTimer) { clearTimeout(this._winSettleTimer); this._winSettleTimer = null; } this.IsGameOver = true; this.pruneDetachedBattleLayers(); this.SkillController?.recycleSkillLayerBullets?.(); if (!result) { this._isWinSettling = false; } if (result) {//胜利逻辑 if (this.BattleType == BattleType.Level) { let isFinish = gg.data.project.isChapterComplete(this.ChapterId, this.Difficulty); if (!isFinish) { //设置通关状态 gg.data.project.setChapterStatus(this.ChapterId, this.Difficulty, 2, 1); } // 普通难度推进章节:与 isFinish 解耦。 // 失败结算里 setChapterStatusByProgress(进度>=1) 会提前写 index=2,导致 isFinish=true 但 chapter 未+1。 if (this.Difficulty == ChapterDifficulty.Normal) { if (this.ChapterId == gg.data.doc.chapter && this.ChapterId < this.MaxChapterId) { this.resetEarlyChapterChallengeCount(); gg.data.doc.chapter += 1; gg.game.CurentSelectChapterId = gg.data.doc.chapter; gg.data.setStatistics(StatisticsType.FailContinuousCountReset, 0) gg.data.addStatistics(StatisticsType.SuccessContinuousCount, 1) gg.data.project.setCurSelectSaveChapterId(gg.data.doc.chapter); gg.game.preLoadChapterRes(gg.data.doc.chapter); } else if (this.ChapterId == gg.data.doc.chapter) { this.resetEarlyChapterChallengeCount(); } } } else { gg.data.project.addReplicaPassCount(this.CurentConfigReplica.id, this.Difficulty); } gg.data.project.setFailCount(this.ChapterId, this.BattleType, this.Difficulty, 0); } else {//失败逻辑 //增加失败次数 //this.addFailWeight(false); this.reportBigBossFailRemainingHpPercentIfNeeded(); } gg.data.addStatistics(StatisticsType.FirstLoginWeight, 1); GEvent.Ins.emit(GEvent.GameOver, result); } /** Boss 模式失败且场上仍有存活大 Boss 时,上报其剩余血量百分比(仅一次/局内终局) */ public reportBigBossFailRemainingHpPercentIfNeeded(): void { if (this.BattleType !== BattleType.BossMode) return; const sp = this.MonsterSpawner; const n = sp?.node; if (!n?.isValid) return; const list = n.getComponentsInChildren(Monster) ?? []; for (const m of list) { if (!m?.Data || m.IsDead) continue; if (m.Data.type !== MonsterType.BigBoss) continue; const pct = m.getRemainingHealthPercentForReport(); gg.sdk.reportDY( 'inLevel', `章节${this.getReportDYChapterId()}-Boss模式剩余血量百分比${pct}`, ); return; } } /**失败加权重*/ addFailWeight(noWave: boolean) { if (!noWave) { if (gg.game.CurentBattle.IsWatchAd) { gg.data.project.addFailCount(this.ChapterId, this.BattleType, this.Difficulty); } } else { if (gg.game.CurentBattle.IsWatchAd && this.CurentWaveNum > 5) { gg.data.project.addFailCount(this.ChapterId, this.BattleType, this.Difficulty); } } } /**获取报告动态章节id */ getReportDYChapterId() { let chapterId = this.ChapterId; //关卡模式 if (this.BattleType == BattleType.Level) { if (this.Difficulty == ChapterDifficulty.Normal) { chapterId = this.ChapterId; } else if (this.Difficulty == ChapterDifficulty.Hard) { chapterId = this.ChapterId + 1000; } else if (this.Difficulty == ChapterDifficulty.Hell) { chapterId = this.ChapterId + 2000; } } return chapterId } /**增加当前使用的武器 */ addCurUseWeaponArr(arr) { // console.log('addCurUseWeaponArr', arr) for (let i = 0; i < arr.length; i++) { let id = parseInt(arr[i]); const config = gg.data.project.getWeaponData(id);//this.weaponConfigs.get(weaponId); //判断是否已经存在,如果存在则不添加 if (this.CurUseWeaponArr.find(item => item.id == id)) { console.log('武器已经存在,不添加', id) continue; } this.CurUseWeaponArr.push(config); if (config) { let weapon = BattleDataManager.getWeaponAddProperty(id) let statusData = new WeaponState(id, config); statusData.remainingCD = weapon.cd; this.SkillController.weaponStates.set(id, statusData); } } this.SkillController.freshDamageStaWeaponList(); this.SkillController.refreshDroneOrLaserLaunch(); BattleDataManager.refreshWeaponFixedPropertyCache() GEvent.Ins.emit(GEvent.ShowLevelSpine) setTimeout(() => { GEvent.Ins.emit(GEvent.UpdateDps, 1) }, 1000); } /**增加当前拥有的技能 */ addCurHaveSkillArr(arr: ITableSkill[]) { console.log('addCurHaveSkillArr', arr) for (let i = 0; i < arr.length; i++) { let skill = arr[i]; if (skill.type == 3) { //增加银币 let num = parseInt(skill.property_para1.split(",")[1]); this.addCoinNum(num); } else { this.CurUseSkillArr.push(skill); // 选中技能后:方块场播放格子升级特效;救救我鸭关闭三选一后再播(避免被面板挡住) if (this.BattleType === BattleType.OrangeWeaponMode_Ya) { this._pendingJiujiuwoyaSkillUpgradeWeaponId = skill.weapon; } else if (this.TetraMap?._tetrisTable?.isValid) { this.playTetriUpgradeEffectByWeaponId(skill.weapon); } else { this.playOtherMapUpgradeEffectByWeaponId(skill.weapon); } } } BattleDataManager.refreshWeaponFixedPropertyCache(); // 荧光棒等单点激光:橙色技能增加 burstCount 后需同步墙上发射点/荧光棒表现,否则第二根不会「出现→待机」一直显示 this.SkillController.refreshDroneOrLaserLaunch(); GEvent.Ins.emit(GEvent.ShowLevelSpine) GEvent.Ins.emit(GEvent.UpdateDps, 2) } /** 救救我鸭:场上各角色当前武器 id */ getJiujiuwoyaOnFieldWeaponIds(): number[] { const carriers = this.jiujiuwoyaWeaponCarriers ?? []; const ids: number[] = []; for (let i = 0; i < carriers.length; i++) { const id = carriers[i]?.getCurWeaponId(); if (Number.isFinite(id) && id > 0) ids.push(id); } return ids; } /** * 救救我鸭武器箱:随机「场上角色尚未装备」的武器 */ getJiujiuwoyaWeapons(isActive: boolean = false, count: number = 3): ITableWeapon2[] { const onField = new Set(this.getJiujiuwoyaOnFieldWeaponIds()); let list = gg.data.project.getEquippedWeaponDataList(); list = list.filter((item) => !onField.has(item.weaponid)); if (this.BattleType === BattleType.OrangeWeaponMode_Dan || this.BattleType === BattleType.SpeedMode) { list = list.filter((item) => item.weaponid !== 1); } if (count > list.length) count = list.length; return MTools.getRandomElements(list, count); } /** * 救救我鸭:三选一武器项对应的角色下标(优先仍是初始武器 id=1 的角色) */ getJiujiuwoyaWeaponCarrierIndices(optionCount: number): number[] { const carriers = this.jiujiuwoyaWeaponCarriers ?? []; const initFirst: number[] = []; const others: number[] = []; for (let i = 0; i < carriers.length; i++) { if (carriers[i]?.getCurWeaponId() === JIUJIUWOYA_INIT_WEAPON_ID) { initFirst.push(i); } else { others.push(i); } } const ordered = initFirst.concat(others); const result: number[] = []; for (let i = 0; i < optionCount; i++) { result.push(ordered[i] ?? i); } return result; } /** 优先替换仍为初始武器(id=1)的角色槽位 */ resolveJiujiuwoyaCarrierIndex(preferredIndex: number): number { const carriers = this.jiujiuwoyaWeaponCarriers ?? []; if (!carriers.length) return -1; if ( preferredIndex >= 0 && preferredIndex < carriers.length && carriers[preferredIndex]?.getCurWeaponId() === JIUJIUWOYA_INIT_WEAPON_ID ) { return preferredIndex; } for (let i = 0; i < carriers.length; i++) { if (carriers[i]?.getCurWeaponId() === JIUJIUWOYA_INIT_WEAPON_ID) return i; } return preferredIndex >= 0 && preferredIndex < carriers.length ? preferredIndex : 0; } /** 救救我鸭:按角色槽位直接换武器(不匹配方块类型) */ changeJiujiuwoyaWeapon(carrierIndex: number, weaponId: number) { if (!Number.isFinite(weaponId) || weaponId <= 0) return; const carriers = this.jiujiuwoyaWeaponCarriers ?? []; const idx = this.resolveJiujiuwoyaCarrierIndex(carrierIndex); if (idx < 0 || idx >= carriers.length) return; carriers[idx]?.applyWeaponId(weaponId, true); this.addCurUseWeaponArr([weaponId]); } /** 救救我鸭:三选一面板关闭后播放待处理的技能升级特效 */ flushPendingJiujiuwoyaSkillUpgradeEffect() { const weaponId = this._pendingJiujiuwoyaSkillUpgradeWeaponId; this._pendingJiujiuwoyaSkillUpgradeWeaponId = 0; if (!weaponId) return; this.playOtherMapUpgradeEffectByWeaponId(weaponId); } /** 救救我鸭:对应武器载体播放三选一完成格子效果(技能升级) */ playOtherMapUpgradeEffectByWeaponId(weaponId: number) { if (!Number.isFinite(weaponId) || weaponId <= 0) { return; } const carriers = this.jiujiuwoyaWeaponCarriers ?? []; if (!carriers.length) return; let matched = false; for (let i = 0; i < carriers.length; i++) { const carrier = carriers[i]; if (!carrier?.node?.isValid) continue; if (carrier.getCurWeaponId() !== weaponId) continue; matched = true; carrier.showSkillSelectCompleteEffect(true, 0); } if (!matched) { for (let i = 0; i < carriers.length; i++) { const carrier = carriers[i]; if (carrier?.node?.isValid) { carrier.showSkillSelectCompleteEffect(true, 0); } } } } /**按武器 id 播放场上已落稳方块升级特效 */ private playTetriUpgradeEffectByWeaponId(weaponId: number) { if (!Number.isFinite(weaponId) || weaponId <= 0) { return; } if (!this.TetraMap?._tetrisTable?.isValid) { return; } const nodes = this._getAllWeaponStackTetriNodes(); for (let i = 0; i < nodes.length; i++) { const tNode = nodes[i]; if (!tNode?.node?.isValid) continue; if (!tNode.shouldCountTowardWeaponStack()) continue; if (tNode._curweaponId !== weaponId) continue; if (!tNode.node.activeInHierarchy) continue; tNode.showUpLevelEffect(true, 0.5); } } /**增加银币 */ addCoinNum(num: number) { if (num <= 0) return; this.CurentCoinNum += num; GEvent.Ins.emit(GEvent.showCoinFly, this.ScoreLayer.worldPosition) } subCoinNum(num: number) { this.CurentCoinNum -= num; GEvent.Ins.emit(GEvent.UpdateGameCoinNum) console.log('subCoinNum', this.CurentCoinNum) } /**验证当前地图上是否有怪物无法到达终点或者所有出怪口是否有无法到达终点 */ checkMonsterAndSpawnerReachEnd(self: MapGrid) { const now = Date.now(); const key = `${self?.Index ?? -1}`; if (now - this._lastReachabilityCheckMs <= 60 && this._lastReachabilityCheckKey === key) { return this._lastReachabilityCheckResult; } let endPosArr = this.Map.mapTargetParent.children.map(x => x.worldPosition); let startPosArr = this.MonsterSpawner.node.children.map(x => x.worldPosition); //所有出怪口 let allStartsCanReachAnyEnd = this.Map.spawnPointParent.children.map(x => x.worldPosition); startPosArr = startPosArr.concat(allStartsCanReachAnyEnd); const ok = this.Map.checkAllStartsCanReachAnyEnd(startPosArr, endPosArr, false, self); this._lastReachabilityCheckMs = now; this._lastReachabilityCheckKey = key; this._lastReachabilityCheckResult = ok; return ok; } /**显示出怪口到城墙划线路径 */ showWallLinePath() { this._pendingWallLineBuild = true; this.tryStartWallLinePathBuild(); } private tryStartWallLinePathBuild() { if (!this._pendingWallLineBuild || this._wallLineBuildActive) return; const now = Date.now(); if (now - this._lastWallLineBuildMs < this._wallLineMinIntervalMs) { return; } this._lastWallLineBuildMs = now; this._pendingWallLineBuild = false; this.GraphicsLineLayer.active = true; const wallTargets = this.Map.mapTargetParent.children; let spawnPoints: Node[] = this.Map.spawnPointParent.children; if (wallTargets.length === 0 || spawnPoints.length === 0) { this._wallLineBuildActive = false; this._wallLineBuildPaths = []; this.GraphicsLineLayer.getOrAddComponent(ArrowFlowLine).setPaths([]); return; } spawnPoints = spawnPoints.filter(x => { let arr = x.name.split("_"); let index = parseInt(arr[1]); if (index >= 7 && index <= 11) { return true; } }); spawnPoints.unshift(this.Map.mapGrids[6].node); this._wallLineBuildSpawnPoints = spawnPoints; this._wallLineBuildIndex = 0; this._wallLineBuildPaths = []; this._wallLineBuildActive = true; this.stepWallLinePathBuild(); } /***画线路径分帧绘制 */ private stepWallLinePathBuild() { if (!this._wallLineBuildActive) return; const end = Math.min(this._wallLineBuildIndex + this._wallLineBuildBatchPerFrame, this._wallLineBuildSpawnPoints.length); for (let i = this._wallLineBuildIndex; i < end; i++) { const startNode = this._wallLineBuildSpawnPoints[i] const startGrid = this.Map.getMapGridByWorldPos(startNode.worldPosition) if (!startGrid || !startGrid.isCanWalk) continue; // 直接使用“最近攻击点”的距离场结果,不再对每个终点跑 A* const targetNode = this.Map.getPathShortestTarget(startNode.worldPosition) if (!targetNode) continue; const endGrid = this.Map.getMapGridByWorldPos(targetNode.worldPosition) if (!endGrid || !endGrid.isCanWalk) continue; const paths: MapGrid[] = this.Map.findPath8DirFast(startGrid, endGrid); if (!paths || paths.length <= 0) continue; const path = paths.map(x => x.node.position.clone().toVec2()) this._wallLineBuildPaths.push(path); } this._wallLineBuildIndex = end; if (this._wallLineBuildIndex < this._wallLineBuildSpawnPoints.length) { return; } let flowLine = this.GraphicsLineLayer.getOrAddComponent(ArrowFlowLine) flowLine.setPaths(this._wallLineBuildPaths) this._wallLineBuildActive = false; this._wallLineBuildSpawnPoints = []; this._wallLineBuildPaths = []; } /**获取激光攻击目标 */ getOneLaserAtkTarget(dis = 200, index: number = 0) { const sourceNode = this.WallTargetNode?.getChildByName(`ui_laserPoint${index + 1}`) || this.WallTargetNode; const sourcePos = sourceNode?.worldPosition; let arr = this.MonsterSpawner.node.children .map(x => x.getComponent(Monster)) .filter(x => x && x.node && x.node.isValid); arr = arr.filter(x => x.node.y < dis && !x.checkTag(MonsterTag.IS_BE_ATTACKED_BY_LASER + index)); if (arr.length > 0) { if (!sourcePos) return arr[0]; arr.sort((a, b) => Vec3.distance(a.node.worldPosition, sourcePos) - Vec3.distance(b.node.worldPosition, sourcePos)); return arr[0]; } return null; } /**根据障碍物类型获取当前地图上已经存在的所有障碍物对象 */ getObstaclesByType(type: ObstacleType) { let arr = this.Map.mapGrids.map(x => x.getComponent(MapObstacle)); arr = arr.filter(x => x.type == type); return arr; } getRogueFreeRefreshTimes() { return this.rogueFreeRefreshTimes } subRogueFreeRefreshTimes() { this.rogueFreeRefreshTimes-- } getRogueADRefreshTimes() { return this.rogueADRefreshTimes } subRogueADRefreshTimes() { this.rogueADRefreshTimes-- } getRogueFreeAllGetTimes() { return this.rogueFreeAllGetTimes } addCurRogueSkillTimes() { this.curRogueSkillTimes++ } getRogueADAllGetTimes() { return this.rogueADAllGetTimes } getCurRogueSkillTimes() { return this.curRogueSkillTimes } getCurRogueWeaponTimes() { return this.curRogueWeaponTimes } addCurRogueWeaponTimes() { this.curRogueWeaponTimes++ } subRogueFreeAllGetTimes() { this.rogueFreeAllGetTimes-- } subRogueADAllGetTimes() { this.rogueADAllGetTimes-- } /** * 获取3张武器卡片(黄金宝箱) * @param isActive false自动/true主动刷新 */ getWeapons(isActive: boolean = false, count: number = 3) { let result: ITableWeapon2[] = [] let list = gg.data.project.getEquippedWeaponDataList(); list = list.filter(item => !this.CurUseWeaponArr.find(x => x.id == item.weaponid)); if (this.CurUseWeaponArr.length < 2) { if (gg.data.doc.chapter == 1 && this.Difficulty == ChapterDifficulty.Normal) { let str = ConstManager.getConstStringById(ConstEnum.ChapterWeaponFixedID); let idArr = str.split(',').map(Number); // 转换为数字数组 list = list.filter(item => idArr.includes(item.weaponid)); } else { //list = list.filter(item => item.sort === 2); } } if (count > list.length) { count = list.length; } // if(this.BattleType == BattleType.OrangeWeaponMode_Dan || this.BattleType == BattleType.SpeedMode){ list = list.filter(item => item.weaponid !== 1); } result = MTools.getRandomElements(list, count); return result; } /**获取当前武器的DPS(每秒伤害) */ private getDPS() { let dps = 0; let statCache = this.DamageStatistics.getWeaponPartialDamageMap(); let conf = gg.data.table.getSkillCostData(SkillCostType.skillCostType_1203); for (let i = 0; i < this.CurUseWeaponArr.length; i++) { let weapon = this.CurUseWeaponArr[i]; let damage = statCache.get(weapon.id) || 0; let count = 0; for (let j = 0; j < this.MonsterSpawner.node.children.length; j++) { let m = this.MonsterSpawner.node.children[j].getComponent(Monster); //验证是否被该武器攻击 let isAttacked = m.checkTag("weaponAtkCount" + weapon.weaponid); if (isAttacked) { count++; } } if (count >= conf.constValue) { dps += damage / count; } console.log('武器D' + weapon.weaponid + 'DPS', damage, count, dps); } this.CurentDPS = dps / this.WeaponAtkMonsterMaxTime; this.clearDPSTag(); } /**清除武器攻击怪物的标签 */ clearDPSTag() { this.DamageStatistics.resetWeaponPartialDamageMap(); for (let i = 0; i < this.CurUseWeaponArr.length; i++) { let weapon = this.CurUseWeaponArr[i]; for (let j = 0; j < this.MonsterSpawner.node.children.length; j++) { let m = this.MonsterSpawner.node.children[j].getComponent(Monster); m.setTag("weaponAtkCount" + weapon.weaponid, false); } } } /**根据武器id获取武器技能数量 */ getWeaponSkillNum(weaponid: number) { return this.CurUseSkillArr.filter(item => item.weapon == weaponid).length; } /***根据武器ID获取到弹道的等级显示不同的弹道 */ getWeaponBulletLevel(weaponid: number) { // 规则:取 Skill 表里 `bullet`(弹道替换)字段解析出的最大“等级值” // bullet 常见为字符串:可能为空/数字/"2,xxx"/"2|xxx" 等,这里抓取其中的第一个数字。 //初始子弹弹道Weapon2表的bullet字段 let weaponData = gg.data.project.getWeaponDataByWeaponId(weaponid) let defaultBullet = weaponData.bullet const list = this.CurUseSkillArr?.filter(s => s && s.weapon === weaponid) ?? []; if (!list.length) return defaultBullet; let maxLv = defaultBullet; for (let i = 0; i < list.length; i++) { const b = list[i].bullet if (b) { const v = b; if (Number.isFinite(v) && v > maxLv) maxLv = v; } } return maxLv; } /**根据技能id获取技能数量 */ getSkillNum(skillid: number) { return this.CurUseSkillArr.filter(item => item.id == skillid).length; } /**抽卡副本增加波次 */ cacheAddWaveNum() { this.CurentWaveNum++; if (this.CurentWaveNum > this.TotalWaveNum) { this.CurentWaveNum = this.TotalWaveNum; } GEvent.Ins.emit(GEvent.BattleWaveRefresh); // 抽卡副本同样:切波后先预警 this.startWarnTime(); } /**银币变化 */ curentCoinNumChange(num: number) { this.CurentCoinNum += num; GEvent.Ins.emit(GEvent.UpdateCoin); } /**找出当前已拥有技能里面包含的武器技能 */ getCurHaveWeaponSkill(weaponid: number) { let curHaveSkillArr = this.CurUseSkillArr let haveSkill = [] for (let i = 0; i < curHaveSkillArr.length; i++) { let skill = curHaveSkillArr[i] if (skill.weapon == weaponid) { haveSkill.push(skill) } } return haveSkill.length } /**获取所有木桶宝箱列表 */ getAllWoodenTreasureList() { let chapterConfig: ITableChapter = this.CurentConfigChapter2 || this.CurentConfigChapter let treasureArr = chapterConfig.box_time.split('|') return treasureArr } /**宝箱模式下的技能和武器宝箱数据初始化 */ initBoxModeData() { let isModelBox = gg.game.CurentSelectBattleType == BattleType.BoxMode if (!isModelBox) { return } let _allSkillList = gg.data.table.getTableList(TableNames.Skill); let wuqiBoxArray = [] //武器宝箱 let skillBoxArray = []//技能宝箱 let treasureArr = this.getAllWoodenTreasureList() let playerSkillGroupIdArray = [] let playerWeaponGroupIdArray = [] for (let i = 0; i < treasureArr.length; i++) { let type = Number(treasureArr[i].split(',')[0]) if (type == 2) { wuqiBoxArray.push(i) } else if (type == 1) { skillBoxArray.push(i) } } //直线武器 let canTaskWeapon = [2, 6, 11, 14, 17, 19, 20] //反转canTaskWeapon canTaskWeapon.reverse() //从canTaskWeapon里面找出来我已经解锁的武器(只需要解锁就可以使用) let haveArr = gg.data.project.getHaveWeaponIds() let taskWeaponArray = [] for (let i = 0; i < canTaskWeapon.length; i++) { let weaponId = canTaskWeapon[i] //haveArr里面包含weaponId的武器 haveArr.map(item => { if (item == weaponId) { taskWeaponArray.push(item) } }) } let weaponArray = [] for (let i = 0; i < wuqiBoxArray.length; i++) { //taskWeaponArray排除weaponArray已经有的武器,随机选取一个放到weaponArray let newArr = [] for (let j = 0; j < taskWeaponArray.length; j++) { let weaponId = taskWeaponArray[j] if (weaponArray.indexOf(weaponId) == -1) { newArr.push(weaponId) } } // 黄金宝箱位可能比「已解锁的直线武器」还多:无未选用武器时退回整张任务池,允许重复,避免出现 undefined 导致 playerWeaponGroupIdArray 缺项 let pool = newArr.length > 0 ? newArr : taskWeaponArray if (pool.length === 0) { console.warn('initBoxModeData: 无主线路武器,用已拥有武器兜底') pool = haveArr.length > 0 ? haveArr.slice() : canTaskWeapon.slice() } let randomWeaponId = pool[Math.floor(Math.random() * pool.length)] weaponArray.push(randomWeaponId) } //五个武器没有错 //根据武器获取技能 let wuqiTemp = [1] let wuqiShowIndex = 0 let curHaveSkillArr = [] for (let i = 0; i < treasureArr.length; i++) { let type = Number(treasureArr[i].split(',')[0]) if (type == 2) { wuqiShowIndex++ for (let kk = 0; kk < wuqiShowIndex; kk++) { if (wuqiTemp.indexOf(weaponArray[kk]) == -1) { wuqiTemp.push(weaponArray[kk]) } } } else if (type == 1) { //获取技能 let list = gg.game.CurentBattle.SkillCore.randomSkills(3, false, wuqiTemp, curHaveSkillArr) // randomSkills 里会先 roll 银币(type=2),三选一可能含银币;只应在 type==1 里按品质挑,否则会误选 list[0] 的银币 let bestQuality = -1 let choose = -1 for (let j = 0; j < list.length; j++) { let skillResult = list[j] if (skillResult.type != 1 || !skillResult.skill) continue let skillConfig = _allSkillList.find(item => item.id == skillResult.skill.id) if (!skillConfig) continue let level = skillConfig.quality if (level > bestQuality) { bestQuality = level choose = j } } if (choose < 0) { choose = list.findIndex((r) => r.type == 1 && r.skill) } console.log(`initBoxModeData 技能箱选牌: choose=${choose}, bestQuality=${bestQuality}, types=${list.map((r) => r.type).join(',')}`) if (choose < 0) { console.warn('initBoxModeData: 白银箱 randomSkills 无 type=1 技能项', list) } else { curHaveSkillArr.push(list[choose]) } } } // let allList = gg.data.project.getWeaponDataList() for (let i = 0; i < weaponArray.length; i++) { let arr3 = [] let id = weaponArray[i] let obj = allList.find((item) => item.weaponid == id) ?? gg.data.project.getWeaponData(id) if (obj) { arr3.push(obj) playerWeaponGroupIdArray.push(arr3) } else { console.warn('initBoxModeData: Weapon2 无 weaponid=', id, '已跳过,黄金宝箱数与 playerWeaponGroupIdArray 可能不一致') } } for (let i = 0; i < curHaveSkillArr.length; i++) { let arr3 = [] arr3.push(curHaveSkillArr[i]) playerSkillGroupIdArray.push(arr3) } gg.game.playerSkillGroupIdArray = playerSkillGroupIdArray gg.game.playerWeaponGroupIdArray = playerWeaponGroupIdArray //打印技能id let skillIdArr = [] for (let i = 0; i < playerSkillGroupIdArray.length; i++) { let skillData = playerSkillGroupIdArray[i][0] skillIdArr.push(skillData.skill.id) } console.log('playerSkillGroupIdArray', playerSkillGroupIdArray.length, skillIdArr.length, skillIdArr) console.log('playerWeaponGroupIdArray', playerWeaponGroupIdArray) } /**显示怪物问号 */ showMonsterWenHao() { for (let i = 0; i < this.PlacedObstacleArr.length; i++) { let grid = this.PlacedObstacleArr[i]; let monsters = this.MonsterSpawner.getMonstersInArea(grid.node.worldPosition, 100); for (let j = 0; j < monsters.length; j++) { let monster = monsters[j]; if (monster) { monster.getComponent(Monster)?.showWenHaoEff(); } } } } /***修改方块武器的配置*/ changeTetriWeaponConfig(weaponType: TetriWeaponType, weaponid: number) { //获取weaponType的方块 BattleCube const battleCubeList = gg.data.table.getTableList(TableNames.BattleCube); const cubeNames = battleCubeList.filter(item => item.weapontype == weaponType); // cubeNames 去重(按 cubename,保留原顺序) const seen = new Set(); const uniqueCubeNames = cubeNames.filter(it => { const n = it?.cubename ?? ''; if (!n) return false; if (seen.has(n)) return false; seen.add(n); return true; }); if (this.haveWeaponType.indexOf(weaponType) == -1) { this.haveWeaponType.push(weaponType) } console.log('changeTetriWeaponConfig', weaponType, weaponid, uniqueCubeNames) this.addCurUseWeaponArr([weaponid]) // cubeNames 里面都改成 weaponid for (let i = 0; i < uniqueCubeNames.length; i++) { let cubeName = uniqueCubeNames[i].cubename this.TetriWeaponMap.set(cubeName, weaponid) GEvent.Ins.emit(GEvent.ChangeCubeWeaponConfig, cubeName, weaponid) } } /**随机一个方块类型(基础池中会去除已拥有类型、以及本次已随机出的类型) */ getTetrTypeRandom(excludeTypes: number[] = []) { const basePool = [1, 2, 3, 4, 5, 6]; const orangeAllow = this.BattleType === BattleType.OrangeWeaponMode_Dan && this.ModechooseTetriType?.length ? basePool.filter((t) => this.ModechooseTetriType.indexOf(t) !== -1) : null; const weaponShapePool = orangeAllow?.length ? orangeAllow : basePool; const exclude = new Set((excludeTypes ?? []).filter(x => Number.isFinite(x as any) && (x as any) > 0) as number[]); // 当前可选池:基础池中去除已拥有武器类型 + 去除本次已随机出的类型 const curentPool = weaponShapePool.filter(item => this.haveWeaponType.indexOf(item) === -1 && !exclude.has(item) ); // 场上已落稳的方块类型(同样要排除本次已随机出的类型) let havePoolAll = this.getAllTetrType().filter(t => !exclude.has(t)); //havePoolAll去掉大于6的类型 havePoolAll = havePoolAll.filter(t => t <= 6 && weaponShapePool.indexOf(t) !== -1); // 判断 havePool 是否全部都属于“已拥有武器类型” let isAllContain = true; for (let i = 0; i < havePoolAll.length; i++) { if (this.haveWeaponType.indexOf(havePoolAll[i]) === -1) { isAllContain = false; break; } } let pool: number[] = []; if (isAllContain) { // havePool 的类型全部包含在 haveWeaponType 中:优先随机 curentPool(新类型) pool = curentPool; } else { // havePool 的类型没有全部包含在 haveWeaponType 中:随机一个“已经拥有的方块类型”(从场上类型中挑 owned) //havePoolAll 去掉haveWeaponType里面有的类型 for (let i = 0; i < havePoolAll.length; i++) { let type = havePoolAll[i] if (this.haveWeaponType.indexOf(type) === -1) { pool.push(type) } } } if (pool.length === 0) { const fallback = weaponShapePool.filter(t => !exclude.has(t)); return fallback.length ? fallback[Math.floor(Math.random() * fallback.length)] : weaponShapePool[0] ?? 1; } return pool[Math.floor(Math.random() * pool.length)]; } /**获取弹武器技能宝箱类型 */ getOpenSkillSelectType() { let index = this.CurentWaveNum - 1; if (index < 0) index = 0; if (index >= this.OpenSkillSelectTypeArr.length) return 0; return this.OpenSkillSelectTypeArr[index]; } /**获取所有已落稳的方块类型 */ getAllTetrType() { const tetrTypeArr: number[] = [] if (!this.TetraMap) { return [] } const battleCubeList = gg.data.table.getTableList(TableNames.BattleCube); if (!battleCubeList?.length) { return tetrTypeArr; } // BattleCube 中的 weapontype 存在重复项,先去重后按当前堆叠数量筛选 const allTypes = new Set(); for (let i = 0; i < battleCubeList.length; i++) { const type = battleCubeList[i]?.weapontype; if (Number.isFinite(type) && type > 0) { allTypes.add(type); } } allTypes.forEach((type) => { if (this.getTetrStackNumByType(type) > 0) { tetrTypeArr.push(type); } }); return tetrTypeArr } /** * 场上所有应计入叠层/DPS 的 tetriNode。 * 藤蔓合并后邻块会被 setParent 到同一根节点下,仅遍历 _tetrisTable 直属子节点会漏计嵌套方块。 */ private _getAllWeaponStackTetriNodes(): tetriNode[] { if (!this.TetraMap?._tetrisTable?.isValid) return []; return this.TetraMap._tetrisTable.getComponentsInChildren(tetriNode) ?? []; } /**根据武器id获取当前堆叠的方块数量 */ getTetrStackNum(weaponid: number) { if (!Number.isFinite(weaponid) || weaponid <= 0) { return 0; } if (!this.TetraMap?._tetrisTable?.isValid) { return 0; } let tetrStackNum = 0; const nodes = this._getAllWeaponStackTetriNodes(); for (let i = 0; i < nodes.length; i++) { const tNode = nodes[i]; if (!tNode?.node?.isValid) continue; if (!tNode.shouldCountTowardWeaponStack()) continue; if (tNode._curweaponId !== weaponid) continue; tetrStackNum++; } return tetrStackNum; } /**根据武器id获取当前堆叠的方块“等级总和”(仅统计已落稳方块) */ getTetrStackLevelSum(weaponid: number) { if (!Number.isFinite(weaponid) || weaponid <= 0) { return 0; } if (this.BattleType === BattleType.OrangeWeaponMode_Ya) { return this._getJiujiuwoyaWeaponStackLevelSum(weaponid); } if (!this.TetraMap?._tetrisTable?.isValid) { return 0; } let sum = 0; const nodes = this._getAllWeaponStackTetriNodes(); for (let i = 0; i < nodes.length; i++) { const tNode = nodes[i]; if (!tNode?.node?.isValid) continue; if (!tNode.shouldCountTowardWeaponStack()) continue; if (tNode._curweaponId !== weaponid) continue; const lv = tNode.getNodeLevel?.() ?? 1; sum += (Number.isFinite(lv as any) && (lv as any) > 0) ? (lv as any) : 1; } return sum; } /** * 救救我鸭:场上无方块,按 jiujiuwoyaWeaponCarriers 中该武器的载体数量, * 每个载体计入对应武器等级(玩家武器等级,无方块时 getCarrierTetriLevel 为 1)。 */ private _getJiujiuwoyaWeaponStackLevelSum(weaponid: number): number { const carriers = this.jiujiuwoyaWeaponCarriers ?? []; const weaponLv = gg.data.project.getWeaponLevel(weaponid); const lvPerCarrier = (Number.isFinite(weaponLv) && weaponLv > 0) ? weaponLv : 1; let sum = 0; for (let i = 0; i < carriers.length; i++) { const carrier = carriers[i]; if (!carrier?.node?.isValid) continue; if (carrier.getCurWeaponId() !== weaponid) continue; const carrierLv = carrier.getCarrierTetriLevel(); const lv = (Number.isFinite(carrierLv) && carrierLv > 1) ? carrierLv : lvPerCarrier; sum += lv; } return sum; } /**根据方块类型获取方块数量 */ getTetrStackNumByType(type: number) { if (!Number.isFinite(type) || type <= 0) { return 0; } if (!this.TetraMap?._tetrisTable?.isValid) { return 0; } let tetrStackNum = 0; const nodes = this._getAllWeaponStackTetriNodes(); for (let i = 0; i < nodes.length; i++) { const tNode = nodes[i]; if (!tNode?.node?.isValid) continue; if (!tNode.shouldCountTowardWeaponStack()) continue; const curType = tNode.config?.weapontype; if (curType === type) { tetrStackNum++; } } return tetrStackNum; } /**根据方块类型获取方块“等级总和”(仅统计已落稳方块) */ getTetrStackLevelSumByType(type: number) { if (!Number.isFinite(type) || type <= 0) { return 0; } if (!this.TetraMap?._tetrisTable?.isValid) { return 0; } let sum = 0; const nodes = this._getAllWeaponStackTetriNodes(); for (let i = 0; i < nodes.length; i++) { const tNode = nodes[i]; if (!tNode?.node?.isValid) continue; if (!tNode.shouldCountTowardWeaponStack()) continue; const curType = tNode.config?.weapontype; if (curType !== type) continue; const lv = tNode.getNodeLevel?.() ?? 1; sum += (Number.isFinite(lv as any) && (lv as any) > 0) ? (lv as any) : 1; } return sum; } /**获取当前所有武器的DPS总和 */ dps() { if (gg.game.IsPlayingGuideStory) { return 0; } let dps = gg.data.project.getAllDps(this.CurUseSkillArr, this.CurUseWeaponArr.map(item => item.weaponid)); return dps; } /** * 技能三选一:选中该技能后相对当前面板的 DPS 变化(与 `dps()` / `getAllDps` 同源)。 */ getSkillSelectDpsDeltaForSkill(skill: ITableSkill): number { if (gg.game.IsPlayingGuideStory || !skill) { return 0; } const weaponIds = this.CurUseWeaponArr.map(item => item.weaponid); const cur = gg.data.project.getAllDps(this.CurUseSkillArr, weaponIds); const next = gg.data.project.getAllDps([...this.CurUseSkillArr, skill], weaponIds); return Math.floor(next - cur); } /** * 假设把某方块类型绑定到指定武器后,各武器「等级总和」分布(用于武器三选一 DPS 预览)。 */ getHypotheticalStackSumMapForWeaponPick(newWeaponId: number, tetriType: number): Map { if (this.BattleType === BattleType.OrangeWeaponMode_Ya) { return this._getHypotheticalJiujiuwoyaStackSumMap(newWeaponId, tetriType); } const sums = new Map(); const nodes = this._getAllWeaponStackTetriNodes(); for (let i = 0; i < nodes.length; i++) { const tNode = nodes[i]; if (!tNode?.node?.isValid) continue; if (!tNode.shouldCountTowardWeaponStack()) continue; const lv = tNode.getNodeLevel?.() ?? 1; const level = (Number.isFinite(lv as any) && (lv as any) > 0) ? (lv as any) : 1; let wid = tNode._curweaponId; const wt = tNode.config?.weapontype; if (wt === tetriType) { wid = newWeaponId; } if (!Number.isFinite(wid) || wid <= 0) continue; sums.set(wid, (sums.get(wid) || 0) + level); } return sums; } /** 救救我鸭:按角色 weapontype 假设换武器后的叠层分布 */ private _getHypotheticalJiujiuwoyaStackSumMap(newWeaponId: number, tetriType: number): Map { const sums = new Map(); const carriers = this.jiujiuwoyaWeaponCarriers ?? []; for (let i = 0; i < carriers.length; i++) { const carrier = carriers[i]; if (!carrier?.node?.isValid) continue; let wid = carrier.getCurWeaponId(); if (carrier.getWeapontype?.() === tetriType) { wid = newWeaponId; } if (!Number.isFinite(wid) || wid <= 0) continue; const carrierLv = carrier.getCarrierTetriLevel?.() ?? 1; const weaponLv = gg.data.project.getWeaponLevel(wid); const lv = (Number.isFinite(carrierLv) && carrierLv > 1) ? carrierLv : ((Number.isFinite(weaponLv) && weaponLv > 0) ? weaponLv : 1); sums.set(wid, (sums.get(wid) || 0) + lv); } return sums; } /** * 武器三选一:选中该武器+方块类型后相对当前面板的 DPS 变化。 */ getSkillSelectDpsDeltaForWeapon(weapon: ITableWeapon2, tetriType: number): number { if (gg.game.IsPlayingGuideStory || !weapon) { return 0; } const curIds = this.CurUseWeaponArr.map(w => w.weaponid); const hypoIds = curIds.includes(weapon.weaponid) ? [...curIds] : [...curIds, weapon.weaponid]; const stackMap = this.getHypotheticalStackSumMapForWeaponPick(weapon.weaponid, tetriType); const cur = gg.data.project.getAllDps(this.CurUseSkillArr, curIds); const next = gg.data.project.getAllDps(this.CurUseSkillArr, hypoIds, stackMap); return Math.floor(next - cur); } /** 竞速模式:按当前堆高度档位给的攻击乘区(仅 SpeedMode;其余模式恒为 1) */ getSpeedModeAttackBonus(): number { if (this.BattleType !== BattleType.SpeedMode || !this.CurentConfigReplica?.atk?.length || !this.CurentConfigReplica?.hight?.length) { return 1; } const atkArr = this.CurentConfigReplica.atk; const tier = this.getSpeedModeHeightTierIndex(); const idx = Math.min(tier, atkArr.length - 1); return atkArr[idx] / 10000 + 1; } /** * 竞速:当前堆高已达到的最高里程碑索引(hight 表应为升序,如 10/30/60/100)。 * 旧写法在第一个「米数>=档位」就 break,升序表下会永远停在 0 档。 */ getSpeedModeHeightTierIndex(): number { if (this.BattleType !== BattleType.SpeedMode || !this.CurentConfigReplica?.hight?.length) return 0; const heightArr = this.CurentConfigReplica.hight; let tier = 0; for (let i = 0; i < heightArr.length; i++) { if (this.TetriStackHeightMeters >= heightArr[i]) tier = i; } return Math.min(Math.max(0, tier), heightArr.length - 1); } /** * 竞速 UI:下一个待达成的里程碑索引(未满第一档→0;已超过全部→最后一档)。 */ getNextSpeedModeHeightMilestoneIndex(): number { if (this.BattleType !== BattleType.SpeedMode || !this.CurentConfigReplica?.hight?.length) return 0; const heightArr = this.CurentConfigReplica.hight; for (let i = 0; i < heightArr.length; i++) { if (this.TetriStackHeightMeters < heightArr[i]) return i; } return heightArr.length - 1; } /** @deprecated 逻辑错误且易混淆;请用 getSpeedModeHeightTierIndex / getNextSpeedModeHeightMilestoneIndex */ getCurentHeightIndex(): number { return this.getNextSpeedModeHeightMilestoneIndex(); } get curMap(): tetriMap | jiujiuwoMap { if(this.TetraMap){ return this.TetraMap; }else if(this.JiujiuwoMap){ return this.JiujiuwoMap; } return null; } }