]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/monsters/monster/mage.qc
Detonate mage missiles instantly if the mage dies
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / monsters / monster / mage.qc
1 #ifdef REGISTER_MONSTER
2 REGISTER_MONSTER(
3 /* MON_##id   */ MAGE,
4 /* function   */ m_mage,
5 /* spawnflags */ 0,
6 /* mins,maxs  */ '-36 -36 -24', '36 36 50',
7 /* model      */ "mage.dpm",
8 /* netname    */ "mage",
9 /* fullname   */ _("Mage")
10 );
11
12 #define MAGE_SETTINGS(monster) \
13         MON_ADD_CVAR(monster, health) \
14         MON_ADD_CVAR(monster, attack_spike_damage) \
15         MON_ADD_CVAR(monster, attack_spike_radius) \
16         MON_ADD_CVAR(monster, attack_spike_delay) \
17         MON_ADD_CVAR(monster, attack_spike_accel) \
18         MON_ADD_CVAR(monster, attack_spike_decel) \
19         MON_ADD_CVAR(monster, attack_spike_turnrate) \
20         MON_ADD_CVAR(monster, attack_spike_speed_max) \
21         MON_ADD_CVAR(monster, attack_spike_smart) \
22         MON_ADD_CVAR(monster, attack_spike_smart_trace_min) \
23         MON_ADD_CVAR(monster, attack_spike_smart_trace_max) \
24         MON_ADD_CVAR(monster, attack_spike_smart_mindist) \
25         MON_ADD_CVAR(monster, attack_melee_damage) \
26         MON_ADD_CVAR(monster, attack_melee_delay) \
27         MON_ADD_CVAR(monster, attack_grenade_damage) \
28         MON_ADD_CVAR(monster, attack_grenade_edgedamage) \
29         MON_ADD_CVAR(monster, attack_grenade_force) \
30         MON_ADD_CVAR(monster, attack_grenade_radius) \
31         MON_ADD_CVAR(monster, attack_grenade_lifetime) \
32         MON_ADD_CVAR(monster, attack_grenade_chance) \
33         MON_ADD_CVAR(monster, attack_grenade_speed) \
34         MON_ADD_CVAR(monster, attack_grenade_speed_up) \
35         MON_ADD_CVAR(monster, heal_self) \
36         MON_ADD_CVAR(monster, heal_allies) \
37         MON_ADD_CVAR(monster, heal_minhealth) \
38         MON_ADD_CVAR(monster, heal_range) \
39         MON_ADD_CVAR(monster, heal_delay) \
40         MON_ADD_CVAR(monster, shield_time) \
41         MON_ADD_CVAR(monster, shield_delay) \
42         MON_ADD_CVAR(monster, shield_blockpercent) \
43         MON_ADD_CVAR(monster, speed_stop) \
44         MON_ADD_CVAR(monster, speed_run) \
45         MON_ADD_CVAR(monster, speed_walk) 
46
47 #ifdef SVQC
48 MAGE_SETTINGS(mage)
49 #endif // SVQC
50 #else
51 #ifdef SVQC
52 const float mage_anim_idle              = 0;
53 const float mage_anim_walk              = 1;
54 const float mage_anim_attack    = 2;
55 const float mage_anim_pain              = 3;
56 const float mage_anim_death     = 4;
57 const float mage_anim_run               = 5;
58
59 void() mage_heal;
60 void() mage_shield;
61 void() mage_shield_die;
62
63 float friend_needshelp(entity e)
64 {
65         if(e == world)
66                 return FALSE;
67         if(e.health <= 0)
68                 return FALSE;
69         if(vlen(e.origin - self.origin) > MON_CVAR(mage, heal_range))
70                 return FALSE;
71         if(IsDifferentTeam(e, self))
72                 return FALSE;
73         if(e.frozen)
74                 return FALSE;
75         if(!IS_PLAYER(e))
76                 return (e.health < e.max_health);
77         if(e.items & IT_INVINCIBLE)
78                 return FALSE;
79
80         switch(self.skin)
81         {
82                 case 0:
83                 {
84                         if(e.health < autocvar_g_balance_health_regenstable)
85                                 return TRUE;
86                         break;
87                 }
88                 case 1:
89                 {
90                         if((e.ammo_cells && e.ammo_cells < g_pickup_cells_max) || (e.ammo_rockets && e.ammo_rockets < g_pickup_rockets_max) || (e.ammo_nails && e.ammo_nails < g_pickup_nails_max) || (e.ammo_shells && e.ammo_shells < g_pickup_shells_max))
91                                 return TRUE;
92                         break;
93                 }
94                 case 2:
95                 {
96                         if(e.armorvalue < autocvar_g_balance_armor_regenstable)
97                                 return TRUE;
98                         break;
99                 }
100                 case 3:
101                 {
102                         if(e.health > 0)
103                                 return TRUE;
104                         break;
105                 }
106         }
107         
108         return FALSE;
109 }
110
111 void mageattack_melee()
112 {
113         monster_melee(self.enemy, MON_CVAR(mage, attack_melee_damage), 0.3, DEATH_MONSTER_MAGE, TRUE);
114 }
115
116 void mage_grenade_explode()
117 {
118         pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1);
119         
120         sound(self, CH_SHOTS, "weapons/grenade_impact.wav", VOL_BASE, ATTN_NORM);
121         RadiusDamage (self, self.realowner, MON_CVAR(mage, attack_grenade_damage), MON_CVAR(mage, attack_grenade_edgedamage), MON_CVAR(mage, attack_grenade_radius), world, MON_CVAR(mage, attack_grenade_force), DEATH_MONSTER_MAGE, other);
122         remove(self);
123 }
124
125 void mage_grenade_touch()
126 {
127         if(IS_PLAYER(other))
128         {
129                 PROJECTILE_TOUCH;
130                 mage_grenade_explode();
131                 return;
132         }
133 }
134
135 void mage_throw_itemgrenade()
136 {
137         makevectors(self.angles);
138         
139         entity gren = spawn ();
140         gren.owner = gren.realowner = self;
141         gren.classname = "grenade";
142         gren.bot_dodge = FALSE;
143         gren.movetype = MOVETYPE_BOUNCE;
144         gren.solid = SOLID_TRIGGER;
145         gren.projectiledeathtype = DEATH_MONSTER_MAGE;
146         setorigin(gren, CENTER_OR_VIEWOFS(self));
147         setsize(gren, '-64 -64 -64', '64 64 64');
148
149         gren.nextthink = time + MON_CVAR(mage, attack_grenade_lifetime);
150         gren.think = mage_grenade_explode;
151         gren.use = mage_grenade_explode;
152         gren.touch = mage_grenade_touch;
153
154         gren.missile_flags = MIF_SPLASH | MIF_ARC;
155         W_SetupProjectileVelocityEx(gren, v_forward, v_up, MON_CVAR(mage, attack_grenade_speed), MON_CVAR(mage, attack_grenade_speed_up), 0, 0, FALSE);
156         
157         gren.flags = FL_PROJECTILE;
158         
159         setmodel(gren, "models/items/g_h50.md3");
160         
161         self.attack_finished_single = time + 1.5;
162 }
163
164 void mage_spike_explode()
165 {
166         self.event_damage = func_null;
167         
168         sound(self, CH_SHOTS, "weapons/grenade_impact.wav", VOL_BASE, ATTN_NORM);
169         
170         pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1);
171         RadiusDamage (self, self.realowner, MON_CVAR(mage, attack_spike_damage), MON_CVAR(mage, attack_spike_damage) * 0.5, MON_CVAR(mage, attack_spike_radius), world, 0, DEATH_MONSTER_MAGE, other);
172
173         remove (self);
174 }
175
176 void mage_spike_touch()
177 {
178         PROJECTILE_TOUCH;
179
180         mage_spike_explode();
181 }
182
183 // copied from W_Seeker_Think
184 void mage_spike_think()
185 {
186         entity e;
187         vector desireddir, olddir, newdir, eorg;
188         float turnrate;
189         float dist;
190         float spd;
191
192         if (time > self.ltime || self.enemy.health <= 0 || self.owner.health <= 0)
193         {
194                 self.projectiledeathtype |= HITTYPE_SPLASH;
195                 mage_spike_explode();
196         }
197
198         spd = vlen(self.velocity);
199         spd = bound(
200                 spd - MON_CVAR(mage, attack_spike_decel) * frametime,
201                 MON_CVAR(mage, attack_spike_speed_max),
202                 spd + MON_CVAR(mage, attack_spike_accel) * frametime
203         );
204
205         if (self.enemy != world)
206                 if (self.enemy.takedamage != DAMAGE_AIM || self.enemy.deadflag != DEAD_NO)
207                         self.enemy = world;
208
209         if (self.enemy != world)
210         {
211                 e               = self.enemy;
212                 eorg            = 0.5 * (e.absmin + e.absmax);
213                 turnrate        = MON_CVAR(mage, attack_spike_turnrate); // how fast to turn
214                 desireddir      = normalize(eorg - self.origin);
215                 olddir          = normalize(self.velocity); // get my current direction
216                 dist            = vlen(eorg - self.origin);
217
218                 // Do evasive maneuvers for world objects? ( this should be a cpu hog. :P )
219                 if (MON_CVAR(mage, attack_spike_smart) && (dist > MON_CVAR(mage, attack_spike_smart_mindist)))
220                 {
221                         // Is it a better idea (shorter distance) to trace to the target itself?
222                         if ( vlen(self.origin + olddir * self.wait) < dist)
223                                 traceline(self.origin, self.origin + olddir * self.wait, FALSE, self);
224                         else
225                                 traceline(self.origin, eorg, FALSE, self);
226
227                         // Setup adaptive tracelength
228                         self.wait = bound(MON_CVAR(mage, attack_spike_smart_trace_min), vlen(self.origin - trace_endpos), self.wait = MON_CVAR(mage, attack_spike_smart_trace_max));
229
230                         // Calc how important it is that we turn and add this to the desierd (enemy) dir.
231                         desireddir  = normalize(((trace_plane_normal * (1 - trace_fraction)) + (desireddir * trace_fraction)) * 0.5);
232                 }
233                 
234                 newdir = normalize(olddir + desireddir * turnrate); // take the average of the 2 directions; not the best method but simple & easy
235                 self.velocity = newdir * spd; // make me fly in the new direction at my flight speed
236         }
237         else
238                 dist = 0;
239                 
240         ///////////////
241
242         //self.angles = vectoangles(self.velocity);                     // turn model in the new flight direction
243         self.nextthink = time;// + 0.05; // csqc projectiles
244         UpdateCSQCProjectile(self);
245 }
246
247 void mage_spike()
248 {
249         entity missile;
250         vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
251
252         makevectors(self.angles);
253
254         missile = spawn ();
255         missile.owner = missile.realowner = self;
256         missile.think = mage_spike_think;
257         missile.ltime = time + 7;
258         missile.nextthink = time;
259         missile.solid = SOLID_BBOX;
260         missile.movetype = MOVETYPE_FLYMISSILE;
261         missile.flags = FL_PROJECTILE;
262         setorigin(missile, self.origin + v_forward * 14 + '0 0 30' + v_right * -14);
263         setsize (missile, '0 0 0', '0 0 0');    
264         missile.velocity = dir * 400;
265         missile.avelocity = '300 300 300';
266         missile.enemy = self.enemy;
267         missile.touch = mage_spike_touch;
268         
269         CSQCProjectile(missile, TRUE, PROJECTILE_MAGE_SPIKE, TRUE);
270 }
271
272 void mage_heal()
273 {
274         entity head;
275         float washealed = FALSE;
276         
277         for(head = world; (head = findfloat(head, monster_attack, TRUE)); ) if(friend_needshelp(head))
278         {
279                 washealed = TRUE;
280                 string fx = "";
281                 if(IS_PLAYER(head))
282                 {
283                         switch(self.skin)
284                         {
285                                 case 0:
286                                         if(head.health < autocvar_g_balance_health_regenstable) head.health = bound(0, head.health + MON_CVAR(mage, heal_allies), autocvar_g_balance_health_regenstable);
287                                         fx = "healing_fx";
288                                         break;
289                                 case 1:
290                                         if(head.ammo_cells) head.ammo_cells = bound(head.ammo_cells, head.ammo_cells + 1, g_pickup_cells_max);
291                                         if(head.ammo_rockets) head.ammo_rockets = bound(head.ammo_rockets, head.ammo_rockets + 1, g_pickup_rockets_max);
292                                         if(head.ammo_shells) head.ammo_shells = bound(head.ammo_shells, head.ammo_shells + 2, g_pickup_shells_max);
293                                         if(head.ammo_nails) head.ammo_nails = bound(head.ammo_nails, head.ammo_nails + 5, g_pickup_nails_max);
294                                         fx = "ammoregen_fx";
295                                         break;
296                                 case 2:
297                                         if(head.armorvalue < autocvar_g_balance_armor_regenstable)
298                                         {
299                                                 head.armorvalue = bound(0, head.armorvalue + MON_CVAR(mage, heal_allies), autocvar_g_balance_armor_regenstable);
300                                                 fx = "armorrepair_fx";
301                                         }
302                                         break;
303                                 case 3:
304                                         head.health = bound(0, head.health - ((head == self)  ? MON_CVAR(mage, heal_self) : MON_CVAR(mage, heal_allies)), autocvar_g_balance_health_regenstable);
305                                         fx = "rage";
306                                         break;
307                         }
308                         
309                         pointparticles(particleeffectnum(fx), head.origin, '0 0 0', 1);
310                 }
311                 else
312                 {
313                         pointparticles(particleeffectnum("healing_fx"), head.origin, '0 0 0', 1);
314                         head.health = bound(0, head.health + MON_CVAR(mage, heal_allies), head.max_health);
315                         WaypointSprite_UpdateHealth(head.sprite, head.health);
316                 }
317         }
318         
319         if(washealed)
320         {
321                 monsters_setframe(mage_anim_attack);
322                 self.attack_finished_single = time + MON_CVAR(mage, heal_delay);
323         }
324 }
325
326 void mage_shield_die()
327 {
328         if not(self.weaponentity)
329                 return; // why would this be called without a shield?
330         
331         self.armorvalue = 1;
332         
333         remove(self.weaponentity);
334         
335         self.weaponentity = world;
336 }
337
338 void mage_shield()
339 {
340         if(self.weaponentity)
341                 return; // already have a shield
342                 
343         entity shield = spawn();
344
345         shield.owner = self;
346         shield.team = self.team;
347         shield.ltime = time + MON_CVAR(mage, shield_time);
348         shield.health = 70;
349         shield.classname = "shield";
350         shield.effects = EF_ADDITIVE;
351         shield.movetype = MOVETYPE_NOCLIP;
352         shield.solid = SOLID_TRIGGER;
353         shield.avelocity = '7 0 11';
354         shield.scale = self.scale * 0.6;
355         
356         setattachment(shield, self, "");
357         setmodel(shield, "models/ctf/shield.md3");
358         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
359         
360         self.weaponentity = shield;
361         
362         self.lastshielded = time + MON_CVAR(mage, shield_delay);
363         
364         monsters_setframe(mage_anim_attack);
365         self.attack_finished_single = time + 1;
366         
367         self.armorvalue = MON_CVAR(mage, shield_blockpercent) / 100;
368 }
369
370 float mage_attack(float attack_type)
371 {
372         switch(attack_type)
373         {
374                 case MONSTER_ATTACK_MELEE:
375                 {
376                         monsters_setframe(mage_anim_attack);
377                         self.attack_finished_single = time + MON_CVAR(mage, attack_melee_delay);
378                         defer(0.2, mageattack_melee);
379                         
380                         return TRUE;
381                 }
382                 case MONSTER_ATTACK_RANGED:
383                 {
384                         if(random() < MON_CVAR(mage, attack_grenade_chance) / 100)
385                         {
386                                 mage_throw_itemgrenade();
387                                 return TRUE;
388                         }
389         
390                         monsters_setframe(mage_anim_attack);
391                         self.attack_finished_single = time + MON_CVAR(mage, attack_spike_delay);
392                         defer(0.2, mage_spike);
393                         
394                         return TRUE;
395                 }
396         }
397         
398         return FALSE;
399 }
400
401 void spawnfunc_monster_mage()
402 {
403         self.classname = "monster_mage";
404         
405         self.monster_spawnfunc = spawnfunc_monster_mage;
406         
407         if(Monster_CheckAppearFlags(self))
408                 return;
409         
410         if not(monster_initialize(MON_MAGE, FALSE)) { remove(self); return; }
411 }
412
413 // compatibility with old spawns
414 void spawnfunc_monster_shalrath() { spawnfunc_monster_mage(); }
415
416 float m_mage(float req)
417 {
418         switch(req)
419         {
420                 case MR_THINK:
421                 {
422                         entity head;
423                         float need_help = FALSE;
424                         
425                         FOR_EACH_PLAYER(head)
426                         if(friend_needshelp(head))
427                         {
428                                 need_help = TRUE;
429                                 break; // found 1 player near us who is low on health
430                         }
431                         if(!need_help)
432                         FOR_EACH_MONSTER(head)
433                         if(head != self)
434                         if(friend_needshelp(head))
435                         {
436                                 need_help = TRUE;
437                                 break; // found 1 player near us who is low on health
438                         }
439                         
440                         if(self.weaponentity)
441                         if(time >= self.weaponentity.ltime)
442                                 mage_shield_die();
443                                 
444                         if(self.health < MON_CVAR(mage, heal_minhealth) || need_help)
445                         if(time >= self.attack_finished_single)
446                         if(random() < 0.5)
447                                 mage_heal();
448                                 
449                         if(self.enemy)
450                         if(self.health < self.max_health)
451                         if(time >= self.lastshielded)
452                         if(random() < 0.5)
453                                 mage_shield();
454                         
455                         monster_move(MON_CVAR(mage, speed_run), MON_CVAR(mage, speed_walk), MON_CVAR(mage, speed_stop), mage_anim_walk, mage_anim_run, mage_anim_idle);
456                         return TRUE;
457                 }
458                 case MR_DEATH:
459                 {
460                         monsters_setframe(mage_anim_death);
461                         return TRUE;
462                 }
463                 case MR_SETUP:
464                 {
465                         if not(self.health) self.health = MON_CVAR(mage, health);
466                         
467                         self.monster_loot = spawnfunc_item_health_large;
468                         self.monster_attackfunc = mage_attack;
469                         monsters_setframe(mage_anim_walk);
470                         
471                         return TRUE;
472                 }
473                 case MR_INIT:
474                 {
475                         // nothing
476                         return TRUE;
477                 }
478                 case MR_CONFIG:
479                 {
480                         MON_CONFIG_SETTINGS(MAGE_SETTINGS(mage))
481                         return TRUE;
482                 }
483         }
484         
485         return TRUE;
486 }
487
488 #endif // SVQC
489 #ifdef CSQC
490 float m_mage(float req)
491 {
492         switch(req)
493         {
494                 case MR_DEATH:
495                 {
496                         // nothing
497                         return TRUE;
498                 }
499                 case MR_INIT:
500                 {
501                         precache_model ("models/monsters/mage.dpm");
502                         return TRUE;
503                 }
504         }
505         
506         return TRUE;
507 }
508
509 #endif // CSQC
510 #endif // REGISTER_MONSTER