import * as Phaser from "phaser"; import { ComboSmallUI } from "./ComboSmallUI"; import { GAME_TOP_UI_BAR_HEIGHT } from "./GameTopUI"; import { computeUiLayout } from "../../utils/UiLayout"; /** 连续成功操作计为连击链的时间窗口(毫秒)。 */ const COMBO_CHAIN_WINDOW_MS = 1400; export class ComboManager { private scene: Phaser.Scene; // ── 连击状态 ── private comboClickCount = 0; private comboLastSuccessAtMs = 0; private comboUi: ComboSmallUI | undefined; private comboUiLastCount = 0; private comboPraiseText: Phaser.GameObjects.Text | undefined; private comboPraiseTween: Phaser.Tweens.Tween | null = null; private comboPraiseTimer: Phaser.Time.TimerEvent | null = null; private comboPraiseLastShownComboCount = 0; constructor(scene: Phaser.Scene) { this.scene = scene; } // ── 公共 getter / setter ────────────────────────────── /** 获取当前连击点击次数 */ get clickCount(): number { return this.comboClickCount; } /** 设置连击点击次数 */ set clickCount(v: number) { this.comboClickCount = v; } /** 获取上次成功操作的时间戳(毫秒) */ get lastSuccessAtMs(): number { return this.comboLastSuccessAtMs; } /** 设置上次成功操作的时间戳(毫秒) */ set lastSuccessAtMs(v: number) { this.comboLastSuccessAtMs = v; } /** 获取连击链时间窗口(毫秒) */ get chainWindowMs(): number { return COMBO_CHAIN_WINDOW_MS; } // ── 生命周期 ────────────────────────────────────────────── /** * 创建 ComboSmallUI 组件。在场景 create() 中调用一次。 * @param layout 包含宽度和UI缩放比例的布局信息 */ createUi(layout: { width: number; uiScale: number }): void { this.comboUi = new ComboSmallUI(this.scene, layout.width * 0.5, 0, { uiScale: layout.uiScale, progress: 1, comboCount: 1, depth: 1600, }); this.comboUi.setScrollFactor(0); this.comboUi.setVisible(false); this.refreshComboUiPlacement(); // 定期刷新倒计时 this.scene.time.addEvent({ delay: 33, loop: true, callback: () => this.refreshComboUiCountdown(), }); } /** * 重置所有连击状态。在每关开始时调用。 */ resetState(): void { this.comboClickCount = 0; this.comboLastSuccessAtMs = 0; this.comboUiLastCount = 0; this.comboPraiseLastShownComboCount = 0; this.clearComboPraiseText(); } // ── 原先在 Game 上的方法 ───────────────────────────── /** 成功消除路径后处理连击UI显示。 */ handleComboUiOnSuccess(): void { const comboUi = this.comboUi; if (!comboUi) { return; } if (this.comboClickCount < 3) { this.hideComboUi(); return; } if (!comboUi.visible) { comboUi.setVisible(true); this.refreshComboUiPlacement(); } comboUi.setProgress(1); if (this.comboUiLastCount !== this.comboClickCount) { comboUi.setCombo(this.comboClickCount); comboUi.playComboPulse(); this.comboUiLastCount = this.comboClickCount; this.refreshComboUiPlacement(); } } /** 刷新连击UI倒计时进度条。 */ refreshComboUiCountdown(): void { const comboUi = this.comboUi; if (!comboUi || !comboUi.visible) { return; } if (this.comboClickCount < 3 || this.comboLastSuccessAtMs <= 0) { this.hideComboUi(); return; } const now = this.scene.time.now; const elapsed = now - this.comboLastSuccessAtMs; const remain = COMBO_CHAIN_WINDOW_MS - elapsed; if (remain <= 0) { this.comboClickCount = 0; this.comboLastSuccessAtMs = 0; this.comboPraiseLastShownComboCount = 0; this.clearComboPraiseText(); this.hideComboUi(); return; } comboUi.setProgress(remain / COMBO_CHAIN_WINDOW_MS); } /** 隐藏连击UI组件。 */ hideComboUi(): void { if (this.comboUi) { this.comboUi.setVisible(false); this.comboUi.setProgress(0); this.comboUi.setScale(1); } this.comboUiLastCount = 0; } /** 刷新连击UI组件的位置。 */ refreshComboUiPlacement(): void { const comboUi = this.comboUi; if (!comboUi) { return; } const layout = computeUiLayout(this.scene); const { width, height, uiScale, vScale } = layout; const topUiBottom = GAME_TOP_UI_BAR_HEIGHT * vScale; const targetComboLeft = 10 * uiScale; const targetTextTop = topUiBottom + 10 * vScale; const marginRight = 12 * uiScale; comboUi.setScale(1); comboUi.setPosition(0, 0); const glyphLeftLocal = comboUi.getComboGlyphLeftLocal(); const glyphTopLocal = comboUi.getTextGlyphTopLocal(); const baseBounds = comboUi.getBounds(); const localRight = baseBounds.right; const distToRight = localRight - glyphLeftLocal; const availableW = Math.max(1, width - marginRight - targetComboLeft); const fitScale = distToRight > 0 ? Math.min(1, availableW / distToRight) : 1; comboUi.setScale(fitScale); const x = targetComboLeft - glyphLeftLocal * fitScale; const y = targetTextTop - glyphTopLocal * fitScale; comboUi.setPosition(x, y); } /** 清除连击表扬文本及其相关动画。 */ clearComboPraiseText(): void { if (this.comboPraiseTween) { this.comboPraiseTween.stop(); this.comboPraiseTween = null; } if (this.comboPraiseTimer) { this.comboPraiseTimer.remove(false); this.comboPraiseTimer = null; } if (this.comboPraiseText) { this.scene.tweens.killTweensOf(this.comboPraiseText); this.comboPraiseText.destroy(); this.comboPraiseText = undefined; } } /** * 根据连击次数获取对应的表扬文本。 * @param comboCount 连击次数 * @returns 表扬文本,如果不需要显示则返回 null */ getComboPraiseMessage(comboCount: number): string | null { const c = Math.max(0, Math.floor(comboCount || 0)); if (c < 3) { return null; } if (c >= 15) { if (c === 15 || (c > 15 && (c - 15) % 3 === 0)) { const tiers = [ "LEGENDARY!", "GODLIKE!", "UNREAL!", "PHENOMENAL!", "SUPREME!", ]; const tierIndex = Math.floor((c - 15) / 3) % tiers.length; return tiers[tierIndex] ?? "LEGENDARY!"; } return null; } switch (c) { case 3: return "GOOD!"; case 4: return "NICE!"; case 5: return "GREAT!"; case 6: return "AWESOME!"; case 7: return "AMAZING!"; case 8: return "UNSTOPPABLE!"; case 10: return "INCREDIBLE!"; case 12: return "INSANE!"; default: return null; } } /** * 在点击位置(或屏幕中央)显示连击表扬文本。 * @param comboCount 当前连击次数 * @param lastTapScreenX 上次点击的屏幕X坐标 * @param lastTapScreenY 上次点击的屏幕Y坐标 * @param hasLastTapScreenPos 点击位置是否有效 */ maybeShowComboPraiseText( comboCount: number, lastTapScreenX: number, lastTapScreenY: number, hasLastTapScreenPos: boolean, ): void { const message = this.getComboPraiseMessage(comboCount); if (!message) { return; } const c = Math.max(0, Math.floor(comboCount || 0)); if (c === this.comboPraiseLastShownComboCount) { return; } this.comboPraiseLastShownComboCount = c; this.clearComboPraiseText(); const layout = computeUiLayout(this.scene); const { centerX, uiScale, vScale } = layout; const baseX = hasLastTapScreenPos ? lastTapScreenX : centerX; const baseY = hasLastTapScreenPos ? lastTapScreenY - 30 * uiScale : GAME_TOP_UI_BAR_HEIGHT * vScale + 180 * vScale; const hint = this.scene.add.text(baseX, baseY + 14 * uiScale, message, { fontFamily: "Poppins, Arial, sans-serif", fontSize: `${26 * uiScale}px`, color: "#1184ff", fontStyle: "bold", align: "center", }); hint.setOrigin(0.5, 0.5); hint.setScale(0.6); hint.setScrollFactor(0); hint.setDepth(1700); hint.setAlpha(0.98); // 简洁描边 hint.setStroke("#FFFFFF", 2 * uiScale); // 蓝色发光效果 hint.setShadow(0, 0, "#1184ff", 2 * uiScale, true, false); this.comboPraiseText = hint; /** 清理表扬文本 */ const cleanup = () => { if (this.comboPraiseText === hint) { this.comboPraiseText = undefined; } this.comboPraiseTween = null; if (this.comboPraiseTimer) { this.comboPraiseTimer.remove(false); this.comboPraiseTimer = null; } hint.destroy(); }; const finalY = baseY; const popStartY = baseY + 10 * uiScale; hint.y = popStartY; // 弹出动画:从下方弹出 -> 缩放回弹 -> 延迟后淡出上移 this.comboPraiseTween = this.scene.tweens.add({ targets: hint, y: finalY, alpha: 1, scale: 1.12, duration: 90, ease: "Back.Out", onComplete: () => { this.comboPraiseTween = this.scene.tweens.add({ targets: hint, scale: 1, duration: 60, ease: "Sine.Out", onComplete: () => { this.comboPraiseTween = null; this.comboPraiseTimer = this.scene.time.delayedCall(260, () => { this.comboPraiseTimer = null; this.comboPraiseTween = this.scene.tweens.add({ targets: hint, y: finalY - 70 * uiScale, alpha: 0, duration: 360, ease: "Cubic.In", onComplete: cleanup, }); }); }, }); }, }); } }