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