]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/common/monsters/sv_monsters.qc
Merge branch 'master' into Mario/monsters
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / monsters / sv_monsters.qc
index 9283ae2f9a2984a7d71f61bb303111f31d79d654..e7056f6c6cf6d5986cca83c2073bd07402f26167 100644 (file)
@@ -1,6 +1,5 @@
 #include "sv_monsters.qh"
 
-#include <server/g_subs.qh>
 #include <lib/warpzone/common.qh>
 #include "../constants.qh"
 #include "../teams.qh"
@@ -18,7 +17,7 @@
 #include "../vehicles/all.qh"
 #include <server/campaign.qh>
 #include <server/command/_mod.qh>
-#include "../triggers/triggers.qh"
+#include "../mapobjects/triggers.qh"
 #include <lib/csqcmodel/sv_model.qh>
 #include <server/round_handler.qh>
 #include <server/weapons/_mod.qh>
@@ -56,6 +55,21 @@ void monster_dropitem(entity this, entity attacker)
        }
 }
 
+bool monster_facing(entity this, entity targ)
+{
+       // relies on target having an origin
+       makevectors(this.angles);
+       vector targ_org = targ.origin, my_org = this.origin;
+       if(autocvar_g_monsters_target_infront_2d)
+       {
+               targ_org = vec2(targ_org);
+               my_org = vec2(my_org);
+       }
+       float dot = normalize(targ_org - my_org) * v_forward;
+
+       return !(dot <= autocvar_g_monsters_target_infront_range);
+}
+
 void monster_makevectors(entity this, entity targ)
 {
        if(IS_MONSTER(this))
@@ -72,25 +86,26 @@ void monster_makevectors(entity this, entity targ)
 // Target handling
 // ===============
 
-bool Monster_ValidTarget(entity this, entity targ)
+bool Monster_ValidTarget(entity this, entity targ, bool skipfacing)
 {
        // ensure we're not checking nonexistent monster/target
        if(!this || !targ) { return false; }
 
        if((targ == this)
-       || (autocvar_g_monsters_lineofsight && !checkpvs(this.origin + this.view_ofs, targ)) // enemy cannot be seen
-       || (IS_VEHICLE(targ) && !((Monsters_from(this.monsterid)).spawnflags & MON_FLAG_RANGED)) // melee vs vehicle is useless
        || (time < game_starttime) // monsters do nothing before match has started
        || (targ.takedamage == DAMAGE_NO)
+       || (game_stopped)
        || (targ.items & IT_INVISIBILITY)
        || (IS_SPEC(targ) || IS_OBSERVER(targ)) // don't attack spectators
-       || (!IS_VEHICLE(targ) && (IS_DEAD(targ) || IS_DEAD(this) || targ.health <= 0 || this.health <= 0))
+       || (!IS_VEHICLE(targ) && (IS_DEAD(targ) || IS_DEAD(this) || GetResourceAmount(targ, RESOURCE_HEALTH) <= 0 || GetResourceAmount(this, RESOURCE_HEALTH) <= 0))
        || (this.monster_follow == targ || targ.monster_follow == this)
        || (!IS_VEHICLE(targ) && (targ.flags & FL_NOTARGET))
        || (!autocvar_g_monsters_typefrag && PHYS_INPUT_BUTTON_CHAT(targ))
+       || (IS_VEHICLE(targ) && !((Monsters_from(this.monsterid)).spawnflags & MON_FLAG_RANGED)) // melee vs vehicle is useless
        || (SAME_TEAM(targ, this))
        || (STAT(FROZEN, targ))
        || (targ.alpha != 0 && targ.alpha < 0.5)
+       || (autocvar_g_monsters_lineofsight && !checkpvs(this.origin + this.view_ofs, targ)) // enemy cannot be seen
        || (MUTATOR_CALLHOOK(MonsterValidTarget, this, targ))
        )
        {
@@ -99,18 +114,16 @@ bool Monster_ValidTarget(entity this, entity targ)
        }
 
        vector targ_origin = ((targ.absmin + targ.absmax) * 0.5);
-       traceline(this.origin + this.view_ofs, targ_origin, MOVE_NOMONSTERS, this);
+       traceline(this.origin + this.view_ofs, targ_origin, MOVE_NOMONSTERS, this); // TODO: maybe we can rely a bit on PVS data instead?
 
        if(trace_fraction < 1 && trace_ent != targ)
                return false; // solid
 
-       if(autocvar_g_monsters_target_infront || (this.spawnflags & MONSTERFLAG_INFRONT))
+       if(!skipfacing && (autocvar_g_monsters_target_infront || (this.spawnflags & MONSTERFLAG_INFRONT)))
        if(this.enemy != targ)
        {
-               makevectors (this.angles);
-               float dot = normalize (targ.origin - this.origin) * v_forward;
-
-               if(dot <= autocvar_g_monsters_target_infront_range) { return false; }
+               if(!monster_facing(this, targ))
+                       return false;
        }
 
        return true; // this target is valid!
@@ -132,7 +145,7 @@ entity Monster_FindTarget(entity this)
                vector theirmid = (it.absmin + it.absmax) * 0.5;
                if(vdist(theirmid - this.origin, >, trange))
                        continue;
-               if(!Monster_ValidTarget(this, it))
+               if(!Monster_ValidTarget(this, it, false))
                        continue;
 
                // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
@@ -200,7 +213,8 @@ void monster_changeteam(entity this, int newteam)
 .void(entity) monster_delayedfunc;
 void Monster_Delay_Action(entity this)
 {
-       if(Monster_ValidTarget(this.owner, this.owner.enemy)) { this.monster_delayedfunc(this.owner); }
+       // TODO: maybe do check for facing here
+       if(Monster_ValidTarget(this.owner, this.owner.enemy, false)) { this.monster_delayedfunc(this.owner); }
 
        if(this.cnt > 1)
        {
@@ -286,7 +300,7 @@ void Monster_Sounds_Precache(entity this)
 
 void Monster_Sounds_Clear(entity this)
 {
-#define _MSOUND(m) if(this.monstersound_##m) { strunzone(this.monstersound_##m); this.monstersound_##m = string_null; }
+#define _MSOUND(m) strfree(this.monstersound_##m);
        ALLMONSTERSOUNDS
 #undef _MSOUND
 }
@@ -321,9 +335,7 @@ bool Monster_Sounds_Load(entity this, string f, int first)
                field = Monster_Sound_SampleField(argv(0));
                if(GetMonsterSoundSampleField_notFound)
                        continue;
-               if (this.(field))
-                       strunzone(this.(field));
-               this.(field) = strzone(strcat(argv(1), " ", argv(2)));
+               strcpy(this.(field), strcat(argv(1), " ", argv(2)));
        }
        fclose(fh);
        return true;
@@ -388,7 +400,7 @@ bool Monster_Attack_Leap_Check(entity this, vector vel)
                return false; // already attacking
        if(!IS_ONGROUND(this))
                return false; // not on the ground
-       if(this.health <= 0 || IS_DEAD(this))
+       if(GetResourceAmount(this, RESOURCE_HEALTH) <= 0 || IS_DEAD(this))
                return false; // called when dead?
        if(time < this.attack_finished_single[0])
                return false; // still attacking
@@ -433,6 +445,7 @@ void Monster_Attack_Check(entity this, entity targ, .entity weaponentity)
        if((!this || !targ)
        || (!this.monster_attackfunc)
        || (time < this.attack_finished_single[slot])
+       || ((autocvar_g_monsters_target_infront || (this.spawnflags & MONSTERFLAG_INFRONT)) && !monster_facing(this, targ))
        ) { return; }
 
        if(vdist(targ.origin - this.origin, <=, this.attack_range))
@@ -482,10 +495,8 @@ void Monster_Touch(entity this, entity toucher)
 {
        if(toucher == NULL) { return; }
 
-       if(toucher.monster_attack)
-       if(this.enemy != toucher)
-       if(!IS_MONSTER(toucher))
-       if(Monster_ValidTarget(this, toucher))
+       if(toucher.monster_attack && this.enemy != toucher && !IS_MONSTER(toucher) && time >= this.spawn_time)
+       if(Monster_ValidTarget(this, toucher, true))
                this.enemy = toucher;
 }
 
@@ -499,7 +510,7 @@ void Monster_Miniboss_Check(entity this)
        // g_monsters_miniboss_chance cvar or spawnflags 64 causes a monster to be a miniboss
        if ((this.spawnflags & MONSTERFLAG_MINIBOSS) || (chance < autocvar_g_monsters_miniboss_chance))
        {
-               this.health += autocvar_g_monsters_miniboss_healthboost;
+               GiveResource(this, RESOURCE_HEALTH, autocvar_g_monsters_miniboss_healthboost);
                this.effects |= EF_RED;
                if(!this.weapon)
                        this.weapon = WEP_VORTEX.m_id;
@@ -540,10 +551,11 @@ void Monster_Dead_Fade(entity this)
                        this.pos2 = this.angles;
                }
                this.event_damage = func_null;
+               this.event_heal = func_null;
                this.takedamage = DAMAGE_NO;
                setorigin(this, this.pos1);
                this.angles = this.pos2;
-               this.health = this.max_health;
+               SetResourceAmountExplicit(this, RESOURCE_HEALTH, this.max_health);
                setmodel(this, MDL_Null);
        }
        else
@@ -557,7 +569,7 @@ void Monster_Dead_Fade(entity this)
 
 void Monster_Use(entity this, entity actor, entity trigger)
 {
-       if(Monster_ValidTarget(this, actor)) { this.enemy = actor; }
+       if(Monster_ValidTarget(this, actor, true)) { this.enemy = actor; }
 }
 
 .float pass_distance;
@@ -572,7 +584,7 @@ vector Monster_Move_Target(entity this, entity targ)
 
                // cases where the enemy may have changed their state (don't need to check everything here)
                if((!this.enemy)
-                       || (IS_DEAD(this.enemy) || this.enemy.health < 1)
+                       || (IS_DEAD(this.enemy) || GetResourceAmount(this.enemy, RESOURCE_HEALTH) < 1)
                        || (STAT(FROZEN, this.enemy))
                        || (this.enemy.flags & FL_NOTARGET)
                        || (this.enemy.alpha < 0.5 && this.enemy.alpha != 0)
@@ -670,42 +682,6 @@ vector Monster_Move_Target(entity this, entity targ)
        }
 }
 
-void Monster_CalculateVelocity(entity this, vector to, vector from, float turnrate, float movespeed)
-{
-       //float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
-       //float initial_height = 0; //min(50, (targ_distance * tanh(20)));
-       //float current_height = (initial_height * min(1, (this.pass_distance) ? (current_distance / this.pass_distance) : current_distance));
-       //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
-
-       vector targpos = to;
-#if 0
-       if(current_height) // make sure we can actually do this arcing path
-       {
-               targpos = (to + ('0 0 1' * current_height));
-               WarpZone_TraceLine(this.origin, targpos, MOVE_NOMONSTERS, this);
-               if(trace_fraction < 1)
-               {
-                       //print("normal arc line failed, trying to find new pos...");
-                       WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, this);
-                       targpos = (trace_endpos + '0 0 -10');
-                       WarpZone_TraceLine(this.origin, targpos, MOVE_NOMONSTERS, this);
-                       if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
-                       /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
-               }
-       }
-       else { targpos = to; }
-#endif
-
-       //this.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
-
-       vector desired_direction = normalize(targpos - from);
-       if(turnrate) { this.velocity = (normalize(normalize(this.velocity) + (desired_direction * 50)) * movespeed); }
-       else { this.velocity = (desired_direction * movespeed); }
-
-       //this.steerto = steerlib_attract2(targpos, 0.5, 500, 0.95);
-       //this.angles = vectoangles(this.velocity);
-}
-
 .entity draggedby;
 .entity target2;
 
