mafia / frontend /src /ui /StatusBar.ts
Alfaxad's picture
Migrate Mafia game to ZeroGPU Gradio Space
c670567 verified
Raw
History Blame Contribute Delete
6.63 kB
/**
* ============================================================================
* 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;
}
}