]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypointsprites.qc
Move waypoint autocvars and initialization
[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 STATIC_INIT(WaypointSprite_Load) {
788     WaypointSprite_Load();
789     WaypointSprite_Load_Frames(".tga");
790     WaypointSprite_Load_Frames(".jpg");
791 }
792 void WaypointSprite_Load()
793 {
794     waypointsprite_fadedistance = vlen(mi_scale);
795     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
796     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
797     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
798     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
799     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
800     waypointsprite_scale = autocvar_g_waypointsprite_scale;
801     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
802     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
803     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
804     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
805     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
806     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
807     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
808     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
809     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
810     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
811     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
812     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
813     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
814     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
815     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
816
817     waypointsprite_count = waypointsprite_newcount;
818     waypointsprite_newcount = 0;
819 }
820 #endif
821
822 #ifdef SVQC
823 void WaypointSprite_UpdateSprites(entity e, string m1, string m2, string m3)
824 {
825     if (m1 != e.model1)
826     {
827         e.model1 = m1;
828         e.SendFlags |= 2;
829     }
830     if (m2 != e.model2)
831     {
832         e.model2 = m2;
833         e.SendFlags |= 4;
834     }
835     if (m3 != e.model3)
836     {
837         e.model3 = m3;
838         e.SendFlags |= 8;
839     }
840 }
841
842 void WaypointSprite_UpdateHealth(entity e, float f)
843 {
844     f = bound(0, f, e.max_health);
845     if (f != e.health || e.pain_finished)
846     {
847         e.health = f;
848         e.pain_finished = 0;
849         e.SendFlags |= 0x80;
850     }
851 }
852
853 void WaypointSprite_UpdateMaxHealth(entity e, float f)
854 {
855     if (f != e.max_health || e.pain_finished)
856     {
857         e.max_health = f;
858         e.pain_finished = 0;
859         e.SendFlags |= 0x80;
860     }
861 }
862
863 void WaypointSprite_UpdateBuildFinished(entity e, float f)
864 {
865     if (f != e.pain_finished || e.max_health)
866     {
867         e.max_health = 0;
868         e.pain_finished = f;
869         e.SendFlags |= 0x80;
870     }
871 }
872
873 void WaypointSprite_UpdateOrigin(entity e, vector o)
874 {
875     if (o != e.origin)
876     {
877         setorigin(e, o);
878         e.SendFlags |= 64;
879     }
880 }
881
882 void WaypointSprite_UpdateRule(entity e, float t, float r)
883 {
884     // no check, as this is never called without doing an actual change (usually only once)
885     e.rule = r;
886     e.team = t;
887     e.SendFlags |= 1;
888 }
889
890 void WaypointSprite_UpdateTeamRadar(entity e, float icon, vector col)
891 {
892     // no check, as this is never called without doing an actual change (usually only once)
893     e.cnt = (icon & 0x7F) | (e.cnt & 0x80);
894     e.colormod = col;
895     e.SendFlags |= 32;
896 }
897
898 void WaypointSprite_Ping(entity e)
899 {
900     // anti spam
901     if (time < e.waypointsprite_pingtime) return;
902     e.waypointsprite_pingtime = time + 0.3;
903     // ALWAYS sends (this causes a radar circle), thus no check
904     e.cnt |= 0x80;
905     e.SendFlags |= 32;
906 }
907
908 void WaypointSprite_HelpMePing(entity e)
909 {
910     WaypointSprite_Ping(e);
911     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
912     e.SendFlags |= 32;
913 }
914
915 void WaypointSprite_FadeOutIn(entity e, float t)
916 {
917     if (!e.fade_time)
918     {
919         e.fade_time = t;
920         e.teleport_time = time + t;
921     }
922     else if (t < (e.teleport_time - time))
923     {
924         // accelerate the waypoint's dying
925         // ensure:
926         //   (e.teleport_time - time) / wp.fade_time stays
927         //   e.teleport_time = time + fadetime
928         float current_fadetime;
929         current_fadetime = e.teleport_time - time;
930         e.teleport_time = time + t;
931         e.fade_time = e.fade_time * t / current_fadetime;
932     }
933
934     e.SendFlags |= 16;
935 }
936
937 void WaypointSprite_Init()
938 {
939     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
940     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
941     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
942 }
943
944 void WaypointSprite_InitClient(entity e)
945 {
946 }
947
948 void WaypointSprite_Kill(entity wp)
949 {
950     if (!wp) return;
951     if (wp.owner) wp.owner.(wp.owned_by_field) = world;
952     remove(wp);
953 }
954
955 void WaypointSprite_Disown(entity wp, float fadetime)
956 {
957     if (!wp) return;
958     if (wp.classname != "sprite_waypoint")
959     {
960         backtrace("Trying to disown a non-waypointsprite");
961         return;
962     }
963     if (wp.owner)
964     {
965         if (wp.exteriormodeltoclient == wp.owner)
966             wp.exteriormodeltoclient = world;
967         wp.owner.(wp.owned_by_field) = world;
968         wp.owner = world;
969
970         WaypointSprite_FadeOutIn(wp, fadetime);
971     }
972 }
973
974 void WaypointSprite_Think()
975 {
976     bool doremove = false;
977
978     if (self.fade_time && time >= self.teleport_time)
979     {
980         doremove = true;
981     }
982
983     if (self.exteriormodeltoclient)
984         WaypointSprite_UpdateOrigin(self, self.exteriormodeltoclient.origin + self.view_ofs);
985
986     if (doremove)
987         WaypointSprite_Kill(self);
988     else
989         self.nextthink = time; // WHY?!?
990 }
991
992 float WaypointSprite_visible_for_player(entity e)
993 {
994     // personal waypoints
995     if (self.enemy && self.enemy != e)
996         return false;
997
998     // team waypoints
999     if (self.rule == SPRITERULE_SPECTATOR)
1000     {
1001         if (!autocvar_sv_itemstime)
1002             return false;
1003         if (!warmup_stage && IS_PLAYER(e))
1004             return false;
1005     }
1006     else if (self.team && self.rule == SPRITERULE_DEFAULT)
1007     {
1008         if (self.team != e.team)
1009             return false;
1010         if (!IS_PLAYER(e))
1011             return false;
1012     }
1013
1014     return true;
1015 }
1016
1017 entity WaypointSprite_getviewentity(entity e)
1018 {
1019     if (IS_SPEC(e)) e = e.enemy;
1020     /* TODO idea (check this breaks nothing)
1021     else if (e.classname == "observer")
1022         e = world;
1023     */
1024     return e;
1025 }
1026
1027 float WaypointSprite_isteammate(entity e, entity e2)
1028 {
1029     if (teamplay)
1030         return e2.team == e.team;
1031     return e2 == e;
1032 }
1033
1034 float WaypointSprite_Customize()
1035 {
1036     // this is not in SendEntity because it shall run every frame, not just every update
1037
1038     // make spectators see what the player would see
1039     entity e = WaypointSprite_getviewentity(other);
1040
1041     if (MUTATOR_CALLHOOK(CustomizeWaypoint, self, other))
1042         return false;
1043
1044     return self.waypointsprite_visible_for_player(e);
1045 }
1046
1047 float WaypointSprite_SendEntity(entity to, float sendflags);
1048
1049 void WaypointSprite_Reset()
1050 {
1051     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
1052
1053     if (self.fade_time) // there was there before: || g_keyhunt, do we really need this?
1054         WaypointSprite_Kill(self);
1055 }
1056
1057 entity WaypointSprite_Spawn(
1058     string spr, // sprite
1059     float _lifetime, float maxdistance, // lifetime, max distance
1060     entity ref, vector ofs, // position
1061     entity showto, float t, // show to whom? Use a flag to indicate a team
1062     entity own, .entity ownfield, // remove when own gets killed
1063     float hideable, // true when it should be controlled by cl_hidewaypoints
1064     float icon, vector rgb // initial icon and color
1065 )
1066 {
1067     entity wp = new(sprite_waypoint);
1068     wp.teleport_time = time + _lifetime;
1069     wp.fade_time = _lifetime;
1070     wp.exteriormodeltoclient = ref;
1071     if (ref)
1072     {
1073         wp.view_ofs = ofs;
1074         setorigin(wp, ref.origin + ofs);
1075     }
1076     else
1077         setorigin(wp, ofs);
1078     wp.enemy = showto;
1079     wp.team = t;
1080     wp.owner = own;
1081     wp.currentammo = hideable;
1082     if (own)
1083     {
1084         if (own.(ownfield))
1085             remove(own.(ownfield));
1086         own.(ownfield) = wp;
1087         wp.owned_by_field = ownfield;
1088     }
1089     wp.fade_rate = maxdistance;
1090     wp.think = WaypointSprite_Think;
1091     wp.nextthink = time;
1092     wp.model1 = spr;
1093     wp.customizeentityforclient = WaypointSprite_Customize;
1094     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1095     wp.reset2 = WaypointSprite_Reset;
1096     wp.cnt = icon;
1097     wp.colormod = rgb;
1098     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1099     return wp;
1100 }
1101
1102 entity WaypointSprite_SpawnFixed(
1103     string spr,
1104     vector ofs,
1105     entity own,
1106     .entity ownfield,
1107     float icon, vector rgb // initial icon and color
1108 )
1109 {
1110     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, own, ownfield, true, icon, rgb);
1111 }
1112
1113 entity WaypointSprite_DeployFixed(
1114     string spr,
1115     float limited_range,
1116     vector ofs,
1117     float icon, vector rgb // initial icon and color
1118 )
1119 {
1120     float t;
1121     if (teamplay)
1122         t = self.team;
1123     else
1124         t = 0;
1125     float maxdistance;
1126     if (limited_range)
1127         maxdistance = waypointsprite_limitedrange;
1128     else
1129         maxdistance = 0;
1130     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, world, ofs, world, t, self, waypointsprite_deployed_fixed, false, icon, rgb);
1131 }
1132
1133 entity WaypointSprite_DeployPersonal(
1134     string spr,
1135     vector ofs,
1136     float icon, vector rgb // initial icon and color
1137 )
1138 {
1139     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, self, waypointsprite_deployed_personal, false, icon, rgb);
1140 }
1141
1142 entity WaypointSprite_Attach(
1143     string spr,
1144     float limited_range,
1145     float icon, vector rgb // initial icon and color
1146 )
1147 {
1148     float t;
1149     if (self.waypointsprite_attachedforcarrier)
1150         return world; // can't attach to FC
1151     if (teamplay)
1152         t = self.team;
1153     else
1154         t = 0;
1155     float maxdistance;
1156     if (limited_range)
1157         maxdistance = waypointsprite_limitedrange;
1158     else
1159         maxdistance = 0;
1160     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, self, '0 0 64', world, t, self, waypointsprite_attached, false, icon, rgb);
1161 }
1162
1163 entity WaypointSprite_AttachCarrier(
1164     string spr,
1165     entity carrier,
1166     float icon, vector rgb // initial icon and color
1167 )
1168 {
1169     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1170     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', world, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon, rgb);
1171     if (e)
1172     {
1173         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
1174         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1175     }
1176     return e;
1177 }
1178
1179 void WaypointSprite_DetachCarrier(entity carrier)
1180 {
1181     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1182 }
1183
1184 void WaypointSprite_ClearPersonal()
1185 {
1186     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1187 }
1188
1189 void WaypointSprite_ClearOwned()
1190 {
1191     WaypointSprite_Kill(self.waypointsprite_deployed_fixed);
1192     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1193     WaypointSprite_Kill(self.waypointsprite_attached);
1194 }
1195
1196 void WaypointSprite_PlayerDead()
1197 {
1198     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1199     WaypointSprite_DetachCarrier(self);
1200 }
1201
1202 void WaypointSprite_PlayerGone()
1203 {
1204     WaypointSprite_Disown(self.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1205     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1206     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1207     WaypointSprite_DetachCarrier(self);
1208 }
1209 #endif