]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/w_laser.qc
Begin updating the cvars
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / w_laser.qc
1 #ifdef REGISTER_WEAPON
2 REGISTER_WEAPON(LASER, w_laser, 0, 1, WEP_FLAG_NORMAL | WEP_FLAG_RELOADABLE | WEP_FLAG_CANCLIMB | WEP_TYPE_SPLASH, 0, "laser", "laser", _("Laser"))
3 #else
4 #ifdef SVQC
5 void(float imp) W_SwitchWeapon;
6 void() W_LastWeapon;
7 .float swing_prev;
8 .entity swing_alreadyhit;
9
10 void SendCSQCShockwaveParticle(float spread) 
11 {
12         //WarpZone_UnTransformOrigin(WarpZone_trace_transform, trace_endpos);
13         WriteByte(MSG_BROADCAST, SVC_TEMPENTITY);
14         WriteByte(MSG_BROADCAST, TE_CSQC_SHOCKWAVEPARTICLE);
15         WriteCoord(MSG_BROADCAST, w_shotorg_x);
16         WriteCoord(MSG_BROADCAST, w_shotorg_y);
17         WriteCoord(MSG_BROADCAST, w_shotorg_z);
18         WriteCoord(MSG_BROADCAST, w_shotdir_x);
19         WriteCoord(MSG_BROADCAST, w_shotdir_y);
20         WriteCoord(MSG_BROADCAST, w_shotdir_z);
21         WriteByte(MSG_BROADCAST, bound(0, 255 * spread, 255));
22 }
23
24 void W_Laser_Touch (void)
25 {
26         PROJECTILE_TOUCH;
27
28         self.event_damage = SUB_Null;
29         if (self.dmg)
30                 RadiusDamage (self, self.realowner, autocvar_g_balance_laser_secondary_damage, autocvar_g_balance_laser_secondary_edgedamage, autocvar_g_balance_laser_secondary_radius, world, world, autocvar_g_balance_laser_secondary_force, self.projectiledeathtype, other);
31         else
32                 RadiusDamage (self, self.realowner, autocvar_g_balance_laser_primary_damage, autocvar_g_balance_laser_primary_edgedamage, autocvar_g_balance_laser_primary_radius, world, world, autocvar_g_balance_laser_primary_force, self.projectiledeathtype, other);
33
34         remove (self);
35 }
36
37 void W_Laser_Think()
38 {
39         self.movetype = MOVETYPE_FLY;
40         self.think = SUB_Remove;
41         if (self.dmg)
42                 self.nextthink = time + autocvar_g_balance_laser_secondary_lifetime;
43         else
44                 self.nextthink = time + autocvar_g_balance_laser_primary_lifetime;
45         CSQCProjectile(self, TRUE, PROJECTILE_LASER, TRUE);
46 }
47
48
49 float W_Laser_Shockwave_CheckSpread(vector targetorg, vector nearest_on_line, vector sw_shotorg, vector attack_hitpos)
50 {
51         float spreadlimit;
52         float distance_of_attack = vlen(sw_shotorg - attack_hitpos);
53         float distance_from_line = vlen(targetorg - nearest_on_line);
54         
55         spreadlimit = (distance_of_attack ? min(1, (vlen(sw_shotorg - nearest_on_line) / distance_of_attack)) : 1);
56         spreadlimit = (autocvar_g_balance_laser_primary_spread_min * (1 - spreadlimit) + autocvar_g_balance_laser_primary_spread_max * spreadlimit);
57         
58         if(spreadlimit && (distance_from_line <= spreadlimit))
59                 return bound(0, (distance_from_line / spreadlimit), 1);
60         else
61                 return FALSE;
62 }
63
64 float W_Laser_Shockwave_IsVisible(entity head, vector nearest_on_line, vector sw_shotorg, vector attack_hitpos)
65 {
66         vector nearest_to_attacker = head.WarpZone_findradius_nearest;
67         vector center = (head.origin + (head.mins + head.maxs) * 0.5);
68         vector corner;
69         float i;
70
71         // STEP ONE: Check if the nearest point is clear
72         if(W_Laser_Shockwave_CheckSpread(nearest_to_attacker, nearest_on_line, sw_shotorg, attack_hitpos))
73         {
74                 WarpZone_TraceLine(sw_shotorg, nearest_to_attacker, MOVE_WORLDONLY, self);
75                 if(trace_fraction == 1) { return TRUE; } // yes, the nearest point is clear and we can allow the damage
76         }
77
78         // STEP TWO: Check if shotorg to center point is clear
79         if(W_Laser_Shockwave_CheckSpread(center, nearest_on_line, sw_shotorg, attack_hitpos))
80         {
81                 WarpZone_TraceLine(sw_shotorg, center, MOVE_WORLDONLY, self);
82                 if(trace_fraction == 1) { return TRUE; } // yes, the center point is clear and we can allow the damage
83         }
84
85         // STEP THREE: Check each corner to see if they are clear
86         for(i=1; i<=8; ++i)
87         {
88                 corner = get_corner_position(head, i);
89                 if(W_Laser_Shockwave_CheckSpread(corner, nearest_on_line, sw_shotorg, attack_hitpos))
90                 {
91                         WarpZone_TraceLine(sw_shotorg, corner, MOVE_WORLDONLY, self);
92                         if(trace_fraction == 1) { return TRUE; } // yes, this corner is clear and we can allow the damage
93                 }
94         }
95
96         return FALSE;
97 }
98
99 void W_Laser_Shockwave (void)
100 {
101         // declarations
102         float multiplier, multiplier_from_accuracy, multiplier_from_distance;
103         float final_damage, final_spread;
104         vector final_force, center;
105         entity head, next;
106         
107         // set up the shot direction
108         vector wanted_shot_direction = (v_forward * cos(autocvar_g_balance_laser_primary_shotangle * DEG2RAD) + v_up * sin(autocvar_g_balance_laser_primary_shotangle * DEG2RAD));
109         W_SetupShot_Dir(self, wanted_shot_direction, FALSE, 3, "weapons/lasergun_fire.wav", CH_WEAPON_B, autocvar_g_balance_laser_primary_damage);
110         vector attack_endpos = (w_shotorg + (w_shotdir * autocvar_g_balance_laser_primary_radius));
111
112         // find out what we're pointing at and acquire the warpzone transform
113         WarpZone_TraceLine(w_shotorg, attack_endpos, FALSE, self);
114         entity aim_ent = trace_ent;
115         vector attack_hitpos = trace_endpos;
116         float distance_of_attack = vlen(w_shotorg - attack_hitpos);
117         
118         // do the jump explosion now (also handles the impact effect)
119         RadiusDamageForSource(self, trace_endpos, '0 0 0', self, autocvar_g_balance_laser_primary_damage, autocvar_g_balance_laser_primary_edgedamage, autocvar_g_balance_laser_primary_jumpradius, world, self, TRUE, autocvar_g_balance_laser_primary_force, WEP_LASER, world);
120         
121         // also do the firing effect now
122         SendCSQCShockwaveParticle(autocvar_g_balance_laser_primary_spread);
123         
124         // did we hit a player directly?
125         if(aim_ent.takedamage)
126         {
127                 // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
128                 center = (aim_ent.origin + ((aim_ent.classname == "player") ? aim_ent.view_ofs : ((aim_ent.mins + aim_ent.maxs) * 0.5)));
129                 
130                 multiplier_from_accuracy = 1;
131                 multiplier_from_distance = (1 - (distance_of_attack ? min(1, (distance_of_attack / autocvar_g_balance_laser_primary_radius)) : 0));
132                 multiplier = max(autocvar_g_balance_laser_primary_multiplier_min, ((multiplier_from_accuracy * autocvar_g_balance_laser_primary_multiplier_accuracy) + (multiplier_from_distance * autocvar_g_balance_laser_primary_multiplier_distance)));
133                 
134                 final_force = ((normalize(center - attack_hitpos) * autocvar_g_balance_laser_primary_force) * multiplier);
135                 final_damage = (autocvar_g_balance_laser_primary_damage * multiplier);
136                 Damage(aim_ent, self, self, final_damage, WEP_LASER, aim_ent.origin, final_force);
137                 
138                 print("multiplier = ", ftos(multiplier), ", multiplier_from_accuracy = ", ftos(multiplier_from_accuracy), ", multiplier_from_distance = ", ftos(multiplier_from_distance), ", ");
139                 print(strcat("direct hit damage = ", ftos(autocvar_g_balance_laser_primary_damage), ", force = ", vtos(final_force), ".\n"));
140         }
141
142         // now figure out if I hit anything else than what my aim directly pointed at...
143         head = WarpZone_FindRadius(w_shotorg, autocvar_g_balance_laser_primary_radius, FALSE);
144         while(head)
145         {
146                 next = head.chain;
147                 
148                 if((head != self && head != aim_ent) && (head.takedamage))
149                 {
150                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc) 
151                         center = (head.origin + ((head.classname == "player") ? head.view_ofs : ((head.mins + head.maxs) * 0.5)));
152
153                         // find the closest point on the enemy to the center of the attack
154                         float ang; // angle between shotdir and h
155                         float h; // hypotenuse, which is the distance between attacker to head
156                         float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
157                         
158                         h = vlen(center - self.origin);
159                         ang = acos(dotproduct(normalize(center - self.origin), w_shotdir));
160                         a = h * cos(ang);
161
162                         vector nearest_on_line = (w_shotorg + a * w_shotdir);
163                         vector nearest_to_attacker = WarpZoneLib_NearestPointOnBox(center + head.mins, center + head.maxs, nearest_on_line);
164                         float distance_to_target = vlen(w_shotorg - nearest_to_attacker);
165
166                         if(distance_to_target <= autocvar_g_balance_laser_primary_radius)
167                         {
168                                 if(W_Laser_Shockwave_IsVisible(head, nearest_on_line, w_shotorg, attack_hitpos))
169                                 {
170                                         multiplier_from_accuracy = (1 - W_Laser_Shockwave_CheckSpread(nearest_to_attacker, nearest_on_line, w_shotorg, attack_hitpos));
171                                         multiplier_from_distance = (1 - (distance_of_attack ? min(1, (distance_to_target / autocvar_g_balance_laser_primary_radius)) : 0));
172                                         multiplier = max(autocvar_g_balance_laser_primary_multiplier_min, ((multiplier_from_accuracy * autocvar_g_balance_laser_primary_multiplier_accuracy) + (multiplier_from_distance * autocvar_g_balance_laser_primary_multiplier_distance)));
173
174                                         final_force = ((normalize(center - nearest_on_line) * autocvar_g_balance_laser_primary_force) * multiplier);
175                                         final_damage = (autocvar_g_balance_laser_primary_damage * multiplier + autocvar_g_balance_laser_primary_edgedamage * (1 - multiplier));
176                                         Damage(head, self, self, final_damage, WEP_LASER, head.origin, final_force);
177
178                                         print("multiplier = ", ftos(multiplier), ", multiplier_from_accuracy = ", ftos(multiplier_from_accuracy), ", multiplier_from_distance = ", ftos(multiplier_from_distance), ", ");
179                                         print(strcat("edge hit damage = ", ftos(final_damage), ", force = ", vtos(final_force), ".\n"));
180                                         
181                                         //pointparticles(particleeffectnum("rocket_guide"), w_shotorg, w_shotdir * 1000, 1);
182                                         //SendCSQCShockwaveParticle(autocvar_g_balance_laser_primary_spread, trace_endpos);
183                                 }
184                         }
185                 }
186                 head = next;
187         }
188 }
189
190 void W_Laser_Melee_Think(void)
191 {
192         // declarations
193         float i, f, swing, swing_factor, swing_damage, meleetime, is_player;
194         entity target_victim;
195         vector targpos;
196
197         if(!self.cnt) // set start time of melee
198         {
199                 self.cnt = time; 
200                 W_PlayStrengthSound(self.realowner);
201         }
202
203         makevectors(self.realowner.v_angle); // update values for v_* vectors
204         
205         // calculate swing percentage based on time
206         meleetime = autocvar_g_balance_laser_secondary_melee_time * W_WeaponRateFactor();
207         swing = bound(0, (self.cnt + meleetime - time) / meleetime, 10);
208         f = ((1 - swing) * autocvar_g_balance_laser_secondary_melee_traces);
209         
210         // check to see if we can still continue, otherwise give up now
211         if((self.realowner.deadflag != DEAD_NO) && autocvar_g_balance_laser_secondary_melee_no_doubleslap)
212         {
213                 remove(self);
214                 return;
215         }
216         
217         // if okay, perform the traces needed for this frame 
218         for(i=self.swing_prev; i < f; ++i)
219         {
220                 swing_factor = ((1 - (i / autocvar_g_balance_laser_secondary_melee_traces)) * 2 - 1);
221                 
222                 targpos = (self.realowner.origin + self.realowner.view_ofs 
223                         + (v_forward * autocvar_g_balance_laser_secondary_melee_range)
224                         + (v_up * swing_factor * autocvar_g_balance_laser_secondary_melee_swing_up)
225                         + (v_right * swing_factor * autocvar_g_balance_laser_secondary_melee_swing_side));
226
227                 WarpZone_traceline_antilag(self.realowner, self.realowner.origin + self.realowner.view_ofs, targpos, FALSE, self.realowner, ANTILAG_LATENCY(self.realowner));
228                 
229                 // draw lightning beams for debugging
230                 //te_lightning2(world, targpos, self.realowner.origin + self.realowner.view_ofs + v_forward * 5 - v_up * 5); 
231                 //te_customflash(targpos, 40,  2, '1 1 1');
232                 
233                 is_player = (trace_ent.classname == "player" || trace_ent.classname == "body");
234
235                 if((trace_fraction < 1) // if trace is good, apply the damage and remove self
236                         && (trace_ent.takedamage == DAMAGE_AIM)  
237                         && (trace_ent != self.swing_alreadyhit)
238                         && (is_player || autocvar_g_balance_laser_secondary_melee_nonplayerdamage))
239                 {
240                         target_victim = trace_ent; // so it persists through other calls
241                         
242                         if(is_player) // this allows us to be able to nerf the non-player damage done in e.g. assault or onslaught.
243                                 swing_damage = (autocvar_g_balance_laser_secondary_damage * min(1, swing_factor + 1));
244                         else
245                                 swing_damage = (autocvar_g_balance_laser_secondary_melee_nonplayerdamage * min(1, swing_factor + 1));
246                         
247                         //print(strcat(self.realowner.netname, " hitting ", target_victim.netname, " with ", strcat(ftos(swing_damage), " damage (factor: ", ftos(swing_factor), ") at "), ftos(time), " seconds.\n"));
248                         
249                         Damage(target_victim, self.realowner, self.realowner, 
250                                 swing_damage, WEP_LASER | HITTYPE_SECONDARY, 
251                                 self.realowner.origin + self.realowner.view_ofs, 
252                                 v_forward * autocvar_g_balance_laser_secondary_force);
253                                 
254                         if(accuracy_isgooddamage(self.realowner, target_victim)) { accuracy_add(self.realowner, WEP_LASER, 0, swing_damage); }
255                                 
256                         // draw large red flash for debugging
257                         //te_customflash(targpos, 200, 2, '15 0 0');
258                         
259                         if(autocvar_g_balance_laser_secondary_melee_multihit) // allow multiple hits with one swing, but not against the same player twice.
260                         {
261                                 self.swing_alreadyhit = target_victim;
262                                 continue; // move along to next trace
263                         }
264                         else
265                         {
266                                 remove(self);
267                                 return;
268                         }
269                 }
270         }
271         
272         if(time >= self.cnt + meleetime)
273         {
274                 // melee is finished
275                 remove(self);
276                 return;
277         }
278         else
279         {
280                 // set up next frame 
281                 self.swing_prev = i;
282                 self.nextthink = time;
283         }
284 }
285
286 void W_Laser_Melee(void)
287 {
288         sound(self, CH_WEAPON_A, "weapons/shotgun_melee.wav", VOL_BASE, ATTN_NORM);
289         weapon_thinkf(WFRAME_FIRE2, autocvar_g_balance_laser_secondary_animtime, w_ready);
290
291         entity meleetemp;
292         meleetemp = spawn();
293         meleetemp.owner = meleetemp.realowner = self;
294         meleetemp.think = W_Laser_Melee_Think;
295         meleetemp.nextthink = time + autocvar_g_balance_laser_secondary_melee_delay * W_WeaponRateFactor();
296         W_SetupShot_Range(self, TRUE, 0, "", 0, autocvar_g_balance_laser_secondary_damage, autocvar_g_balance_laser_secondary_melee_range);
297 }
298
299 void W_Laser_Attack (float issecondary)
300 {
301         entity missile;
302         vector s_forward;
303         float a;
304         float nodamage;
305
306         if(issecondary == 2) // minstanex shot
307                 nodamage = g_minstagib;
308         else
309                 nodamage = FALSE;
310
311         a = autocvar_g_balance_laser_primary_shotangle;
312         s_forward = v_forward * cos(a * DEG2RAD) + v_up * sin(a * DEG2RAD);
313
314         if(nodamage)
315                 W_SetupShot_Dir (self, s_forward, FALSE, 3, "weapons/lasergun_fire.wav", CH_WEAPON_B, 0);
316         else if(issecondary == 1)
317                 W_SetupShot_Dir (self, s_forward, FALSE, 3, "weapons/lasergun_fire.wav", CH_WEAPON_B, autocvar_g_balance_laser_secondary_damage);
318         else
319                 W_SetupShot_Dir (self, s_forward, FALSE, 3, "weapons/lasergun_fire.wav", CH_WEAPON_B, autocvar_g_balance_laser_primary_damage);
320         pointparticles(particleeffectnum("laser_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
321
322         missile = spawn ();
323         missile.owner = missile.realowner = self;
324         missile.classname = "laserbolt";
325         missile.dmg = 0;
326         if(!nodamage)
327         {
328                 missile.bot_dodge = TRUE;
329                 missile.bot_dodgerating = autocvar_g_balance_laser_primary_damage;
330         }
331
332         PROJECTILE_MAKETRIGGER(missile);
333         missile.projectiledeathtype = WEP_LASER;
334
335         setorigin (missile, w_shotorg);
336         setsize(missile, '0 0 0', '0 0 0');
337
338         W_SETUPPROJECTILEVELOCITY(missile, g_balance_laser_primary);
339         missile.angles = vectoangles (missile.velocity);
340         //missile.glow_color = 250; // 244, 250
341         //missile.glow_size = 120;
342         missile.touch = W_Laser_Touch;
343
344         missile.flags = FL_PROJECTILE;
345         missile.missile_flags = MIF_SPLASH; 
346
347         missile.think = W_Laser_Think;
348         missile.nextthink = time + autocvar_g_balance_laser_primary_delay;
349
350         other = missile; MUTATOR_CALLHOOK(EditProjectile);
351
352         if(time >= missile.nextthink)
353         {
354                 entity oldself;
355                 oldself = self;
356                 self = missile;
357                 self.think();
358                 self = oldself;
359         }
360 }
361
362 void spawnfunc_weapon_laser (void)
363 {
364         weapon_defaultspawnfunc(WEP_LASER);
365 }
366
367 float w_laser(float req)
368 {
369         float r1;
370         float r2;
371         if (req == WR_AIM)
372         {
373                 if(autocvar_g_balance_laser_secondary)
374                 {
375                         r1 = autocvar_g_balance_laser_primary_damage;
376                         r2 = autocvar_g_balance_laser_secondary_damage;
377                         if (random() * (r2 + r1) > r1)
378                                 self.BUTTON_ATCK2 = bot_aim(autocvar_g_balance_laser_secondary_speed, 0, autocvar_g_balance_laser_secondary_lifetime, FALSE);
379                         else
380                                 self.BUTTON_ATCK = bot_aim(autocvar_g_balance_laser_primary_speed, 0, autocvar_g_balance_laser_primary_lifetime, FALSE);
381                 }
382                 else
383                         self.BUTTON_ATCK = bot_aim(autocvar_g_balance_laser_primary_speed, 0, autocvar_g_balance_laser_primary_lifetime, FALSE);
384         }
385         else if (req == WR_THINK)
386         {
387                 if(autocvar_g_balance_laser_reload_ammo && self.clip_load < 1) // forced reload
388                         weapon_action(self.weapon, WR_RELOAD);
389                 else if (self.BUTTON_ATCK)
390                 {
391                         if (weapon_prepareattack(0, autocvar_g_balance_laser_primary_refire))
392                         {
393                                 W_DecreaseAmmo(ammo_none, 1, TRUE);
394
395
396                                 if not(autocvar_g_balance_laser_oldprimary)
397                                         W_Laser_Shockwave();
398                                 else
399                                         W_Laser_Attack(FALSE);
400
401                                 weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_laser_primary_animtime, w_ready);
402                         }
403                 }
404                 else if (self.BUTTON_ATCK2)
405                 {
406                         switch(autocvar_g_balance_laser_secondary)
407                         {
408                                 case 0: // switch to last used weapon
409                                 {
410                                         if(self.switchweapon == WEP_LASER) // don't do this if already switching
411                                                 W_LastWeapon();
412
413                                         break;
414                                 }
415
416                                 case 1: // normal projectile secondary
417                                 {
418                                         if(weapon_prepareattack(0, autocvar_g_balance_laser_secondary_refire))
419                                         {
420                                                 W_DecreaseAmmo(ammo_none, 1, TRUE);
421                                                 W_Laser_Attack(TRUE);
422                                                 weapon_thinkf(WFRAME_FIRE2, autocvar_g_balance_laser_secondary_animtime, w_ready);
423                                         }
424
425                                         break;
426                                 }
427
428                                 case 2: // melee attack secondary
429                                 {
430                                         if (self.clip_load >= 0) // we are not currently reloading
431                                         if (!self.crouch) // we are not currently crouching; this fixes an exploit where your melee anim is not visible, and besides wouldn't make much sense
432                                         if (weapon_prepareattack(1, autocvar_g_balance_laser_secondary_refire))
433                                         {
434                                                 // attempt forcing playback of the anim by switching to another anim (that we never play) here...
435                                                 weapon_thinkf(WFRAME_FIRE1, 0, W_Laser_Melee);
436                                         }
437                                 }
438                         }
439                 }
440         }
441         else if (req == WR_PRECACHE)
442         {
443                 precache_model ("models/weapons/g_laser.md3");
444                 precache_model ("models/weapons/v_laser.md3");
445                 precache_model ("models/weapons/h_laser.iqm");
446                 precache_sound ("weapons/lasergun_fire.wav");
447                 //precache_sound ("weapons/reload.wav"); // until weapons have individual reload sounds, precache the reload sound somewhere else
448         }
449         else if (req == WR_SETUP)
450         {
451                 weapon_setup(WEP_LASER);
452                 self.current_ammo = ammo_none;
453         }
454         else if (req == WR_CHECKAMMO1)
455         {
456                 return TRUE;
457         }
458         else if (req == WR_CHECKAMMO2)
459         {
460                 return TRUE;
461         }
462         else if (req == WR_RELOAD)
463         {
464                 W_Reload(0, autocvar_g_balance_laser_reload_ammo, autocvar_g_balance_laser_reload_time, "weapons/reload.wav");
465         }
466         return TRUE;
467 }
468 #endif
469 #ifdef CSQC
470 float w_laser(float req)
471 {
472         if(req == WR_IMPACTEFFECT)
473         {
474                 vector org2;
475                 org2 = w_org + w_backoff * 6;
476                 pointparticles(particleeffectnum("new_laser_impact"), org2, w_backoff * 1000, 1);
477                 if(!w_issilent)
478                         sound(self, CH_SHOTS, "weapons/laserimpact.wav", VOL_BASE, ATTN_NORM);
479         }
480         else if(req == WR_PRECACHE)
481         {
482                 precache_sound("weapons/laserimpact.wav");
483         }
484         else if (req == WR_SUICIDEMESSAGE)
485                 w_deathtypestring = _("%s lasered themself to hell");
486         else if (req == WR_KILLMESSAGE)
487         {
488                 if(w_deathtype & HITTYPE_SECONDARY)
489                         w_deathtypestring = _("%s was cut in half by %s's gauntlet"); // unchecked: SPLASH // TODO 
490                 else
491                         w_deathtypestring = _("%s was lasered to death by %s"); // unchecked: SPLASH
492         }
493         return TRUE;
494 }
495 #endif
496 #endif