if(autocvar_bot_god)
this.flags |= FL_GODMODE;
- this.bot_nextthink = max(time, this.bot_nextthink) + max(0.01, autocvar_bot_ai_thinkinterval * min(14 / (skill + this.bot_aiskill + 14), 1));
+ // if bot skill is high enough don't limit their think frequency
+ if (SUPERBOT)
+ this.bot_nextthink = max(time, this.bot_nextthink) + 0.005;
+ else
+ this.bot_nextthink = max(time, this.bot_nextthink) + max(0.01, autocvar_bot_ai_thinkinterval * min(14 / (skill + this.bot_aiskill + 14), 1));
if (!IS_PLAYER(this) || (autocvar_g_campaign && !campaign_bots_may_start))
{
this.dmg_save = 0;
this.dmg_inflictor = NULL;
- // calculate an aiming latency based on the skill setting
- // (simulated network latency + naturally delayed reflexes)
- //this.ping = 0.7 - bound(0, 0.05 * skill, 0.5); // moved the reflexes to bot_aimdir (under the name 'think')
- // minimum ping 20+10 random
- CS(this).ping = bound(0,0.07 - bound(0, (skill + this.bot_pingskill) * 0.005,0.05)+random()*0.01,0.65); // Now holds real lag to server, and higer skill players take a less laggy server
- // skill 10 = ping 0.2 (adrenaline)
- // skill 0 = ping 0.7 (slightly drunk)
+ // if bot skill is high enough don't assign latency to them
+ if (SUPERBOT)
+ CS(this).ping = 0;
+ else
+ {
+ // calculate an aiming latency based on the skill setting
+ // (simulated network latency + naturally delayed reflexes)
+ //this.ping = 0.7 - bound(0, 0.05 * skill, 0.5); // moved the reflexes to bot_aimdir (under the name 'think')
+ // minimum ping 20+10 random
+ CS(this).ping = bound(0,0.07 - bound(0, (skill + this.bot_pingskill) * 0.005,0.05)+random()*0.01,0.65); // Now holds real lag to server, and higher skill players take a less laggy server
+ // skill 10 = ping 0.2 (adrenaline)
+ // skill 0 = ping 0.7 (slightly drunk)
+ }
// clear buttons
PHYS_INPUT_BUTTON_ATCK(this) = false;
READSKILL(bot_thinkskill, 1, 0.5); // think skill
READSKILL(bot_aiskill, 2, 0); // "ai" skill
+ // if bot skill is high enough don't limit their skill
+ if (SUPERBOT)
+ {
+ // commented out means they're meaningless with this high skill
+ // no reason to set them, uncomment if this changes
+ //this.havocbot_keyboardskill = 10;
+ //this.bot_moveskill = 10; //midair modifier sets this to 0 to disable bhop
+ //this.bot_dodgeskill = 10;
+ //this.bot_pingskill = 10;
+ //this.bot_weaponskill = 10;
+ //this.bot_aggresskill = 10;
+ this.bot_rangepreference = 1; // no range preference modification
+ //this.bot_aimskill = 10;
+ //this.bot_offsetskill = 10;
+ //this.bot_mouseskill = 10;
+ //this.bot_thinkskill = 10;
+ //this.bot_aiskill = 10;
+ }
if (file >= 0 && argv(prio) != "")
LOG_INFOF("^1Warning^7: too many parameters for bot %s, please check format of %s", bot_name, autocvar_bot_config_file);
return selected;
}
+// Check for water/slime/lava and dangerous edges
+// (only when the bot is on the ground or jumping intentionally)
+// returns true for danger
+bool havocbot_checkdanger(entity this, vector dst_ahead)
+{
+ vector dst_down = dst_ahead - '0 0 3000';
+ traceline(this.origin + this.view_ofs, dst_ahead, true, NULL);
+
+ float s = CONTENT_SOLID;
+ if (trace_fraction == 1 && !this.jumppadcount
+ && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
+ && !(this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)))
+ if ((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
+ {
+ // Look downwards
+ traceline(dst_ahead , dst_down, true, NULL);
+ //te_lightning2(NULL, this.origin + this.view_ofs, dst_ahead); // Draw "ahead" look
+ //te_lightning2(NULL, dst_ahead, trace_endpos); // Draw "downwards" look
+ if (trace_endpos.z < this.origin.z + this.mins.z)
+ {
+ if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
+ return true;
+ else if (trace_endpos.z < min(this.origin.z + this.mins.z, this.goalcurrent.origin.z) - 100)
+ return true;
+ else
+ {
+ s = pointcontents(trace_endpos + '0 0 1');
+ if (s != CONTENT_SOLID)
+ {
+ if (s == CONTENT_LAVA || s == CONTENT_SLIME)
+ return true;
+ else if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
+ {
+ // the traceline check isn't enough but is good as optimization,
+ // when not true (most of the time) this tracebox call is avoided
+ tracebox(dst_ahead, this.mins, this.maxs, dst_down, true, this);
+ if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
+
void havocbot_movetogoal(entity this)
{
vector diff;
vector dir;
vector flatdir;
float dodge_enemy_factor = 1;
- float maxspeed;
+ float maxspeed = autocvar_sv_maxspeed;
//float dist;
vector dodge;
//if (this.goalentity)
// te_lightning2(this, this.origin, (this.goalentity.absmin + this.goalentity.absmax) * 0.5);
CS(this).movement = '0 0 0';
- maxspeed = autocvar_sv_maxspeed;
PHYS_INPUT_BUTTON_CROUCH(this) = boolean(this.goalcurrent.wpflags & WAYPOINTFLAG_CROUCH);
}
}
vector gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5;
- if (this.origin.z > gco.z && vdist(vec2(this.velocity), <, autocvar_sv_maxspeed))
+ if (this.origin.z > gco.z && vdist(vec2(this.velocity), <, maxspeed))
{
if (this.velocity.z < 0)
this.aistatus &= ~AI_STATUS_OUT_JUMPPAD;
if(this.velocity.z > 0 && this.origin.z - this.lastteleport_origin.z > (this.maxs.z - this.mins.z) * 0.5)
{
vector velxy = this.velocity; velxy_z = 0;
- if(vdist(velxy, <, autocvar_sv_maxspeed * 0.2))
+ if(vdist(velxy, <, maxspeed * 0.2))
{
LOG_TRACE("Warning: ", this.netname, " got stuck on a jumppad (velocity in xy is ", vtos(velxy), "), trying to get out of it now");
this.aistatus |= AI_STATUS_OUT_JUMPPAD;
}
else
{
- float s;
+ float s = 0;
vector offset;
if(this.aistatus & AI_STATUS_OUT_WATER)
this.aistatus &= ~AI_STATUS_OUT_WATER;
// Check for water/slime/lava and dangerous edges
// (only when the bot is on the ground or jumping intentionally)
-
offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.2 : flatdir * 32);
vector dst_ahead = this.origin + this.view_ofs + offset;
- vector dst_down = dst_ahead - '0 0 3000';
- traceline(this.origin + this.view_ofs, dst_ahead, true, NULL);
-
bool unreachable = false;
- s = CONTENT_SOLID;
- if (trace_fraction == 1 && !this.jumppadcount
- && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
- && !(this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)))
- if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
+ if (havocbot_checkdanger(this, dst_ahead))
{
- // Look downwards
- traceline(dst_ahead , dst_down, true, NULL);
- //te_lightning2(NULL, this.origin + this.view_ofs, dst_ahead); // Draw "ahead" look
- //te_lightning2(NULL, dst_ahead, trace_endpos); // Draw "downwards" look
- if(trace_endpos.z < this.origin.z + this.mins.z)
+ if (destorg.z > this.origin.z + jumpstepheightvec.z)
{
- if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
- danger_detected = true;
- else if (trace_endpos.z < min(this.origin.z + this.mins.z, this.goalcurrent.origin.z) - 100)
- danger_detected = true;
- else
- {
- s = pointcontents(trace_endpos + '0 0 1');
- if (s != CONTENT_SOLID)
- {
- if (s == CONTENT_LAVA || s == CONTENT_SLIME)
- danger_detected = true;
- else if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
- {
- // the traceline check isn't enough but is good as optimization,
- // when not true (most of the time) this tracebox call is avoided
- tracebox(dst_ahead, this.mins, this.maxs, dst_down, true, this);
- if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
- {
- if (destorg.z > this.origin.z + jumpstepheightvec.z)
- {
- // the goal is probably on an upper platform, assume bot can't get there
- unreachable = true;
- }
- else
- danger_detected = true;
- }
- }
- }
- }
+ // the goal is probably on an upper platform, assume bot can't get there
+ unreachable = true;
}
+ else
+ danger_detected = true;
}
dir = flatdir;
dodge = havocbot_dodge(this);
if (dodge)
dodge *= bound(0, 0.5 + (skill + this.bot_dodgeskill) * 0.1, 1);
+ // midair sets moveskill to 0 so avoid jumping when dodging in midair mutator
+ if (dodge.z > 0 && this.bot_moveskill == 0)
+ dodge.z = 0;
if (this.enemy)
{
traceline(this.origin, (this.enemy.absmin + this.enemy.absmax) * 0.5, true, NULL);
if (IS_PLAYER(trace_ent))
dodge_enemy_factor = bound(0, (skill + this.bot_dodgeskill) / 7, 1);
}
- // this.bot_dodgevector = dir;
- // this.bot_dodgevector_jumpbutton = PHYS_INPUT_BUTTON_JUMP(this);
+ //this.bot_dodgevector = dir;
+ //this.bot_dodgevector_jumpbutton = PHYS_INPUT_BUTTON_JUMP(this);
+
+ // don't dodge to danger
+ if (havocbot_checkdanger(this, this.origin + this.view_ofs + dodge * 32))
+ {
+ dodge = '0 0 0';
+ }
}
float ladder_zdir = 0;
CS(this).movement_y = dir * v_right * maxspeed;
CS(this).movement_z = dir * v_up * maxspeed;
+ // when high enough skill bots engage in combat they move randomly
+ if (SUPERBOT && this.aistatus == AI_STATUS_ATTACKING && !dodge)
+ {
+ if (!this.randomdirectiontime || this.randomdirectiontime + 0.35 < time)
+ {
+ // 75% chance to generate a random direction to follow for
+ // 0.3 seconds, there's a 15% chance to fail the generation
+ // and only generation attempt one every 0.35s so bots move
+ // towards their goal slightly
+ if (random() < 0.15)
+ this.randomdirection = '0 0 0';
+ else
+ {
+ // random values from -1 to 1
+ this.randomdirection.x = crandom() * maxspeed;
+ this.randomdirection.y = crandom() * maxspeed;
+ //this.randomdirection.z = crandom() * maxspeed;
+ }
+
+ this.randomdirectiontime = time;
+ }
+ if (this.randomdirectiontime + 0.3 >= time && this.randomdirection)
+ {
+ CS(this).movement_x = this.randomdirection.x;
+ CS(this).movement_y = this.randomdirection.y;
+ // no random vertical direction
+ }
+ }
+
+
// Emulate keyboard interface
if (skill < 10)
havocbot_keyboard_movement(this, destorg);
if (dodge * v_up > 0 && random() * frametime >= 0.2 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
PHYS_INPUT_BUTTON_JUMP(this) = true;
if (dodge * v_up < 0 && random() * frametime >= 0.5 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
+ {
+ if(IS_ONGROUND(this))
+ PHYS_INPUT_BUTTON_JUMP(this) = false;
this.havocbot_ducktime = time + 0.3 / bound(0.1, skill + this.bot_dodgeskill, 10);
+ PHYS_INPUT_BUTTON_CROUCH(this) = true;
+ }
}
}
}
if (time < this.havocbot_chooseenemy_finished)
return;
- this.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval;
+ // don't limit the detection interval to several seconds for bots with enough skill
+ if (SUPERBOT)
+ this.havocbot_chooseenemy_finished = time + 0.1;
+ else
+ this.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval;
+
vector eye = this.origin + this.view_ofs;
entity best = NULL;
float bestrating = autocvar_bot_ai_enemydetectionradius ** 2;
continue;
vector v = (it.absmin + it.absmax) * 0.5;
- float rating = vlen2(v - eye);
- if (rating < bestrating && bot_shouldattack(this, it))
+ float distance = vlen2(v - eye);
+
+ if (SUPERBOT)
+ {
+ if (bot_shouldattack(this, it))
+ {
+ // skilled enough bots take account target health and distance
+ float health = GetResource(it, RES_HEALTH);
+ float armor = GetResource(it, RES_ARMOR);
+ float rating = bound(50, health + armor, 250) * distance;
+ if (!best || (rating < bestrating))
+ {
+ traceline(eye, v, true, this);
+ if (trace_ent == it || trace_fraction >= 1)
+ {
+ best = it;
+ bestrating = rating;
+ }
+ }
+ }
+ }
+ else
{
- traceline(eye, v, true, this);
- if (trace_ent == it || trace_fraction >= 1)
+ if (distance < bestrating && bot_shouldattack(this, it))
{
- best = it;
- bestrating = rating;
+ traceline(eye, v, true, this);
+ if (trace_ent == it || trace_fraction >= 1)
+ {
+ best = it;
+ bestrating = distance;
+ }
}
}
});
scan_secondary_targets = true;
// restart the loop
bestrating = autocvar_bot_ai_enemydetectionradius ** 2;
+
goto scan_targets;
}
void havocbot_chooseweapon(entity this, .entity weaponentity)
{
int i;
+ float w;
// ;)
if(g_weaponarena_weapons == WEPSET(TUBA))
}
// TODO: clean this up by moving it to weapon code
- if(this.enemy==NULL)
+ if (this.enemy == NULL)
{
- // If no weapon was chosen get the first available weapon
- if(this.(weaponentity).m_weapon==WEP_Null)
- FOREACH(Weapons, it != WEP_Null, {
- if(client_hasweapon(this, it, weaponentity, true, false))
+ // Choose the first available weapon from medium range weaponlist
+ // TODO: don't do this but don't make bots hold out a blaster out either
+ for (i = 0; i < REGISTRY_COUNT(Weapons) && bot_weapons_mid[i] != -1 ; ++i){
+ w = bot_weapons_mid[i];
+ if (bot_custom_weapon)
{
- this.(weaponentity).m_switchweapon = it;
- return;
+ if (client_hasweapon(this, REGISTRY_GET(Weapons, w), weaponentity, true, false))
+ {
+ if ((this.(weaponentity).m_weapon == WEP_Null) || havocbot_chooseweapon_checkreload(this, weaponentity, w))
+ continue;
+ this.(weaponentity).m_switchweapon = REGISTRY_GET(Weapons, w);
+ return;
+ }
}
- });
+ }
+
+ // If no weapon was chosen get the first available weapon
+ if (this.(weaponentity).m_weapon == WEP_Null)
+ FOREACH(Weapons, it != WEP_Null, {
+ if (client_hasweapon(this, it, weaponentity, true, false))
+ {
+ this.(weaponentity).m_switchweapon = it;
+ return;
+ }
+ });
return;
}
if(f < 1)
return;
- float w;
float distance; distance=bound(10,vlen(this.origin-this.enemy.origin)-200,10000);
// Should it do a weapon combo?
vector havocbot_dodge(entity this)
{
// LordHavoc: disabled because this is too expensive
- return '0 0 0';
-#if 0
+ // Dr. Jaska: re-enable this but only for bots with high enough skill
+ if (!SUPERBOT)
+ return '0 0 0';
+
+#if 1
entity head;
vector dodge, v, n;
float danger, bestdanger, vl, d;
if (d > (0 - head.bot_dodgerating))
if (d < (vl * 0.2 + head.bot_dodgerating))
{
- // calculate direction and distance from the flight path, by removing the forward axis
+ // calculate direction and distance from the
+ // flight path by removing the forward axis
v = v - (n * (v * n));
danger = head.bot_dodgerating - vlen(v);
if (bestdanger < danger)
head = head.chain;
}
return dodge;
+#else
+ return '0 0 0';
#endif
}