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