@@ -808,13 +784,15 @@ void Monster_Move(entity this, float runspeed, float walkspeed, float stpspeed)
        if(!(this.spawnflags & MONSTERFLAG_FLY_VERTICAL) && !(this.flags & FL_SWIM))
                this.moveto_z = this.origin_z;
 
-       if(vdist(this.origin - this.moveto, >, 100))
+       fixedmakevectors(this.angles);
+       float vz = this.velocity_z;
+
+       if(!turret_closetotarget(this, this.moveto))
        {
                bool do_run = (this.enemy || this.monster_moveto);
-               if(IS_ONGROUND(this) || ((this.flags & FL_FLY) || (this.flags & FL_SWIM)))
-                       Monster_CalculateVelocity(this, this.moveto, this.origin, true, ((do_run) ? runspeed : walkspeed));
+               movelib_move_simple(this, v_forward, ((do_run) ? runspeed : walkspeed), 0.4);
 
-               if(time > this.pain_finished && time > this.anim_finished) // TODO: use anim_finished instead!?
+               if(time > this.pain_finished && time > this.anim_finished)
                if(!this.state)
                if(vdist(this.velocity, >, 10))
                        setanim(this, ((do_run) ? this.anim_run : this.anim_walk), true, false, false);
@@ -836,6 +814,9 @@ void Monster_Move(entity this, float runspeed, float walkspeed, float stpspeed)
                        setanim(this, this.anim_idle, true, false, false);
        }
 
