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