]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/w_minelayer.qc
Get rid of TNSF_FAR, short is good enougth. Hide server based turret head from client.
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / w_minelayer.qc
1 #ifdef REGISTER_WEAPON
2 REGISTER_WEAPON(MINE_LAYER, w_minelayer, IT_ROCKETS, 4, WEP_FLAG_RELOADABLE | WEP_TYPE_SPLASH, BOT_PICKUP_RATING_HIGH, "minelayer", "minelayer", _("Mine Layer"))
3 #else
4 #ifdef SVQC
5 void W_Mine_Think (void);
6 .float minelayer_detonate, mine_explodeanyway;
7 .float mine_time;
8
9 void spawnfunc_weapon_minelayer (void)
10 {
11         weapon_defaultspawnfunc(WEP_MINE_LAYER);
12 }
13
14 void W_Mine_Stick ()
15 {
16         spamsound (self, CHAN_PROJECTILE, "weapons/mine_stick.wav", VOL_BASE, ATTN_NORM);
17
18         // in order for mines to face properly when sticking to the ground, they must be a server side entity rather than a csqc projectile
19
20         entity newmine;
21         newmine = spawn();
22         newmine.classname = self.classname;
23
24         newmine.bot_dodge = self.bot_dodge;
25         newmine.bot_dodgerating = self.bot_dodgerating;
26
27         newmine.owner = self.owner;
28         setsize(newmine, '-4 -4 -4', '4 4 4');
29         setorigin(newmine, self.origin);
30         setmodel(newmine, "models/mine.md3");
31         newmine.angles = vectoangles(-trace_plane_normal); // face against the surface
32
33         newmine.oldvelocity = self.velocity;
34
35         newmine.takedamage = self.takedamage;
36         newmine.damageforcescale = self.damageforcescale;
37         newmine.health = self.health;
38         newmine.event_damage = self.event_damage;
39         newmine.spawnshieldtime = self.spawnshieldtime;
40
41         newmine.movetype = MOVETYPE_NONE; // lock the mine in place
42         newmine.projectiledeathtype = self.projectiledeathtype;
43
44         newmine.mine_time = self.mine_time;
45
46         newmine.touch = SUB_Null;
47         newmine.think = W_Mine_Think;
48         newmine.nextthink = time;
49         newmine.cnt = self.cnt;
50         newmine.flags = self.flags;
51
52         remove(self);
53         self = newmine;
54 }
55
56 void W_Mine_Explode ()
57 {
58         if(other.takedamage == DAMAGE_AIM)
59                 if(other.classname == "player")
60                         if(IsDifferentTeam(self.owner, other))
61                                 if(other.deadflag == DEAD_NO)
62                                         if(IsFlying(other))
63                                                 AnnounceTo(self.owner, "airshot");
64
65         self.event_damage = SUB_Null;
66         self.takedamage = DAMAGE_NO;
67
68         RadiusDamage (self, self.owner, autocvar_g_balance_minelayer_damage, autocvar_g_balance_minelayer_edgedamage, autocvar_g_balance_minelayer_radius, world, autocvar_g_balance_minelayer_force, self.projectiledeathtype, other);
69
70         if (self.owner.weapon == WEP_MINE_LAYER)
71         {
72                 entity oldself;
73                 oldself = self;
74                 self = self.owner;
75                 if (!weapon_action(WEP_MINE_LAYER, WR_CHECKAMMO1))
76                 {
77                         self.cnt = WEP_MINE_LAYER;
78                         ATTACK_FINISHED(self) = time;
79                         self.switchweapon = w_getbestweapon(self);
80                 }
81                 self = oldself;
82         }
83         self.owner.minelayer_mines -= 1;
84         remove (self);
85 }
86
87 void W_Mine_DoRemoteExplode ()
88 {
89         self.event_damage = SUB_Null;
90         self.takedamage = DAMAGE_NO;
91
92         if(self.movetype == MOVETYPE_NONE)
93                 self.velocity = self.oldvelocity;
94
95         RadiusDamage (self, self.owner, autocvar_g_balance_minelayer_remote_damage, autocvar_g_balance_minelayer_remote_edgedamage, autocvar_g_balance_minelayer_remote_radius, world, autocvar_g_balance_minelayer_remote_force, self.projectiledeathtype | HITTYPE_BOUNCE, world);
96
97         if (self.owner.weapon == WEP_MINE_LAYER)
98         {
99                 entity oldself;
100                 oldself = self;
101                 self = self.owner;
102                 if (!weapon_action(WEP_MINE_LAYER, WR_CHECKAMMO1))
103                 {
104                         self.cnt = WEP_MINE_LAYER;
105                         ATTACK_FINISHED(self) = time;
106                         self.switchweapon = w_getbestweapon(self);
107                 }
108                 self = oldself;
109         }
110         self.owner.minelayer_mines -= 1;
111         remove (self);
112 }
113
114 void W_Mine_RemoteExplode ()
115 {
116         if(self.owner.deadflag == DEAD_NO)
117                 if((self.spawnshieldtime >= 0)
118                         ? (time >= self.spawnshieldtime) // timer
119                         : (vlen(NearestPointOnBox(self.owner, self.origin) - self.origin) > autocvar_g_balance_minelayer_remote_radius) // safety device
120                 )
121                 {
122                         W_Mine_DoRemoteExplode();
123                 }
124 }
125
126 void W_Mine_ProximityExplode ()
127 {
128         // make sure no friend is in the mine's radius. If there is any, explosion is delayed until he's at a safe distance
129         if(autocvar_g_balance_minelayer_protection && self.mine_explodeanyway == 0)
130         {
131                 entity head;
132                 head = findradius(self.origin, autocvar_g_balance_minelayer_radius);
133                 while(head)
134                 {
135                         if(head == self.owner || !IsDifferentTeam(head, self.owner))
136                                 return;
137                         head = head.chain;
138                 }
139         }
140
141         self.mine_time = 0;
142         W_Mine_Explode();
143 }
144
145 float W_Mine_Count(entity e)
146 {
147         float minecount;
148         entity mine;
149         for(mine = world; (mine = find(mine, classname, "mine")); ) if(mine.owner == e)
150                 minecount += 1;
151                 
152         return minecount;
153 }
154
155 void W_Mine_Think (void)
156 {
157         entity head;
158
159         self.nextthink = time;
160         
161         // our lifetime has expired, it's time to die - mine_time just allows us to play a sound for this
162         // TODO: replace this mine_trigger.wav sound with a real countdown
163         if ((time > self.cnt) && (!self.mine_time))
164         {
165                 if(autocvar_g_balance_minelayer_lifetime_countdown > 0)
166                         spamsound (self, CHAN_PROJECTILE, "weapons/mine_trigger.wav", VOL_BASE, ATTN_NORM);
167                 self.mine_time = time + autocvar_g_balance_minelayer_lifetime_countdown;
168                 self.mine_explodeanyway = 1; // make the mine super aggressive -- Samual: Rather, make it not care if a team mate is near.
169         }
170
171         // a player's mines shall explode if he disconnects or dies
172         // TODO: Do this on team change too -- Samual: But isn't a player killed when they switch teams?
173         if(self.owner.classname != "player" || self.owner.deadflag != DEAD_NO)
174         {
175                 other = world;
176                 self.projectiledeathtype |= HITTYPE_BOUNCE;
177                 W_Mine_Explode();
178                 return;
179         }
180
181         // set the mine for detonation when a foe gets close enough
182         head = findradius(self.origin, autocvar_g_balance_minelayer_proximityradius);
183         while(head)
184         {
185                 if(head.classname == "player" && head.deadflag == DEAD_NO)
186                 if(head != self.owner && IsDifferentTeam(head, self.owner)) // don't trigger for team mates
187                 if(!self.mine_time)
188                 {
189                         spamsound (self, CHAN_PROJECTILE, "weapons/mine_trigger.wav", VOL_BASE, ATTN_NORM);
190                         self.mine_time = time + autocvar_g_balance_minelayer_time;
191                 }
192                 head = head.chain;
193         }
194
195         // explode if it's time to
196         if(self.mine_time && time >= self.mine_time)
197         {
198                 W_Mine_ProximityExplode();
199                 return;
200         }
201
202         // remote detonation
203         if (self.owner.weapon == WEP_MINE_LAYER)
204         if (self.owner.deadflag == DEAD_NO)
205         if (self.minelayer_detonate)
206                 W_Mine_RemoteExplode();
207 }
208
209 void W_Mine_Touch (void)
210 {
211         PROJECTILE_TOUCH;
212         if(!other || (other.takedamage != DAMAGE_AIM && other.movetype == MOVETYPE_NONE))
213                 W_Mine_Stick();
214         else if(self.movetype != MOVETYPE_NONE) // don't unstick a locked mine when someone touches it
215                 self.velocity = '0 0 0';
216 }
217
218 void W_Mine_Damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
219 {
220         if (self.health <= 0)
221                 return;
222         self.health = self.health - damage;
223         self.angles = vectoangles(self.velocity);
224         if (self.health <= 0)
225                 W_PrepareExplosionByDamage(attacker, W_Mine_Explode);
226 }
227
228 void W_Mine_Attack (void)
229 {
230         entity mine;
231         entity flash;
232
233         // scan how many mines we placed, and return if we reached our limit
234         if(autocvar_g_balance_minelayer_limit)
235         {
236         
237                 if(W_Mine_Count(self) >= autocvar_g_balance_minelayer_limit)
238                 {
239                         // the refire delay keeps this message from being spammed
240                         sprint(self, strcat("You cannot place more than ^2", ftos(autocvar_g_balance_minelayer_limit), " ^7mines at a time\n") );
241                         play2(self, "weapons/unavailable.wav");
242                         return;
243                 }
244         }
245
246         W_DecreaseAmmo(ammo_rockets, autocvar_g_balance_minelayer_ammo, autocvar_g_balance_minelayer_reload_ammo);
247
248         W_SetupShot_ProjectileSize (self, '-4 -4 -4', '4 4 4', FALSE, 5, "weapons/mine_fire.wav", CHAN_WEAPON, autocvar_g_balance_minelayer_damage);
249         pointparticles(particleeffectnum("rocketlauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
250
251         mine = WarpZone_RefSys_SpawnSameRefSys(self);
252         mine.owner = self;
253         if(autocvar_g_balance_minelayer_detonatedelay >= 0)
254                 mine.spawnshieldtime = time + autocvar_g_balance_minelayer_detonatedelay;
255         else
256                 mine.spawnshieldtime = -1;
257         mine.classname = "mine";
258         mine.bot_dodge = TRUE;
259         mine.bot_dodgerating = autocvar_g_balance_minelayer_damage * 2; // * 2 because it can detonate inflight which makes it even more dangerous
260
261         mine.takedamage = DAMAGE_YES;
262         mine.damageforcescale = autocvar_g_balance_minelayer_damageforcescale;
263         mine.health = autocvar_g_balance_minelayer_health;
264         mine.event_damage = W_Mine_Damage;
265
266         mine.movetype = MOVETYPE_TOSS;
267         PROJECTILE_MAKETRIGGER(mine);
268         mine.projectiledeathtype = WEP_MINE_LAYER;
269         setsize (mine, '-4 -4 -4', '4 4 4'); // give it some size so it can be shot
270
271         setorigin (mine, w_shotorg - v_forward * 4); // move it back so it hits the wall at the right point
272         W_SetupProjectileVelocity(mine, autocvar_g_balance_minelayer_speed, 0);
273         mine.angles = vectoangles (mine.velocity);
274
275         mine.touch = W_Mine_Touch;
276         mine.think = W_Mine_Think;
277         mine.nextthink = time;
278         mine.cnt = time + (autocvar_g_balance_minelayer_lifetime - autocvar_g_balance_minelayer_lifetime_countdown);
279         mine.flags = FL_PROJECTILE;
280
281         CSQCProjectile(mine, TRUE, PROJECTILE_MINE, TRUE);
282
283         // muzzle flash for 1st person view
284         flash = spawn ();
285         setmodel (flash, "models/flash.md3"); // precision set below
286         SUB_SetFade (flash, time, 0.1);
287         flash.effects = EF_ADDITIVE | EF_FULLBRIGHT | EF_LOWPRECISION;
288         W_AttachToShotorg(flash, '5 0 0');
289
290         // common properties
291
292         other = mine; MUTATOR_CALLHOOK(EditProjectile);
293         
294         self.minelayer_mines = W_Mine_Count(self);
295 }
296
297 void spawnfunc_weapon_minelayer (void); // defined in t_items.qc
298
299 float W_PlacedMines(float detonate)
300 {
301         entity mine;
302         float minfound;
303
304         for(mine = world; (mine = find(mine, classname, "mine")); ) if(mine.owner == self)
305         {
306                 if(detonate)
307                 {
308                         if(!mine.minelayer_detonate)
309                         {
310                                 mine.minelayer_detonate = TRUE;
311                                 minfound = 1;
312                         }
313                 }
314                 else
315                         minfound = 1;
316         }
317         return minfound;
318 }
319
320 float w_minelayer(float req)
321 {
322         entity mine;
323         float ammo_amount;
324
325         if (req == WR_AIM)
326         {
327                 // aim and decide to fire if appropriate
328                 self.BUTTON_ATCK = bot_aim(autocvar_g_balance_minelayer_speed, 0, autocvar_g_balance_minelayer_lifetime, FALSE);
329                 if(skill >= 2) // skill 0 and 1 bots won't detonate mines!
330                 {
331                         // decide whether to detonate mines
332                         entity targetlist, targ;
333                         float edgedamage, coredamage, edgeradius, recipricoledgeradius, d;
334                         float selfdamage, teamdamage, enemydamage;
335                         edgedamage = autocvar_g_balance_minelayer_edgedamage;
336                         coredamage = autocvar_g_balance_minelayer_damage;
337                         edgeradius = autocvar_g_balance_minelayer_radius;
338                         recipricoledgeradius = 1 / edgeradius;
339                         selfdamage = 0;
340                         teamdamage = 0;
341                         enemydamage = 0;
342                         targetlist = findchainfloat(bot_attack, TRUE);
343                         mine = find(world, classname, "mine");
344                         while (mine)
345                         {
346                                 if (mine.owner != self)
347                                 {
348                                         mine = find(mine, classname, "mine");
349                                         continue;
350                                 }
351                                 targ = targetlist;
352                                 while (targ)
353                                 {
354                                         d = vlen(targ.origin + (targ.mins + targ.maxs) * 0.5 - mine.origin);
355                                         d = bound(0, edgedamage + (coredamage - edgedamage) * sqrt(1 - d * recipricoledgeradius), 10000);
356                                         // count potential damage according to type of target
357                                         if (targ == self)
358                                                 selfdamage = selfdamage + d;
359                                         else if (targ.team == self.team && teamplay)
360                                                 teamdamage = teamdamage + d;
361                                         else if (bot_shouldattack(targ))
362                                                 enemydamage = enemydamage + d;
363                                         targ = targ.chain;
364                                 }
365                                 mine = find(mine, classname, "mine");
366                         }
367                         float desirabledamage;
368                         desirabledamage = enemydamage;
369                         if (time > self.invincible_finished && time > self.spawnshieldtime)
370                                 desirabledamage = desirabledamage - selfdamage * autocvar_g_balance_selfdamagepercent;
371                         if (teamplay && self.team)
372                                 desirabledamage = desirabledamage - teamdamage;
373
374                         mine = find(world, classname, "mine");
375                         while (mine)
376                         {
377                                 if (mine.owner != self)
378                                 {
379                                         mine = find(mine, classname, "mine");
380                                         continue;
381                                 }
382                                 makevectors(mine.v_angle);
383                                 targ = targetlist;
384                                 if (skill > 9) // normal players only do this for the target they are tracking
385                                 {
386                                         targ = targetlist;
387                                         while (targ)
388                                         {
389                                                 if (
390                                                         (v_forward * normalize(mine.origin - targ.origin)< 0.1)
391                                                         && desirabledamage > 0.1*coredamage
392                                                 )self.BUTTON_ATCK2 = TRUE;
393                                                 targ = targ.chain;
394                                         }
395                                 }else{
396                                         float distance; distance= bound(300,vlen(self.origin-self.enemy.origin),30000);
397                                         //As the distance gets larger, a correct detonation gets near imposible
398                                         //Bots are assumed to use the mine spawnfunc_light to see if the mine gets near a player
399                                         if(v_forward * normalize(mine.origin - self.enemy.origin)< 0.1)
400                                                 if(self.enemy.classname == "player")
401                                                         if(desirabledamage >= 0.1*coredamage)
402                                                                 if(random()/distance*300 > frametime*bound(0,(10-skill)*0.2,1))
403                                                                         self.BUTTON_ATCK2 = TRUE;
404                                 //      dprint(ftos(random()/distance*300),">");dprint(ftos(frametime*bound(0,(10-skill)*0.2,1)),"\n");
405                                 }
406
407                                 mine = find(mine, classname, "mine");
408                         }
409                         // if we would be doing at X percent of the core damage, detonate it
410                         // but don't fire a new shot at the same time!
411                         if (desirabledamage >= 0.75 * coredamage) //this should do group damage in rare fortunate events
412                                 self.BUTTON_ATCK2 = TRUE;
413                         if ((skill > 6.5) && (selfdamage > self.health))
414                                 self.BUTTON_ATCK2 = FALSE;
415                         //if(self.BUTTON_ATCK2 == TRUE)
416                         //      dprint(ftos(desirabledamage),"\n");
417                         if (self.BUTTON_ATCK2 == TRUE) self.BUTTON_ATCK = FALSE;
418                 }
419         }
420         else if (req == WR_THINK)
421         {
422                 if(autocvar_g_balance_minelayer_reload_ammo && self.clip_load < autocvar_g_balance_minelayer_ammo) // forced reload
423                 {
424                         // not if we're holding the minelayer without enough ammo, but can detonate existing mines
425                         if not (W_PlacedMines(FALSE) && self.ammo_rockets < autocvar_g_balance_minelayer_ammo)
426                                 weapon_action(self.weapon, WR_RELOAD);
427                 }
428                 else if (self.BUTTON_ATCK)
429                 {
430                         if(weapon_prepareattack(0, autocvar_g_balance_minelayer_refire))
431                         {
432                                 W_Mine_Attack();
433                                 weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_minelayer_animtime, w_ready);
434                         }
435                 }
436
437                 if (self.BUTTON_ATCK2)
438                 {
439                         if(W_PlacedMines(TRUE))
440                                 sound (self, CHAN_WEAPON2, "weapons/mine_det.wav", VOL_BASE, ATTN_NORM);
441                 }
442         }
443         else if (req == WR_PRECACHE)
444         {
445                 precache_model ("models/flash.md3");
446                 precache_model ("models/mine.md3");
447                 precache_model ("models/weapons/g_minelayer.md3");
448                 precache_model ("models/weapons/v_minelayer.md3");
449                 precache_model ("models/weapons/h_minelayer.iqm");
450                 precache_sound ("weapons/mine_det.wav");
451                 precache_sound ("weapons/mine_fire.wav");
452                 precache_sound ("weapons/mine_stick.wav");
453                 precache_sound ("weapons/mine_trigger.wav");
454                 //precache_sound ("weapons/reload.wav"); // until weapons have individual reload sounds, precache the reload sound somewhere else
455         }
456         else if (req == WR_SETUP)
457         {
458                 weapon_setup(WEP_MINE_LAYER);
459                 self.current_ammo = ammo_rockets;
460         }
461         else if (req == WR_CHECKAMMO1)
462         {
463                 // don't switch while placing a mine
464                 if (ATTACK_FINISHED(self) <= time || self.weapon != WEP_MINE_LAYER)
465                 {
466                         ammo_amount = self.ammo_rockets >= autocvar_g_balance_minelayer_ammo;
467                         ammo_amount += self.weapon_load[WEP_MINE_LAYER] >= autocvar_g_balance_minelayer_ammo;
468                         return ammo_amount;
469                 }
470         }
471         else if (req == WR_CHECKAMMO2)
472         {
473                 if (W_PlacedMines(FALSE))
474                         return TRUE;
475                 else
476                         return FALSE;
477         }
478         else if (req == WR_RELOAD)
479         {
480                 W_Reload(autocvar_g_balance_minelayer_ammo, autocvar_g_balance_minelayer_reload_ammo, autocvar_g_balance_minelayer_reload_time, "weapons/reload.wav");
481         }
482         return TRUE;
483 };
484 #endif
485 #ifdef CSQC
486 float w_minelayer(float req)
487 {
488         if(req == WR_IMPACTEFFECT)
489         {
490                 vector org2;
491                 org2 = w_org + w_backoff * 12;
492                 pointparticles(particleeffectnum("rocket_explode"), org2, '0 0 0', 1);
493                 if(!w_issilent)
494                         sound(self, CHAN_PROJECTILE, "weapons/mine_exp.wav", VOL_BASE, ATTN_NORM);
495         }
496         else if(req == WR_PRECACHE)
497         {
498                 precache_sound("weapons/mine_exp.wav");
499         }
500         else if (req == WR_SUICIDEMESSAGE)
501                 w_deathtypestring = _("%s exploded");
502         else if (req == WR_KILLMESSAGE)
503         {
504                 if(w_deathtype & HITTYPE_BOUNCE) // (remote detonation)
505                         w_deathtypestring = _("%s got too close to %s's mine");
506                 else if(w_deathtype & HITTYPE_SPLASH)
507                         w_deathtypestring = _("%s almost dodged %s's mine");
508                 else
509                         w_deathtypestring = _("%s stepped on %s's mine");
510         }
511         return TRUE;
512 }
513 #endif
514 #endif