]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/monsters/lib/monsters.qc
d4813cafde367351ff0f09776389519c413b53d8
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / monsters / lib / monsters.qc
1 // TODO: clean up this file?
2
3 void M_Item_Touch ()
4 {
5         if(self && IS_PLAYER(other) && other.deadflag == DEAD_NO)
6         {
7                 Item_Touch();
8                 self.think = SUB_Remove;
9                 self.nextthink = time + 0.1;
10         }
11 }
12
13 void monster_item_spawn()
14 {
15         self.monster_delayedattack();
16                 
17         self.think = monster_item_spawn;
18         self.nextthink = time + 0.1;
19         self.gravity = 1;
20         self.velocity = randomvec() * 175 + '0 0 325';
21         self.touch = M_Item_Touch;
22         
23         SUB_SetFade(self, time + 5, 1);
24 }
25
26 void Monster_DropItem (string itype, string itemsize)
27 {
28         vector org = self.origin + ((self.mins + self.maxs) * 0.5);
29         entity e = spawn();
30         
31         setorigin(e, org);
32         
33         switch(itype)
34         {
35                 case "armor":
36                 {
37                         switch(itemsize)
38                         {
39                                 case "mega": e.monster_delayedattack = spawnfunc_item_armor_large; break;
40                                 case "large": e.monster_delayedattack = spawnfunc_item_armor_big; break;
41                                 case "medium": e.monster_delayedattack = spawnfunc_item_armor_medium; break;
42                                 case "small": e.monster_delayedattack = spawnfunc_item_armor_small; break;
43                                 default: break;
44                         }
45                         break; // break here?
46                 }
47                 case "health":
48                 {
49                         switch(itemsize)
50                         {
51                                 case "mega": e.monster_delayedattack = spawnfunc_item_health_mega; break;
52                                 case "large": e.monster_delayedattack = spawnfunc_item_health_large; break;
53                                 case "medium": e.monster_delayedattack = spawnfunc_item_health_medium; break;
54                                 case "small": e.monster_delayedattack = spawnfunc_item_health_small; break;
55                                 default: break;
56                         }
57                         break; // break here?
58                 }
59                 case "ammo":
60                 {
61                         switch(itemsize)
62                         {
63                                 case "shells": e.monster_delayedattack = spawnfunc_item_shells; break;
64                                 case "cells": e.monster_delayedattack = spawnfunc_item_cells; break;
65                                 case "rockets": e.monster_delayedattack = spawnfunc_item_rockets; break;
66                                 case "bullets":
67                                 case "nails": e.monster_delayedattack = spawnfunc_item_bullets; break;
68                                 default: break;
69                         }
70                         break;
71                 }
72                 default: break;
73         }
74         
75         if(g_minstagib)
76                 e.monster_delayedattack = spawnfunc_item_minst_cells;
77                 
78         e.think = monster_item_spawn;
79         e.nextthink = time + 0.1;
80 }
81
82 void monsters_setframe(float _frame)
83 {
84         if(self.frame == _frame)
85                 return;
86                 
87         self.anim_start_time = time;
88         self.frame = _frame;
89         self.SendFlags |= MSF_ANIM;
90 }
91
92 float monster_isvalidtarget (entity targ, entity ent)
93 {
94         if(!targ || !ent)
95                 return FALSE; // this check should fix a crash
96                 
97         if(time < game_starttime)
98                 return FALSE; // monsters do nothing before the match has started
99                 
100         WarpZone_TraceLine(ent.origin, targ.origin, MOVE_NORMAL, ent);
101         
102         if(vlen(targ.origin - ent.origin) >= ent.target_range)
103                 return FALSE; // enemy is too far away
104                 
105         if not(targ.vehicle_flags & VHF_ISVEHICLE)
106         if(trace_ent != targ)
107                 return FALSE; // we can't see the enemy
108                 
109         if(targ.takedamage == DAMAGE_NO)
110                 return FALSE; // enemy can't be damaged
111                 
112         if(targ.items & IT_INVISIBILITY)
113                 return FALSE; // enemy is invisible
114         
115         if(IS_SPEC(targ) || IS_OBSERVER(targ))
116                 return FALSE; // enemy is a spectator
117         
118         if not(targ.vehicle_flags & VHF_ISVEHICLE) // vehicles dont count as alive?
119         if(targ.deadflag != DEAD_NO || ent.deadflag != DEAD_NO || targ.health <= 0 || ent.health <= 0)
120                 return FALSE; // enemy/self is dead
121         
122         if(targ.monster_owner == ent || ent.monster_owner == targ)
123                 return FALSE; // enemy owns us, or we own them
124         
125         if not(targ.vehicle_flags & VHF_ISVEHICLE)
126         if(targ.flags & FL_NOTARGET)
127                 return FALSE; // enemy can't be targetted
128         
129         if not(autocvar_g_monsters_typefrag)
130         if(targ.BUTTON_CHAT)
131                 return FALSE; // no typefragging!
132         
133         if not(IsDifferentTeam(targ, ent))
134                 return FALSE; // enemy is on our team
135         
136         return TRUE;
137 }
138
139 entity FindTarget (entity ent) 
140 {
141         if(MUTATOR_CALLHOOK(MonsterFindTarget)) { return ent.enemy; } // Handled by a mutator
142         entity e;
143         
144         for(e = world; (e = findflags(e, monster_attack, TRUE)); ) 
145         if(monster_isvalidtarget(e, ent))
146                 return e;
147
148         return world;
149 }
150
151 void MonsterTouch ()
152 {
153         if(other == world)
154                 return;
155                 
156         if(self.enemy != other)
157         if not(other.flags & FL_MONSTER)
158         if(monster_isvalidtarget(other, self))
159                 self.enemy = other;
160 }
161
162 void monster_sound(string msound, float sound_delay, float delaytoo)
163 {
164         if(delaytoo && time < self.msound_delay)
165                 return; // too early
166                 
167         if(msound == "")
168                 return; // sound doesn't exist
169
170         sound(self, CHAN_AUTO, msound, VOL_BASE, ATTN_NORM);
171
172         self.msound_delay = time + sound_delay;
173 }
174
175 void monster_precachesounds(entity e)
176 {
177         precache_sound(e.msound_idle);
178         precache_sound(e.msound_death);
179         precache_sound(e.msound_attack_melee);
180         precache_sound(e.msound_attack_ranged);
181         precache_sound(e.msound_sight);
182         precache_sound(e.msound_pain);
183 }
184
185 void monster_setupsounds(string mon)
186 {
187         if(self.msound_idle == "") self.msound_idle = strzone(strcat("monsters/", mon, "_idle.wav"));
188         if(self.msound_death == "") self.msound_death = strzone(strcat("monsters/", mon, "_death.wav"));
189         if(self.msound_pain == "") self.msound_pain = strzone(strcat("monsters/", mon, "_pain.wav"));
190         if(self.msound_attack_melee == "") self.msound_attack_melee = strzone(strcat("monsters/", mon, "_melee.wav"));
191         if(self.msound_attack_ranged == "") self.msound_attack_ranged = strzone(strcat("monsters/", mon, "_attack.wav"));
192         if(self.msound_sight == "") self.msound_sight = strzone(strcat("monsters/", mon, "_sight.wav"));
193 }
194
195 void monster_melee (entity targ, float damg, float er, float deathtype, float dostop)
196 {
197         float bigdmg, dot, rdmg = damg * random();
198
199         if (self.health <= 0)
200                 return;
201         if (targ == world)
202                 return;
203                 
204         if(dostop)
205         {
206                 self.velocity_x = 0;
207                 self.velocity_y = 0;
208                 self.state = MONSTER_STATE_ATTACK_MELEE;
209                 self.SendFlags |= MSF_MOVE;
210         }
211
212         makevectors (self.angles);
213         dot = normalize (targ.origin - self.origin) * v_forward;
214         
215         bigdmg = rdmg * self.scale;
216         
217         if(dot > er)
218                 Damage(targ, self, self, bigdmg * monster_skill, deathtype, targ.origin, normalize(targ.origin - self.origin));
219 }
220
221 void Monster_CheckDropCvars (string mon)
222 {
223         if not(self.candrop)
224                 return; // forced off
225         
226         string dropitem;
227         string dropsize;
228         
229         dropitem = cvar_string(strcat("g_monster_", mon, "_drop"));
230         dropsize = cvar_string(strcat("g_monster_", mon, "_drop_size"));
231         
232         monster_dropitem = dropitem;
233         monster_dropsize = dropsize;
234         MUTATOR_CALLHOOK(MonsterDropItem);
235         dropitem = monster_dropitem;
236         dropsize = monster_dropsize;
237         
238         if(autocvar_g_monsters_forcedrop)
239                 Monster_DropItem(autocvar_g_monsters_drop_type, autocvar_g_monsters_drop_size);
240         else if(dropitem != "")
241                 Monster_DropItem(dropitem, dropsize);      
242         else
243                 Monster_DropItem("armor", "medium");
244 }
245
246 void ScaleMonster (float scle, vector min_s, vector max_s)
247 {
248         // this should prevent monster from falling through floor when scale changes
249         self.scale = scle;
250         setsize(self, min_s * scle, max_s * scle);
251         setorigin(self, self.origin + ('0 0 30' * scle));
252         self.SendFlags |= MSF_SIZE | MSF_MOVE;
253 }
254
255 void Monster_CheckMinibossFlag ()
256 {
257         if(MUTATOR_CALLHOOK(MonsterCheckBossFlag))
258                 return;
259                 
260         if(self.spawnflags & MONSTERFLAG_GIANT)
261                 return; // 1 size modifier at a time
262                 
263         float r = random() * 4, chance = random() * 100;
264
265         // g_monsters_miniboss_chance cvar or spawnflags 64 causes a monster to be a miniboss
266         if ((self.spawnflags & MONSTERFLAG_MINIBOSS) || (chance < autocvar_g_monsters_miniboss_chance))
267         {
268                 self.health += autocvar_g_monsters_miniboss_healthboost;
269                 ScaleMonster(1.5, self.mins, self.maxs);
270                 self.flags |= MONSTERFLAG_MINIBOSS;
271                 if not(self.weapon)
272                         self.weapon = WEP_NEX;
273                 
274                 if (r < 2 || self.team == NUM_TEAM_2)
275                 {
276                         self.strength_finished = -1;  
277                         self.effects |= (EF_FULLBRIGHT | EF_BLUE);
278                 }
279                 else if (r >= 1 || self.team == NUM_TEAM_1)
280                 {
281                         self.invincible_finished = -1;
282                         self.effects |= (EF_FULLBRIGHT | EF_RED);
283                 }
284                 else
285                         self.effects |= (EF_FULLBRIGHT | EF_RED | EF_BLUE);
286                 
287                 if not(self.team)
288                         self.colormod = randomvec();
289         }
290 }
291
292 float Monster_CanRespawn(entity ent)
293 {
294         other = ent;
295         if(MUTATOR_CALLHOOK(MonsterRespawn))
296                 return TRUE; // enabled by a mutator
297                 
298         if(ent.spawnflags & MONSTERFLAG_NORESPAWN)
299                 return FALSE;
300                 
301         if not(autocvar_g_monsters_respawn)
302                 return FALSE;
303                 
304         return TRUE;
305 }
306
307 void Monster_Fade ()
308 {
309         if(Monster_CanRespawn(self))
310         {
311                 self.monster_respawned = TRUE;
312                 self.netname = ""; // needed to fix sprite (TODO: fix!)
313                 self.think = self.monster_spawnfunc;
314                 self.nextthink = time + self.respawntime;
315                 if(self.respawnflags & MONSTER_RESPAWN_DEATHPOINT)
316                 {
317                         self.pos1 = self.origin;
318                         self.pos2 = self.angles;
319                 }
320                 setorigin(self, self.pos1);
321                 self.angles = self.pos2;
322                 self.health = self.max_health; // TODO: check if resetting to max_health is wise here
323                 return;
324         }
325         self.think = SUB_Remove;
326         self.nextthink = time + 4;
327         SUB_SetFade(self, time + 3, 1);
328 }
329
330 float Monster_CanJump (vector vel)
331 {
332         if(self.state)
333                 return FALSE; // already attacking
334         if not(self.flags & FL_ONGROUND)
335                 return FALSE; // not on the ground
336         if(self.health <= 0)
337                 return FALSE; // called when dead?
338         if(time < self.attack_finished_single)
339                 return FALSE; // still attacking
340
341         vector old = self.velocity;
342         
343         self.velocity = vel;
344         tracetoss(self, self);
345         self.velocity = old;
346         if (trace_ent != self.enemy)
347                 return FALSE;
348
349         return TRUE;
350 }
351
352 float monster_leap (float anm, void() touchfunc, vector vel, float anim_finished)
353 {
354         if(!Monster_CanJump(vel))
355                 return FALSE;
356                 
357         monsters_setframe(anm);
358         self.state = MONSTER_STATE_ATTACK_LEAP;
359         self.touch = touchfunc;
360         self.origin_z += 1;
361         self.velocity = vel;
362         self.flags &~= FL_ONGROUND;
363                 
364         self.attack_finished_single = time + anim_finished;
365         
366         return TRUE;
367 }
368
369 float GenericCheckAttack ()
370 {
371         // checking attack while dead?
372         if (self.health <= 0 || self.enemy == world)
373                 return FALSE;
374                 
375         if(self.monster_delayedattack && self.delay != -1)
376         {
377                 if(time < self.delay)
378                         return FALSE;
379                         
380                 self.monster_delayedattack();
381         }
382         
383         if (time < self.attack_finished_single)
384                 return FALSE;
385                 
386         if(self.attack_melee)
387         if(vlen(self.enemy.origin - self.origin) <= 100 * self.scale)
388         {
389                 monster_sound(self.msound_attack_melee, 0, FALSE); // no delay for attack sounds
390                 self.attack_melee(); // don't wait for nextthink - too slow
391                 return TRUE;
392         }
393         
394         // monster doesn't have a ranged attack function, so stop here
395         if not(self.attack_ranged)
396                 return FALSE;
397
398         // see if any entities are in the way of the shot
399         if not(findtrajectorywithleading(self.origin, '0 0 0', '0 0 0', self.enemy, 800, 0, 2.5, 0, self))
400                 return FALSE;
401
402         if(self.attack_ranged())
403         {
404                 monster_sound(self.msound_attack_ranged, 0, FALSE); // no delay for attack sounds
405                 return TRUE;
406         }
407
408         return FALSE;
409 }
410
411 void monster_use ()
412 {
413         if (self.enemy)
414                 return;
415         if (self.health <= 0)
416                 return;
417
418         if(!monster_isvalidtarget(activator, self))
419                 return;
420
421         self.enemy = activator;
422 }
423
424 float trace_path(vector from, vector to)
425 {
426         vector dir = normalize(to - from) * 15, offset = '0 0 0';
427         float trace1 = trace_fraction;
428         
429         offset_x = dir_y;
430         offset_y = -dir_x;
431         traceline (from+offset, to+offset, TRUE, self);
432         
433         traceline(from-offset, to-offset, TRUE, self);
434                 
435         return ((trace1 < trace_fraction) ? trace1 : trace_fraction);
436 }
437
438 .float last_trace;
439 vector monster_pickmovetarget(entity targ)
440 {
441         // enemy is always preferred target
442         if(self.enemy)
443         {
444                 self.monster_movestate = MONSTER_MOVE_ENEMY;
445                 self.last_trace = time + 0.1;
446                 return self.enemy.origin;
447         }
448         if(targ)
449         {
450                 self.monster_movestate = MONSTER_MOVE_WANDER;
451                 self.last_trace = time + 0.5;
452                 return targ.origin;
453         }
454         
455         switch(self.monster_moveflags)
456         {
457                 case MONSTER_MOVE_OWNER:
458                 {
459                         self.monster_movestate = MONSTER_MOVE_OWNER;
460                         self.last_trace = time + 0.3;
461                         if(self.monster_owner && self.monster_owner.classname != "monster_swarm")
462                                 return self.monster_owner.origin;
463                 }
464                 case MONSTER_MOVE_WANDER:
465                 {
466                         self.monster_movestate = MONSTER_MOVE_WANDER;
467                         self.last_trace = time + 2;
468                                 
469                         self.angles_y = random() * 500;
470                         makevectors(self.angles);
471                         return self.origin + v_forward * 600;
472                 }
473                 case MONSTER_MOVE_SPAWNLOC:
474                 {
475                         self.monster_movestate = MONSTER_MOVE_SPAWNLOC;
476                         self.last_trace = time + 2;
477                         return self.pos1;
478                 }
479                 default:
480                 case MONSTER_MOVE_NOMOVE:
481                 {
482                         self.monster_movestate = MONSTER_MOVE_NOMOVE;
483                         self.last_trace = time + 2;
484                         return self.origin;
485                 }
486         }
487 }
488
489 void monster_move(float runspeed, float walkspeed, float stopspeed, float manim_run, float manim_walk, float manim_idle)
490 {
491         if(self.target)
492                 self.goalentity = find(world, targetname, self.target);
493                 
494         entity targ;
495
496         if(self.frozen)
497         {
498                 self.revive_progress = bound(0, self.revive_progress + frametime * self.revive_speed, 1);
499                 self.health = max(1, self.max_health * self.revive_progress);
500                 
501                 if(self.sprite) WaypointSprite_UpdateHealth(self.sprite, self.health);
502                         
503                 movelib_beak_simple(stopspeed);
504                         
505                 self.velocity = '0 0 0';
506                 self.enemy = world;
507                 self.nextthink = time + 0.1;
508                 
509                 if(self.revive_progress >= 1)
510                         Unfreeze(self); // wait for next think before attacking
511                         
512                 return; // no moving while frozen
513         }
514         
515         if(self.flags & FL_SWIM)
516         {
517                 if(self.waterlevel < WATERLEVEL_WETFEET)
518                 {
519                         if(time >= self.last_trace)
520                         {
521                                 self.last_trace = time + 0.4;
522                                 
523                                 Damage (self, world, world, 2, DEATH_DROWN, self.origin, '0 0 0');
524                                 self.angles = '90 90 0';
525                                 if(random() < 0.5)
526                                 {
527                                         self.velocity_y += random() * 50;
528                                         self.velocity_x -= random() * 50;
529                                 }
530                                 else
531                                 {
532                                         self.velocity_y -= random() * 50;
533                                         self.velocity_x += random() * 50;
534                                 }
535                                 self.velocity_z += random() * 150;
536                         }
537                                 
538                         
539                         self.movetype = MOVETYPE_BOUNCE;
540                         //self.velocity_z = -200;
541                                 
542                         self.SendFlags |= MSF_MOVE | MSF_ANG;
543                         
544                         return;
545                 }
546                 else
547                 {
548                         self.angles = '0 0 0';
549                         self.movetype = MOVETYPE_WALK;
550                 }
551         }
552         
553         if(gameover || time < game_starttime)
554         {
555                 runspeed = walkspeed = 0;
556                 monsters_setframe(manim_idle);
557                 movelib_beak_simple(stopspeed);
558                 return;
559         }
560         
561         targ = self.goalentity;
562         
563         monster_target = targ;
564         monster_speed_run = runspeed;
565         monster_speed_walk = walkspeed;
566         MUTATOR_CALLHOOK(MonsterMove);
567         targ = monster_target;
568         runspeed = monster_speed_run;
569         walkspeed = monster_speed_walk;
570                 
571         if(IsDifferentTeam(self.monster_owner, self))
572                 self.monster_owner = world;
573                 
574         if not(monster_isvalidtarget(self.enemy, self))
575                 self.enemy = world; // check enemy each think frame?
576                 
577         if not(self.enemy)
578         {
579                 self.enemy = FindTarget(self);
580                 if(self.enemy)
581                         monster_sound(self.msound_sight, 0, FALSE);
582         }
583         
584         if(self.state == MONSTER_STATE_ATTACK_MELEE && time >= self.attack_finished_single)
585                 self.state = 0;
586                 
587         if(self.state != MONSTER_STATE_ATTACK_MELEE) // don't move if set
588         if(time >= self.last_trace || self.enemy) // update enemy instantly
589                 self.moveto = monster_pickmovetarget(targ);
590
591         if not(self.enemy)
592                 monster_sound(self.msound_idle, 5, TRUE);
593         
594         vector angles_face = vectoangles(self.moveto - self.origin);
595         vector owner_face = vectoangles(self.monster_owner.origin - self.origin);
596         vector enemy_face = vectoangles(self.enemy.origin - self.origin);
597         
598         if(self.state != MONSTER_STATE_ATTACK_LEAP)
599         {
600                 self.angles_y = angles_face_y;
601                 self.v_angle = self.angles;
602         }
603         
604         if(self.state == MONSTER_STATE_ATTACK_LEAP && (self.flags & FL_ONGROUND))
605         {
606                 self.state = 0;
607                 self.touch = MonsterTouch;
608         }
609          
610         v_forward = normalize(self.moveto - self.origin);
611         
612         float l = vlen(self.moveto - self.origin);
613         float t1 = trace_path(self.origin+'0 0 10', self.moveto+'0 0 10');
614         float t2 = trace_path(self.origin-'0 0 15', self.moveto-'0 0 15'); 
615         
616         if(t1*l-t2*l>50 && (t1*l > 100 || t1 > 0.8))
617         if(self.flags & FL_ONGROUND)
618                 movelib_jump_simple(100);
619
620         if(vlen(self.origin - self.moveto) > 64 * self.scale)
621         {
622                 if(self.flags & FL_FLY || self.flags & FL_SWIM)
623                         movelib_move_simple(v_forward, ((self.enemy) ? runspeed : walkspeed), 0.6);
624                 else
625                         movelib_move_simple_gravity(v_forward, ((self.enemy) ? runspeed : walkspeed), 0.6);
626                 if(time > self.pain_finished)
627                 if(time > self.attack_finished_single)
628                         monsters_setframe((self.enemy) ? manim_run : manim_walk);
629         }
630         else
631         {
632                 movelib_beak_simple(stopspeed);
633                 if(time > self.attack_finished_single)
634                 if(time > self.pain_finished)
635                 if (vlen(self.velocity) <= 30)
636                 {
637                         monsters_setframe(manim_idle);
638                         if(self.enemy)
639                                 self.angles_y = enemy_face_y;
640                         else
641                                 self.angles_y = ((self.monster_owner) ? owner_face_y : self.pos2_y); // reset looking angle now?
642                 }
643         }
644                 
645         if(self.enemy && self.checkattack)
646                 self.checkattack();
647                 
648         self.SendFlags |= MSF_ANG;
649         self.SendFlags |= MSF_MOVE;
650 }
651
652 void monster_dead_think()
653 {
654         self.think = monster_dead_think;
655         self.nextthink = time + 0.3; // don't need to update so often now
656
657         if(time >= self.ltime)
658         {
659                 Monster_Fade();
660                 return;
661         }
662         
663         self.SendFlags |= MSF_MOVE; // keep up to date on the monster's location
664 }
665
666 void monsters_setstatus()
667 {
668         self.stat_monsters_total = monsters_total;
669         self.stat_monsters_killed = monsters_killed;
670 }
671
672 void Monster_Appear()
673 {
674         self.enemy = activator;
675         self.spawnflags &~= MONSTERFLAG_APPEAR;
676         self.monster_spawnfunc();
677 }
678
679 float Monster_CheckAppearFlags(entity ent)
680 {
681         if not(ent.spawnflags & MONSTERFLAG_APPEAR)
682                 return FALSE;
683         
684         ent.think = func_null;
685         ent.nextthink = -1;
686         ent.use = Monster_Appear;
687         ent.flags = FL_MONSTER; // set so this monster can get butchered
688         
689         return TRUE;
690 }
691
692 void monsters_reset()
693 {
694         setorigin(self, self.pos1);
695         self.angles = self.pos2;
696         
697         self.health = self.max_health;
698         self.velocity = '0 0 0';
699         self.enemy = world;
700         self.goalentity = world;
701         self.attack_finished_single = 0;
702         self.moveto = self.origin;
703         
704         WaypointSprite_UpdateHealth(self.sprite, self.health);
705 }
706
707 float monster_send(entity to, float sf)
708 {
709         WriteByte(MSG_ENTITY, ENT_CLIENT_MONSTER);    
710         WriteByte(MSG_ENTITY, sf);
711         if(sf & MSF_SETUP)
712         {
713             WriteByte(MSG_ENTITY, self.monsterid);
714             
715             WriteCoord(MSG_ENTITY, self.origin_x);
716             WriteCoord(MSG_ENTITY, self.origin_y);
717             WriteCoord(MSG_ENTITY, self.origin_z);
718             
719             WriteAngle(MSG_ENTITY, self.angles_x);
720             WriteAngle(MSG_ENTITY, self.angles_y);
721                 
722                 WriteByte(MSG_ENTITY, self.scale);
723                 WriteByte(MSG_ENTITY, self.skin);
724     }
725         
726         if(sf & MSF_SIZE)
727         {
728                 WriteByte(MSG_ENTITY, self.scale);
729                 
730                 WriteCoord(MSG_ENTITY, self.mins_x);
731             WriteCoord(MSG_ENTITY, self.mins_y);
732             WriteCoord(MSG_ENTITY, self.mins_z);
733                 
734                 WriteCoord(MSG_ENTITY, self.maxs_x);
735             WriteCoord(MSG_ENTITY, self.maxs_y);
736             WriteCoord(MSG_ENTITY, self.maxs_z);
737         }
738     
739     if(sf & MSF_ANG)
740     {
741         WriteShort(MSG_ENTITY, rint(self.angles_x));
742         WriteShort(MSG_ENTITY, rint(self.angles_y));
743     }
744     
745     if(sf & MSF_MOVE)
746     {
747         WriteShort(MSG_ENTITY, rint(self.origin_x));
748         WriteShort(MSG_ENTITY, rint(self.origin_y));
749         WriteShort(MSG_ENTITY, rint(self.origin_z));
750
751         WriteShort(MSG_ENTITY, rint(self.velocity_x));
752         WriteShort(MSG_ENTITY, rint(self.velocity_y));
753         WriteShort(MSG_ENTITY, rint(self.velocity_z));        
754         
755         WriteShort(MSG_ENTITY, rint(self.angles_y));        
756     }
757     
758     if(sf & MSF_ANIM)
759     {
760         WriteCoord(MSG_ENTITY, self.anim_start_time);
761         WriteByte(MSG_ENTITY, self.frame);
762     }
763     
764     if(sf & MSF_STATUS)
765     {
766                 WriteByte(MSG_ENTITY, self.skin);
767                 
768         WriteByte(MSG_ENTITY, self.team);
769         
770         if(self.health <= 0)
771             WriteByte(MSG_ENTITY, 0);
772         else
773             WriteByte(MSG_ENTITY, ceil((self.health / self.max_health) * 255));
774     }
775     
776         return TRUE;
777 }
778
779 void monster_link(void() spawnproc)
780 {
781     Net_LinkEntity(self, TRUE, 0, monster_send);
782     self.think      = spawnproc;
783     self.nextthink  = time;
784 }
785
786 void monsters_corpse_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
787 {
788         self.health -= damage;
789                 
790         Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker);
791                 
792         if(self.health <= -100) // 100 health until gone?
793         {
794                 Violence_GibSplash(self, 1, 0.5, attacker);
795                 
796                 self.think = SUB_Remove;
797                 self.nextthink = time + 0.1;
798         }
799 }
800
801 void monsters_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
802 {
803         string t1, t2;
804
805         if(self.frozen && deathtype != DEATH_KILL)
806                 return;
807                 
808         if(time < self.pain_finished && deathtype != DEATH_KILL)
809                 return;
810                 
811         if((ignore_turrets && !(attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)) || !ignore_turrets)
812         if(monster_isvalidtarget(attacker, self))
813                 self.enemy = attacker;
814                 
815         if(deathtype != DEATH_KILL)
816                 damage *= self.armorvalue;
817                 
818         self.health -= damage;
819         
820         if(self.sprite)
821                 WaypointSprite_UpdateHealth(self.sprite, self.health);
822                 
823         self.dmg_time = time;
824
825         if(sound_allowed(MSG_BROADCAST, attacker) && deathtype != DEATH_DROWN)
826                 spamsound (self, CH_PAIN, "misc/bodyimpact1.wav", VOL_BASE, ATTN_NORM);  // FIXME: PLACEHOLDER
827         
828         self.velocity += force * self.damageforcescale;
829                 
830         if(deathtype != DEATH_DROWN)
831         {
832                 Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker);
833                 if (damage > 50)
834                         Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, self, attacker);
835                 if (damage > 100)
836                         Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, self, attacker);
837         }
838                 
839         if(self.health <= 0)
840         {        
841                 if(self.sprite)
842                 {
843                         // Update one more time to avoid waypoint fading without emptying healthbar
844                         WaypointSprite_UpdateHealth(self.sprite, 0);
845                 }
846                 
847                 if(deathtype == DEATH_KILL)
848                         self.candrop = FALSE; // killed by mobkill command
849                         
850                 // TODO: fix this?
851                 activator = attacker;
852                 other = self.enemy;
853                 t1 = self.target;
854                 t2 = self.target2;
855                 self.target = self.target2;
856                 self.target2 = "";
857                 SUB_UseTargets();
858                 self.target = t1;
859                 self.target2 = t2;
860         
861                 self.monster_die();
862                 
863                 frag_attacker = attacker;
864                 frag_target = self;
865                 MUTATOR_CALLHOOK(MonsterDies);
866                 
867                 if(self.health <= -100) // check if we're already gibbed
868                 {
869                         Violence_GibSplash(self, 1, 0.5, attacker);
870                 
871                         self.think = SUB_Remove;
872                         self.nextthink = time + 0.1;
873                 }
874         }
875         
876         self.SendFlags |= MSF_STATUS;
877 }
878
879 // used to hook into monster post death functions without a mutator
880 void monster_hook_death()
881 {
882         WaypointSprite_Kill(self.sprite);
883                 
884         if(self.weaponentity)
885         {
886                 remove(self.weaponentity);
887                 self.weaponentity = world;
888         }
889                 
890         monster_sound(self.msound_death, 0, FALSE);
891                 
892         if(!(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned)
893                 monsters_killed += 1;
894                 
895         if(self.candrop && self.weapon)
896                 W_ThrowNewWeapon(self, self.weapon, 0, self.origin, randomvec() * 150 + '0 0 325');     
897                 
898         if(IS_CLIENT(self.realowner))
899                 self.realowner.monstercount -= 1;
900                 
901         self.event_damage       = monsters_corpse_damage;
902         self.solid                      = SOLID_CORPSE;
903         self.takedamage         = DAMAGE_AIM;
904         self.enemy                      = world;
905         self.movetype           = MOVETYPE_TOSS;
906         self.moveto                     = self.origin;
907         
908         if not(self.flags & FL_FLY)
909                 self.velocity = '0 0 0';
910         
911         self.SendFlags |= MSF_MOVE;
912                 
913         totalspawned -= 1;
914 }
915
916 // used to hook into monster post spawn functions without a mutator
917 void monster_hook_spawn()
918 {
919         if not(self.monster_respawned)
920                 Monster_CheckMinibossFlag();
921
922         self.max_health = self.health;
923         self.pain_finished = self.nextthink;
924         self.anim_start_time = time;
925
926         monster_precachesounds(self);
927         
928         if(self.team)
929         {
930                 //self.colormod = Team_ColorRGB(self.team);
931                 //self.glowmod = self.colormod;
932                 self.colormap = 1024 + (self.team - 1) * 17;
933                 self.colormod = '1 1 1';
934                 self.glowmod = Team_ColorRGB(self.team);
935                 self.monster_attack = TRUE;
936         }
937         else
938                 self.colormod = self.glowmod = '1 1 1';
939         
940         // TODO: fix this...
941         if (self.target)
942         {
943                 self.target2 = self.target;
944                 self.goalentity = find(world, targetname, self.target);
945         }
946         
947         self.sprite_height *= self.scale; // do this after setting the monster's scale
948                 
949         if(autocvar_g_monsters_healthbars)
950         {
951                 WaypointSprite_Spawn(self.message, 0, 600, self, '0 0 1' * self.sprite_height, world, 0, self, sprite, FALSE, RADARICON_DANGER, ((self.team) ? Team_ColorRGB(self.team) : '1 0 0'));    
952                 WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
953                 WaypointSprite_UpdateHealth(self.sprite, self.health);
954         }
955         
956         self.message = self.oldmessage;
957         
958         monster_sound(self.msound_spawn, 0, FALSE);
959
960         MUTATOR_CALLHOOK(MonsterSpawn);
961         
962         self.SendFlags = MSF_SETUP;
963 }
964
965 float monster_initialize(string  net_name, float mon_id,
966                                                  vector  min_s,
967                                                  vector  max_s,
968                                                  float   nodrop,
969                                                  void() dieproc,
970                                                  void() spawnproc)
971 {
972         if not(autocvar_g_monsters)
973                 return FALSE;
974                 
975         // support for quake style removing monsters based on skill
976         if(monster_skill <= autocvar_g_monsters_skill_easy && (self.spawnflags & MONSTERSKILL_NOTEASY)) { return FALSE; }
977         if(monster_skill == autocvar_g_monsters_skill_normal && (self.spawnflags & MONSTERSKILL_NOTMEDIUM)) { return FALSE; }
978         if(monster_skill == autocvar_g_monsters_skill_hard && (self.spawnflags & MONSTERSKILL_NOTHARD)) { return FALSE; }
979         if(monster_skill == autocvar_g_monsters_skill_insane && (self.spawnflags & MONSTERSKILL_NOTINSANE)) { return FALSE; }
980         if(monster_skill >= autocvar_g_monsters_skill_nightmare && (self.spawnflags & MONSTERSKILL_NOTNIGHTMARE)) { return FALSE; }
981
982         if(self.netname == "")
983         {
984                 if(net_name != "" && IS_PLAYER(self.realowner))
985                         net_name = strzone(sprintf("%s^7's %s", self.realowner.netname, net_name));
986                 self.netname = ((net_name == "") ? self.classname : net_name);
987                 self.oldmessage = self.message;
988                 self.message = strzone(strdecolorize(self.netname));
989         }
990         
991         if(autocvar_g_monsters_giants_only)
992                 self.spawnflags |= MONSTERFLAG_GIANT;
993         
994         if not(self.scale)
995                 self.scale = 1;
996                 
997         if(self.spawnflags & MONSTERFLAG_GIANT && !autocvar_g_monsters_nogiants)
998                 self.scale = 5;
999         
1000         if(self.team && !teamplay)
1001                 self.team = 0;
1002
1003         self.flags = FL_MONSTER;
1004                 
1005         if not(self.spawnflags & MONSTERFLAG_SPAWNED) // naturally spawned monster
1006         if not(self.monster_respawned)
1007                 monsters_total += 1;
1008
1009         ScaleMonster(self.scale, min_s, max_s);
1010
1011         self.takedamage                 = DAMAGE_AIM;
1012         self.bot_attack                 = TRUE;
1013         self.iscreature                 = TRUE;
1014         self.teleportable               = TRUE;
1015         self.damagedbycontents  = TRUE;
1016         self.monsterid                  = mon_id;
1017         self.damageforcescale   = 0.003;
1018         self.monster_die                = dieproc;
1019         self.event_damage               = monsters_damage;
1020         self.touch                              = MonsterTouch;
1021         self.use                                = monster_use;
1022         self.solid                              = SOLID_BBOX;
1023         self.movetype                   = MOVETYPE_WALK;
1024         self.delay                              = -1; // used in attack delay code
1025         monsters_spawned           += 1;
1026         self.enemy                              = world;
1027         self.velocity                   = '0 0 0';
1028         self.moveto                             = self.origin;
1029         self.pos2                               = self.angles;
1030         self.reset                              = monsters_reset;
1031         self.candrop                    = TRUE;
1032         self.view_ofs                   = '0 0 1' * (self.maxs_z * 0.5);
1033         
1034         if not(self.ticrate)
1035                 self.ticrate = autocvar_g_monsters_think_delay;
1036                 
1037         self.ticrate = bound(sys_frametime, self.ticrate, 60);
1038         
1039         if not(self.armorvalue)
1040                 self.armorvalue = 1; // multiplier
1041         
1042         if not(self.target_range)
1043                 self.target_range = autocvar_g_monsters_target_range;
1044         
1045         if not(self.respawntime)
1046                 self.respawntime = autocvar_g_monsters_respawn_delay;
1047         
1048         if not(self.monster_moveflags)
1049                 self.monster_moveflags = MONSTER_MOVE_WANDER;
1050
1051         if(autocvar_g_nodepthtestplayers)
1052                 self.effects |= EF_NODEPTHTEST;
1053
1054         if(autocvar_g_fullbrightplayers)
1055                 self.effects |= EF_FULLBRIGHT;
1056
1057         if not(nodrop)
1058         {
1059                 setorigin(self, self.origin);
1060                 tracebox(self.origin + '0 0 100', self.mins, self.maxs, self.origin - '0 0 10000', MOVE_WORLDONLY, self);
1061                 setorigin(self, trace_endpos);
1062         }
1063         
1064         self.pos1 = self.origin;
1065         
1066         monster_link(spawnproc);
1067
1068         return TRUE;
1069 }