]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
1cb06abedf3c2d857c5570cebc2c4a0471618344
[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, {
263         return it.m_name;
264     });
265
266     return s;
267 }
268
269 string spritelookupicon(entity this, string s)
270 {
271     // TODO: needs icons! //if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
272     if (s == WP_Weapon.netname) return Weapons_from(this.wp_extra).model2;
273     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_icon;
274     if (s == WP_Vehicle.netname) return Vehicles_from(this.wp_extra).m_icon;
275     //if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).m_icon;
276     if (MUTATOR_CALLHOOK(WP_Format, this, s))
277     {
278         return M_ARGV(4, string);
279     }
280
281     // need to loop, as our netname could be one of three
282     FOREACH(Waypoints, it.netname == s, {
283         return it.m_icon;
284     });
285
286     return s;
287 }
288 #endif
289
290 #ifdef CSQC
291 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
292 {
293     vector v1, v2, v3, v4;
294
295     hotspot = -1 * hotspot;
296
297     // hotspot-relative coordinates of the corners
298     v1 = hotspot;
299     v2 = hotspot + '1 0 0' * sz.x;
300     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
301     v4 = hotspot                  + '0 1 0' * sz.y;
302
303     // rotate them, and make them absolute
304     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
305     v1 = Rotate(v1, rot) + org;
306     v2 = Rotate(v2, rot) + org;
307     v3 = Rotate(v3, rot) + org;
308     v4 = Rotate(v4, rot) + org;
309
310     // draw them
311     R_BeginPolygon(pic, f);
312     R_PolygonVertex(v1, '0 0 0', rgb, a);
313     R_PolygonVertex(v2, '1 0 0', rgb, a);
314     R_PolygonVertex(v3, '1 1 0', rgb, a);
315     R_PolygonVertex(v4, '0 1 0', rgb, a);
316     R_EndPolygon();
317 }
318
319 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
320 {
321     R_BeginPolygon(pic, f);
322     R_PolygonVertex(o, '0 0 0', rgb, a);
323     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
324     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
325     R_PolygonVertex(o + up, '0 1 0', rgb, a);
326     R_EndPolygon();
327 }
328
329 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)
330 {
331     vector o, ri, up;
332     float owidth; // outer width
333
334     hotspot = -1 * hotspot;
335
336     // hotspot-relative coordinates of the healthbar corners
337     o = hotspot;
338     ri = '1 0 0';
339     up = '0 1 0';
340
341     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
342     o = Rotate(o, rot) + org;
343     ri = Rotate(ri, rot);
344     up = Rotate(up, rot);
345
346     owidth = width + 2 * border;
347     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
348
349     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
350     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
351     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
352     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
353     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
354 }
355
356 // returns location of sprite text
357 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
358 {
359     float size   = 9.0 * t;
360     float border = 1.5 * t;
361     float margin = 4.0 * t;
362
363     float borderDiag = border * 1.414;
364     vector arrowX  = eX * size;
365     vector arrowY  = eY * (size+borderDiag);
366     vector borderX = eX * (size+borderDiag);
367     vector borderY = eY * (size+borderDiag+border);
368
369     R_BeginPolygon("", DRAWFLAG_NORMAL);
370     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
371     R_PolygonVertex(o + Rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
372     R_PolygonVertex(o + Rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
373     R_PolygonVertex(o + Rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
374     R_PolygonVertex(o + Rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
375     R_EndPolygon();
376
377     R_BeginPolygon("", DRAWFLAG_ADDITIVE);
378     R_PolygonVertex(o + Rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
379     R_PolygonVertex(o + Rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
380     R_PolygonVertex(o + Rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
381     R_EndPolygon();
382
383     return o + Rotate(eY * (borderDiag+size+margin), ang);
384 }
385
386 // returns location of sprite healthbar
387 vector drawsprite_TextOrIcon(bool is_text, vector o, float ang, float minwidth, vector rgb, float a, vector sz, string str)
388 {
389     float algnx, algny;
390     float sw, w, h;
391     float aspect, sa, ca;
392
393     if (is_text)
394         sw = stringwidth(str, false, sz);
395     else
396         sw = sz.x;
397
398     if (sw > minwidth)
399         w = sw;
400     else
401         w = minwidth;
402     h = sz.y;
403
404     // how do corners work?
405     aspect = vid_conwidth / vid_conheight;
406     sa = sin(ang);
407     ca = cos(ang) * aspect;
408     if (fabs(sa) > fabs(ca))
409     {
410         algnx = (sa < 0);
411         float f = fabs(sa);
412         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
413     }
414     else
415     {
416         float f = fabs(ca);
417         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
418         algny = (ca < 0);
419     }
420
421     // align
422     o.x -= w * algnx;
423     o.y -= h * algny;
424
425     // we want to be onscreen
426     if (o.x < 0)
427         o.x = 0;
428     if (o.y < 0)
429         o.y = 0;
430     if (o.x > vid_conwidth - w)
431         o.x = vid_conwidth - w;
432     if (o.y > vid_conheight - h)
433         o.y = vid_conheight - h;
434
435     o.x += 0.5 * (w - sw);
436
437     if (is_text)
438         drawstring(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
439     else
440         drawpic(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
441
442     o.x += 0.5 * sw;
443     o.y += 0.5 * h;
444
445     return o;
446 }
447
448 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
449 {
450     vector yvec = '0.299 0.587 0.114';
451     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
452 }
453
454 vector fixrgbexcess(vector rgb)
455 {
456     if (rgb.x > 1) {
457         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
458         if (rgb.y > 1) {
459             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
460             if (rgb.z > 1) rgb.z = 1;
461         } else if (rgb.z > 1) {
462             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
463             if (rgb.y > 1) rgb.y = 1;
464         }
465     } else if (rgb.y > 1) {
466         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
467         if (rgb.x > 1) {
468             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
469             if (rgb.z > 1) rgb.z = 1;
470         } else if (rgb.z > 1) {
471             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
472             if (rgb.x > 1) rgb.x = 1;
473         }
474     } else if (rgb.z > 1) {
475         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
476         if (rgb.x > 1) {
477             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
478             if (rgb.y > 1) rgb.y = 1;
479         } else if (rgb.y > 1) {
480             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
481             if (rgb.x > 1) rgb.x = 1;
482         }
483     }
484     return rgb;
485 }
486
487 void Draw_WaypointSprite(entity this)
488 {
489     if (this.lifetime > 0)
490         this.alpha = (bound(0, (this.fadetime - time) / this.lifetime, 1) ** waypointsprite_timealphaexponent);
491     else
492         this.alpha = 1;
493
494     if (this.hideflags & 2)
495         return; // radar only
496
497     if (autocvar_cl_hidewaypoints >= 2)
498         return;
499
500     if (this.hideflags & 1 && autocvar_cl_hidewaypoints)
501         return; // fixed waypoint
502
503     InterpolateOrigin_Do(this);
504
505     float t = entcs_GetTeam(player_localnum) + 1;
506     string spriteimage = "";
507
508     // choose the sprite
509     switch (this.rule)
510     {
511         case SPRITERULE_SPECTATOR:
512             if (!(
513                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
514             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
515                 ))
516                 return;
517             spriteimage = this.netname;
518             break;
519         case SPRITERULE_DEFAULT:
520             if (this.team)
521             {
522                 if (this.team == t)
523                     spriteimage = this.netname;
524                 else
525                     spriteimage = "";
526             }
527             else
528                 spriteimage = this.netname;
529             break;
530         case SPRITERULE_TEAMPLAY:
531             if (t == NUM_SPECTATOR + 1)
532                 spriteimage = this.netname3;
533             else if (this.team == t)
534                 spriteimage = this.netname2;
535             else
536                 spriteimage = this.netname;
537             break;
538         default:
539             error("Invalid waypointsprite rule!");
540             break;
541     }
542
543     if (spriteimage == "")
544         return;
545
546     ++waypointsprite_newcount;
547
548     float dist = vlen(this.origin - view_origin);
549     float a = this.alpha * autocvar_hud_panel_fg_alpha;
550
551     if (this.maxdistance > waypointsprite_normdistance)
552         a *= (bound(0, (this.maxdistance - dist) / (this.maxdistance - waypointsprite_normdistance), 1) ** waypointsprite_distancealphaexponent);
553     else if (this.maxdistance > 0)
554         a *= (bound(0, (waypointsprite_fadedistance - dist) / (waypointsprite_fadedistance - waypointsprite_normdistance), 1) ** waypointsprite_distancealphaexponent) * (1 - waypointsprite_minalpha) + waypointsprite_minalpha;
555
556     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
557     if (rgb == '0 0 0')
558     {
559         this.teamradar_color = '1 0 1';
560         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it", spriteimage);
561     }
562
563     if (time - floor(time) > 0.5)
564     {
565         if (this.helpme && time < this.helpme)
566             a *= SPRITE_HELPME_BLINK;
567         else if (this.lifetime > 0) // fading out waypoints don't blink
568             a *= spritelookupblinkvalue(this, spriteimage);
569     }
570
571     if (a > 1)
572     {
573         rgb *= a;
574         a = 1;
575     }
576
577     if (a <= 0.003)
578         return;
579
580     rgb = fixrgbexcess(rgb);
581
582     vector o;
583     float ang;
584
585     o = project_3d_to_2d(this.origin);
586     if (o.z < 0
587     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
588     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
589     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
590     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
591     {
592         // scale it to be just in view
593         vector d;
594         float f1, f2;
595
596         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
597         ang = atan2(-d.x, -d.y);
598         if (o.z < 0)
599             ang += M_PI;
600
601         f1 = d.x / vid_conwidth;
602         f2 = d.y / vid_conheight;
603
604         if (max(f1, -f1) > max(f2, -f2)) {
605             if (d.z * f1 > 0) {
606                 // RIGHT edge
607                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
608             } else {
609                 // LEFT edge
610                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
611             }
612         } else {
613             if (d.z * f2 > 0) {
614                 // BOTTOM edge
615                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
616             } else {
617                 // TOP edge
618                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
619             }
620         }
621
622         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
623     }
624     else
625     {
626 #if 1
627         ang = M_PI;
628 #else
629         vector d;
630         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
631         ang = atan2(-d.x, -d.y);
632 #endif
633     }
634     o.z = 0;
635
636     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
637     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
638     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
639     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
640
641     float crosshairdistance = sqrt( ((o.x - vid_conwidth/2) ** 2) + ((o.y - vid_conheight/2) ** 2) );
642
643     t = waypointsprite_scale;
644     a *= waypointsprite_alpha;
645
646     {
647         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
648         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
649     }
650     if (edgedistance_min < waypointsprite_edgefadedistance) {
651         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
652         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
653     }
654     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
655         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
656         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
657     }
658
659     if (this.build_finished)
660     {
661         if (time < this.build_finished + 0.25)
662         {
663             if (time < this.build_started)
664                 this.health = this.build_starthealth;
665             else if (time < this.build_finished)
666                 this.health = (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth;
667             else
668                 this.health = 1;
669         }
670         else
671             this.health = -1;
672     }
673
674     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
675
676     vector iconcolor = ((autocvar_g_waypointsprite_iconcolor) ? '1 1 1' : rgb);
677     string spr_icon = spritelookupicon(this, spriteimage);
678     string pic = spr_icon;
679     bool icon_found = !(!spr_icon || spr_icon == "");
680     if (icon_found) // it's valid, but let's make sure it exists!
681     {
682         pic = strcat(hud_skin_path, "/", spr_icon);
683         if(precache_pic(pic) == "")
684         {
685             pic = strcat("gfx/hud/default/", spr_icon);
686             if(!precache_pic(pic))
687                 icon_found = false;
688         }
689     }
690
691     string txt = string_null;
692     if (autocvar_g_waypointsprite_text || !icon_found)
693     {
694         if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
695             txt = _("Spam");
696         else
697             txt = spritelookuptext(this, spriteimage);
698         if (this.helpme && time < this.helpme)
699             txt = sprintf(_("%s needing help!"), txt);
700         if (autocvar_g_waypointsprite_uppercase)
701             txt = strtoupper(txt);
702     }
703
704     draw_beginBoldFont();
705     bool is_text = (autocvar_g_waypointsprite_text || !icon_found);
706     vector sz;
707     if (is_text)
708         sz = waypointsprite_fontsize * '1 1 0';
709     else
710         sz = autocvar_g_waypointsprite_iconsize * '1 1 0';
711     if (this.health >= 0)
712     {
713         float align = 0, marg;
714         if (this.build_finished)
715             align = 0.5;
716         else
717             align = 0;
718         if (cos(ang) > 0)
719             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * sz.y;
720         else
721             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * sz.y;
722
723         float minwidth = (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t;
724         o = drawsprite_TextOrIcon(is_text, o, ang, minwidth, iconcolor, a, sz, pic);
725         drawhealthbar(
726                 o,
727                 0,
728                 this.health,
729                 '0 0 0',
730                 '0 0 0',
731                 SPRITE_HEALTHBAR_WIDTH * t,
732                 SPRITE_HEALTHBAR_HEIGHT * t,
733                 marg,
734                 SPRITE_HEALTHBAR_BORDER * t,
735                 align,
736                 rgb,
737                 a * SPRITE_HEALTHBAR_BORDERALPHA,
738                 rgb,
739                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
740                 DRAWFLAG_NORMAL
741                  );
742     }
743     else
744     {
745         drawsprite_TextOrIcon(is_text, o, ang, 0, iconcolor, a, sz, pic);
746     }
747
748     draw_endBoldFont();
749 }
750
751 void WaypointSprite_Load_Frames(string ext)
752 {
753     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
754     if (dh < 0) return;
755     int ext_len = strlen(ext);
756     int n = search_getsize(dh);
757     for (int i = 0; i < n; ++i)
758     {
759         string s = search_getfilename(dh, i);
760         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
761
762         int o = strstrofs(s, "_frame", 0);
763         string sname = strcat("/spriteframes/", substring(s, 0, o));
764         string sframes = substring(s, o + 6, strlen(s) - o - 6);
765         int f = stof(sframes) + 1;
766         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
767     }
768     search_end(dh);
769 }
770
771 void WaypointSprite_Load();
772 STATIC_INIT(WaypointSprite_Load) {
773     WaypointSprite_Load();
774     WaypointSprite_Load_Frames(".tga");
775     WaypointSprite_Load_Frames(".jpg");
776 }
777 void WaypointSprite_Load()
778 {
779     waypointsprite_fadedistance = vlen(mi_scale);
780     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
781     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
782     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
783     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
784     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
785     waypointsprite_scale = autocvar_g_waypointsprite_scale;
786     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
787     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
788     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
789     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
790     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
791     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
792     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
793     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
794     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
795     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
796     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
797     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
798     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
799     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
800     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
801
802     waypointsprite_count = waypointsprite_newcount;
803     waypointsprite_newcount = 0;
804 }
805 #endif
806
807 #ifdef SVQC
808 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
809 {
810     string m1 = _m1.netname;
811     string m2 = _m2.netname;
812     string m3 = _m3.netname;
813     if (m1 != e.model1)
814     {
815         e.model1 = m1;
816         e.SendFlags |= 2;
817     }
818     if (m2 != e.model2)
819     {
820         e.model2 = m2;
821         e.SendFlags |= 4;
822     }
823     if (m3 != e.model3)
824     {
825         e.model3 = m3;
826         e.SendFlags |= 8;
827     }
828 }
829
830 void WaypointSprite_UpdateHealth(entity e, float f)
831 {
832     f = bound(0, f, e.max_health);
833     if (f != e.health || e.pain_finished)
834     {
835         e.health = f;
836         e.pain_finished = 0;
837         e.SendFlags |= 0x80;
838     }
839 }
840
841 void WaypointSprite_UpdateMaxHealth(entity e, float f)
842 {
843     if (f != e.max_health || e.pain_finished)
844     {
845         e.max_health = f;
846         e.pain_finished = 0;
847         e.SendFlags |= 0x80;
848     }
849 }
850
851 void WaypointSprite_UpdateBuildFinished(entity e, float f)
852 {
853     if (f != e.pain_finished || e.max_health)
854     {
855         e.max_health = 0;
856         e.pain_finished = f;
857         e.SendFlags |= 0x80;
858     }
859 }
860
861 void WaypointSprite_UpdateOrigin(entity e, vector o)
862 {
863     if (o != e.origin)
864     {
865         setorigin(e, o);
866         e.SendFlags |= 64;
867     }
868 }
869
870 void WaypointSprite_UpdateRule(entity e, float t, float r)
871 {
872     // no check, as this is never called without doing an actual change (usually only once)
873     e.rule = r;
874     e.team = t;
875     e.SendFlags |= 1;
876 }
877
878 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
879 {
880     // no check, as this is never called without doing an actual change (usually only once)
881     int i = icon.m_id;
882     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
883     e.colormod = col;
884     e.SendFlags |= 32;
885 }
886
887 void WaypointSprite_Ping(entity e)
888 {
889     // anti spam
890     if (time < e.waypointsprite_pingtime) return;
891     e.waypointsprite_pingtime = time + 0.3;
892     // ALWAYS sends (this causes a radar circle), thus no check
893     e.cnt |= BIT(7);
894     e.SendFlags |= 32;
895 }
896
897 void WaypointSprite_HelpMePing(entity e)
898 {
899     WaypointSprite_Ping(e);
900     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
901     e.SendFlags |= 32;
902 }
903
904 void WaypointSprite_FadeOutIn(entity e, float t)
905 {
906     if (!e.fade_time)
907     {
908         e.fade_time = t;
909         e.teleport_time = time + t;
910     }
911     else if (t < (e.teleport_time - time))
912     {
913         // accelerate the waypoint's dying
914         // ensure:
915         //   (e.teleport_time - time) / wp.fade_time stays
916         //   e.teleport_time = time + fadetime
917         float current_fadetime = e.teleport_time - time;
918         e.teleport_time = time + t;
919         if (e.fade_time < 0)
920                 e.fade_time = -e.fade_time;
921         e.fade_time = e.fade_time * t / current_fadetime;
922     }
923
924     e.SendFlags |= 16;
925 }
926
927 void WaypointSprite_Init()
928 {
929     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
930     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
931     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
932 }
933
934 void WaypointSprite_Kill(entity wp)
935 {
936     if (!wp) return;
937     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
938     delete(wp);
939 }
940
941 void WaypointSprite_Disown(entity wp, float fadetime)
942 {
943     if (!wp) return;
944     if (wp.classname != "sprite_waypoint")
945     {
946         backtrace("Trying to disown a non-waypointsprite");
947         return;
948     }
949     if (wp.owner)
950     {
951         if (wp.exteriormodeltoclient == wp.owner)
952             wp.exteriormodeltoclient = NULL;
953         wp.owner.(wp.owned_by_field) = NULL;
954         wp.owner = NULL;
955
956         WaypointSprite_FadeOutIn(wp, fadetime);
957     }
958 }
959
960 void WaypointSprite_Think(entity this)
961 {
962     bool doremove = false;
963
964     if (this.fade_time && time >= this.teleport_time)
965     {
966         doremove = true;
967     }
968
969     if (this.exteriormodeltoclient)
970         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
971
972     if (doremove)
973         WaypointSprite_Kill(this);
974     else
975         this.nextthink = time; // WHY?!?
976 }
977
978 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
979 {
980     // personal waypoints
981     if (this.enemy && this.enemy != view)
982         return false;
983
984     // team waypoints
985     if (this.rule == SPRITERULE_SPECTATOR)
986     {
987         if (!autocvar_sv_itemstime)
988             return false;
989         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
990             return false;
991     }
992     else if (this.team && this.rule == SPRITERULE_DEFAULT)
993     {
994         if (this.team != view.team)
995             return false;
996         if (!IS_PLAYER(view))
997             return false;
998     }
999
1000     return true;
1001 }
1002
1003 entity WaypointSprite_getviewentity(entity e)
1004 {
1005     if (IS_SPEC(e)) e = e.enemy;
1006     /* TODO idea (check this breaks nothing)
1007     else if (e.classname == "observer")
1008         e = NULL;
1009     */
1010     return e;
1011 }
1012
1013 float WaypointSprite_isteammate(entity e, entity e2)
1014 {
1015     if (teamplay)
1016         return e2.team == e.team;
1017     return e2 == e;
1018 }
1019
1020 bool WaypointSprite_Customize(entity this, entity client)
1021 {
1022     // this is not in SendEntity because it shall run every frame, not just every update
1023
1024     // make spectators see what the player would see
1025     entity e = WaypointSprite_getviewentity(client);
1026
1027     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
1028         return false;
1029
1030     return this.waypointsprite_visible_for_player(this, client, e);
1031 }
1032
1033 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
1034
1035 void WaypointSprite_Reset(entity this)
1036 {
1037     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
1038
1039     if (this.fade_time)
1040         WaypointSprite_Kill(this);
1041 }
1042
1043 entity WaypointSprite_Spawn(
1044     entity spr, // sprite
1045     float _lifetime, float maxdistance, // lifetime, max distance
1046     entity ref, vector ofs, // position
1047     entity showto, float t, // show to whom? Use a flag to indicate a team
1048     entity own, .entity ownfield, // remove when own gets killed
1049     float hideable, // true when it should be controlled by cl_hidewaypoints
1050     entity icon // initial icon
1051 )
1052 {
1053     entity wp = new(sprite_waypoint);
1054     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1055     if(_lifetime < 0)
1056         _lifetime = -_lifetime;
1057     wp.teleport_time = time + _lifetime;
1058     wp.exteriormodeltoclient = ref;
1059     if (ref)
1060     {
1061         wp.view_ofs = ofs;
1062         setorigin(wp, ref.origin + ofs);
1063     }
1064     else
1065         setorigin(wp, ofs);
1066     wp.enemy = showto;
1067     wp.team = t;
1068     wp.owner = own;
1069     wp.currentammo = hideable;
1070     if (own)
1071     {
1072         if (own.(ownfield))
1073             delete(own.(ownfield));
1074         own.(ownfield) = wp;
1075         wp.owned_by_field = ownfield;
1076     }
1077     wp.fade_rate = maxdistance;
1078     setthink(wp, WaypointSprite_Think);
1079     wp.nextthink = time;
1080     wp.model1 = spr.netname;
1081     setcefc(wp, WaypointSprite_Customize);
1082     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1083     wp.reset2 = WaypointSprite_Reset;
1084     wp.cnt = icon.m_id;
1085     wp.colormod = spr.m_color;
1086     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1087     return wp;
1088 }
1089
1090 entity WaypointSprite_SpawnFixed(
1091     entity spr,
1092     vector ofs,
1093     entity own,
1094     .entity ownfield,
1095     entity icon // initial icon
1096 )
1097 {
1098     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1099 }
1100
1101 entity WaypointSprite_DeployFixed(
1102     entity spr,
1103     float limited_range,
1104     entity player,
1105     vector ofs,
1106     entity icon // initial icon
1107 )
1108 {
1109     float t;
1110     if (teamplay)
1111         t = player.team;
1112     else
1113         t = 0;
1114     float maxdistance;
1115     if (limited_range)
1116         maxdistance = waypointsprite_limitedrange;
1117     else
1118         maxdistance = 0;
1119     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1120 }
1121
1122 entity WaypointSprite_DeployPersonal(
1123     entity spr,
1124     entity player,
1125     vector ofs,
1126     entity icon // initial icon
1127 )
1128 {
1129     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1130 }
1131
1132 entity WaypointSprite_Attach(
1133     entity spr,
1134     entity player,
1135     float limited_range,
1136     entity icon // initial icon
1137 )
1138 {
1139     float t;
1140     if (player.waypointsprite_attachedforcarrier)
1141         return NULL; // can't attach to FC
1142     if (teamplay)
1143         t = player.team;
1144     else
1145         t = 0;
1146     float maxdistance;
1147     if (limited_range)
1148         maxdistance = waypointsprite_limitedrange;
1149     else
1150         maxdistance = 0;
1151     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1152 }
1153
1154 entity WaypointSprite_AttachCarrier(
1155     entity spr,
1156     entity carrier,
1157     entity icon // initial icon and color
1158 )
1159 {
1160     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1161     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1162     if (carrier.health)
1163     {
1164         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1165         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1166     }
1167     return e;
1168 }
1169
1170 void WaypointSprite_DetachCarrier(entity carrier)
1171 {
1172     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1173 }
1174
1175 void WaypointSprite_ClearPersonal(entity this)
1176 {
1177     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1178 }
1179
1180 void WaypointSprite_ClearOwned(entity this)
1181 {
1182     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1183     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1184     WaypointSprite_Kill(this.waypointsprite_attached);
1185 }
1186
1187 void WaypointSprite_PlayerDead(entity this)
1188 {
1189     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1190     WaypointSprite_DetachCarrier(this);
1191 }
1192
1193 void WaypointSprite_PlayerGone(entity this)
1194 {
1195     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1196     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1197     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1198     WaypointSprite_DetachCarrier(this);
1199 }
1200 #endif