@@ -11,3 +11,143 @@ Health has the following methods:
1111- ` setShield(number) ` - Set the shield value
1212- ` showHealthbar() ` - Show the health bar
1313- ` hideHealthbar() ` - Hide the health bar
14+
15+ ``` ts
16+ import { Behavior , EntityDestroyed , RawPixi , value } from " @dreamlab/engine" ;
17+ import * as PIXI from " @dreamlab/vendor/pixi.ts" ;
18+
19+ export default class Health extends Behavior {
20+ @value ()
21+ public health = 100 ;
22+
23+ @value ()
24+ public maxHealth = 100 ;
25+
26+ @value ()
27+ shield = 0 ;
28+
29+ @value ()
30+ xOffset = 0 ;
31+
32+ @value ()
33+ yOffset = 0 ;
34+
35+ @value ()
36+ scale = 1 ;
37+
38+ rawPixi! : RawPixi ;
39+ graphics! : PIXI .Graphics ;
40+
41+ // Style knobs
42+ private readonly BAR_WIDTH = 64 ; // total width of the bar (px)
43+ private readonly BAR_HEIGHT = 8 ; // total height of the bar (px)
44+ private readonly RADIUS = 3 ; // corner radius (px)
45+ private readonly PADDING = 1 ; // inner padding between outline and fill (px)
46+ private readonly OUTLINE = 1 ; // outline thickness (px)
47+
48+ // Colors
49+ private readonly COLOR_BG = " #1e1f26" ;
50+ private readonly COLOR_OUTLINE = " #2f3342" ;
51+ private readonly COLOR_HEALTH = " #3ed36f" ;
52+ private readonly COLOR_HEALTH_LOW = " #e55039" ;
53+ private readonly COLOR_SHIELD = " #55b6ff" ;
54+ private readonly CONST_SCALEDOWN_MULTIPLIER = 0.1 ; // so everything isn't huge;
55+
56+ onInitialize(): void {
57+ if (! this .game .isClient ()) return ;
58+ this .createHealthbarIfNotExists ();
59+ }
60+
61+ createHealthbarIfNotExists() {
62+ if (! this .game .isClient ()) return ;
63+ if (this .rawPixi ) return ;
64+ this .rawPixi = this .game .local .spawn ({ type: RawPixi , name: " HealthBar" });
65+ this .graphics = new PIXI .Graphics ();
66+
67+ // Add once; we'll just redraw the Graphics' geometry each change
68+ this .rawPixi .container ?.addChild (this .graphics );
69+
70+ this .values .get (" health" )?.onChanged (() => this .redrawHealthbar ());
71+ this .values .get (" maxHealth" )?.onChanged (() => this .redrawHealthbar ());
72+ this .values .get (" shield" )?.onChanged (() => this .redrawHealthbar ());
73+ this .redrawHealthbar ();
74+
75+ this .listen (this .entity , EntityDestroyed , () => {
76+ this .rawPixi .destroy ();
77+ });
78+ }
79+
80+ updatePosition() {
81+ this .rawPixi .globalTransform .position .x = this .entity .pos .x + this .xOffset ;
82+ this .rawPixi .globalTransform .position .y = this .entity .pos .y + this .yOffset ;
83+
84+ this .rawPixi .globalTransform .position .x -= this .BAR_WIDTH
85+ * (this .scale * this .CONST_SCALEDOWN_MULTIPLIER ) / 2 ; // center it
86+ }
87+
88+ onTick() {
89+ if (! this .game .isClient ()) return ;
90+ this .createHealthbarIfNotExists ();
91+ this .updatePosition ();
92+ }
93+
94+ redrawHealthbar(): void {
95+ const max = Math .max (0 , this .maxHealth );
96+ const hp = Math .max (0 , Math .min (this .health , max ));
97+ const hasShield = this .shield > 0 && max > 0 ;
98+
99+ // Avoid division by zero; if max is 0, show empty bar
100+ const ratio = max > 0 ? hp / max : 0 ;
101+
102+ // Health bar fill color shifts when low
103+ const healthColor = ratio <= 0.25 ? this .COLOR_HEALTH_LOW : this .COLOR_HEALTH ;
104+
105+ const w = this .BAR_WIDTH ;
106+ const h = this .BAR_HEIGHT ;
107+ const innerW = w - 2 * (this .PADDING + this .OUTLINE );
108+ const innerH = h - 2 * (this .PADDING + this .OUTLINE );
109+
110+ const healthFillW = Math .max (0 , Math .min (innerW , Math .round (innerW * ratio )));
111+
112+ // Shield: show as a thin bar above health, proportional to shield/max
113+ const shieldRatio = max > 0 ? Math .max (0 , Math .min (1 , this .shield / max )) : 0 ;
114+ const shieldFillW = Math .max (0 , Math .min (innerW , Math .round (innerW * shieldRatio )));
115+ const shieldHeight = hasShield ? Math .max (2 , Math .floor (innerH * 0.35 )) : 0 ;
116+
117+ // Left-aligned means (0,0) is the top-left of the bar
118+ this .graphics .clear ();
119+
120+ // Background (rounded)
121+ this .graphics
122+ .roundRect (0 , 0 , w , h , this .RADIUS )
123+ .fill ({ color: this .COLOR_BG });
124+
125+ // Outline
126+ this .graphics
127+ .roundRect (0 , 0 , w , h , this .RADIUS )
128+ .stroke ({ color: this .COLOR_OUTLINE , width: this .OUTLINE });
129+
130+ // Health fill (rounded only on left; but Graphics API rounds all corners, so draw a plain rect inside padding)
131+ if (healthFillW > 0 ) {
132+ this .graphics
133+ .rect (this .OUTLINE + this .PADDING , this .OUTLINE + this .PADDING , healthFillW , innerH )
134+ .fill ({ color: healthColor });
135+ }
136+
137+ // Shield overlay as a slim strip at the top, left-aligned
138+ if (shieldHeight > 0 && shieldFillW > 0 ) {
139+ this .graphics
140+ .rect (
141+ this .OUTLINE + this .PADDING ,
142+ this .OUTLINE + this .PADDING ,
143+ shieldFillW ,
144+ shieldHeight ,
145+ )
146+ .fill ({ color: this .COLOR_SHIELD , alpha: 0.9 });
147+ }
148+
149+ this .graphics .scale .set (this .scale * this .CONST_SCALEDOWN_MULTIPLIER );
150+ }
151+ }
152+
153+ ```
0 commit comments