Frikbot Item desirability

Discuss Artificial Intelligence and Bot programming.
Dr. Shadowborg
InsideQC Staff
Posts: 1120
Joined: Sat Oct 16, 2004 3:34 pm

Re: Frikbot Item desirability

Post by Dr. Shadowborg »

Lightning Hunter wrote:I noticed a very small issue with the Grenade Launcher aiming. The bots don't always seem to fire when their target is moving a lot. They will point at the target and attempt to aim, but never actually fire. I just saw a bot with a SG kill another bot with a GL, because the bot with the GL never fired once. He just kept trying to find the correct aiming. This is not a huge issue, but I think maybe the bots should be forced to fire, even if they aren't 100% sure of their aim. If there is no easy fix for this, then don't worry about it. The whole GL aim code doesn't need to be rewritten or anything to fix this, if that much work needs to be done.
This is my bad. :oops:

Do this:

In bot_ai.qc, within bot_angle_set in the grenade block, add self.dontshoot = FALSE; so that it looks like the block below:

Code: Select all

		    if(self.weapon == IT_GRENADE_LAUNCHER) // using grenades? Aim properly then!
            {
			   local float realrange, flitime;
			   local vector grndir;

			   grndir = normalize(view - (self.origin + self.view_ofs));//normalize(self.enemy.origin - self.origin);
			   realrange = dist2*mathlib_cos(grndir_x);
			   flitime = realrange / self.missile_speed;
			  
               view = view + '0 0 -20'; // aim at feet
			   grndir = normalize(view - (self.origin + self.view_ofs));//normalize(self.enemy.origin - self.origin);
	    	   grndir = grndir * 600;
			   grndir_z = grndir_z + (sv_gravity*flitime) * 0.3;
			   view = normalize(grndir);
			   self.dontshoot = FALSE;
			}
Then, lower in the function, change the shot verification code to look like this: (adds a grenade launcher weapon exclusion)

Code: Select all

		 // DRS: Do a viability test on whether we actually should shoot or not
         if(self.enemy.solid != SOLID_BSP && self.weapon != IT_GRENADE_LAUNCHER)
	      {
		   traceline(self.origin+self.view_ofs, view, TRUE, self);
		   if(vlen(trace_endpos - (self.origin+self.view_ofs)) < (dist2 - 32))
		    self.dontshoot = TRUE;
		   else if(vlen(trace_endpos - (self.origin+self.view_ofs)) > (dist2 + 32))
		    self.dontshoot = TRUE;
		   else self.dontshoot = FALSE;
		  }
That takes care of the grenade bug you mentioned.

Moving on to fix the RL on doors thing, open bot_fight.qc, within bot_weapon_switch, change the lower block:

Code: Select all

		if ((self.ammo_rockets >= 1) && (it & 32) && (!(self.enemy.solid == SOLID_BSP && (vlen(realorigin(self.enemy) - self.origin) < 140)))) // DRS: Don't use RL when too close to doors / buttons
		{
			flag = 32;
			pulse = 7;
		}
Next, within bot_fight_style, add this right below the grenade button fix:

Code: Select all

// DRS: Don't shoot RL if too close to button or door
			if(self.enemy.solid == SOLID_BSP && self.weapon == IT_ROCKET_LAUNCHER && (vlen(org - self.origin) < 140))
		          bot_weapon_switch(foedist);
That should fix the rocketlauncher secret door / button issue.

I've been unable to duplicate the teleporter bug, so I dunno. I did see an occasional glitch that I figure could maybe be due to the excessive number of waypoints in that map.

I haven't noticed any "fixation on waypoint after death" thus far. Looking at the code, they *should* be clearing all their waypoints, enemies, and targets upon death / respawn.

Precision bug appears to maybe be your bad. On fragtwn4.bsp, I moved the precision waypoint (24qu is the distance threshold according to the code) away from the two waypoints that are involved with the rooftop jump, and bots appeared to make the jump just fine afterwards, so I'd assume that you're not supposed to put waypoints with precision flags directly in the path of critical jumps like that.

Soooo, I guess the next item on order is the distance spawning thing. I'll get back to you when I've got it working.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

