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.
1215 lines
48 KiB
1215 lines
48 KiB
import { _decorator, Collider2D, Component, ERigidBody2DType, EventTouch, Graphics, instantiate, Intersection2D, Node, PolygonCollider2D, RigidBody2D, tween, Tween, UIOpacity, UITransform, Vec2, Vec3, view, Color, Rect, Label, Sprite } from 'cc';
|
|
import { tetriMap } from '../tetriMap/tetriMap';
|
|
import { GEvent } from '../../mx/module/event/GEvent';
|
|
import { TetriType } from '../game/ConfigProjectData';
|
|
import { tetriNode } from './tetriNode';
|
|
import { tetriWeaponCarrier } from './tetriWeaponCarrier';
|
|
import { tetriAbility } from './tetriAbility';
|
|
import { tetriFloorNode } from './tetriFloorNode';
|
|
import { UIID } from '../game/ConfigRes';
|
|
import { StatusType } from '../game/GameData';
|
|
import MTools from '../../mx/tools/MTools';
|
|
import { BattleType } from '../manager/ChapterDataManager';
|
|
import { playTetriBlockSpine, TetriBlockSpineState } from './tetriBlockSpine';
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
/***方块摆放的item节点, 放在tetraGame上的ui_card的子节点上,会自动根据方块的类型创建对应的方块节点
|
|
* 可以进行触摸移动
|
|
*
|
|
* 点击卡牌后从模板克隆纯视觉预览;松手时再从模板实例化全新战场方块(与预览彻底分离,无物理开关竞态)。
|
|
* 根据类型不同,所以方块有两个脚本
|
|
* tetriCardItem: 自由下落的方块节点
|
|
* tetraFloorNode: 无重力方块,墙体。接触到其他方块会立马停住做为底板使用
|
|
* 方块会绑定武器和技能根据武器不同,调度进行发射子弹
|
|
* TetriType == 3||TetriType == 4 添加脚本tetriAbility脚本 自带脚本tetriNode
|
|
* TetriType == 1 || TetriType == 5 添加脚本tetriWeaponCarrier脚本 自带脚本tetriNode
|
|
* TetriType == 2 自带脚本tetriFloorNode
|
|
*/
|
|
@ccclass('tetriCardItem')
|
|
export class tetriCardItem extends Component {
|
|
|
|
/**本卡牌对应的方块配置(BattleCube 表) */
|
|
public config: ITableBattleCube | null = null;
|
|
|
|
public setConfig(cfg: ITableBattleCube | null) {
|
|
this.config = cfg;
|
|
this.refreshCoinNum()
|
|
}
|
|
|
|
wenhaoBtnNode: Node | null = null;
|
|
tetriNameLabel: Label | null = null;
|
|
/**放置区域下边界节点(默认从 CurentBattle.TetraMap 获取,也可外部注入) */
|
|
highLine: Node | null = null;
|
|
chooseMask: Node | null = null;
|
|
liangkuangNode: Node | null = null;
|
|
/**拖到该节点上松手则取消(chooseMask 的子节点:cancleNode) */
|
|
cancleNode: Node | null = null;
|
|
/**真实下落/稳定的方块父节点(地图 GameRoot) */
|
|
physicsRoot: Node | null = null;
|
|
/**绑定地图脚本(用于读取镜头缩放,做 x 坐标换算) */
|
|
private _boundMap: tetriMap | null = null;
|
|
|
|
/**外部回调:松手且满足放置条件时触发;返回 true 表示战场已接管 battleNode */
|
|
onPlaceRequested: ((self: tetriCardItem, battleNode: Node, worldPos: Vec3) => boolean) | null = null;
|
|
/**外部回调:卡牌被成功消耗(放置成功) */
|
|
onConsumed: (() => void) | null = null;
|
|
|
|
// 原始状态(用于回弹复位)
|
|
private _originPos: Vec3 = new Vec3();
|
|
private _originSiblingIdx: number = 0;
|
|
|
|
// 方块子节点及其原始本地坐标(用于预览创建/销毁)
|
|
private _pieceNode: Node | null = null;
|
|
/***预览二号 */
|
|
private _pieceNodeCopyMove: Node | null = null;
|
|
private _pieceOriginLocalPos: Vec3 = new Vec3();
|
|
private _holdScheduled: boolean = false;
|
|
private _lastTouchEvent: EventTouch | null = null;
|
|
private _startPos: Vec2 = new Vec2();
|
|
private _startTime: number = 0;
|
|
private static readonly HOLD_SECONDS = 0;
|
|
|
|
private _isDragging: boolean = false;
|
|
private _snapTween: Tween<Node> | null = null;
|
|
private _previewNode: Node | null = null;
|
|
/** 预览二号:仅视觉对齐,跟手点上方偏移;不参与物理/掉落/角色武器 */
|
|
private _previewAlignNode: Node | null = null;
|
|
/** 拿起方块时记录的 scale,拖拽期内乘镜头缩放;交还卡牌/下落前恢复 */
|
|
private _previewDragBaseScale: Vec3 | null = null;
|
|
private _guideLine: Node | null = null;
|
|
private _guideLineParent: Node | null = null;
|
|
private _isOverCancel: boolean = false;
|
|
private _cancelNodeBaseScale: Vec3 | null = null;
|
|
|
|
/**钳制安全边距(像素),避免边缘出现少量溢出;可在编辑器里调 */
|
|
@property({ tooltip: '拖拽钳制的安全边距(px),越大越不容易“露出一点点”' })
|
|
public clampPaddingPx: number = 8;
|
|
|
|
/**
|
|
* 预览二号(对齐用):相对手指触点沿 UI 坐标 Y 轴向上偏移。
|
|
* 需求约 30cm 时请在编辑器按设计分辨率/DPI 换算为像素后填写。
|
|
*/
|
|
@property({ tooltip: '预览二号:手指触点正上方偏移(UI 像素,与 getUILocation 同坐标系)' })
|
|
public alignPreviewOffsetAboveFingerPx: number = 400;
|
|
|
|
|
|
public enableAlignPreview: boolean = false;
|
|
|
|
// 位置钳制缓存(mask 本地坐标系):AABB 相对 previewRoot.position 的偏移量
|
|
private _clampRelAABB: { minX: number; minY: number; maxX: number; maxY: number } | null = null;
|
|
|
|
// 引导区域边界缓存(基于 tetriMap 里的 GameRoot 子节点 UITransform 计算)
|
|
private _chooseMaskTopY: number = 0;
|
|
|
|
/**银币是否够放置方块 */
|
|
_affordable: boolean = true;
|
|
/** world -> local */
|
|
private _worldToLocal(parent: Node, world: Vec3): Vec3 {
|
|
const ut = parent.getComponent(UITransform);
|
|
if (!ut) return world.clone();
|
|
const local = new Vec3();
|
|
ut.convertToNodeSpaceAR(world, local);
|
|
return local;
|
|
}
|
|
|
|
private _setLayerRecursively(node: Node, layer: number) {
|
|
node.layer = layer;
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
this._setLayerRecursively(node.children[i], layer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 拖拽预览专用:剥玩法脚本并移除刚体,仅保留渲染与 Polygon(重叠检测)。
|
|
* 预览节点用完即毁,永不进入战场,从设计上避免「关物理 → 再开物理」竞态。
|
|
*/
|
|
private _stripPreviewForDrag(root: Node | null): void {
|
|
if (!root?.isValid) return;
|
|
this._stripAlignPreviewGameplay(root);
|
|
for (const rb of root.getComponentsInChildren(RigidBody2D)) {
|
|
rb.destroy();
|
|
}
|
|
}
|
|
|
|
/** 从卡牌模板实例化战场方块(与拖拽预览彻底分离) */
|
|
public createBattlePiece(): Node | null {
|
|
if (!this._pieceNode?.isValid) return null;
|
|
const node = instantiate(this._pieceNode);
|
|
if (node?.isValid) {
|
|
for (const wc of node.getComponentsInChildren(tetriWeaponCarrier)) {
|
|
wc.resetWeaponForBattlePlace();
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/** 递归销毁名为「角色」的子树(预览二号禁止挂角色/武器) */
|
|
private _destroyRoleNamedSubtrees(node: Node | null): void {
|
|
if (!node?.isValid) return;
|
|
const kids = node.children.slice();
|
|
for (const ch of kids) {
|
|
if (ch.name === '角色') {
|
|
ch.destroy();
|
|
} else {
|
|
this._destroyRoleNamedSubtrees(ch);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 预览:剥掉下落/碰撞玩法脚本,保留 tetriWeaponCarrier 仅作展示(禁止射击)。
|
|
*/
|
|
private _stripAlignPreviewGameplay(root: Node | null): void {
|
|
if (!root?.isValid) return;
|
|
for (const c of root.getComponentsInChildren(tetriAbility)) {
|
|
c.destroy();
|
|
}
|
|
for (const c of root.getComponentsInChildren(tetriWeaponCarrier)) {
|
|
c.setIsShootCan(false);
|
|
}
|
|
this._destroyRoleNamedSubtrees(root);
|
|
for (const c of root.getComponentsInChildren(tetriNode)) {
|
|
c.destroy();
|
|
}
|
|
for (const c of root.getComponentsInChildren(tetriFloorNode)) {
|
|
c.destroy();
|
|
}
|
|
}
|
|
|
|
private _destroyAlignPreview(): void {
|
|
if (this._previewAlignNode?.isValid) {
|
|
this._previewAlignNode.destroy();
|
|
}
|
|
this._previewAlignNode = null;
|
|
}
|
|
|
|
private _setCardVisible(visible: boolean): void {
|
|
const op = this.node.getComponent(UIOpacity) ?? this.node.addComponent(UIOpacity);
|
|
op.opacity = visible ? 255 : 0;
|
|
}
|
|
|
|
/**外部可拿当前预览节点(用于放置成功时接管) */
|
|
public get previewNode(): Node | null { return this._previewNode; }
|
|
|
|
public setChooseMask(mask: Node | null) {
|
|
this.chooseMask = mask;
|
|
}
|
|
public setCancleNode(node: Node | null) {
|
|
this.cancleNode = node;
|
|
this._cancelNodeBaseScale = node ? node.scale.clone() : null;
|
|
}
|
|
/**允许外部注入 map(推荐在发牌后立刻调用一次) */
|
|
public bindMap(map: tetriMap | null) {
|
|
if (!map) return;
|
|
this._boundMap = map;
|
|
this.physicsRoot = map._gameRoot ?? null;
|
|
this.highLine = map._highLine ?? null;
|
|
}
|
|
|
|
/**
|
|
* 拖拽主预览在 UI 坐标下跟手;白线副本/最终下落在游戏相机下渲染。
|
|
* 镜头拉远后,两者 x 需要按 zoom 做换算,否则会出现“偏离引导线”。
|
|
*/
|
|
private _toGameWorldXFromUiWorldX(uiWorldX: number): number {
|
|
const map = this._boundMap;
|
|
if (map?.uiWorldXToGameWorldX) {
|
|
return map.uiWorldXToGameWorldX(uiWorldX);
|
|
}
|
|
const zoom = map?.getVisualZoomScale?.() ?? 1;
|
|
if (!Number.isFinite(zoom) || zoom <= 0 || Math.abs(zoom - 1) < 0.0001) return uiWorldX;
|
|
const camX = (globalThis as any)?.gg?.ui?.GameCamera?.node?.worldPosition.x ?? 0;
|
|
return camX + (uiWorldX - camX) / zoom;
|
|
}
|
|
|
|
private _toGameWorldYFromUiWorldY(uiWorldY: number): number {
|
|
const map = this._boundMap;
|
|
if (map?.uiWorldYToGameWorldY) {
|
|
return map.uiWorldYToGameWorldY(uiWorldY);
|
|
}
|
|
const zoom = map?.getVisualZoomScale?.() ?? 1;
|
|
if (!Number.isFinite(zoom) || zoom <= 0 || Math.abs(zoom - 1) < 0.0001) return uiWorldY;
|
|
const camY = (globalThis as any)?.gg?.ui?.GameCamera?.node?.worldPosition.y ?? 0;
|
|
return camY + (uiWorldY - camY) / zoom;
|
|
}
|
|
|
|
/**外部显式指定“卡牌里哪一个节点是方块”(强烈推荐在发牌时调用) */
|
|
public setPieceNode(node: Node | null) {
|
|
this._pieceNode = node;
|
|
if (this._pieceNode) {
|
|
this._pieceOriginLocalPos = this._pieceNode.position.clone();
|
|
}
|
|
}
|
|
|
|
protected onLoad(): void {
|
|
this._originPos = this.node.position.clone();
|
|
this._originSiblingIdx = this.node.getSiblingIndex();
|
|
this.liangkuangNode = this.node.getChildByName('亮框');
|
|
this.wenhaoBtnNode = this.node.getChildByName('btnwenhao');
|
|
this.tetriNameLabel = this.node.getChildByName('tetriName').getComponent(Label);
|
|
|
|
// 兜底:如果外部没调用 setPieceNode,则尝试自动抓取
|
|
if (!this._pieceNode) {
|
|
this._pieceNode = this.node.getChildByName('Tetris') ?? this.node.children[0] ?? null;
|
|
if (this._pieceNode) {
|
|
this._pieceOriginLocalPos = this._pieceNode.position.clone();
|
|
}
|
|
}
|
|
|
|
// 如果没有外部注入,尝试从当前战斗里取 tetriMap 绑定
|
|
try {
|
|
const ggAny = (globalThis as any).gg;
|
|
const map = ggAny?.game?.CurentBattle?.TetraMap as tetriMap | null;
|
|
if (map) this.bindMap(map);
|
|
} catch { }
|
|
|
|
this.node.on(Node.EventType.TOUCH_START, this._onTouchStart, this);
|
|
this.node.on(Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
|
|
this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
|
|
this.node.on(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
|
|
|
|
GEvent.Ins.on(GEvent.showCoinFly, this.refreshCoinNum2, this);
|
|
GEvent.Ins.on(GEvent.UpdateGameCoinNum, this.refreshCoinNum, this);
|
|
GEvent.Ins.on(GEvent.UpdateCoin, this.refreshCoinNum, this);
|
|
|
|
this.refreshCoinNum()
|
|
}
|
|
|
|
|
|
|
|
protected onDestroy(): void {
|
|
this.node.off(Node.EventType.TOUCH_START, this._onTouchStart, this);
|
|
this.node.off(Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
|
|
this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
|
|
this.node.off(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
|
|
|
|
GEvent.Ins.off(GEvent.showCoinFly, this.refreshCoinNum2, this);
|
|
GEvent.Ins.off(GEvent.UpdateGameCoinNum, this.refreshCoinNum, this);
|
|
GEvent.Ins.off(GEvent.UpdateCoin, this.refreshCoinNum, this);
|
|
|
|
this._destroyAlignPreview();
|
|
}
|
|
|
|
private _onTouchStart(event: EventTouch): void {
|
|
// 地图异步晚到时补绑,避免 highLine/layer 为空导致无 copy、无法掉落
|
|
if (!this._boundMap || !this.highLine?.isValid) {
|
|
try {
|
|
const map = (globalThis as any).gg?.game?.CurentBattle?.TetraMap as tetriMap | null;
|
|
if (map) this.bindMap(map);
|
|
} catch { }
|
|
}
|
|
|
|
if (!this._affordable) {
|
|
//
|
|
if (!MTools.beforeTimes(1000, "goldNotEnoughTimeStamp")) {
|
|
gg.ui.showToast(gg.lang.get('银币不足'))
|
|
}
|
|
|
|
|
|
return
|
|
}
|
|
//触摸穿透 为了maplayer的可以监听到点击事件给下落的方块左右移动
|
|
//如果是使用锤子阶段就触摸穿透
|
|
if (gg.game.CurentBattle.IsUseShovel) {
|
|
event.preventSwallow = true;
|
|
event.propagationStopped = true;
|
|
}
|
|
this._snapTween?.stop();
|
|
this._snapTween = null;
|
|
this._isDragging = true;
|
|
this._startPos = event.getUILocation();
|
|
this._startTime = Date.now();
|
|
this._lastTouchEvent = event;
|
|
|
|
this.scheduleOnce(() => {
|
|
if (this._previewNode) {
|
|
this._syncPreviewToTouch(event);
|
|
}
|
|
}, gg.game.CurentBattle.cancelTimeMs / 1000);
|
|
|
|
// 置顶,避免被其他 UI 遮挡
|
|
if (this.node.parent) {
|
|
this.node.setSiblingIndex(this.node.parent.children.length - 1);
|
|
}
|
|
|
|
if(gg.data.getStatus(StatusType.IsShowPlayGuidePop) == 0){
|
|
//打点
|
|
gg.sdk.reportDY("inLevel", `章节${gg.game.CurentBattle.getReportDYChapterId()}_${gg.game.CurentBattle.CurentWaveNum}-首次拖动方块1`);
|
|
}
|
|
// 按住 0.5s 后才真正“拿起方块”:卡牌透明 + 方块移到 GameRoot
|
|
if (!this._holdScheduled) {
|
|
this._holdScheduled = true;
|
|
this.scheduleOnce(() => {
|
|
this._holdScheduled = false;
|
|
if (!this._isDragging) return;
|
|
if (this._previewNode) return;
|
|
this._createPreview();
|
|
if (!this._previewNode) return;
|
|
this._setCardVisible(false);
|
|
if (this._lastTouchEvent) this._syncPreviewToTouch(this._lastTouchEvent);
|
|
if(gg.data.doc.chapter == 1){
|
|
GEvent.Ins.emit(GEvent.TetriGameTouchStart);
|
|
}
|
|
|
|
if(gg.data.getStatus(StatusType.IsShowPlayGuidePop) == 0){
|
|
//打点
|
|
gg.sdk.reportDY("inLevel", `章节${gg.game.CurentBattle.getReportDYChapterId()}_${gg.game.CurentBattle.CurentWaveNum}-首次拖动方块2`);
|
|
}
|
|
|
|
}, tetriCardItem.HOLD_SECONDS);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
//移动添加区域保护不能超过chooseMask包围的区域
|
|
private _onTouchMove(event: EventTouch): void {
|
|
if (!this._isDragging) return;
|
|
this._lastTouchEvent = event;
|
|
if (this._previewNode) {
|
|
this._syncPreviewToTouch(event);
|
|
}
|
|
|
|
}
|
|
|
|
private _onTouchEnd(event: EventTouch): void {
|
|
if (gg.game.CurentBattle.IsUseShovel) {
|
|
event.preventSwallow = true;
|
|
event.propagationStopped = true;
|
|
}
|
|
if (!this._isDragging) return;
|
|
this._isDragging = false;
|
|
this._lastTouchEvent = null;
|
|
|
|
// 如果还没到“按住 0.5s 拿起”的时机,直接结束(不消耗、不改变卡牌)
|
|
if (!this._previewNode) {
|
|
this._setCardVisible(true);
|
|
this.snapBack();
|
|
return;
|
|
}
|
|
|
|
// 拖到取消区松手:直接取消
|
|
if (this._isOverCancel) {
|
|
this.cancelPlace();
|
|
return;
|
|
}
|
|
|
|
// 新需求:任何地方松手都放置;默认 x 取当前位置,y 固定为白线处开始掉落;isShowBlockDrop 时在手指释放处落点并做 Polygon 重叠校验
|
|
const pos = this._getDropStartWorldPos();
|
|
if (!pos) {
|
|
this.cancelPlace();
|
|
return;
|
|
}
|
|
if (gg.game.isShowBlockDrop && this._previewPolygonsOverlapGameplayBlocksAt(pos)) {
|
|
this.cancelPlace();
|
|
return;
|
|
}
|
|
if (this.onPlaceRequested) {
|
|
const battleNode = this.createBattlePiece();
|
|
if (!battleNode) {
|
|
this.cancelPlace();
|
|
return;
|
|
}
|
|
const placed = this.onPlaceRequested(this, battleNode, pos) === true;
|
|
if (!placed) {
|
|
if (battleNode.isValid) battleNode.destroy();
|
|
return;
|
|
}
|
|
} else {
|
|
this.consume();
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/**放置成功:销毁预览、扣银币、隐藏卡牌 */
|
|
public consume(): void {
|
|
this._snapTween?.stop();
|
|
this._tearDownDragPreview();
|
|
if (gg.game.CurentBattle.BattleType !== BattleType.OrangeWeaponMode_Dian) {
|
|
gg.game.CurentBattle.subCoinNum(this.config?.price ?? 0);
|
|
}
|
|
this.node.active = false;
|
|
this.onConsumed?.();
|
|
if (gg.data.doc.chapter == 1) {
|
|
GEvent.Ins.emit(GEvent.TetriGameTouchEnd);
|
|
}
|
|
}
|
|
|
|
/**放置失败:销毁预览,卡牌回弹 */
|
|
public cancelPlace(): void {
|
|
this._destroyPreview();
|
|
this._setCardVisible(true);
|
|
this.snapBack();
|
|
if(gg.data.doc.chapter == 1){
|
|
GEvent.Ins.emit(GEvent.TetriGameTouchEnd);
|
|
}
|
|
}
|
|
|
|
/**弹回原始位置 */
|
|
public snapBack(): void {
|
|
if (this.node.parent) {
|
|
this.node.setSiblingIndex(this._originSiblingIdx);
|
|
}
|
|
this._setCardVisible(true);
|
|
this._snapTween = tween(this.node)
|
|
.to(0.3, { position: this._originPos }, { easing: 'backOut' })
|
|
.start();
|
|
|
|
if(gg.data.doc.chapter == 1){
|
|
GEvent.Ins.emit(GEvent.TetriGameTouchEnd);
|
|
}
|
|
}
|
|
|
|
private _isAboveLine(_event: EventTouch): boolean {
|
|
if (!this.highLine || !this._previewNode) return false;
|
|
const ut = this._previewNode.getComponent(UITransform);
|
|
const halfH = ut ? ut.getBoundingBoxToWorld().height / 2 : 0;
|
|
const bottomY = this._previewNode.worldPosition.y - halfH;
|
|
return bottomY > this.highLine.worldPosition.y;
|
|
}
|
|
|
|
/** 预览二号:底边高于高度线(白线)时隐藏,移回线下方再显示 */
|
|
private _syncAlignPreviewVisibilityByHighLine(): void {
|
|
if (!this.enableAlignPreview) return;
|
|
if (!this._previewAlignNode?.isValid) return;
|
|
if (!this.highLine?.isValid) {
|
|
this._previewAlignNode.active = true;
|
|
return;
|
|
}
|
|
const ut = this._previewAlignNode.getComponent(UITransform);
|
|
const halfH = ut ? ut.getBoundingBoxToWorld().height / 2 : 0;
|
|
const bottomY = this._previewAlignNode.worldPosition.y - halfH;
|
|
const overHighLine = bottomY > this.highLine.worldPosition.y;
|
|
this._previewAlignNode.active = !overHighLine;
|
|
}
|
|
|
|
private _isOverCancleNode(event: EventTouch): boolean {
|
|
if (!this.cancleNode) return false;
|
|
const ut = this.cancleNode.getComponent(UITransform);
|
|
if (!ut) return false;
|
|
const rect = ut.getBoundingBoxToWorld();
|
|
const p = event.getUILocation();
|
|
// 仅在 cancleNode 的矩形区域内才视为“取消”
|
|
return p.x >= rect.xMin && p.x <= rect.xMax && p.y >= rect.yMin && p.y <= rect.yMax;
|
|
}
|
|
|
|
private _getDropStartWorldPos(): Vec3 | null {
|
|
if (!this._previewNode) return null;
|
|
const footprintNode =
|
|
this._pieceNodeCopyMove?.isValid ? this._pieceNodeCopyMove : this._previewNode;
|
|
const centerGameX = this._pieceNodeCopyMove?.isValid
|
|
? this._pieceNodeCopyMove.worldPosition.x
|
|
: this._toGameWorldXFromUiWorldX(this._previewNode.worldPosition.x);
|
|
let x = centerGameX;
|
|
if (footprintNode?.isValid && this._boundMap?.snapPlaceCenterWorldX) {
|
|
x = this._boundMap.snapPlaceCenterWorldX(centerGameX, footprintNode);
|
|
}
|
|
const z = this._previewNode.worldPosition.z;
|
|
if (gg.game.isShowBlockDrop) {
|
|
const y = this._toGameWorldYFromUiWorldY(this._previewNode.worldPosition.y);
|
|
return new Vec3(x, y, z);
|
|
}
|
|
if (!this.highLine) return null;
|
|
const ut = this._previewNode.getComponent(UITransform);
|
|
const halfH = ut ? ut.getBoundingBoxToWorld().height / 2 : 0;
|
|
// 下落起点固定为白线处(底边贴线)。即使拖拽到很高也只影响 X,不影响下落起点 Y。
|
|
const minCenterY = this.highLine.worldPosition.y + halfH;
|
|
return new Vec3(x, minCenterY, z);
|
|
}
|
|
|
|
/**白线预览块固定 Y:白线 worldY + 方块半高(让底边贴线) */
|
|
private _getWhiteLineFixedWorldYFor(node: Node): number | null {
|
|
if (!this.highLine) return null;
|
|
const rect = this._getWorldRect(node);
|
|
const halfH = rect ? rect.height / 2 : 0;
|
|
return this.highLine.worldPosition.y + halfH;
|
|
}
|
|
|
|
/** PolygonCollider2D 顶点变换到世界 XY(与 enabled 无关,用于精确重叠判定) */
|
|
private _getPolygonColliderWorldPoints2D(poly: PolygonCollider2D): Vec2[] {
|
|
const owner = poly.node;
|
|
const m = owner.getWorldMatrix();
|
|
const offset = (poly as any).offset as Vec2 | undefined;
|
|
const offX = offset?.x ?? 0;
|
|
const offY = offset?.y ?? 0;
|
|
const pts = poly.points ?? [];
|
|
const wv = new Vec3();
|
|
const lv = new Vec3();
|
|
const out: Vec2[] = [];
|
|
for (const p of pts) {
|
|
lv.set(p.x + offX, p.y + offY, 0);
|
|
Vec3.transformMat4(wv, lv, m);
|
|
out.push(new Vec2(wv.x, wv.y));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private _polygonHasEnoughVerts(pts: readonly Vec2[]): boolean {
|
|
return (pts?.length ?? 0) >= 3;
|
|
}
|
|
|
|
/** 节点是否属于场上玩法方块(tetriNode / tetriFloorNode 子树) */
|
|
private _isGameplayBlockColliderNode(n: Node | null): boolean {
|
|
let cur: Node | null = n;
|
|
while (cur) {
|
|
if (cur.getComponent(tetriNode) || cur.getComponent(tetriFloorNode)) return true;
|
|
cur = cur.parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 在指定世界落点下,预览块 Polygon 是否与场上已存在方块的启用 PolygonCollider2D 相交。
|
|
* 预览仍在 chooseMask 下,通过平移当前世界多边形到落点做判定。
|
|
*/
|
|
private _previewPolygonsOverlapGameplayBlocksAt(dropWorldPos: Vec3): boolean {
|
|
if (!this._previewNode?.isValid) return true;
|
|
const gameRoot = this._boundMap?._gameRoot;
|
|
if (!gameRoot?.isValid) return false;
|
|
|
|
const root = this._previewNode;
|
|
const dx = dropWorldPos.x - root.worldPosition.x;
|
|
const dy = dropWorldPos.y - root.worldPosition.y;
|
|
|
|
const previewPolys = root.getComponentsInChildren(PolygonCollider2D) ?? [];
|
|
const shiftedPreview: Vec2[][] = [];
|
|
for (const pp of previewPolys) {
|
|
const w = this._getPolygonColliderWorldPoints2D(pp);
|
|
if (!this._polygonHasEnoughVerts(w)) continue;
|
|
const shifted: Vec2[] = [];
|
|
for (const p of w) {
|
|
shifted.push(new Vec2(p.x + dx, p.y + dy));
|
|
}
|
|
shiftedPreview.push(shifted);
|
|
}
|
|
if (shiftedPreview.length === 0) return false;
|
|
|
|
const others = gameRoot.getComponentsInChildren(PolygonCollider2D) ?? [];
|
|
for (const op of others) {
|
|
if (!op?.node?.isValid || !op.enabled) continue;
|
|
if (!this._isGameplayBlockColliderNode(op.node)) continue;
|
|
const ow = this._getPolygonColliderWorldPoints2D(op);
|
|
if (!this._polygonHasEnoughVerts(ow)) continue;
|
|
|
|
for (const sp of shiftedPreview) {
|
|
if (Intersection2D.polygonPolygon(sp, ow)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**创建预览:从模板克隆纯视觉节点;模板 _pieceNode 始终留在卡牌上 */
|
|
private _createPreview(): void {
|
|
if (!this._pieceNode || !this.chooseMask) return;
|
|
|
|
this.chooseMask.active = true;
|
|
this.cancleNode.active = true;
|
|
|
|
const preview = instantiate(this._pieceNode);
|
|
this.chooseMask.addChild(preview);
|
|
this._setLayerRecursively(preview, this.chooseMask.layer);
|
|
preview.active = true;
|
|
preview.angle = this._pieceNode.angle;
|
|
this._stripPreviewForDrag(preview);
|
|
this._previewDragBaseScale = this._pieceNode.scale.clone();
|
|
this._previewNode = preview;
|
|
|
|
if (this._pieceNodeCopyMove && this._pieceNodeCopyMove.isValid) {
|
|
this._pieceNodeCopyMove.destroy();
|
|
this._pieceNodeCopyMove = null;
|
|
}
|
|
if (!gg.game.isShowBlockDrop) {
|
|
this._pieceNodeCopyMove = instantiate(this._pieceNode);
|
|
this._stripPreviewForDrag(this._pieceNodeCopyMove);
|
|
this._pieceNodeCopyMove.angle = 0;
|
|
this.chooseMask.addChild(this._pieceNodeCopyMove);
|
|
const mapLayer = gg.game.CurentBattle.TetraMap?.node?.layer ?? this.chooseMask.layer;
|
|
this._setLayerRecursively(this._pieceNodeCopyMove, mapLayer);
|
|
this._pieceNodeCopyMove.active = true;
|
|
const fixedY = this._getWhiteLineFixedWorldYFor(this._pieceNodeCopyMove);
|
|
const startX = preview.worldPosition.x;
|
|
const startZ = this._pieceNodeCopyMove.worldPosition.z;
|
|
if (fixedY != null) {
|
|
this._pieceNodeCopyMove.setWorldPosition(new Vec3(startX, fixedY, startZ));
|
|
}
|
|
} else {
|
|
this._pieceNodeCopyMove = null;
|
|
}
|
|
|
|
this._createAlignPreview();
|
|
this._playPreviewBlockSpine(TetriBlockSpineState.Falling);
|
|
if (preview?.isValid && preview.parent === this.chooseMask) {
|
|
preview.setSiblingIndex(this.chooseMask.children.length - 1);
|
|
}
|
|
this._createGuideLine();
|
|
}
|
|
|
|
/** 预览二号:与主预览同形,位置为手指触点正上方(可配置像素偏移),仅对齐用 */
|
|
private _createAlignPreview(): void {
|
|
this._destroyAlignPreview();
|
|
if (!this.enableAlignPreview) return;
|
|
if (!this._pieceNode?.isValid || !this.chooseMask) return;
|
|
|
|
const n = instantiate(this._pieceNode);
|
|
this._stripAlignPreviewGameplay(n);
|
|
this.chooseMask.addChild(n);
|
|
this._setLayerRecursively(n, this.chooseMask.layer);
|
|
n.active = true;
|
|
n.angle = this._pieceNode.angle;
|
|
this._stripPreviewForDrag(n);
|
|
this._previewAlignNode = n;
|
|
}
|
|
|
|
/** 堆叠升高镜头拉远时与 tetriMap 一致,主预览与预览二号同比例缩小,贴近场上/白线副本观感 */
|
|
private _syncPreviewDragScaleForMapZoom(): void {
|
|
if (!this._previewDragBaseScale || !this._previewNode?.isValid) return;
|
|
const z = this._boundMap?.getVisualZoomScale?.() ?? 1;
|
|
const b = this._previewDragBaseScale;
|
|
const sx = b.x * z;
|
|
const sy = b.y * z;
|
|
const sz = b.z * z;
|
|
this._previewNode.setScale(sx, sy, sz);
|
|
if (this._previewAlignNode?.isValid) {
|
|
this._previewAlignNode.setScale(sx, sy, sz);
|
|
}
|
|
}
|
|
|
|
/** 结束拖拽:销毁预览克隆,模板方块始终留在卡牌上 */
|
|
private _tearDownDragPreview(): void {
|
|
this._destroyGuideLine();
|
|
this._destroyAlignPreview();
|
|
if (this._pieceNodeCopyMove?.isValid) {
|
|
this._pieceNodeCopyMove.destroy();
|
|
}
|
|
this._pieceNodeCopyMove = null;
|
|
if (this._previewNode?.isValid) {
|
|
this._previewNode.destroy();
|
|
}
|
|
this._previewNode = null;
|
|
this._previewDragBaseScale = null;
|
|
if (this._pieceNode?.isValid) {
|
|
playTetriBlockSpine(this._pieceNode, TetriBlockSpineState.Shop);
|
|
}
|
|
if (this.chooseMask) this.chooseMask.active = false;
|
|
if (this.cancleNode) this.cancleNode.active = false;
|
|
this._restoreCancelNodeScale();
|
|
this._clampRelAABB = null;
|
|
}
|
|
|
|
private _destroyPreview(): void {
|
|
this._tearDownDragPreview();
|
|
}
|
|
|
|
/**拖入取消区时做一次轻微放大反馈 */
|
|
private _playCancelHoverFeedback(): void {
|
|
if (!this.cancleNode?.isValid) return;
|
|
if (!this._cancelNodeBaseScale) {
|
|
this._cancelNodeBaseScale = this.cancleNode.scale.clone();
|
|
}
|
|
const base = this._cancelNodeBaseScale;
|
|
const bump = new Vec3(base.x * 1.08, base.y * 1.08, base.z);
|
|
Tween.stopAllByTarget(this.cancleNode);
|
|
this.cancleNode.setScale(base);
|
|
tween(this.cancleNode)
|
|
.to(0.08, { scale: bump }, { easing: 'quadOut' })
|
|
.to(0.12, { scale: base }, { easing: 'quadIn' })
|
|
.start();
|
|
}
|
|
|
|
private _restoreCancelNodeScale(): void {
|
|
if (!this.cancleNode?.isValid) return;
|
|
if (!this._cancelNodeBaseScale) return;
|
|
Tween.stopAllByTarget(this.cancleNode);
|
|
this.cancleNode.setScale(this._cancelNodeBaseScale);
|
|
}
|
|
|
|
/**获取节点(含子节点)在世界坐标的合并包围盒(基于 UITransform) */
|
|
private _getWorldRectByUI(node: Node): Rect | null {
|
|
const uts = node.getComponentsInChildren(UITransform);
|
|
let out: Rect | null = null;
|
|
for (const ut of uts) {
|
|
const r = ut.getBoundingBoxToWorld();
|
|
if (!out) {
|
|
out = new Rect(r.x, r.y, r.width, r.height);
|
|
} else {
|
|
const xMin = Math.min(out.xMin, r.xMin);
|
|
const yMin = Math.min(out.yMin, r.yMin);
|
|
const xMax = Math.max(out.xMax, r.xMax);
|
|
const yMax = Math.max(out.yMax, r.yMax);
|
|
out.x = xMin;
|
|
out.y = yMin;
|
|
out.width = xMax - xMin;
|
|
out.height = yMax - yMin;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**获取节点(含子节点)在世界坐标的合并包围盒(优先 UITransform,否则用 2D 物理 Collider) */
|
|
private _getWorldRect(node: Node): Rect | null {
|
|
const uiRect = this._getWorldRectByUI(node);
|
|
if (uiRect) return uiRect;
|
|
|
|
// 预览阶段我们会禁用 PolygonCollider2D,导致 worldAABB 可能不可用。
|
|
// 这里用 collider 的点位手算 AABB(即使 collider disabled 也能取到 points)。
|
|
const polyRect = this._getWorldRectByPolygonCollider(node);
|
|
if (polyRect) return polyRect;
|
|
|
|
const cols = node.getComponentsInChildren(Collider2D);
|
|
let out: Rect | null = null;
|
|
for (const col of cols) {
|
|
const r: any = (col as any).worldAABB;
|
|
if (!r) continue;
|
|
// worldAABB 是 math.Rect(字段是 x/y/width/height + xMin/xMax/yMin/yMax getter)
|
|
if (!out) {
|
|
out = new Rect(r.x, r.y, r.width, r.height);
|
|
} else {
|
|
const xMin = Math.min(out.xMin, r.xMin);
|
|
const yMin = Math.min(out.yMin, r.yMin);
|
|
const xMax = Math.max(out.xMax, r.xMax);
|
|
const yMax = Math.max(out.yMax, r.yMax);
|
|
out.x = xMin;
|
|
out.y = yMin;
|
|
out.width = xMax - xMin;
|
|
out.height = yMax - yMin;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**基于 PolygonCollider2D.points 手算世界 AABB(忽略 enabled 状态) */
|
|
private _getWorldRectByPolygonCollider(node: Node): Rect | null {
|
|
const polys = node.getComponentsInChildren(PolygonCollider2D);
|
|
if (!polys?.length) return null;
|
|
|
|
let minX = Number.POSITIVE_INFINITY;
|
|
let minY = Number.POSITIVE_INFINITY;
|
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
|
|
const wv = new Vec3();
|
|
const lv = new Vec3();
|
|
|
|
for (const poly of polys) {
|
|
const owner = poly.node;
|
|
const m = owner.getWorldMatrix();
|
|
const offset = (poly as any).offset as Vec2 | undefined;
|
|
const offX = offset?.x ?? 0;
|
|
const offY = offset?.y ?? 0;
|
|
const pts = poly.points ?? [];
|
|
for (const p of pts) {
|
|
lv.set(p.x + offX, p.y + offY, 0);
|
|
Vec3.transformMat4(wv, lv, m);
|
|
if (wv.x < minX) minX = wv.x;
|
|
if (wv.y < minY) minY = wv.y;
|
|
if (wv.x > maxX) maxX = wv.x;
|
|
if (wv.y > maxY) maxY = wv.y;
|
|
}
|
|
}
|
|
|
|
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
|
|
return null;
|
|
}
|
|
return new Rect(minX, minY, maxX - minX, maxY - minY);
|
|
}
|
|
|
|
/**按 PolygonCollider2D 点位计算“横向占用格子列数”(不使用包围盒 API) */
|
|
private _getGridColumnCountByPolygon(node: Node, cellWorldWidth: number): number {
|
|
const polys = node.getComponentsInChildren(PolygonCollider2D);
|
|
if (!polys?.length) return 1;
|
|
const cell = Math.max(1, cellWorldWidth);
|
|
let minX = Number.POSITIVE_INFINITY;
|
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
const wv = new Vec3();
|
|
const lv = new Vec3();
|
|
|
|
for (const poly of polys) {
|
|
const owner = poly.node;
|
|
const m = owner.getWorldMatrix();
|
|
const offset = (poly as any).offset as Vec2 | undefined;
|
|
const offX = offset?.x ?? 0;
|
|
const offY = offset?.y ?? 0;
|
|
const pts = poly.points ?? [];
|
|
for (const p of pts) {
|
|
lv.set(p.x + offX, p.y + offY, 0);
|
|
Vec3.transformMat4(wv, lv, m);
|
|
if (wv.x < minX) minX = wv.x;
|
|
if (wv.x > maxX) maxX = wv.x;
|
|
}
|
|
}
|
|
|
|
if (!Number.isFinite(minX) || !Number.isFinite(maxX)) return 1;
|
|
const cols = Math.round((maxX - minX) / cell);
|
|
return Math.max(1, cols);
|
|
}
|
|
|
|
/**把“预览整体大小”合并成 mask 本地 AABB(UITransform + PolygonCollider2D) */
|
|
private _getPreviewLocalAABBInMask(maskUT: UITransform, previewRoot: Node): { minX: number; minY: number; maxX: number; maxY: number } | null {
|
|
let minX = Number.POSITIVE_INFINITY;
|
|
let minY = Number.POSITIVE_INFINITY;
|
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
|
|
const tmp = new Vec3();
|
|
const toLocal = (w: Vec3) => {
|
|
maskUT.convertToNodeSpaceAR(w, tmp);
|
|
if (tmp.x < minX) minX = tmp.x;
|
|
if (tmp.y < minY) minY = tmp.y;
|
|
if (tmp.x > maxX) maxX = tmp.x;
|
|
if (tmp.y > maxY) maxY = tmp.y;
|
|
};
|
|
|
|
// 1) UITransform 的 worldRect 四角投影到 mask 本地
|
|
const uts = previewRoot.getComponentsInChildren(UITransform);
|
|
for (const ut of uts) {
|
|
const r = ut.getBoundingBoxToWorld();
|
|
toLocal(new Vec3(r.xMin, r.yMin, 0));
|
|
toLocal(new Vec3(r.xMin, r.yMax, 0));
|
|
toLocal(new Vec3(r.xMax, r.yMin, 0));
|
|
toLocal(new Vec3(r.xMax, r.yMax, 0));
|
|
}
|
|
|
|
// 2) PolygonCollider2D 的点(即使 disabled 也能取 points)投影到 mask 本地
|
|
const polys = previewRoot.getComponentsInChildren(PolygonCollider2D);
|
|
const wv = new Vec3();
|
|
const lv = new Vec3();
|
|
for (const poly of polys) {
|
|
const owner = poly.node;
|
|
const m = owner.getWorldMatrix();
|
|
const offset = (poly as any).offset as Vec2 | undefined;
|
|
const offX = offset?.x ?? 0;
|
|
const offY = offset?.y ?? 0;
|
|
const pts = poly.points ?? [];
|
|
for (const p of pts) {
|
|
lv.set(p.x + offX, p.y + offY, 0);
|
|
Vec3.transformMat4(wv, lv, m);
|
|
toLocal(wv);
|
|
}
|
|
}
|
|
|
|
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) return null;
|
|
return { minX, minY, maxX, maxY };
|
|
}
|
|
|
|
/**初始化位置钳制缓存:把当前预览整体 AABB 转成“相对 previewRoot.position 的偏移” */
|
|
private _ensureClampCache(maskUT: UITransform, previewRoot: Node): void {
|
|
if (this._clampRelAABB) return;
|
|
const aabb = this._getPreviewLocalAABBInMask(maskUT, previewRoot);
|
|
if (!aabb) return;
|
|
const p = previewRoot.position; // previewRoot 本身就在 chooseMask 下,本地坐标系一致
|
|
this._clampRelAABB = {
|
|
minX: aabb.minX - p.x,
|
|
minY: aabb.minY - p.y,
|
|
maxX: aabb.maxX - p.x,
|
|
maxY: aabb.maxY - p.y,
|
|
};
|
|
}
|
|
|
|
/**使用 position + 缓存 AABB 做钳制(不做每帧遍历/矩阵计算) */
|
|
private _clampPreviewByPosition(maskUT: UITransform, previewRoot: Node): void {
|
|
this._ensureClampCache(maskUT, previewRoot);
|
|
if (!this._clampRelAABB) return;
|
|
|
|
const maskW = maskUT.contentSize.width;
|
|
const maskH = maskUT.contentSize.height;
|
|
const ax = maskUT.anchorPoint.x;
|
|
const ay = maskUT.anchorPoint.y;
|
|
const maskLocalMinX = -maskW * ax;
|
|
const maskLocalMaxX = maskW * (1 - ax);
|
|
const maskLocalMinY = -maskH * ay;
|
|
const maskLocalMaxY = maskH * (1 - ay);
|
|
|
|
const pad = this.clampPaddingPx ?? 0;
|
|
const rel = this._clampRelAABB;
|
|
const p = previewRoot.position;
|
|
|
|
// 约束:p.x + rel.minX - pad >= maskMinX 且 p.x + rel.maxX + pad <= maskMaxX
|
|
const minPX = maskLocalMinX - rel.minX + pad;
|
|
const maxPX = maskLocalMaxX - rel.maxX - pad;
|
|
const minPY = maskLocalMinY - rel.minY + pad;
|
|
const maxPY = maskLocalMaxY - rel.maxY - pad;
|
|
|
|
const clampedX = Math.min(Math.max(p.x, minPX), maxPX);
|
|
const clampedY = Math.min(Math.max(p.y, minPY), maxPY);
|
|
if (clampedX !== p.x || clampedY !== p.y) {
|
|
previewRoot.setPosition(clampedX, clampedY, p.z);
|
|
}
|
|
}
|
|
|
|
/** 白线预览块:每帧按当前高度线重算 Y(堆叠升高时白线会平滑上移) */
|
|
private _syncWhiteLinePreviewPosition(): void {
|
|
if (gg.game.isShowBlockDrop) return;
|
|
if (!this._pieceNodeCopyMove?.isValid || !this._previewNode?.isValid) return;
|
|
const fixedY = this._getWhiteLineFixedWorldYFor(this._pieceNodeCopyMove);
|
|
if (fixedY == null) return;
|
|
const x = this._toGameWorldXFromUiWorldX(this._previewNode.worldPosition.x);
|
|
const z = this._pieceNodeCopyMove.worldPosition.z;
|
|
this._pieceNodeCopyMove.setWorldPosition(new Vec3(x, fixedY, z));
|
|
}
|
|
|
|
protected lateUpdate(): void {
|
|
if (!this._previewNode?.isValid) return;
|
|
this._syncPreviewDragScaleForMapZoom();
|
|
this._syncWhiteLinePreviewPosition();
|
|
if (this._guideLine?.active) {
|
|
this._updateGuideLine();
|
|
}
|
|
}
|
|
|
|
private _syncPreviewToTouch(event: EventTouch): void {
|
|
if (!this._previewNode || !this.chooseMask) return;
|
|
const uiPos = event.getUILocation();
|
|
const worldPos = new Vec3(uiPos.x, uiPos.y, 0);
|
|
|
|
// 先按触摸点移动,再做 chooseMask 区域边界钳制(保证预览整体不超过 mask)
|
|
const maskUT = this.chooseMask.getComponent(UITransform);
|
|
if (maskUT) {
|
|
const localPos = new Vec3();
|
|
maskUT.convertToNodeSpaceAR(worldPos, localPos);
|
|
this._previewNode.setPosition(localPos);
|
|
|
|
if (this._previewAlignNode?.isValid) {
|
|
const alignWorld = new Vec3(
|
|
uiPos.x,
|
|
uiPos.y + (this.alignPreviewOffsetAboveFingerPx ?? 0),
|
|
0,
|
|
);
|
|
const alignLocal = new Vec3();
|
|
maskUT.convertToNodeSpaceAR(alignWorld, alignLocal);
|
|
this._previewAlignNode.setPosition(alignLocal);
|
|
}
|
|
} else {
|
|
this._previewNode.setWorldPosition(worldPos);
|
|
if (this._previewAlignNode?.isValid) {
|
|
this._previewAlignNode.setWorldPosition(
|
|
new Vec3(
|
|
uiPos.x,
|
|
uiPos.y + (this.alignPreviewOffsetAboveFingerPx ?? 0),
|
|
this._previewAlignNode.worldPosition.z,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 边界保护:在 chooseMask 的本地坐标系中钳制(避免 UI 坐标/世界坐标不一致导致钳制失效)
|
|
// if (maskUT) {
|
|
// this._clampPreviewByPosition(maskUT, this._previewNode);
|
|
// }
|
|
|
|
this._syncPreviewDragScaleForMapZoom();
|
|
this._syncWhiteLinePreviewPosition();
|
|
|
|
let time = Date.now() - this._startTime;
|
|
const wasOverCancel = this._isOverCancel;
|
|
|
|
if (time < gg.game.CurentBattle.cancelTimeMs) {
|
|
this._isOverCancel = true;
|
|
} else {
|
|
this._isOverCancel = this._isOverCancleNode(event);
|
|
}
|
|
|
|
if (!wasOverCancel && this._isOverCancel) {
|
|
this._playCancelHoverFeedback();
|
|
}
|
|
|
|
if (this._guideLine) {
|
|
// 拖拽时始终显示引导区域(避免“没超过高线就完全不显示”看起来像没画出来)
|
|
this._guideLine.active = true;
|
|
this._updateGuideLine();
|
|
}
|
|
|
|
this._syncAlignPreviewVisibilityByHighLine();
|
|
}
|
|
|
|
private _createGuideLine(): void {
|
|
if (!this.chooseMask) return;
|
|
// guideLine 放在 chooseMask 的父节点(通常是 Canvas/UI 根),避免被 chooseMask 裁剪
|
|
this._guideLineParent = this.chooseMask.parent ?? this.chooseMask;
|
|
|
|
// 垂直覆盖使用超大范围,保证镜头缩放后仍能完整贯穿屏幕
|
|
this._chooseMaskTopY = 10000;
|
|
|
|
|
|
const node = new Node('GuideLine');
|
|
// 初始 layer 先跟 chooseMask,绘制时会再同步到目标预览层
|
|
node.layer = this.chooseMask.layer;
|
|
node.addComponent(UITransform);
|
|
node.addComponent(Graphics);
|
|
this._guideLineParent.addChild(node);
|
|
node.setSiblingIndex(this._guideLineParent.children.length - 1);
|
|
this._guideLine = node;
|
|
}
|
|
|
|
private _updateGuideLine(): void {
|
|
if (!this._guideLine || !this._previewNode) return;
|
|
const g = this._guideLine.getComponent(Graphics)!;
|
|
g.clear();
|
|
|
|
const parent = this._guideLineParent ?? this.chooseMask ?? this._previewNode.parent;
|
|
if (!parent) return;
|
|
|
|
// 引导线完全使用“白线副本”的世界包围盒(与真实落点同源)。
|
|
const targetNode = (this._pieceNodeCopyMove && this._pieceNodeCopyMove.isValid) ? this._pieceNodeCopyMove : this._previewNode;
|
|
// 与目标节点保持同一 layer/camera,避免镜头缩放后屏幕投影不一致
|
|
if (this._guideLine.layer !== targetNode.layer) {
|
|
this._guideLine.layer = targetNode.layer;
|
|
}
|
|
const centerWorld = targetNode.worldPosition;
|
|
// 宽度按“方块实际占用格子列数”计算(列数 * 固定格宽),不使用包围盒。
|
|
const cellWorldWidth = this._boundMap?.blockHeight ?? 36;
|
|
const rectCenterX = centerWorld.x;
|
|
const cols = this._getGridColumnCountByPolygon(targetNode, cellWorldWidth);
|
|
const snappedWorldWidth = cols * cellWorldWidth;
|
|
const halfW = snappedWorldWidth * 0.5;
|
|
|
|
let left = -halfW;
|
|
let right = halfW;
|
|
const leftWorldX = rectCenterX - halfW;
|
|
const rightWorldX = rectCenterX + halfW;
|
|
left = this._worldToLocal(parent, new Vec3(leftWorldX, centerWorld.y, 0)).x;
|
|
right = this._worldToLocal(parent, new Vec3(rightWorldX, centerWorld.y, 0)).x;
|
|
// 画线区域左右各 +1 像素(按 parent 的本地坐标系单位,即 UI 像素)
|
|
// left -= 1;
|
|
// right += 1;
|
|
|
|
const topY = this._chooseMaskTopY || (view.getDesignResolutionSize().height / 2);
|
|
// 新需求:线要很长,直接贯穿整个可视区域
|
|
const bottomY = -topY;
|
|
|
|
// 拖到取消区:引导区域变红
|
|
g.fillColor = this._isOverCancel ? new Color(255, 0, 0, 50) : new Color(0, 255, 0, 50);
|
|
g.rect(left, bottomY, right - left, topY - bottomY);
|
|
g.fill();
|
|
|
|
g.strokeColor = this._isOverCancel ? new Color(255, 0, 0, 200) : new Color(0, 255, 0, 200);
|
|
g.lineWidth = 3;
|
|
g.moveTo(left, topY);
|
|
g.lineTo(left, bottomY);
|
|
g.stroke();
|
|
g.moveTo(right, topY);
|
|
g.lineTo(right, bottomY);
|
|
g.stroke();
|
|
}
|
|
|
|
private _destroyGuideLine(): void {
|
|
if (this._guideLine) {
|
|
const line = this._guideLine;
|
|
this._guideLine = null;
|
|
line.destroy();
|
|
}
|
|
}
|
|
|
|
refreshCoinNum2(){
|
|
this.scheduleOnce(()=>{
|
|
if (gg.game.CurentBattle.BattleType === BattleType.OrangeWeaponMode_Dian) {
|
|
|
|
this._affordable = true
|
|
return;
|
|
}
|
|
let cost = this.node.getChildByName('cost').getComponent(Label)
|
|
if (gg.game.CurentBattle.CurentCoinNum < this.config.price) {
|
|
this._affordable = false
|
|
cost.color = new Color(255, 0, 0)
|
|
} else {
|
|
this._affordable = true
|
|
cost.color = new Color(255, 255, 255)
|
|
}
|
|
},1.5)
|
|
|
|
}
|
|
/**刷新金币*/
|
|
refreshCoinNum() {
|
|
if (!this.config) {
|
|
return
|
|
}
|
|
const lightning = gg.game.CurentBattle.BattleType === BattleType.OrangeWeaponMode_Dian;
|
|
const yinbi = this.node.getChildByName('jinbi');
|
|
const costNode = this.node.getChildByName('cost');
|
|
if (lightning) {
|
|
if (yinbi) yinbi.active = false;
|
|
//if (costNode) costNode.active = false;
|
|
const cost = costNode?.getComponent(Label);
|
|
if (costNode && cost) {
|
|
costNode.x = -30
|
|
cost.string = '免费';
|
|
}
|
|
this._affordable = true;
|
|
} else {
|
|
if (yinbi) yinbi.active = true;
|
|
if (costNode) costNode.active = true;
|
|
const cost = costNode?.getComponent(Label);
|
|
if (cost) {
|
|
cost.string = `${this.config.price}`;
|
|
if (gg.game.CurentBattle.CurentCoinNum < this.config.price) {
|
|
this._affordable = false;
|
|
cost.color = new Color(255, 0, 0);
|
|
} else {
|
|
this._affordable = true;
|
|
cost.color = new Color(255, 255, 255);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!this.wenhaoBtnNode){
|
|
return
|
|
}
|
|
|
|
if(gg.game.CurentBattle.ChapterId == 1 || gg.game.CurentBattle.ChapterId == 2){
|
|
if(this.config.type != TetriType.Tetris_Baseattk){
|
|
this.wenhaoBtnNode.active = true
|
|
this.tetriNameLabel.node.active = true
|
|
this.setNameType(this.config.type)
|
|
}else{
|
|
this.wenhaoBtnNode.active = false
|
|
this.tetriNameLabel.node.active = false
|
|
}
|
|
}else{
|
|
this.wenhaoBtnNode.active = false
|
|
this.tetriNameLabel.node.active = false
|
|
}
|
|
}
|
|
|
|
|
|
click_wenhaoBtn(){
|
|
//
|
|
let type = this.config.type
|
|
if(type == TetriType.Tetris_Basefloor){
|
|
gg.ui.openPanel(UIID.TetriDoc, { data: 3});
|
|
}else{
|
|
gg.ui.openPanel(UIID.TetriDoc, { data: 4 });
|
|
}
|
|
|
|
}
|
|
|
|
setNameType(type: TetriType){
|
|
|
|
// Tetris_Basefloor
|
|
// Tetris_BaseGold
|
|
// Tetris_Baseheal
|
|
// Tetris_Basevine
|
|
|
|
if(type == TetriType.Tetris_Basefloor){
|
|
this.tetriNameLabel.string = '悬浮'
|
|
}else if(type == TetriType.Tetris_BaseGold){
|
|
this.tetriNameLabel.string = '产币'
|
|
}else if(type == TetriType.Tetris_Baseheal){
|
|
this.tetriNameLabel.string = '回血'
|
|
}else if(type == TetriType.Tetris_Basevine){
|
|
this.tetriNameLabel.string = '绑定'
|
|
}
|
|
}
|
|
|
|
/** 同步主预览、白线副本、对齐预览的方块表情 Spine */
|
|
private _playPreviewBlockSpine(state: TetriBlockSpineState): void {
|
|
playTetriBlockSpine(this._pieceNode, state);
|
|
playTetriBlockSpine(this._previewNode, state);
|
|
playTetriBlockSpine(this._pieceNodeCopyMove, state);
|
|
playTetriBlockSpine(this._previewAlignNode, state);
|
|
}
|
|
}
|
|
|
|
|
|
|