//********************* // create by 流云 // time: Mon May 18 2026 // desc: //********************* import { _decorator, Color, Collider2D, Component, Contact2DType, ERigidBody2DType, find, instantiate, isValid, Layers, Node, ParticleSystem2D, PhysicsSystem2D, ProgressBar, RigidBody2D, sp, Tween, UITransform, tween, v3, Vec3, Label, IPhysics2DContact, } from 'cc'; import { AelementLayer } from './AelementLayer'; import { Aelement } from './Aelement'; import { LuosiDing } from './LuosiDing'; import { Auto_jiujiuwoyaGame } from './Auto_jiujiuwoyaGame'; import { BattleCore } from '../BattleGame/BattleCore'; import { GBundle, UIID } from '../../game/ConfigRes'; import { MonsterSpawner } from '../BattleGame/MonsterSpawner'; import { SkillController } from '../BattleGame/WeaponSkill/SkillController'; import { jiujiuwoMap } from '../../jiujiuwoMap/jiujiuwoMap'; import MTools from 'db://assets/mx/tools/MTools'; import { GEvent } from 'db://assets/mx/module/event/GEvent'; import { tetriWeaponCarrier } from '../../tetriCard/tetriWeaponCarrier'; import { TableNames } from '../../game/ConfigTableData'; import { Physics2DGate } from '../../game/Physics2DGate'; import { tetriMap } from '../../tetriMap/tetriMap'; const { ccclass, property } = _decorator; @ccclass('UIjiujiuwoyaGame') export class UIjiujiuwoyaGame extends Auto_jiujiuwoyaGame { private CurentBattle: BattleCore = null; private roleNodes: Node[] = []; /** 预生成的螺丝种类队列(与 allHoleNum 等长) */ private luosiTypePool: number[] = []; private luosiTypePoolIndex = 0; /** 本局待生成木板总数 / 已完成数 */ private _mubanSpawnTotal = 0; private _mubanSpawnDone = 0; private _battleStartedAfterMuban = false; /** 经验粒子飞向进度条时长(秒) */ private static readonly EXP_FLY_DURATION = 0.55; /** 宝箱拷贝飞向屏幕中心时长(秒) */ private static readonly TREASURE_CHEST_FLY_DURATION = 0.55; /** 飞行结束时的绝对 scale(xy) */ private static readonly TREASURE_CHEST_FLY_SCALE = 2; /** ui_playExp 上的宝箱(仅作拷贝源,不参与飞行) */ private _luosiReplicaChestSrc: Node = null; private _luosiReplicaChestOriginWorld: Vec3 | null = null; private _luosiReplicaTreasureAnimBusy = false; /**预警倒计时 UI:每秒刷新一次 */ private _warnUiAccSec: number = 0; private _warnUiLastShownSec: number = -1; private _speedupRateNormal: number = 1; private _speedupRateFast: number = 1.5; /** 木板 prefab 名(与 jiujiuwoyaCard 资源一致) */ private static readonly MUBAN_PREFAB_NAMES = [ '木板1_2','木板2_2','木板3_2','木板4_2','木板5_2','木板6_1','木板7_3','木板8_2' ]; onLoad(): void { super.onLoad(); } onEnable(): void { super.onEnable(); this._ensureJiujiuwoyaPhysics(); this._bindMubanNoBouncePhysics(); GEvent.Ins.on(GEvent.playerHit, this.onPlayerHit, this); GEvent.Ins.on(GEvent.tetriGameStart, this.onTetriGameStart, this); GEvent.Ins.on(GEvent.GameReborn, this.gameReborn, this); GEvent.Ins.on(GEvent.UpdateDps, this.updateDps, this); GEvent.Ins.on(GEvent.UpdateWallHp, this.refreshWallHp, this); GEvent.Ins.on(GEvent.BattleWaveRefresh, this.onBattleWaveRefresh, this); } onDisable(): void { super.onDisable(); this._unbindMubanNoBouncePhysics(); GEvent.Ins.off(GEvent.playerHit, this.onPlayerHit, this); GEvent.Ins.off(GEvent.tetriGameStart, this.onTetriGameStart, this); GEvent.Ins.off(GEvent.GameReborn, this.gameReborn, this); GEvent.Ins.off(GEvent.UpdateDps, this.updateDps, this); GEvent.Ins.off(GEvent.UpdateWallHp, this.refreshWallHp, this); GEvent.Ins.off(GEvent.BattleWaveRefresh, this.onBattleWaveRefresh, this); if (gg.game.CurentBattle) { gg.game.CurentBattle.onJiujiuwoyaLuosiEnteredSlot = null; gg.game.CurentBattle.onJiujiuwoyaLuosiPairRemovedFromSlot = null; gg.game.CurentBattle.onJiujiuwoyaReplicaTreasureBeforeOpen = null; } this.unscheduleAllCallbacks(); } gameReborn() { //如果卡槽里面有螺丝就消除最多2组 this.scheduleOnce(() => { this._ensureJiujiuwoyaPhysics(); this.CurentBattle.jiujiuwoyaSoltFullFail = false; this.CurentBattle.startGame() this.eliminateTwoScrewPairs(); this.scheduleOnce(() => this._reconcileAllBoardPhysics(), 1.5); }, 1.0) } /** 复活后按场上实际螺丝数恢复木板约束/掉落物理 */ private _reconcileAllBoardPhysics() { const parent = this.ui_mubanLayer; if (!parent?.isValid) return; for (const layer of parent.children) { if (!layer?.isValid) continue; for (const board of layer.children) { board.getComponent(Aelement)?.reconcilePhysicsState(); } } } updateDps(delay: number = 0) { if(gg.game.noMonsterShow) return; let dps = this.CurentBattle.dps(); dps = Math.floor(dps); if (this.CurentBattle?.skipNextDpsFlyAnim) { this.CurentBattle.skipNextDpsFlyAnim = false; this._lastDps = dps; this.lb_dps.string = MTools.formatChineseUnit({ num: dps }); return; } if (dps == this._lastDps || dps == 0) { if (dps == 0) { this.lb_dps.string = '0'; this._lastDps = dps; } return; } // console.log('updateDps',dps,this._lastDps) //如果dps比this._lastDps大播放动画 if (dps > this._lastDps || dps < this._lastDps) { //播放动画 let addDps = 0; let isAdd = false if (dps > this._lastDps) { addDps = dps - this._lastDps; isAdd = true; } else { addDps = this._lastDps - dps; isAdd = false; } this._lastDps = dps; this.scheduleOnce(() => { console.log('播放动画', dps, this._lastDps) let copyNode = instantiate(this.ui_dpsMove); copyNode.parent = this.ui_dpsMove.parent; copyNode.active = true let posiy = 0 copyNode.y = posiy; let lb_moveDps = copyNode.getChildByName('lb_moveDps')?.getComponent(Label); lb_moveDps.string = isAdd ? '+' + addDps : '-' + addDps; let movePosy = copyNode.position.y + 50; let endPos = this.ui_dps.position.clone(); tween(copyNode) .to(1, { y: movePosy }) .to(0.5, { position: endPos, scale: v3(0.3, 0.3, 0.3) }) .call(() => { copyNode.destroy(); this.lb_dps.string = MTools.formatChineseUnit({ num: dps }); Tween.stopAllByTarget(this.ui_dps); tween(this.ui_dps) .to(0.2, { scale: v3(1.2, 1.2, 1.2) }) .to(0.2, { scale: v3(1, 1, 1) }) .start(); }) .start(); }, delay) } else { this._lastDps = dps; this.lb_dps.string = MTools.formatChineseUnit({ num: dps }); Tween.stopAllByTarget(this.ui_dps); tween(this.ui_dps) .to(0.2, { scale: v3(1.2, 1.2, 1.2) }) .to(0.2, { scale: v3(1, 1, 1) }) .start(); } } private _lastDps: number = -1; /** 当前分支离局走 Physics2DGate.endBattle,仅救救我鸭开战时恢复物理 */ private _ensureJiujiuwoyaPhysics() { Physics2DGate.beginBattle(); } /** 木板/地板碰撞:解算前强制无弹性(不在此阶段读写 velocity,避免只读 Vec2 报错) */ private _bindMubanNoBouncePhysics() { PhysicsSystem2D.instance.on(Contact2DType.PRE_SOLVE, this._onMubanPreSolve, this); } private _unbindMubanNoBouncePhysics() { PhysicsSystem2D.instance.off(Contact2DType.PRE_SOLVE, this._onMubanPreSolve, this); } private _onMubanPreSolve(self: Collider2D, other: Collider2D, contact: IPhysics2DContact | null) { if (!contact) return; const aeA = this._getAelementFromCollider(self); const aeB = this._getAelementFromCollider(other); // 堆叠木板互相穿插,开刚体后分离冲量会表现为弹跳/颤抖:木板之间一律不碰撞 if (aeA && aeB) { contact.disabled = true; return; } const swinging = aeA?.isSwinging || aeB?.isSwinging; if (swinging) { contact.disabled = true; return; } if (!aeA && !aeB) return; try { contact.setRestitution(0); } catch { /* ignore */ } try { contact.setFriction(0.4); } catch { /* ignore */ } } private _getAelementFromCollider(col: Collider2D): Aelement | null { let n: Node | null = col?.node ?? null; while (n?.isValid) { const ae = n.getComponent(Aelement); if (ae) return ae; n = n.parent; } return null; } 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); } } } onInit(): void { let curWaveNum = gg.game.CurentBattle.CurentWaveNum; gg.sdk.reportDY("inLevel", `章节${gg.game.CurentBattle.getReportDYChapterId()}_${curWaveNum}-显示画面`) console.log('欢迎来到俄罗斯方块游戏'); gg.audio.playMusic({ name: "战斗背景音", path: "bgm/" }); gg.game.hideLoading(); //加载map1 this.CurentBattle = gg.game.CurentBattle; let mapNode = gg.res.getNode(`prefab/`, `tetriMap${gg.game.CurentBattle.MapId}`, GBundle.tetriMap); if (!mapNode) { console.error(`tetriMap${this.CurentBattle.MapId} prefab load failed`); gg.sdk?.pfLogKey?.('BATTLE:MAP_FAIL', `jiujiuwo:tetriMap${this.CurentBattle.MapId}`); return; } mapNode.parent = this.ui_mapParentNode this.setLayerMask(mapNode, this.ui_mapParentNode.layer); const staleTetriMap = mapNode.getComponent(tetriMap); if (staleTetriMap) staleTetriMap.enabled = false; this.CurentBattle.TetraMap = null; let jMap = mapNode.getComponent(jiujiuwoMap); if (!jMap) { jMap = mapNode.addComponent(jiujiuwoMap); } this.CurentBattle.JiujiuwoMap = jMap; const wallTarget = find('目标点/wallTarget1', mapNode) ?? find('wallTarget1', mapNode); if (wallTarget?.isValid) { this.CurentBattle.WallTargetNode = wallTarget; } this.CurentBattle.MonsterSpawner = this.ui_monsterParent.getComponent(MonsterSpawner); this.CurentBattle.MonsterSpawner.initMonsterConfigs(); // 拖尾 SuperTrail 为 UIRenderer,须与方块场一致走 UI_2D(预制体默认为 DEFAULT 会导致不显示) this.CurentBattle.ui_skillTraillayer = this.ui_skillTraillayer; if (this.ui_skillTraillayer?.isValid) { this.setLayerMask(this.ui_skillTraillayer, Layers.Enum.UI_2D); } if (this.ui_skilllayer?.isValid) { this.setLayerMask(this.ui_skilllayer, Layers.Enum.UI_2D); } //技能控制器 this.CurentBattle.SkillController = this.ui_skilllayer.getComponent(SkillController); this.CurentBattle.SkillController.initAll(); this.CurentBattle.ScoreLayer = this.ui_scoreLayer; this.CurentBattle.TopSpineLayer = this.ui_topSpineLayer; this.CurentBattle.ui_mubanLayer = this.ui_mubanLayer; this.CurentBattle.ui_floorNode = this.ui_floorNode; this.CurentBattle.ui_luosiding = this.ui_luosiding; this.CurentBattle.luosiSlotRemainTypes = []; this.CurentBattle.onJiujiuwoyaLuosiEnteredSlot = (luosi) => this.onLuosiEnteredSlot(luosi); this.CurentBattle.onJiujiuwoyaLuosiPairRemovedFromSlot = (a, b) => this.onLuosiPairRemovedFromSlot(a, b); this._luosiReplicaChestSrc = find('宝箱', this.ui_playExp); if (this._luosiReplicaChestSrc?.isValid) { this._luosiReplicaChestOriginWorld = this._luosiReplicaChestSrc.worldPosition.clone(); } this.CurentBattle.onJiujiuwoyaReplicaTreasureBeforeOpen = (_type, openPanels) => { this.playLuosiReplicaTreasureChestAnim(openPanels); }; if (this.ui_expPlist?.isValid) this.ui_expPlist.active = false; this.updateExpPro(); this.initFloorPhysics(); //给已经生成的角色添加武器 let role1 = find('角色1',this.ui_fightArea) let role2 = find('角色2',this.ui_fightArea) let role3 = find('角色3',this.ui_fightArea) let role4 = find('角色4',this.ui_fightArea) let weaponType = [1,2,3,4,5,6] //随机获取4中不同类型 let randomWeaponType = MTools.getRandomElements(weaponType,4) //ITableBattleCube获取type==1的配置 let cardList = gg.data.table.getTableList(TableNames.BattleCube).filter(x=>x.type==1) //cardList里面获取weapontype == randomWeaponType[i]的多个配置里面的随机一个 let cards = [] for(let i = 0; i < randomWeaponType.length; i++){ let weaponType = randomWeaponType[i] let allCard = cardList.filter(x=>x.weapontype==weaponType) let card = MTools.getRandomValueInArray(allCard) cards.push(card) } // let roleNodes = [role1, role2, role3, role4] this.roleNodes = roleNodes console.log('cards+++++++++++==',cards) //给roleNodes添加tetriWeaponCarrier脚本 const carriers: tetriWeaponCarrier[] = []; for(let i = 0; i < roleNodes.length; i++){ let roleNode = roleNodes[i] if (!roleNode?.isValid) continue; const carrier = roleNode.getOrAddComponent(tetriWeaponCarrier) carrier.setConfig(cards[i], true, true) carrier.ensureBattleReady() carriers.push(carrier); let weaponId = carrier.getCurWeaponId() if (weaponId) { gg.game.CurentBattle.addCurUseWeaponArr([weaponId]); } } this.CurentBattle.jiujiuwoyaWeaponCarriers = carriers; this.onBattleWaveRefresh() //开始根据配置创建目标板子 //ui_yazi tween(this.ui_yazi) .to(0.5,{y:-105}) .call(()=>{ this.createTargetBoard(); }) .start() } /** 斜坡地板:确保 Static 刚体与碰撞体已刷新进物理世界 */ private initFloorPhysics() { this._ensureJiujiuwoyaPhysics(); const root = this.ui_floorNode; if (!root?.isValid) return; const floorGroup = 1 << 6; for (const n of root.children) { if (!n?.isValid) continue; let rb = n.getComponent(RigidBody2D); if (!rb) rb = n.addComponent(RigidBody2D); rb.type = ERigidBody2DType.Static; rb.group = floorGroup; rb.enabled = true; rb.awakeOnLoad = true; const col = n.getComponent(Collider2D); if (col) { col.group = floorGroup; col.sensor = false; col.enabled = true; col.restitution = 0; col.friction = 0.35; const apply = (col as { apply?: () => void }).apply; if (typeof apply === 'function') apply.call(col); } } } createTargetBoard(){ //螺丝的类型1-14种颜色,对应的螺丝图片也是螺丝1到螺丝14 let luositype = 9 //luosiCardType种类 可以获取每个木板上的螺丝数目每个种类的螺丝都是双数 = ['木板1_2','木板2_2','木板3_2','木板4_2','木板5_2','木板6_2','木板7_2','木板8_2'] let luosiCardType = 8 let maxLayer = 25; //创建layer节点 名字为layer1-layer25添加AelementLayer脚本 const parent = this.ui_mubanLayer; if (!parent?.isValid) return; parent.removeAllChildren(); for (let i = 1; i <= maxLayer; i++) { const layerNode = new Node(`layer${i}`); layerNode.parent = parent; layerNode.addComponent(UITransform); layerNode.getOrAddComponent(AelementLayer); this.setLayerMask(layerNode, parent.layer); } //创建螺丝木板luosiCardType种类的木板每个种类的目标16个,分批延迟创建,随机分布在不同的layer层,随机角度和位置但是不要超过ui_mubanLayer的大小范围 const perTypeCount = 16; const createDelayStep = 0.05; const allHoleNum = this.calcAllHoleNum(perTypeCount, luosiCardType); this.luosiTypePool = LuosiDing.buildEvenTypePool(allHoleNum, luositype); this.luosiTypePoolIndex = 0; if (!LuosiDing.validateEvenTypePool(this.luosiTypePool, allHoleNum)) { console.error(`[jiujiuwoya] 螺丝种类分配异常 allHoleNum=${allHoleNum}`, this.luosiTypePool); } let spawnIndex = 0; for (let typeIdx = 0; typeIdx < luosiCardType; typeIdx++) { const prefabName = UIjiujiuwoyaGame.MUBAN_PREFAB_NAMES[typeIdx]; if (!prefabName) continue; for (let n = 0; n < perTypeCount; n++) { const delay = spawnIndex * createDelayStep; this.scheduleOnce(() => { const layerIdx = MTools.getRandomValue(1, maxLayer); this.spawnMubanCard(prefabName, layerIdx); }, delay); spawnIndex++; } } this._mubanSpawnTotal = spawnIndex; this._mubanSpawnDone = 0; this._battleStartedAfterMuban = false; } /** 单块木板生成流程结束(含螺丝),全部完成后开启战斗预警 */ private onMubanSpawnFinished() { this._mubanSpawnDone++; if (this._battleStartedAfterMuban || this._mubanSpawnDone < this._mubanSpawnTotal) return; this._finalizeMubanSpawnPhysics(); this._battleStartedAfterMuban = true; this.CurentBattle?.startWarnTime(); this.CurentBattle.mubanSpawnFinished = true; } /** 全部木板与螺丝就位后,仅开启 Polygon 遮挡检测(不启用刚体,避免 128 板 + 铰链拖垮物理) */ private _finalizeMubanSpawnPhysics() { const parent = this.ui_mubanLayer; if (!parent?.isValid) return; for (const layer of parent.children) { if (!layer?.isValid) continue; for (const board of layer.children) { board.getComponent(Aelement)?.prepareColliderForGameplay(); } } } /** 在指定 layer 内生成一块木板,随机位置与角度 */ private spawnMubanCard(prefabName: string, layerIndex: number) { const parent = this.ui_mubanLayer; if (!parent?.isValid) { this.onMubanSpawnFinished(); return; } const layerNode = find(`layer${layerIndex}`, parent); if (!layerNode?.isValid) { this.onMubanSpawnFinished(); return; } const boardNode = gg.res.getNode('prefab/', prefabName, GBundle.jiujiuwoyaCard); if (!boardNode?.isValid) { console.error(`muban prefab load failed: ${prefabName}`); gg.sdk?.pfLogKey?.('BATTLE:PREFAB_FAIL', `muban:${prefabName}`); this.onMubanSpawnFinished(); return; } boardNode.parent = layerNode; boardNode.getComponent(Aelement)?.prepareColliderForGameplay(); boardNode.scale = v3(0, 0, 0); tween(boardNode) .to(0.1, { scale: v3(1.1, 1.1, 1.1) }) .to(0.1, { scale: v3(1, 1, 1) }) .call(() => { boardNode.angle = MTools.getRandomValue(0, 359); this.spawnScrewsOnBoard(boardNode); this.onMubanSpawnFinished(); }) .start(); const pos = this.getRandomPosInMubanLayer(); boardNode.setPosition(pos.x, pos.y, 0); this.setLayerMask(boardNode, parent.layer); } /** 从 prefab 名解析孔洞数,如 木板1_2 → 2 */ private getHoleCountFromPrefabName(prefabName: string): number { const m = prefabName.match(/_(\d+)$/); return m ? parseInt(m[1], 10) : 2; } /** 所有木板的孔洞总数 */ private calcAllHoleNum(perTypeCount: number, cardTypeCount: number): number { let sum = 0; for (let i = 0; i < cardTypeCount; i++) { const name = UIjiujiuwoyaGame.MUBAN_PREFAB_NAMES[i]; if (!name) continue; sum += this.getHoleCountFromPrefabName(name) * perTypeCount; } return sum; } /** 在木板各孔洞上生成螺丝(从 luosiTypePool 依次取种类) */ private spawnScrewsOnBoard(boardNode: Node) { const parent = this.ui_mubanLayer; if (!boardNode?.isValid || !parent?.isValid) return; const aelement = boardNode.getComponent(Aelement); const holes = aelement?.getHoleNodes() ?? []; aelement?.initScrewCount(holes.length); for (const hole of holes) { if (!hole?.isValid) continue; const typeId = this.luosiTypePool[this.luosiTypePoolIndex++]; if (!typeId) { console.warn('[jiujiuwoya] 螺丝种类池已用完'); break; } const screwNode = gg.res.getNode('prefab/', '螺丝', GBundle.jiujiuwoyaCard); if (!screwNode?.isValid) { console.error('螺丝 prefab load failed'); gg.sdk?.pfLogKey?.('BATTLE:PREFAB_FAIL', '螺丝'); continue; } screwNode.parent = hole; screwNode.setPosition(0, 0, 0); screwNode.setRotationFromEuler(0, 0, 0); const luosi = screwNode.getOrAddComponent(LuosiDing); luosi.setLuosiType(typeId); luosi.bindBoardPhysics(boardNode); this.setLayerMask(screwNode, parent.layer); } } /** * 螺丝落入 ui_luosiding:每次失败检测直接从场景读取入槽序,不维护额外数组。 */ onLuosiEnteredSlot(luosi: LuosiDing) { const battle = this.CurentBattle ?? gg.game.CurentBattle; if (!battle?.mubanSpawnFinished || battle.IsGameOver || battle.IsEdit) return; if (!luosi?.isValid || luosi.isRemoving()) return; this.tryCheckLuosiSlotFail('enter', luosi.luosiType); this.scheduleOnce(() => this.tryCheckLuosiSlotFail('settle'), 0.65); } /** 卡槽失败:现场入槽序相邻消对后 remain.length >= luosiKacaoNum */ private tryCheckLuosiSlotFail(reason: string, lastType?: number) { const battle = this.CurentBattle ?? gg.game.CurentBattle; if (!battle?.mubanSpawnFinished || battle.IsGameOver || battle.IsEdit) return; const entryTypes = this.getSlotEntryTypesInOrder(); const logicalRemain = this.calcLogicalRemainTypes(entryTypes); const limit = battle.getLuosiSlotFailLimit(); const shouldFail = logicalRemain.length >= limit; console.log( `[jiujiuwoya][卡槽] ${reason} type=${lastType ?? '-'}` + ` 入槽序=${JSON.stringify(entryTypes)}(${entryTypes.length})` + ` 相邻消对后remain=${JSON.stringify(logicalRemain)}(${logicalRemain.length})` + ` luosiKacaoNum=${battle.luosiKacaoNum} 失败阈值=${limit}` + ` =>${shouldFail ? '失败' : '继续'}`, ); battle.luosiSlotRemainTypes = logicalRemain; if (shouldFail) { console.log('[jiujiuwoya][卡槽] 触发 failGame'); battle.jiujiuwoyaSoltFullFail = true; battle.failGame(); } } /** 螺丝节点已在 ui_luosiding 下且未处于消除中 */ private isLuosiInSlotNode(luosi: LuosiDing): boolean { const slot = this.ui_luosiding; return !!slot?.isValid && !!luosi?.node?.isValid && luosi.node.parent === slot && !luosi.isRemoving(); } /** 从 ui_luosiding 现场读取入槽序(Y 小→大,下方先入槽) */ private getSlotEntryTypesInOrder(): number[] { const slot = this.ui_luosiding; if (!slot?.isValid) return []; const list: LuosiDing[] = []; for (const child of slot.children) { if (!child?.isValid) continue; const luosi = child.getComponent(LuosiDing); if (luosi?.isValid && this.isLuosiInSlotNode(luosi)) { list.push(luosi); } } list.sort((a, b) => a.node.worldPosition.y - b.node.worldPosition.y); return list.map(s => s.luosiType); } /** 物理消对后(onJiujiuwoyaLuosiPairRemovedFromSlot) */ onLuosiPairRemovedFromSlot(a: LuosiDing, b: LuosiDing) { const battle = this.CurentBattle ?? gg.game.CurentBattle; if (battle) { battle.luosiSlotRemainTypes = this.calcLogicalRemainTypes(); } this.playExpFlyToBar(a, b); } /** 消除一组螺丝:生成 ui_expPlist 粒子(螺丝色)飞向进度条增长处,落地后再加经验 */ private playExpFlyToBar(a: LuosiDing, b: LuosiDing) { const battle = this.CurentBattle ?? gg.game.CurentBattle; if (!battle || battle.isLuosiReplicaLevelMax()) return; if (!this.ui_expPlist?.isValid || !this.sp_expProgressTop?.isValid) { battle.addLuosiXiaochuExp(); this.updateExpPro(); return; } const max = battle.getLuosiReplicaLevelNeedExp(); if (max <= 0) { battle.addLuosiXiaochuExp(); this.updateExpPro(); return; } const curExp = battle.luosiXiaochuExp; const fromFill = Math.min(1, curExp / max); const toFill = Math.min(1, (curExp + BattleCore.LUOSI_XIAOCHU_EXP_PER_PAIR) / max); const startWorld = this.getLuosiPairMidWorldPos(a, b); const endWorld = this.getExpBarFillWorldPos(toFill); const parent = this.ui_topSpineLayer?.isValid ? this.ui_topSpineLayer : this.node; const flyNode = instantiate(this.ui_expPlist); flyNode.active = true; flyNode.parent = parent; flyNode.setWorldPosition(startWorld); const color = LuosiDing.getLuosiColorByType(a?.luosiType ?? b?.luosiType ?? 1); const ps = flyNode.getComponent(ParticleSystem2D); if (ps) { ps.startColor = color; ps.endColor = new Color(color.r, color.g, color.b, 0); ps.resetSystem(); } Tween.stopAllByTarget(flyNode); const flyPos = startWorld.clone(); flyNode.setWorldPosition(flyPos); const flyProxy = { t: 0 }; tween(flyProxy) .to(UIjiujiuwoyaGame.EXP_FLY_DURATION, { t: 1 }, { easing: 'quadIn', onUpdate: () => { if (!flyNode?.isValid) return; const t = flyProxy.t; flyPos.set( startWorld.x + (endWorld.x - startWorld.x) * t, startWorld.y + (endWorld.y - startWorld.y) * t, startWorld.z + (endWorld.z - startWorld.z) * t, ); flyNode.setWorldPosition(flyPos); }, }) .call(() => { if (ps?.isValid) ps.stopSystem(); battle.addLuosiXiaochuExp(); this.updateExpPro(true, fromFill); if (flyNode?.isValid) { this.scheduleOnce(() => { if (flyNode?.isValid) flyNode.destroy(); }, 0.35); } }) .start(); } private getLuosiPairMidWorldPos(a: LuosiDing, b: LuosiDing): Vec3 { const pa = a?.node?.isValid ? a.node.worldPosition : null; const pb = b?.node?.isValid ? b.node.worldPosition : null; if (pa && pb) { return v3((pa.x + pb.x) * 0.5, (pa.y + pb.y) * 0.5, (pa.z + pb.z) * 0.5); } if (pa) return pa.clone(); if (pb) return pb.clone(); const slot = this.ui_luosiding; return slot?.isValid ? slot.worldPosition.clone() : this.node.worldPosition.clone(); } /** 经验条横向填充比例对应的世界坐标(fillRange 0~1,左→右) */ private getExpBarFillWorldPos(fillRatio: number): Vec3 { const barNode = this.sp_expProgressTop.node; const tf = barNode.getComponent(UITransform); if (!tf) return barNode.worldPosition.clone(); const r = Math.max(0, Math.min(1, fillRatio)); const localX = -tf.width * tf.anchorX + tf.width * r; const out = new Vec3(); tf.convertToWorldSpaceAR(v3(localX, 0, 0), out); return out; } /** 入槽序相邻同色逻辑消对后的剩余类型 */ private calcLogicalRemainTypes(entryTypes?: number[]): number[] { const types = (entryTypes ?? this.getSlotEntryTypesInOrder()).slice(); let changed = true; while (changed) { changed = false; for (let i = 0; i < types.length - 1; i++) { if (types[i] !== types[i + 1]) continue; types.splice(i, 2); changed = true; break; } } return types; } /** 广告消除:最多 2 组同色螺丝(每组 2 颗) */ private eliminateTwoScrewPairs() { const temp = this.ui_luosidingTemp; if (!temp?.isValid) return; const used = new Set(); const pairs: [LuosiDing, LuosiDing][] = []; const targetPairCount = 2; const slotScrews = this.collectLuosidingScrewsTopDown().slice(0, 2); for (const slotScrew of slotScrews) { if (pairs.length >= targetPairCount || used.has(slotScrew)) continue; const boardMatch = this.findBoardScrewByType(slotScrew.luosiType, used); if (!boardMatch) continue; used.add(slotScrew); used.add(boardMatch); pairs.push([slotScrew, boardMatch]); } while (pairs.length < targetPairCount) { const boardPair = this.findBoardPairTopDown(used); if (!boardPair) break; used.add(boardPair[0]); used.add(boardPair[1]); pairs.push(boardPair); } for (const pair of pairs) { LuosiDing.forceEliminatePair(pair[0], pair[1], temp); } const battle = this.CurentBattle ?? gg.game.CurentBattle; if (battle) battle.luosiSlotRemainTypes = this.calcLogicalRemainTypes(); } /** ui_luosiding 内已弹出螺丝,从上往下(Y 大→小) */ private collectLuosidingScrewsTopDown(): LuosiDing[] { const slot = this.ui_luosiding; if (!slot?.isValid) return []; const list: LuosiDing[] = []; for (const child of slot.children) { if (!child?.isValid) continue; const luosi = child.getComponent(LuosiDing); if (luosi?.isValid && luosi.isInLuosidingSlot() && !luosi.isRemoving()) { list.push(luosi); } } list.sort((a, b) => b.node.worldPosition.y - a.node.worldPosition.y); return list; } /** 从最上层 layer 到最下层,找第一颗指定颜色且未使用的木板螺丝 */ private findBoardScrewByType(type: number, used: Set): LuosiDing | null { let found: LuosiDing | null = null; this.forEachBoardScrewTopDown((luosi) => { if (luosi.luosiType !== type) return; found = luosi; return true; }, used); return found; } /** 从最上层往下找第一对同色木板螺丝 */ private findBoardPairTopDown(used: Set): [LuosiDing, LuosiDing] | null { let first: LuosiDing | null = null; this.forEachBoardScrewTopDown((luosi) => { first = luosi; return true; }, used); if (!first) return null; const moreUsed = new Set(used); moreUsed.add(first); let second: LuosiDing | null = null; this.forEachBoardScrewTopDown((luosi) => { if (luosi.luosiType !== first!.luosiType) return; second = luosi; return true; }, moreUsed); return second ? [first, second] : null; } /** * 遍历 ui_mubanLayer 各 layer(上→下)与木板(上→下)上的螺丝; * 回调返回 true 时停止遍历。 */ private forEachBoardScrewTopDown( fn: (luosi: LuosiDing) => boolean | void, exclude: Set, ) { const mubanLayer = this.ui_mubanLayer; if (!mubanLayer?.isValid) return; const layers = mubanLayer.children .filter(c => c?.isValid && c.getComponent(AelementLayer)) .sort((a, b) => b.getSiblingIndex() - a.getSiblingIndex()); for (const layerNode of layers) { const boards = [...layerNode.children] .filter(c => c?.isValid) .sort((a, b) => b.getSiblingIndex() - a.getSiblingIndex()); for (const boardNode of boards) { const ae = boardNode.getComponent(Aelement); if (!ae || ae.isFalling) continue; for (const hole of ae.getHoleNodes()) { for (const child of hole.children) { if (!child?.isValid) continue; const luosi = child.getComponent(LuosiDing); if (!luosi?.isValid || !luosi.isAttachedOnBoard() || exclude.has(luosi)) { continue; } if (fn(luosi) === true) return; } } } } } /** 打乱木板上仍固定的螺丝颜色;卡槽有螺丝时最上层木板优先露出对应颜色(最多 2 颗) */ private shuffleBoardLuosiTypes() { const layer = this.ui_mubanLayer; if (!layer?.isValid) return; const screws = layer.getComponentsInChildren(LuosiDing) .filter(s => s?.isValid && s.isAttachedOnBoard()); if (screws.length < 2) return; const topLayerScrews = this.getTopLayerBoardScrews(); const slotTypes = this.getSlotTypesForShufflePriority().filter(t => t > 0); const pool = screws.map(s => s.luosiType); // 仅从池中预留与顶层可放置数量一致的种类,避免顶层螺丝不足时池被多扣导致 undefined const maxPriority = Math.min(2, slotTypes.length, topLayerScrews.length); const priorityTypes: number[] = []; for (const t of slotTypes) { if (priorityTypes.length >= maxPriority) break; const idx = pool.indexOf(t); if (idx < 0) continue; priorityTypes.push(t); pool.splice(idx, 1); } const topTargets = topLayerScrews.slice(0, priorityTypes.length); this.shuffleNumberArray(pool); const typeByScrew = new Map(); for (let i = 0; i < topTargets.length; i++) { typeByScrew.set(topTargets[i], priorityTypes[i]); } let poolIdx = 0; for (const s of screws) { if (typeByScrew.has(s)) continue; const t = pool[poolIdx++]; if (t == null) { console.error('[jiujiuwoya] 打乱螺丝种类池耗尽,保留原种类', s.luosiType); typeByScrew.set(s, s.luosiType); continue; } typeByScrew.set(s, t); } for (const s of screws) { const t = typeByScrew.get(s); if (t == null) continue; s.setLuosiType(t); s.playTypeShuffleScaleAnim(); } } /** 卡槽内螺丝类型(入槽顺序,用于打乱时优先匹配) */ private getSlotTypesForShufflePriority(): number[] { return this.getSlotEntryTypesInOrder(); } /** 最上层 layer 上所有仍固定在木板上的螺丝(上→下遍历的第一层) */ private getTopLayerBoardScrews(): LuosiDing[] { const mubanLayer = this.ui_mubanLayer; if (!mubanLayer?.isValid) return []; const layers = mubanLayer.children .filter(c => c?.isValid && c.getComponent(AelementLayer)) .sort((a, b) => b.getSiblingIndex() - a.getSiblingIndex()); const topLayer = layers[0]; if (!topLayer?.isValid) return []; const list: LuosiDing[] = []; for (const boardNode of topLayer.children) { if (!boardNode?.isValid) continue; const ae = boardNode.getComponent(Aelement); if (!ae || ae.isFalling) continue; for (const hole of ae.getHoleNodes()) { for (const child of hole.children) { if (!child?.isValid) continue; const luosi = child.getComponent(LuosiDing); if (luosi?.isValid && luosi.isAttachedOnBoard()) { list.push(luosi); } } } } return list; } private shuffleNumberArray(arr: number[]) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } /** 在 ui_mubanLayer 范围内取随机坐标(留边距避免木板超出) */ private getRandomPosInMubanLayer(margin = 120): { x: number; y: number } { const tf = this.ui_mubanLayer.getComponent(UITransform); const halfW = Math.max(0, tf.width / 2 - margin); const halfH = Math.max(0, tf.height / 2 - margin); return { x: (Math.random() * 2 - 1) * halfW, y: (Math.random() * 2 - 1) * halfH, }; } update(dt: number){ this.CurentBattle.update(dt); this.showHpWarn(false) if (this.CurentBattle?.IsWarnTimeActive) { if (this.ui_timeNode?.isValid) { this.showWarnTimeUI() } // 每 1 秒刷新一次文本(或秒数变化时刷新) this._warnUiAccSec += dt; const secLeft = Math.max(0, Math.ceil(this.CurentBattle.tetriWarnTimeFix)); if (this._warnUiAccSec >= 1 || secLeft !== this._warnUiLastShownSec) { this._warnUiAccSec = 0; this._warnUiLastShownSec = secLeft; if (this.lb_dTime?.isValid) { this.lb_dTime.string = MTools.formatTimeString(secLeft, "mm:ss"); } } } else { this._warnUiAccSec = 0; this._warnUiLastShownSec = -1; } } onPlayerHit(target: Node) { this.showHpWarn(true) this.refreshWallHp(); } showHpWarn(bShow: boolean) { if (this.ui_hpWarn.active && MTools.beforeTimes(1500)) return; this.ui_hpWarn.active = bShow; } refreshWallHp() { let curWallHp = this.CurentBattle.CurentWallHp let maxWallHp = this.CurentBattle.WallMaxHp this.lb_hpNum.string = `${curWallHp}` this.ui_progressBar.getComponent(ProgressBar).progress = curWallHp / maxWallHp } /** * 经验达标开宝箱:拷贝 ui_playExp/宝箱 → 飞向屏幕中心 → 播 play → 再弹 SkillSelect/转盘并销毁拷贝。 */ private playLuosiReplicaTreasureChestAnim(openPanels: () => void) { const src = this._luosiReplicaChestSrc; if (!src?.isValid) { openPanels(); return; } if (this._luosiReplicaTreasureAnimBusy) { openPanels(); return; } this._luosiReplicaTreasureAnimBusy = true; const parent = this.ui_topSpineLayer?.isValid ? this.ui_topSpineLayer : this.node; const clone = instantiate(src); clone.active = true; clone.parent = parent; clone.setSiblingIndex(parent.children.length - 1); this.setLayerMask(clone, parent.layer); const start = (this._luosiReplicaChestOriginWorld ?? src.worldPosition).clone(); clone.setWorldPosition(start); const startScale = clone.scale.clone(); const flyScale = UIjiujiuwoyaGame.TREASURE_CHEST_FLY_SCALE; const endScale = v3(flyScale, flyScale, startScale.z); const parentTf = parent.getComponent(UITransform); const end = parentTf ? parentTf.convertToWorldSpaceAR(v3(0, 0, 0)) : v3(0, 0, 0); const dx = end.x - start.x; const dy = end.y - start.y; if (dx * dx + dy * dy < 1) { clone.setScale(endScale); this._playLuosiReplicaChestOpenAnim(clone, openPanels); return; } const mid = v3((start.x + end.x) * 0.5, (start.y + end.y) * 0.5, start.z); const dist = Math.sqrt(dx * dx + dy * dy); const lift = Math.max(120, Math.min(500, dist * 0.8)); const ctrl = v3(mid.x, mid.y + lift, start.z); Tween.stopAllByTarget(clone); const proxy = { t: 0 }; tween(proxy) .to(UIjiujiuwoyaGame.TREASURE_CHEST_FLY_DURATION, { t: 1 }, { easing: 'quadOut', onUpdate: () => { if (!clone?.isValid) return; const t = proxy.t; const it = 1 - t; const x = it * it * start.x + 2 * it * t * ctrl.x + t * t * end.x; const y = it * it * start.y + 2 * it * t * ctrl.y + t * t * end.y; clone.setWorldPosition(x, y, start.z); clone.setScale( startScale.x + (flyScale - startScale.x) * t, startScale.y + (flyScale - startScale.y) * t, startScale.z, ); }, }) .call(() => { if (!clone?.isValid) { this._luosiReplicaTreasureAnimBusy = false; openPanels(); return; } this._playLuosiReplicaChestOpenAnim(clone, openPanels); }) .start(); } private _playLuosiReplicaChestOpenAnim(clone: Node, openPanels: () => void) { const cleanup = () => { this._luosiReplicaTreasureAnimBusy = false; if (clone?.isValid) clone.destroy(); }; const sk = clone.getComponent(sp.Skeleton); if (!sk?.skeletonData) { openPanels(); cleanup(); return; } this.scheduleOnce(()=>{ if (!isValid(sk)) return; //sk.setCompleteListener(null); openPanels(); cleanup(); },1) // sk.setCompleteListener(() => { // }); if (sk.findAnimation('play')) { sk.clearTracks(); sk.setAnimation(0, 'play', false); } else { openPanels(); cleanup(); } } /** 根据 luosiXiaochuLevel / luosiXiaochuExp 与 ReplicaLevel 刷新经验条 */ updateExpPro(animate = false, fromFill?: number) { const battle = this.CurentBattle ?? gg.game.CurentBattle; if (!battle) return; const max = battle.getLuosiReplicaLevelNeedExp(); const isMax = battle.isLuosiReplicaLevelMax(); const cur = isMax ? max : battle.luosiXiaochuExp; if (this.lb_expNum) { this.lb_expNum.string = `${cur}/${max}`; } const targetFill = max > 0 ? Math.min(1, cur / max) : 0; const spr = this.sp_expProgressTop; if (!spr) return; if (!animate) { spr.fillRange = targetFill; return; } const startFill = fromFill ?? spr.fillRange; Tween.stopAllByTarget(spr); const proxy = { t: 0 }; tween(proxy) .to(0.28, { t: 1 }, { easing: 'quadOut', onUpdate: () => { spr.fillRange = startFill + (targetFill - startFill) * proxy.t; }, }) .call(() => { spr.fillRange = targetFill; }) .start(); } /***消除2组螺丝 */ click_btnXiaochu() { if (!gg.game.CurentBattle.mubanSpawnFinished) { return } gg.sdk.showVideoAd((res) => { if (res) { //白银宝箱 let curWaveNum = gg.game.CurentBattle.CurentWaveNum gg.sdk.reportDY("inLevel", `章节${this.CurentBattle.getReportDYChapterId()}_${curWaveNum}-AD-消除2组螺丝`) this.eliminateTwoScrewPairs(); return } else { gg.ui.showToast("观看视频失败"); } }); } /**点击打算螺丝重新排版颜色*/ click_btnDaduan() { if (!gg.game.CurentBattle.mubanSpawnFinished) { return } gg.sdk.showVideoAd((res) => { if (res) { //点击打算螺丝重新排版颜色 let curWaveNum = gg.game.CurentBattle.CurentWaveNum gg.sdk.reportDY("inLevel", `章节${this.CurentBattle.getReportDYChapterId()}_${curWaveNum}-AD-打乱螺丝`) this.shuffleBoardLuosiTypes(); return } else { gg.ui.showToast("观看视频失败"); } }); } click_btnJieSuo() { if (!gg.game.CurentBattle.mubanSpawnFinished) { return } //解锁 gg.sdk.showVideoAd((res) => { if (res) { //白银宝箱 // this.CurentBattle.addTestSkill() this.btnJieSuo.active = false; let curWaveNum = gg.game.CurentBattle.CurentWaveNum gg.sdk.reportDY("inLevel", `章节${this.CurentBattle.getReportDYChapterId()}_${curWaveNum}-AD-解锁螺丝卡槽`) this.CurentBattle.luosiKacaoNum++; console.log( '[jiujiuwoya][卡槽] 解锁卡槽 luosiKacaoNum=', this.CurentBattle.luosiKacaoNum, '失败阈值=', this.CurentBattle.getLuosiSlotFailLimit(), ); let floor5 = find('floor5',this.ui_floorNode) floor5.active = false; return } else { gg.ui.showToast("观看视频失败"); } }); } click_btnPause() { this.CurentBattle.pause('BattlePause'); gg.ui.openPanel(UIID.BattlePause); } onBattleWaveRefresh(){ this.lb_boci.string = `第${this.CurentBattle.CurentWaveNum}/${this.CurentBattle.TotalWaveNum}波` } /**波次倒计时结束,开始战斗*/ onTetriGameStart() { if (this.ui_timeNode?.isValid && this.ui_timeNode.active) { tween(this.ui_timeNode) .to(0.1, { scale: v3(0.1, 0.1, 0.1) }) .call(() => { this.ui_timeNode.active = false; }) .start(); } } showWarnTimeUI() { //播放一次动画 const shouldPlayAnim = !this.ui_timeNode.active; if (shouldPlayAnim && this.CurentBattle.tetriWarnTimeFix > 0) { // this.CurentBattle.TetraMap.showQiPaoEffect() console.log('showWarnTimeUI 播放一次动画'); this.ui_timeNode.active = true; if(gg.game.noMonsterShow) { this.ui_timeNode.active = false; }; this.ui_timeNode.scale = v3(0.1, 0.1, 0.1); Tween.stopAllByTarget(this.ui_timeNode); tween(this.ui_timeNode) .to(0.2, { scale: v3(1.1, 1.1, 1.1) }, { easing: 'quadOut' }) .to(0.2, { scale: v3(1, 1, 1) }, { easing: 'quadInOut' }) .start(); } this.lb_waveDesc.string = `第${this.CurentBattle.CurentWaveNum}波怪物来袭`; this.lb_dTime.string = MTools.formatTimeString(this.CurentBattle.tetriWarnTimeFix, "mm:ss"); } click_btnSpeedUp() { if (!this.CurentBattle) return; //第二章解锁 if (gg.data.doc.chapter == 1) { gg.ui.showToast(gg.lang.get('通关第{1}章解锁', [1]), false) return } //打点 const cur = this.CurentBattle.TimeScale > this._speedupRateNormal ? this._speedupRateFast : this._speedupRateNormal; this.CurentBattle.TimeScale = (cur >= this._speedupRateFast) ? this._speedupRateNormal : this._speedupRateFast; let curWaveNum = gg.game.CurentBattle.CurentWaveNum gg.sdk.reportDY("inLevel", `章节${gg.game.CurentBattle.getReportDYChapterId()}_${curWaveNum}-加速点击` + this.CurentBattle.TimeScale) this._refreshSpeedUpBtnView(); } private _refreshSpeedUpBtnView() { if (!this.btnSpeedUp?.isValid || !this.CurentBattle) return; const isFast = this.CurentBattle.TimeScale > this._speedupRateNormal; const sp1 = this.btnSpeedUp.getChildByName("1"); const sp2 = this.btnSpeedUp.getChildByName("2"); if (sp1?.isValid) sp1.active = !isFast; if (sp2?.isValid) sp2.active = isFast; } }