I've been unable to duplicate the teleporter bug, so I dunno.
I have uploaded a new version of Dranzdm4 for you that only has about 5 waypoints near the teleporter in question going to the Red Armor, so you can reproduce the bug easier. To see the bug occur, just manually teleport a bot using the bot management menu to the waypoint shown in the screenshot here (the coordinates are also shown in the screenshot):
Image
Next, just view the bot and let him go. The bot is supposed to go through the teleporter to get the red armor. Instead, the bot should mess up and start randomly walking toward the mega health (there are no waypoints there, as you can see). Download the new waypoint file here (make sure you delete the old ones I sent you)
http://fbe.am/ul1
I did see an occasional glitch that I figure could maybe be due to the excessive number of waypoints in that map.
So far, I have not seen any bugs that have to do with the number of waypoints in a map, and I have observed the bots for thousands upon thousands of hours while waypointing both small and large maps. The only possible side effect is that bots take slightly longer to calculate a new path after they have collected an item (not even half a second longer, so it's no big deal). On the flipside of the coin, there are many downsides to having too few waypoints. You may have noticed that I have many waypoints in obscure areas that seem pointless, but they actually aren't. These waypoints exist in case a bot gets stuck somewhere off track that it isn't supposed to be. The waypoint tells the bot how to get back on path. Sometimes, bots can get snagged in corners during combat, so I put waypoints in corners to tell the bot how to get out. Otherwise, they will be stuck permanently until they are killed by another bot/player. Also, a lot of difficult jumps require several waypoints to set the bot up for the jump. This significantly increases their chances of making the jump without falling. Anyway, just thought I would explain what might seem like a chaotic waypoint system. My original waypointed maps had very few waypoints, until I realized that more waypoints = less mistakes with the Frikbots. I am currently going through my oldest works and adding more waypoints, so the bots can succeed more in their jumps and not ever get stuck.
I haven't noticed any "fixation on waypoint after death" thus far. Looking at the code, they *should* be clearing all their waypoints, enemies, and targets upon death / respawn.
Maybe it has something to do with the bot getting killed in the air with the RL or something? I have definitely seen this bug occur, but cannot pinpoint exaclty why it happens...
Precision bug appears to maybe be your bad. On fragtwn4.bsp, I moved the precision waypoint (24qu is the distance threshold according to the code) away from the two waypoints that are involved with the rooftop jump, and bots appeared to make the jump just fine afterwards, so I'd assume that you're not supposed to put waypoints with precision flags directly in the path of critical jumps like that.
I do not believe this is the case, if you will excuse my bluntness! I am referring to the jump from the lightning gun building to the lower building (hopefully we are talking about the same jump). If you observe how the waypoints are set up, there is a single waypoint with the precision flag that is supposed to slow down the bot to prepare him for the jump. Then, the bot is supposed to gain speed as he passes through 2 other waypoints to make the jump. These two waypoints are not marked for precision, so the bot is supposed to turn off precision mode long before making the jump. However, the bot will sometimes keep precision mode ON, causing a failed jump. Like I said before, I have sometimes seen the bots keep precision mode on for a looong time (even after they have passed through like 10-15 waypoints). Precision mode can sometimes get stuck until they have reached their current goal before they turn it off. I can't say why this happens however, and I'm not sure how I can help you further on this matter. :? Since this bug cannot be reproduced on a regular basis, I say ignore it until we can figure out what exactly is causing it.

Edit: Hey Shadowborg, are the bots coded to automatically turn off precision mode when they have touched a new waypoint without a precision flag?

Edit #2: I think I just made a breakthrough discovery with the precision bug! As it turns out, something with the precision flag is not being loaded correctly when the map is first loaded. Once waypoint editing mode is entered, the flag is fixed. For example, if I load Fragtwn4 and add a bot without going in to waypoint editing mode, the bot will display the precision bug and mess up on the building jump 100% of the time. However, if I go in to waypoint editing mode THEN add a bot, the bot will succeed in the jump 100% of the time and NEVER show the precision bug. All I have to do is go in to editing mode one time, and the bot will behave correctly on that jump the rest of the match, until I restart the map (even after exiting the editing mode). Shadowborg, this is why you thought you fixed the bug by moving around a few waypoints. Moving them around didn't fix them; it was merely the fact that you entered waypoint editing mode.

Here is a video showing what I mean. The first bot that messes up on the jump was before entering editing mode. The second bot that succeeds in the jump is after I enter editing mode. Now you just have to figure out how to get the flag to properly load without us having to load editing mode first. Perhaps this will also fix some other bugs?
https://www.youtube.com/watch?v=Y35doL9 ... CUl0WMPoXQ
Last edited by Lightning Hunter on Wed Sep 24, 2014 6:33 pm, edited 1 time in total.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

Sorry for double post, but my last post had too many edits. Make sure you read the edits I made in the previous post Shadowborg. They are very important!

Did I implement all your code correctly in bot_fight? I just want to make sure I didn't mess anything up.

Code: Select all

/***********************************************
*                                              *
*             FrikBot Fight Code               *
*      "Because I ain't no Ghandi code"        *
*                                              *
***********************************************/

/*

This program is in the Public Domain. My crack legal
team would like to add:

RYAN "FRIKAC" SMITH IS PROVIDING THIS SOFTWARE "AS IS"
AND MAKES NO WARRANTY, EXPRESS OR IMPLIED, AS TO THE
ACCURACY, CAPABILITY, EFFICIENCY, MERCHANTABILITY, OR
FUNCTIONING OF THIS SOFTWARE AND/OR DOCUMENTATION. IN
NO EVENT WILL RYAN "FRIKAC" SMITH BE LIABLE FOR ANY
GENERAL, CONSEQUENTIAL, INDIRECT, INCIDENTAL,
EXEMPLARY, OR SPECIAL DAMAGES, EVEN IF RYAN "FRIKAC"
SMITH HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES, IRRESPECTIVE OF THE CAUSE OF SUCH DAMAGES. 

You accept this software on the condition that you
indemnify and hold harmless Ryan "FrikaC" Smith from
any and all liability or damages to third parties,
including attorney fees, court costs, and other
related costs and expenses, arising out of your use
of this software irrespective of the cause of said
liability. 

The export from the United States or the subsequent
reexport of this software is subject to compliance
with United States export control and munitions
control restrictions. You agree that in the event you
seek to export this software, you assume full
responsibility for obtaining all necessary export
licenses and approvals and for assuring compliance
with applicable reexport restrictions. 

Any reproduction of this software must contain
this notice in its entirety. 

*/

.entity avoid;

float(entity e) bot_size_player =
{
	local float sz;

	sz = e.health + e.armorvalue * e.armortype;
	if (e.weapon == 32)
		sz = sz + 60;
	else if (e.weapon == 64)
		sz = sz + 60;
	else if (e.weapon == 16)
		sz = sz + 50;
	else if (e.weapon == 8)
		sz = sz + 50;
	else if (e.weapon == 4)
		sz = sz + 40;
	else if (e.weapon == 2)
		sz = sz + 40;
	else if (e.weapon == 1)
		sz = sz + 10;
	else if (e.weapon == 4096)
		sz = sz - 50;
	if (e.items & 4194304) // Quad
		sz = sz + 200;
	if (e.items & 1048576) // Invul
		sz = sz + 300;
	if (e.items & 524288) // Invis
		sz = sz + 250;
	return sz;
};

void() bot_dodge_stuff =
{
	local entity foe;
	local float avdist, foesz, flen, tsz;
	local vector v;

	if (waypoint_mode > WM_LOADED)
		return;

	self.avoid = world;


	if (self.enemy)
	{
		v = self.origin - realorigin(self.enemy);

		foesz = bot_size_player(self.enemy);
		foesz = foesz + vlen(v) * 0.5;
	}
	else 
		foesz = 9999999;
	avdist = 256;

	foe = find(world, classname, "grenade");
	while(foe)
	{
		flen = vlen(foe.origin - self.origin);
		if (flen < avdist)
		{
			avdist = flen;
			self.avoid = foe;
		}
		foe = find(foe, classname, "grenade");
	}
	if (!self.avoid)
	{
		foe = find(world, classname, "missile");
		while(foe)
		{
			if (foe.owner != self)
			{
				flen = vlen(foe.origin - self.origin);
				if (flen < avdist)
				{
					avdist = flen;
					self.avoid = foe;
				}
			}
			foe = find(foe, classname, "missile");
		}
		if (!self.avoid)
		{
			foe = find(world, classname, "spike");
			while(foe)
			{
				if (foe.owner != self)
				{
					flen = vlen(foe.origin - self.origin);
					if (flen < avdist)
					{
						avdist = flen;
						self.avoid = foe;
					}
				}
				foe = find(foe, classname, "spike");
			}
		}
	}
	if (coop)
	{
		if (!self.enemy)
		{
			foe = findradius(self.origin, 9999);
			while(foe)
			{
				if(foe.flags & FL_MONSTER)
				{
					if(foe.health > 0)
					{
						tsz = bot_size_player(foe) + vlen(foe.origin - self.origin) * 0.5;
						if (tsz < foesz)
						{
							if (fisible(foe))
							{
								self.enemy = foe;
								foesz = tsz;
							}
						}
					}
				}
				foe = foe.chain;
			}
		}		
	}
	else
	{
		foe = player_head;
		while(foe)
		{
			if(foe != self)
			{
				if (foe.modelindex != 0)
				{
					if (foe.health > 0)
					{
						if (!(teamplay && self.team == foe.team))
						{
							tsz = bot_size_player(foe) + vlen(foe.origin - self.origin) * 0.5;
							if (tsz < foesz)
							{
								if (fov(foe) || foe.b_sound > time || self.b_skill == 3)
								{
									if (fisible(foe))
									{
										self.enemy = foe;
										foesz = tsz;
									}
								}
							}
						}
					}
				}
			}
			foe = foe._next;
		}
	}
};




/*
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

weapon_range

_x "sweet spot range" - try to maintain this range if possible
_y minimum range bot can be to be effective (rl/gl) (move away)
_z maximum range bot can be to be effective (lg/axe) (move in)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
*/

vector(float wep) weapon_range =
{
 local vector returnvalue, grendist;
 local float heightdiff;
 
   if (wep == 4096) // IT_AXE
    returnvalue = '48 0 64';
   else if (wep == 1) // IT_SHOTGUN
    returnvalue = '128 8 3000';
   else if (wep == 2) // IT_SUPER_SHOTGUN
    returnvalue = '128 0 3000';
   else if (wep == 4) // IT_NAILGUN
    returnvalue = '180 0 3000';
   else if (wep == 8) // IT_SUPER_NAILGUN
    returnvalue = '180 0 3000';
   else if (wep == 16) // IT_GRENADE_LAUNCHER
     returnvalue = '180 48 600';
   else if (wep == 32) // IT_ROCKET_LAUNCHER
    returnvalue = '180 48 3000';
   else if (wep == 64) // IT_LIGHTNING
    returnvalue = '350 0 512';
 
  return returnvalue;
};

/*
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

bot_weapon_switch

Pick a weapon based on range / ammo

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
*/

void(float brange) bot_weapon_switch =
{
	local	float	it, flag, pulse;
	local vector v;

	it = self.items & 127;

	while(it)
	{
      if ((self.ammo_rockets >= 1) && (it & 32) && (!(self.enemy.solid == SOLID_BSP && (vlen(realorigin(self.enemy) - self.origin) < 140)))) // DRS: Don't use RL when too close to doors / buttons
      {
         flag = 32;
         pulse = 7;
      }
		else if (self.waterlevel <= 1 && self.ammo_cells >= 1 && (it & 64))
		{
			flag = 64;
			pulse = 8;
		}
		else if(self.ammo_nails >= 2 && (it & 8))
		{
			flag = 8;
			pulse = 5;
		}
		else if ((self.ammo_rockets >= 1) && (it & 16) && (self.enemy.solid != SOLID_BSP || self.enemy == world)) // DRS: Grenades don't explode on buttons...
		{
			flag = 16;
			pulse = 6;
		}
		else if(self.ammo_shells >= 2 && (it & 2))
		{
			flag = 2;
			pulse = 3;
		}
		else if(self.ammo_nails >= 1 && (it & 4))
		{
			flag = 4;
			pulse = 4;
		}
		else if(self.ammo_shells >= 1 && (it & 1))
		{
			flag = 1;
			pulse = 2;
		}
		else
		{
			if (pulse)
				self.impulse = pulse;
			return;
		}

		if (brange == -1)
		{
			if (pulse)
				self.impulse = pulse;
			return;
		}
		v = weapon_range(flag);
		if (brange < v_y || brange > v_z)
			it = it - flag;
		else
		{
			if (pulse)
				self.impulse = pulse;
			return;
		}
	}
};

void() bot_shoot = 
{
	// quick little function to stop making him shoot the wrong way ! Argh
	local float g;
	g = angcomp(self.v_angle_x, self.b_angle_x);
	if (fabs(g) > 30)
		return; // argh, too far away
	g = angcomp(self.v_angle_y, self.b_angle_y);
	if (fabs(g) > 30)
		return; // not again!
	self.button0 = TRUE;
};

/*
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Bot_fight_style

This is the core of the bot's thinking when
attacking an enemy. 

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
*/

void() bot_fight_style =
{
	local vector v, v1, v2, org;
	local float foedist, mysz, foesz;

// DRS: Buttons should be shot with shotguns, nails or rockets (they shouldn't shoot rockets if the range is too close by default),
// not grenades because grenades don't explode on buttons

	if(self.enemy.solid == SOLID_BSP && self.weapon == IT_GRENADE_LAUNCHER)
  	 bot_weapon_switch(foedist);

// DRS: Don't shoot RL if too close to button or door
         if(self.enemy.solid == SOLID_BSP && self.weapon == IT_ROCKET_LAUNCHER && (vlen(org - self.origin) < 140))
                bot_weapon_switch(foedist);

	if (self.enemy.health <= 0)
	{
		self.enemy = world;
		return;
	}
	else if (!self.enemy.takedamage)
	{
		self.enemy = world;
		return;
	}
	else if (!fisible(self.enemy))
	{
		self.enemy = world;
		return;
	}

	org = realorigin(self.enemy);
	makevectors(self.v_angle);

	// decide if I should shoot

	foedist = vlen(org - self.origin);
	v = weapon_range(self.weapon);

	   // DRS: Select optimal weapon during fight
  	 bot_weapon_switch(foedist);

   	if (foedist > v_y && foedist < v_z)
	{
		traceline(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * v_z, FALSE, self);
		if (vlen(trace_endpos - (self.origin + self.view_ofs)) >= v_y)
		{
         // try to avoid shooting teammates
         if (trace_ent.classname == "player")
            if ((trace_ent.team == self.team && teamplay) || (coop))
               return;
         // DRS: If dontshoot is TRUE, that means we don't have a straight line shot
         if(self.dontshoot == TRUE)
          return;
			bot_shoot();
		}
	}
	else
		bot_weapon_switch(foedist);

	if (!(self.b_aiflags & (AI_PRECISION | AI_BLIND | AI_OBSTRUCTED)))
	{
		foesz = bot_size_player(self.enemy);
		mysz = bot_size_player(self) + 5;

		if (foesz > mysz)
		{
			if (teamplay)
			{
				if (random() < 0.02)
				{
					bot_start_topic(5);
					self.b_chattime = 1;
				}
			}
			
			return;
		}
		else if (mysz < 140)
			return;
		else if (self.avoid)
		{
			if (self.avoid.velocity)
				v = self.avoid.velocity;
			else
				v = normalize(self.avoid.origin - self.origin);
			v1_x = v_y * -1;
			v1_y = v_x;
			v2_x = v_y;
			v2_y = v_x * -1;
			foedist = vlen(self.avoid.origin - (self.origin + v1));
			if (foedist < vlen(self.avoid.origin - (self.origin + v2)))
				frik_walkmove(v2);
			else
				frik_walkmove(v1);
		}
		else if (!self.enemy.flags & FL_MONSTER)
		{
			if (foedist + 32 <  v_x)
			frik_walkmove(self.origin - org);
			else if (foedist - 32 >  v_x)
				frik_walkmove(org - self.origin);
			else if (self.wallhug)
				frik_walkmove(v_right);
			else
				frik_walkmove(v_right * -1);
		}
	}
	else
	{
		foesz = bot_size_player(self.enemy);
		mysz = bot_size_player(self) + 5;

		if (foesz > mysz)
			return;
		else if (mysz < 140)
			return;
		self.keys = self.keys & 960;
	}
};
This does seem to work great! I really can't help but admire your grenade aiming code. The bots seriously rule at using the GL now. I even saw them fire grenades through windows high up off the ground in Fragtwn1. That will surely surprise some players when they see grenades lobbed through a window that high up. I must thank you again for doing these improvements. The frikbots are already so much better.
Dr. Shadowborg
InsideQC Staff
Posts: 1120
Joined: Sat Oct 16, 2004 3:34 pm

Re: Frikbot Item desirability

Post by Dr. Shadowborg »

You need to move the button / door grenade and rocket checks to at least below this part:

Code: Select all

      // DRS: Select optimal weapon during fight
      bot_weapon_switch(foedist);
If you don't the rocket launcher check will not work properly as org has not been defined properly at that point in the code.

Now then, I have fixes for both precision bug and teleport bug.

First the precision bug:

Open bot_way.qc and put setsize(self.enemy, '0 0 0', '0 0 0'); at the very top of FixThisWaypoint(); so that it looks like this:

Code: Select all

void() FixThisWaypoint = 
{
	setsize(self.enemy, '0 0 0', '0 0 0');
	self.enemy.target1 = WaypointForNum(self.enemy.b_pants);
	self.enemy.target2 = WaypointForNum(self.enemy.b_skill);
	self.enemy.target3 = WaypointForNum(self.enemy.b_shirt);
	self.enemy.target4 = WaypointForNum(self.enemy.b_frags);
	self.enemy = self.enemy._next;
	self.nextthink = time;
	if (self.enemy == world)
	{
		remove(self);
		fixer = world;
	}
};
That fixes the precision bug. (Apparently, waypoints were originally entities with no size, no mass, no movetype, and no physical appearance. Unfortunately, having no size causes all manner of problems, so this will fix it. :P )

Now, fixing the teleport bug means fixing a flaw within the stock QC code. I'll probably do a separate re-release of this finished FBX version bundled with a cleaned / fixed GPL QC at some point.

Anyway, open bot.qc, and add .float real_teleport_time; right below the // DRS: Extra shizz here, so that it looks like this:

Code: Select all

// DRS: Extra shizz here
float(float num) mathlib_cos;
.float dontshoot;
.float real_teleport_time;
Next, open triggers.qc, in teleport_touch, within the if(other.classname == "player") block, add other.real_teleport_time + 0.1; so that it looks like this:

Code: Select all

	if (other.classname == "player")
	{
		other.fixangle = 1;		// turn this way immediately
		other.teleport_time = time + 0.7;
		// DRS: Bot Teleporter fix
		other.real_teleport_time = time + 0.1;
		if (other.flags & FL_ONGROUND)
			other.flags = other.flags - FL_ONGROUND;
		other.velocity = v_forward * 300;
	}
Next, open bot_ai.qc, within the bot_check_lost function, right below the trigger_multiple block, but before the lost target above bot's head part add this:

Code: Select all

	// DRS: Teleporter target fix
	if(targ.classname == "trigger_teleport" && self.real_teleport_time > time)
	 bot_lost(targ, TRUE);
That should fix the teleport bug. The reason we had to do that is that .teleport_time is also used for getting out of water (engineside stuff), so it breaks proper detection (for the bot) of going through a teleporter.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

Man, you are fast! So far, it does look like the precision bug is fixed. I will continue to observe the bots to see if there are any other issues in this regard. The teleport bug is indeed fixed with the water issue in Dranzdm4, but there is still a remaining issue with teleporters. I found a map in which this is really evident:
http://fbe.am/ulM

There is one teleporter in particular near a blue armor/GL in which the bot will enter the teleporter, but instantaneously be teleported back to the same area (you hear the teleport sound twice in rapid succession; its almost hard to notice the sound being played twice). The bot will then try to get toward the incorrect waypoint, running in to a corner as a result.

Also take a look at this map if you want to see more teleporter issues (it may be the same issue):
http://fbe.am/ulN

I'm not sure exactly what is going on in this map, but the bots will quite frequently teleport, get "disoriented", and walk toward a wall (probably because they didn't register teleporting correctly). In fact, there is a whole plethora of teleport bugs going on in this map. You might be even more likely to spot them here than the first map I posted.
Dr. Shadowborg
InsideQC Staff
Posts: 1120
Joined: Sat Oct 16, 2004 3:34 pm

Re: Frikbot Item desirability

Post by Dr. Shadowborg »

Lightning Hunter wrote:The teleport bug is indeed fixed with the water issue in Dranzdm4, but there is still a remaining issue with teleporters. I found a map in which this is really evident:
http://fbe.am/ulM

There is one teleporter in particular near a blue armor/GL in which the bot will enter the teleporter, but instantaneously be teleported back to the same area (you hear the teleport sound twice in rapid succession; its almost hard to notice the sound being played twice). The bot will then try to get toward the incorrect waypoint, running in to a corner as a result.

Also take a look at this map if you want to see more teleporter issues (it may be the same issue):
http://fbe.am/ulN

I'm not sure exactly what is going on in this map, but the bots will quite frequently teleport, get "disoriented", and walk toward a wall (probably because they didn't register teleporting correctly). In fact, there is a whole plethora of teleport bugs going on in this map. You might be even more likely to spot them here than the first map I posted.
This appears to be a clipping issue. Which is something I can't really fix because its built into the maps in question. :/ (the way teleporters work is that setorigin will revert to the oldorigin if the new origin is likely to get you stuck inside a wall, hence the bots are triggering some clipping issue within the enginecode and thus do not teleport properly)

