-//bot configuration: name model skin shirt pants team keyboard use moving dodging ping weapon use aggressivity range aiming calmhand mouse fightthink aithink
-//default team values (team-override): 1 = red, 2 = blue, 3 = yellow, 4 = pink
-//use -1 for shirt-color or pants-color to get random colors
-Hellfire ignis 0 4 0 0 0 0 -0.5 -1 1 1 -0.5 -1 -1 2 0.5 -1
-Toxic gakmasked 0 14 7 0 -1 -1.5 -0.5 0 1 0 0 0 2 -0.5 -0.5 1
-Discovery erebus 0 2 6 0 0 -1 -0.5 -0.5 1 -0.5 0.5 1.5 -0.5 -1 1 0.5
-Pegasus umbra 0 13 11 0 1 1 1 1 -1 0 0.5 0 -2 0 -1 0
-Eureka umbra 0 12 7 0 0 0 -1.5 -0.5 -0.5 0 0 0 0 -0.5 1.5 1.5
-Airhead ignis 0 11 1 0 -1 -1.5 -1 -0.5 1 1 -1 1 -0.5 1 0.5 0
-Gator gak 0 3 10 0 0 1 0 0.5 -0.5 0.5 -0.5 -1 0 0 -0.5 0
-Delirium gakmasked 0 8 12 0 0 -1 -1 -1 0 2 0 1 0 2 -1 -1
-Death gakmasked 0 4 11 0 -0.5 0 0 1 -0.5 0 1 0 0 0 0 0
-Scorcher ignismasked 0 13 13 0 0 -1 0 -0.5 0.5 1 0 1 -2 1 0 0
-Necrotic nyx 0 12 14 0 0 0 0 1 0 -1 -0.5 -1 1 0 0 0
-Dominator nyx 0 3 9 0 0 0 0 2 -1 0 0 0 -1 0 0 0
-Thunderstorm erebus 0 13 6 0 0 0 0 -0.5 -1 1 0.5 0.5 -0.5 0 0.5 0
-Mystery pyria 0 9 14 0 1 1 1 1 -1 -1 0 1 1 -2 -1 -1
-Lion ignismasked 0 0 4 0 1 1.5 2 -1 -1 -1 1 0 1 -0.5 -1 -1
-Sensible seraphina 0 9 9 0 0 0 0.5 -1 0 -1 0 -1 2.5 -1.5 1 0.5
-Shadow seraphinamasked 0 4 8 0 -0.5 2 1 0 0 -1 0 -1 0 1 -1 -0.5
-Resurrection umbra 0 1 1 0 0 -0.5 -0.5 0 2 -1 -1 -1 0 -1 1 1
+//bot configuration:
+// default team values (team-override): 1 = red, 2 = blue, 3 = yellow, 4 = pink
+// use -1 for shirt-color or pants-color to get random colors
+//name model skin shirt pants team keyboard moving dodging ping weapon aggres range aiming calmhand mouse fightthink aithink
+Hellfire ignis 0 4 0 0 0 0 -0.5 -1 1 1 -0.5 -1 -1 2 0.5 -1
+Toxic gakmasked 0 14 7 0 -1 -1.5 -0.5 0 1 0 0 0 2 -0.5 -0.5 1
+Discovery erebus 0 2 6 0 0 -1 -0.5 -0.5 1 -0.5 0.5 1.5 -0.5 -1 1 0.5
+Pegasus umbra 0 13 11 0 1 1 1 1 -1 0 0.5 0 -2 0 -1 0
+Eureka umbra 0 12 7 0 0 0 -1.5 -0.5 -0.5 0 0 0 0 -0.5 1.5 1.5
+Airhead ignis 0 11 1 0 -1 -1.5 -1 -0.5 1 1 -1 1 -0.5 1 0.5 0
+Gator gak 0 3 10 0 0 1 0 0.5 -0.5 0.5 -0.5 -1 0 0 -0.5 0
+Delirium gakmasked 0 8 12 0 0 -1 -1 -1 0 2 0 1 0 2 -1 -1
+Death gakmasked 0 4 11 0 -0.5 0 0 1 -0.5 0 1 0 0 0 0 0
+Scorcher ignismasked 0 13 13 0 0 -1 0 -0.5 0.5 1 0 1 -2 1 0 0
+Necrotic nyx 0 12 14 0 0 0 0 1 0 -1 -0.5 -1 1 0 0 0
+Dominator nyx 0 3 9 0 0 0 0 2 -1 0 0 0 -1 0 0 0
+Thunderstorm erebus 0 13 6 0 0 0 0 -0.5 -1 1 0.5 0.5 -0.5 0 0.5 0
+Mystery pyria 0 9 14 0 1 1 1 1 -1 -1 0 1 1 -2 -1 -1
+Lion ignismasked 0 0 4 0 1 1.5 2 -1 -1 -1 1 0 1 -0.5 -1 -1
+Sensible seraphina 0 9 9 0 0 0 0.5 -1 0 -1 0 -1 2.5 -1.5 1 0.5
+Shadow seraphinamasked 0 4 8 0 -0.5 2 1 0 0 -1 0 -1 0 1 -1 -0.5
+Resurrection umbra 0 1 1 0 0 -0.5 -0.5 0 2 -1 -1 -1 0 -1 1 1
ATTRIB(Armor, m_mins, vector, '-16 -16 0');
ATTRIB(Armor, m_maxs, vector, '16 16 48');
ATTRIB(Armor, m_pickupevalfunc, float(entity player, entity item), healtharmor_pickupevalfunc);
- ATTRIB(Armor, m_botvalue, int, 3000);
+ ATTRIB(Armor, m_botvalue, int, 5000);
#endif
ENDCLASS(Armor)
ATTRIB(Health, m_mins, vector, '-16 -16 0');
ATTRIB(Health, m_maxs, vector, '16 16 48');
ATTRIB(Health, m_pickupevalfunc, float(entity player, entity item), healtharmor_pickupevalfunc);
- ATTRIB(Health, m_botvalue, int, 2500);
+ ATTRIB(Health, m_botvalue, int, 5000);
#endif
ENDCLASS(Health)
return 0;
return ammo_pickupevalfunc(player, item);
}
- return item.bot_pickupbasevalue;
+
+ // reduce weapon value if bot already got a good arsenal
+ float c = 1;
+ int weapons_value = 0;
+ FOREACH(Weapons, it != WEP_Null && (player.weapons & it.m_wepset), {
+ weapons_value += it.bot_pickupbasevalue;
+ });
+ c -= bound(0, weapons_value / 20000, 1) * 0.5;
+
+ return item.bot_pickupbasevalue * c;
}
float ammo_pickupevalfunc(entity player, entity item)
if (need_shells)
if (item.ammo_shells)
if (player.ammo_shells < g_pickup_shells_max)
- c = (player.ammo_shells + item.ammo_shells) / player.ammo_shells;
+ c = item.ammo_shells / player.ammo_shells;
if (need_nails)
if (item.ammo_nails)
if (player.ammo_nails < g_pickup_nails_max)
- c = (player.ammo_nails + item.ammo_nails) / player.ammo_nails;
+ c = item.ammo_nails / player.ammo_nails;
if (need_rockets)
if (item.ammo_rockets)
if (player.ammo_rockets < g_pickup_rockets_max)
- c = (player.ammo_rockets + item.ammo_rockets) / player.ammo_rockets;
+ c = item.ammo_rockets / player.ammo_rockets;
if (need_cells)
if (item.ammo_cells)
if (player.ammo_cells < g_pickup_cells_max)
- c = (player.ammo_cells + item.ammo_cells) / player.ammo_cells;
+ c = item.ammo_cells / player.ammo_cells;
if (need_plasma)
if (item.ammo_plasma)
if (player.ammo_plasma < g_pickup_plasma_max)
- c = (player.ammo_plasma + item.ammo_plasma) / player.ammo_plasma;
+ c = item.ammo_plasma / player.ammo_plasma;
if (need_fuel)
if (item.ammo_fuel)
if (player.ammo_fuel < g_pickup_fuel_max)
- c = (player.ammo_fuel + item.ammo_fuel) / player.ammo_fuel;
+ c = item.ammo_fuel / player.ammo_fuel;
- rating *= min(3, c);
+ rating *= min(2, c);
if(wpn)
- // Skilled bots will grab more
- rating += wpn.bot_pickupbasevalue * (0.1 + 0.1 * bound(0, skill / 10, 1));
+ rating += wpn.bot_pickupbasevalue * 0.1;
return rating;
}
+.int item_group;
+.int item_group_count;
float healtharmor_pickupevalfunc(entity player, entity item)
{
float c = 0;
float rating = item.bot_pickupbasevalue;
- if (item.armorvalue)
+ float itemarmor = item.armorvalue;
+ float itemhealth = item.health;
+ if(item.item_group)
+ {
+ itemarmor *= min(4, item.item_group_count);
+ itemhealth *= min(4, item.item_group_count);
+ }
+ if (itemarmor)
if (player.armorvalue < item.max_armorvalue)
- c = (player.armorvalue + player.health + item.armorvalue) / (max(1, player.armorvalue + player.health));
- if (item.health)
+ c = itemarmor / max(1, player.armorvalue * 2/3 + player.health * 1/3);
+ if (itemhealth)
if (player.health < item.max_health)
- c = (player.health + item.health) / (max(1, player.health));
+ c = itemhealth / max(1, player.health);
- rating *= min(3, c);
+ rating *= min(2, c);
return rating;
}
delete(this);
return;
}
+
+ setItemGroup(this);
}
void StartItem(entity this, GameItem def)
);
}
+#define IS_SMALL(def) ((def.instanceOfHealth && def == ITEM_HealthSmall) || (def.instanceOfArmor && def == ITEM_ArmorSmall))
+int group_count = 1;
+
+void setItemGroup(entity this)
+{
+ if(!IS_SMALL(this.itemdef))
+ return;
+
+ FOREACH_ENTITY_RADIUS(this.origin, 120, (it != this) && IS_SMALL(it.itemdef),
+ {
+ if(!this.item_group)
+ {
+ if(!it.item_group)
+ {
+ it.item_group = group_count;
+ group_count++;
+ }
+ this.item_group = it.item_group;
+ }
+ else // spawning item is already part of a item_group X
+ {
+ if(!it.item_group)
+ it.item_group = this.item_group;
+ else if(it.item_group != this.item_group) // found an item near the spawning item that is part of a different item_group Y
+ {
+ int grY = it.item_group;
+ // move all items of item_group Y to item_group X
+ FOREACH_ENTITY(IS_SMALL(it.itemdef),
+ {
+ if(it.item_group == grY)
+ it.item_group = this.item_group;
+ });
+ }
+ }
+ });
+}
+
+void setItemGroupCount()
+{
+ for (int k = 1; k <= group_count; k++)
+ {
+ int count = 0;
+ FOREACH_ENTITY(IS_SMALL(it.itemdef) && it.item_group == k, { count++; });
+ if (count)
+ FOREACH_ENTITY(IS_SMALL(it.itemdef) && it.item_group == k, { it.item_group_count = count; });
+ }
+}
+
spawnfunc(item_rockets)
{
if(!this.ammo_rockets)
.entity itemdef;
void _StartItem(entity this, entity def, float defaultrespawntime, float defaultrespawntimejitter);
+void setItemGroup(entity this);
+void setItemGroupCount();
float GiveWeapon(entity e, float wpn, float op, float val);
entity bot = spawnclient();
if (bot)
{
+ setItemGroupCount();
currentbots = currentbots + 1;
bot_setnameandstuff(bot);
ClientConnect(bot);
if(autocvar_bot_god)
this.flags |= FL_GODMODE;
- this.bot_nextthink = this.bot_nextthink + autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill);
- if(this.bot_nextthink < time)
- this.bot_nextthink = time + autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill);
+ this.bot_nextthink = max(time, this.bot_nextthink) + max(0.01, autocvar_bot_ai_thinkinterval * pow(0.5, this.bot_aiskill) * min(14 / (skill + 14), 1));
+
//if (this.bot_painintensity > 0)
// this.bot_painintensity = this.bot_painintensity - (skill + 1) * 40 * frametime;
.float bot_pickup;
.float bot_pickupbasevalue;
+.bool bot_pickup_respawning;
.float bot_canfire;
.float bot_strategytime;
maxspeed = autocvar_sv_maxspeed;
- if(this.aistatus & AI_STATUS_DANGER_AHEAD)
+ if(this.aistatus & AI_STATUS_RUNNING && vdist(this.velocity, <, autocvar_sv_maxspeed * 0.75)
+ || this.aistatus & AI_STATUS_DANGER_AHEAD)
{
this.aistatus &= ~AI_STATUS_RUNNING;
PHYS_INPUT_BUTTON_JUMP(this) = false;
// Run only to visible goals
if(IS_ONGROUND(this))
- if(this.speed==maxspeed)
+ if(vlen(this.velocity - eZ * this.velocity.z) >= autocvar_sv_maxspeed) // if -really- running
if(checkpvs(this.origin + this.view_ofs, this.goalcurrent))
{
this.bot_lastseengoal = this.goalcurrent;
vector m2;
vector evadeobstacle;
vector evadelava;
- float s;
float maxspeed;
vector gco;
//float dist;
evadeobstacle = '0 0 0';
evadelava = '0 0 0';
+ makevectors(this.v_angle.y * '0 1 0');
if (this.waterlevel)
{
if(this.waterlevel>WATERLEVEL_SWIMMING)
PHYS_INPUT_BUTTON_JUMP(this) = false;
}
dir = normalize(flatdir);
- makevectors(this.v_angle.y * '0 1 0');
}
else
{
+ float s;
+ vector offset;
if(this.aistatus & AI_STATUS_OUT_WATER)
this.aistatus &= ~AI_STATUS_OUT_WATER;
// jump if going toward an obstacle that doesn't look like stairs we
// can walk up directly
- tracebox(this.origin, this.mins, this.maxs, this.origin + this.velocity * 0.2, false, this);
+ offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.2 : v_forward * 32);
+ tracebox(this.origin, this.mins, this.maxs, this.origin + offset, false, this);
if (trace_fraction < 1)
if (trace_plane_normal.z < 0.7)
{
s = trace_fraction;
- tracebox(this.origin + stepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + stepheightvec, false, this);
+ tracebox(this.origin + stepheightvec, this.mins, this.maxs, this.origin + offset + stepheightvec, false, this);
if (trace_fraction < s + 0.01)
if (trace_plane_normal.z < 0.7)
{
s = trace_fraction;
- tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, this.origin + this.velocity * 0.2 + jumpstepheightvec, false, this);
+ tracebox(this.origin + jumpstepheightvec, this.mins, this.maxs, this.origin + offset + jumpstepheightvec, false, this);
if (trace_fraction > s)
PHYS_INPUT_BUTTON_JUMP(this) = true;
}
}
// avoiding dangers and obstacles
- vector dst_ahead = this.origin + this.view_ofs + this.velocity * 0.5;
+ offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.5 : v_forward * 32);
+ vector dst_ahead = this.origin + this.view_ofs + offset;
vector dst_down = dst_ahead - '0 0 3000';
// Look ahead
// (only when the bot is on the ground or jumping intentionally)
this.aistatus &= ~AI_STATUS_DANGER_AHEAD;
+ bool unreachable = false;
+ s = CONTENT_SOLID;
if(trace_fraction == 1 && this.jumppadcount == 0 && !this.goalcurrent.wphardwired )
if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
{
if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
{
if (gco.z > this.origin.z + jumpstepheightvec.z)
- {
+ {
// the goal is probably on an upper platform, assume bot can't get there
- LOG_TRACE("bot ", this.netname, " avoided the goal ", this.goalcurrent.classname, " ", etos(this.goalcurrent), " because it led to a dangerous path; goal stack cleared");
- navigation_clearroute(this);
- this.bot_strategytime = 0;
+ unreachable = true;
}
else
evadelava = normalize(this.velocity) * -1;
evadelava.z = 0;
makevectors(this.v_angle.y * '0 1 0');
- if(evadeobstacle!='0 0 0'||evadelava!='0 0 0')
+ if(evadeobstacle || evadelava || (s == CONTENT_WATER))
+ {
this.aistatus |= AI_STATUS_DANGER_AHEAD;
+ if(IS_PLAYER(this.goalcurrent))
+ unreachable = true;
+ }
+ if(unreachable)
+ {
+ navigation_clearroute(this);
+ this.bot_strategytime = 0;
+ }
}
dodge = havocbot_dodge(this);
{
rating = 0;
+ if(!it.solid)
+ {
+ if(!it.scheduledrespawntime)
+ continue;
+ if(it.respawntime < 30 || (it.respawntimejitter && !it.itemdef.instanceOfPowerup))
+ continue;
+
+ float t = 0;
+ if(it.itemdef.instanceOfPowerup)
+ t = bound(0, skill / 10, 1) * 6;
+ else if(skill >= 9)
+ t = 4;
+
+ if(time < it.scheduledrespawntime - t)
+ continue;
+
+ it.bot_pickup_respawning = true;
+ }
o = (it.absmin + it.absmax) * 0.5;
- if(!it.solid || vdist(o - org, >, sradius) || (it == this.ignoregoal && time < this.ignoregoaltime) )
+ if(vdist(o - org, >, sradius) || (it == this.ignoregoal && time < this.ignoregoaltime) )
continue;
// Check if the item can be picked up safely
});
}
+#define BOT_RATING_ENEMY 2500
void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius)
{
if (autocvar_bot_nofire)
if(this.waterlevel>WATERLEVEL_WETFEET)
return;
- int t;
+ ratingscale = ratingscale * 0.00005; // enemies are rated around 20000 already
+ float t;
FOREACH_CLIENT(IS_PLAYER(it) && bot_shouldattack(this, it), LAMBDA(
// TODO: Merge this logic with the bot_shouldattack function
if(vdist(it.origin - org, <, 100) || vdist(it.origin - org, >, sradius))
continue;
*/
- if((it.flags & FL_INWATER) || (it.flags & FL_PARTIALGROUND))
- continue;
-
- // not falling
- if(!IS_ONGROUND(it))
+ t = ((this.health + this.armorvalue) - (it.health + it.armorvalue)) / 150;
+ t = bound(0, 1 + t, 3);
+ if (skill > 3)
{
- traceline(it.origin, it.origin + '0 0 -1500', true, NULL);
- t = pointcontents(trace_endpos + '0 0 1');
- if(t != CONTENT_SOLID )
- if(t == CONTENT_WATER || t == CONTENT_SLIME || t == CONTENT_LAVA)
- continue;
- if(tracebox_hits_trigger_hurt(it.origin, it.mins, it.maxs, trace_endpos))
- continue;
+ if (time < this.strength_finished - 1) t += 0.5;
+ if (time < it.strength_finished - 1) t -= 0.5;
}
-
- // TODO: rate waypoints near the targeted player at that moment, instead of the player itself
- // adding a player as a goal seems to be quite dangerous, especially on space maps
- // remove hack in navigation_poptouchedgoals() after performing this change
-
- t = (this.health + this.armorvalue ) / (it.health + it.armorvalue );
- navigation_routerating(this, it, t * ratingscale, 2000);
+ t += max(0, 8 - skill) * 0.05; // less skilled bots attack more mindlessly
+ ratingscale *= t;
+ if (ratingscale > 0)
+ navigation_routerating(this, it, ratingscale * BOT_RATING_ENEMY, 2000);
));
}
// updates the best goal according to a weighted calculation of travel cost and item value of a new proposed item
void navigation_routerating(entity this, entity e, float f, float rangebias)
{
- entity nwp;
- vector o;
if (!e)
return;
if(e.blacklisted)
return;
- o = (e.absmin + e.absmax) * 0.5;
+ if (IS_PLAYER(e))
+ {
+ bool rate_wps = false;
+ if((e.flags & FL_INWATER) || (e.flags & FL_PARTIALGROUND))
+ rate_wps = true;
+
+ if(!IS_ONGROUND(e))
+ {
+ traceline(e.origin, e.origin + '0 0 -1500', true, NULL);
+ int t = pointcontents(trace_endpos + '0 0 1');
+ if(t != CONTENT_SOLID )
+ {
+ if(t == CONTENT_WATER || t == CONTENT_SLIME || t == CONTENT_LAVA)
+ rate_wps = true;
+ else if(tracebox_hits_trigger_hurt(e.origin, e.mins, e.maxs, trace_endpos))
+ return;
+ }
+ }
+
+ if(rate_wps)
+ {
+ entity theEnemy = e;
+ entity best_wp = NULL;
+ float best_dist = 10000;
+ IL_EACH(g_waypoints, vdist(it.origin - theEnemy.origin, <, 500) && vdist(it.origin - this.origin, >, 100),
+ {
+ float dist = vlen(it.origin - theEnemy.origin);
+ if (dist < best_dist)
+ {
+ best_wp = it;
+ best_dist = dist;
+ }
+ });
+ if (!best_wp)
+ return;
+ e = best_wp;
+ }
+ }
+
+ vector o = (e.absmin + e.absmax) * 0.5;
//print("routerating ", etos(e), " = ", ftos(f), " - ", ftos(rangebias), "\n");
}
}
+ entity nwp;
//te_wizspike(e.origin);
//bprint(etos(e));
//bprint("\n");
}
}
+ if(this.goalcurrent.bot_pickup_respawning)
+ {
+ if(!this.goalcurrent.solid)
+ return;
+ this.goalcurrent.bot_pickup_respawning = false;
+ }
+
// If for some reason the bot is closer to the next goal, pop the current one
if(this.goalstack01 && !wasfreed(this.goalstack01))
if(vlen2(this.goalcurrent.origin - this.origin) > vlen2(this.goalstack01.origin - this.origin))
// personality property
}
- // HACK: remove players/bots as goals, they can lead a bot to unexpected places (cliffs, lava, etc)
- // TODO: rate waypoints near the targetted player at that moment, instead of the player itself
- if(IS_PLAYER(this.goalcurrent))
- navigation_poproute(this);
-
// Loose goal touching check when running
if(this.aistatus & AI_STATUS_RUNNING)
- if(this.speed >= autocvar_sv_maxspeed) // if -really- running
if(this.goalcurrent.classname=="waypoint")
if(!(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT))
+ if(vlen(this.velocity - eZ * this.velocity.z) >= autocvar_sv_maxspeed) // if -really- running
{
if(vdist(this.origin - this.goalcurrent.origin, <, 150))
{
}
}
- while (this.goalcurrent && boxesoverlap(m1, m2, this.goalcurrent.absmin, this.goalcurrent.absmax))
+ while (this.goalcurrent && !IS_PLAYER(this.goalcurrent) && boxesoverlap(m1, m2, this.goalcurrent.absmin, this.goalcurrent.absmax))
{
if((this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT))
break;