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