]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/monsters/monster/mage.qc
Improve mage homing missiles
[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)
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         if (self.enemy.deadflag != DEAD_NO || self.owner.health < 1)
243         {
244                 self.enemy = world;
245                 self.ltime = time + 1 + (random() * 4);
246                 self.nextthink = self.ltime;
247                 return;
248         }
249
250         //self.angles = vectoangles(self.velocity);                     // turn model in the new flight direction
251         self.nextthink = time;// + 0.05; // csqc projectiles
252         UpdateCSQCProjectile(self);
253 }
254
255 void mage_spike()
256 {
257         entity missile;
258         vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
259
260         makevectors(self.angles);
261
262         missile = spawn ();
263         missile.owner = missile.realowner = self;
264         missile.think = mage_spike_think;
265         missile.ltime = time + 7;
266         missile.nextthink = time;
267         missile.solid = SOLID_BBOX;
268         missile.movetype = MOVETYPE_FLYMISSILE;
269         missile.flags = FL_PROJECTILE;
270         setorigin(missile, self.origin + v_forward * 14 + '0 0 30' + v_right * -14);
271         setsize (missile, '0 0 0', '0 0 0');    
272         missile.velocity = dir * 400;
273         missile.avelocity = '300 300 300';
274         missile.enemy = self.enemy;
275         missile.touch = mage_spike_touch;
276         
277         CSQCProjectile(missile, TRUE, PROJECTILE_MAGE_SPIKE, TRUE);
278 }
279
280 void mage_heal()
281 {
282         entity head;
283         float washealed = FALSE;
284         
285         for(head = world; (head = findfloat(head, monster_attack, TRUE)); ) if(friend_needshelp(head))
286         {
287                 washealed = TRUE;
288                 string fx = "";
289                 if(IS_PLAYER(head))
290                 {
291                         switch(self.skin)
292                         {
293                                 case 0:
294                                         if(head.health < autocvar_g_balance_health_regenstable) head.health = bound(0, head.health + MON_CVAR(mage, heal_allies), autocvar_g_balance_health_regenstable);
295                                         fx = "healing_fx";
296                                         break;
297                                 case 1:
298                                         if(head.ammo_cells) head.ammo_cells = bound(head.ammo_cells, head.ammo_cells + 1, g_pickup_cells_max);
299                                         if(head.ammo_rockets) head.ammo_rockets = bound(head.ammo_rockets, head.ammo_rockets + 1, g_pickup_rockets_max);
300                                         if(head.ammo_shells) head.ammo_shells = bound(head.ammo_shells, head.ammo_shells + 2, g_pickup_shells_max);
301                                         if(head.ammo_nails) head.ammo_nails = bound(head.ammo_nails, head.ammo_nails + 5, g_pickup_nails_max);
302                                         fx = "ammoregen_fx";
303                                         break;
304                                 case 2:
305                                         if(head.armorvalue < autocvar_g_balance_armor_regenstable)
306                                         {
307                                                 head.armorvalue = bound(0, head.armorvalue + MON_CVAR(mage, heal_allies), autocvar_g_balance_armor_regenstable);
308                                                 fx = "armorrepair_fx";
309                                         }
310                                         break;
311                                 case 3:
312                                         head.health = bound(0, head.health - ((head == self)  ? MON_CVAR(mage, heal_self) : MON_CVAR(mage, heal_allies)), autocvar_g_balance_health_regenstable);
313                                         fx = "rage";
314                                         break;
315                         }
316                         
317                         pointparticles(particleeffectnum(fx), head.origin, '0 0 0', 1);
318                 }
319                 else
320                 {
321                         pointparticles(particleeffectnum("healing_fx"), head.origin, '0 0 0', 1);
322                         head.health = bound(0, head.health + MON_CVAR(mage, heal_allies), head.max_health);
323                         WaypointSprite_UpdateHealth(head.sprite, head.health);
324                 }
325         }
326         
327         if(washealed)
328         {
329                 monsters_setframe(mage_anim_attack);
330                 self.attack_finished_single = time + MON_CVAR(mage, heal_delay);
331         }
332 }
333
334 void mage_shield_die()
335 {
336         if not(self.weaponentity)
337                 return; // why would this be called without a shield?
338         
339         self.armorvalue = 1;
340         
341         remove(self.weaponentity);
342         
343         self.weaponentity = world;
344 }
345
346 void mage_shield()
347 {
348         if(self.weaponentity)
349                 return; // already have a shield
350                 
351         entity shield = spawn();
352
353         shield.owner = self;
354         shield.team = self.team;
355         shield.ltime = time + MON_CVAR(mage, shield_time);
356         shield.health = 70;
357         shield.classname = "shield";
358         shield.effects = EF_ADDITIVE;
359         shield.movetype = MOVETYPE_NOCLIP;
360         shield.solid = SOLID_TRIGGER;
361         shield.avelocity = '7 0 11';
362         shield.scale = self.scale * 0.6;
363         
364         setattachment(shield, self, "");
365         setmodel(shield, "models/ctf/shield.md3");
366         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
367         
368         self.weaponentity = shield;
369         
370         self.lastshielded = time + MON_CVAR(mage, shield_delay);
371         
372         monsters_setframe(mage_anim_attack);
373         self.attack_finished_single = time + 1;
374         
375         self.armorvalue = MON_CVAR(mage, shield_blockpercent) / 100;
376 }
377
378 float mage_attack(float attack_type)
379 {
380         switch(attack_type)
381         {
382                 case MONSTER_ATTACK_MELEE:
383                 {
384                         monsters_setframe(mage_anim_attack);
385                         self.attack_finished_single = time + MON_CVAR(mage, attack_melee_delay);
386                         defer(0.2, mageattack_melee);
387                         
388                         return TRUE;
389                 }
390                 case MONSTER_ATTACK_RANGED:
391                 {
392                         if(random() < MON_CVAR(mage, attack_grenade_chance) / 100)
393                         {
394                                 mage_throw_itemgrenade();
395                                 return TRUE;
396                         }
397         
398                         monsters_setframe(mage_anim_attack);
399                         self.attack_finished_single = time + MON_CVAR(mage, attack_spike_delay);
400                         defer(0.2, mage_spike);
401                         
402                         return TRUE;
403                 }
404         }
405         
406         return FALSE;
407 }
408
409 void spawnfunc_monster_mage()
410 {
411         self.classname = "monster_mage";
412         
413         self.monster_spawnfunc = spawnfunc_monster_mage;
414         
415         if(Monster_CheckAppearFlags(self))
416                 return;
417         
418         if not(monster_initialize(MON_MAGE, FALSE)) { remove(self); return; }
419 }
420
421 // compatibility with old spawns
422 void spawnfunc_monster_shalrath() { spawnfunc_monster_mage(); }
423
424 float m_mage(float req)
425 {
426         switch(req)
427         {
428                 case MR_THINK:
429                 {
430                         entity head;
431                         float need_help = FALSE;
432                         
433                         FOR_EACH_PLAYER(head)
434                         if(friend_needshelp(head))
435                         {
436                                 need_help = TRUE;
437                                 break; // found 1 player near us who is low on health
438                         }
439                         if(!need_help)
440                         FOR_EACH_MONSTER(head)
441                         if(head != self)
442                         if(friend_needshelp(head))
443                         {
444                                 need_help = TRUE;
445                                 break; // found 1 player near us who is low on health
446                         }
447                         
448                         if(self.weaponentity)
449                         if(time >= self.weaponentity.ltime)
450                                 mage_shield_die();
451                                 
452                         if(self.health < MON_CVAR(mage, heal_minhealth) || need_help)
453                         if(time >= self.attack_finished_single)
454                         if(random() < 0.5)
455                                 mage_heal();
456                                 
457                         if(self.enemy)
458                         if(self.health < self.max_health)
459                         if(time >= self.lastshielded)
460                         if(random() < 0.5)
461                                 mage_shield();
462                         
463                         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);
464                         return TRUE;
465                 }
466                 case MR_DEATH:
467                 {
468                         monsters_setframe(mage_anim_death);
469                         return TRUE;
470                 }
471                 case MR_SETUP:
472                 {
473                         if not(self.health) self.health = MON_CVAR(mage, health);
474                         
475                         self.monster_loot = spawnfunc_item_health_large;
476                         self.monster_attackfunc = mage_attack;
477                         monsters_setframe(mage_anim_walk);
478                         
479                         return TRUE;
480                 }
481                 case MR_INIT:
482                 {
483                         // nothing
484                         return TRUE;
485                 }
486                 case MR_CONFIG:
487                 {
488                         MON_CONFIG_SETTINGS(MAGE_SETTINGS(mage))
489                         return TRUE;
490                 }
491         }
492         
493         return TRUE;
494 }
495
496 #endif // SVQC
497 #ifdef CSQC
498 float m_mage(float req)
499 {
500         switch(req)
501         {
502                 case MR_DEATH:
503                 {
504                         // nothing
505                         return TRUE;
506                 }
507                 case MR_INIT:
508                 {
509                         precache_model ("models/monsters/mage.dpm");
510                         return TRUE;
511                 }
512         }
513         
514         return TRUE;
515 }
516
517 #endif // CSQC
518 #endif // REGISTER_MONSTER