All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m19s
537 lines
19 KiB
TypeScript
537 lines
19 KiB
TypeScript
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(isFastSpin) {
|
||
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;
|
||
|
||
// 开转时旧图标快速退场:底行先出,普通模式做轻微错峰
|
||
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 = 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 = 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<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.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 = [];
|
||
}
|
||
}
|