import * as Phaser from "phaser"; /** ComboSmallUI 组件的配置选项 */ export interface ComboSmallUIConfig { uiScale?: number; barWidth?: number; barHeight?: number; progress?: number; comboCount?: number; depth?: number; } /** * 连击小型UI组件,显示 "COMBO X数字" 文本。 * 包含动画效果(呼吸缩放、脉冲等)。 */ export class ComboSmallUI extends Phaser.GameObjects.Container { private readonly uiScale: number; private readonly textPad: number; private readonly textPadRight: number; private readonly textGap: number; private readonly strokeThickness: number; private progress = 0; private readonly comboText: Phaser.GameObjects.Text; private readonly xText: Phaser.GameObjects.Text; private xTween: Phaser.Tweens.Tween | undefined; constructor( scene: Phaser.Scene, x: number, y: number, config: ComboSmallUIConfig = {}, ) { super(scene, x, y); scene.add.existing(this); this.uiScale = config.uiScale ?? 1; const fontSize = 40 * this.uiScale; const strokeThickness = 8 * this.uiScale; this.strokeThickness = strokeThickness; const pad = Math.ceil(strokeThickness * 3 + fontSize * 0.1); const padRight = Math.ceil(strokeThickness * 6 + fontSize * 0.4); this.textPad = pad; this.textPadRight = padRight; // 创建 "COMBO" 文本 const comboText = scene.add.text(0, 0, "COMBO", { fontFamily: "Poppins", fontSize: `${fontSize + 6}px`, color: "#007AFF", // 蓝色主色调 fontStyle: "bold", }); // 描边效果 comboText.setStroke("#FFFFFF", strokeThickness + 3); // 白色描边 comboText.setShadow(0, 0, "#007AFF", 8, true, false); // 蓝色外发光 // 轻微倾斜角度 comboText.angle = -3; comboText.setPadding( this.textPad + 4, this.textPad, this.textPadRight + 4, this.textPad, ); comboText.setOrigin(1, 0.5); // COMBO 文本呼吸动画 scene.tweens.add({ targets: comboText, scaleX: 1.1, scaleY: 1.1, angle: 3, duration: 300, yoyo: true, repeat: -1, ease: "Sine.easeInOut", }); // 创建 "X数字" 文本 const xText = scene.add.text(0, 0, "X1", { fontFamily: "Poppins", fontSize: `${fontSize}px`, color: "#007AFF", // 蓝色主色调 fontStyle: "bold italic", }); xText.setResolution(2); xText.setStroke("#FFFFFF", strokeThickness + 3); // 白色描边 xText.setShadow(0, 0, "#007AFF", 4, true, true); // 蓝色外发光 xText.setPadding( this.textPad, this.textPad, this.textPadRight, this.textPad, ); xText.setOrigin(0, 0.5); this.textGap = 2 * this.uiScale; this.comboText = comboText; this.xText = xText; this.add(comboText); this.add(xText); this.layoutComboText(); this.recomputeSize(); if (typeof config.comboCount === "number") { this.setCombo(config.comboCount); } if (typeof config.progress === "number") { this.setProgress(config.progress); } if (typeof config.depth === "number") { this.setDepth(config.depth); } } /** * 设置连击进度(控制整体透明度)。 * @param progress 进度值(0~1) */ public setProgress(progress: number): void { this.progress = Phaser.Math.Clamp(progress, 0, 1); const minAlpha = 0.25; const alpha = minAlpha + (1 - minAlpha) * this.progress; this.setAlpha(alpha); } /** * 设置连击计数并更新显示文本。 * @param count 连击次数 */ public setCombo(count: number): void { const safeCount = Math.max(1, Math.floor(count)); this.xText.setText(`X${safeCount}`); this.xText.setPadding( this.textPad, this.textPad, this.textPadRight, this.textPad, ); this.layoutComboText(); this.recomputeSize(); } /** 获取 COMBO 文字图形的本地左边界X坐标。 */ public getComboGlyphLeftLocal(): number { const comboW = this.comboText.displayWidth || 0; const comboPadL = this.textPad * this.comboText.scaleX; return this.comboText.x - comboW + comboPadL; } /** 获取文字图形的本地顶部Y坐标。 */ public getTextGlyphTopLocal(): number { const topFor = (t: Phaser.GameObjects.Text) => { const padTop = this.textPad * t.scaleY; const h = t.displayHeight || 0; return t.y - h / 2 + padTop; }; return Math.min(topFor(this.comboText), topFor(this.xText)); } /** 播放连击脉冲缩放动画。 */ public playComboPulse(): void { const scene = this.scene; if (!scene) { return; } if (this.xTween) { this.xTween.stop(); scene.tweens.remove(this.xTween); this.xTween = undefined; } this.xText.setScale(1); this.xTween = scene.tweens.add({ targets: this.xText, scale: 1.18, duration: 140, yoyo: true, ease: "Sine.easeOut", onComplete: () => { this.xText.setScale(1); this.xTween = undefined; }, }); } /** 重新布局 COMBO 和 X数字 文本的水平位置。 */ private layoutComboText(): void { const comboW = this.comboText.displayWidth || 0; const xW = this.xText.displayWidth || 0; const comboPadL = this.textPad * this.comboText.scaleX; const comboPadR = this.textPadRight * this.comboText.scaleX; const xPadL = this.textPad * this.xText.scaleX; const xPadR = this.textPadRight * this.xText.scaleX; const comboGlyphW = Math.max(0, comboW - comboPadL - comboPadR); const xGlyphW = Math.max(0, xW - xPadL - xPadR); const totalW = comboGlyphW + this.textGap + xGlyphW; const startX = -totalW / 2; const seamX = startX + comboGlyphW; this.comboText.setPosition(seamX + comboPadR, 0); this.xText.setPosition(seamX + this.textGap - xPadL, 0); } /** 重新计算容器的整体尺寸。 */ private recomputeSize(): void { const strokePadding = this.strokeThickness * 2; const textGroupW = (this.comboText.displayWidth || 0) + this.textGap + (this.xText.displayWidth || 0); const totalWidth = textGroupW + strokePadding; const totalHeight = Math.max(this.comboText.displayHeight, this.xText.displayHeight) + strokePadding; this.setSize(totalWidth, totalHeight); } }