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