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