+       if(!((this.flags & FL_FLY) || (this.flags & FL_SWIM)))
+               this.velocity_z = vz;
+
        this.steerto = steerlib_attract2(this, ((this.monster_face) ? this.monster_face : this.moveto), 0.5, 500, 0.95);
 
        vector real_angle = vectoangles(this.steerto) - this.angles;
@@ -874,6 +855,9 @@ void Monster_Dead_Think(entity this)
 {
        this.nextthink = time + this.ticrate;
 
+       Monster mon = Monsters_from(this.monsterid);
+       mon.mr_deadthink(mon, this);
+
        if(this.monster_lifetime != 0)
        if(time >= this.monster_lifetime)
        {
@@ -909,7 +893,7 @@ void Monster_Reset(entity this)
 
        Unfreeze(this); // remove any icy remains
 
-       this.health = this.max_health;
+       SetResourceAmountExplicit(this, RESOURCE_HEALTH, this.max_health);
        this.velocity = '0 0 0';
        this.enemy = NULL;
        this.goalentity = NULL;
@@ -919,11 +903,11 @@ void Monster_Reset(entity this)
 
 void Monster_Dead_Damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
 {
-       this.health -= damage;
+       TakeResource(this, RESOURCE_HEALTH, damage);
 
        Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker);
 
-       if(this.health <= -50) // 100 health until gone?
+       if(GetResourceAmount(this, RESOURCE_HEALTH) <= -50) // 100 health until gone?
        {
                Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker);
 
@@ -945,7 +929,7 @@ void Monster_Dead(entity this, entity attacker, float gibbed)
        if(STAT(FROZEN, this))
        {
                Unfreeze(this); // remove any icy remains
-               this.health = 0; // reset by Unfreeze
+               SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0); // reset by Unfreeze (TODO)
        }
 
        monster_dropitem(this, attacker);
@@ -969,6 +953,7 @@ void Monster_Dead(entity this, entity attacker, float gibbed)
                _setmodel(this, this.mdl_dead);
 
        this.event_damage       = ((gibbed) ? func_null : Monster_Dead_Damage);
+       this.event_heal         = func_null;
        this.solid                      = SOLID_CORPSE;
        this.takedamage         = DAMAGE_AIM;
        this.deadflag           = DEAD_DEAD;
@@ -980,6 +965,7 @@ void Monster_Dead(entity this, entity attacker, float gibbed)
        this.state                      = 0;
        this.attack_finished_single[0] = 0;
        this.effects = 0;
+       this.dphitcontentsmask &= ~DPCONTENTS_BODY;
 
        if(!((this.flags & FL_FLY) || (this.flags & FL_SWIM)))
                this.velocity = '0 0 0';
@@ -1013,7 +999,7 @@ void Monster_Damage(entity this, entity inflictor, entity attacker, float damage
        if(deathtype == DEATH_FALL.m_id && this.draggedby != NULL)
                return;
 
-       vector v = healtharmor_applydamage(100, this.armorvalue / 100, deathtype, damage);
+       vector v = healtharmor_applydamage(100, GetResourceAmount(this, RESOURCE_ARMOR) / 100, deathtype, damage);
        float take = v.x;
        //float save = v.y;
 
@@ -1022,12 +1008,12 @@ void Monster_Damage(entity this, entity inflictor, entity attacker, float damage
 
        if(take)
        {
-               this.health -= take;
+               TakeResource(this, RESOURCE_HEALTH, take);
                Monster_Sound(this, monstersound_pain, 1.2, true, CH_PAIN);
        }
 
        if(this.sprite)
-               WaypointSprite_UpdateHealth(this.sprite, this.health);
+               WaypointSprite_UpdateHealth(this.sprite, GetResourceAmount(this, RESOURCE_HEALTH));
 
        this.dmg_time = time;
 
@@ -1045,7 +1031,7 @@ void Monster_Damage(entity this, entity inflictor, entity attacker, float damage
                        Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, this, attacker);
        }
 
-       if(this.health <= 0)
+       if(GetResourceAmount(this, RESOURCE_HEALTH) <= 0)
        {
                if(deathtype == DEATH_KILL.m_id)
                        this.candrop = false; // killed by mobkill command
@@ -1054,13 +1040,13 @@ void Monster_Damage(entity this, entity inflictor, entity attacker, float damage
                SUB_UseTargets(this, attacker, this.enemy);
                this.target2 = this.oldtarget2; // reset to original target on death, incase we respawn
 
-               Monster_Dead(this, attacker, (this.health <= -100 || deathtype == DEATH_KILL.m_id));
+               Monster_Dead(this, attacker, (GetResourceAmount(this, RESOURCE_HEALTH) <= -100 || deathtype == DEATH_KILL.m_id));
 
                WaypointSprite_Kill(this.sprite);
 
                MUTATOR_CALLHOOK(MonsterDies, this, attacker, deathtype);
 
-               if(this.health <= -100 || deathtype == DEATH_KILL.m_id) // check if we're already gibbed
+               if(GetResourceAmount(this, RESOURCE_HEALTH) <= -100 || deathtype == DEATH_KILL.m_id) // check if we're already gibbed
                {
                        Violence_GibSplash(this, 1, 0.5, attacker);
 
@@ -1070,6 +1056,18 @@ void Monster_Damage(entity this, entity inflictor, entity attacker, float damage
        }
 }
 
+bool Monster_Heal(entity targ, entity inflictor, float amount, float limit)
+{
+       float true_limit = ((limit != RESOURCE_LIMIT_NONE) ? limit : targ.max_health);
+       if(GetResourceAmount(targ, RESOURCE_HEALTH) <= 0 || GetResourceAmount(targ, RESOURCE_HEALTH) >= true_limit)
+               return false;
+
+       GiveResourceWithLimit(targ, RESOURCE_HEALTH, amount, true_limit);
+       if(targ.sprite)
+               WaypointSprite_UpdateHealth(targ.sprite, GetResourceAmount(targ, RESOURCE_HEALTH));
+       return true;
+}
+
 // don't check for enemies, just keep walking in a straight line
 void Monster_Move_2D(entity this, float mspeed, bool allow_jumpoff)
 {
@@ -1163,11 +1161,11 @@ void Monster_Frozen_Think(entity this)
        if(STAT(FROZEN, this) == 2)
        {
                STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) + this.ticrate * this.revive_speed, 1);
-               this.health = max(1, STAT(REVIVE_PROGRESS, this) * this.max_health);
+               SetResourceAmountExplicit(this, RESOURCE_HEALTH, max(1, STAT(REVIVE_PROGRESS, this) * this.max_health));
                this.iceblock.alpha = bound(0.2, 1 - STAT(REVIVE_PROGRESS, this), 1);
 
                if(!(this.spawnflags & MONSTERFLAG_INVINCIBLE) && this.sprite)
-                       WaypointSprite_UpdateHealth(this.sprite, this.health);
+                       WaypointSprite_UpdateHealth(this.sprite, GetResourceAmount(this, RESOURCE_HEALTH));
 
                if(STAT(REVIVE_PROGRESS, this) >= 1)
                        Unfreeze(this);
@@ -1175,15 +1173,15 @@ void Monster_Frozen_Think(entity this)
        else if(STAT(FROZEN, this) == 3)
        {
                STAT(REVIVE_PROGRESS, this) = bound(0, STAT(REVIVE_PROGRESS, this) - this.ticrate * this.revive_speed, 1);
-               this.health = max(0, autocvar_g_nades_ice_health + (this.max_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this) );
+               SetResourceAmountExplicit(this, RESOURCE_HEALTH, max(0, autocvar_g_nades_ice_health + (this.max_health-autocvar_g_nades_ice_health) * STAT(REVIVE_PROGRESS, this)));
 
                if(!(this.spawnflags & MONSTERFLAG_INVINCIBLE) && this.sprite)
-                       WaypointSprite_UpdateHealth(this.sprite, this.health);
+                       WaypointSprite_UpdateHealth(this.sprite, GetResourceAmount(this, RESOURCE_HEALTH));
 
-               if(this.health < 1)
+               if(GetResourceAmount(this, RESOURCE_HEALTH) < 1)
                {
                        Unfreeze(this);
-                       this.health = 0;
+                       SetResourceAmountExplicit(this, RESOURCE_HEALTH, 0);
                        if(this.event_damage)
                                this.event_damage(this, this, this.frozen_by, 1, DEATH_NADE_ICE_FREEZE.m_id, DMG_NOWEP, this.origin, '0 0 0');
                }
@@ -1223,7 +1221,7 @@ void Monster_Think(entity this)
 
        if(this.monster_lifetime && time >= this.monster_lifetime)
        {
-               Damage(this, this, this, this.health + this.max_health, DEATH_KILL.m_id, DMG_NOWEP, this.origin, this.origin);
+               Damage(this, this, this, GetResourceAmount(this, RESOURCE_HEALTH) + this.max_health, DEATH_KILL.m_id, DMG_NOWEP, this.origin, this.origin);
                return;
        }
 
@@ -1255,8 +1253,8 @@ bool Monster_Spawn_Setup(entity this)
        mon.mr_setup(mon, this);
 
        // ensure some basic needs are met
-       if(!this.health) { this.health = 100; }
-       if(!this.armorvalue) { this.armorvalue = bound(0.2, 0.5 * MONSTER_SKILLMOD(this), 0.9); }
+       if(!GetResourceAmount(this, RESOURCE_HEALTH)) { SetResourceAmountExplicit(this, RESOURCE_HEALTH, 100); }
+       if(!GetResourceAmount(this, RESOURCE_ARMOR)) { SetResourceAmountExplicit(this, RESOURCE_ARMOR, bound(0.2, 0.5 * MONSTER_SKILLMOD(this), 0.9)); }
        if(!this.target_range) { this.target_range = autocvar_g_monsters_target_range; }
        if(!this.respawntime) { this.respawntime = autocvar_g_monsters_respawn_delay; }
        if(!this.monster_moveflags) { this.monster_moveflags = MONSTER_MOVE_WANDER; }
@@ -1266,14 +1264,15 @@ bool Monster_Spawn_Setup(entity this)
        if(!(this.spawnflags & MONSTERFLAG_RESPAWNED))
        {
                Monster_Miniboss_Check(this);
-               this.health *= MONSTER_SKILLMOD(this);
+               SetResourceAmountExplicit(this, RESOURCE_HEALTH, GetResourceAmount(this, RESOURCE_HEALTH) * MONSTER_SKILLMOD(this));
 
                if(!this.skin)
                        this.skin = rint(random() * 4);
        }
 
-       this.max_health = this.health;
+       this.max_health = GetResourceAmount(this, RESOURCE_HEALTH);
        this.pain_finished = this.nextthink;
+       this.last_enemycheck = this.spawn_time + random(); // slight delay
 
        if(IS_PLAYER(this.monster_follow))
                this.effects |= EF_DIMLIGHT;
@@ -1301,7 +1300,7 @@ bool Monster_Spawn_Setup(entity this)
                if(!(this.spawnflags & MONSTERFLAG_INVINCIBLE))
                {
                        WaypointSprite_UpdateMaxHealth(this.sprite, this.max_health);
-                       WaypointSprite_UpdateHealth(this.sprite, this.health);
+                       WaypointSprite_UpdateHealth(this.sprite, GetResourceAmount(this, RESOURCE_HEALTH));
                }
        }
 
@@ -1366,6 +1365,7 @@ bool Monster_Spawn(entity this, bool check_appear, int mon_id)
        this.damagedbycontents  = true;
        this.monsterid                  = mon_id;
        this.event_damage               = Monster_Damage;
+       this.event_heal                 = Monster_Heal;
        settouch(this, Monster_Touch);
        this.use                                = Monster_Use;
        this.solid                              = SOLID_BBOX;
@@ -1381,7 +1381,6 @@ bool Monster_Spawn(entity this, bool check_appear, int mon_id)
        this.monster_attackfunc = mon.monster_attackfunc;
        this.monster_name               = mon.monster_name;
        this.candrop                    = true;
-       this.view_ofs                   = '0 0 0.7' * (this.maxs_z * 0.5);
        this.oldtarget2                 = this.target2;
        //this.pass_distance            = 0;
        this.deadflag                   = DEAD_NO;
@@ -1411,6 +1410,7 @@ bool Monster_Spawn(entity this, bool check_appear, int mon_id)
                this.scale *= 1.3;
 
        setsize(this, mon.m_mins * this.scale, mon.m_maxs * this.scale);
+       this.view_ofs                   = '0 0 0.7' * (this.maxs_z * 0.5);
 
        this.ticrate = bound(sys_frametime, ((!this.ticrate) ? autocvar_g_monsters_think_delay : this.ticrate), 60);