This usually happens when they try to "slide" into a teleport from the edges, the only fix I can provide is to suggest tweaking your waypoints to make them go straight in from the center, and to provide some improved teleport detection code below:

In triggers.qc, within teleport_touch, change everything from the if(other.classname == "player") block to the bottom of the function to look like this:

Code: Select all

	if (other.classname == "player")
	{
		other.fixangle = 1;		// turn this way immediately
		other.teleport_time = time + 0.7;
		// DRS: Bot Teleporter fix
		other.real_teleport_time = time + 0.1;
		other.velocity = v_forward * 300;
	}
		if (other.flags & FL_ONGROUND)
	      other.flags = other.flags - (other.flags & FL_ONGROUND);
This will fix a potental bug with the setting of the FL_ONGROUND flag.

Next, in client.qc, within PlayerPostFrame, right below W_WeaponFrame(); add this:

Code: Select all

// DRS: Teleport bug fix
	if((self.flags & FL_ONGROUND) && self.real_teleport_time > time)
	 self.real_teleport_time = 0;
I think that should do it.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

This usually happens when they try to "slide" into a teleport from the edges, the only fix I can provide is to suggest tweaking your waypoints to make them go straight in from the center
Yup, already ahead of you on that one. I have been going through my maps in alphabetical order and improving some of my original waypoints that I made years ago. Those two maps I sent you are some of the oldest waypoints I did. They were next on my list to improve, but I thought I would send them to you first without teleporter improvements so you could attempt to fix the teleporter issues. Those two particular maps are also the worst I think I've seen with teleporter issues. I'll be able to improve them using some precision flags.

I'll try the new code as well and let you know how it works. 8)

By the way, once you finish these improvements (and it looks like you are very close), I was wondering if you wouldn't mind applying some of your fixes to the Painkeep mod Frikbots. I was planning on doing it myself and including it with my next release, but I think you could do it much easier and quicker than me (and I would probably mess something up). The Painkeep mod is the only mod that I have created waypoints for, and for good reason! It is quite fun to play against the Frikbots. I think I have done waypoints for nearly 30 maps for this mod, and they are all interesting to play. I'm not sure who originally added Frikbot support to this mod, but I can send you the code.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

I think the new teleport code actually made the bots worse in certain maps with lots of teleporters. An example is XL1DM1:
http://fbe.am/unu
I tested this map with the old code, and the bots didn't mess up once in 5 minutes. After updating the code, they messed up almost every other teleport. I'll post my current client.qc and triggers.qc, so you can check to see if I modified them correctly.

client.qc:

Code: Select all


// prototypes
void () W_WeaponFrame;
void() W_SetCurrentAmmo;
void() player_pain;
void() player_stand1;
void (vector org) spawn_tfog;
void (vector org, entity death_owner) spawn_tdeath;

float	modelindex_eyes, modelindex_player;

/*
=============================================================================

				LEVEL CHANGING / INTERMISSION

=============================================================================
*/

float	intermission_running;
float	intermission_exittime;

/*QUAKED info_intermission (1 0.5 0.5) (-16 -16 -16) (16 16 16)
This is the camera point for the intermission.
Use mangle instead of angle, so you can set pitch or roll as well as yaw.  'pitch roll yaw'
*/
void() info_intermission =
{
};



void() SetChangeParms =
{
	if (self.health <= 0)
	{
		SetNewParms ();
		return;
	}

// remove items
	self.items = self.items - (self.items & 
	(IT_KEY1 | IT_KEY2 | IT_INVISIBILITY | IT_INVULNERABILITY | IT_SUIT | IT_QUAD) );
	
// cap super health
	if (self.health > 100)
		self.health = 100;
	if (self.health < 50)
		self.health = 50;
	parm1 = self.items;
	parm2 = self.health;
	parm3 = self.armorvalue;
	if (self.ammo_shells < 25)
		parm4 = 25;
	else
		parm4 = self.ammo_shells;
	parm5 = self.ammo_nails;
	parm6 = self.ammo_rockets;
	parm7 = self.ammo_cells;
	parm8 = self.weapon;
	parm9 = self.armortype * 100;
};

void() SetNewParms =
{
	parm1 = IT_SHOTGUN | IT_AXE;
	parm2 = 100;
	parm3 = 0;
	parm4 = 25;
	parm5 = 0;
	parm6 = 0;
	parm7 = 0;
	parm8 = 1;
	parm9 = 0;
};

void() DecodeLevelParms =
{
//	if (serverflags)
	if (!deathmatch)	
	{
		if (world.model == "maps/start.bsp")
			SetNewParms ();		// take away all stuff on starting new episode
	}
	
	self.items = parm1;
	self.health = parm2;
	self.armorvalue = parm3;
	self.ammo_shells = parm4;
	self.ammo_nails = parm5;
	self.ammo_rockets = parm6;
	self.ammo_cells = parm7;
	self.weapon = parm8;
	self.armortype = parm9 * 0.01;
};

/*
============
FindIntermission

Returns the entity to view from
============
*/
entity() FindIntermission =
{
	local	entity spot;
	local	float cyc;

// look for info_intermission first
	spot = find (world, classname, "info_intermission");
	if (spot)
	{	// pick a random one
		cyc = random() * 4;
		while (cyc > 1)
		{
			spot = find (spot, classname, "info_intermission");
			if (!spot)
				spot = find (spot, classname, "info_intermission");
			cyc = cyc - 1;
		}
		return spot;
	}

// then look for the start position
	spot = find (world, classname, "info_player_start");
	if (spot)
		return spot;
	
// testinfo_player_start is only found in regioned levels
	spot = find (world, classname, "testplayerstart");
	if (spot)
		return spot;
	
	objerror ("FindIntermission: no spot");
	
	return world;
};

//~ FrikBot changes
string nextmap;
float done_changelevel;
void() GotoNextMap ={
	local float p;
	if (done_changelevel) {
		return;
	}
	done_changelevel = TRUE;
	p = cvar("samelevel");
	if (p > 1) {
		localcmd("map");
		localcmd(ftos(ceil(random() * p)));
		localcmd("\n");
	}
	else if (p < 0) {
		localcmd("map");
		localcmd(ftos(floor(serverflags/16)+1));
		localcmd("\n");
		serverflags = serverflags+16;
		if (floor((serverflags/16)+1) > (0 - p)) {
			serverflags = serverflags & 16;
		}
	}
	else if (p) {
		// if samelevel is set, stay on same level
		changelevel (mapname);
	}
	else {
		changelevel (nextmap);
	}
};


void() ExitIntermission =
{
// skip any text in deathmatch
	if (deathmatch)
	{
		GotoNextMap ();
		return;
	}
	
	intermission_exittime = time + 1;
	intermission_running = intermission_running + 1;

//
// run some text if at the end of an episode
//
	if (intermission_running == 2)
	{
		if (world.model == "maps/e1m7.bsp")
		{
			WriteByte (MSG_ALL, SVC_CDTRACK);
			WriteByte (MSG_ALL, 2);
			WriteByte (MSG_ALL, 3);
			if (!cvar("registered"))
			{
				WriteByte (MSG_ALL, SVC_FINALE);
				WriteString (MSG_ALL, "As the corpse of the monstrous entity\nChthon sinks back into the lava whence\nit rose, you grip the Rune of Earth\nMagic tightly. Now that you have\nconquered the Dimension of the Doomed,\nrealm of Earth Magic, you are ready to\ncomplete your task in the other three\nhaunted lands of Quake. Or are you? If\nyou don't register Quake, you'll never\nknow what awaits you in the Realm of\nBlack Magic, the Netherworld, and the\nElder World!");
			}
			else
			{
				WriteByte (MSG_ALL, SVC_FINALE);
				WriteString (MSG_ALL, "As the corpse of the monstrous entity\nChthon sinks back into the lava whence\nit rose, you grip the Rune of Earth\nMagic tightly. Now that you have\nconquered the Dimension of the Doomed,\nrealm of Earth Magic, you are ready to\ncomplete your task. A Rune of magic\npower lies at the end of each haunted\nland of Quake. Go forth, seek the\ntotality of the four Runes!");
			}
			return;
		}
		else if (world.model == "maps/e2m6.bsp")
		{
			WriteByte (MSG_ALL, SVC_CDTRACK);
			WriteByte (MSG_ALL, 2);
			WriteByte (MSG_ALL, 3);

			WriteByte (MSG_ALL, SVC_FINALE);
			WriteString (MSG_ALL, "The Rune of Black Magic throbs evilly in\nyour hand and whispers dark thoughts\ninto your brain. You learn the inmost\nlore of the Hell-Mother; Shub-Niggurath!\nYou now know that she is behind all the\nterrible plotting which has led to so\nmuch death and horror. But she is not\ninviolate! Armed with this Rune, you\nrealize that once all four Runes are\ncombined, the gate to Shub-Niggurath's\nPit will open, and you can face the\nWitch-Goddess herself in her frightful\notherworld cathedral.");
			return;
		}
		else if (world.model == "maps/e3m6.bsp")
		{
			WriteByte (MSG_ALL, SVC_CDTRACK);
			WriteByte (MSG_ALL, 2);
			WriteByte (MSG_ALL, 3);

			WriteByte (MSG_ALL, SVC_FINALE);
			WriteString (MSG_ALL, "The charred viscera of diabolic horrors\nbubble viscously as you seize the Rune\nof Hell Magic. Its heat scorches your\nhand, and its terrible secrets blight\nyour mind. Gathering the shreds of your\ncourage, you shake the devil's shackles\nfrom your soul, and become ever more\nhard and determined to destroy the\nhideous creatures whose mere existence\nthreatens the souls and psyches of all\nthe population of Earth.");
			return;
		}
		else if (world.model == "maps/e4m7.bsp")
		{
			WriteByte (MSG_ALL, SVC_CDTRACK);
			WriteByte (MSG_ALL, 2);
			WriteByte (MSG_ALL, 3);

			WriteByte (MSG_ALL, SVC_FINALE);
			WriteString (MSG_ALL, "Despite the awful might of the Elder\nWorld, you have achieved the Rune of\nElder Magic, capstone of all types of\narcane wisdom. Beyond good and evil,\nbeyond life and death, the Rune\npulsates, heavy with import. Patient and\npotent, the Elder Being Shub-Niggurath\nweaves her dire plans to clear off all\nlife from the Earth, and bring her own\nfoul offspring to our world! For all the\ndwellers in these nightmare dimensions\nare her descendants! Once all Runes of\nmagic power are united, the energy\nbehind them will blast open the Gateway\nto Shub-Niggurath, and you can travel\nthere to foil the Hell-Mother's plots\nin person.");
			return;
		}

		GotoNextMap();
	}
	
	if (intermission_running == 3)
	{
		if (!cvar("registered"))
		{	// shareware episode has been completed, go to sell screen
			WriteByte (MSG_ALL, SVC_SELLSCREEN);
			return;
		}
		
		if ( (serverflags&15) == 15)
		{
			WriteByte (MSG_ALL, SVC_FINALE);
			WriteString (MSG_ALL, "Now, you have all four Runes. You sense\ntremendous invisible forces moving to\nunseal ancient barriers. Shub-Niggurath\nhad hoped to use the Runes Herself to\nclear off the Earth, but now instead,\nyou will use them to enter her home and\nconfront her as an avatar of avenging\nEarth-life. If you defeat her, you will\nbe remembered forever as the savior of\nthe planet. If she conquers, it will be\nas if you had never been born.");
			return;
		}
		
	}

	GotoNextMap();
};

/*
============
IntermissionThink

When the player presses attack or jump, change to the next level
============
*/
void() IntermissionThink =
{
	if (time < intermission_exittime)
		return;

	if (!self.button0 && !self.button1 && !self.button2)
		return;
	
	ExitIntermission ();
};

void() execute_changelevel =
{
	local entity	pos;

	intermission_running = 1;
	
// enforce a wait time before allowing changelevel
	if (deathmatch)
		intermission_exittime = time + 5;
	else
		intermission_exittime = time + 2;

	WriteByte (MSG_ALL, SVC_CDTRACK);
	WriteByte (MSG_ALL, 3);
	WriteByte (MSG_ALL, 3);
	
	pos = FindIntermission ();

	other = find (world, classname, "player");
	while (other != world)
	{
		other.view_ofs = '0 0 0';
		other.angles = other.v_angle = pos.mangle;
		other.fixangle = TRUE;		// turn this way immediately
		other.nextthink = time + 0.5;
		other.takedamage = DAMAGE_NO;
		other.solid = SOLID_NOT;
		other.movetype = MOVETYPE_NONE;
		other.modelindex = 0;
		setorigin (other, pos.origin);
		other = find (other, classname, "player");
	}	

	WriteByte (MSG_ALL, SVC_INTERMISSION);
};


