rp_11009/assets/Game/Scripts/Roller.ts
2026-04-10 10:40:03 +08:00

539 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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], 1);
}
}
createNormalIcon(pos: number, iconIndex: number, test: number = 0): 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<Node>();
let exitTopY = this.getIconPosition(4).y - (this.row - 2) * this.iconHeight;
// 图标向上飞出,顶行先出(行间隔 0.03s,快速模式同时)
// 修改为图标向下飞出,底行先出,速度不变
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 - i) * 0.03;
tween(iconRef)
.delay(exitDelay)
.to(0.18, { 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.18 : 0.18 + (this.row - 1) * 0.03;
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;
// 普通模式行间隔 0.05s;快速/急停无间隔
let rowInterval = isFast ? 0 : 0.05;
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.15), 0.32);
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.08;
let bounceDown = 0.06;
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<Node>();
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<Node>();
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.05;
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.12), 0.28);
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.06 + bounceH1 * 0.002, 0.08), 0.12);
let bounceTime1Down = bounceTime1Up * 0.85;
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 = [];
}
}