Spaces:
Running on Zero
Running on Zero
| /** | |
| * ============================================================================ | |
| * STATUS BAR - HP / MP / Score / Progress bar component | |
| * ============================================================================ | |
| * | |
| * A reusable bar component for displaying any numeric value as a | |
| * filled rectangle with optional label and value text. | |
| * | |
| * FEATURES: | |
| * - Smooth animated fill transitions (tweened) | |
| * - Color change at thresholds (green -> yellow -> red) | |
| * - Optional label text (e.g., "HP", "Mana") | |
| * - Optional value text (e.g., "75/100") | |
| * - Customizable frame/border | |
| * | |
| * USAGE: | |
| * const hpBar = new StatusBar(scene, 100, 50, { | |
| * width: 200, height: 24, | |
| * maxValue: 100, | |
| * label: 'HP', | |
| * showValue: true, | |
| * colorThresholds: [ | |
| * { percent: 0.6, color: 0x00ff00 }, | |
| * { percent: 0.3, color: 0xffff00 }, | |
| * { percent: 0.0, color: 0xff0000 }, | |
| * ], | |
| * }); | |
| * hpBar.setValue(75); // animated fill to 75% | |
| */ | |
| import Phaser from 'phaser'; | |
| export interface ColorThreshold { | |
| /** Minimum percentage (0-1) for this color */ | |
| percent: number; | |
| /** Fill color at this threshold */ | |
| color: number; | |
| } | |
| export interface StatusBarConfig { | |
| /** Bar width */ | |
| width: number; | |
| /** Bar height */ | |
| height: number; | |
| /** Maximum value */ | |
| maxValue: number; | |
| /** Initial value (default: maxValue) */ | |
| initialValue?: number; | |
| /** Label text (e.g., "HP") */ | |
| label?: string; | |
| /** Show "current/max" value text */ | |
| showValue?: boolean; | |
| /** Fill color (default: 0x00ff00) */ | |
| fillColor?: number; | |
| /** Background color (default: 0x333333) */ | |
| backgroundColor?: number; | |
| /** Border color (default: 0xffffff) */ | |
| borderColor?: number; | |
| /** Border width (default: 2) */ | |
| borderWidth?: number; | |
| /** Color thresholds (sorted by percent descending) */ | |
| colorThresholds?: ColorThreshold[]; | |
| /** Animation duration for value changes (ms, default: 300) */ | |
| animDuration?: number; | |
| } | |
| export class StatusBar extends Phaser.GameObjects.Container { | |
| private barConfig: StatusBarConfig; | |
| private background!: Phaser.GameObjects.Rectangle; | |
| private fill!: Phaser.GameObjects.Rectangle; | |
| private border?: Phaser.GameObjects.Rectangle; | |
| private labelText?: Phaser.GameObjects.Text; | |
| private valueText?: Phaser.GameObjects.Text; | |
| private currentValue: number; | |
| private maxValue: number; | |
| constructor( | |
| scene: Phaser.Scene, | |
| x: number, | |
| y: number, | |
| config: StatusBarConfig, | |
| ) { | |
| super(scene, x, y); | |
| this.barConfig = config; | |
| this.maxValue = config.maxValue; | |
| this.currentValue = config.initialValue ?? config.maxValue; | |
| scene.add.existing(this); | |
| this.setDepth(150); | |
| this.createBar(); | |
| } | |
| // -- Public API -- | |
| /** Set the current value (animated). */ | |
| setValue(value: number, animate: boolean = true): void { | |
| const oldValue = this.currentValue; | |
| this.currentValue = Phaser.Math.Clamp(value, 0, this.maxValue); | |
| const percent = this.getPercent(); | |
| const targetWidth = Math.max(1, this.barConfig.width * percent); | |
| const color = this.getColorForPercent(percent); | |
| if (animate && this.barConfig.animDuration !== 0) { | |
| this.scene.tweens.add({ | |
| targets: this.fill, | |
| displayWidth: targetWidth, | |
| duration: this.barConfig.animDuration ?? 300, | |
| ease: 'Cubic.easeOut', | |
| onUpdate: () => { | |
| // Update color during animation | |
| const animPercent = this.fill.displayWidth / this.barConfig.width; | |
| this.fill.setFillStyle(this.getColorForPercent(animPercent)); | |
| }, | |
| }); | |
| } else { | |
| this.fill.displayWidth = targetWidth; | |
| this.fill.setFillStyle(color); | |
| } | |
| // Update value text | |
| if (this.valueText) { | |
| this.valueText.setText( | |
| `${Math.ceil(this.currentValue)}/${this.maxValue}`, | |
| ); | |
| } | |
| } | |
| /** Set the maximum value. */ | |
| setMaxValue(maxValue: number): void { | |
| this.maxValue = maxValue; | |
| this.setValue(Math.min(this.currentValue, maxValue)); | |
| } | |
| /** Get current value. */ | |
| getValue(): number { | |
| return this.currentValue; | |
| } | |
| /** Get percentage (0-1). */ | |
| getPercent(): number { | |
| return this.maxValue > 0 ? this.currentValue / this.maxValue : 0; | |
| } | |
| // -- Internal -- | |
| private createBar(): void { | |
| const cfg = this.barConfig; | |
| const w = cfg.width; | |
| const h = cfg.height; | |
| // Label (left of bar) | |
| let labelOffset = 0; | |
| if (cfg.label) { | |
| this.labelText = this.scene.add | |
| .text(-w / 2, 0, cfg.label, { | |
| fontSize: '14px', | |
| fontFamily: 'Arial', | |
| color: '#ffffff', | |
| fontStyle: 'bold', | |
| }) | |
| .setOrigin(1, 0.5); | |
| this.labelText.setX(-w / 2 - 8); | |
| this.add(this.labelText); | |
| } | |
| // Background | |
| this.background = this.scene.add.rectangle( | |
| 0, | |
| 0, | |
| w, | |
| h, | |
| cfg.backgroundColor ?? 0x333333, | |
| ); | |
| this.background.setOrigin(0, 0.5); | |
| this.background.setX(-w / 2); | |
| this.add(this.background); | |
| // Fill | |
| const percent = this.getPercent(); | |
| const fillWidth = Math.max(1, w * percent); | |
| const fillColor = this.getColorForPercent(percent); | |
| this.fill = this.scene.add.rectangle(0, 0, fillWidth, h - 4, fillColor); | |
| this.fill.setOrigin(0, 0.5); | |
| this.fill.setX(-w / 2 + 2); | |
| this.add(this.fill); | |
| // Border | |
| if (cfg.borderWidth !== 0) { | |
| this.border = this.scene.add.rectangle(0, 0, w, h); | |
| this.border.setStrokeStyle( | |
| cfg.borderWidth ?? 2, | |
| cfg.borderColor ?? 0xffffff, | |
| ); | |
| this.border.setFillStyle(0x000000, 0); | |
| this.border.setOrigin(0, 0.5); | |
| this.border.setX(-w / 2); | |
| this.add(this.border); | |
| } | |
| // Value text (inside bar) | |
| if (cfg.showValue) { | |
| this.valueText = this.scene.add | |
| .text(0, 0, `${Math.ceil(this.currentValue)}/${this.maxValue}`, { | |
| fontSize: '12px', | |
| fontFamily: 'Arial', | |
| color: '#ffffff', | |
| fontStyle: 'bold', | |
| stroke: '#000000', | |
| strokeThickness: 2, | |
| }) | |
| .setOrigin(0.5); | |
| this.add(this.valueText); | |
| } | |
| } | |
| private getColorForPercent(percent: number): number { | |
| if ( | |
| !this.barConfig.colorThresholds || | |
| this.barConfig.colorThresholds.length === 0 | |
| ) { | |
| return this.barConfig.fillColor ?? 0x00ff00; | |
| } | |
| // Sort descending by percent for correct lookup | |
| const sorted = [...this.barConfig.colorThresholds].sort( | |
| (a, b) => b.percent - a.percent, | |
| ); | |
| for (const threshold of sorted) { | |
| if (percent >= threshold.percent) return threshold.color; | |
| } | |
| return sorted[sorted.length - 1].color; | |
| } | |
| } | |