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