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