void() changelevel_touch =
{
	local entity	pos;

	if (other.classname != "player")
		return;

	if ((cvar("noexit") == 1) || ((cvar("noexit") == 2) && (mapname != "start")))
	{
		T_Damage (other, self, self, 50000);
		return;
	}

	if (coop || deathmatch)
	{
		bprint (other.netname);
		bprint (" exited the level\n");
	}
	
	nextmap = self.map;

	SUB_UseTargets ();

	if ( (self.spawnflags & 1) && (deathmatch == 0) )
	{	// NO_INTERMISSION
		GotoNextMap();
		return;
	}
	
	self.touch = SUB_Null;

// we can't move people right now, because touch functions are called
// in the middle of C movement code, so set a think time to do it
	self.think = execute_changelevel;
	self.nextthink = time + 0.1;
};

/*QUAKED trigger_changelevel (0.5 0.5 0.5) ? NO_INTERMISSION
When the player touches this, he gets sent to the map listed in the "map" variable.  Unless the NO_INTERMISSION flag is set, the view will go to the info_intermission spot and display stats.
*/
void() trigger_changelevel =
{
	if (!self.map)
		objerror ("changelevel trigger doesn't have map");
	
	InitTrigger ();
	self.touch = changelevel_touch;
};


/*
=============================================================================

				PLAYER GAME EDGE FUNCTIONS

=============================================================================
*/

void() set_suicide_frame;

// called by ClientKill and DeadThink
void() respawn =
{
	if (coop)
	{
		// make a copy of the dead body for appearances sake
		CopyToBodyQue (self);
		// get the spawn parms as they were at level start
		setspawnparms (self);
		// respawn		
		PutClientInServer ();
	}
	else if (deathmatch)
	{
		// make a copy of the dead body for appearances sake
		CopyToBodyQue (self);
		// set default spawn parms
		SetNewParms ();
		// respawn		
		PutClientInServer ();
	}
	else
	{	// restart the entire server
		localcmd ("restart\n");
	}
};


/*
============
ClientKill

Player entered the suicide command
============
*/
void() ClientKill =
{
	if ((intermission_running)&&((coop)||(deathmatch)))  // not allowed during intermission
		return;
	bprint (self.netname);
	bprint (" suicides\n");
	set_suicide_frame ();
	self.modelindex = modelindex_player;
	self.frags = self.frags - 2;	// extra penalty
	respawn ();
};

float(vector v) CheckSpawnPoint =
{
	return FALSE;
};

/*
============
SelectSpawnPoint

Returns the entity to spawn at
============
*/
entity() SelectSpawnPoint =
{
	local	entity spot;
	local	entity thing;
	local	float  pcount;
	
// testinfo_player_start is only found in regioned levels
	spot = find (world, classname, "testplayerstart");
	if (spot)
		return spot;
		
// choose a info_player_deathmatch point
	if (coop)
	{
		lastspawn = find(lastspawn, classname, "info_player_coop");
		if (lastspawn == world)
			lastspawn = find (lastspawn, classname, "info_player_start");
		if (lastspawn != world)
			return lastspawn;
	}
	else if (deathmatch)
	{
	//~ random respawn start
				local float	freespots, totalspots;
		local entity	lastfreespot;

		totalspots = 0;

		// loop through all spots to find widely free spots (radius 84)
		freespots = 0;
		lastfreespot = world;

		spot = find(world, classname, "info_player_deathmatch");
		while (spot)
		{
			totalspots = totalspots + 1;

			if (spot != lastspawn)
			{
				pcount = 0;
				thing = findradius(spot.origin, 84);		// search for players
				while ( (thing!=world) && (pcount == 0) )
				{
					if ((thing.classname == "player") && (thing.health > 0))	// player found
						pcount = 1;
					thing = thing.chain;
				}
				if (!pcount)					// this is a free spot
				{
					spot.goalentity = lastfreespot;		// create reverse free spot list
					lastfreespot = spot;			// begin of reverse free spot list
					freespots = freespots + 1;
				}
			}

			// Get the next spot in the chain
			spot = find(spot, classname, "info_player_deathmatch");
		}

		if (totalspots)
		{
			if (freespots)
			{
				// We now have the number of frre spots available on the map
				// Generate a random number between 0 and (freespots - 1)
				freespots = freespots - 0.01;	// random() result varies between 0.0 and (n-1).99
				freespots = floor(random() * freespots);

				spot = lastfreespot;
				while (freespots)
				{
					spot = spot.goalentity;
					freespots = freespots - 1;
				}

				lastspawn = spot;
				return spot;
			}

			// ack, they are all guarded
			// loop through all spots to find free spots (radius 32)
			freespots = 0;
			lastfreespot = world;

			spot = find(world, classname, "info_player_deathmatch");
			while (spot)
			{
				if (spot != lastspawn)
				{
					pcount = 0;
					thing = findradius(spot.origin, 32);		// search for players
					while ( (thing!=world) && (pcount == 0) )
					{
						if ((thing.classname == "player") && (thing.health > 0))	// player found
							pcount = 1;
						thing = thing.chain;
					}
					if (!pcount)					// this is a free spot
					{
						spot.goalentity = lastfreespot;		// create reverse free spot list
						lastfreespot = spot;			// begin of reverse free spot list
						freespots = freespots + 1;
					}
				}

				// Get the next spot in the chain
				spot = find(spot, classname, "info_player_deathmatch");
			}

			if (freespots)
			{
				// We now have the number of free spots available on the map
				// Generate a random number between 0 and (freespots - 1)
				freespots = freespots - 0.01;	// random() result varies between 0.0 and (n-1).99
				freespots = floor(random() * freespots);

				spot = lastfreespot;
				while (freespots)
				{
					spot = spot.goalentity;
					freespots = freespots - 1;
				}

				lastspawn = spot;
				return spot;
			}

			// ack, they are all full, just pick one at random
			// Generate a random number between 0 and (totalspots - 1)
			totalspots = totalspots - 0.01;	// random() result varies between 0.0 and (n-1).99
			totalspots = floor(random() * totalspots);

			spot = find(world, classname, "info_player_deathmatch");
			while (totalspots)
			{
				totalspots = totalspots - 1;
				spot = find(spot, classname, "info_player_deathmatch");
			}

			lastspawn = spot;
			return spot;
		}
/*  		spot = lastspawn;
		while (1)
		{
			spot = find(spot, classname, "info_player_deathmatch");
			if (spot != world)
			{
				if (spot == lastspawn)
					return lastspawn;
				pcount = 0;
				thing = findradius(spot.origin, 32);
				while(thing)
				{
					if (thing.classname == "player")
						pcount = pcount + 1;
					thing = thing.chain;
				}
				if (pcount == 0)
				{
					lastspawn = spot;
					return spot;
				}
			}
		}
 */	//~ random respawn end
	}

	if (serverflags)
	{	// return with a rune to start
		spot = find (world, classname, "info_player_start2");
		if (spot)
			return spot;
	}
	
	spot = find (world, classname, "info_player_start");
	if (!spot)
		error ("PutClientInServer: no info_player_start on level");
	
	return spot;
};

/*
===========
PutClientInServer

called each time a player is spawned
============
*/
void() DecodeLevelParms;
void() PlayerDie;


void() PutClientInServer =
{
	local	entity spot;

	spot = SelectSpawnPoint ();

	self.classname = "player";
	self.health = 100;
	self.takedamage = DAMAGE_AIM;
	self.solid = SOLID_SLIDEBOX;
	self.movetype = MOVETYPE_WALK;
	self.show_hostile = 0;
	self.max_health = 100;
	self.flags = FL_CLIENT;
	self.air_finished = time + 12;
	self.dmg = 2;   		// initial water damage
	self.super_damage_finished = 0;
	self.radsuit_finished = 0;
	self.invisible_finished = 0;
	self.invincible_finished = 0;
	self.effects = 0;
	self.invincible_time = 0;

	DecodeLevelParms ();
	
	W_SetCurrentAmmo ();

	self.attack_finished = time;
	self.th_pain = player_pain;
	self.th_die = PlayerDie;
	
	self.deadflag = DEAD_NO;
// paustime is set by teleporters to keep the player from moving a while
	self.pausetime = 0;
	
//	spot = SelectSpawnPoint ();

//	self.origin = spot.origin + '0 0 1';
	self.origin = self.oldorigin = spot.origin + '0 0 1';	// 1998-07-21 Respawning where player died fix by Robert Field
	self.angles = spot.angles;
	self.fixangle = TRUE;		// turn this way immediately

// oh, this is a hack!
	setmodel (self, "progs/eyes.mdl");
	modelindex_eyes = self.modelindex;

	setmodel (self, "progs/player.mdl");
	modelindex_player = self.modelindex;
	
	setsize (self, VEC_HULL_MIN, VEC_HULL_MAX);
	
	self.view_ofs = '0 0 22';
	
	self.velocity = '0 0 0';	// 1998-07-21 Player moves after respawn fix by Xian

	player_stand1 ();
	
	if (deathmatch || coop)
	{
		makevectors(self.angles);
		spawn_tfog (self.origin + v_forward*20);
	}

	spawn_tdeath (self.origin, self);
};


/*
=============================================================================

				QUAKED FUNCTIONS

=============================================================================
*/


/*QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 24)
The normal starting point for a level.
*/
void() info_player_start =
{
};


/*QUAKED info_player_start2 (1 0 0) (-16 -16 -24) (16 16 24)
Only used on start map for the return point from an episode.
*/
void() info_player_start2 =
{
};


/*
saved out by quaked in region mode
*/
void() testplayerstart =
{
};

/*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 24)
potential spawning position for deathmatch games
*/
void() info_player_deathmatch =
{
};

/*QUAKED info_player_coop (1 0 1) (-16 -16 -24) (16 16 24)
potential spawning position for coop games
*/
void() info_player_coop =
{
};

/*
===============================================================================

RULES

===============================================================================
*/

/*
go to the next level for deathmatch
only called if a time or frag limit has expired
*/
void() NextLevel =
{
	local entity o;

	if (mapname == "start")
	{
		if (!cvar("registered"))
		{
			mapname = "e1m1";
		}
		else if (!(serverflags & 1))
		{
			mapname = "e1m1";
			serverflags = serverflags | 1;
		}
		else if (!(serverflags & 2))
		{
			mapname = "e2m1";
			serverflags = serverflags | 2;
		}
		else if (!(serverflags & 4))
		{
			mapname = "e3m1";
			serverflags = serverflags | 4;
		}
		else if (!(serverflags & 8))
		{
			mapname = "e4m1";
			serverflags = serverflags - 7;
		}

		o = spawn();
		o.map = mapname;
	}
	else
	{
		// find a trigger changelevel
		o = find(world, classname, "trigger_changelevel");

		// go back to start if no trigger_changelevel
		if (!o)
		{
			mapname = "start";
			o = spawn();
			o.map = mapname;
		}
	}

	nextmap = o.map;
	gameover = TRUE;
	
	if (o.nextthink < time)
	{
		o.think = execute_changelevel;
		o.nextthink = time + 0.1;
	}
};

/*
============
CheckRules

Exit deathmatch games upon conditions
============
*/
void() CheckRules =
{
	local	float		timelimit;
	local	float		fraglimit;
	
	if (gameover)	// someone else quit the game already
		return;
		
	timelimit = cvar("timelimit") * 60;
	fraglimit = cvar("fraglimit");
	
//	if (timelimit && time >= timelimit)
	if (deathmatch && timelimit && time >= timelimit)	// 1998-07-27 Timelimit/Fraglimit fix by Maddes
	{
		NextLevel ();
		return;
	}
	
//	if (fraglimit && self.frags >= fraglimit)
	if (deathmatch && fraglimit && self.frags >= fraglimit)	// 1998-07-27 Timelimit/Fraglimit fix by Maddes
	{
		NextLevel ();
		return;
	}	
};

//============================================================================

void() PlayerDeathThink =
{
	local entity	old_self;
	local float		forward;

	if ((self.flags & FL_ONGROUND))
	{
		forward = vlen (self.velocity);
		forward = forward - 20;
		if (forward <= 0)
			self.velocity = '0 0 0';
		else	
			self.velocity = forward * normalize(self.velocity);
	}

// wait for all buttons released
	if (self.deadflag == DEAD_DEAD)
	{
		if (self.button2 || self.button1 || self.button0)
			return;
		self.deadflag = DEAD_RESPAWNABLE;
		return;
	}

// wait for any button down
	if (!self.button2 && !self.button1 && !self.button0)
		return;

	self.button0 = 0;
	self.button1 = 0;
	self.button2 = 0;
	respawn();
};


