import { _decorator, Component, Node, UITransform, Vec2, v2, Vec3, v3, tween, Tween } from 'cc'; import { IconFactory } from './IconFactory'; import { Icon } from './Icon'; import { ICON_HEIGHT, ICON_SERVER_MAP, ICON_WIDTH, ROLLER_EVENT } from './Define'; import { EDITOR } from "cc/env"; import { AudioManager } from '../../Main/Scripts/managers/AudioManager'; let { ccclass, property, executeInEditMode } = _decorator; export enum ROLLER_STATE { ACCELERATE = 1, UNIFORM = 2, DECELERATE = 3, LAST_PAGE_CREATE = 4, BOUNCE = 5, STOP = 6, } export class Info { icons: Node[] = []; iconRule: number[] = []; noBounce: boolean = false; receiveStopData: boolean = false; /** 退场动画已完成,可以触发掉落 */ speedDataComplete: boolean = false; state: ROLLER_STATE = ROLLER_STATE.STOP; isFastSpin: boolean = false; resetLxInfo() { this.receiveStopData = false; this.speedDataComplete = false; this.noBounce = false; } } @ccclass('Roller') @executeInEditMode export class Roller extends Component { @property({ tooltip: '图标宽度' }) iconWidth: number = ICON_WIDTH; @property({ tooltip: '图标高度' }) iconHeight: number = ICON_HEIGHT; @property({ type: IconFactory, tooltip: '图标工厂' }) iconFactory: IconFactory = null; @property({ tooltip: '行数' }) row: number = 5; protected _rollerId: number = 0; protected _info: Info = new Info(); protected _panData: number[] = []; private grid: (Node | null)[] = []; private _freeMulMap: { [relPos: number]: number } = {}; /** 正在退场(向上飞出)的图标,用于急停时强制回收 */ private _exitingIcons: Node[] = []; /** 待执行掉落时的额外列延迟(秒) */ private _pendingFallDelay: number = 0; @property private _format = false; @property({ tooltip: '本地格式化' }) get format(): boolean { return this._format; } set format(b: boolean) { this._format = b; this.resizeNodeSize(); if (!this.iconFactory) { console.error('IconFactory没有设置'); return; } this.node.removeAllChildren(); this._info.icons = []; this.grid = []; for (let i = 0; i < this.row; i++) { this.grid[i] = null; let iconIndex = Math.floor(Math.random() * this.iconFactory.getIconNum()); this.createNormalIcon(i, iconIndex); } } static create( id: number, row: number, iconWidth: number, iconHeight: number, iconFactory: IconFactory, anchor: Vec2 = v2(0.5, 0.5) ): Roller { let rollerNode = new Node(`Roller${id}`); rollerNode.addComponent(UITransform); let roller = rollerNode.addComponent(Roller); roller._rollerId = id; roller.row = row; roller.iconWidth = iconWidth; roller.iconHeight = iconHeight; roller.iconFactory = iconFactory; rollerNode.getComponent(UITransform).setAnchorPoint(anchor); roller.resizeNodeSize(); roller.initRoller(id); return roller; } resizeNodeSize() { let totalHeight = this.iconHeight * this.row; this.node.getComponent(UITransform).setContentSize(this.iconWidth, totalHeight); } getIconPosition(pos: number, size: number = 1): Vec3 { let contentHeight = this.node.getComponent(UITransform).height; let anchorY = this.node.getComponent(UITransform).anchorY; let topY = contentHeight * (1 - anchorY); let firstCenterY = topY - this.iconHeight / 2; let centerY = firstCenterY - pos * this.iconHeight; let finalY = size > 1 ? centerY - (size - 1) * this.iconHeight / 2 : centerY; return v3(0, finalY, 0); } getIconNode(pos: number): Node { return this.grid[pos]; } get rollerId(): number { return this._rollerId; } initRoller(rollerId: number) { this._rollerId = rollerId; this.node.removeAllChildren(); this.grid = []; for (let i = 0; i < this.row; i++) { this.grid[i] = null; } } initRollerWithIcon(id: number, panData: number[]) { this.iconFactory.init(); this.initRoller(id); this._panData = panData; this.createInitIcons(panData); } createInitIcons(data: number[]) { this.grid = []; for (let i = 0; i < this.row; i++) { this.grid[i] = null; } for (let i = 0; i < this.row; i++) { this.createNormalIcon(i, data[i]); } } createNormalIcon(pos: number, iconIndex: number): Node { let iconNode = this.iconFactory.icfactoryCreateIcon(iconIndex); let icon = iconNode.getComponent(Icon); icon.initIcon(iconIndex, 1, this._rollerId); if (iconIndex === 10 && this._freeMulMap[pos] != null) { icon.setMulti(this._freeMulMap[pos]); icon.multiNode.active = true; } let position = this.getIconPosition(pos, 1); this.node.addChild(iconNode); iconNode.setPosition(position); if (iconIndex === 0) { icon.playScatterIdleSpine(true); } else { icon.playIdleSpine(false); } this.grid[pos] = iconNode; return iconNode; } setFreeMulMap(map: { [relPos: number]: number }) { this._freeMulMap = map || {}; } setRollerIconRule(rollerIconRule: number[]) { this._info.iconRule = rollerIconRule; } setFastSpin(isFastSpin: boolean) { if (this._info.isFastSpin === isFastSpin) return; this._info.isFastSpin = isFastSpin; } setNoBounce(noBounce: boolean) { this._info.noBounce = noBounce; } resetInfo() { this._panData = []; this._pendingFallDelay = 0; this._exitingIcons = []; this._info.resetLxInfo(); } isScroll(): boolean { return this._info.state !== ROLLER_STATE.STOP; } // ─── 旋转主流程 ────────────────────────────────────────────────────────── /** 开始旋转:现有图标向上飞出(退场动画) */ startScroll() { if (this._info.state != ROLLER_STATE.STOP) return; AudioManager.instance.playSFX('Roller_Start'); this._exitingIcons = []; let seen = new Set(); let exitTopY = this.getIconPosition(4).y - (this.row - 2) * this.iconHeight; // 开转时旧图标快速退场:底行先出,普通模式做轻微错峰 for (let i = this.row - 1; i >= 0; i--) { let icon = this.grid[i]; if (icon && !seen.has(icon)) { seen.add(icon); this._exitingIcons.push(icon); let iconRef = icon; let exitDelay = this._info.isFastSpin ? 0 : (this.row - 1 - i) * 0.02; tween(iconRef) .delay(exitDelay) .to(0.14, { position: v3(0, exitTopY, 0) }, { easing: 'quadIn' }) .call(() => { let idx = this._exitingIcons.indexOf(iconRef); if (idx !== -1) this._exitingIcons.splice(idx, 1); if (iconRef && iconRef.isValid) this.iconFactory.recycleIcon(iconRef); }) .start(); } this.grid[i] = null; } // 回收多余动态图标 this._info.icons.forEach(icon => { if (icon && icon.isValid) this.iconFactory.recycleIcon(icon); }); this._info.icons = []; this._info.state = ROLLER_STATE.ACCELERATE; // 退场结束后标记"可掉落" let exitDuration = this._info.isFastSpin ? 0.14 : 0.14 + (this.row - 1) * 0.02; this.scheduleOnce(() => { if (this._info.state !== ROLLER_STATE.ACCELERATE) return; this._info.state = ROLLER_STATE.UNIFORM; this._info.speedDataComplete = true; if (this._info.receiveStopData) { this.doFallAnimation(this._pendingFallDelay); } }, exitDuration); } /** 接收停止数据;退场完毕后立即触发掉落 */ stopScroll(panData: number[], extraDelay: number = 0) { this._panData = panData; this._info.receiveStopData = true; this._pendingFallDelay = extraDelay; if (this._info.speedDataComplete) { this.doFallAnimation(extraDelay); } // 若退场未完成,startScroll 回调会用 _pendingFallDelay 自动触发 } /** 核心:图标从顶部掉落到目标位,含弹跳 */ doFallAnimation(extraDelay: number = 0) { if (this._info.state === ROLLER_STATE.LAST_PAGE_CREATE || this._info.state === ROLLER_STATE.BOUNCE || this._info.state === ROLLER_STATE.STOP) return; this._info.state = ROLLER_STATE.LAST_PAGE_CREATE; this.node.emit(ROLLER_EVENT.LAST_PAGE_CREATE, this._rollerId); let topY = this.getIconPosition(0, 1).y; let isFast = this._info.isFastSpin; // 普通模式行间隔更紧凑;快速/急停无间隔 let rowInterval = isFast ? 0 : 0.035; let maxTime = 0; for (let i = 0; i < this.row; i++) { if (i >= this._panData.length) continue; let panValue = this._panData[i]; // 清理该格已有图标 if (this.grid[i]) { Tween.stopAllByTarget(this.grid[i]); this.iconFactory.recycleIcon(this.grid[i]); this.grid[i] = null; } let iconNode = this.iconFactory.icfactoryCreateIcon(panValue); let iconComp = iconNode.getComponent(Icon); iconComp.initIcon(panValue, 1, this._rollerId); if (panValue === 10 && this._freeMulMap[i] != null) { iconComp.setMulti(this._freeMulMap[i]); iconComp.multiNode.active = true; } // 起始位置:可见区顶部上方 let startY = topY + (i + 2) * this.iconHeight; // 底行最高,顶行最低 let targetPos = this.getIconPosition(i, 1); iconNode.setPosition(0, startY, 0); this.node.addChild(iconNode); this.grid[i] = iconNode; // 列延迟 + 行内延迟 let rowDelay = extraDelay + (this.row - 1 - i) * rowInterval; // 底行先落,顶行后落 let distance = Math.abs(startY - targetPos.y); let fallTime = Math.min(Math.max(Math.sqrt(distance) / 55, 0.12), 0.24); let iconRef = iconNode; if (this._info.noBounce) { tween(iconRef) .delay(rowDelay) .to(fallTime, { position: targetPos }, { easing: 'quadIn' }) .call(() => { let iconCompRef = iconRef.getComponent(Icon); if (iconCompRef?.index === 0) { iconCompRef.playScatterIdleSpine(true); } else { iconCompRef?.playIdleSpine(false); } }) .start(); maxTime = Math.max(maxTime, rowDelay + fallTime); } else { let bounceH = Math.min(Math.max(distance * 0.08, 6), 18); let bounceUp = 0.05; let bounceDown = 0.04; let bouncePos = targetPos.clone().add(v3(0, bounceH, 0)); tween(iconRef) .delay(rowDelay) .to(fallTime, { position: targetPos }, { easing: 'quadIn' }) .call(() => { let iconCompRef = iconRef.getComponent(Icon); if (iconCompRef?.index === 0) { iconCompRef.playScatterIdleSpine(true); } else { iconCompRef?.playIdleSpine(false); } }) .to(bounceUp, { position: bouncePos }, { easing: 'quadOut' }) .to(bounceDown, { position: targetPos }, { easing: 'quadIn' }) .start(); maxTime = Math.max(maxTime, rowDelay + fallTime + bounceUp + bounceDown); } } // 全部落定后:发出 BOUNCE → STOP 事件 this.scheduleOnce(() => { this._info.state = ROLLER_STATE.BOUNCE; this.node.emit(ROLLER_EVENT.ROLLER_BOUNCE, this._rollerId); this.scheduleOnce(() => { this._info.state = ROLLER_STATE.STOP; this.node.emit(ROLLER_EVENT.ROLLER_STOP, this._rollerId); }, 0.05); }, maxTime + 0.05); } // ─── 消除与重建逻辑(保持不变)────────────────────────────────────────── getNodeMsgFromPos(pos: number): { node: Node, start: number, height: number } | null { let iconNode = this.grid[pos]; if (!iconNode) return null; let start = pos; while (start - 1 >= 0 && this.grid[start - 1] === iconNode) start--; let e = pos; while (e + 1 < this.row && this.grid[e + 1] === iconNode) e++; let height = e - start + 1; return { node: iconNode, start: start, height: height }; } deleteIconNode(positions: number[]) { if (positions.length === 0) { this.node.emit(ROLLER_EVENT.ICON_DELETED, this._rollerId); return; } let processed = new Set(); for (let pos of positions) { let msg = this.getNodeMsgFromPos(pos); if (!msg) continue; if (processed.has(msg.node)) continue; for (let i = 0; i < msg.height; i++) { let p = msg.start + i; if (p >= 0 && p < this.row && this.grid[p] === msg.node) { this.grid[p] = null; } } processed.add(msg.node); msg.node.getComponent(Icon).playDeleteSpine(); this.scheduleOnce(() => { this.iconFactory.recycleIcon(msg.node); }, 0.8); this.scheduleOnce(() => { for (let i = 0; i < msg.height; i++) { this.node.emit(ROLLER_EVENT.ICON_DELETED, this._rollerId); } }, 0.8 + 0.1); } } _newCreateIconNode: Node[] = []; createNewIconTop(createDatas: any[]) { this._newCreateIconNode = []; if (createDatas === null || createDatas.length === 0) { this.node.emit(ROLLER_EVENT.ICON_CREATE, this._rollerId); return; } let topY = this.getIconPosition(0, 1).y; let allHeight = createDatas.length; let curHeight = allHeight; for (let i = 0; i < createDatas.length; i++) { let data = createDatas[i]; let iconIndex = data; let lHeight = 1; let startPos = i; curHeight = allHeight - startPos; let icon = this.iconFactory.icfactoryCreateIcon(iconIndex); let iconComp2 = icon.getComponent(Icon); iconComp2.initIcon(iconIndex, lHeight, this._rollerId); if (iconIndex === 10 && this._freeMulMap[i] != null) { iconComp2.setMulti(this._freeMulMap[i]); iconComp2.multiNode.active = true; } let bornY = topY + curHeight * this.iconHeight - (lHeight - 1) * this.iconHeight / 2; icon.setPosition(0, bornY, 0); this._newCreateIconNode.push(icon); this.node.insertChild(icon, 0); } this.node.emit(ROLLER_EVENT.ICON_CREATE, this._rollerId); } iconFallDown() { let allIcons = []; let seen = new Set(); for (let i = 0; i < this.row; i++) { let iconNode = this.grid[i]; if (iconNode && !seen.has(iconNode)) { seen.add(iconNode); allIcons.push(iconNode); } this.grid[i] = null; } allIcons = [...this._newCreateIconNode, ...allIcons]; allIcons.sort((a, b) => b.position.y - a.position.y); let interval = 0.03; let targetPos = this.row; let dealyCount = 0; let timeEnd = 0; let startDelay = 0; for (let i = allIcons.length - 1; i >= 0; i--) { let iconNode = allIcons[i]; startDelay = dealyCount * interval; let iconLheight = iconNode.getComponent(Icon).getHeight(); targetPos -= iconLheight; let targetPosition = this.getIconPosition(targetPos, iconLheight); Tween.stopAllByTarget(iconNode); if (iconNode.position.y !== targetPosition.y) { let distance = Math.abs(iconNode.position.y - targetPosition.y); let fallTime = Math.min(Math.max(Math.sqrt(distance) / 55, 0.10), 0.20); let bounceH1 = Math.min(Math.max(distance * 0.18, 12), 26); let bounceH2 = Math.floor(bounceH1 * 0.5); let bounceTime1Up = Math.min(Math.max(0.05 + bounceH1 * 0.0015, 0.05), 0.08); let bounceTime1Down = bounceTime1Up * 0.8; let bouncePos2 = targetPosition.clone().add(v3(0, bounceH2, 0)); dealyCount++; tween(iconNode) .delay(startDelay) .to(fallTime, { position: targetPosition }, { easing: 'quadIn' }) .call(() => { let iconCompRef = iconNode.getComponent(Icon); if (iconCompRef?.index === 0) { iconCompRef.playScatterIdleSpine(true); } else { iconCompRef?.playIdleSpine(false); } }) .to(bounceTime1Up, { position: bouncePos2 }, { easing: 'quadOut' }) .to(bounceTime1Down, { position: targetPosition }, { easing: 'quadIn' }) .start(); let totalBounce = bounceTime1Up + bounceTime1Down; timeEnd = Math.max(timeEnd, startDelay + fallTime + totalBounce); } for (let j = 0; j < iconLheight; j++) { this.grid[targetPos + j] = iconNode; } } this.scheduleOnce(() => { this.node.emit(ROLLER_EVENT.ICON_FALLEN, this._rollerId); }, timeEnd + 0.1); } getIconWorldPosition(pos: number): Vec3 { let node = this.grid[pos]; if (!node) return null; return this.node.getComponent(UITransform).convertToWorldSpaceAR(node.position); } getState(): ROLLER_STATE { return this._info.state; } onDestroy() { this.grid = []; } }