消消方块阵换皮表情
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

//*********************
// 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;
}
}