void() PlayerJump =
{
	local vector start, end;
	
	if (self.flags & FL_WATERJUMP)
		return;
	
	if (self.waterlevel >= 2)
	{
		if (self.watertype == CONTENT_WATER)
			self.velocity_z = 100;
		else if (self.watertype == CONTENT_SLIME)
			self.velocity_z = 80;
		else
			self.velocity_z = 50;

// play swiming sound
		if (self.swim_flag < time)
		{
			self.swim_flag = time + 1;
			if (random() < 0.5)
				sound (self, CHAN_BODY, "misc/water1.wav", 1, ATTN_NORM);
			else
				sound (self, CHAN_BODY, "misc/water2.wav", 1, ATTN_NORM);
		}

		return;
	}

	if (!(self.flags & FL_ONGROUND))
		return;

	if ( !(self.flags & FL_JUMPRELEASED) )
		return;		// don't pogo stick

	self.flags = self.flags - (self.flags & FL_JUMPRELEASED);

	self.flags = self.flags - FL_ONGROUND;	// don't stairwalk
	
	self.button2 = 0;
// player jumping sound
	sound (self, CHAN_BODY, "player/plyrjmp8.wav", 1, ATTN_NORM);
	self.velocity_z = self.velocity_z + 270;
};


/*
===========
WaterMove

============
*/
.float	dmgtime;

void() WaterMove =
{
//dprint (ftos(self.waterlevel));
	if (self.movetype == MOVETYPE_NOCLIP)
		return;
	if (self.health < 0)
		return;

	if (self.waterlevel != 3)
	{
		if (self.air_finished < time)
			sound (self, CHAN_VOICE, "player/gasp2.wav", 1, ATTN_NORM);
		else if (self.air_finished < time + 9)
			sound (self, CHAN_VOICE, "player/gasp1.wav", 1, ATTN_NORM);
		self.air_finished = time + 12;
		self.dmg = 2;
	}
	else if (self.air_finished < time)
	{	// drown!
		if (self.pain_finished < time)
		{
			self.dmg = self.dmg + 2;
			if (self.dmg > 15)
				self.dmg = 10;
			T_Damage (self, world, world, self.dmg);
			self.pain_finished = time + 1;
		}
	}
	
	if (!self.waterlevel)
	{
		if (self.flags & FL_INWATER)
		{	
			// play leave water sound
			sound (self, CHAN_BODY, "misc/outwater.wav", 1, ATTN_NORM);
			self.flags = self.flags - FL_INWATER;
		}
		return;
	}

	if (self.watertype == CONTENT_LAVA)
	{	// do damage
		if (self.dmgtime < time)
		{
			if (self.radsuit_finished > time)
				self.dmgtime = time + 1;
			else
				self.dmgtime = time + 0.2;

			T_Damage (self, world, world, 10*self.waterlevel);
		}
	}
	else if (self.watertype == CONTENT_SLIME)
	{	// do damage
		if (self.dmgtime < time && self.radsuit_finished < time)
		{
			self.dmgtime = time + 1;
			T_Damage (self, world, world, 4*self.waterlevel);
		}
	}
	
	if ( !(self.flags & FL_INWATER) )
	{	

// player enter water sound

		if (self.watertype == CONTENT_LAVA)
			sound (self, CHAN_BODY, "player/inlava.wav", 1, ATTN_NORM);
		if (self.watertype == CONTENT_WATER)
			sound (self, CHAN_BODY, "player/inh2o.wav", 1, ATTN_NORM);
		if (self.watertype == CONTENT_SLIME)
			sound (self, CHAN_BODY, "player/slimbrn2.wav", 1, ATTN_NORM);

		self.flags = self.flags + FL_INWATER;
		self.dmgtime = 0;
	}
	
	if (! (self.flags & FL_WATERJUMP) )
		self.velocity = self.velocity - 0.8*self.waterlevel*frametime*self.velocity;
};

void() CheckWaterJump =
{
	local vector start, end;

// check for a jump-out-of-water
	makevectors (self.angles);
	start = self.origin;
	start_z = start_z + 8; 
	v_forward_z = 0;
	normalize(v_forward);
	end = start + v_forward*24;
	traceline (start, end, TRUE, self);
	if (trace_fraction < 1)
	{	// solid at waist
		start_z = start_z + self.maxs_z - 8;
		end = start + v_forward*24;
		self.movedir = trace_plane_normal * -50;
		traceline (start, end, TRUE, self);
		if (trace_fraction == 1)
		{	// open at eye level
			self.flags = self.flags | FL_WATERJUMP;
			self.velocity_z = 225;
			self.flags = self.flags - (self.flags & FL_JUMPRELEASED);
			self.teleport_time = time + 2;	// safety net
			return;
		}
	}
};


/*
================
PlayerPreThink

Called every frame before physics are run
================
*/
void() PlayerPreThink =
{
	if (BotPreFrame())	//~ FrikBot
	return;

	local	float	mspeed, aspeed;
	local	float	r;

	if (intermission_running)
	{
		IntermissionThink ();	// otherwise a button could be missed between
		return;					// the think tics
	}

	if (self.view_ofs == '0 0 0')
		return;		// intermission or finale

	makevectors (self.v_angle);		// is this still used

	CheckRules ();
	WaterMove ();

	if (self.waterlevel == 2)
		CheckWaterJump ();

	if (self.deadflag >= DEAD_DEAD)
	{
		PlayerDeathThink ();
		return;
	}
	
	if (self.deadflag == DEAD_DYING)
		return;	// dying, so do nothing

	if (self.button2)
	{
		PlayerJump ();
	}
	else
		self.flags = self.flags | FL_JUMPRELEASED;

// teleporters can force a non-moving pause time	
	if (time < self.pausetime)
		self.velocity = '0 0 0';

	if(time > self.attack_finished && self.currentammo == 0 && self.weapon != IT_AXE)
	{
		self.weapon = W_BestWeapon ();
		W_SetCurrentAmmo ();
	}
};
	
/*
================
CheckPowerups

Check for turning off powerups
================
*/
void() CheckPowerups =
{
	if (self.health <= 0)
		return;

// invisibility
	if (self.invisible_finished)
	{
// sound and screen flash when items starts to run out
		if (self.invisible_sound < time)
		{
			sound (self, CHAN_AUTO, "items/inv3.wav", 0.5, ATTN_IDLE);
			self.invisible_sound = time + ((random() * 3) + 1);
		}


		if (self.invisible_finished < time + 3)
		{
			if (self.invisible_time == 1)
			{
				sprint (self, "Ring of Shadows magic is fading\n");
				stuffcmd (self, "bf\n");
				sound (self, CHAN_AUTO, "items/inv2.wav", 1, ATTN_NORM);
				self.invisible_time = time + 1;
			}
			
			if (self.invisible_time < time)
			{
				self.invisible_time = time + 1;
				stuffcmd (self, "bf\n");
			}
		}

		if (self.invisible_finished < time)
		{	// just stopped
			self.items = self.items - IT_INVISIBILITY;
			self.invisible_finished = 0;
			self.invisible_time = 0;
		}
		
	// use the eyes
		self.frame = 0;
		self.modelindex = modelindex_eyes;
	}
	else
		self.modelindex = modelindex_player;	// don't use eyes
	
// invincibility
	if (self.invincible_finished)
	{
// sound and screen flash when items starts to run out
		if (self.invincible_finished < time + 3)
		{
			if (self.invincible_time == 1)
			{
				sprint (self, "Protection is almost burned out\n");
				stuffcmd (self, "bf\n");
				sound (self, CHAN_AUTO, "items/protect2.wav", 1, ATTN_NORM);
				self.invincible_time = time + 1;
			}
			
			if (self.invincible_time < time)
			{
				self.invincible_time = time + 1;
				stuffcmd (self, "bf\n");
			}
		}
		
		if (self.invincible_finished < time)
		{	// just stopped
			self.items = self.items - IT_INVULNERABILITY;
			self.invincible_time = 0;
			self.invincible_finished = 0;
		}
		if (self.invincible_finished > time)
			self.effects = self.effects | EF_DIMLIGHT;
		else
			self.effects = self.effects - (self.effects & EF_DIMLIGHT);
	}

// super damage
	if (self.super_damage_finished)
	{

// sound and screen flash when items starts to run out

		if (self.super_damage_finished < time + 3)
		{
			if (self.super_time == 1)
			{
				sprint (self, "Quad Damage is wearing off\n");
				stuffcmd (self, "bf\n");
				sound (self, CHAN_AUTO, "items/damage2.wav", 1, ATTN_NORM);
				self.super_time = time + 1;
			}	  
			
			if (self.super_time < time)
			{
				self.super_time = time + 1;
				stuffcmd (self, "bf\n");
			}
		}

		if (self.super_damage_finished < time)
		{	// just stopped
			self.items = self.items - IT_QUAD;
			self.super_damage_finished = 0;
			self.super_time = 0;
		}
		if (self.super_damage_finished > time)
			self.effects = self.effects | EF_DIMLIGHT;
		else
			self.effects = self.effects - (self.effects & EF_DIMLIGHT);
	}	

// suit	
	if (self.radsuit_finished)
	{
		self.air_finished = time + 12;		// don't drown

// sound and screen flash when items starts to run out
		if (self.radsuit_finished < time + 3)
		{
			if (self.rad_time == 1)
			{
				sprint (self, "Air supply in Biosuit expiring\n");
				stuffcmd (self, "bf\n");
				sound (self, CHAN_AUTO, "items/suit2.wav", 1, ATTN_NORM);
				self.rad_time = time + 1;
			}
			
			if (self.rad_time < time)
			{
				self.rad_time = time + 1;
				stuffcmd (self, "bf\n");
			}
		}

		if (self.radsuit_finished < time)
		{	// just stopped
			self.items = self.items - IT_SUIT;
			self.rad_time = 0;
			self.radsuit_finished = 0;
		}
	}	

};


/*
================
PlayerPostThink

Called every frame after physics are run
================
*/
void() PlayerPostThink =
{
	if (BotPostFrame())	//~ FrikBot
	return;

	local	float	mspeed, aspeed;
	local	float	r;

	if (self.view_ofs == '0 0 0')
		return;		// intermission or finale
	if (self.deadflag)
		return;
		
// do weapon stuff

	W_WeaponFrame ();

// DRS: Teleport bug fix
   if((self.flags & FL_ONGROUND) && self.real_teleport_time > time)
    self.real_teleport_time = 0;

// check to see if player landed and play landing sound	
	if ((self.jump_flag < -300) && (self.flags & FL_ONGROUND) && (self.health > 0))
	{
		if (self.watertype == CONTENT_WATER)
			sound (self, CHAN_BODY, "player/h2ojump.wav", 1, ATTN_NORM);
		else if (self.jump_flag < -650)
		{
			T_Damage (self, world, world, 5); 
			sound (self, CHAN_VOICE, "player/land2.wav", 1, ATTN_NORM);
			self.deathtype = "falling";
		}
		else
			sound (self, CHAN_VOICE, "player/land.wav", 1, ATTN_NORM);

		self.jump_flag = 0;
	}

	if (!(self.flags & FL_ONGROUND))
		self.jump_flag = self.velocity_z;

	CheckPowerups ();
};


/*
===========
ClientConnect

called when a player connects to a server
============
*/
void() ClientConnect =
{
	ClientInRankings();	//~ FrikBot
	bprint (self.netname);
	bprint (" entered the game\n");
	
// a client connecting during an intermission can cause problems
	if (intermission_running)
		ExitIntermission ();
};


/*
===========
ClientDisconnect

called when a player disconnects from a server
============
*/
void() ClientDisconnect =
{
	ClientDisconnected();	//~ FrikBot
	if (gameover)
		return;
	// if the level end trigger has been activated, just return
	// since they aren't *really* leaving

	// let everyone else know
	bprint (self.netname);
	bprint (" left the game with ");
	bprint (ftos(self.frags));
	bprint (" frags\n");
	sound (self, CHAN_BODY, "player/tornoff2.wav", 1, ATTN_NONE);
	set_suicide_frame ();
};

