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