From d810761e7610e9394ed57717761d3c2c73580b66 Mon Sep 17 00:00:00 2001 From: div0 Date: Wed, 14 Jan 2009 09:07:30 +0000 Subject: [PATCH] monster framework from dpmod git-svn-id: svn://svn.icculus.org/nexuiz/trunk@5515 f962a42d-fe04-0410-a3ab-8c8b0445ebaa --- data/qcsrc/server/monsters/ai.qc | 885 ++++++++++++++++++ data/qcsrc/server/monsters/defs.qc | 53 ++ data/qcsrc/server/monsters/fight.qc | 252 +++++ data/qcsrc/server/monsters/m_monsters.qc | 467 +++++++++ data/qcsrc/server/monsters/mode_management.qc | 526 +++++++++++ data/qcsrc/server/progs.src | 5 + data/qcsrc/server/t_teleporters.qc | 70 +- 7 files changed, 2228 insertions(+), 30 deletions(-) create mode 100644 data/qcsrc/server/monsters/ai.qc create mode 100644 data/qcsrc/server/monsters/defs.qc create mode 100644 data/qcsrc/server/monsters/fight.qc create mode 100644 data/qcsrc/server/monsters/m_monsters.qc create mode 100644 data/qcsrc/server/monsters/mode_management.qc diff --git a/data/qcsrc/server/monsters/ai.qc b/data/qcsrc/server/monsters/ai.qc new file mode 100644 index 000000000..2a615f140 --- /dev/null +++ b/data/qcsrc/server/monsters/ai.qc @@ -0,0 +1,885 @@ +void() movetarget_f; +void() t_movetarget; +void() FoundTarget; + +float MONSTER_WANDER = 64; // disable wandering around +float MONSTER_APPEAR = 128; // spawn invisible, and appear when triggered + +.float ismonster; +.float monsterawaitingteleport; // avoid awaking monsters in teleport rooms + +// when a monster becomes angry at a player, that monster will be used +// as the sight target the next frame so that monsters near that one +// will wake up even if they wouldn't have noticed the player +// +entity sight_entity; +float sight_entity_time; + +/* + +.enemy +Will be world if not currently angry at anyone. + +.movetarget +The next path spot to walk toward. If .enemy, ignore .movetarget. +When an enemy is killed, the monster will try to return to it's path. + +.huntt_ime +Set to time + something when the player is in sight, but movement straight for +him is blocked. This causes the monster to use wall following code for +movement direction instead of sighting on the player. + +.ideal_yaw +A yaw angle of the intended direction, which will be turned towards at up +to 45 deg / state. If the enemy is in view and hunt_time is not active, +this will be the exact line towards the enemy. + +.pausetime +A monster will leave it's stand state and head towards it's .movetarget when +time > .pausetime. + +walkmove(angle, speed) primitive is all or nothing +*/ + + +// +// globals +// +//float current_yaw; + +float(float v) anglemod = +{ + v = v - 360 * floor(v / 360); + return v; +}; + +/* +============================================================================== + +MOVETARGET CODE + +The angle of the movetarget effects standing and bowing direction, but has no effect on movement, which allways heads to the next target. + +targetname +must be present. The name of this movetarget. + +target +the next spot to move to. If not present, stop here for good. + +pausetime +The number of seconds to spend standing or bowing for path_stand or path_bow + +============================================================================== +*/ + + +void() movetarget_f = +{ + if (!self.targetname) + objerror ("monster_movetarget: no targetname"); + + self.solid = SOLID_TRIGGER; + self.touch = t_movetarget; + setsize (self, '-8 -8 -8', '8 8 8'); +}; + +/*QUAKED path_corner (0.5 0.3 0) (-8 -8 -8) (8 8 8) +Monsters will continue walking towards the next target corner. +*/ +void() path_corner = +{ + movetarget_f (); +}; + +/* +============= +t_movetarget + +Something has bumped into a movetarget. If it is a monster +moving towards it, change the next destination and continue. +============== +*/ +void() t_movetarget = +{ + local entity temp; + + if (other.health < 1) + return; + if (other.movetarget != self) + return; + + if (other.enemy) + return; // fighting, not following a path + + temp = self; + self = other; + other = temp; + + if (self.classname == "monster_ogre") + sound (self, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound + +//dprint ("t_movetarget\n"); + self.goalentity = self.movetarget = find (world, targetname, other.target); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + if (!self.movetarget) + { + self.pausetime = time + 999999; + self.th_stand (); + return; + } +}; + +void() monster_wanderpaththink = +{ + local vector v, v1; + local float b, c; + self.nextthink = time + random() * 10 + 1; + if (self.owner.health < 1) // dead, also handled in death code + { + self.owner.movetarget = world; + remove(self); + return; + } + b = -1; + c = 10; + while (c > 0) + { + c = c - 1; + v = randomvec(); + traceline(self.owner.origin, v * 1024 + self.owner.origin, FALSE, self); + v = trace_endpos - (normalize(v) * 16) - self.owner.origin; + if (vlen(v) > b) + { + b = vlen(v); + v1 = v; + } + } + setorigin(self, v1 + self.owner.origin); + self.owner.ideal_yaw = vectoyaw(self.origin - self.owner.origin); +}; + +void() monster_wanderpathtouch = +{ + if (other.health < 1) + return; + if (other.movetarget != self) + return; + + if (other.enemy) + return; // fighting, not following a path + + if (other.classname == "monster_ogre") + sound (other, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound + monster_wanderpaththink(); +}; + +void() monster_spawnwanderpath = +{ + newmis = spawn(); + newmis.classname = "monster_wanderpath"; + newmis.solid = SOLID_TRIGGER; + newmis.touch = monster_wanderpathtouch; + setsize (newmis, '-8 -8 -8', '8 8 8'); + newmis.think = monster_wanderpaththink; + newmis.nextthink = time + random() * 10 + 1; + newmis.owner = self; + self.goalentity = self.movetarget = newmis; +}; + +void() monster_checkbossflag = +{ + local float healthboost; + local float r; + +#if 0 + // monsterbosses cvar or spawnflag 64 causes a monster to be a miniboss + if ((self.spawnflags & 64) || (random() * 100 < cvar("monsterbosspercent"))) + { + self.radsuit_finished = time + 1000000000; + r = random() * 4; + if (r < 2) + { + self.super_damage_finished = time + 1000000000; + healthboost = 30 + self.health * 0.5; + self.effects = self.effects | (EF_FULLBRIGHT | EF_BLUE); + } + if (r >= 1) + { + healthboost = 30 + self.health * bound(0.5, skill * 0.5, 1.5); + self.effects = self.effects | (EF_FULLBRIGHT | EF_RED); + self.healthregen = max(self.healthregen, min(skill * 10, 30)); + } + self.health = self.health + healthboost; + self.max_health = self.health; + self.bodyhealth = self.bodyhealth * 2 + healthboost; + do + { + self.colormod_x = random(); + self.colormod_y = random(); + self.colormod_z = random(); + self.colormod = normalize(self.colormod); + } + while (self.colormod_x > 0.6 && self.colormod_y > 0.6 && self.colormod_z > 0.6); + } +#endif +}; + + +//============================================================================ + +/* +============= +range + +returns the range catagorization of an entity reletive to self +0 melee range, will become hostile even if back is turned +1 visibility and infront, or visibility and show hostile +2 infront and show hostile +3 only triggered by damage +============= +*/ +float(entity targ) range = +{ + local float r; + r = vlen ((self.origin + self.view_ofs) - (targ.origin + targ.view_ofs)); + if (r < 120) + return RANGE_MELEE; + if (r < 500) + return RANGE_NEAR; + if (r < 2000) // increased from 1000 for DP + return RANGE_MID; + return RANGE_FAR; +}; + +/* +============= +visible + +returns 1 if the entity is visible to self, even if not infront () +============= +*/ +float (entity targ) visible = +{ + if (vlen(targ.origin - self.origin) > 5000) // long traces are slow + return FALSE; + + traceline ((self.origin + self.view_ofs), (targ.origin + targ.view_ofs), TRUE, self); // see through other monsters + + if (trace_inopen && trace_inwater) + return FALSE; // sight line crossed contents + + if (trace_fraction == 1) + return TRUE; + return FALSE; +}; + + +/* +============= +infront + +returns 1 if the entity is in front (in sight) of self +============= +*/ +float(entity targ) infront = +{ + local float dot; + + makevectors (self.angles); + dot = normalize (targ.origin - self.origin) * v_forward; + + return (dot > 0.3); +}; +// returns 0 if not infront, or the dotproduct if infront +float(vector dir, entity targ) infront2 = +{ + local float dot; + + dir = normalize(dir); + dot = normalize (targ.origin - self.origin) * dir; + + if (dot >= 0.3) return dot; // infront + return 0; +}; + + +//============================================================================ + +/* +=========== +ChangeYaw + +Turns towards self.ideal_yaw at self.yaw_speed +Sets the global variable current_yaw +Called every 0.1 sec by monsters +============ +*/ +/* + +void() ChangeYaw = +{ + local float ideal, move; + +//current_yaw = self.ideal_yaw; +// mod down the current angle + current_yaw = anglemod( self.angles_y ); + ideal = self.ideal_yaw; + + if (current_yaw == ideal) + return; + + move = ideal - current_yaw; + if (ideal > current_yaw) + { + if (move > 180) + move = move - 360; + } + else + { + if (move < -180) + move = move + 360; + } + + if (move > 0) + { + if (move > self.yaw_speed) + move = self.yaw_speed; + } + else + { + if (move < 0-self.yaw_speed ) + move = 0-self.yaw_speed; + } + + current_yaw = anglemod (current_yaw + move); + + self.angles_y = current_yaw; +}; + +*/ + + +//============================================================================ + +void() HuntTarget = +{ + self.goalentity = self.enemy; + self.think = self.th_run; + self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); + self.nextthink = time + 0.1; + SUB_AttackFinished (1); // wait a while before first attack +}; + +.void() th_sightsound; + +void() SightSound = +{ + if (self.health < 1) + return; + // skill 5 does not play sight sounds, instead you only hear the appear sound as they are about to attack + if (skill >= 5) + if (self.classname != "monster_hellfish") + return; + + if (self.th_sightsound) + self.th_sightsound(); +}; + +void() FoundTarget = +{ + if (self.health < 1 || !self.th_run) + return; + if (self.enemy.health < 1 || !self.enemy.takedamage) + return; + if (self.enemy.classname == "player") + { + // let other monsters see this monster for a while + sight_entity = self; + sight_entity_time = time + 0.1; + } + + self.show_hostile = time + 1; // wake up other monsters + + SightSound (); + HuntTarget (); +}; + +/* +//float checkplayertime; +entity lastcheckplayer; +entity havocbot_list; + + +entity() checkplayer = +{ + local entity check; + local float worldcount; + // we can just fallback on checkclient if there are no bots + if (!havocbot_list) + return checkclient(); +*/ + /* + if (time < checkplayertime) + { + traceline(self.origin + self.view_ofs, lastcheckplayer.origin + lastcheckplayer.view_ofs, TRUE, self); + if (trace_fraction == 1) + return lastcheckplayer; + if (trace_ent == lastcheckplayer) + return lastcheckplayer; + } + checkplayertime = time + 0.1; + */ +/* + check = lastcheckplayer; + worldcount = 0; + c = 0; + do + { + c = c + 1; + check = findfloat(check, havocattack, TRUE); + if (check.classname == "player" || check.classname == "turretbase") + { + traceline(self.origin + self.view_ofs, check.origin + check.view_ofs, TRUE, self); + if (trace_fraction == 1) + return lastcheckplayer = check; + if (trace_ent == check) + return lastcheckplayer = check; + } + else if (check == world) + { + worldcount = worldcount + 1; + if (worldcount >= 2) + return lastcheckplayer = check; + } + } + while(check != lastcheckplayer && c < 100); + return world; +}; +*/ + +/* +=========== +FindTarget + +Self is currently not attacking anything, so try to find a target + +Returns TRUE if an enemy was sighted + +When a player fires a missile, the point of impact becomes a fakeplayer so +that monsters that see the impact will respond as if they had seen the +player. + +To avoid spending too much time, only a single client (or fakeclient) is +checked each frame. This means multi player games will have slightly +slower noticing monsters. +============ +*/ +.float findtarget; +float() FindTarget = +{ + local entity client; + local float r; + + if (self.health < 1) + return FALSE; + + // if the first or second spawnflag bit is set, the monster will only + // wake up on really seeing the player, not another monster getting angry + + if (self.spawnflags & 3) + { + // don't wake up on seeing another monster getting angry + client = checkclient (); + if (!client) + return FALSE; // current check entity isn't in PVS + } + else + { + if (sight_entity_time >= time) + { + client = sight_entity; + if (client.enemy == self.enemy) + return TRUE; + } + else + { + client = checkclient (); + if (!client) + return FALSE; // current check entity isn't in PVS + } + } + + if (client == self.enemy) + return FALSE; + + if (client.flags & FL_NOTARGET) + return FALSE; + +#if 0 + if (client.items & IT_INVISIBILITY) + return FALSE; +#endif + + // on skill 5 the monsters usually ignore the player and remain ghostlike + if (skill >= 5) + if (self.classname != "monster_hellfish") + if (random() < 0.99) + return FALSE; + + r = range(client); + if (r == RANGE_FAR) + return FALSE; + + if (!visible (client)) + return FALSE; + + if (r == RANGE_NEAR) + { + if (client.show_hostile < time && !infront (client)) + return FALSE; + } + else if (r == RANGE_MID) + { + // LordHavoc: was if ( /* client.show_hostile < time || */ !infront (client)) + if (client.show_hostile < time && !infront (client)) + return FALSE; + } + + // + // got one + // + + if (client.model == "") + return FALSE; + self.enemy = client; + if (self.enemy.classname != "player" && self.enemy.classname != "turretbase") + { + self.enemy = self.enemy.enemy; + if (self.enemy.classname != "player" && self.enemy.classname != "turretbase") + { + self.enemy = world; + return FALSE; + } + } + + FoundTarget (); + + return TRUE; +}; + + +//============================================================================= + +void(float dist) ai_forward = +{ + walkmove (self.angles_y, dist); +}; + +void(float dist) ai_back = +{ + walkmove ( (self.angles_y+180), dist); +}; + + +void(float a) monster_setalpha; + +/* +============= +ai_pain + +stagger back a bit +============= +*/ +void(float dist) ai_pain = +{ + if (self.health < 1) + return; + ai_back (dist); +}; + +/* +============= +ai_painforward + +stagger back a bit +============= +*/ +void(float dist) ai_painforward = +{ + if (self.health < 1) + return; + walkmove (self.ideal_yaw, dist); +}; + +/* +============= +ai_walk + +The monster is walking it's beat +============= +*/ +void(float dist) ai_walk = +{ + if (self.health < 1) + return; + + movedist = dist; + + // check for noticing a player + if (self.oldenemy.takedamage) + if (self.oldenemy.health >= 1) + { + self.enemy = self.oldenemy; + self.oldenemy = world; + FoundTarget(); + monster_setalpha(0); + return; + } + if (self.enemy) + { + if (self.enemy.takedamage) + { + if (self.enemy.health >= 1) + { + FoundTarget(); + monster_setalpha(0); + return; + } + else + self.enemy = world; + } + else + self.enemy = world; + } + + self.findtarget = TRUE; + + movetogoal (dist); + monster_setalpha(0); +}; + + +/* +============= +ai_stand + +The monster is staying in one place for a while, with slight angle turns +============= +*/ +void() ai_stand = +{ + if (self.health < 1) + return; + if (self.enemy) + { + if (self.enemy.takedamage) + { + if (self.enemy.health >= 1) + { + FoundTarget(); + monster_setalpha(0); + return; + } + else + self.enemy = world; + } + else + self.enemy = world; + } + self.findtarget = TRUE; + + if (time > self.pausetime) + { + self.th_walk (); + monster_setalpha(0); + return; + } + +// change angle slightly + + monster_setalpha(0); +}; + +/* +============= +ai_turn + +don't move, but turn towards ideal_yaw +============= +*/ +void() ai_turn = +{ + if (self.enemy) + { + if (self.enemy.takedamage) + { + if (self.enemy.health >= 1) + { + FoundTarget(); + monster_setalpha(0); + return; + } + else + self.enemy = world; + } + else + self.enemy = world; + } + self.findtarget = TRUE; + + ChangeYaw (); + monster_setalpha(0); +}; + +//============================================================================= + +/* +============= +ChooseTurn +============= +*/ +void(vector pDestvec) ChooseTurn = +{ + local vector dir, newdir; + + dir = self.origin - pDestvec; + + newdir_x = trace_plane_normal_y; + newdir_y = 0 - trace_plane_normal_x; + newdir_z = 0; + + if (dir * newdir > 0) + { + dir_x = 0 - trace_plane_normal_y; + dir_y = trace_plane_normal_x; + } + else + { + dir_x = trace_plane_normal_y; + dir_y = 0 - trace_plane_normal_x; + } + + dir_z = 0; + self.ideal_yaw = vectoyaw(dir); +}; + +/* +============ +FacingIdeal + +============ +*/ +float() FacingIdeal = +{ + local float delta; + + delta = anglemod(self.angles_y - self.ideal_yaw); + if (delta > 45 && delta < 315) + return FALSE; + return TRUE; +}; + + +//============================================================================= + +.float() th_checkattack; + + + +/* +============= +ai_run + +The monster has an enemy it is trying to kill +============= +*/ +void(float dist) ai_run = +{ + local float ofs; + if (self.health < 1) + return; + movedist = dist; + // see if the enemy is dead + if (self.enemy.health < 1 || self.enemy.takedamage == DAMAGE_NO) + { + self.enemy = world; + // FIXME: look all around for other targets + if (self.oldenemy.health >= 1 && self.oldenemy.takedamage) + { + self.enemy = self.oldenemy; + self.oldenemy = world; + HuntTarget (); + } + else + { + if (self.movetarget) + self.th_walk (); + else + self.th_stand (); + return; + } + } + + // wake up other monsters + self.show_hostile = time + 1; + + // check knowledge of enemy + enemy_range = range(self.enemy); + + self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); + ChangeYaw (); + + if (self.attack_state == AS_MELEE) + { + //dprint ("ai_run_melee\n"); + //Turn and close until within an angle to launch a melee attack + if (FacingIdeal()) + { + self.th_melee (); + self.attack_state = AS_STRAIGHT; + } + return; + } + else if (self.attack_state == AS_MISSILE) + { + //dprint ("ai_run_missile\n"); + //Turn in place until within an angle to launch a missile attack + if (FacingIdeal()) + if (self.th_missile ()) + self.attack_state = AS_STRAIGHT; + return; + } + + if (self.th_checkattack()) + return; // beginning an attack + + if (visible(self.enemy)) + self.search_time = time + 5; + else if (coop) + { + // look for other coop players + if (self.search_time < time) + self.findtarget = TRUE; + } + + if (self.attack_state == AS_SLIDING) + { + //dprint ("ai_run_slide\n"); + //Strafe sideways, but stay at aproximately the same range + if (self.lefty) + ofs = 90; + else + ofs = -90; + + if (walkmove (self.ideal_yaw + ofs, movedist)) + return; + + self.lefty = !self.lefty; + + walkmove (self.ideal_yaw - ofs, movedist); + } + + // head straight in + movetogoal (dist); // done in C code... +}; + diff --git a/data/qcsrc/server/monsters/defs.qc b/data/qcsrc/server/monsters/defs.qc new file mode 100644 index 000000000..777e114d3 --- /dev/null +++ b/data/qcsrc/server/monsters/defs.qc @@ -0,0 +1,53 @@ +.entity movetarget; +.float pausetime; + +.void() th_stand; +.void() th_walk; +.void() th_run; +.float() th_missile; // LordHavoc: changed from void() to float(), returns true if attacking +.void() th_melee; +.void(entity attacker, float damage, float damgtype, string dethtype) th_pain; +.void() th_die; +.entity oldenemy; // mad at this player before taking damage +entity newmis; // launch_spike sets this after spawning it + +// range values +float RANGE_MELEE = 0; +float RANGE_NEAR = 1; +float RANGE_MID = 2; +float RANGE_FAR = 3; + +float DMG_KNIGHT_MELEE_BASE = 0; +float DMG_KNIGHT_MELEE_RANDOM1 = 3; +float DMG_KNIGHT_MELEE_RANDOM2 = 3; +float DMG_KNIGHT_MELEE_RANDOM3 = 3; + +.float show_hostile; + // set to time+0.2 whenever a client fires a + // weapon or takes damage. Used to alert + // monsters that otherwise would let the player go + +float movedist; +.float lefty; +.float search_time; +.float attack_state; + +float AS_STRAIGHT = 1; +float AS_SLIDING = 2; +float AS_MELEE = 3; +float AS_MISSILE = 4; + +float SKILL4_MINALPHA = 0.4; + +float monsterwander; +/* + monsterwander = cvar("monsterwander"); + // monsterwander is always on in skill 5 + if (skill >= 5) + monsterwander = TRUE; +*/ + +.float candrown; + +.void(vector org, float bodydamage, float armordamage, vector vel, float damgtype) bleedfunc; +void(vector org, float bodydamage, float armordamage, vector vel, float damgtype) genericbleedfunc; diff --git a/data/qcsrc/server/monsters/fight.qc b/data/qcsrc/server/monsters/fight.qc new file mode 100644 index 000000000..10d00a16b --- /dev/null +++ b/data/qcsrc/server/monsters/fight.qc @@ -0,0 +1,252 @@ + +/* + +A monster is in fight mode if it thinks it can effectively attack its +enemy. + +When it decides it can't attack, it goes into hunt mode. + +*/ + +void SUB_AttackFinished (float normal) +{ + self.cnt = 0; // refire count for nightmare + if (skill < 3) + ATTACK_FINISHED(self) = time + normal; +} + +float CanDamage(entity targ, entity inflictor) +{ + if (targ.movetype == MOVETYPE_PUSH) + { + traceline(inflictor.origin, 0.5 * (targ.absmin + targ.absmax), TRUE, self); + if (trace_fraction == 1) + return TRUE; + if (trace_ent == targ) + return TRUE; + return FALSE; + } + + traceline(inflictor.origin, targ.origin, TRUE, self); + if (trace_fraction == 1) + return TRUE; + traceline(inflictor.origin, targ.origin + '15 15 0', TRUE, self); + if (trace_fraction == 1) + return TRUE; + traceline(inflictor.origin, targ.origin + '-15 -15 0', TRUE, self); + if (trace_fraction == 1) + return TRUE; + traceline(inflictor.origin, targ.origin + '-15 15 0', TRUE, self); + if (trace_fraction == 1) + return TRUE; + traceline(inflictor.origin, targ.origin + '15 -15 0', TRUE, self); + if (trace_fraction == 1) + return TRUE; + + return FALSE; +} + +float(float v) anglemod; + +void(vector dest) ChooseTurn; + +void() ai_face; + + +float enemy_range; + + +//============================================================================= + +/* +=========== +GenericCheckAttack + +The player is in view, so decide to move or launch an attack +Returns FALSE if movement should continue +============ +*/ +float() GenericCheckAttack = +{ + local vector spot1, spot2; + local entity targ; + local float chance; + + if (self.health < 1) + return FALSE; + targ = self.enemy; + + if (vlen(targ.origin - self.origin) > 5000) // long traces are slow + return FALSE; + +// see if any entities are in the way of the shot + spot1 = self.origin + self.view_ofs; + spot2 = targ.origin + targ.view_ofs; + + traceline (spot1, spot2, FALSE, self); + + if (trace_ent != targ) + return FALSE; // don't have a clear shot + + if (trace_inopen && trace_inwater) + return FALSE; // sight line crossed contents + + if (enemy_range == RANGE_MELEE) + { // melee attack + if (self.th_melee) + { + self.th_melee (); + return TRUE; + } + } + +// missile attack + if (time < ATTACK_FINISHED(self)) + return FALSE; + + if (!self.th_missile) + return FALSE; + + if (enemy_range == RANGE_FAR) + return FALSE; + + if (enemy_range == RANGE_MELEE) + { + chance = 0.9; + ATTACK_FINISHED(self) = 0; + } + else if (enemy_range == RANGE_NEAR) + { + if (self.th_melee) + chance = 0.2; + else + chance = 0.4; + } + else if (enemy_range == RANGE_MID) + { + if (self.th_melee) + chance = 0.05; + else + chance = 0.1; + } + else + chance = 0; + + if (random () < chance) + if (self.th_missile ()) + { + SUB_AttackFinished (2*random()); + return TRUE; + } + + return FALSE; +}; + + +/* +============= +ai_face + +Stay facing the enemy +============= +*/ +void() ai_face = +{ + self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); + ChangeYaw (); +}; + +/* +============= +ai_charge + +The monster is in a melee attack, so get as close as possible to .enemy +============= +*/ +float (entity targ) visible; +float(entity targ) infront; +float(entity targ) range; + +void(float d) ai_charge = +{ + if (self.health < 1) + return; + ai_face (); + movetogoal (d); // done in C code... +}; + +void() ai_charge_side = +{ + if (self.health < 1) + return; + local vector dtemp; + local float heading; + +// aim to the left of the enemy for a flyby + + self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin); + ChangeYaw (); + + makevectors (self.angles); + dtemp = self.enemy.origin - 30*v_right; + heading = vectoyaw(dtemp - self.origin); + + walkmove(heading, 20); +}; + + +/* +============= +ai_melee + +============= +*/ +void() ai_melee = +{ + local vector delta; + local float ldmg; + + if (self.health < 1) + return; + if (!self.enemy) + return; // removed before stroke + + delta = self.enemy.origin - self.origin; + + if (vlen(delta) > 60) + return; + + ldmg = DMG_KNIGHT_MELEE_BASE + DMG_KNIGHT_MELEE_RANDOM1 * random(); + ldmg = ldmg + DMG_KNIGHT_MELEE_RANDOM2 * random(); + ldmg = ldmg + DMG_KNIGHT_MELEE_RANDOM3 * random(); + traceline(self.origin, self.enemy.origin, FALSE, self); + + Damage (self.enemy, self, self, ldmg, self.projectiledeathtype, trace_endpos, '0 0 0'); // TODO add force to monster melee attacks? +}; + + +void() ai_melee_side = +{ + local vector delta; + local float ldmg; + + if (self.health < 1) + return; + if (!self.enemy) + return; // removed before stroke + + ai_charge_side(); + + delta = self.enemy.origin - self.origin; + + if (vlen(delta) > 60) + return; + if (!CanDamage (self.enemy, self)) + return; + ldmg = DMG_KNIGHT_MELEE_BASE + DMG_KNIGHT_MELEE_RANDOM1 * random(); + ldmg = ldmg + DMG_KNIGHT_MELEE_RANDOM2 * random(); + ldmg = ldmg + DMG_KNIGHT_MELEE_RANDOM3 * random(); + traceline(self.origin, self.enemy.origin, FALSE, self); + Damage (self.enemy, self, self, ldmg, self.projectiledeathtype, trace_endpos, '0 0 0'); +}; + diff --git a/data/qcsrc/server/monsters/m_monsters.qc b/data/qcsrc/server/monsters/m_monsters.qc new file mode 100644 index 000000000..67203f516 --- /dev/null +++ b/data/qcsrc/server/monsters/m_monsters.qc @@ -0,0 +1,467 @@ +/* ALL MONSTERS SHOULD BE 1 0 0 IN COLOR */ + +// name =[framenum, nexttime, nextthink] {code} +// expands to: +// name () +// { +// self.frame=framenum; +// self.nextthink = time + nexttime; +// self.think = nextthink +// +// }; + +.float ismonster; + +.float modelindex2; + +/* +================ +monster_use + +Using a monster makes it angry at the current activator +LordHavoc: using a monster with the spawnflag 'Appear' makes it appear +================ +*/ +void() monster_use = +{ + if (self.enemy) + return; + if (self.health < 1) + return; + if (self.mdl) + if (self.spawnflags & MONSTER_APPEAR) + { + self.nextthink = time + 0.1; + self.spawnflags = self.spawnflags - MONSTER_APPEAR; + self.solid = SOLID_SLIDEBOX; + self.takedamage = DAMAGE_AIM; + //self.movetype = MOVETYPE_STEP; + self.model = self.mdl; + self.mdl = ""; + self.modelindex = self.modelindex2; + self.modelindex2 = 0; + //setorigin(self, self.origin + '0 0 1'); + spawn_tdeath(self.origin, self, self.origin); + return; + } + +#if 0 + if (activator.items & IT_INVISIBILITY) + return; +#endif + if (activator.flags & FL_NOTARGET) + return; + if (activator.classname != "player") + return; + + // delay reaction so if the monster is teleported, its sound is still heard + self.enemy = activator; + self.nextthink = time + 0.1; + self.think = FoundTarget; +}; + +void() monster_appearsetup = +{ + if ((self.spawnflags & MONSTER_APPEAR) == 0) + return; + self.mdl = self.model; + self.modelindex2 = self.modelindex; + self.modelindex = 0; + self.solid = SOLID_NOT; + self.takedamage = DAMAGE_NO; + //self.movetype = MOVETYPE_NONE; + self.nextthink = -1; + self.model = ""; +}; + +/* +================ +monster_setalpha + +Sets relative alpha of monster in skill 4 mode. +================ +*/ +void(float a) monster_setalpha = +{ + if (skill < 4 || self.classname == "monster_hellfish") + { + self.alpha = 1.0; + return; + } + + if (skill >= 5) + { + // randomly forget enemy, this makes monsters randomly return to their normal ghostlike state + if (a == 0) + if (self.enemy) + if (random() < 0.1) + self.enemy = world; + // randomly blink (playing the same alarming sound as if attacking) + if (self.enemy == world) + { + a = 0; + if (time >= 0.3) // don't blink during the init process because it might become permanent + if (random() < 0.005) + { + // blink for an instant, this causes the appear sound, alarming the player as if under attack + sound(self, CHAN_AUTO, "wizard/wsight.wav", 1, ATTN_NORM); + a = 1; + } + } + // if ghosted, become non-solid and immune to damage + if (a <= 0 || self.enemy == world) + { + self.solid = SOLID_NOT; + self.takedamage = DAMAGE_NO; + } + else + { + // if unghosting, make sure we have an enemy, otherwise stay ghosted (even if blinking) so we can't be shot while blinking + if (self.solid != SOLID_SLIDEBOX) + sound(self, CHAN_AUTO, "wizard/wsight.wav", 1, ATTN_NORM); + self.solid = SOLID_SLIDEBOX; + self.takedamage = DAMAGE_AIM; + } + } + self.alpha = SKILL4_MINALPHA + (1 - SKILL4_MINALPHA) * bound(0, a, 1); +}; + +/* +================ +monster_death_use + +When a mosnter dies, it fires all of its targets with the current +enemy as activator. +================ +*/ +void() monster_death_use = +{ +// fall to ground + if (self.flags & FL_FLY) + self.flags = self.flags - FL_FLY; + if (self.flags & FL_SWIM) + self.flags = self.flags - FL_SWIM; + + if (!self.target) + return; + + activator = self.enemy; + SUB_UseTargets (); +}; + + +void() monsterinwall = +{ + local entity e; + if (!cvar("developer")) + return; + // this is handy for level designers, + // puts a spikey ball where the error is... + e = spawn(); + setorigin(e, self.origin); + setmodel (e, "progs/star.mdl"); + e.movetype = MOVETYPE_NONE; + e.solid = SOLID_NOT; + e.think = SUB_Null; + e.nextthink = -1; +}; + +//============================================================================ + +void() walkmonster_start_go = +{ + self.origin_z = self.origin_z + 1; // raise off floor a bit + + tracebox(self.origin, self.mins, self.maxs, self.origin, TRUE, self); + if (trace_startsolid) + { + dprint("walkmonster in wall at: "); + dprint(vtos(self.origin)); + dprint("\n"); + monsterinwall(); + droptofloor(); + } + else + { + droptofloor(); + if (!walkmove(0,0)) + { + dprint("walkmonster in wall at: "); + dprint(vtos(self.origin)); + dprint("\n"); + monsterinwall(); + } + } + + //self.cantrigger = TRUE; + + self.takedamage = DAMAGE_AIM; + + self.ideal_yaw = self.angles * '0 1 0'; + if (!self.yaw_speed) + self.yaw_speed = 20; + self.view_ofs = '0 0 25'; + self.use = monster_use; + + self.flags = self.flags | FL_MONSTER; + + if (monsterwander) + self.spawnflags = self.spawnflags | MONSTER_WANDER; + + if (self.target) + { + self.goalentity = self.movetarget = find(world, targetname, self.target); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + if (!self.movetarget) + { + dprint("Monster can't find target at "); + dprint(vtos(self.origin)); + dprint("\n"); + } + // this used to be an objerror + if (self.movetarget.classname == "path_corner") + self.th_walk (); + else + { + if ((self.spawnflags & MONSTER_WANDER) && (!self.monsterawaitingteleport) && (self.spawnflags & 3) == 0 && (world.model != "maps/e1m7.bsp")) + { + monster_spawnwanderpath(); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + self.th_walk (); + } + else + { + self.pausetime = 99999999; + self.th_stand (); + } + } + } + else + { + if ((self.spawnflags & MONSTER_WANDER) && (!self.monsterawaitingteleport) && (self.spawnflags & 3) == 0 && (world.model != "maps/e1m7.bsp")) + { + monster_spawnwanderpath(); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + self.th_walk (); + } + else + { + self.pausetime = 99999999; + self.th_stand (); + } + } + +// spread think times so they don't all happen at same time + self.nextthink = self.nextthink + random()*0.5 + 0.1; + self.iscreature = TRUE; + + force_retouch = 2; // mainly to detect teleports + + monster_appearsetup(); +}; + + +void() walkmonster_start = +{ + self.candrown = 1; // this is turned off by some monsters like zombies + // delay drop to floor to make sure all doors have been spawned + // spread think times so they don't all happen at same time + self.nextthink = time + random()*0.5 + 0.3; + self.think = walkmonster_start_go; + total_monsters = total_monsters + 1; + self.bot_attack = TRUE; + self.frags = 2; // actually just used to get havocbots to attack it... + self.bleedfunc = genericbleedfunc; + self.ismonster = TRUE; + + monster_setalpha (0); +}; + + + +void() flymonster_start_go = +{ + self.takedamage = DAMAGE_AIM; + + self.ideal_yaw = self.angles * '0 1 0'; + if (!self.yaw_speed) + self.yaw_speed = 10; + self.view_ofs = '0 0 25'; + self.use = monster_use; + + self.flags = self.flags | FL_FLY; + self.flags = self.flags | FL_MONSTER; + + if (!walkmove(0,0)) + { + dprint("flymonster in wall at: "); + dprint(vtos(self.origin)); + dprint("\n"); + monsterinwall(); + } + + //self.cantrigger = TRUE; + + if (monsterwander) + self.spawnflags = self.spawnflags | MONSTER_WANDER; + + if (self.target) + { + self.goalentity = self.movetarget = find(world, targetname, self.target); + if (!self.movetarget) + { + dprint("Monster can't find target at "); + dprint(vtos(self.origin)); + dprint("\n"); + } + // this used to be an objerror + if (self.movetarget.classname == "path_corner") + self.th_walk (); + else + { + if ((self.spawnflags & MONSTER_WANDER) && (!self.monsterawaitingteleport) && (self.spawnflags & 3) == 0 && (world.model != "maps/e1m7.bsp")) + { + monster_spawnwanderpath(); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + self.th_walk (); + } + else + { + self.pausetime = 99999999; + self.th_stand (); + } + } + } + else + { + if ((self.spawnflags & MONSTER_WANDER) && (!self.monsterawaitingteleport) && (self.spawnflags & 3) == 0 && (world.model != "maps/e1m7.bsp")) + { + monster_spawnwanderpath(); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + self.th_walk (); + } + else + { + self.pausetime = 99999999; + self.th_stand (); + } + } + self.iscreature = TRUE; + + force_retouch = 2; // mainly to detect teleports + + monster_appearsetup(); +}; + +void() flymonster_start = +{ + self.candrown = 1; + // spread think times so they don't all happen at same time + self.nextthink = time + random()*0.5 + 0.1; + self.think = flymonster_start_go; + total_monsters = total_monsters + 1; + self.bot_attack = TRUE; + self.frags = 2; // actually just used to get havocbots to attack it... + self.bleedfunc = genericbleedfunc; + self.ismonster = TRUE; + + monster_setalpha (0); +}; + + +void() swimmonster_start_go = +{ + if (deathmatch) + { + remove(self); + return; + } + + //self.cantrigger = TRUE; + + self.takedamage = DAMAGE_AIM; + + self.ideal_yaw = self.angles * '0 1 0'; + if (!self.yaw_speed) + self.yaw_speed = 10; + self.view_ofs = '0 0 10'; + self.use = monster_use; + + self.flags = self.flags | FL_SWIM; + self.flags = self.flags | FL_MONSTER; + + if (monsterwander) + self.spawnflags = self.spawnflags | MONSTER_WANDER; + + if (self.target) + { + self.goalentity = self.movetarget = find(world, targetname, self.target); + if (!self.movetarget) + { + dprint("Monster can't find target at "); + dprint(vtos(self.origin)); + dprint("\n"); + } + // this used to be an objerror + if (self.movetarget.classname == "path_corner") + self.th_walk (); + else + { + if ((self.spawnflags & MONSTER_WANDER) && (!self.monsterawaitingteleport) && (self.spawnflags & 3) == 0 && (world.model != "maps/e1m7.bsp")) + { + monster_spawnwanderpath(); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + self.th_walk (); + } + else + { + self.pausetime = 99999999; + self.th_stand (); + } + } + } + else + { + if ((self.spawnflags & MONSTER_WANDER) && (!self.monsterawaitingteleport) && (self.spawnflags & 3) == 0 && (world.model != "maps/e1m7.bsp")) + { + monster_spawnwanderpath(); + self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin); + self.th_walk (); + } + else + { + self.pausetime = 99999999; + self.th_stand (); + } + } + self.iscreature = TRUE; + + force_retouch = 2; // mainly to detect teleports + + monster_appearsetup(); +}; + +void() swimmonster_start = +{ + // spread think times so they don't all happen at same time + self.candrown = 0; + self.nextthink = time + random()*0.5 + 0.1; + self.think = swimmonster_start_go; + total_monsters = total_monsters + 1; + self.bot_attack = TRUE; + self.frags = 2; // actually just used to get havocbots to attack it... + self.bleedfunc = genericbleedfunc; + self.ismonster = TRUE; + + monster_setalpha(0); +}; + +void(vector org, float bodydamage, float armordamage, vector force, float damgtype) genericbleedfunc = +{ + local vector v; + v = '0 0 0' - force * 0.05; + if (armordamage > 0) + te_spark(org, v, armordamage * 3); + if (bodydamage > 0) + te_blood(org, v, bodydamage); +} diff --git a/data/qcsrc/server/monsters/mode_management.qc b/data/qcsrc/server/monsters/mode_management.qc new file mode 100644 index 000000000..b9ebf9183 --- /dev/null +++ b/data/qcsrc/server/monsters/mode_management.qc @@ -0,0 +1,526 @@ + +string(float c) colorname = +{ + // yikes, the quake color set is HARD to describe + // many are easy, but, uh, 2 browns??? + // 2 purples??? + // that 'pink' is hard to classify + // I think 'biege' is a fairly good name for color 10 + // oh well, gotta do all the color names... + if (c == 0) return "white"; + else if (c == 1) return "brown"; + else if (c == 2) return "lightblue"; + else if (c == 3) return "green"; + else if (c == 4) return "red"; + else if (c == 5) return "lighterbrown"; + else if (c == 6) return "orange"; + else if (c == 7) return "pink"; + else if (c == 8) return "purple"; + else if (c == 9) return "redishpurple"; + else if (c == 10) return "biege"; + else if (c == 11) return "aqua"; + else if (c == 12) return "yellow"; + else if (c == 13) return "blue"; + else if (c == 14) return "flamingorange"; + else if (c == 15) return "psychadelic"; + else return "INVALID COLOR"; +}; + +float mode_shirtmustmatchpants; +float mode_numteams; +float mode_allowedteams[17]; +float mode_teamcount[17]; +float mode_teamscore[17]; + +void() mode_initallowedteams = +{ + local float c; + c = 0; + while(c < 17) + { + mode_allowedteams[c] = FALSE; + c = c + 1; + } + mode_allowedteams[5] = TRUE; // red + mode_allowedteams[14] = TRUE; // blue + if (deathmatch == DM_ELIM + || deathmatch == DM_ONEVSALL + || deathmatch == DM_CTF_2TEAM + || deathmatch == DM_DOMINATION + || deathmatch == DM_SUPERDOMINATION) + mode_numteams = 2; + else if (deathmatch == DM_CTF_3TEAM) + { + mode_numteams = 3; + mode_allowedteams[13] = TRUE; // yellow + } + else + { + mode_numteams = 16; + c = 1; + while(c < 17) + { + mode_allowedteams[c] = TRUE; + c = c + 1; + } + } +}; + +float(float t) validteam = +{ + return mode_allowedteams[t]; +}; + +float() weakestteam = +{ + local float bestteam; + local float bestteamcount; + local float headcount; + local float c; + bestteam = -1; + bestteamcount = 0; + c = 1; + while (c < 17) + { + if (mode_allowedteams[c]) + { + headcount = mode_teamcount[c]; + if (bestteamcount > headcount || bestteam == -1) + { + bestteamcount = headcount; + bestteam = c; + } + } + c = c + 1; + } + return bestteam; +}; + +void() updateteams = +{ + local entity head; + local float c; + c = 1; + while (c < 17) + { + mode_teamcount[c] = 0; + mode_teamscore[c] = 0; + c = c + 1; + } + c = 0; + head = nextent(world); + while (c < maxclients) + { + mode_teamcount[head.team] = mode_teamcount[head.team] + 1; + mode_teamscore[head.team] = mode_teamscore[head.team] + head.frags; + c = c + 1; + head = nextent(head); + } +}; + +float(float p) checkteamcolor = +{ + if (!validteam(p + 1)) + p = weakestteam() - 1; + return p; +}; + +void(float c) SV_ChangeTeam = +{ + local float pants, shirt, old; + old = self.clientcolors & 15; + if (c >= 0) + { + pants = c & 15; + shirt = (c / 16) & 15; + } + else + { + pants = -1; + shirt = -1; + } + pants = checkteamcolor(pants); + if (mode_shirtmustmatchpants || shirt < 0) + shirt = pants; + setcolor(self, pants + shirt * 16); + if (pants != old && old >= 0 && teamplay && deathmatch) + { + T_Damage(self, self, self, 0, 0, " changed teams", DT_TELEFRAG, self.origin, '0 0 0', Obituary_Generic); + self.frags = 0; + PutClientInServer (); + } +}; + +void() checkinvalidteam = +{ + // call SV_ChangeTeam to trigger the weakestteam change + if (!validteam(self.team)) + SV_ChangeTeam(self.team - 1); +}; + + +string dmmessage; + +void(string m) setdm = +{ + dmmessage = m; + if (cvar_string("deathmatch") != m) + cvar_set("deathmatch", m); +} + +void(string m) setteamplay = +{ + dmmessage = m; + if (cvar_string("teamplay") != m) + cvar_set("teamplay", m); +} + +void() mode_updatecvars = +{ + local float dm, tp; + dm = cvar("deathmatch"); + tp = cvar("teamplay"); + // now set deathmatch cvar + if (dm == 0) setdm("0?Dark Places - Coop"); + else if (dm == 1) setdm("1?Dark Places - Deathmatch"); + else if (dm == 2) setdm("2?Dark Places - Deathmatch 2 (can only pickup gun once)"); + else if (dm == 3) setdm("3?Dark Places - Deathmatch 3 (quick ammo respawn)"); + else if (dm == 5) setdm("5?Dark Places - Frag Fest (spawn with full pack)"); +// else if (dm == 6) setdm("6?Dark Places - Random Weapons (spawn with 2 random weapons)"); // removed + else if (dm == 7) setdm("7?Dark Places - Monsters"); +// else if (dm == 8) setdm("8?Dark Places - Elimination"); +// else if (dm == 9) setdm("9?Dark Places - Kill The Leader Mode"); + else if (dm == 10) setdm("10?Dark Places - Capture The Flag - 2 Team"); + else if (dm == 11) setdm("11?Dark Places - Capture The Flag - 3 Team"); + else if (dm == 12) setdm("12?Dark Places - Domination"); + else if (dm == 13) setdm("13?Dark Places - Monster Capture The Flag - 2 Team"); + else if (dm == 14) setdm("14?Dark Places - Super Domination"); + else if (dm == 30) setdm("30?Dark Places - Role Playing Game"); + else setdm("1?Dark Places - Deathmatch"); + + // now set teamplay cvar + if (dm == 0) setteamplay("4?Dark Places - Coop (Can't hurt other players)"); + //else if (dm == 8) setteamplay("3?Dark Places - Elimination"); + //else if (dm == 9) setteamplay("3?Dark Places - Kill The Leader"); + else if (dm == 10) setteamplay("3?Dark Places - Capture The Flag - 2 Team"); + else if (dm == 11) setteamplay("3?Dark Places - Capture The Flag - 3 Team"); + else if (dm == 12) setteamplay("3?Dark Places - Domination"); + else if (dm == 13) setteamplay("3?Dark Places - Monster Capture The Flag - 2 Team"); + else + { + if (tp == 0) setteamplay("0?Dark Places - No Teamplay"); + else if (tp == 1) setteamplay("1?Dark Places - No team damage"); + else if (tp == 2) setteamplay("2?Dark Places - Can hurt anyone"); + else if (tp == 3) setteamplay("3?Dark Places - No team damage, but can hurt self"); + else setteamplay("0?Dark Places - No Teamplay"); + } +}; + +float nextcvarupdate; +void() deathmatch7update; +void() modeupdate = +{ + if (time > nextcvarupdate) + { + nextcvarupdate = time + 1; + mode_updatecvars(); + } + deathmatch7update(); +}; + +// true if items should respawn +float itemrespawn; +// when the next monster spawning check will occur in deathmatch 7 mode +float spawnchecktime; + +void() precachemonsters; +void() superdomination_precache; +void() modesetup = +{ + mode_shirtmustmatchpants = deathmatch >= DM_TEAM_MODS_START && deathmatch < DM_TEAM_MODS_END; + mode_initallowedteams(); + + itemrespawn = cvar("deathmatch") + cvar("coop"); + + // don't spawn any monsters until 15 seconds + spawnchecktime = 15; + if (deathmatch == 7 || cvar("spawnmonsters") >= 1) + precachemonsters(); + + superdomination_precache(); +}; + +float monsterspawn; +void() spawnmonster_think = +{ + //local float c; + local void() sfunc; + self.nextthink = time; + if (time > self.cnt) + { + remove(self); + return; + } + if (vlen(self.velocity) > 5) + return; // try again later + + //if (!(self.flags & FL_FLY)) + // droptofloor(); + // don't spawn if something is in the way + /* + // walk around a lot + if (walkmove(0,0)) + { + if (self.lefty > 0) + { + c = 100; + self.lefty = self.lefty - 1; + self.angles = '0 0 0'; + while(c > 0) + { + c = c - 1; + if (!walkmove(self.angles_y, 16)) + self.angles_y = random() * 360; + } + self.angles = '0 0 0'; + return; + } + } + */ + // don't spawn if something is in the way + if (!walkmove(0,0)) + { + self.lefty = 10; + setorigin(self, self.dest); + self.flags = self.flags - (self.flags & FL_ONGROUND); + self.velocity = randomvec() * 700 + '0 0 1000'; + return; + } + newmis = findchain(classname, "player"); + while (newmis) + { + if (vlen(newmis.origin - self.origin) < 300) + return; + newmis = newmis.chain; + } + + if (self.netname == "monster_fish") + { + if (pointcontents(self.origin) != CONTENT_WATER) + { + remove(self); + return; + } + } + + // spawn in + self.movetype = MOVETYPE_NONE; + self.solid = SOLID_NOT; + self.velocity = '0 0 0'; + self.flags = 0; + self.model = ""; + self.modelindex = 0; + setorigin(self, self.origin); + self.angles = '0 360 0' * random(); + self.classname = self.netname; + self.netname = ""; + self.cnt = 0; + self.think = SUB_Remove; + sfunc = self.th_run; + self.th_run = SUB_Null; + te_teleport(self.origin); + monsterspawn = TRUE; + sfunc(); + monsterspawn = FALSE; +}; + +void(vector org, float c1, float c2, string cname, void() spawnfunc, vector m1, vector m2) spawnmonster = +{ + local float c; + c = (c2 - c1) * random() + c1; + c = rint(c); + while (c > 0) + { + c = c - 1; + + newmis = spawn(); + newmis.cnt = time + 10; + if (cname == "monster_wizard") + newmis.cnt = time + 2; + newmis.lefty = 10; + newmis.dest = org; + newmis.classname = "spawningmonster"; + newmis.netname = cname; + newmis.solid = SOLID_TRIGGER; + newmis.movetype = MOVETYPE_TOSS; + newmis.flags = FL_MONSTER; // make this count as a monster even though it hasn't spawned in yet + newmis.velocity = randomvec() * 700 + '0 0 1000'; + newmis.th_run = spawnfunc; + newmis.think = spawnmonster_think; + newmis.nextthink = time + random() * 0.5 + 0.3; + setorigin(newmis, org); + setmodel(newmis, "progs/s_explod.spr"); + setsize(newmis, m1, m2); + } +}; + +void() monster_army; +void() monster_demon1; +void() monster_dog; +void() monster_enforcer; +void() monster_hell_knight; +void() monster_knight; +void() monster_ogre; +void() monster_shalrath; +void() monster_shambler; +void() monster_tarbaby; +void() monster_wizard; +void() monster_zombie; +void() monster_fish; +void() monster_hellfish; + +void() spawnmonsters = +{ + local float r; + local vector org; + local entity head, e; + head = findchain(classname, "info_player_deathmatch"); + if (head == world) + { + head = findchain(classname, "info_player_coop"); + if (head == world) + { + head = findchain(classname, "info_player_start"); + if (head == world) + return; + } + } + + // count the spawn points + r = 0; + e = head; + while (e) + { + r = r + 1; + e = e.chain; + } + + // pick a random one + r = random() * r; + e = head; + while (r > 0) + { + r = r - 1; + org = e.origin; + e = e.chain; + } + + // pick a type of monster + if (cvar("registered")) + { + r = floor(random() * 13); + if (r > 12) + r = 12; + } + else + { + r = floor(random() * 8); + if (r > 7) + r = 7; + } + if (r == 0) spawnmonster(org, 5, 10, "monster_army" , monster_army , '-16 -16 -24', '16 16 32'); + else if (r == 1) spawnmonster(org, 3, 6, "monster_demon1" , monster_demon1 , '-32 -32 -24', '32 32 64'); + else if (r == 2) spawnmonster(org, 6, 12, "monster_dog" , monster_dog , '-16 -16 -24', '16 16 32'); + else if (r == 3) spawnmonster(org, 6, 12, "monster_knight" , monster_knight , '-16 -16 -24', '16 16 32'); + else if (r == 4) spawnmonster(org, 3, 6, "monster_ogre" , monster_ogre , '-32 -32 -24', '32 32 64'); + else if (r == 5) spawnmonster(org, 1, 1, "monster_shambler" , monster_shambler , '-32 -32 -24', '32 32 64'); + else if (r == 6) spawnmonster(org, 6, 10, "monster_wizard" , monster_wizard , '-16 -16 -24', '16 16 32'); + else if (r == 7) spawnmonster(org, 8, 16, "monster_zombie" , monster_zombie , '-16 -16 -24', '16 16 32'); + else if (r == 8) spawnmonster(org, 4, 8, "monster_enforcer" , monster_enforcer , '-16 -16 -24', '16 16 32'); + else if (r == 9) spawnmonster(org, 4, 8, "monster_hell_knight", monster_hell_knight, '-16 -16 -24', '16 16 32'); + else if (r == 10) spawnmonster(org, 1, 3, "monster_shalrath" , monster_shalrath , '-32 -32 -24', '32 32 64'); + else if (r == 11) spawnmonster(org, 10, 15, "monster_tarbaby" , monster_tarbaby , '-16 -16 -24', '16 16 32'); + else if (r == 12) spawnmonster(org, 4, 8, "monster_fish" , monster_fish , '-16 -16 -24', '16 16 32'); +}; + +float monstersprecached; +void() precachemonster_army; +void() precachemonster_demon1; +void() precachemonster_dog; +void() precachemonster_enforcer; +void() precachemonster_hell_knight; +void() precachemonster_knight; +void() precachemonster_ogre; +void() precachemonster_shalrath; +void() precachemonster_shambler; +void() precachemonster_tarbaby; +void() precachemonster_wizard; +void() precachemonster_zombie; +void() precachemonster_fish; + +void() precachemonsters = +{ + precachemonster_army(); + precachemonster_demon1(); + precachemonster_dog(); + precachemonster_knight(); + precachemonster_ogre(); + precachemonster_shambler(); + precachemonster_wizard(); + precachemonster_zombie(); + if (cvar("registered")) + { + precachemonster_enforcer(); + precachemonster_hell_knight(); + precachemonster_shalrath(); + precachemonster_tarbaby(); + precachemonster_fish(); + } + monstersprecached = TRUE; +}; + +float spawnedexitmonsters; +void() deathmatch7update = +{ + local entity e; + local float f, monster_count, monsters; + if (skill >= 5) + if (!deathmatch) + { + if (!spawnedexitmonsters) + if (time >= 2) + { + spawnedexitmonsters = TRUE; + e = find(world, classname, "trigger_changelevel"); + while (e) + { + spawnmonster(e.origin + (e.mins + e.maxs) * 0.5, 8, 8, "monster_hellfish", monster_hellfish, '-16 -16 -24', '16 16 32'); + e = find(e, classname, "trigger_changelevel"); + } + } + return; + } + if (time < spawnchecktime) + return; + if (!monstersprecached) + return; + spawnchecktime = time + 0.2; + monsters = 0; + if (deathmatch == 7) + monsters = 50; + f = cvar("spawnmonsters"); + if (f >= 1) + monsters = f; + if (monsters < 1) + return; + monster_count = 0; + e = findchainflags(flags, FL_MONSTER); + while (e) + { + monster_count = monster_count + 1; + e = e.chain; + } + if (monster_count >= monsters) + return; + spawnmonsters(); +} diff --git a/data/qcsrc/server/progs.src b/data/qcsrc/server/progs.src index 094a6eaf5..65c589a53 100644 --- a/data/qcsrc/server/progs.src +++ b/data/qcsrc/server/progs.src @@ -157,3 +157,8 @@ target_spawn.qc func_breakable.qc ../common/items.qc + +monsters/defs.qc +monsters/fight.qc +monsters/ai.qc +monsters/m_monsters.qc diff --git a/data/qcsrc/server/t_teleporters.qc b/data/qcsrc/server/t_teleporters.qc index 1d0ab3e9c..622e1615f 100644 --- a/data/qcsrc/server/t_teleporters.qc +++ b/data/qcsrc/server/t_teleporters.qc @@ -1,9 +1,47 @@ +void tdeath(entity player, entity teleporter, entity telefragger, vector telefragmin, vector telefragmax) +{ + entity head; + vector deathmin; + vector deathmax; + float deathradius; + deathmin = player.absmin; + deathmax = player.absmax; + if(telefragmin != telefragmax) + { + if(deathmin_x > telefragmin_x) deathmin_x = telefragmin_x; + if(deathmin_y > telefragmin_y) deathmin_y = telefragmin_y; + if(deathmin_z > telefragmin_z) deathmin_z = telefragmin_z; + if(deathmax_x < telefragmax_x) deathmax_x = telefragmax_x; + if(deathmax_y < telefragmax_y) deathmax_y = telefragmax_y; + if(deathmax_z < telefragmax_z) deathmax_z = telefragmax_z; + } + deathradius = max(vlen(deathmin), vlen(deathmax)); + for(head = findradius(player.origin, deathradius); head; head = head.chain) + if(head != player) + if(head.takedamage) + if(boxesoverlap(deathmin, deathmax, head.absmin, head.absmax)) + { + if ((player.classname == "player") && (player.health >= 1)) + Damage (head, teleporter, telefragger, 10000, DEATH_TELEFRAG, head.origin, '0 0 0'); + else if (telefragger.health < 1) // corpses gib + Damage (head, teleporter, telefragger, 10000, DEATH_TELEFRAG, head.origin, '0 0 0'); + else // dead bodies and monsters gib themselves instead of telefragging + Damage (telefragger, teleporter, telefragger, 10000, DEATH_TELEFRAG, telefragger.origin, '0 0 0'); + } +} + +void spawn_tdeath(vector v0, entity e, vector v) +{ + tdeath(e, e, e, '0 0 0', '0 0 0'); +} + .entity pusher; void TeleportPlayer(entity teleporter, entity player, vector to, vector to_angles, vector to_velocity, vector telefragmin, vector telefragmax) { entity head; entity oldself; entity telefragger; + vector from; if(teleporter.owner) telefragger = teleporter.owner; @@ -22,6 +60,7 @@ void TeleportPlayer(entity teleporter, entity player, vector to, vector to_angle // Relocate the player // assuming to allows PL_MIN to PL_MAX box and some more + from = player.origin; setorigin (player, to); player.angles = to_angles; player.fixangle = TRUE; @@ -29,37 +68,8 @@ void TeleportPlayer(entity teleporter, entity player, vector to, vector to_angle if(player.classname == "player") { - // Kill anyone else in the teleporter box (NO MORE TDEATH) if(player.takedamage && player.deadflag == DEAD_NO && !g_race) - { - vector deathmin; - vector deathmax; - float deathradius; - deathmin = player.absmin; - deathmax = player.absmax; - if(telefragmin != telefragmax) - { - if(deathmin_x > telefragmin_x) deathmin_x = telefragmin_x; - if(deathmin_y > telefragmin_y) deathmin_y = telefragmin_y; - if(deathmin_z > telefragmin_z) deathmin_z = telefragmin_z; - if(deathmax_x < telefragmax_x) deathmax_x = telefragmax_x; - if(deathmax_y < telefragmax_y) deathmax_y = telefragmax_y; - if(deathmax_z < telefragmax_z) deathmax_z = telefragmax_z; - } - deathradius = max(vlen(deathmin), vlen(deathmax)); - for(head = findradius(player.origin, deathradius); head; head = head.chain) - if(head != player) - if(head.takedamage) - if(boxesoverlap(deathmin, deathmax, head.absmin, head.absmax)) - { - if ((player.classname == "player") && (player.health >= 1)) - Damage (head, teleporter, telefragger, 10000, DEATH_TELEFRAG, head.origin, '0 0 0'); - else if (telefragger.health < 1) // corpses gib - Damage (head, teleporter, telefragger, 10000, DEATH_TELEFRAG, head.origin, '0 0 0'); - else // dead bodies and monsters gib themselves instead of telefragging - Damage (telefragger, teleporter, telefragger, 10000, DEATH_TELEFRAG, telefragger.origin, '0 0 0'); - } - } + tdeath(player, teleporter, telefragger, telefragmin, telefragmax); // hide myself a tic player.effects = player.effects | EF_NODRAW; -- 2.39.2