/*
===========
ClientObituary

called when a player dies
============
*/
void(entity targ, entity attacker) ClientObituary =
{
	local	float rnum;
	local	string deathstring, deathstring2;
	rnum = random();

	if (targ.classname == "player")
	{
		if (attacker.classname == "teledeath")
		{
			bprint (targ.netname);
			bprint (" was telefragged by ");
			bprint (attacker.owner.netname);
			bprint ("\n");

			attacker.owner.frags = attacker.owner.frags + 1;
			return;
		}

		if (attacker.classname == "teledeath2")
		{
			//~ exorcise code start
			//~ bprint ("Satan's power deflects ");
			bprint ("Solomon's seal deflects ");
			//~ exorcise code end
			bprint (targ.netname);
			bprint ("'s telefrag\n");

			targ.frags = targ.frags - 1;
			return;
		}

		if (attacker.classname == "player")
		{
			if (targ == attacker)
			{
				// killed self
				attacker.frags = attacker.frags - 1;
				bprint (targ.netname);
				
				if (targ.weapon == 64 && targ.waterlevel > 1)
				{
					bprint (" discharges into the water.\n");
					return;
				}
				if (targ.weapon == IT_GRENADE_LAUNCHER)
					bprint (" tries to put the pin back in\n");
				else
					bprint (" becomes bored with life\n");
				return;
			}
			else if ( (teamplay == 2) && (targ.team > 0)&&(targ.team == attacker.team) )
			{
				if (rnum < 0.25)
					deathstring = " mows down a teammate\n";
				else if (rnum < 0.50)
					deathstring = " checks his glasses\n";
				else if (rnum < 0.75)
					deathstring = " gets a frag for the other team\n";
				else
					deathstring = " loses another friend\n";
				bprint (attacker.netname);
				bprint (deathstring);
				attacker.frags = attacker.frags - 1;
				return;
			}
			else
			{
				attacker.frags = attacker.frags + 1;

				rnum = attacker.weapon;
				if (rnum == IT_AXE)
				{
					deathstring = " was ax-murdered by ";
					deathstring2 = "\n";
				}
				if (rnum == IT_SHOTGUN)
				{
					deathstring = " chewed on ";
					deathstring2 = "'s boomstick\n";
				}
				if (rnum == IT_SUPER_SHOTGUN)
				{
					deathstring = " ate 2 loads of ";
					deathstring2 = "'s buckshot\n";
				}
				if (rnum == IT_NAILGUN)
				{
					deathstring = " was nailed by ";
					deathstring2 = "\n";
				}
				if (rnum == IT_SUPER_NAILGUN)
				{
					deathstring = " was punctured by ";
					deathstring2 = "\n";
				}
				if (rnum == IT_GRENADE_LAUNCHER)
				{
					deathstring = " eats ";
					deathstring2 = "'s pineapple\n";
					if (targ.health < -40)
					{
						deathstring = " was gibbed by ";
						deathstring2 = "'s grenade\n";
					}
				}
				if (rnum == IT_ROCKET_LAUNCHER)
				{
					deathstring = " rides ";
					deathstring2 = "'s rocket\n";
					if (targ.health < -40)
					{
						deathstring = " was gibbed by ";
						deathstring2 = "'s rocket\n" ;
					}
				}
				if (rnum == IT_LIGHTNING)
				{
					deathstring = " accepts ";
					if (attacker.waterlevel > 1)
						deathstring2 = "'s discharge\n";
					else
						deathstring2 = "'s shaft\n";
				}
				bprint (targ.netname);
				bprint (deathstring);
				bprint (attacker.netname);
				bprint (deathstring2);
			}
			return;
		}
		else
		{
			targ.frags = targ.frags - 1;
			bprint (targ.netname);

			// killed by a montser?
			if (attacker.flags & FL_MONSTER)
			{
				if (attacker.classname == "monster_army")
					bprint (" was shot by a Grunt\n");
				if (attacker.classname == "monster_demon1")
					bprint (" was eviscerated by a Fiend\n");
				if (attacker.classname == "monster_dog")
					bprint (" was mauled by a Rottweiler\n");
				if (attacker.classname == "monster_dragon")
					bprint (" was fried by a Dragon\n");
				if (attacker.classname == "monster_enforcer")
					bprint (" was blasted by an Enforcer\n");
				if (attacker.classname == "monster_fish")
					bprint (" was fed to the Rotfish\n");
				if (attacker.classname == "monster_hell_knight")
					bprint (" was slain by a Death Knight\n");
				if (attacker.classname == "monster_knight")
					bprint (" was slashed by a Knight\n");
				if (attacker.classname == "monster_ogre")
					bprint (" was destroyed by an Ogre\n");
				if (attacker.classname == "monster_oldone")
					bprint (" became one with Shub-Niggurath\n");
				if (attacker.classname == "monster_shalrath")
					bprint (" was exploded by a Vore\n");
				if (attacker.classname == "monster_shambler")
					bprint (" was smashed by a Shambler\n");
				if (attacker.classname == "monster_tarbaby")
					bprint (" was slimed by a Spawn\n");
				if (attacker.classname == "monster_vomit")
					bprint (" was vomited on by a Vomitus\n");
				if (attacker.classname == "monster_wizard")
					bprint (" was scragged by a Scrag\n");
				if (attacker.classname == "monster_zombie")
					bprint (" joins the Zombies\n");

				return;
			}

			// tricks and traps
			if (attacker.classname == "explo_box")
			{
				bprint (" blew up\n");
				return;
			}
			if (attacker.solid == SOLID_BSP && attacker != world)
			{	
				bprint (" was squished\n");
				return;
			}
			if (attacker.classname == "trap_shooter" || attacker.classname == "trap_spikeshooter")
			{
				bprint (" was spiked\n");
				return;
			}
			if (attacker.classname == "fireball")
			{
				bprint (" ate a lavaball\n");
				return;
			}
			if (attacker.classname == "trigger_changelevel")
			{
				bprint (" tried to leave\n");
				return;
			}

			// in-water deaths
			rnum = targ.watertype;
			if (rnum == -3)
			{
				if (random() < 0.5)
					bprint (" sleeps with the fishes\n");
				else
					bprint (" sucks it down\n");
				return;
			}
			else if (rnum == -4)
			{
				if (random() < 0.5)
					bprint (" gulped a load of slime\n");
				else
					bprint (" can't exist on slime alone\n");
				return;
			}
			else if (rnum == -5)
			{
				if (targ.health < -15)
				{
					bprint (" burst into flames\n");
					return;
				}
				if (random() < 0.5)
					bprint (" turned into hot slag\n");
				else
					bprint (" visits the Volcano God\n");
				return;
			}

			// fell to their death?
			if (targ.deathtype == "falling")
			{
				targ.deathtype = "";
				bprint (" fell to his death\n");
				return;
			}

			// hell if I know; he's just dead!!!
			bprint (" died\n");
		}
	}
};
Triggers.qc:

Code: Select all


entity stemp, otemp, s, old;


void() trigger_reactivate =
{
	self.solid = SOLID_TRIGGER;
};

//=============================================================================

float	SPAWNFLAG_NOMESSAGE = 1;
float	SPAWNFLAG_NOTOUCH = 1;

// the wait time has passed, so set back up for another activation
void() multi_wait =
{
	if (self.max_health)
	{
		self.health = self.max_health;
		self.takedamage = DAMAGE_YES;
		self.solid = SOLID_BBOX;
	}
};


// the trigger was just touched/killed/used
// self.enemy should be set to the activator so it can be held through a delay
// so wait for the delay time before firing
void() multi_trigger =
{
	if (self.nextthink > time)
	{
		return;		// allready been triggered
	}

	if (self.classname == "trigger_secret")
	{
		if (self.enemy.classname != "player")
			return;
		found_secrets = found_secrets + 1;
		WriteByte (MSG_ALL, SVC_FOUNDSECRET);
	}

	if (self.noise)
		sound (self, CHAN_VOICE, self.noise, 1, ATTN_NORM);

// don't trigger again until reset
	self.takedamage = DAMAGE_NO;

	activator = self.enemy;
	
	SUB_UseTargets();

	if (self.wait > 0)	
	{
		self.think = multi_wait;
		self.nextthink = time + self.wait;
	}
	else
	{	// we can't just remove (self) here, because this is a touch function
		// called wheil C code is looping through area links...
		self.touch = SUB_Null;
		self.nextthink = time + 0.1;
		self.think = SUB_Remove;
	}
};

void() multi_killed =
{
	self.enemy = damage_attacker;
	multi_trigger();
};

void() multi_use =
{
	self.enemy = activator;
	multi_trigger();
};

void() multi_touch =
{
	if (other.classname != "player")
		return;
	
// if the trigger has an angles field, check player's facing direction
	if (self.movedir != '0 0 0')
	{
		makevectors (other.angles);
		if (v_forward * self.movedir < 0)
			return;		// not facing the right way
	}
	
	self.enemy = other;
	multi_trigger ();
};

/*QUAKED trigger_multiple (.5 .5 .5) ? notouch
Variable sized repeatable trigger.  Must be targeted at one or more entities.  If "health" is set, the trigger must be killed to activate each time.
If "delay" is set, the trigger waits some time after activating before firing.
"wait" : Seconds between triggerings. (.2 default)
If notouch is set, the trigger is only fired by other entities, not by touching.
NOTOUCH has been obsoleted by trigger_relay!
sounds
1)	secret
2)	beep beep
3)	large switch
4)
set "message" to text string
*/
void() trigger_multiple =
{
	if (self.sounds == 1)
	{
		precache_sound ("misc/secret.wav");
		self.noise = "misc/secret.wav";
	}
	else if (self.sounds == 2)
	{
		precache_sound ("misc/talk.wav");
		self.noise = "misc/talk.wav";
	}
	else if (self.sounds == 3)
	{
		precache_sound ("misc/trigger1.wav");
		self.noise = "misc/trigger1.wav";
	}
	
	if (!self.wait)
		self.wait = 0.2;
	self.use = multi_use;

	InitTrigger ();

	if (self.health)
	{
		if (self.spawnflags & SPAWNFLAG_NOTOUCH)
			objerror ("health and notouch don't make sense\n");
		self.max_health = self.health;
		self.th_die = multi_killed;
		self.takedamage = DAMAGE_YES;
		self.solid = SOLID_BBOX;
		setorigin (self, self.origin);	// make sure it links into the world
	}
	else
	{
		if ( !(self.spawnflags & SPAWNFLAG_NOTOUCH) )
		{
			self.touch = multi_touch;
		}
	}
};


/*QUAKED trigger_once (.5 .5 .5) ? notouch
Variable sized trigger. Triggers once, then removes itself.  You must set the key "target" to the name of another object in the level that has a matching
"targetname".  If "health" is set, the trigger must be killed to activate.
If notouch is set, the trigger is only fired by other entities, not by touching.
if "killtarget" is set, any objects that have a matching "target" will be removed when the trigger is fired.
if "angle" is set, the trigger will only fire when someone is facing the direction of the angle.  Use "360" for an angle of 0.
sounds
1)	secret
2)	beep beep
3)	large switch
4)
set "message" to text string
*/
void() trigger_once =
{
	self.wait = -1;
	trigger_multiple();
};

//=============================================================================

/*QUAKED trigger_relay (.5 .5 .5) (-8 -8 -8) (8 8 8)
This fixed size trigger cannot be touched, it can only be fired by other events.  It can contain killtargets, targets, delays, and messages.
*/
void() trigger_relay =
{
	self.use = SUB_UseTargets;
};


//=============================================================================

/*QUAKED trigger_secret (.5 .5 .5) ?
secret counter trigger
sounds
1)	secret
2)	beep beep
3)
4)
set "message" to text string
*/
void() trigger_secret =
{
	total_secrets = total_secrets + 1;
	self.wait = -1;
	if (!self.message)
		self.message = "You found a secret area!";
	if (!self.sounds)
		self.sounds = 1;
	
	if (self.sounds == 1)
	{
		precache_sound ("misc/secret.wav");
		self.noise = "misc/secret.wav";
	}
	else if (self.sounds == 2)
	{
		precache_sound ("misc/talk.wav");
		self.noise = "misc/talk.wav";
	}

	trigger_multiple ();
};

//=============================================================================


void() counter_use =
{
	local string junk;

	self.count = self.count - 1;
	if (self.count < 0)
		return;
	
	if (self.count != 0)
	{
		if (activator.classname == "player"
		&& (self.spawnflags & SPAWNFLAG_NOMESSAGE) == 0)
		{
			if (self.count >= 4)
				centerprint (activator, "There are more to go...");
			else if (self.count == 3)
				centerprint (activator, "Only 3 more to go...");
			else if (self.count == 2)
				centerprint (activator, "Only 2 more to go...");
			else
				centerprint (activator, "Only 1 more to go...");
		}
		return;
	}
	
	if (activator.classname == "player"
	&& (self.spawnflags & SPAWNFLAG_NOMESSAGE) == 0)
		centerprint(activator, "Sequence completed!");
	self.enemy = activator;
	multi_trigger ();
};

/*QUAKED trigger_counter (.5 .5 .5) ? nomessage
Acts as an intermediary for an action that takes multiple inputs.

If nomessage is not set, t will print "1 more.. " etc when triggered and "sequence complete" when finished.

After the counter has been triggered "count" times (default 2), it will fire all of it's targets and remove itself.
*/
void() trigger_counter =
{
	self.wait = -1;
	if (!self.count)
		self.count = 2;

	self.use = counter_use;
};


/*
==============================================================================

TELEPORT TRIGGERS

==============================================================================
*/

float	PLAYER_ONLY	= 1;
float	SILENT = 2;

void() play_teleport =
{
	local	float v;
	local	string tmpstr;

	v = random() * 5;
	if (v < 1)
		tmpstr = "misc/r_tele1.wav";
	else if (v < 2)
		tmpstr = "misc/r_tele2.wav";
	else if (v < 3)
		tmpstr = "misc/r_tele3.wav";
	else if (v < 4)
		tmpstr = "misc/r_tele4.wav";
	else
		tmpstr = "misc/r_tele5.wav";

	sound (self, CHAN_VOICE, tmpstr, 1, ATTN_NORM);
	remove (self);
};

