You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1286 lines
50 KiB
1286 lines
50 KiB
|
|
//*********************
|
|
// 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<ITableBattleCube>(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<LuosiDing>();
|
|
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>): 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, 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<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());
|
|
|
|
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<LuosiDing, number>();
|
|
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;
|
|
}
|
|
}
|
|
|