]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/weapons/weapon/crylink.qc
Merge branch 'master' into Mario/balance
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / weapons / weapon / crylink.qc
1 #ifndef IMPLEMENTATION
2 CLASS(Crylink, Weapon)
3 /* ammotype  */ ATTRIB(Crylink, ammo_field, .int, ammo_cells)
4 /* impulse   */ ATTRIB(Crylink, impulse, int, 6)
5 /* flags     */ ATTRIB(Crylink, spawnflags, int, WEP_FLAG_NORMAL | WEP_FLAG_RELOADABLE | WEP_TYPE_SPLASH);
6 /* rating    */ ATTRIB(Crylink, bot_pickupbasevalue, float, BOT_PICKUP_RATING_MID);
7 /* color     */ ATTRIB(Crylink, wpcolor, vector, '1 0.5 1');
8 /* modelname */ ATTRIB(Crylink, mdl, string, "crylink");
9 #ifndef MENUQC
10 /* model     */ ATTRIB(Crylink, m_model, Model, MDL_CRYLINK_ITEM);
11 #endif
12 /* crosshair */ ATTRIB(Crylink, w_crosshair, string, "gfx/crosshaircrylink");
13 /* crosshair */ ATTRIB(Crylink, w_crosshair_size, float, 0.5);
14 /* wepimg    */ ATTRIB(Crylink, model2, string, "weaponcrylink");
15 /* refname   */ ATTRIB(Crylink, netname, string, "crylink");
16 /* wepname   */ ATTRIB(Crylink, m_name, string, _("Crylink"));
17
18 #define X(BEGIN, P, END, class, prefix) \
19         BEGIN(class) \
20                 P(class, prefix, ammo, float, BOTH) \
21         P(class, prefix, animtime, float, BOTH) \
22         P(class, prefix, bouncedamagefactor, float, BOTH) \
23         P(class, prefix, bounces, float, BOTH) \
24         P(class, prefix, damage, float, BOTH) \
25         P(class, prefix, edgedamage, float, BOTH) \
26         P(class, prefix, force, float, BOTH) \
27         P(class, prefix, joindelay, float, BOTH) \
28         P(class, prefix, joinexplode, float, BOTH) \
29         P(class, prefix, joinexplode_damage, float, BOTH) \
30         P(class, prefix, joinexplode_edgedamage, float, BOTH) \
31         P(class, prefix, joinexplode_force, float, BOTH) \
32         P(class, prefix, joinexplode_radius, float, BOTH) \
33         P(class, prefix, joinspread, float, BOTH) \
34         P(class, prefix, linkexplode, float, BOTH) \
35         P(class, prefix, middle_fadetime, float, BOTH) \
36         P(class, prefix, middle_lifetime, float, BOTH) \
37         P(class, prefix, other_fadetime, float, BOTH) \
38         P(class, prefix, other_lifetime, float, BOTH) \
39         P(class, prefix, radius, float, BOTH) \
40         P(class, prefix, refire, float, BOTH) \
41         P(class, prefix, reload_ammo, float, NONE) \
42         P(class, prefix, reload_time, float, NONE) \
43         P(class, prefix, secondary, float, NONE) \
44         P(class, prefix, shots, float, BOTH) \
45         P(class, prefix, speed, float, BOTH) \
46         P(class, prefix, spreadtype, float, SEC) \
47         P(class, prefix, spread, float, BOTH) \
48         P(class, prefix, switchdelay_drop, float, NONE) \
49         P(class, prefix, switchdelay_raise, float, NONE) \
50         P(class, prefix, weaponreplace, string, NONE) \
51         P(class, prefix, weaponstartoverride, float, NONE) \
52         P(class, prefix, weaponstart, float, NONE) \
53         P(class, prefix, weaponthrowable, float, NONE) \
54         END()
55         W_PROPS(X, Crylink, crylink)
56 #undef X
57
58 ENDCLASS(Crylink)
59 REGISTER_WEAPON(CRYLINK, crylink, NEW(Crylink));
60
61 #ifdef SVQC
62 .float gravity;
63 .float crylink_waitrelease;
64 .entity crylink_lastgroup;
65
66 .entity queuenext;
67 .entity queueprev;
68 #endif
69 #endif
70 #ifdef IMPLEMENTATION
71 #ifdef SVQC
72 spawnfunc(weapon_crylink) { weapon_defaultspawnfunc(this, WEP_CRYLINK); }
73
74 void W_Crylink_CheckLinks(entity e)
75 {
76         float i;
77         entity p;
78
79         if(e == NULL)
80                 error("W_Crylink_CheckLinks: entity is NULL");
81         if(e.classname != "spike" || wasfreed(e))
82                 error(sprintf("W_Crylink_CheckLinks: entity is not a spike but a %s (freed: %d)", e.classname, wasfreed(e)));
83
84         p = e;
85         for(i = 0; i < 1000; ++i)
86         {
87                 if(p.queuenext.queueprev != p || p.queueprev.queuenext != p)
88                         error("W_Crylink_CheckLinks: queue is inconsistent");
89                 p = p.queuenext;
90                 if(p == e)
91                         break;
92         }
93         if(i >= 1000)
94                 error("W_Crylink_CheckLinks: infinite chain");
95 }
96
97 void W_Crylink_Dequeue_Raw(entity own, entity prev, entity me, entity next)
98 {
99         W_Crylink_CheckLinks(next);
100         if(me == own.crylink_lastgroup)
101                 own.crylink_lastgroup = ((me == next) ? NULL : next);
102         prev.queuenext = next;
103         next.queueprev = prev;
104         me.classname = "spike_oktoremove";
105         if(me != next)
106                 W_Crylink_CheckLinks(next);
107 }
108
109 void W_Crylink_Dequeue(entity e)
110 {
111         W_Crylink_Dequeue_Raw(e.realowner, e.queueprev, e, e.queuenext);
112 }
113
114 void W_Crylink_Reset(entity this)
115 {
116         W_Crylink_Dequeue(this);
117         remove(this);
118 }
119
120 // force projectile to explode
121 void W_Crylink_LinkExplode(entity e, entity e2)
122 {
123         float a;
124
125         if(e == e2)
126                 return;
127
128         a = bound(0, 1 - (time - e.fade_time) * e.fade_rate, 1);
129
130         if(e == e.realowner.crylink_lastgroup)
131                 e.realowner.crylink_lastgroup = NULL;
132
133         float isprimary = !(e.projectiledeathtype & HITTYPE_SECONDARY);
134
135         RadiusDamage(e, e.realowner, WEP_CVAR_BOTH(crylink, isprimary, damage) * a, WEP_CVAR_BOTH(crylink, isprimary, edgedamage) * a, WEP_CVAR_BOTH(crylink, isprimary, radius), NULL, NULL, WEP_CVAR_BOTH(crylink, isprimary, force) * a, e.projectiledeathtype, other);
136
137         W_Crylink_LinkExplode(e.queuenext, e2);
138
139         e.classname = "spike_oktoremove";
140         remove(e);
141 }
142
143 // adjust towards center
144 // returns the origin where they will meet... and the time till the meeting is
145 // stored in w_crylink_linkjoin_time.
146 // could possibly network this origin and time, and display a special particle
147 // effect when projectiles meet there :P
148 // jspeed: joining speed (calculate this as join spread * initial speed)
149 float w_crylink_linkjoin_time;
150 vector W_Crylink_LinkJoin(entity e, float jspeed)
151 {
152         vector avg_origin, avg_velocity;
153         vector targ_origin;
154         float avg_dist, n;
155         entity p;
156
157         // FIXME remove this debug code
158         W_Crylink_CheckLinks(e);
159
160         w_crylink_linkjoin_time = 0;
161
162         avg_origin = e.origin;
163         avg_velocity = e.velocity;
164         n = 1;
165         for(p = e; (p = p.queuenext) != e; )
166         {
167                 avg_origin += WarpZone_RefSys_TransformOrigin(p, e, p.origin);
168                 avg_velocity += WarpZone_RefSys_TransformVelocity(p, e, p.velocity);
169                 ++n;
170         }
171         avg_origin *= (1.0 / n);
172         avg_velocity *= (1.0 / n);
173
174         if(n < 2)
175                 return avg_origin; // nothing to do
176
177         // yes, mathematically we can do this in ONE step, but beware of 32bit floats...
178         avg_dist = pow(vlen(e.origin - avg_origin), 2);
179         for(p = e; (p = p.queuenext) != e; )
180                 avg_dist += pow(vlen(WarpZone_RefSys_TransformOrigin(p, e, p.origin) - avg_origin), 2);
181         avg_dist *= (1.0 / n);
182         avg_dist = sqrt(avg_dist);
183
184         if(avg_dist == 0)
185                 return avg_origin; // no change needed
186
187         if(jspeed == 0)
188         {
189                 e.velocity = avg_velocity;
190                 UpdateCSQCProjectile(e);
191                 for(p = e; (p = p.queuenext) != e; )
192                 {
193                         p.velocity = WarpZone_RefSys_TransformVelocity(e, p, avg_velocity);
194                         UpdateCSQCProjectile(p);
195                 }
196                 targ_origin = avg_origin + 1000000000 * normalize(avg_velocity); // HUUUUUUGE
197         }
198         else
199         {
200                 w_crylink_linkjoin_time = avg_dist / jspeed;
201                 targ_origin = avg_origin + w_crylink_linkjoin_time * avg_velocity;
202
203                 e.velocity = (targ_origin - e.origin) * (1.0 / w_crylink_linkjoin_time);
204                 UpdateCSQCProjectile(e);
205                 for(p = e; (p = p.queuenext) != e; )
206                 {
207                         p.velocity = WarpZone_RefSys_TransformVelocity(e, p, (targ_origin - WarpZone_RefSys_TransformOrigin(p, e, p.origin)) * (1.0 / w_crylink_linkjoin_time));
208                         UpdateCSQCProjectile(p);
209                 }
210
211                 // analysis:
212                 //   jspeed -> +infinity:
213                 //      w_crylink_linkjoin_time -> +0
214                 //      targ_origin -> avg_origin
215                 //      p->velocity -> HUEG towards center
216                 //   jspeed -> 0:
217                 //      w_crylink_linkjoin_time -> +/- infinity
218                 //      targ_origin -> avg_velocity * +/- infinity
219                 //      p->velocity -> avg_velocity
220                 //   jspeed -> -infinity:
221                 //      w_crylink_linkjoin_time -> -0
222                 //      targ_origin -> avg_origin
223                 //      p->velocity -> HUEG away from center
224         }
225
226         W_Crylink_CheckLinks(e);
227
228         return targ_origin;
229 }
230
231 void W_Crylink_LinkJoinEffect_Think(entity this)
232 {
233         // is there at least 2 projectiles very close?
234         entity e, p;
235         float n;
236         e = this.owner.crylink_lastgroup;
237         n = 0;
238         if(e)
239         {
240                 if(vlen2(e.origin - this.origin) < vlen2(e.velocity) * frametime)
241                         ++n;
242                 for(p = e; (p = p.queuenext) != e; )
243                 {
244                         if(vlen2(p.origin - this.origin) < vlen2(p.velocity) * frametime)
245                                 ++n;
246                 }
247                 if(n >= 2)
248                 {
249                         float isprimary = !(e.projectiledeathtype & HITTYPE_SECONDARY);
250
251                         if(WEP_CVAR_BOTH(crylink, isprimary, joinexplode))
252                         {
253                                 n /= WEP_CVAR_BOTH(crylink, isprimary, shots);
254                                 RadiusDamage(
255                                         e,
256                                         e.realowner,
257                                         WEP_CVAR_BOTH(crylink, isprimary, joinexplode_damage) * n,
258                                         WEP_CVAR_BOTH(crylink, isprimary, joinexplode_edgedamage) * n,
259                                         WEP_CVAR_BOTH(crylink, isprimary, joinexplode_radius) * n,
260                                         e.realowner,
261                                         NULL,
262                                         WEP_CVAR_BOTH(crylink, isprimary, joinexplode_force) * n,
263                                         e.projectiledeathtype,
264                                         other
265                                 );
266                                 Send_Effect(EFFECT_CRYLINK_JOINEXPLODE, this.origin, '0 0 0', n);
267                         }
268                 }
269         }
270         remove(this);
271 }
272
273 float W_Crylink_Touch_WouldHitFriendly(entity projectile, float rad)
274 {
275         entity head = WarpZone_FindRadius((projectile.origin + (projectile.mins + projectile.maxs) * 0.5), rad + MAX_DAMAGEEXTRARADIUS, false);
276         float hit_friendly = 0;
277         float hit_enemy = 0;
278
279         while(head)
280         {
281                 if((head.takedamage != DAMAGE_NO) && (!IS_DEAD(head)))
282                 {
283                         if(SAME_TEAM(head, projectile.realowner))
284                                 ++hit_friendly;
285                         else
286                                 ++hit_enemy;
287                 }
288
289                 head = head.chain;
290         }
291
292         return (hit_enemy ? false : hit_friendly);
293 }
294
295 // NO bounce protection, as bounces are limited!
296 void W_Crylink_Touch(entity this)
297 {
298         float finalhit;
299         float f;
300         float isprimary = !(this.projectiledeathtype & HITTYPE_SECONDARY);
301         PROJECTILE_TOUCH(this);
302
303         float a;
304         a = bound(0, 1 - (time - this.fade_time) * this.fade_rate, 1);
305
306         finalhit = ((this.cnt <= 0) || (other.takedamage != DAMAGE_NO));
307         if(finalhit)
308                 f = 1;
309         else
310                 f = WEP_CVAR_BOTH(crylink, isprimary, bouncedamagefactor);
311         if(a)
312                 f *= a;
313
314         float totaldamage = RadiusDamage(this, this.realowner, WEP_CVAR_BOTH(crylink, isprimary, damage) * f, WEP_CVAR_BOTH(crylink, isprimary, edgedamage) * f, WEP_CVAR_BOTH(crylink, isprimary, radius), NULL, NULL, WEP_CVAR_BOTH(crylink, isprimary, force) * f, this.projectiledeathtype, other);
315
316         if(totaldamage && ((WEP_CVAR_BOTH(crylink, isprimary, linkexplode) == 2) || ((WEP_CVAR_BOTH(crylink, isprimary, linkexplode) == 1) && !W_Crylink_Touch_WouldHitFriendly(this, WEP_CVAR_BOTH(crylink, isprimary, radius)))))
317         {
318                 if(this == this.realowner.crylink_lastgroup)
319                         this.realowner.crylink_lastgroup = NULL;
320                 W_Crylink_LinkExplode(this.queuenext, this);
321                 this.classname = "spike_oktoremove";
322                 remove(this);
323                 return;
324         }
325         else if(finalhit)
326         {
327                 // just unlink
328                 W_Crylink_Dequeue(this);
329                 remove(this);
330                 return;
331         }
332         this.cnt = this.cnt - 1;
333         this.angles = vectoangles(this.velocity);
334         this.owner = NULL;
335         this.projectiledeathtype |= HITTYPE_BOUNCE;
336         // commented out as it causes a little hitch...
337         //if(proj.cnt == 0)
338         //      CSQCProjectile(proj, true, PROJECTILE_CRYLINK, true);
339 }
340
341 void W_Crylink_Fadethink(entity this)
342 {
343         W_Crylink_Dequeue(this);
344         remove(this);
345 }
346
347 void W_Crylink_Attack(Weapon thiswep, entity actor)
348 {
349         float counter, shots;
350         entity proj, prevproj, firstproj;
351         vector s;
352         vector forward, right, up;
353         float maxdmg;
354
355         W_DecreaseAmmo(thiswep, actor, WEP_CVAR_PRI(crylink, ammo));
356
357         maxdmg = WEP_CVAR_PRI(crylink, damage) * WEP_CVAR_PRI(crylink, shots);
358         maxdmg *= 1 + WEP_CVAR_PRI(crylink, bouncedamagefactor) * WEP_CVAR_PRI(crylink, bounces);
359         if(WEP_CVAR_PRI(crylink, joinexplode))
360                 maxdmg += WEP_CVAR_PRI(crylink, joinexplode_damage);
361
362         W_SetupShot(actor, false, 2, SND_CRYLINK_FIRE, CH_WEAPON_A, maxdmg);
363         forward = v_forward;
364         right = v_right;
365         up = v_up;
366
367         shots = WEP_CVAR_PRI(crylink, shots);
368         Send_Effect(EFFECT_CRYLINK_MUZZLEFLASH, w_shotorg, w_shotdir * 1000, shots);
369         proj = prevproj = firstproj = NULL;
370         for(counter = 0; counter < shots; ++counter)
371         {
372                 proj = new(spike);
373                 proj.reset = W_Crylink_Reset;
374                 proj.realowner = proj.owner = actor;
375                 proj.bot_dodge = true;
376                 proj.bot_dodgerating = WEP_CVAR_PRI(crylink, damage);
377                 if(shots == 1) {
378                         proj.queuenext = proj;
379                         proj.queueprev = proj;
380                 }
381                 else if(counter == 0) { // first projectile, store in firstproj for now
382                         firstproj = proj;
383                 }
384                 else if(counter == shots - 1) { // last projectile, link up with first projectile
385                         prevproj.queuenext = proj;
386                         firstproj.queueprev = proj;
387                         proj.queuenext = firstproj;
388                         proj.queueprev = prevproj;
389                 }
390                 else { // else link up with previous projectile
391                         prevproj.queuenext = proj;
392                         proj.queueprev = prevproj;
393                 }
394
395                 prevproj = proj;
396
397                 proj.movetype = MOVETYPE_BOUNCEMISSILE;
398                 PROJECTILE_MAKETRIGGER(proj);
399                 proj.projectiledeathtype = WEP_CRYLINK.m_id;
400                 //proj.gravity = 0.001;
401
402                 setorigin(proj, w_shotorg);
403                 setsize(proj, '0 0 0', '0 0 0');
404
405
406                 s = '0 0 0';
407                 if(counter == 0)
408                         s = '0 0 0';
409                 else
410                 {
411                         makevectors('0 360 0' * (0.75 + (counter - 0.5) / (shots - 1)));
412                         s.y = v_forward.x;
413                         s.z = v_forward.y;
414                 }
415                 s = s * WEP_CVAR_PRI(crylink, spread) * g_weaponspreadfactor;
416                 W_SetupProjVelocity_Explicit(proj, w_shotdir + right * s.y + up * s.z, v_up, WEP_CVAR_PRI(crylink, speed), 0, 0, 0, false);
417                 settouch(proj, W_Crylink_Touch);
418
419                 setthink(proj, W_Crylink_Fadethink);
420                 if(counter == 0)
421                 {
422                         proj.fade_time = time + WEP_CVAR_PRI(crylink, middle_lifetime);
423                         proj.fade_rate = 1 / WEP_CVAR_PRI(crylink, middle_fadetime);
424                         proj.nextthink = time + WEP_CVAR_PRI(crylink, middle_lifetime) + WEP_CVAR_PRI(crylink, middle_fadetime);
425                 }
426                 else
427                 {
428                         proj.fade_time = time + WEP_CVAR_PRI(crylink, other_lifetime);
429                         proj.fade_rate = 1 / WEP_CVAR_PRI(crylink, other_fadetime);
430                         proj.nextthink = time + WEP_CVAR_PRI(crylink, other_lifetime) + WEP_CVAR_PRI(crylink, other_fadetime);
431                 }
432                 proj.teleport_time = time + WEP_CVAR_PRI(crylink, joindelay);
433                 proj.cnt = WEP_CVAR_PRI(crylink, bounces);
434                 //proj.scale = 1 + 1 * proj.cnt;
435
436                 proj.angles = vectoangles(proj.velocity);
437
438                 //proj.glow_size = 20;
439
440                 proj.flags = FL_PROJECTILE;
441                 proj.missile_flags = MIF_SPLASH;
442
443                 CSQCProjectile(proj, true, (proj.cnt ? PROJECTILE_CRYLINK_BOUNCING : PROJECTILE_CRYLINK), true);
444
445                 MUTATOR_CALLHOOK(EditProjectile, actor, proj);
446         }
447         if(WEP_CVAR_PRI(crylink, joinspread) != 0)
448         {
449                 actor.crylink_lastgroup = proj;
450                 W_Crylink_CheckLinks(proj);
451                 actor.crylink_waitrelease = 1;
452         }
453 }
454
455 void W_Crylink_Attack2(Weapon thiswep, entity actor)
456 {
457         float counter, shots;
458         entity proj, prevproj, firstproj;
459         vector s;
460         vector forward, right, up;
461         float maxdmg;
462
463         W_DecreaseAmmo(thiswep, actor, WEP_CVAR_SEC(crylink, ammo));
464
465         maxdmg = WEP_CVAR_SEC(crylink, damage) * WEP_CVAR_SEC(crylink, shots);
466         maxdmg *= 1 + WEP_CVAR_SEC(crylink, bouncedamagefactor) * WEP_CVAR_SEC(crylink, bounces);
467         if(WEP_CVAR_SEC(crylink, joinexplode))
468                 maxdmg += WEP_CVAR_SEC(crylink, joinexplode_damage);
469
470         W_SetupShot(actor, false, 2, SND_CRYLINK_FIRE2, CH_WEAPON_A, maxdmg);
471         forward = v_forward;
472         right = v_right;
473         up = v_up;
474
475         shots = WEP_CVAR_SEC(crylink, shots);
476         Send_Effect(EFFECT_CRYLINK_MUZZLEFLASH, w_shotorg, w_shotdir * 1000, shots);
477         proj = prevproj = firstproj = NULL;
478         for(counter = 0; counter < shots; ++counter)
479         {
480                 proj = new(spike);
481                 proj.reset = W_Crylink_Reset;
482                 proj.realowner = proj.owner = actor;
483                 proj.bot_dodge = true;
484                 proj.bot_dodgerating = WEP_CVAR_SEC(crylink, damage);
485                 if(shots == 1) {
486                         proj.queuenext = proj;
487                         proj.queueprev = proj;
488                 }
489                 else if(counter == 0) { // first projectile, store in firstproj for now
490                         firstproj = proj;
491                 }
492                 else if(counter == shots - 1) { // last projectile, link up with first projectile
493                         prevproj.queuenext = proj;
494                         firstproj.queueprev = proj;
495                         proj.queuenext = firstproj;
496                         proj.queueprev = prevproj;
497                 }
498                 else { // else link up with previous projectile
499                         prevproj.queuenext = proj;
500                         proj.queueprev = prevproj;
501                 }
502
503                 prevproj = proj;
504
505                 proj.movetype = MOVETYPE_BOUNCEMISSILE;
506                 PROJECTILE_MAKETRIGGER(proj);
507                 proj.projectiledeathtype = WEP_CRYLINK.m_id | HITTYPE_SECONDARY;
508                 //proj.gravity = 0.001;
509
510                 setorigin(proj, w_shotorg);
511                 setsize(proj, '0 0 0', '0 0 0');
512
513                 if(WEP_CVAR_SEC(crylink, spreadtype) == 1)
514                 {
515                         s = '0 0 0';
516                         if(counter == 0)
517                                 s = '0 0 0';
518                         else
519                         {
520                                 makevectors('0 360 0' * (0.75 + (counter - 0.5) / (shots - 1)));
521                                 s.y = v_forward.x;
522                                 s.z = v_forward.y;
523                         }
524                         s = s * WEP_CVAR_SEC(crylink, spread) * g_weaponspreadfactor;
525                         s = w_shotdir + right * s.y + up * s.z;
526                 }
527                 else
528                 {
529                         s = (w_shotdir + (((counter + 0.5) / shots) * 2 - 1) * v_right * WEP_CVAR_SEC(crylink, spread) * g_weaponspreadfactor);
530                 }
531
532                 W_SetupProjVelocity_Explicit(proj, s, v_up, WEP_CVAR_SEC(crylink, speed), 0, 0, 0, false);
533                 settouch(proj, W_Crylink_Touch);
534                 setthink(proj, W_Crylink_Fadethink);
535                 if(counter == (shots - 1) / 2)
536                 {
537                         proj.fade_time = time + WEP_CVAR_SEC(crylink, middle_lifetime);
538                         proj.fade_rate = 1 / WEP_CVAR_SEC(crylink, middle_fadetime);
539                         proj.nextthink = time + WEP_CVAR_SEC(crylink, middle_lifetime) + WEP_CVAR_SEC(crylink, middle_fadetime);
540                 }
541                 else
542                 {
543                         proj.fade_time = time + WEP_CVAR_SEC(crylink, other_lifetime);
544                         proj.fade_rate = 1 / WEP_CVAR_SEC(crylink, other_fadetime);
545                         proj.nextthink = time + WEP_CVAR_SEC(crylink, other_lifetime) + WEP_CVAR_SEC(crylink, other_fadetime);
546                 }
547                 proj.teleport_time = time + WEP_CVAR_SEC(crylink, joindelay);
548                 proj.cnt = WEP_CVAR_SEC(crylink, bounces);
549                 //proj.scale = 1 + 1 * proj.cnt;
550
551                 proj.angles = vectoangles(proj.velocity);
552
553                 //proj.glow_size = 20;
554
555                 proj.flags = FL_PROJECTILE;
556         proj.missile_flags = MIF_SPLASH;
557
558                 CSQCProjectile(proj, true, (proj.cnt ? PROJECTILE_CRYLINK_BOUNCING : PROJECTILE_CRYLINK), true);
559
560                 MUTATOR_CALLHOOK(EditProjectile, actor, proj);
561         }
562         if(WEP_CVAR_SEC(crylink, joinspread) != 0)
563         {
564                 actor.crylink_lastgroup = proj;
565                 W_Crylink_CheckLinks(proj);
566                 actor.crylink_waitrelease = 2;
567         }
568 }
569
570 METHOD(Crylink, wr_aim, void(entity thiswep, entity actor))
571 {
572     if(random() < 0.10)
573         PHYS_INPUT_BUTTON_ATCK(actor) = bot_aim(actor, WEP_CVAR_PRI(crylink, speed), 0, WEP_CVAR_PRI(crylink, middle_lifetime), false);
574     else
575         PHYS_INPUT_BUTTON_ATCK2(actor) = bot_aim(actor, WEP_CVAR_SEC(crylink, speed), 0, WEP_CVAR_SEC(crylink, middle_lifetime), false);
576 }
577 METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentity, int fire))
578 {
579     if(autocvar_g_balance_crylink_reload_ammo && actor.clip_load < min(WEP_CVAR_PRI(crylink, ammo), WEP_CVAR_SEC(crylink, ammo))) { // forced reload
580         thiswep.wr_reload(thiswep, actor, weaponentity);
581     }
582
583     if(fire & 1)
584     {
585         if(actor.crylink_waitrelease != 1)
586         if(weapon_prepareattack(thiswep, actor, weaponentity, false, WEP_CVAR_PRI(crylink, refire)))
587         {
588             W_Crylink_Attack(thiswep, actor);
589             weapon_thinkf(actor, weaponentity, WFRAME_FIRE1, WEP_CVAR_PRI(crylink, animtime), w_ready);
590         }
591     }
592
593     if((fire & 2) && autocvar_g_balance_crylink_secondary)
594     {
595         if(actor.crylink_waitrelease != 2)
596         if(weapon_prepareattack(thiswep, actor, weaponentity, true, WEP_CVAR_SEC(crylink, refire)))
597         {
598             W_Crylink_Attack2(thiswep, actor);
599             weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(crylink, animtime), w_ready);
600         }
601     }
602
603     if((actor.crylink_waitrelease == 1 && !(fire & 1)) || (actor.crylink_waitrelease == 2 && !(fire & 2)))
604     {
605         if(!actor.crylink_lastgroup || time > actor.crylink_lastgroup.teleport_time)
606         {
607             // fired and released now!
608             if(actor.crylink_lastgroup)
609             {
610                 vector pos;
611                 entity linkjoineffect;
612                 float isprimary = (actor.crylink_waitrelease == 1);
613
614                 pos = W_Crylink_LinkJoin(actor.crylink_lastgroup, WEP_CVAR_BOTH(crylink, isprimary, joinspread) * WEP_CVAR_BOTH(crylink, isprimary, speed));
615
616                 linkjoineffect = new(linkjoineffect);
617                 setthink(linkjoineffect, W_Crylink_LinkJoinEffect_Think);
618                 linkjoineffect.nextthink = time + w_crylink_linkjoin_time;
619                 linkjoineffect.owner = actor;
620                 setorigin(linkjoineffect, pos);
621             }
622             actor.crylink_waitrelease = 0;
623             if(!thiswep.wr_checkammo1(thiswep, actor) && !thiswep.wr_checkammo2(thiswep, actor))
624             if(!(actor.items & IT_UNLIMITED_WEAPON_AMMO))
625             {
626                 // ran out of ammo!
627                 actor.cnt = WEP_CRYLINK.m_id;
628                 PS(actor).m_switchweapon = w_getbestweapon(actor);
629             }
630         }
631     }
632 }
633 METHOD(Crylink, wr_checkammo1, bool(entity thiswep, entity actor))
634 {
635     // don't "run out of ammo" and switch weapons while waiting for release
636     if(actor.crylink_lastgroup && actor.crylink_waitrelease)
637         return true;
638
639     float ammo_amount = actor.(thiswep.ammo_field) >= WEP_CVAR_PRI(crylink, ammo);
640     ammo_amount += actor.(weapon_load[WEP_CRYLINK.m_id]) >= WEP_CVAR_PRI(crylink, ammo);
641     return ammo_amount;
642 }
643 METHOD(Crylink, wr_checkammo2, bool(entity thiswep, entity actor))
644 {
645     // don't "run out of ammo" and switch weapons while waiting for release
646     if(actor.crylink_lastgroup && actor.crylink_waitrelease)
647         return true;
648
649     float ammo_amount = actor.(thiswep.ammo_field) >= WEP_CVAR_SEC(crylink, ammo);
650     ammo_amount += actor.(weapon_load[WEP_CRYLINK.m_id]) >= WEP_CVAR_SEC(crylink, ammo);
651     return ammo_amount;
652 }
653 METHOD(Crylink, wr_reload, void(entity thiswep, entity actor, .entity weaponentity))
654 {
655     W_Reload(actor, min(WEP_CVAR_PRI(crylink, ammo), WEP_CVAR_SEC(crylink, ammo)), SND_RELOAD);
656 }
657 METHOD(Crylink, wr_suicidemessage, Notification(entity thiswep))
658 {
659     return WEAPON_CRYLINK_SUICIDE;
660 }
661 METHOD(Crylink, wr_killmessage, Notification(entity thiswep))
662 {
663     return WEAPON_CRYLINK_MURDER;
664 }
665 #endif
666 #ifdef CSQC
667 METHOD(Crylink, wr_impacteffect, void(entity thiswep, entity actor))
668 {
669     vector org2;
670     org2 = w_org + w_backoff * 2;
671     if(w_deathtype & HITTYPE_SECONDARY)
672     {
673         pointparticles(EFFECT_CRYLINK_IMPACT2, org2, '0 0 0', 1);
674         if(!w_issilent)
675             sound(actor, CH_SHOTS, SND_CRYLINK_IMPACT2, VOL_BASE, ATTN_NORM);
676     }
677     else
678     {
679         pointparticles(EFFECT_CRYLINK_IMPACT, org2, '0 0 0', 1);
680         if(!w_issilent)
681             sound(actor, CH_SHOTS, SND_CRYLINK_IMPACT, VOL_BASE, ATTN_NORM);
682     }
683 }
684 #endif
685 #endif