]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/w_minelayer.qc
Don't cancel mine explosion if the foe left its vicinity before it goes off
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / w_minelayer.qc
1 /*TODO list (things left to do before this weapon should be ready, delete once it's all done):
2 - The weapon currently uses sounds and models from other weapons. We need a modeler and sound artist to make this weapon its own (the gun model should probably be something between the porto and rocket launcher design-wise).
3 - Mines remain stuck in the air if they hit a moving entity (like an elevator or players). They should normally stick to them... perhaps set them as an attachment?
4 - Bot code for the weapon may be needed. The bot AI may not have any info about this gun yet.
5 - The mine model needs to face properly when it sticks to a surface. Once we'll have a correct mine model, we can't afford the model facing any way it falls to the ground. Should probably look at the porto code to see how portals face in the right direction when sticking to walls. 
6 */
7
8 #ifdef REGISTER_WEAPON
9 REGISTER_WEAPON(MINE_LAYER, w_minelayer, IT_ROCKETS, 4, WEP_FLAG_NORMAL | WEP_TYPE_SPLASH, BOT_PICKUP_RATING_HIGH, "minelayer", "minelayer", "Mine Layer");
10 #else
11 #ifdef SVQC
12 .float minelayer_detonate;
13 .float mine_number, mine_time;
14
15 void spawnfunc_weapon_minelayer (void)
16 {
17         weapon_defaultspawnfunc(WEP_MINE_LAYER);
18 }
19
20 void W_Mine_Unregister()
21 {
22         if(self.owner && self.owner.lastmine == self)
23                 self.owner.lastmine = world;
24 }
25
26 void W_Mine_Explode ()
27 {
28         W_Mine_Unregister();
29
30         if(other.takedamage == DAMAGE_AIM)
31                 if(other.classname == "player")
32                         if(IsDifferentTeam(self.owner, other))
33                                 if(IsFlying(other))
34                                         AnnounceTo(self.owner, "airshot");
35
36         self.event_damage = SUB_Null;
37         self.takedamage = DAMAGE_NO;
38
39         RadiusDamage (self, self.owner, cvar("g_balance_minelayer_damage"), cvar("g_balance_minelayer_edgedamage"), cvar("g_balance_minelayer_radius"), world, cvar("g_balance_minelayer_force"), self.projectiledeathtype, other);
40
41         if (self.owner.weapon == WEP_MINE_LAYER)
42         {
43                 if(self.owner.ammo_rockets < cvar("g_balance_minelayer_ammo"))
44                 {
45                         self.owner.cnt = WEP_MINE_LAYER;
46                         ATTACK_FINISHED(self.owner) = time;
47                         self.owner.switchweapon = w_getbestweapon(self.owner);
48                 }
49         }
50         remove (self);
51 }
52
53 void W_Mine_DoRemoteExplode ()
54 {
55         W_Mine_Unregister();
56
57         self.event_damage = SUB_Null;
58         self.takedamage = DAMAGE_NO;
59
60         RadiusDamage (self, self.owner, cvar("g_balance_minelayer_remote_damage"), cvar("g_balance_minelayer_remote_edgedamage"), cvar("g_balance_minelayer_remote_radius"), world, cvar("g_balance_minelayer_remote_force"), self.projectiledeathtype | HITTYPE_BOUNCE, world);
61
62         if (self.owner.weapon == WEP_MINE_LAYER)
63         {
64                 if(self.owner.ammo_rockets < cvar("g_balance_minelayer_ammo"))
65                 {
66                         self.owner.cnt = WEP_MINE_LAYER;
67                         ATTACK_FINISHED(self.owner) = time;
68                         self.owner.switchweapon = w_getbestweapon(self.owner);
69                 }
70         }
71         remove (self);
72 }
73
74 void W_Mine_RemoteExplode()
75 {
76         if(self.owner.deadflag == DEAD_NO)
77         if(self.owner.lastmine)
78         {
79                 if((self.spawnshieldtime >= 0)
80                         ? (time >= self.spawnshieldtime) // timer
81                         : (vlen(NearestPointOnBox(self.owner, self.origin) - self.origin) > cvar("g_balance_minelayer_radius")) // safety device
82                 )
83                 {
84                         W_Mine_DoRemoteExplode();
85                 }
86         }
87 }
88
89 void W_Mine_ProximityExplode()
90 {
91         // make sure no friend is in the mine's radius. If there is any, explosion is delayed until he's at a safe distance
92         if(cvar("g_balance_minelayer_protection"))
93         {
94                 entity head;
95                 head = findradius(self.origin, cvar("g_balance_minelayer_radius"));
96                 while(head)
97                 {
98                         if(head == self.owner || !IsDifferentTeam(head, self.owner))
99                                 return;
100                         head = head.chain;
101                 }
102         }
103
104         self.mine_time = 0;
105         W_Mine_Explode();
106 }
107
108 void W_Mine_Think (void)
109 {
110         entity head;
111
112         self.nextthink = time;
113         if (time > self.cnt)
114         {
115                 other = world;
116                 self.projectiledeathtype |= HITTYPE_BOUNCE;
117                 W_Mine_Explode ();
118                 return;
119         }
120
121         // set the mine for detonation when a foe gets too close
122         head = findradius(self.origin, cvar("g_balance_minelayer_detectionradius"));
123         while(head)
124         {
125                 if(head.classname == "player" && head.deadflag == DEAD_NO)
126                 if(head != self.owner && IsDifferentTeam(head, self.owner)) // don't trigger for team mates
127                 if(!self.mine_time)
128                 {
129                         spamsound (self, CHAN_PROJECTILE, "weapons/mine_trigger.wav", VOL_BASE, ATTN_NORM);
130                         self.mine_time = time + cvar("g_balance_minelayer_time");
131                 }
132                 head = head.chain;
133         }
134
135         // explode if it's time to
136         if(self.mine_time && time >= self.mine_time)
137                 W_Mine_ProximityExplode();
138
139         // remote detonation
140         if (self.owner.weapon == WEP_MINE_LAYER)
141         if (self.owner.deadflag == DEAD_NO)
142         if (self.minelayer_detonate)
143                 W_Mine_RemoteExplode();
144
145         if(self.csqcprojectile_clientanimate == 0)
146                 UpdateCSQCProjectile(self);
147 }
148
149 void W_Mine_Touch (void)
150 {
151         PROJECTILE_TOUCH;
152         spamsound (self, CHAN_PROJECTILE, "weapons/mine_stick.wav", VOL_BASE, ATTN_NORM);
153         self.movetype = MOVETYPE_NONE; // lock the mine in place
154         // TODO: make sure this doesn't cause the mine to get stuck in the air if it falls over a moving entity
155 }
156
157 void W_Mine_Damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
158 {
159         if (self.health <= 0)
160                 return;
161         self.health = self.health - damage;
162         self.angles = vectoangles(self.velocity);
163         if (self.health <= 0)
164                 W_PrepareExplosionByDamage(attacker, W_Mine_Explode);
165 }
166
167 void W_Mine_Attack (void)
168 {
169         local entity mine;
170         local entity flash;
171
172         // scan how many mines we placed, and return if we reached our limit
173         if(cvar("g_balance_minelayer_limit"))
174         {
175                 entity mine;
176                 self.mine_number = 0;
177                 for(mine = world; (mine = find(mine, classname, "mine")); ) if(mine.owner == self)
178                         self.mine_number += 1;
179
180                 if(self.mine_number >= cvar("g_balance_minelayer_limit"))
181                 {
182                         // the refire delay keeps this message from being spammed
183                         sprint(self, strcat("You cannot place more than ^2", cvar_string("g_balance_minelayer_limit"), " ^7mines at a time\n") );
184                         play2(self, "weapons/unavailable.wav");
185                         return;
186                 }
187         }
188
189         if not(self.items & IT_UNLIMITED_WEAPON_AMMO)
190                 self.ammo_rockets = self.ammo_rockets - cvar("g_balance_minelayer_ammo");
191
192         W_SetupShot_ProjectileSize (self, '-4 -4 -4', '4 4 4', FALSE, 5, "weapons/mine_fire.wav", cvar("g_balance_minelayer_damage"));
193         pointparticles(particleeffectnum("rocketlauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
194
195         mine = WarpZone_RefSys_SpawnSameRefSys(self);
196         mine.owner = self;
197         self.lastmine = mine;
198         if(cvar("g_balance_minelayer_detonatedelay") >= 0)
199                 mine.spawnshieldtime = time + cvar("g_balance_minelayer_detonatedelay");
200         else
201                 mine.spawnshieldtime = -1;
202         mine.classname = "mine";
203         mine.bot_dodge = TRUE;
204         mine.bot_dodgerating = cvar("g_balance_minelayer_damage") * 2; // * 2 because it can detonate inflight which makes it even more dangerous
205
206         mine.takedamage = DAMAGE_YES;
207         mine.damageforcescale = cvar("g_balance_minelayer_damageforcescale");
208         mine.health = cvar("g_balance_minelayer_health");
209         mine.event_damage = W_Mine_Damage;
210
211         mine.movetype = MOVETYPE_TOSS;
212         PROJECTILE_MAKETRIGGER(mine);
213         mine.projectiledeathtype = WEP_MINE_LAYER;
214         setsize (mine, '-4 -4 -4', '4 4 4'); // give it some size so it can be shot
215
216         setorigin (mine, w_shotorg - v_forward * 4); // move it back so it hits the wall at the right point
217         W_SetupProjectileVelocity(mine, cvar("g_balance_minelayer_speedstart"), 0);
218         mine.angles = vectoangles (mine.velocity);
219
220         mine.touch = W_Mine_Touch;
221         mine.think = W_Mine_Think;
222         mine.nextthink = time;
223         mine.cnt = time + cvar("g_balance_minelayer_lifetime");
224         mine.flags = FL_PROJECTILE;
225
226         CSQCProjectile(mine, FALSE, PROJECTILE_MINE, TRUE);
227
228         // muzzle flash for 1st person view
229         flash = spawn ();
230         setmodel (flash, "models/flash.md3"); // precision set below
231         SUB_SetFade (flash, time, 0.1);
232         flash.effects = EF_ADDITIVE | EF_FULLBRIGHT | EF_LOWPRECISION;
233         W_AttachToShotorg(flash, '5 0 0');
234
235         // common properties
236 }
237
238 void spawnfunc_weapon_minelayer (void); // defined in t_items.qc
239
240 float w_minelayer(float req)
241 {
242         entity mine;
243         float minfound;
244         if (req == WR_AIM)
245         {
246                 // aim and decide to fire if appropriate
247                 self.BUTTON_ATCK = bot_aim(cvar("g_balance_minelayer_speed"), 0, cvar("g_balance_minelayer_lifetime"), FALSE);
248                 if(skill >= 2) // skill 0 and 1 bots won't detonate mines!
249                 {
250                         // decide whether to detonate mines
251                         local entity mine, targetlist, targ;
252                         local float edgedamage, coredamage, edgeradius, recipricoledgeradius, d;
253                         local float selfdamage, teamdamage, enemydamage;
254                         edgedamage = cvar("g_balance_minelayer_edgedamage");
255                         coredamage = cvar("g_balance_minelayer_damage");
256                         edgeradius = cvar("g_balance_minelayer_radius");
257                         recipricoledgeradius = 1 / edgeradius;
258                         selfdamage = 0;
259                         teamdamage = 0;
260                         enemydamage = 0;
261                         targetlist = findchainfloat(bot_attack, TRUE);
262                         mine = find(world, classname, "mine");
263                         while (mine)
264                         {
265                                 if (mine.owner != self)
266                                 {
267                                         mine = find(mine, classname, "mine");
268                                         continue;
269                                 }
270                                 targ = targetlist;
271                                 while (targ)
272                                 {
273                                         d = vlen(targ.origin + (targ.mins + targ.maxs) * 0.5 - mine.origin);
274                                         d = bound(0, edgedamage + (coredamage - edgedamage) * sqrt(1 - d * recipricoledgeradius), 10000);
275                                         // count potential damage according to type of target
276                                         if (targ == self)
277                                                 selfdamage = selfdamage + d;
278                                         else if (targ.team == self.team && teams_matter)
279                                                 teamdamage = teamdamage + d;
280                                         else if (bot_shouldattack(targ))
281                                                 enemydamage = enemydamage + d;
282                                         targ = targ.chain;
283                                 }
284                                 mine = find(mine, classname, "mine");
285                         }
286                         local float desirabledamage;
287                         desirabledamage = enemydamage;
288                         if (teamplay != 1 && time > self.invincible_finished && time > self.spawnshieldtime)
289                                 desirabledamage = desirabledamage - selfdamage * cvar("g_balance_selfdamagepercent");
290                         if (self.team && teamplay != 1)
291                                 desirabledamage = desirabledamage - teamdamage;
292
293                         mine = find(world, classname, "mine");
294                         while (mine)
295                         {
296                                 if (mine.owner != self)
297                                 {
298                                         mine = find(mine, classname, "mine");
299                                         continue;
300                                 }
301                                 makevectors(mine.v_angle);
302                                 targ = targetlist;
303                                 if (skill > 9) // normal players only do this for the target they are tracking
304                                 {
305                                         targ = targetlist;
306                                         while (targ)
307                                         {
308                                                 if (
309                                                         (v_forward * normalize(mine.origin - targ.origin)< 0.1)
310                                                         && desirabledamage > 0.1*coredamage
311                                                 )self.BUTTON_ATCK2 = TRUE;
312                                                 targ = targ.chain;
313                                         }
314                                 }else{
315                                         local float distance; distance= bound(300,vlen(self.origin-self.enemy.origin),30000);
316                                         //As the distance gets larger, a correct detonation gets near imposible
317                                         //Bots are assumed to use the mine spawnfunc_light to see if the mine gets near a player
318                                         if(v_forward * normalize(mine.origin - self.enemy.origin)< 0.1)
319                                                 if(self.enemy.classname == "player")
320                                                         if(desirabledamage >= 0.1*coredamage)
321                                                                 if(random()/distance*300 > frametime*bound(0,(10-skill)*0.2,1))
322                                                                         self.BUTTON_ATCK2 = TRUE;
323                                 //      dprint(ftos(random()/distance*300),">");dprint(ftos(frametime*bound(0,(10-skill)*0.2,1)),"\n");
324                                 }
325
326                                 mine = find(mine, classname, "mine");
327                         }
328                         // if we would be doing at X percent of the core damage, detonate it
329                         // but don't fire a new shot at the same time!
330                         if (desirabledamage >= 0.75 * coredamage) //this should do group damage in rare fortunate events
331                                 self.BUTTON_ATCK2 = TRUE;
332                         if ((skill > 6.5) && (selfdamage > self.health))
333                                 self.BUTTON_ATCK2 = FALSE;
334                         //if(self.BUTTON_ATCK2 == TRUE)
335                         //      dprint(ftos(desirabledamage),"\n");
336                         if (self.BUTTON_ATCK2 == TRUE) self.BUTTON_ATCK = FALSE;
337                 }
338         }
339         else if (req == WR_THINK)
340         {
341                 if (self.BUTTON_ATCK)
342                 {
343                         if(weapon_prepareattack(0, cvar("g_balance_minelayer_refire")))
344                         {
345                                 W_Mine_Attack();
346                                 weapon_thinkf(WFRAME_FIRE1, cvar("g_balance_minelayer_animtime"), w_ready);
347                         }
348                 }
349
350                 if (self.BUTTON_ATCK2)
351                 {
352                         minfound = 0;
353                         for(mine = world; (mine = find(mine, classname, "mine")); ) if(mine.owner == self)
354                         {
355                                 if(!mine.minelayer_detonate)
356                                 {
357                                         mine.minelayer_detonate = TRUE;
358                                         minfound = 1;
359                                 }
360                         }
361                         if(minfound)
362                                 sound (self, CHAN_WEAPON2, "weapons/mine_det.wav", VOL_BASE, ATTN_NORM);
363                 }
364         }
365         else if (req == WR_PRECACHE)
366         {
367                 precache_model ("models/flash.md3");
368                 precache_model ("models/weapons/g_minelayer.md3");
369                 precache_model ("models/weapons/v_minelayer.md3");
370                 precache_model ("models/weapons/h_minelayer.iqm");
371                 precache_sound ("weapons/mine_det.wav");
372                 precache_sound ("weapons/mine_fire.wav");
373                 precache_sound ("weapons/mine_stick.wav");
374                 precache_sound ("weapons/mine_trigger.wav");
375         }
376         else if (req == WR_SETUP)
377         {
378                 weapon_setup(WEP_MINE_LAYER);
379         }
380         else if (req == WR_CHECKAMMO1)
381         {
382                 // don't switch while placing a mine
383                 if ((ATTACK_FINISHED(self) <= time || self.weapon != WEP_MINE_LAYER)
384                         && self.ammo_rockets < cvar("g_balance_minelayer_ammo"))
385                         return FALSE;
386         }
387         else if (req == WR_CHECKAMMO2)
388                 return FALSE;
389         return TRUE;
390 };
391 #endif
392 #ifdef CSQC
393 float w_minelayer(float req)
394 {
395         if(req == WR_IMPACTEFFECT)
396         {
397                 vector org2;
398                 org2 = w_org + w_backoff * 12;
399                 pointparticles(particleeffectnum("rocket_explode"), org2, '0 0 0', 1);
400                 if(!w_issilent)
401                         sound(self, CHAN_PROJECTILE, "weapons/mine_exp.wav", VOL_BASE, ATTN_NORM);
402         }
403         else if(req == WR_PRECACHE)
404         {
405                 precache_sound("weapons/mine_exp.wav");
406         }
407         else if (req == WR_SUICIDEMESSAGE)
408                 w_deathtypestring = "%s exploded";
409         else if (req == WR_KILLMESSAGE)
410         {
411                 if(w_deathtype & HITTYPE_BOUNCE) // (remote detonation)
412                         w_deathtypestring = "%s got too close to %s's mine";
413                 else if(w_deathtype & HITTYPE_SPLASH)
414                         w_deathtypestring = "%s almost dodged %s's mine";
415                 else
416                         w_deathtypestring = "%s stepped on %s's mine";
417         }
418         return TRUE;
419 }
420 #endif
421 #endif