void(vector org) spawn_tfog =
{
	s = spawn ();
	s.origin = org;
	s.nextthink = time + 0.2;
	s.think = play_teleport;

	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
	WriteByte (MSG_BROADCAST, TE_TELEPORT);
	WriteCoord (MSG_BROADCAST, org_x);
	WriteCoord (MSG_BROADCAST, org_y);
	WriteCoord (MSG_BROADCAST, org_z);
};


void() tdeath_touch =
{
	if (other == self.owner)
		return;

// frag anyone who teleports in on top of an invincible player
	if (other.classname == "player")
	{
		if (other.invincible_finished > time)
			self.classname = "teledeath2";
		if (self.owner.classname != "player")
		{	// other monsters explode themselves
			T_Damage (self.owner, self, self, 50000);
			return;
		}
		
	}

	if (other.health)
	{
		T_Damage (other, self, self, 50000);
	}
};


void(vector org, entity death_owner) spawn_tdeath =
{
local entity	death;

	death = spawn();
	death.classname = "teledeath";
	death.movetype = MOVETYPE_NONE;
	death.solid = SOLID_TRIGGER;
	death.angles = '0 0 0';
	setsize (death, death_owner.mins - '1 1 1', death_owner.maxs + '1 1 1');
	setorigin (death, org);
	death.touch = tdeath_touch;
	death.nextthink = time + 0.2;
	death.think = SUB_Remove;
	death.owner = death_owner;
	
	force_retouch = 2;		// make sure even still objects get hit
};

void() teleport_touch =
{
local entity	t;
local vector	org;

	if (self.targetname)
	{
		if (self.nextthink < time)
		{
			return;		// not fired yet
		}
	}

	if (self.spawnflags & PLAYER_ONLY)
	{
		if (other.classname != "player")
			return;
	}

// only teleport living creatures
	if (other.health <= 0 || other.solid != SOLID_SLIDEBOX)
		return;

	SUB_UseTargets ();

// put a tfog where the player was
	spawn_tfog (other.origin);

	t = find (world, targetname, self.target);
	if (!t)
		objerror ("couldn't find target");
		
// spawn a tfog flash in front of the destination
	makevectors (t.mangle);
	org = t.origin + 32 * v_forward;

	spawn_tfog (org);
	spawn_tdeath(t.origin, other);

// move the player and lock him down for a little while
	if (!other.health)
	{
		other.origin = t.origin;
		other.velocity = (v_forward * other.velocity_x) + (v_forward * other.velocity_y);
		return;
	}

	setorigin (other, t.origin);
	other.angles = t.mangle;
       if (other.classname == "player")
       {
          other.fixangle = 1;      // turn this way immediately
          other.teleport_time = time + 0.7;
          // DRS: Bot Teleporter fix
          other.real_teleport_time = time + 0.1;
          other.velocity = v_forward * 300;
       }
          if (other.flags & FL_ONGROUND)
             other.flags = other.flags - (other.flags & FL_ONGROUND);

};

/*QUAKED info_teleport_destination (.5 .5 .5) (-8 -8 -8) (8 8 32)
This is the destination marker for a teleporter.  It should have a "targetname" field with the same value as a teleporter's "target" field.
*/
void() info_teleport_destination =
{
// this does nothing, just serves as a target spot
	self.mangle = self.angles;
	self.angles = '0 0 0';
	self.model = "";
	self.origin = self.origin + '0 0 27';
	if (!self.targetname)
		objerror ("no targetname");
};

void() teleport_use =
{
	self.nextthink = time + 0.2;
	force_retouch = 2;		// make sure even still objects get hit
	self.think = SUB_Null;
};

/*QUAKED trigger_teleport (.5 .5 .5) ? PLAYER_ONLY SILENT
Any object touching this will be transported to the corresponding info_teleport_destination entity. You must set the "target" field, and create an object with a "targetname" field that matches.

If the trigger_teleport has a targetname, it will only teleport entities when it has been fired.
*/
void() trigger_teleport =
{
	local vector o;

	InitTrigger ();
	self.touch = teleport_touch;
	// find the destination 
	if (!self.target)
		objerror ("no target");
	self.use = teleport_use;

	if (!(self.spawnflags & SILENT))
	{
		precache_sound ("ambience/hum1.wav");
		o = (self.mins + self.maxs)*0.5;
		ambientsound (o, "ambience/hum1.wav",0.5 , ATTN_STATIC);
	}
};

/*
==============================================================================

trigger_setskill

==============================================================================
*/

void() trigger_skill_touch =
{
	if (other.classname != "player")
		return;
		
	cvar_set ("skill", self.message);
};

/*QUAKED trigger_setskill (.5 .5 .5) ?
sets skill level to the value of "message".
Only used on start map.
*/
void() trigger_setskill =
{
	InitTrigger ();
	self.touch = trigger_skill_touch;
};


/*
==============================================================================

ONLY REGISTERED TRIGGERS

==============================================================================
*/

void() trigger_onlyregistered_touch =
{
	if (other.classname != "player")
		return;
	if (self.attack_finished > time)
		return;

	self.attack_finished = time + 2;
	if (cvar("registered"))
	{
		self.message = "";
		SUB_UseTargets ();
		remove (self);
	}
	else
	{
		if (self.message != "")
		{
			centerprint (other, self.message);
			sound (other, CHAN_BODY, "misc/talk.wav", 1, ATTN_NORM);
		}
	}
};

/*QUAKED trigger_onlyregistered (.5 .5 .5) ?
Only fires if playing the registered version, otherwise prints the message
*/
void() trigger_onlyregistered =
{
	precache_sound ("misc/talk.wav");
	InitTrigger ();
	self.touch = trigger_onlyregistered_touch;
};

//============================================================================

void() hurt_on =
{
	self.solid = SOLID_TRIGGER;
	self.nextthink = -1;
};

void() hurt_touch =
{
	if (other.takedamage)
	{
		self.solid = SOLID_NOT;
		T_Damage (other, self, self, self.dmg);
		self.think = hurt_on;
		self.nextthink = time + 1;
	}

	return;
};

/*QUAKED trigger_hurt (.5 .5 .5) ?
Any object touching this will be hurt
set dmg to damage amount
defalt dmg = 5
*/
void() trigger_hurt =
{
	InitTrigger ();
	self.touch = hurt_touch;
	if (!self.dmg)
		self.dmg = 5;
};

//============================================================================

float PUSH_ONCE = 1;

void() trigger_push_touch =
{
	if (other.classname == "grenade")
		other.velocity = self.speed * self.movedir * 10;
	else if (other.health > 0)
	{
		other.velocity = self.speed * self.movedir * 10;
		if (other.classname == "player")
		{
			if (other.fly_sound < time)
			{
				other.fly_sound = time + 1.5;
				sound (other, CHAN_AUTO, "ambience/windfly.wav", 1, ATTN_NORM);
			}
		}
	}
	if (self.spawnflags & PUSH_ONCE)
		remove(self);
};


/*QUAKED trigger_push (.5 .5 .5) ? PUSH_ONCE
Pushes the player
*/
void() trigger_push =
{
	InitTrigger ();
	precache_sound ("ambience/windfly.wav");
	self.touch = trigger_push_touch;
	if (!self.speed)
		self.speed = 1000;
};

//============================================================================

void() trigger_monsterjump_touch =
{
	if ( other.flags & (FL_MONSTER | FL_FLY | FL_SWIM) != FL_MONSTER )
		return;

// set XY even if not on ground, so the jump will clear lips
	other.velocity_x = self.movedir_x * self.speed;
	other.velocity_y = self.movedir_y * self.speed;
	
	if ( !(other.flags & FL_ONGROUND) )
		return;
	
	other.flags = other.flags - FL_ONGROUND;

	other.velocity_z = self.height;
};

/*QUAKED trigger_monsterjump (.5 .5 .5) ?
Walking monsters that touch this will jump in the direction of the trigger's angle
"speed" default to 200, the speed thrown forward
"height" default to 200, the speed thrown upwards
*/
void() trigger_monsterjump =
{
	if (!self.speed)
		self.speed = 200;
	if (!self.height)
		self.height = 200;
	if (self.angles == '0 0 0')
		self.angles = '0 360 0';
	InitTrigger ();
	self.touch = trigger_monsterjump_touch;
};
Dr. Shadowborg
InsideQC Staff
Posts: 1120
Joined: Sat Oct 16, 2004 3:34 pm

Re: Frikbot Item desirability

Post by Dr. Shadowborg »

Lightning Hunter wrote:I think the new teleport code actually made the bots worse in certain maps with lots of teleporters. An example is XL1DM1:
http://fbe.am/unu
I tested this map with the old code, and the bots didn't mess up once in 5 minutes. After updating the code, they messed up almost every other teleport. I'll post my current client.qc and triggers.qc, so you can check to see if I modified them correctly.
Egad, that map is the worst I've seen yet in regards to the bot having trouble with clipping and teleports.

Okay, lets try a different method of teleportation detection:

In bot.qc, right below where we define .float real_teleport_time;, add .entity bot_teleport_dest; so that it looks like this:

Code: Select all

// DRS: Extra shizz here
float(float num) mathlib_cos;
.float dontshoot;
.float real_teleport_time;
.entity bot_teleport_dest;
Next, open triggers.qc, within teleport_touch, right below spawn_tdeath(t.origin, other);, add this:

Code: Select all

//  DRS: Bot Teleport Fix
    other.bot_teleport_dest = t;
Next, in bot_ai.qc, within the bot_check_lost function, replace the teleporter target fix block with this:

Code: Select all

	// DRS: Teleporter target fix
	if(targ.classname == "trigger_teleport" && (self.bot_teleport_dest.classname == "info_teleport_destination" && vlen(self.bot_teleport_dest.origin - self.origin) < 32))
	{
	 self.bot_teleport_dest = world;
	 bot_lost(targ, TRUE);
	}
Give that a try and see if it helps any. It should make them a little more determined to go through a teleporter, and if they don't make it the first time they'll try again.

Examination of spawning code indicates that igor9 actually did (mostly) install a "spawn players away from other players" function into his randomizer. (it's set to 84qu, then 32qu, then just pick a spot if those checks fail) You can enhance this to check for larger radius and visible checks by doing this:

In client.qc, right before SelectSpawnPoint, put:

Code: Select all

float (entity targ) visible;
Next, within SelectSpawnPoint, increase the findradius from 84qu to 256qu or however far you want like below:

Code: Select all

				thing = findradius(spot.origin, 256);		// search for players
Next, within the while loop just below that findradius, change the if check to look like this:

Code: Select all

					if ((thing.classname == "player") && (thing.health > 0) && visible(thing))	// player found
						pcount = 1;
It should then attempt to find spawnpoints well away from players if at all possible.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

The teleporter code definitely works a lot better now! I saw them mess up on one teleporter in XL1DM1, but the waypoint was partially to blame. I moved it around, and didn't see the issue again. I'll continue to test and let you know if I spot any more problems. 8)

