Use a trick to prevent handling of items that aren't registered with .itemdef (should...
[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                 IL_PUSH(g_bot_dodge, proj);
445                 proj.missile_flags = MIF_SPLASH;
446
447                 CSQCProjectile(proj, true, (proj.cnt ? PROJECTILE_CRYLINK_BOUNCING : PROJECTILE_CRYLINK), true);
448
449                 MUTATOR_CALLHOOK(EditProjectile, actor, proj);
450         }
451         if(WEP_CVAR_PRI(crylink, joinspread) != 0)
452         {
453                 actor.crylink_lastgroup = proj;
454                 W_Crylink_CheckLinks(proj);
455                 actor.crylink_waitrelease = 1;
456         }
457 }
458
459 void W_Crylink_Attack2(Weapon thiswep, entity actor, .entity weaponentity)
460 {
461         float counter, shots;
462         entity proj, prevproj, firstproj;
463         vector s;
464         vector forward, right, up;
465         float maxdmg;
466
467         W_DecreaseAmmo(thiswep, actor, WEP_CVAR_SEC(crylink, ammo));
468
469         maxdmg = WEP_CVAR_SEC(crylink, damage) * WEP_CVAR_SEC(crylink, shots);
470         maxdmg *= 1 + WEP_CVAR_SEC(crylink, bouncedamagefactor) * WEP_CVAR_SEC(crylink, bounces);
471         if(WEP_CVAR_SEC(crylink, joinexplode))
472                 maxdmg += WEP_CVAR_SEC(crylink, joinexplode_damage);
473
474         W_SetupShot(actor, weaponentity, false, 2, SND_CRYLINK_FIRE2, CH_WEAPON_A, maxdmg);
475         forward = v_forward;
476         right = v_right;
477         up = v_up;
478
479         shots = WEP_CVAR_SEC(crylink, shots);
480         Send_Effect(EFFECT_CRYLINK_MUZZLEFLASH, w_shotorg, w_shotdir * 1000, shots);
481         proj = prevproj = firstproj = NULL;
482         for(counter = 0; counter < shots; ++counter)
483         {
484                 proj = new(spike);
485                 proj.reset = W_Crylink_Reset;
486                 proj.realowner = proj.owner = actor;
487                 proj.bot_dodge = true;
488                 proj.bot_dodgerating = WEP_CVAR_SEC(crylink, damage);
489                 if(shots == 1) {
490                         proj.queuenext = proj;
491                         proj.queueprev = proj;
492                 }
493                 else if(counter == 0) { // first projectile, store in firstproj for now
494                         firstproj = proj;
495                 }
496                 else if(counter == shots - 1) { // last projectile, link up with first projectile
497                         prevproj.queuenext = proj;
498                         firstproj.queueprev = proj;
499                         proj.queuenext = firstproj;
500                         proj.queueprev = prevproj;
501                 }
502                 else { // else link up with previous projectile
503                         prevproj.queuenext = proj;
504                         proj.queueprev = prevproj;
505                 }
506
507                 prevproj = proj;
508
509                 set_movetype(proj, MOVETYPE_BOUNCEMISSILE);
510                 PROJECTILE_MAKETRIGGER(proj);
511                 proj.projectiledeathtype = WEP_CRYLINK.m_id | HITTYPE_SECONDARY;
512                 //proj.gravity = 0.001;
513
514                 setorigin(proj, w_shotorg);
515                 setsize(proj, '0 0 0', '0 0 0');
516
517                 if(WEP_CVAR_SEC(crylink, spreadtype) == 1)
518                 {
519                         s = '0 0 0';
520                         if(counter == 0)
521                                 s = '0 0 0';
522                         else
523                         {
524                                 makevectors('0 360 0' * (0.75 + (counter - 0.5) / (shots - 1)));
525                                 s.y = v_forward.x;
526                                 s.z = v_forward.y;
527                         }
528                         s = s * WEP_CVAR_SEC(crylink, spread) * g_weaponspreadfactor;
529                         s = w_shotdir + right * s.y + up * s.z;
530                 }
531                 else
532                 {
533                         s = (w_shotdir + (((counter + 0.5) / shots) * 2 - 1) * v_right * WEP_CVAR_SEC(crylink, spread) * g_weaponspreadfactor);
534                 }
535
536                 W_SetupProjVelocity_Explicit(proj, s, v_up, WEP_CVAR_SEC(crylink, speed), 0, 0, 0, false);
537                 settouch(proj, W_Crylink_Touch);
538                 setthink(proj, W_Crylink_Fadethink);
539                 if(counter == (shots - 1) / 2)
540                 {
541                         proj.fade_time = time + WEP_CVAR_SEC(crylink, middle_lifetime);
542                         proj.fade_rate = 1 / WEP_CVAR_SEC(crylink, middle_fadetime);
543                         proj.nextthink = time + WEP_CVAR_SEC(crylink, middle_lifetime) + WEP_CVAR_SEC(crylink, middle_fadetime);
544                 }
545                 else
546                 {
547                         proj.fade_time = time + WEP_CVAR_SEC(crylink, other_lifetime);
548                         proj.fade_rate = 1 / WEP_CVAR_SEC(crylink, other_fadetime);
549                         proj.nextthink = time + WEP_CVAR_SEC(crylink, other_lifetime) + WEP_CVAR_SEC(crylink, other_fadetime);
550                 }
551                 proj.teleport_time = time + WEP_CVAR_SEC(crylink, joindelay);
552                 proj.cnt = WEP_CVAR_SEC(crylink, bounces);
553                 //proj.scale = 1 + 1 * proj.cnt;
554
555                 proj.angles = vectoangles(proj.velocity);
556
557                 //proj.glow_size = 20;
558
559                 proj.flags = FL_PROJECTILE;
560                 IL_PUSH(g_projectiles, proj);
561                 IL_PUSH(g_bot_dodge, proj);
562         proj.missile_flags = MIF_SPLASH;
563
564                 CSQCProjectile(proj, true, (proj.cnt ? PROJECTILE_CRYLINK_BOUNCING : PROJECTILE_CRYLINK), true);
565
566                 MUTATOR_CALLHOOK(EditProjectile, actor, proj);
567         }
568         if(WEP_CVAR_SEC(crylink, joinspread) != 0)
569         {
570                 actor.crylink_lastgroup = proj;
571                 W_Crylink_CheckLinks(proj);
572                 actor.crylink_waitrelease = 2;
573         }
574 }
575
576 METHOD(Crylink, wr_aim, void(entity thiswep, entity actor))
577 {
578     if(random() < 0.10)
579         PHYS_INPUT_BUTTON_ATCK(actor) = bot_aim(actor, WEP_CVAR_PRI(crylink, speed), 0, WEP_CVAR_PRI(crylink, middle_lifetime), false);
580     else
581         PHYS_INPUT_BUTTON_ATCK2(actor) = bot_aim(actor, WEP_CVAR_SEC(crylink, speed), 0, WEP_CVAR_SEC(crylink, middle_lifetime), false);
582 }
583 METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentity, int fire))
584 {
585     if(autocvar_g_balance_crylink_reload_ammo && actor.clip_load < min(WEP_CVAR_PRI(crylink, ammo), WEP_CVAR_SEC(crylink, ammo))) { // forced reload
586         thiswep.wr_reload(thiswep, actor, weaponentity);
587     }
588
589     if(fire & 1)
590     {
591         if(actor.crylink_waitrelease != 1)
592         if(weapon_prepareattack(thiswep, actor, weaponentity, false, WEP_CVAR_PRI(crylink, refire)))
593         {
594             W_Crylink_Attack(thiswep, actor, weaponentity);
595             weapon_thinkf(actor, weaponentity, WFRAME_FIRE1, WEP_CVAR_PRI(crylink, animtime), w_ready);
596         }
597     }
598
599     if((fire & 2) && autocvar_g_balance_crylink_secondary)
600     {
601         if(actor.crylink_waitrelease != 2)
602         if(weapon_prepareattack(thiswep, actor, weaponentity, true, WEP_CVAR_SEC(crylink, refire)))
603         {
604             W_Crylink_Attack2(thiswep, actor, weaponentity);
605             weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(crylink, animtime), w_ready);
606         }
607     }
608
609     if((actor.crylink_waitrelease == 1 && !(fire & 1)) || (actor.crylink_waitrelease == 2 && !(fire & 2)))
610     {
611         if(!actor.crylink_lastgroup || time > actor.crylink_lastgroup.teleport_time)
612         {
613             // fired and released now!
614             if(actor.crylink_lastgroup)
615             {
616                 vector pos;
617                 entity linkjoineffect;
618                 float isprimary = (actor.crylink_waitrelease == 1);
619
620                 pos = W_Crylink_LinkJoin(actor.crylink_lastgroup, WEP_CVAR_BOTH(crylink, isprimary, joinspread) * WEP_CVAR_BOTH(crylink, isprimary, speed));
621
622                 linkjoineffect = new(linkjoineffect);
623                 setthink(linkjoineffect, W_Crylink_LinkJoinEffect_Think);
624                 linkjoineffect.nextthink = time + w_crylink_linkjoin_time;
625                 linkjoineffect.owner = actor;
626                 setorigin(linkjoineffect, pos);
627             }
628             actor.crylink_waitrelease = 0;
629             if(!thiswep.wr_checkammo1(thiswep, actor) && !thiswep.wr_checkammo2(thiswep, actor))
630             if(!(actor.items & IT_UNLIMITED_WEAPON_AMMO))
631             {
632                 // ran out of ammo!
633                 actor.cnt = WEP_CRYLINK.m_id;
634                 PS(actor).m_switchweapon = w_getbestweapon(actor);
635             }
636         }
637     }
638 }
639 METHOD(Crylink, wr_checkammo1, bool(entity thiswep, entity actor))
640 {
641     // don't "run out of ammo" and switch weapons while waiting for release
642     if(actor.crylink_lastgroup && actor.crylink_waitrelease)
643         return true;
644
645     float ammo_amount = actor.(thiswep.ammo_field) >= WEP_CVAR_PRI(crylink, ammo);
646     ammo_amount += actor.(weapon_load[WEP_CRYLINK.m_id]) >= WEP_CVAR_PRI(crylink, ammo);
647     return ammo_amount;
648 }
649 METHOD(Crylink, wr_checkammo2, bool(entity thiswep, entity actor))
650 {
651     // don't "run out of ammo" and switch weapons while waiting for release
652     if(actor.crylink_lastgroup && actor.crylink_waitrelease)
653         return true;
654
655     float ammo_amount = actor.(thiswep.ammo_field) >= WEP_CVAR_SEC(crylink, ammo);
656     ammo_amount += actor.(weapon_load[WEP_CRYLINK.m_id]) >= WEP_CVAR_SEC(crylink, ammo);
657     return ammo_amount;
658 }
659 METHOD(Crylink, wr_reload, void(entity thiswep, entity actor, .entity weaponentity))
660 {
661     W_Reload(actor, weaponentity, min(WEP_CVAR_PRI(crylink, ammo), WEP_CVAR_SEC(crylink, ammo)), SND_RELOAD);
662 }
663 METHOD(Crylink, wr_suicidemessage, Notification(entity thiswep))
664 {
665     return WEAPON_CRYLINK_SUICIDE;
666 }
667 METHOD(Crylink, wr_killmessage, Notification(entity thiswep))
668 {
669     return WEAPON_CRYLINK_MURDER;
670 }
671 #endif
672 #ifdef CSQC
673 METHOD(Crylink, wr_impacteffect, void(entity thiswep, entity actor))
674 {
675     vector org2;
676     org2 = w_org + w_backoff * 2;
677     if(w_deathtype & HITTYPE_SECONDARY)
678     {
679         pointparticles(EFFECT_CRYLINK_IMPACT2, org2, '0 0 0', 1);
680         if(!w_issilent)
681             sound(actor, CH_SHOTS, SND_CRYLINK_IMPACT2, VOL_BASE, ATTN_NORM);
682     }
683     else
684     {
685         pointparticles(EFFECT_CRYLINK_IMPACT, org2, '0 0 0', 1);
686         if(!w_issilent)
687             sound(actor, CH_SHOTS, SND_CRYLINK_IMPACT, VOL_BASE, ATTN_NORM);
688     }
689 }
690 #endif
691 #endif