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