I can't tell if the spawn radius code is working correctly or not. I chose a fairly large map and only added 2 bots. It seems that with only two bots, they should always spawn far away from each other. Once or twice, I saw a bot get spawn-killed with the lightning gun. However, I'm not exactly sure how large of a radius 384qu is (that's what I tried setting it to). How far can the Lightning Gun shoot? Can it manage to shoot a bot 384qu away?

Edit: I just now set the spawn radius to 768, and still saw a bot get spawn killed with a lightning gun. Surely it can't shoot that far? Maybe I added the code wrong.

Here is my SelectSpawnPoint, so you can make sure I have everything added correctly:

Code: Select all

/*
============
SelectSpawnPoint

Returns the entity to spawn at
============
*/
float (entity targ) visible;

entity() SelectSpawnPoint =
{
	local	entity spot;
	local	entity thing;
	local	float  pcount;
	
// testinfo_player_start is only found in regioned levels
	spot = find (world, classname, "testplayerstart");
	if (spot)
		return spot;
		
// choose a info_player_deathmatch point
	if (coop)
	{
		lastspawn = find(lastspawn, classname, "info_player_coop");
		if (lastspawn == world)
			lastspawn = find (lastspawn, classname, "info_player_start");
		if (lastspawn != world)
			return lastspawn;
	}
	else if (deathmatch)
	{
	//~ random respawn start
				local float	freespots, totalspots;
		local entity	lastfreespot;

		totalspots = 0;

		// loop through all spots to find widely free spots (radius 84)
		freespots = 0;
		lastfreespot = world;

		spot = find(world, classname, "info_player_deathmatch");
		while (spot)
		{
			totalspots = totalspots + 1;

			if (spot != lastspawn)
			{
				pcount = 0;
				thing = findradius(spot.origin, 384);		// search for players
				while ( (thing!=world) && (pcount == 0) )
				{
               if ((thing.classname == "player") && (thing.health > 0) && visible(thing))   // player found
                  pcount = 1;
					thing = thing.chain;
				}
				if (!pcount)					// this is a free spot
				{
					spot.goalentity = lastfreespot;		// create reverse free spot list
					lastfreespot = spot;			// begin of reverse free spot list
					freespots = freespots + 1;
				}
			}

			// Get the next spot in the chain
			spot = find(spot, classname, "info_player_deathmatch");
		}

		if (totalspots)
		{
			if (freespots)
			{
				// We now have the number of frre spots available on the map
				// Generate a random number between 0 and (freespots - 1)
				freespots = freespots - 0.01;	// random() result varies between 0.0 and (n-1).99
				freespots = floor(random() * freespots);

				spot = lastfreespot;
				while (freespots)
				{
					spot = spot.goalentity;
					freespots = freespots - 1;
				}

				lastspawn = spot;
				return spot;
			}

			// ack, they are all guarded
			// loop through all spots to find free spots (radius 32)
			freespots = 0;
			lastfreespot = world;

			spot = find(world, classname, "info_player_deathmatch");
			while (spot)
			{
				if (spot != lastspawn)
				{
					pcount = 0;
					thing = findradius(spot.origin, 32);		// search for players
					while ( (thing!=world) && (pcount == 0) )
					{
						if ((thing.classname == "player") && (thing.health > 0))	// player found
							pcount = 1;
						thing = thing.chain;
					}
					if (!pcount)					// this is a free spot
					{
						spot.goalentity = lastfreespot;		// create reverse free spot list
						lastfreespot = spot;			// begin of reverse free spot list
						freespots = freespots + 1;
					}
				}

				// Get the next spot in the chain
				spot = find(spot, classname, "info_player_deathmatch");
			}

			if (freespots)
			{
				// We now have the number of free spots available on the map
				// Generate a random number between 0 and (freespots - 1)
				freespots = freespots - 0.01;	// random() result varies between 0.0 and (n-1).99
				freespots = floor(random() * freespots);

				spot = lastfreespot;
				while (freespots)
				{
					spot = spot.goalentity;
					freespots = freespots - 1;
				}

				lastspawn = spot;
				return spot;
			}

			// ack, they are all full, just pick one at random
			// Generate a random number between 0 and (totalspots - 1)
			totalspots = totalspots - 0.01;	// random() result varies between 0.0 and (n-1).99
			totalspots = floor(random() * totalspots);

			spot = find(world, classname, "info_player_deathmatch");
			while (totalspots)
			{
				totalspots = totalspots - 1;
				spot = find(spot, classname, "info_player_deathmatch");
			}

			lastspawn = spot;
			return spot;
		}
/*  		spot = lastspawn;
		while (1)
		{
			spot = find(spot, classname, "info_player_deathmatch");
			if (spot != world)
			{
				if (spot == lastspawn)
					return lastspawn;
				pcount = 0;
				thing = findradius(spot.origin, 32);
				while(thing)
				{
					if (thing.classname == "player")
						pcount = pcount + 1;
					thing = thing.chain;
				}
				if (pcount == 0)
				{
					lastspawn = spot;
					return spot;
				}
			}
		}
 */	//~ random respawn end
	}

	if (serverflags)
	{	// return with a rune to start
		spot = find (world, classname, "info_player_start2");
		if (spot)
			return spot;
	}
	
	spot = find (world, classname, "info_player_start");
	if (!spot)
		error ("PutClientInServer: no info_player_start on level");
	
	return spot;
};
Dr. Shadowborg
InsideQC Staff
Posts: 1120
Joined: Sat Oct 16, 2004 3:34 pm

Re: Frikbot Item desirability

Post by Dr. Shadowborg »

Your code looks right.

Lightning Gun has a range of about 604qu, so that must mean that map has some architecture thats blocking the visible trace. (it only uses one trace) I suppose checking three times from origin, head and feet might work, or perhaps using fisible, but that might generate excessive tracelines.

Show me the map, I'll see if I can work out whats going wrong.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

I am using XL1DM3 to test:
http://fbe.am/uon

The map is large enough that bots should spawn far away from each other, but not overly large (so there is a higher chance of spawn killing if the code is not working right).
Dr. Shadowborg
InsideQC Staff
Posts: 1120
Joined: Sat Oct 16, 2004 3:34 pm

Re: Frikbot Item desirability

Post by Dr. Shadowborg »

Lightning Hunter wrote:I am using XL1DM3 to test:
http://fbe.am/uon

The map is large enough that bots should spawn far away from each other, but not overly large (so there is a higher chance of spawn killing if the code is not working right).
Code was working, but there are limitations to visibility checking.

Use this:

In client.qc, replace your SelectSpawnPoint function with this:

Code: Select all

entity() SelectSpawnPoint =
{
	local	entity spot;
	local	entity thing;
	local	float  pcount;
	
// testinfo_player_start is only found in regioned levels
	spot = find (world, classname, "testplayerstart");
	if (spot)
		return spot;
		
// choose a info_player_deathmatch point
	if (coop)
	{
		lastspawn = find(lastspawn, classname, "info_player_coop");
		if (lastspawn == world)
			lastspawn = find (lastspawn, classname, "info_player_start");
		if (lastspawn != world)
			return lastspawn;
	}
	else if (deathmatch)
	{
	//~ random respawn start
		local float	freespots, totalspots;
		local entity	lastfreespot;

		totalspots = 0;

		// DRS: This loop checks to find an area clear of players
		// within 1000qu.  No visibility checks are performed in order
		// to assure that you are in a relativly unpopulated area...
		freespots = 0;
		lastfreespot = world;

		spot = find(world, classname, "info_player_deathmatch");
		while (spot)
		{
			totalspots = totalspots + 1;

			if (spot != lastspawn)
			{
				pcount = 0;
				thing = findradius(spot.origin, 1000);		// search for players // 84
				while ( (thing!=world) && (pcount == 0) )
				{
					if ((thing.classname == "player") && (thing.health > 0))	// player found
						pcount = 1;
					thing = thing.chain;
				}
				if (!pcount)					// this is a free spot
				{
					spot.goalentity = lastfreespot;		// create reverse free spot list
					lastfreespot = spot;			// begin of reverse free spot list
					freespots = freespots + 1;
				}
			}

			// Get the next spot in the chain
			spot = find(spot, classname, "info_player_deathmatch");
		}

		if (totalspots)
		{
			if (freespots)
			{
				// We now have the number of frre spots available on the map
				// Generate a random number between 0 and (freespots - 1)
				freespots = freespots - 0.01;	// random() result varies between 0.0 and (n-1).99
				freespots = floor(random() * freespots);

				spot = lastfreespot;
				while (freespots)
				{
					spot = spot.goalentity;
					freespots = freespots - 1;
				}
				lastspawn = spot;
				return spot;
			}
		}
			
		// DRS: This loop checks to find an area clear of players
		// within 1000qu.  Visibilty check is performed to at least
		// try and give you a relatively clear start...a failsafe for
		// the first loop, or if you are playing on a small level.
		freespots = 0;
		lastfreespot = world;

		spot = find(world, classname, "info_player_deathmatch");
		while (spot)
		{
			totalspots = totalspots + 1;

			if (spot != lastspawn)
			{
				pcount = 0;
				thing = findradius(spot.origin, 1000);		// search for players // 84
				while ( (thing!=world) && (pcount == 0) )
				{
					if ((thing.classname == "player") && (thing.health > 0) && visible(thing))	// player found
						pcount = 1;
					thing = thing.chain;
				}
				if (!pcount)					// this is a free spot
				{
					spot.goalentity = lastfreespot;		// create reverse free spot list
					lastfreespot = spot;			// begin of reverse free spot list
					freespots = freespots + 1;
				}
			}

			// Get the next spot in the chain
			spot = find(spot, classname, "info_player_deathmatch");
		}

		if (totalspots)
		{
			if (freespots)
			{
				// We now have the number of frre spots available on the map
				// Generate a random number between 0 and (freespots - 1)
				freespots = freespots - 0.01;	// random() result varies between 0.0 and (n-1).99
				freespots = floor(random() * freespots);

				spot = lastfreespot;
				while (freespots)
				{
					spot = spot.goalentity;
					freespots = freespots - 1;
				}
				lastspawn = spot;
				return spot;
			}
		}
			// DRS: This failsafe attempts to find a spot without
			// Telefragging.
			// ack, they are all guarded
			// loop through all spots to find free spots (radius 32)
			freespots = 0;
			lastfreespot = world;

			spot = find(world, classname, "info_player_deathmatch");
			while (spot)
			{
				if (spot != lastspawn)
				{
					pcount = 0;
					thing = findradius(spot.origin, 32);		// search for players
					while ( (thing!=world) && (pcount == 0) )
					{
						if ((thing.classname == "player") && (thing.health > 0))	// player found
							pcount = 1;
						thing = thing.chain;
					}
					if (!pcount)					// this is a free spot
					{
						spot.goalentity = lastfreespot;		// create reverse free spot list
						lastfreespot = spot;			// begin of reverse free spot list
						freespots = freespots + 1;
					}
				}

				// Get the next spot in the chain
				spot = find(spot, classname, "info_player_deathmatch");
			}

			if (freespots)
			{
				// We now have the number of free spots available on the map
				// Generate a random number between 0 and (freespots - 1)
				freespots = freespots - 0.01;	// random() result varies between 0.0 and (n-1).99
				freespots = floor(random() * freespots);

				spot = lastfreespot;
				while (freespots)
				{
					spot = spot.goalentity;
					freespots = freespots - 1;
				}
				lastspawn = spot;
				return spot;
			}

			// DRS: This is the last failsafe, pick a random spot, 
			// Telefragging is guaranteed
			// ack, they are all full, just pick one at random
			// Generate a random number between 0 and (totalspots - 1)
			totalspots = totalspots - 0.01;	// random() result varies between 0.0 and (n-1).99
			totalspots = floor(random() * totalspots);

			spot = find(world, classname, "info_player_deathmatch");
			while (totalspots)
			{
				totalspots = totalspots - 1;
				spot = find(spot, classname, "info_player_deathmatch");
			}
			lastspawn = spot;
			return spot;
		
 	//~ random respawn end
	}

	if (serverflags)
	{	// return with a rune to start
		spot = find (world, classname, "info_player_start2");
		if (spot)
			return spot;
	}
	
	spot = find (world, classname, "info_player_start");
	if (!spot)
		error ("PutClientInServer: no info_player_start on level");
	
	return spot;
};
This first checks to see if there are any players within 1000qu, if all spawnpoints have players within 1000qu, try checking 1000qu with visible checks, if that fails, fall back on random anti-telefrag spawnpoints, if THAT fails, just pick a random point and telefrag somebody.

Keep in mind that this will not work quite as well on smaller levels, and becomes less effective the more populated a level becomes.
Lightning Hunter
Posts: 169
Joined: Wed Nov 28, 2007 6:15 am

Re: Frikbot Item desirability

Post by Lightning Hunter »

That code looks much better! I definitely noticed a difference in XL1DM3. Even with 4 bots, they rarely ever spawned within firing radius of another bot.

I guess that concludes the list of bugs! The only one remaining is that bots sometimes try to get to the wrong waypoint when they die, but I can't tell you how to reproduce this, or why exactly it is even happening. It may not have anything to do with the bot remembering an old waypoint after respawning. I noticed that sometimes when a bot first spawns, it runs up against a wall as if trying to get to the incorrect waypoint before resetting itself and getting on track. This is rare enough that you could probably spawn 100 bots and not even see it happen. With all the hours of testing I do with bots and waypoints, I just happen to come across this bug every now and then. I will understand if you chose not to bother fixing it, however.

Did you see my post about the Painkeep mod? It's ok if you decide not to implement your updates to the Painkeep mod of course. I just thought it would be a nice addition to the Frikbot definitive pack in the next release. If you decide to take a look at it, I uploaded my whole painkeep folder for you. It contains the source (located in the fbxpk_src folder), the main game files, and all the maps I have waypointed for the mod. I may waypoint even more maps if the code is updated to reflect your changes.
Download: https://www.mediafire.com/?ccxj87cvecsigue

I must thank you again for all your hard work, Shadowborg. A lot of these bugs really have been on my nerves for a long time, and I feel that the next release of the Definitive Frikbot Waypack will be more complete with these fixes. I have some more work to do on waypoints, but it won't be too long now. 8)
Dr. Shadowborg
InsideQC Staff
Posts: 1120
Joined: Sat Oct 16, 2004 3:34 pm

Re: Frikbot Item desirability

Post by Dr. Shadowborg »

Eep, giant download file. On mediafire. Mediafire doesn't work so good for me.

I already have the painkeep datafiles, so if possible could you just upload your qc source and waypoints?

Keep in mind I'm only going to impliment the bot fixes, nothing more.
Post Reply