]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/monsters/monster/mage.qc
Add new healing abilities for mage & lower self healing amount
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / monsters / monster / mage.qc
1 const vector MAGE_MIN = '-36 -36 -24';
2 const vector MAGE_MAX = '36 36 50';
3
4 string MAGE_MODEL = "models/monsters/mage.dpm";
5
6 #ifdef SVQC
7 float autocvar_g_monster_mage;
8 float autocvar_g_monster_mage_health;
9 float autocvar_g_monster_mage_speed;
10 float autocvar_g_monster_mage_attack_spike_damage;
11 float autocvar_g_monster_mage_attack_spike_radius;
12 float autocvar_g_monster_mage_attack_spike_delay;
13 float autocvar_g_monster_mage_attack_melee_damage;
14 float autocvar_g_monster_mage_attack_melee_delay;
15 float autocvar_g_monster_mage_heal_self;
16 float autocvar_g_monster_mage_heal_friends;
17 float autocvar_g_monster_mage_heal_minhealth;
18 float autocvar_g_monster_mage_heal_range;
19 float autocvar_g_monster_mage_heal_delay;
20 float autocvar_g_monster_mage_shield_time;
21 float autocvar_g_monster_mage_shield_delay;
22 float autocvar_g_monster_mage_shield_blockpercent;
23 float autocvar_g_monster_mage_attack_grenade_damage;
24 float autocvar_g_monster_mage_attack_grenade_edgedamage;
25 float autocvar_g_monster_mage_attack_grenade_radius;
26 float autocvar_g_monster_mage_attack_grenade_lifetime;
27 float autocvar_g_monster_mage_attack_grenade_force;
28 float autocvar_g_monster_mage_attack_grenade_chance;
29
30 const float mage_anim_idle              = 0;
31 const float mage_anim_walk              = 1;
32 const float mage_anim_attack    = 2;
33 const float mage_anim_pain              = 3;
34 const float mage_anim_death     = 4;
35 const float mage_anim_run               = 5;
36
37 void() mage_heal;
38 void() mage_shield;
39 void() mage_shield_die;
40
41 float friend_needshelp(entity e)
42 {
43         if(e == world)
44                 return FALSE;
45         if(e.health <= 0)
46                 return FALSE;
47         if(vlen(e.origin - self.origin) > autocvar_g_monster_mage_heal_range)
48                 return FALSE;
49         if(IsDifferentTeam(e, self))
50                 return FALSE;
51         if(e.frozen)
52                 return FALSE;
53         if(!IS_PLAYER(e))
54                 return (e.health < e.max_health);
55
56         switch(self.skin)
57         {
58                 case 0:
59                 {
60                         if(e.health < autocvar_g_balance_health_regenstable)
61                                 return TRUE;
62                         break;
63                 }
64                 case 1:
65                 {
66                         if(e.ammo_cells < g_pickup_cells_max || e.ammo_rockets < g_pickup_rockets_max || e.ammo_nails < g_pickup_nails_max || e.ammo_shells < g_pickup_shells_max)
67                                 return TRUE;
68                         break;
69                 }
70                 case 2:
71                 {
72                         if(e.armorvalue < autocvar_g_balance_armor_regenstable)
73                                 return TRUE;
74                         break;
75                 }
76                 case 3:
77                 {
78                         if(e.health > 0)
79                                 return TRUE;
80                         break;
81                 }
82         }
83         
84         return FALSE;
85 }
86
87 void mage_think()
88 {
89         entity head;
90         float need_help = FALSE;
91         
92         FOR_EACH_PLAYER(head)
93         if(friend_needshelp(head))
94         {
95                 need_help = TRUE;
96                 break; // found 1 player near us who is low on health
97         }
98         if(!need_help)
99         FOR_EACH_MONSTER(head)
100         if(head != self)
101         if(friend_needshelp(head))
102         {
103                 need_help = TRUE;
104                 break; // found 1 player near us who is low on health
105         }
106         
107         self.think = mage_think;
108         self.nextthink = time + self.ticrate;
109         
110         if(self.weaponentity)
111         if(time >= self.weaponentity.ltime)
112                 mage_shield_die();
113                 
114         if(self.health < autocvar_g_monster_mage_heal_minhealth || need_help)
115         if(time >= self.attack_finished_single)
116         if(random() < 0.5)
117                 mage_heal();
118                 
119         if(self.enemy)
120         if(self.health < self.max_health)
121         if(time >= self.lastshielded)
122         if(random() < 0.5)
123                 mage_shield();
124         
125         monster_move(autocvar_g_monster_mage_speed, autocvar_g_monster_mage_speed, 50, mage_anim_walk, mage_anim_run, mage_anim_idle);
126 }
127
128 void mageattack_melee()
129 {
130         monster_melee(self.enemy, autocvar_g_monster_mage_attack_melee_damage, 0.3, DEATH_MONSTER_MAGE, TRUE);
131         
132         self.delay = -1;
133         self.monster_delayedattack = func_null;
134 }
135
136 void mage_grenade_explode()
137 {
138         pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1);
139         
140         sound(self, CH_SHOTS, "weapons/grenade_impact.wav", VOL_BASE, ATTN_NORM);
141         RadiusDamage (self, self.realowner, autocvar_g_monster_mage_attack_grenade_damage, autocvar_g_monster_mage_attack_grenade_edgedamage, autocvar_g_monster_mage_attack_grenade_radius, world, autocvar_g_monster_mage_attack_grenade_force, DEATH_MONSTER_MAGE, other);
142         remove(self);
143 }
144
145 void mage_grenade_touch()
146 {
147         if(IS_PLAYER(other))
148         {
149                 PROJECTILE_TOUCH;
150                 mage_grenade_explode();
151                 return;
152         }
153 }
154
155 void mage_throw_itemgrenade()
156 {
157         makevectors(self.angles);
158
159         W_SetupShot_ProjectileSize (self, '-64 -64 -64', '64 64 64', FALSE, 4, "", CH_WEAPON_A, autocvar_g_monster_mage_attack_grenade_damage);
160         w_shotdir = v_forward; // no TrueAim for grenades please
161
162         entity gren = spawn ();
163         gren.owner = gren.realowner = self;
164         gren.classname = "grenade";
165         gren.bot_dodge = FALSE;
166         gren.movetype = MOVETYPE_BOUNCE;
167         gren.solid = SOLID_TRIGGER;
168         gren.projectiledeathtype = DEATH_MONSTER_MAGE;
169         setorigin(gren, w_shotorg);
170         setsize(gren, '-64 -64 -64', '64 64 64');
171
172         gren.nextthink = time + autocvar_g_monster_mage_attack_grenade_lifetime;
173         gren.think = mage_grenade_explode;
174         gren.use = mage_grenade_explode;
175         gren.touch = mage_grenade_touch;
176
177         gren.missile_flags = MIF_SPLASH | MIF_ARC;
178         W_SETUPPROJECTILEVELOCITY_UP(gren, g_monster_mage_attack_grenade);
179         
180         gren.flags = FL_PROJECTILE;
181         
182         setmodel(gren, "models/items/g_h50.md3");
183         
184         self.attack_finished_single = time + 1.5;
185 }
186
187 void mage_spike_explode()
188 {
189         self.event_damage = func_null;
190
191         pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1);
192         RadiusDamage (self, self.realowner, autocvar_g_monster_mage_attack_spike_damage, autocvar_g_monster_mage_attack_spike_damage * 0.5, autocvar_g_monster_mage_attack_spike_radius, world, 0, DEATH_MONSTER_MAGE, other);
193
194         remove (self);
195 }
196
197 void mage_spike_touch()
198 {
199         PROJECTILE_TOUCH;
200
201         mage_spike_explode();
202 }
203
204 void mage_spike_think()
205 {
206         if(self.enemy.health <= 0 || self.owner.health <= 0 || time >= self.ltime)
207         {
208                 mage_spike_explode();
209                 return;
210         }
211         
212         vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
213         
214         UpdateCSQCProjectile(self);
215         
216         if (monster_skill == 3)
217                 self.velocity = dir * 350;
218         else
219                 self.velocity = dir * 250;
220                 
221         self.nextthink = time + 0.2;
222         self.think = mage_spike_think;  
223 }
224
225 void mage_spike()
226 {
227         entity missile;
228         vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
229         
230         self.delay = -1;
231         self.monster_delayedattack = func_null;
232
233         makevectors(self.angles);
234
235         missile = spawn ();
236         missile.owner = missile.realowner = self;
237         missile.think = mage_spike_think;
238         missile.ltime = time + 7;
239         missile.nextthink = time;
240         missile.solid = SOLID_BBOX;
241         missile.movetype = MOVETYPE_FLYMISSILE;
242         missile.flags = FL_PROJECTILE;
243         setorigin(missile, self.origin + v_forward * 14 + '0 0 30' + v_right * -14);
244         setsize (missile, '0 0 0', '0 0 0');    
245         missile.velocity = dir * 400;
246         missile.avelocity = '300 300 300';
247         missile.enemy = self.enemy;
248         missile.touch = mage_spike_touch;
249         
250         CSQCProjectile(missile, TRUE, PROJECTILE_MAGE_SPIKE, TRUE);
251 }
252
253 void mage_heal()
254 {
255         entity head;
256         float washealed = FALSE;
257         
258         for(head = world; (head = findfloat(head, monster_attack, TRUE)); ) if(friend_needshelp(head))
259         {
260                 washealed = TRUE;
261                 string fx = "";
262                 if(IS_PLAYER(head))
263                 {
264                         switch(self.skin)
265                         {
266                                 case 0:
267                                         if(head.health < autocvar_g_balance_health_regenstable) head.health = bound(0, head.health + autocvar_g_monster_mage_heal_friends, autocvar_g_balance_health_regenstable);
268                                         fx = "healing_fx";
269                                         break;
270                                 case 1:
271                                         if(head.ammo_cells) head.ammo_cells = bound(head.ammo_cells, head.ammo_cells + 1, g_pickup_cells_max);
272                                         if(head.ammo_rockets) head.ammo_rockets = bound(head.ammo_rockets, head.ammo_rockets + 1, g_pickup_rockets_max);
273                                         if(head.ammo_shells) head.ammo_shells = bound(head.ammo_shells, head.ammo_shells + 2, g_pickup_shells_max);
274                                         if(head.ammo_nails) head.ammo_nails = bound(head.ammo_nails, head.ammo_nails + 5, g_pickup_nails_max);
275                                         fx = "ammoregen_fx";
276                                         break;
277                                 case 2:
278                                         if(head.armorvalue < autocvar_g_balance_armor_regenstable)
279                                         {
280                                                 head.armorvalue = bound(0, head.armorvalue + autocvar_g_monster_mage_heal_friends, autocvar_g_balance_armor_regenstable);
281                                                 fx = "armorrepair_fx";
282                                         }
283                                         break;
284                                 case 3:
285                                         head.health = bound(0, head.health - ((head == self)  ? autocvar_g_monster_mage_heal_self : autocvar_g_monster_mage_heal_friends), autocvar_g_balance_health_regenstable);
286                                         fx = "rage";
287                                         break;
288                         }
289                         
290                         pointparticles(particleeffectnum(fx), head.origin, '0 0 0', 1);
291                 }
292                 else
293                 {
294                         pointparticles(particleeffectnum("healing_fx"), head.origin, '0 0 0', 1);
295                         head.health = bound(0, head.health + autocvar_g_monster_mage_heal_friends, head.max_health);
296                         WaypointSprite_UpdateHealth(head.sprite, head.health);
297                 }
298         }
299         
300         if(washealed)
301         {
302                 monsters_setframe(mage_anim_attack);
303                 self.attack_finished_single = time + autocvar_g_monster_mage_heal_delay;
304         }
305 }
306
307 void mage_shield_die()
308 {
309         if not(self.weaponentity)
310                 return; // why would this be called without a shield?
311         
312         self.armorvalue = 1;
313         
314         remove(self.weaponentity);
315         
316         self.weaponentity = world;
317 }
318
319 void mage_shield()
320 {
321         if(self.weaponentity)
322                 return; // already have a shield
323                 
324         entity shield = spawn();
325
326         shield.owner = self;
327         shield.team = self.team;
328         shield.ltime = time + autocvar_g_monster_mage_shield_time;
329         shield.health = 70;
330         shield.classname = "shield";
331         shield.effects = EF_ADDITIVE;
332         shield.movetype = MOVETYPE_NOCLIP;
333         shield.solid = SOLID_TRIGGER;
334         shield.avelocity = '7 0 11';
335         shield.scale = self.scale * 0.6;
336         
337         setattachment(shield, self, "");
338         setmodel(shield, "models/ctf/shield.md3");
339         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
340         
341         self.weaponentity = shield;
342         
343         self.lastshielded = time + autocvar_g_monster_mage_shield_delay;
344         
345         monsters_setframe(mage_anim_attack);
346         self.attack_finished_single = time + 1;
347         
348         self.armorvalue = autocvar_g_monster_mage_shield_blockpercent / 100;
349 }
350
351 float mage_attack(float attack_type)
352 {
353         switch(attack_type)
354         {
355                 case MONSTER_ATTACK_MELEE:
356                 {
357                         self.monster_delayedattack = mageattack_melee;
358                         self.delay = time + 0.2;
359                         monsters_setframe(mage_anim_attack);
360                         self.attack_finished_single = time + autocvar_g_monster_mage_attack_melee_delay;
361                         
362                         return TRUE;
363                 }
364                 case MONSTER_ATTACK_RANGED:
365                 {
366                         if(random() < autocvar_g_monster_mage_attack_grenade_chance / 100)
367                         {
368                                 mage_throw_itemgrenade();
369                                 return TRUE;
370                         }
371         
372                         monsters_setframe(mage_anim_attack);
373                         self.delay = time + 0.2;
374                         self.attack_finished_single = time + autocvar_g_monster_mage_attack_spike_delay;
375                         self.monster_delayedattack = mage_spike;
376                         
377                         return TRUE;
378                 }
379         }
380         
381         return FALSE;
382 }
383
384 void mage_die()
385 {
386         Monster_CheckDropCvars ("mage");
387         
388         self.think = monster_dead_think;
389         self.nextthink = time + self.ticrate;
390         self.ltime = time + 5;
391         monsters_setframe(mage_anim_death);
392         
393         monster_hook_death(); // for post-death mods
394 }
395
396 void mage_spawn()
397 {
398         if not(self.health)
399                 self.health = autocvar_g_monster_mage_health;
400
401         self.damageforcescale   = 0.003;
402         self.classname                  = "monster_mage";
403         self.monster_attackfunc = mage_attack;
404         self.nextthink                  = time + random() * 0.5 + 0.1;
405         self.think                              = mage_think;
406         
407         monsters_setframe(mage_anim_walk);
408         
409         monster_setupsounds("mage");
410         
411         monster_hook_spawn(); // for post-spawn mods
412 }
413
414 void spawnfunc_monster_mage()
415 {
416         if not(autocvar_g_monster_mage) { remove(self); return; }
417         
418         self.monster_spawnfunc = spawnfunc_monster_mage;
419         
420         if(Monster_CheckAppearFlags(self))
421                 return;
422         
423         if not (monster_initialize(
424                          "Mage", MONSTER_MAGE,
425                          MAGE_MIN, MAGE_MAX,
426                          FALSE,
427                          mage_die, mage_spawn))
428         {
429                 remove(self);
430                 return;
431         }
432 }
433
434 // compatibility with old spawns
435 void spawnfunc_monster_shalrath() { spawnfunc_monster_mage(); }
436
437 #endif // SVQC