]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
Merge branch 'terencehill/bot_fix' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / waypoints / waypointsprites.qc
1 #include "waypointsprites.qh"
2
3 REGISTER_MUTATOR(waypointsprites, true);
4
5 REGISTER_NET_LINKED(waypointsprites)
6
7 #ifdef SVQC
8 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
9 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags)
10 {
11     WriteHeader(MSG_ENTITY, waypointsprites);
12
13     sendflags = sendflags & 0x7F;
14
15     if (this.max_health || (this.pain_finished && (time < this.pain_finished + 0.25)))
16         sendflags |= 0x80;
17
18     int f = 0;
19     if(this.currentammo == 1)
20         f |= 1; // hideable
21     if(this.exteriormodeltoclient == to)
22         f |= 2; // my own
23     if(this.currentammo == 2)
24         f |= 2; // radar only
25
26     MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, f);
27     sendflags = M_ARGV(2, int);
28     f = M_ARGV(3, int);
29
30     WriteByte(MSG_ENTITY, sendflags);
31     WriteByte(MSG_ENTITY, this.wp_extra);
32
33     if (sendflags & 0x80)
34     {
35         if (this.max_health)
36         {
37             WriteByte(MSG_ENTITY, (this.health / this.max_health) * 191.0);
38         }
39         else
40         {
41             float dt = this.pain_finished - time;
42             dt = bound(0, dt * 32, 16383);
43             WriteByte(MSG_ENTITY, (dt & 0xFF00) / 256 + 192);
44             WriteByte(MSG_ENTITY, (dt & 0x00FF));
45         }
46     }
47
48     if (sendflags & 64)
49     {
50         WriteCoord(MSG_ENTITY, this.origin.x);
51         WriteCoord(MSG_ENTITY, this.origin.y);
52         WriteCoord(MSG_ENTITY, this.origin.z);
53     }
54
55     if (sendflags & 1)
56     {
57         WriteByte(MSG_ENTITY, this.team);
58         WriteByte(MSG_ENTITY, this.rule);
59     }
60
61     if (sendflags & 2)
62         WriteString(MSG_ENTITY, this.model1);
63
64     if (sendflags & 4)
65         WriteString(MSG_ENTITY, this.model2);
66
67     if (sendflags & 8)
68         WriteString(MSG_ENTITY, this.model3);
69
70     if (sendflags & 16)
71     {
72         WriteCoord(MSG_ENTITY, this.fade_time);
73         WriteCoord(MSG_ENTITY, this.teleport_time);
74         WriteShort(MSG_ENTITY, this.fade_rate); // maxdist
75         WriteByte(MSG_ENTITY, f);
76     }
77
78     if (sendflags & 32)
79     {
80         WriteByte(MSG_ENTITY, this.cnt); // icon on radar
81         WriteByte(MSG_ENTITY, this.colormod.x * 255.0);
82         WriteByte(MSG_ENTITY, this.colormod.y * 255.0);
83         WriteByte(MSG_ENTITY, this.colormod.z * 255.0);
84
85         if (WaypointSprite_isteammate(this.owner, WaypointSprite_getviewentity(to)))
86         {
87             float dt = (this.waypointsprite_helpmetime - time) / 0.1;
88             if (dt < 0)
89                 dt = 0;
90             if (dt > 255)
91                 dt = 255;
92             WriteByte(MSG_ENTITY, dt);
93         }
94         else
95             WriteByte(MSG_ENTITY, 0);
96     }
97
98     return true;
99 }
100 #endif
101
102 #ifdef CSQC
103 void Ent_WaypointSprite(entity this, bool isnew);
104 NET_HANDLE(waypointsprites, bool isnew) {
105     Ent_WaypointSprite(this, isnew);
106     return true;
107 }
108
109 void Ent_RemoveWaypointSprite(entity this)
110 {
111     if (this.netname) strunzone(this.netname);
112     if (this.netname2) strunzone(this.netname2);
113     if (this.netname3) strunzone(this.netname3);
114 }
115
116 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
117 void Ent_WaypointSprite(entity this, bool isnew)
118 {
119     int sendflags = ReadByte();
120     this.wp_extra = ReadByte();
121
122     if (!this.spawntime)
123         this.spawntime = time;
124
125     this.draw2d = Draw_WaypointSprite;
126     if (isnew) {
127                 IL_PUSH(g_drawables_2d, this);
128                 IL_PUSH(g_radaricons, this);
129     }
130
131     InterpolateOrigin_Undo(this);
132     this.iflags |= IFLAG_ORIGIN;
133
134     if (sendflags & 0x80)
135     {
136         int t = ReadByte();
137         if (t < 192)
138         {
139             this.health = t / 191.0;
140             this.build_finished = 0;
141         }
142         else
143         {
144             t = (t - 192) * 256 + ReadByte();
145             this.build_started = servertime;
146             if (this.build_finished)
147                 this.build_starthealth = bound(0, this.health, 1);
148             else
149                 this.build_starthealth = 0;
150             this.build_finished = servertime + t / 32;
151         }
152     }
153     else
154     {
155         this.health = -1;
156         this.build_finished = 0;
157     }
158
159     if (sendflags & 64)
160     {
161         // unfortunately, this needs to be exact (for the 3D display)
162         this.origin_x = ReadCoord();
163         this.origin_y = ReadCoord();
164         this.origin_z = ReadCoord();
165         setorigin(this, this.origin);
166     }
167
168     if (sendflags & 1)
169     {
170         this.team = ReadByte();
171         this.rule = ReadByte();
172     }
173
174     if (sendflags & 2)
175     {
176         if (this.netname)
177             strunzone(this.netname);
178         this.netname = strzone(ReadString());
179     }
180
181     if (sendflags & 4)
182     {
183         if (this.netname2)
184             strunzone(this.netname2);
185         this.netname2 = strzone(ReadString());
186     }
187
188     if (sendflags & 8)
189     {
190         if (this.netname3)
191             strunzone(this.netname3);
192         this.netname3 = strzone(ReadString());
193     }
194
195     if (sendflags & 16)
196     {
197         this.lifetime = ReadCoord();
198         this.fadetime = ReadCoord();
199         this.maxdistance = ReadShort();
200         this.hideflags = ReadByte();
201     }
202
203     if (sendflags & 32)
204     {
205         int f = ReadByte();
206         this.teamradar_icon = f & BITS(7);
207         if (f & BIT(7))
208         {
209             this.(teamradar_times[this.teamradar_time_index]) = time;
210             this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
211         }
212         this.teamradar_color_x = ReadByte() / 255.0;
213         this.teamradar_color_y = ReadByte() / 255.0;
214         this.teamradar_color_z = ReadByte() / 255.0;
215         this.helpme = ReadByte() * 0.1;
216         if (this.helpme > 0)
217             this.helpme += servertime;
218     }
219
220     InterpolateOrigin_Note(this);
221
222     this.entremove = Ent_RemoveWaypointSprite;
223 }
224 #endif
225
226 #ifdef CSQC
227 float spritelookupblinkvalue(entity this, string s)
228 {
229     if (s == WP_Weapon.netname) {
230         if (Weapons_from(this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
231             return 2;
232     }
233     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_waypointblink;
234     if(s == WP_FlagReturn.netname) return 2;
235
236     return 1;
237 }
238
239 vector spritelookupcolor(entity this, string s, vector def)
240 {
241     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return Weapons_from(this.wp_extra).wpcolor;
242     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return Items_from(this.wp_extra).m_color;
243     if (MUTATOR_CALLHOOK(WP_Format, this, s))
244     {
245         return M_ARGV(2, vector);
246     }
247     return def;
248 }
249
250 string spritelookuptext(entity this, string s)
251 {
252     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
253     if (s == WP_Weapon.netname) return Weapons_from(this.wp_extra).m_name;
254     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_waypoint;
255     if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).monster_name;
256     if (MUTATOR_CALLHOOK(WP_Format, this, s))
257     {
258         return M_ARGV(3, string);
259     }
260
261     // need to loop, as our netname could be one of three
262     FOREACH(Waypoints, it.netname == s, LAMBDA(
263         return it.m_name;
264     ));
265
266     return s;
267 }
268 #endif
269
270 #ifdef CSQC
271 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
272 {
273     vector v1, v2, v3, v4;
274
275     hotspot = -1 * hotspot;
276
277     // hotspot-relative coordinates of the corners
278     v1 = hotspot;
279     v2 = hotspot + '1 0 0' * sz.x;
280     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
281     v4 = hotspot                  + '0 1 0' * sz.y;
282
283     // rotate them, and make them absolute
284     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
285     v1 = Rotate(v1, rot) + org;
286     v2 = Rotate(v2, rot) + org;
287     v3 = Rotate(v3, rot) + org;
288     v4 = Rotate(v4, rot) + org;
289
290     // draw them
291     R_BeginPolygon(pic, f);
292     R_PolygonVertex(v1, '0 0 0', rgb, a);
293     R_PolygonVertex(v2, '1 0 0', rgb, a);
294     R_PolygonVertex(v3, '1 1 0', rgb, a);
295     R_PolygonVertex(v4, '0 1 0', rgb, a);
296     R_EndPolygon();
297 }
298
299 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
300 {
301     R_BeginPolygon(pic, f);
302     R_PolygonVertex(o, '0 0 0', rgb, a);
303     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
304     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
305     R_PolygonVertex(o + up, '0 1 0', rgb, a);
306     R_EndPolygon();
307 }
308
309 void drawhealthbar(vector org, float rot, float h, vector sz, vector hotspot, float width, float theheight, float margin, float border, float align, vector rgb, float a, vector hrgb, float ha, float f)
310 {
311     vector o, ri, up;
312     float owidth; // outer width
313
314     hotspot = -1 * hotspot;
315
316     // hotspot-relative coordinates of the healthbar corners
317     o = hotspot;
318     ri = '1 0 0';
319     up = '0 1 0';
320
321     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
322     o = Rotate(o, rot) + org;
323     ri = Rotate(ri, rot);
324     up = Rotate(up, rot);
325
326     owidth = width + 2 * border;
327     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
328
329     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
330     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
331     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
332     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
333     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
334 }
335
336 // returns location of sprite text
337 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
338 {
339     float size   = 9.0 * t;
340     float border = 1.5 * t;
341     float margin = 4.0 * t;
342
343     float borderDiag = border * 1.414;
344     vector arrowX  = eX * size;
345     vector arrowY  = eY * (size+borderDiag);
346     vector borderX = eX * (size+borderDiag);
347     vector borderY = eY * (size+borderDiag+border);
348
349     R_BeginPolygon("", DRAWFLAG_NORMAL);
350     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
351     R_PolygonVertex(o + Rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
352     R_PolygonVertex(o + Rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
353     R_PolygonVertex(o + Rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
354     R_PolygonVertex(o + Rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
355     R_EndPolygon();
356
357     R_BeginPolygon("", DRAWFLAG_ADDITIVE);
358     R_PolygonVertex(o + Rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
359     R_PolygonVertex(o + Rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
360     R_PolygonVertex(o + Rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
361     R_EndPolygon();
362
363     return o + Rotate(eY * (borderDiag+size+margin), ang);
364 }
365
366 // returns location of sprite healthbar
367 vector drawspritetext(vector o, float ang, float minwidth, vector rgb, float a, vector fontsize, string s)
368 {
369     float algnx, algny;
370     float sw, w, h;
371     float aspect, sa, ca;
372
373     sw = stringwidth(s, false, fontsize);
374     if (sw > minwidth)
375         w = sw;
376     else
377         w = minwidth;
378     h = fontsize.y;
379
380     // how do corners work?
381     aspect = vid_conwidth / vid_conheight;
382     sa = sin(ang);
383     ca = cos(ang) * aspect;
384     if (fabs(sa) > fabs(ca))
385     {
386         algnx = (sa < 0);
387         float f = fabs(sa);
388         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
389     }
390     else
391     {
392         float f = fabs(ca);
393         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
394         algny = (ca < 0);
395     }
396
397     // align
398     o.x -= w * algnx;
399     o.y -= h * algny;
400
401     // we want to be onscreen
402     if (o.x < 0)
403         o.x = 0;
404     if (o.y < 0)
405         o.y = 0;
406     if (o.x > vid_conwidth - w)
407         o.x = vid_conwidth - w;
408     if (o.y > vid_conheight - h)
409         o.x = vid_conheight - h;
410
411     o.x += 0.5 * (w - sw);
412
413     drawstring(o, s, fontsize, rgb, a, DRAWFLAG_NORMAL);
414
415     o.x += 0.5 * sw;
416     o.y += 0.5 * h;
417
418     return o;
419 }
420
421 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
422 {
423     vector yvec = '0.299 0.587 0.114';
424     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
425 }
426
427 vector fixrgbexcess(vector rgb)
428 {
429     if (rgb.x > 1) {
430         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
431         if (rgb.y > 1) {
432             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
433             if (rgb.z > 1) rgb.z = 1;
434         } else if (rgb.z > 1) {
435             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
436             if (rgb.y > 1) rgb.y = 1;
437         }
438     } else if (rgb.y > 1) {
439         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
440         if (rgb.x > 1) {
441             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
442             if (rgb.z > 1) rgb.z = 1;
443         } else if (rgb.z > 1) {
444             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
445             if (rgb.x > 1) rgb.x = 1;
446         }
447     } else if (rgb.z > 1) {
448         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
449         if (rgb.x > 1) {
450             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
451             if (rgb.y > 1) rgb.y = 1;
452         } else if (rgb.y > 1) {
453             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
454             if (rgb.x > 1) rgb.x = 1;
455         }
456     }
457     return rgb;
458 }
459
460 void Draw_WaypointSprite(entity this)
461 {
462     if (this.lifetime > 0)
463         this.alpha = pow(bound(0, (this.fadetime - time) / this.lifetime, 1), waypointsprite_timealphaexponent);
464     else
465         this.alpha = 1;
466
467     if (this.hideflags & 2)
468         return; // radar only
469
470     if (autocvar_cl_hidewaypoints >= 2)
471         return;
472
473     if (this.hideflags & 1 && autocvar_cl_hidewaypoints)
474         return; // fixed waypoint
475
476     InterpolateOrigin_Do(this);
477
478     float t = entcs_GetTeam(player_localnum) + 1;
479     string spriteimage = "";
480
481     // choose the sprite
482     switch (this.rule)
483     {
484         case SPRITERULE_SPECTATOR:
485             if (!(
486                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
487             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
488                 ))
489                 return;
490             spriteimage = this.netname;
491             break;
492         case SPRITERULE_DEFAULT:
493             if (this.team)
494             {
495                 if (this.team == t)
496                     spriteimage = this.netname;
497                 else
498                     spriteimage = "";
499             }
500             else
501                 spriteimage = this.netname;
502             break;
503         case SPRITERULE_TEAMPLAY:
504             if (t == NUM_SPECTATOR + 1)
505                 spriteimage = this.netname3;
506             else if (this.team == t)
507                 spriteimage = this.netname2;
508             else
509                 spriteimage = this.netname;
510             break;
511         default:
512             error("Invalid waypointsprite rule!");
513             break;
514     }
515
516     if (spriteimage == "")
517         return;
518
519     ++waypointsprite_newcount;
520
521     float dist = vlen(this.origin - view_origin);
522     float a = this.alpha * autocvar_hud_panel_fg_alpha;
523
524     if (this.maxdistance > waypointsprite_normdistance)
525         a *= pow(bound(0, (this.maxdistance - dist) / (this.maxdistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent);
526     else if (this.maxdistance > 0)
527         a *= pow(bound(0, (waypointsprite_fadedistance - dist) / (waypointsprite_fadedistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent) * (1 - waypointsprite_minalpha) + waypointsprite_minalpha;
528
529     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
530     if (rgb == '0 0 0')
531     {
532         this.teamradar_color = '1 0 1';
533         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it\n", spriteimage);
534     }
535
536     if (time - floor(time) > 0.5)
537     {
538         if (this.helpme && time < this.helpme)
539             a *= SPRITE_HELPME_BLINK;
540         else if (this.lifetime > 0) // fading out waypoints don't blink
541             a *= spritelookupblinkvalue(this, spriteimage);
542     }
543
544     if (a > 1)
545     {
546         rgb *= a;
547         a = 1;
548     }
549
550     if (a <= 0.003)
551         return;
552
553     rgb = fixrgbexcess(rgb);
554
555     vector o;
556     float ang;
557
558     o = project_3d_to_2d(this.origin);
559     if (o.z < 0
560     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
561     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
562     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
563     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
564     {
565         // scale it to be just in view
566         vector d;
567         float f1, f2;
568
569         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
570         ang = atan2(-d.x, -d.y);
571         if (o.z < 0)
572             ang += M_PI;
573
574         f1 = d.x / vid_conwidth;
575         f2 = d.y / vid_conheight;
576
577         if (max(f1, -f1) > max(f2, -f2)) {
578             if (d.z * f1 > 0) {
579                 // RIGHT edge
580                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
581             } else {
582                 // LEFT edge
583                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
584             }
585         } else {
586             if (d.z * f2 > 0) {
587                 // BOTTOM edge
588                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
589             } else {
590                 // TOP edge
591                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
592             }
593         }
594
595         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
596     }
597     else
598     {
599 #if 1
600         ang = M_PI;
601 #else
602         vector d;
603         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
604         ang = atan2(-d.x, -d.y);
605 #endif
606     }
607     o.z = 0;
608
609     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
610     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
611     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
612     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
613
614     float crosshairdistance = sqrt( pow(o.x - vid_conwidth/2, 2) + pow(o.y - vid_conheight/2, 2) );
615
616     t = waypointsprite_scale;
617     a *= waypointsprite_alpha;
618
619     {
620         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
621         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
622     }
623     if (edgedistance_min < waypointsprite_edgefadedistance) {
624         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
625         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
626     }
627     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
628         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
629         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
630     }
631
632     if (this.build_finished)
633     {
634         if (time < this.build_finished + 0.25)
635         {
636             if (time < this.build_started)
637                 this.health = this.build_starthealth;
638             else if (time < this.build_finished)
639                 this.health = (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth;
640             else
641                 this.health = 1;
642         }
643         else
644             this.health = -1;
645     }
646
647     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
648
649     string txt;
650     if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
651         txt = _("Spam");
652     else
653         txt = spritelookuptext(this, spriteimage);
654     if (this.helpme && time < this.helpme)
655         txt = sprintf(_("%s needing help!"), txt);
656     if (autocvar_g_waypointsprite_uppercase)
657         txt = strtoupper(txt);
658
659     draw_beginBoldFont();
660     if (this.health >= 0)
661     {
662         o = drawspritetext(o, ang, (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
663
664         float align, marg;
665         if (this.build_finished)
666             align = 0.5;
667         else
668             align = 0;
669         if (cos(ang) > 0)
670             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * waypointsprite_fontsize;
671         else
672             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * waypointsprite_fontsize;
673         drawhealthbar(
674                 o,
675                 0,
676                 this.health,
677                 '0 0 0',
678                 '0 0 0',
679                 SPRITE_HEALTHBAR_WIDTH * t,
680                 SPRITE_HEALTHBAR_HEIGHT * t,
681                 marg,
682                 SPRITE_HEALTHBAR_BORDER * t,
683                 align,
684                 rgb,
685                 a * SPRITE_HEALTHBAR_BORDERALPHA,
686                 rgb,
687                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
688                 DRAWFLAG_NORMAL
689                  );
690     }
691     else
692     {
693         o = drawspritetext(o, ang, 0, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
694     }
695     draw_endBoldFont();
696 }
697
698 void WaypointSprite_Load_Frames(string ext)
699 {
700     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
701     if (dh < 0) return;
702     int ext_len = strlen(ext);
703     int n = search_getsize(dh);
704     for (int i = 0; i < n; ++i)
705     {
706         string s = search_getfilename(dh, i);
707         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
708
709         int o = strstrofs(s, "_frame", 0);
710         string sname = strcat("/spriteframes/", substring(s, 0, o));
711         string sframes = substring(s, o + 6, strlen(s) - o - 6);
712         int f = stof(sframes) + 1;
713         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
714     }
715     search_end(dh);
716 }
717
718 void WaypointSprite_Load();
719 STATIC_INIT(WaypointSprite_Load) {
720     WaypointSprite_Load();
721     WaypointSprite_Load_Frames(".tga");
722     WaypointSprite_Load_Frames(".jpg");
723 }
724 void WaypointSprite_Load()
725 {
726     waypointsprite_fadedistance = vlen(mi_scale);
727     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
728     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
729     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
730     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
731     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
732     waypointsprite_scale = autocvar_g_waypointsprite_scale;
733     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
734     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
735     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
736     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
737     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
738     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
739     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
740     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
741     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
742     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
743     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
744     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
745     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
746     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
747     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
748
749     waypointsprite_count = waypointsprite_newcount;
750     waypointsprite_newcount = 0;
751 }
752 #endif
753
754 #ifdef SVQC
755 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
756 {
757     string m1 = _m1.netname;
758     string m2 = _m2.netname;
759     string m3 = _m3.netname;
760     if (m1 != e.model1)
761     {
762         e.model1 = m1;
763         e.SendFlags |= 2;
764     }
765     if (m2 != e.model2)
766     {
767         e.model2 = m2;
768         e.SendFlags |= 4;
769     }
770     if (m3 != e.model3)
771     {
772         e.model3 = m3;
773         e.SendFlags |= 8;
774     }
775 }
776
777 void WaypointSprite_UpdateHealth(entity e, float f)
778 {
779     f = bound(0, f, e.max_health);
780     if (f != e.health || e.pain_finished)
781     {
782         e.health = f;
783         e.pain_finished = 0;
784         e.SendFlags |= 0x80;
785     }
786 }
787
788 void WaypointSprite_UpdateMaxHealth(entity e, float f)
789 {
790     if (f != e.max_health || e.pain_finished)
791     {
792         e.max_health = f;
793         e.pain_finished = 0;
794         e.SendFlags |= 0x80;
795     }
796 }
797
798 void WaypointSprite_UpdateBuildFinished(entity e, float f)
799 {
800     if (f != e.pain_finished || e.max_health)
801     {
802         e.max_health = 0;
803         e.pain_finished = f;
804         e.SendFlags |= 0x80;
805     }
806 }
807
808 void WaypointSprite_UpdateOrigin(entity e, vector o)
809 {
810     if (o != e.origin)
811     {
812         setorigin(e, o);
813         e.SendFlags |= 64;
814     }
815 }
816
817 void WaypointSprite_UpdateRule(entity e, float t, float r)
818 {
819     // no check, as this is never called without doing an actual change (usually only once)
820     e.rule = r;
821     e.team = t;
822     e.SendFlags |= 1;
823 }
824
825 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
826 {
827     // no check, as this is never called without doing an actual change (usually only once)
828     int i = icon.m_id;
829     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
830     e.colormod = col;
831     e.SendFlags |= 32;
832 }
833
834 void WaypointSprite_Ping(entity e)
835 {
836     // anti spam
837     if (time < e.waypointsprite_pingtime) return;
838     e.waypointsprite_pingtime = time + 0.3;
839     // ALWAYS sends (this causes a radar circle), thus no check
840     e.cnt |= BIT(7);
841     e.SendFlags |= 32;
842 }
843
844 void WaypointSprite_HelpMePing(entity e)
845 {
846     WaypointSprite_Ping(e);
847     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
848     e.SendFlags |= 32;
849 }
850
851 void WaypointSprite_FadeOutIn(entity e, float t)
852 {
853     if (!e.fade_time)
854     {
855         e.fade_time = t;
856         e.teleport_time = time + t;
857     }
858     else if (t < (e.teleport_time - time))
859     {
860         // accelerate the waypoint's dying
861         // ensure:
862         //   (e.teleport_time - time) / wp.fade_time stays
863         //   e.teleport_time = time + fadetime
864         float current_fadetime = e.teleport_time - time;
865         e.teleport_time = time + t;
866         if (e.fade_time < 0)
867                 e.fade_time = -e.fade_time;
868         e.fade_time = e.fade_time * t / current_fadetime;
869     }
870
871     e.SendFlags |= 16;
872 }
873
874 void WaypointSprite_Init()
875 {
876     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
877     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
878     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
879 }
880
881 void WaypointSprite_Kill(entity wp)
882 {
883     if (!wp) return;
884     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
885     delete(wp);
886 }
887
888 void WaypointSprite_Disown(entity wp, float fadetime)
889 {
890     if (!wp) return;
891     if (wp.classname != "sprite_waypoint")
892     {
893         backtrace("Trying to disown a non-waypointsprite");
894         return;
895     }
896     if (wp.owner)
897     {
898         if (wp.exteriormodeltoclient == wp.owner)
899             wp.exteriormodeltoclient = NULL;
900         wp.owner.(wp.owned_by_field) = NULL;
901         wp.owner = NULL;
902
903         WaypointSprite_FadeOutIn(wp, fadetime);
904     }
905 }
906
907 void WaypointSprite_Think(entity this)
908 {
909     bool doremove = false;
910
911     if (this.fade_time && time >= this.teleport_time)
912     {
913         doremove = true;
914     }
915
916     if (this.exteriormodeltoclient)
917         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
918
919     if (doremove)
920         WaypointSprite_Kill(this);
921     else
922         this.nextthink = time; // WHY?!?
923 }
924
925 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
926 {
927     // personal waypoints
928     if (this.enemy && this.enemy != view)
929         return false;
930
931     // team waypoints
932     if (this.rule == SPRITERULE_SPECTATOR)
933     {
934         if (!autocvar_sv_itemstime)
935             return false;
936         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
937             return false;
938     }
939     else if (this.team && this.rule == SPRITERULE_DEFAULT)
940     {
941         if (this.team != view.team)
942             return false;
943         if (!IS_PLAYER(view))
944             return false;
945     }
946
947     return true;
948 }
949
950 entity WaypointSprite_getviewentity(entity e)
951 {
952     if (IS_SPEC(e)) e = e.enemy;
953     /* TODO idea (check this breaks nothing)
954     else if (e.classname == "observer")
955         e = NULL;
956     */
957     return e;
958 }
959
960 float WaypointSprite_isteammate(entity e, entity e2)
961 {
962     if (teamplay)
963         return e2.team == e.team;
964     return e2 == e;
965 }
966
967 bool WaypointSprite_Customize(entity this, entity client)
968 {
969     // this is not in SendEntity because it shall run every frame, not just every update
970
971     // make spectators see what the player would see
972     entity e = WaypointSprite_getviewentity(client);
973
974     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
975         return false;
976
977     return this.waypointsprite_visible_for_player(this, client, e);
978 }
979
980 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
981
982 void WaypointSprite_Reset(entity this)
983 {
984     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
985
986     if (this.fade_time)
987         WaypointSprite_Kill(this);
988 }
989
990 entity WaypointSprite_Spawn(
991     entity spr, // sprite
992     float _lifetime, float maxdistance, // lifetime, max distance
993     entity ref, vector ofs, // position
994     entity showto, float t, // show to whom? Use a flag to indicate a team
995     entity own, .entity ownfield, // remove when own gets killed
996     float hideable, // true when it should be controlled by cl_hidewaypoints
997     entity icon // initial icon
998 )
999 {
1000     entity wp = new(sprite_waypoint);
1001     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1002     if(_lifetime < 0)
1003         _lifetime = -_lifetime;
1004     wp.teleport_time = time + _lifetime;
1005     wp.exteriormodeltoclient = ref;
1006     if (ref)
1007     {
1008         wp.view_ofs = ofs;
1009         setorigin(wp, ref.origin + ofs);
1010     }
1011     else
1012         setorigin(wp, ofs);
1013     wp.enemy = showto;
1014     wp.team = t;
1015     wp.owner = own;
1016     wp.currentammo = hideable;
1017     if (own)
1018     {
1019         if (own.(ownfield))
1020             delete(own.(ownfield));
1021         own.(ownfield) = wp;
1022         wp.owned_by_field = ownfield;
1023     }
1024     wp.fade_rate = maxdistance;
1025     setthink(wp, WaypointSprite_Think);
1026     wp.nextthink = time;
1027     wp.model1 = spr.netname;
1028     setcefc(wp, WaypointSprite_Customize);
1029     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1030     wp.reset2 = WaypointSprite_Reset;
1031     wp.cnt = icon.m_id;
1032     wp.colormod = spr.m_color;
1033     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1034     return wp;
1035 }
1036
1037 entity WaypointSprite_SpawnFixed(
1038     entity spr,
1039     vector ofs,
1040     entity own,
1041     .entity ownfield,
1042     entity icon // initial icon
1043 )
1044 {
1045     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1046 }
1047
1048 entity WaypointSprite_DeployFixed(
1049     entity spr,
1050     float limited_range,
1051     entity player,
1052     vector ofs,
1053     entity icon // initial icon
1054 )
1055 {
1056     float t;
1057     if (teamplay)
1058         t = player.team;
1059     else
1060         t = 0;
1061     float maxdistance;
1062     if (limited_range)
1063         maxdistance = waypointsprite_limitedrange;
1064     else
1065         maxdistance = 0;
1066     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1067 }
1068
1069 entity WaypointSprite_DeployPersonal(
1070     entity spr,
1071     entity player,
1072     vector ofs,
1073     entity icon // initial icon
1074 )
1075 {
1076     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1077 }
1078
1079 entity WaypointSprite_Attach(
1080     entity spr,
1081     entity player,
1082     float limited_range,
1083     entity icon // initial icon
1084 )
1085 {
1086     float t;
1087     if (player.waypointsprite_attachedforcarrier)
1088         return NULL; // can't attach to FC
1089     if (teamplay)
1090         t = player.team;
1091     else
1092         t = 0;
1093     float maxdistance;
1094     if (limited_range)
1095         maxdistance = waypointsprite_limitedrange;
1096     else
1097         maxdistance = 0;
1098     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1099 }
1100
1101 entity WaypointSprite_AttachCarrier(
1102     entity spr,
1103     entity carrier,
1104     entity icon // initial icon and color
1105 )
1106 {
1107     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1108     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1109     if (carrier.health)
1110     {
1111         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1112         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1113     }
1114     return e;
1115 }
1116
1117 void WaypointSprite_DetachCarrier(entity carrier)
1118 {
1119     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1120 }
1121
1122 void WaypointSprite_ClearPersonal(entity this)
1123 {
1124     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1125 }
1126
1127 void WaypointSprite_ClearOwned(entity this)
1128 {
1129     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1130     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1131     WaypointSprite_Kill(this.waypointsprite_attached);
1132 }
1133
1134 void WaypointSprite_PlayerDead(entity this)
1135 {
1136     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1137     WaypointSprite_DetachCarrier(this);
1138 }
1139
1140 void WaypointSprite_PlayerGone(entity this)
1141 {
1142     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1143     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1144     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1145     WaypointSprite_DetachCarrier(this);
1146 }
1147 #endif