@@ -43,6 +43,8 @@ void hud_init(hud_t *h) {
4343 printf ("Warning: could not load fonts/JetBrainsMono-Medium.ttf, using default font\n" );
4444 if (h -> font_label .glyphCount == 0 )
4545 printf ("Warning: could not load fonts/Inter-Medium.ttf, using default font\n" );
46+
47+ annunc_init (& h -> annunciators );
4648}
4749
4850void hud_update (hud_t * h , uint64_t time_usec , bool connected , float dt ) {
@@ -54,6 +56,13 @@ void hud_update(hud_t *h, uint64_t time_usec, bool connected, float dt) {
5456 // Tick toast timer
5557 if (h -> toast_timer > 0.0f )
5658 h -> toast_timer -= dt ;
59+ // Tick annunciator timers
60+ annunc_update (& h -> annunciators , dt );
61+ // Tick STATUSTEXT ticker flash timers (entries persist, only flash fades)
62+ for (int i = 0 ; i < h -> ticker_count ; i ++ ) {
63+ if (h -> ticker [i ].timer > 0.0f )
64+ h -> ticker [i ].timer -= dt ;
65+ }
5766}
5867
5968void hud_toast (hud_t * h , const char * text , float duration_s ) {
@@ -70,15 +79,71 @@ void hud_toast_color(hud_t *h, const char *text, float duration_s, Color color)
7079 h -> toast_color = color ;
7180}
7281
82+ static Color severity_color (uint8_t sev , const theme_t * theme ) {
83+ // Source severity colors from the drone palette warm tones
84+ if (sev <= 2 ) return theme -> drone_palette [2 ]; // critical — palette red/pink
85+ if (sev == 3 ) return theme -> drone_palette [4 ]; // error — palette orange/warm
86+ if (sev == 4 ) return theme -> hud_warn ; // warning — theme warn color
87+ return theme -> hud_dim ; // info+
88+ }
89+
90+ void hud_feed_statustext (hud_t * h , const struct statustext_ring * ring , int drone_idx ) {
91+ if (!ring || drone_idx < 0 || drone_idx >= 16 ) return ;
92+ int last = h -> ticker_consumed [drone_idx ];
93+ if (ring -> head == last ) return ; // no new entries
94+
95+ // Walk from last consumed to current head
96+ int count = ring -> count ;
97+ int head = ring -> head ;
98+ int start = last ;
99+ // How many new entries?
100+ int new_count = head - start ;
101+ if (new_count < 0 ) new_count += STATUSTEXT_RING_SIZE ;
102+ if (new_count > count ) new_count = count ;
103+
104+ for (int n = 0 ; n < new_count ; n ++ ) {
105+ int idx = (start + n ) % STATUSTEXT_RING_SIZE ;
106+ const statustext_entry_t * e = & ring -> entries [idx ];
107+ // Severity may be raw (0-7) or ASCII ('0'-'7')
108+ uint8_t sev = e -> severity ;
109+ if (sev >= '0' ) sev -= '0' ;
110+ if (sev > 4 ) continue ; // only WARNING or worse
111+
112+ // Shift existing ticker entries down
113+ if (h -> ticker_count < HUD_TICKER_MAX ) h -> ticker_count ++ ;
114+ for (int j = h -> ticker_count - 1 ; j > 0 ; j -- )
115+ h -> ticker [j ] = h -> ticker [j - 1 ];
116+
117+ // Insert at top
118+ snprintf (h -> ticker [0 ].text , sizeof (h -> ticker [0 ].text ), "%s" , e -> text );
119+ h -> ticker [0 ].severity = sev ;
120+ h -> ticker [0 ].timer = 8.0f ;
121+ h -> ticker [0 ].total = 8.0f ;
122+ h -> ticker [0 ].drone_idx = drone_idx ;
123+
124+ // Trigger annunciators
125+ annunc_trigger_ticker_flash (& h -> annunciators , 0 );
126+ annunc_trigger_ring_shake (& h -> annunciators , drone_idx );
127+ }
128+ h -> ticker_consumed [drone_idx ] = head ;
129+ }
73130
74131static void draw_numpad (const hud_t * h , const vehicle_t * vehicles ,
75132 const data_source_t * sources , int vehicle_count ,
76133 int selected , float numpad_x , float numpad_y ,
77- Font font_label , float btn_size , float gap , float scale ) {
134+ Font font_label , float btn_size , float gap , float scale ,
135+ int * out_cols , int * out_rows ) {
136+ // Dynamic grid: up to 3 cols, rows sized to fit vehicle_count
137+ int cols = (vehicle_count <= 9 ) ? 3 : (vehicle_count <= 12 ) ? 3 : 4 ;
138+ int rows = (vehicle_count <= 9 ) ? 3 : (vehicle_count <= 12 ) ? 4 : 4 ;
139+ int total_slots = cols * rows ;
140+ if (out_cols ) * out_cols = cols ;
141+ if (out_rows ) * out_rows = rows ;
142+
78143 float fs = 12 * scale ;
79- for (int i = 0 ; i < 9 ; i ++ ) {
80- int row = i / 3 ;
81- int col = i % 3 ;
144+ for (int i = 0 ; i < total_slots ; i ++ ) {
145+ int row = i / cols ;
146+ int col = i % cols ;
82147 float bx = numpad_x + col * (btn_size + gap );
83148 float by = numpad_y + row * (btn_size + gap );
84149 int veh_idx = i ;
@@ -116,11 +181,13 @@ static void draw_numpad(const hud_t *h, const vehicle_t *vehicles,
116181 vehicles [veh_idx ].color .b , 80 });
117182 }
118183
119- char nb [2 ] = { '1' + i , '\0' };
120- Vector2 nw = MeasureTextEx (font_label , nb , fs , 0.5f );
184+ char nb [4 ];
185+ snprintf (nb , sizeof (nb ), "%d" , i + 1 );
186+ float lfs = (i >= 9 ) ? fs * 0.8f : fs ;
187+ Vector2 nw = MeasureTextEx (font_label , nb , lfs , 0.5f );
121188 DrawTextEx (font_label , nb ,
122- (Vector2 ){bx + btn_size /2 - nw .x /2 , by + btn_size /2 - fs /2 },
123- fs , 0.5f , btn_text );
189+ (Vector2 ){bx + btn_size /2 - nw .x /2 , by + btn_size /2 - lfs /2 },
190+ lfs , 0.5f , btn_text );
124191 }
125192}
126193
@@ -141,8 +208,13 @@ static void draw_secondary_row(const hud_t *h, const vehicle_t *pv, int pidx,
141208 (Vector2 ){(float )screen_w , (float )row_y },
142209 1.0f , (Color ){255 , 255 , 255 , 20 });
143210
144- // Color bar
145- DrawRectangle (0 , row_y , (int )(3 * scale ), secondary_h , pv -> color );
211+ // Color bar (fades on marker crossing for this drone)
212+ {
213+ float tab_a = annunc_tab_fade_alpha (& h -> annunciators , pidx );
214+ Color tc = pv -> color ;
215+ tc .a = (unsigned char )(tc .a * tab_a );
216+ DrawRectangle (0 , row_y , (int )(3 * scale ), secondary_h , tc );
217+ }
146218
147219 // Vehicle number
148220 char vnum [4 ];
@@ -247,7 +319,7 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
247319 Color bg = theme -> hud_bg ;
248320 Color border = theme -> hud_border ;
249321 Color warn = theme -> hud_warn ;
250- Color label_color = accent_dim ;
322+ Color label_color = ( Color ){ accent_dim . r , accent_dim . g , accent_dim . b , ( unsigned char )( accent_dim . a * 0.7f )} ;
251323 Color value_color = theme -> hud_value ;
252324 Color dim_color = theme -> hud_dim ;
253325 Color climb_color = theme -> hud_climb ;
@@ -258,12 +330,12 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
258330 if (s < 1.0f ) s = 1.0f ;
259331
260332 // Scaled font sizes
261- float fs_label = 16 * s ;
262- float fs_value = 23 * s ;
263- float fs_unit = 15 * s ;
264- float fs_dim = 15 * s ;
265- float fs_sec_label = 14 * s ;
266- float fs_sec_value = 18 * s ;
333+ float fs_label = fmaxf ( 16 * s , 10.0f ) ;
334+ float fs_value = fmaxf ( 23 * s , 14.0f ) ;
335+ float fs_unit = fmaxf ( 15 * s , 9.0f ) ;
336+ float fs_dim = fmaxf ( 15 * s , 9.0f ) ;
337+ float fs_sec_label = fmaxf ( 14 * s , 9.0f ) ;
338+ float fs_sec_value = fmaxf ( 18 * s , 12.0f ) ;
267339
268340 // Dynamic bar height
269341 int primary_h = (int )(120 * s );
@@ -279,12 +351,70 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
279351 DrawRectangle (0 , bar_y , screen_w , total_bar_h , bg );
280352 DrawLineEx ((Vector2 ){0 , (float )bar_y }, (Vector2 ){(float )screen_w , (float )bar_y }, 1.0f , border );
281353
282- // Toast notification (fades in/out, always above other notices)
354+ // Ticker zone: STATUSTEXT warnings take priority, toast shows when no warnings
283355 float toast_h_used = 0.0f ;
284- if (h -> toast_timer > 0.0f ) {
285- float toast_fs = 14 * s ;
286- Vector2 tw = MeasureTextEx (h -> font_label , h -> toast_text , toast_fs , 0.5f );
287- // Fade: full opacity for most of duration, fade out in last 0.5s
356+ float ticker_fs = fmaxf (14 * s , 10.0f );
357+ float ticker_y = (float )bar_y - 8 * s ;
358+
359+ if (h -> ticker_count > 0 && h -> show_notifications ) {
360+ // STATUSTEXT warnings — take over the ticker zone
361+ for (int i = 0 ; i < h -> ticker_count ; i ++ ) {
362+ float fade = 1.0f ;
363+ if (h -> ticker [i ].timer < 1.0f )
364+ fade = h -> ticker [i ].timer ;
365+ Color sc = severity_color (h -> ticker [i ].severity , theme );
366+
367+ char tag [8 ];
368+ snprintf (tag , sizeof (tag ), "D%d" , h -> ticker [i ].drone_idx + 1 );
369+ Vector2 tag_w = MeasureTextEx (h -> font_label , tag , ticker_fs * 0.8f , 0.5f );
370+ Vector2 msg_w = MeasureTextEx (h -> font_label , h -> ticker [i ].text , ticker_fs , 0.5f );
371+ float total_w = tag_w .x + 8 * s + msg_w .x ;
372+ float tx = (float )(screen_w / 2 ) - total_w / 2.0f ;
373+ float ty = ticker_y - msg_w .y ;
374+
375+ // Gradient background with faded edges
376+ float bg_pad = 40 * s ;
377+ float flash_a = annunc_ticker_flash_alpha (& h -> annunciators , i );
378+ float bg_alpha = fade * (25 + flash_a * 120 );
379+ Color bg_c = (Color ){sc .r , sc .g , sc .b , (unsigned char )bg_alpha };
380+ // Center solid, edges fade to transparent
381+ float bg_x = tx - bg_pad ;
382+ float bg_w = total_w + bg_pad * 2 ;
383+ float bg_h = msg_w .y + 4 * s ;
384+ float edge = bg_pad ;
385+ // Left fade
386+ for (int px = 0 ; px < (int )edge ; px ++ ) {
387+ float a = (float )px / edge ;
388+ Color fc = {bg_c .r , bg_c .g , bg_c .b , (unsigned char )(bg_c .a * a )};
389+ DrawRectangle ((int )(bg_x + px ), (int )ty , 1 , (int )bg_h , fc );
390+ }
391+ // Center solid
392+ DrawRectangle ((int )(bg_x + edge ), (int )ty , (int )(bg_w - edge * 2 ), (int )bg_h , bg_c );
393+ // Right fade
394+ for (int px = 0 ; px < (int )edge ; px ++ ) {
395+ float a = 1.0f - (float )px / edge ;
396+ Color fc = {bg_c .r , bg_c .g , bg_c .b , (unsigned char )(bg_c .a * a )};
397+ DrawRectangle ((int )(bg_x + bg_w - edge + px ), (int )ty , 1 , (int )bg_h , fc );
398+ }
399+
400+ // Drone tag
401+ Color tag_c = (Color ){sc .r , sc .g , sc .b , (unsigned char )(fade * 140 )};
402+ DrawTextEx (h -> font_label , tag , (Vector2 ){tx , ty + 1 }, ticker_fs * 0.8f , 0.5f , tag_c );
403+
404+ // Message text — invert to black during flash
405+ sc .a = (unsigned char )(fade * 255 );
406+ Color text_c = (flash_a > 0.3f )
407+ ? (Color ){0 , 0 , 0 , (unsigned char )(fade * 255 )}
408+ : sc ;
409+ DrawTextEx (h -> font_label , h -> ticker [i ].text ,
410+ (Vector2 ){tx + tag_w .x + 8 * s , ty }, ticker_fs , 0.5f , text_c );
411+
412+ ticker_y = ty - 2 * s ;
413+ }
414+ toast_h_used = (float )bar_y - 8 * s - ticker_y ;
415+ } else if (h -> toast_timer > 0.0f ) {
416+ // Regular toast when no STATUSTEXT warnings active
417+ Vector2 tw = MeasureTextEx (h -> font_label , h -> toast_text , ticker_fs , 0.5f );
288418 float fade = 1.0f ;
289419 if (h -> toast_timer < 0.5f )
290420 fade = h -> toast_timer / 0.5f ;
@@ -294,7 +424,7 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
294424 float toast_y = (float )bar_y - tw .y - 8 * s ;
295425 float toast_x = (float )(screen_w / 2 ) - tw .x / 2.0f ;
296426 DrawTextEx (h -> font_label , h -> toast_text , (Vector2 ){toast_x , toast_y },
297- toast_fs , 0.5f , toast_c );
427+ ticker_fs , 0.5f , toast_c );
298428 toast_h_used = tw .y + 8 * s ;
299429 }
300430
@@ -333,8 +463,13 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
333463 // Offset the main HUD content below the transport row
334464 bar_y += transport_h ;
335465
336- // Primary color bar on left edge
337- DrawRectangle (0 , bar_y , (int )(3 * s ), primary_h , v -> color );
466+ // Primary color bar on left edge (fades on marker crossing)
467+ {
468+ float tab_a = annunc_tab_fade_alpha (& h -> annunciators , selected );
469+ Color tab_c = v -> color ;
470+ tab_c .a = (unsigned char )(tab_c .a * tab_a );
471+ DrawRectangle (0 , bar_y , (int )(3 * s ), primary_h , tab_c );
472+ }
338473
339474 // Instruments -- centered vertically in primary area
340475 float inst_radius = INSTRUMENT_RADIUS * s ;
@@ -356,7 +491,8 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
356491 float np_btn = NUMPAD_BTN_SIZE * s ;
357492 float np_gap = NUMPAD_GAP * s ;
358493 float status_x = (float )(screen_w - 16 * s - 110 * s );
359- float numpad_total_w = 3 * (np_btn + np_gap ) - np_gap ;
494+ int np_cols = (vehicle_count <= 9 ) ? 3 : (vehicle_count <= 12 ) ? 3 : 4 ;
495+ float numpad_total_w = np_cols * (np_btn + np_gap ) - np_gap ;
360496 float numpad_x = status_x - 20 * s - numpad_total_w ;
361497 float timer_x = numpad_x - 24 * s - 60 * s ;
362498
@@ -401,15 +537,20 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
401537 .label_color = label_color , .value_color = value_color ,
402538 .dim_color = dim_color , .warn = warn ,
403539 .climb_color = climb_color , .connected_color = connected_color ,
540+ .selected = selected ,
404541 };
405542
406543 hud_draw_telemetry (h , v , & tlay );
407544
408545 // Numpad (only when vehicle_count > 1)
409546 if (vehicle_count > 1 ) {
410- float np_y = bar_y + (primary_h / 2.0f ) - (3 * (np_btn + np_gap )) / 2.0f ;
547+ int np_r = (vehicle_count <= 9 ) ? 3 : 4 ;
548+ float np_grid_h = np_r * (np_btn + np_gap ) - np_gap ;
549+ float np_y = bar_y + (primary_h / 2.0f ) - np_grid_h / 2.0f ;
550+ int np_c_out , np_r_out ;
411551 draw_numpad (h , vehicles , sources , vehicle_count , selected ,
412- numpad_x , np_y , h -> font_label , np_btn , np_gap , s );
552+ numpad_x , np_y , h -> font_label , np_btn , np_gap , s ,
553+ & np_c_out , & np_r_out );
413554 }
414555
415556 hud_draw_status (h , v , & sources [selected ], & tlay , ghost_mode );
0 commit comments