diff --git a/CMakeLists.txt b/CMakeLists.txt index 06a4622a..b4e1c0b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ set(SRC_COMMON "${DIR_SRC}/maps_map_dm3.c" "${DIR_SRC}/maps_map_dm4.c" "${DIR_SRC}/maps_map_dm6.c" + "${DIR_SRC}/maps_map_e2m2.c" "${DIR_SRC}/maps_map_povdmm4.c" "${DIR_SRC}/marker_load.c" "${DIR_SRC}/marker_util.c" diff --git a/include/fb_globals.h b/include/fb_globals.h index 959f45c4..ad816289 100644 --- a/include/fb_globals.h +++ b/include/fb_globals.h @@ -87,6 +87,10 @@ extern gedict_t *dropper; #define FB_PREFER_ROCKET_LAUNCHER 1 #define FB_PREFER_LIGHTNING_GUN 2 +#define FB_CTF_ROLE_ATTACK 0 +#define FB_CTF_ROLE_MIDFIELD 1 +#define FB_CTF_ROLE_DEFEND 2 + #define GAME_ENABLE_POWERUPS 1 #define GAME_ENABLE_RUNES 2 #define GAME_RUNE_RJ 4 @@ -132,6 +136,9 @@ extern gedict_t *dropper; #define BOTPATH_CURLJUMP_HINT (1 << 23) #define BOTPATH_FULL_AIRCONTROL (1 << 24) #define BOTPATH_RJ_IN_PROGRESS (1 << 25) +#define HOOK (1 << 26) +#define LOOK_BUTTON (1 << 27) // Indicates path which points to a button to shoot +#define FIRE_BUTTON (1 << 28) // Set when a bot should try shooting a button #define DELIBERATE_AIR_WAIT_GROUND (DELIBERATE_AIR | WAIT_GROUND) #define SAVED_DESCRIPTION (DM6_DOOR | ROCKET_JUMP | JUMP_LEDGE | VERTICAL_PLATFORM | BOTPATH_DOOR | BOTPATH_DOOR_CLOSED | NO_DODGE) #define NOT_ROCKET_JUMP (~ROCKET_JUMP) @@ -149,6 +156,10 @@ extern gedict_t *dropper; #define MARKER_DYNAMICALLY_ADDED 256 // Added dynamically by server. Do not include in .bot file generation #define MARKER_EXPLICIT_VIEWOFFSET 512 // Viewoffset has been set by map definition and should be included in .bot file generation #define MARKER_NOTOUCH 1024 // Not touchable - used when two markers on top of each other +#define MARKER_FLAG1_DEFEND 2048 // Point used to defend flag 1 (red) +#define MARKER_FLAG2_DEFEND 4096 // Point used to defend flag 1 (blue) +#define MARKER_LOOK_BUTTON 8192 // A button can be shot from this marker - set automatically +#define MARKER_E2M2_DISCHARGE 16384 // Bots on red team will run here then discharge at the start of the map // Bot flags (FIXME: fb.state? check. consistent naming, comment with descriptions) #define CAMPBOT 1 @@ -169,6 +180,7 @@ float boomstick_only(void); float CountTeams(void); qbool EnemyDefenceless(gedict_t *self); +qbool EnemyHasFlag (gedict_t* self); qbool enemy_shaft_attack(gedict_t *self, gedict_t *enemy); float W_BestWeapon(void); @@ -181,6 +193,7 @@ float WeaponCode(float w); float crandom(void); void BotCanRocketJump(gedict_t *self); +void BotCanHook(gedict_t *self); qbool VisibleEntity(gedict_t *ent); gedict_t* IdentifyMostVisibleTeammate(gedict_t *self); float anglemod(float v); @@ -237,11 +250,11 @@ gedict_t* HigherSightFromFunction(gedict_t *from_marker, gedict_t *to_marker); gedict_t* SightFromMarkerFunction(gedict_t *from_marker, gedict_t *to_marker); gedict_t* SubZoneNextPathMarker(gedict_t *from_marker, gedict_t *to_marker); float SubZoneArrivalTime(float zone_time, gedict_t *middle_marker, gedict_t *to_marker, - qbool rl_routes); + qbool rl_routes, qbool hook_routes); float SightFromTime(gedict_t *from_marker, gedict_t *to_marker); -void ZoneMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, qbool rj_routes); +void ZoneMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, qbool rj_routes, qbool hook_routes); gedict_t* ZonePathMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, - qbool rl_jump_routes); + qbool rl_jump_routes, qbool hook_routes); // botweap.qc void FrogbotSetFirepower(gedict_t *self); @@ -267,6 +280,7 @@ void BotDamageInflictedEvent(gedict_t *attacker, gedict_t *targ); void CheckCombatJump(gedict_t *self); //void BotInLava(void); void BotPerformRocketJump(gedict_t *self); +void BotPerformHook(gedict_t *self); // botgoal.qc void UpdateGoal(gedict_t *self); @@ -288,8 +302,8 @@ void SetMarkerPathFlags(int marker_number, int path_index, int flags); void SetMarkerPath(int source_marker, int path_index, int next_marker); void SetMarkerViewOffset(int marker, float zOffset); -#define FROGBOT_PATH_FLAG_OPTIONS "w6rjva" -#define FROGBOT_MARKER_FLAG_OPTIONS "u6fbte" +#define FROGBOT_PATH_FLAG_OPTIONS "w6rjvahl" +#define FROGBOT_MARKER_FLAG_OPTIONS "u6fbte12" // added for ktx qbool fb_lg_disabled(void); @@ -343,7 +357,7 @@ void SetJumpFlag(gedict_t *player, qbool jumping, const char *explanation); void PathScoringLogic(float goal_respawn_time, qbool be_quiet, float lookahead_time, qbool path_normal, vec3_t player_origin, vec3_t player_direction, gedict_t *touch_marker_, gedict_t *goalentity_marker, qbool rocket_alert, - qbool rocket_jump_routes_allowed, qbool trace_bprint, gedict_t *player, + qbool rocket_jump_routes_allowed, qbool hook_routes_allowed, qbool trace_bprint, gedict_t *player, float *best_score, gedict_t **linked_marker_, int *new_path_state, int *new_angle_hint, int *new_rj_frame_delay, float new_rj_angles[2]); diff --git a/include/g_local.h b/include/g_local.h index 8795f7f5..1844772b 100644 --- a/include/g_local.h +++ b/include/g_local.h @@ -620,6 +620,7 @@ void RegenFlags(qbool yes); void AddHook(qbool yes); void CTF_Obituary(gedict_t *targ, gedict_t *attacker); void CTF_CheckFlagsAsKeys(void); +void TossFlag(void); // logs.c void log_open(const char *fmt, ...) PRINTF_FUNC(1); diff --git a/include/progs.h b/include/progs.h index d3b297c0..bc674a8f 100644 --- a/include/progs.h +++ b/include/progs.h @@ -419,7 +419,7 @@ typedef void (*fb_entity_funcref_t)(struct gedict_s* item); #define NUMBER_PATHS 8 #endif #ifndef NUMBER_SUBZONES -#define NUMBER_SUBZONES 32 +#define NUMBER_SUBZONES 128 // TODO hiipe - check this, should be ok though? #endif typedef struct fb_runaway_route_s { @@ -433,6 +433,7 @@ typedef struct fb_path_s { struct gedict_s* next_marker; // next marker in the graph float time; // time to travel if walking (0 if teleporting) float rj_time; // time to travel if using rocket jump + float hook_time; // time to travel if using hook int flags; // hints on how to travel to next marker short angle_hint; // When travelling to marker, offset to standard angle (+ = anti-clockwise) @@ -445,14 +446,18 @@ typedef struct fb_goal_s { struct gedict_s* next_marker; float time; struct gedict_s* next_marker_rj; + struct gedict_s* next_marker_hook; float rj_time; + float hook_time; } fb_goal_t; typedef struct fb_subzone_s { struct gedict_s* next_marker; float time; struct gedict_s* next_marker_rj; + struct gedict_s* next_marker_hook; float rj_time; + float hook_time; } fb_subzone_t; typedef struct fb_zone_s { @@ -467,6 +472,11 @@ typedef struct fb_zone_s { float rj_time; struct gedict_s* next_rj; + // Hook + struct gedict_s* marker_hook; + float hook_time; + struct gedict_s* next_hook; + float reverse_time; struct gedict_s* reverse_marker; struct gedict_s* reverse_next; @@ -525,6 +535,8 @@ typedef struct fb_botskill_s { float combat_jump_chance; float missile_dodge_time; // minimum time in seconds before bot dodges missile + int ctf_role; // attack, midfield or defense + qbool customised; // if set, customised file qbool wiggle_run_dmm4; // if set, wiggle run on dmm4 (and up) @@ -686,6 +698,12 @@ typedef struct fb_entvars_s { int rocketJumpAngles[2]; // pitch/yaw for rocket jump angle int lavaJumpState; // keep track of submerge/rise/fire sequence + // Hook logic + qbool canHook; + qbool hooking; + struct gedict_s* hookTarget; + vec3_t hookOldPosition; + // Editor int last_jump_frame; // framecount when player last jumped. used to help setting rj fields diff --git a/src/bot_aim.c b/src/bot_aim.c index 3e25da5b..a4549f32 100644 --- a/src/bot_aim.c +++ b/src/bot_aim.c @@ -9,6 +9,7 @@ #define ATTACK_RESPAWN_TIME 3 qbool DM6FireAtDoor(gedict_t *self, vec3_t rel_pos); +qbool E2M2DischargeLogic(gedict_t* self); static void BotsModifyAimAtPlayerLogic(gedict_t *self); static void BotSetDesiredAngles(gedict_t *self, vec3_t rel_pos) @@ -50,11 +51,11 @@ static void BotSetDesiredAngles(gedict_t *self, vec3_t rel_pos) static void BotStopFiring(gedict_t *bot) { qbool continuous = bot->fb.desired_weapon_impulse == 4 || bot->fb.desired_weapon_impulse == 5 - || bot->fb.desired_weapon_impulse == 8; + || bot->fb.desired_weapon_impulse == 8 || bot->fb.desired_weapon_impulse == 22; qbool correct_weapon = BotUsingCorrectWeapon(bot); qbool enemy_alive = bot->s.v.enemy && ISLIVE(&g_edicts[bot->s.v.enemy]); - bot->fb.firing &= (continuous && correct_weapon && enemy_alive) || bot->fb.rocketJumping; + bot->fb.firing &= (continuous && correct_weapon && enemy_alive) || bot->fb.rocketJumping || bot->fb.hooking; } // Magic numbers here: 400 = 0.5 * sv_gravity @@ -143,6 +144,16 @@ static void BotsFireAtWorldLogic(gedict_t *self, vec3_t rel_pos, float *rel_dist return; } + if (E2M2DischargeLogic(self)) + { + return; + } + + if (self->fb.path_state & FIRE_BUTTON) + { + return; + } + if (*rel_dist < 160) { vec3_t rel_pos2; @@ -477,7 +488,7 @@ void BotsFireLogic(void) AttackRespawns(self); // a_attackfix() - if (!self->fb.rocketJumping && (self->s.v.enemy == 0) && !(self->fb.state & SHOT_FOR_LUCK)) + if (!self->fb.rocketJumping && !self->fb.hooking && (self->s.v.enemy == 0) && !(self->fb.state & SHOT_FOR_LUCK) && !E2M2DischargeLogic(self)) { self->fb.firing = false; } @@ -497,13 +508,18 @@ void BotsFireLogic(void) BotsAimAtFloor(self, rel_pos, rel_dist); - if (!self->fb.rocketJumping) + if (!self->fb.rocketJumping && !self->fb.hooking) { - SetFireButton(self, rel_pos, rel_dist); + SetFireButton(self, rel_pos, rel_dist); } BotSetDesiredAngles(self, rel_pos); } + + if (E2M2DischargeLogic(self)) + { + self->fb.firing = true; + } } #endif // BOT_SUPPORT diff --git a/src/bot_botenemy.c b/src/bot_botenemy.c index 8e921aa6..e0010373 100644 --- a/src/bot_botenemy.c +++ b/src/bot_botenemy.c @@ -123,9 +123,9 @@ static void BestEnemy_apply(gedict_t *test_enemy, float *best_score, gedict_t ** look_marker = SightFromMarkerFunction(from_marker, to_marker); if (look_marker != NULL) { - ZoneMarker(from_marker, look_marker, path_normal, test_enemy->fb.canRocketJump); + ZoneMarker(from_marker, look_marker, path_normal, test_enemy->fb.canRocketJump, test_enemy->fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, look_marker, - test_enemy->fb.canRocketJump); + test_enemy->fb.canRocketJump, test_enemy->fb.canHook); enemy_score = traveltime + g_random(); } else @@ -134,7 +134,10 @@ static void BestEnemy_apply(gedict_t *test_enemy, float *best_score, gedict_t ** enemy_score = look_traveltime + g_random(); } - if (enemy_score < *best_score) + // Prioritize enemy with flag + if (test_enemy->ctf_flag & CTF_FLAG) enemy_score /= 2; + + if (enemy_score < *best_score && look_marker != NULL) // TODO hiipe - crashes here sometimes! Surely it should be possible for look_marker to be NULL? { vec3_t marker_view; vec3_t to_marker_view; diff --git a/src/bot_botgoals.c b/src/bot_botgoals.c index 541191f7..e3f0416b 100644 --- a/src/bot_botgoals.c +++ b/src/bot_botgoals.c @@ -57,6 +57,8 @@ void UpdateGoalEntity(gedict_t *item, gedict_t *taker) // FIXME: If teammate is considerably weaker and bot needs/wants item, shout COMING instead static qbool GoalLeaveForTeammate(gedict_t *self, gedict_t *goal_entity) { + gedict_t* plr; + if ((g_globalvars.time < goal_entity->fb.touchPlayerTime) && goal_entity->fb.touchPlayer && (goal_entity->fb.touchPlayer != self)) { @@ -68,6 +70,56 @@ static qbool GoalLeaveForTeammate(gedict_t *self, gedict_t *goal_entity) } } + // Check bot teammates so the bots don't all gang up for items + for (plr = world; (plr = find_plr(plr));) + { + if (plr->isBot && SameTeam(self, plr)) + { + // Don't ever leave these items! + char* ignore[] = + { "item_artifact_invulnerability", "item_artifact_invisibility", + "item_artifact_super_damage", "item_flag_team1", "item_flag_team2" }; + + int i; + for (i = 0; i < sizeof(ignore) / sizeof(ignore[0]); ++i) + { + if (streq(goal_entity->classname, ignore[i])) return false; + } + + // Let the bot with the highest desire take the item, if the bots can see each other. + if (goal_entity == plr->fb.best_goal + && Visible_360(self, plr) + && goal_entity->fb.saved_goal_desire <= plr->fb.best_goal->fb.saved_goal_desire) + { + goal_entity->fb.saved_goal_desire = 0; + return true; + } + } + } + + return false; +} + +static qbool ShouldGoalIgnoreDistance(gedict_t *goal_entity) +{ + if (isCTF()) + { + if ((streq(goal_entity->classname, "item_flag_team1") || streq(goal_entity->classname, "item_flag_team2")) + && (self->fb.skill.ctf_role == FB_CTF_ROLE_ATTACK || goal_entity->fb.saved_goal_desire > 5000)) + { + return true; + } + else if (streq(goal_entity->classname, "item_artifact_super_damage") + && self->fb.skill.ctf_role == FB_CTF_ROLE_MIDFIELD) + { + return true; + } + else if (streq(goal_entity->classname, "marker") && (goal_entity->fb.T & MARKER_FLAG1_DEFEND || goal_entity->fb.T & MARKER_FLAG2_DEFEND) + && self->fb.skill.ctf_role == FB_CTF_ROLE_DEFEND) + { + return true; + } + } return false; } @@ -77,6 +129,7 @@ void EvalGoal(gedict_t *self, gedict_t *goal_entity) float goal_desire = goal_entity && goal_entity->fb.desire ? goal_entity->fb.desire(self, goal_entity) : 0; float goal_time = 0.0f; + qbool ignoreDistance; if (!goal_entity) { @@ -113,9 +166,9 @@ void EvalGoal(gedict_t *self, gedict_t *goal_entity) // Calculate travel time to the goal from_marker = self->fb.touch_marker; to_marker = goal_entity->fb.touch_marker; - ZoneMarker(from_marker, to_marker, path_normal, self->fb.canRocketJump); + ZoneMarker(from_marker, to_marker, path_normal, self->fb.canRocketJump, self->fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, - self->fb.canRocketJump); + self->fb.canRocketJump, self->fb.canHook); goal_time = traveltime; if (self->fb.goal_enemy_repel) @@ -123,9 +176,10 @@ void EvalGoal(gedict_t *self, gedict_t *goal_entity) // Time for our enemy to get there from_marker = g_edicts[self->s.v.enemy].fb.touch_marker; ZoneMarker(from_marker, to_marker, path_normal, - g_edicts[self->s.v.enemy].fb.canRocketJump); + g_edicts[self->s.v.enemy].fb.canRocketJump, + g_edicts[self->s.v.enemy].fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, - g_edicts[self->s.v.enemy].fb.canRocketJump); + g_edicts[self->s.v.enemy].fb.canRocketJump, self->fb.canHook); // If enemy will get there much faster than we will... if (traveltime <= (goal_time - 1.25)) @@ -162,10 +216,11 @@ void EvalGoal(gedict_t *self, gedict_t *goal_entity) if (self->fb.goal_enemy_repel) { qbool rl_routes = g_edicts[self->s.v.enemy].fb.canRocketJump; + qbool hook_routes = g_edicts[self->s.v.enemy].fb.canHook; from_marker = g_edicts[self->s.v.enemy].fb.touch_marker; - ZoneMarker(from_marker, to_marker, path_normal, rl_routes); - traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, rl_routes); + ZoneMarker(from_marker, to_marker, path_normal, rl_routes, hook_routes); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, rl_routes, hook_routes); goal_entity->fb.saved_enemy_time_squared = traveltime * traveltime; } @@ -176,12 +231,13 @@ void EvalGoal(gedict_t *self, gedict_t *goal_entity) } } + ignoreDistance = ShouldGoalIgnoreDistance(goal_entity); + // If the bot can think far enough ahead... - if (goal_time < self->fb.skill.lookahead_time) + if (goal_time < self->fb.skill.lookahead_time || ignoreDistance) { - float goal_score = goal_desire * (self->fb.skill.lookahead_time - goal_time) - / (goal_time + 5); - + float goal_score = ignoreDistance ? goal_desire : goal_desire * (self->fb.skill.lookahead_time - goal_time) / (goal_time + 5); + if (goal_score > self->fb.best_goal_score) { self->fb.best_goal_score = goal_score; @@ -192,10 +248,11 @@ void EvalGoal(gedict_t *self, gedict_t *goal_entity) } // FIXME: parameters -static void EvalGoal2(gedict_t *goal_entity, gedict_t *best_goal_marker, qbool canRocketJump) +static void EvalGoal2(gedict_t *goal_entity, gedict_t *best_goal_marker, qbool canRocketJump, qbool canHook) { float goal_desire = 0.0f; float traveltime2 = 0.0f; + qbool ignoreDistance; if (goal_entity == NULL) { @@ -210,9 +267,9 @@ static void EvalGoal2(gedict_t *goal_entity, gedict_t *best_goal_marker, qbool c { gedict_t *goal_marker2 = goal_entity->fb.touch_marker; from_marker = goal_marker2; - ZoneMarker(from_marker, best_goal_marker, path_normal, canRocketJump); + ZoneMarker(from_marker, best_goal_marker, path_normal, canRocketJump, canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, best_goal_marker, - canRocketJump); + canRocketJump, canHook); traveltime2 = max(best_respawn_time, goal_time2 + traveltime); if (self->fb.bot_evade && self->fb.goal_enemy_repel) @@ -239,8 +296,8 @@ static void EvalGoal2(gedict_t *goal_entity, gedict_t *best_goal_marker, qbool c } from_marker = best_goal_marker; - ZoneMarker(from_marker, goal_marker2, path_normal, canRocketJump); - traveltime = SubZoneArrivalTime(zone_time, middle_marker, goal_marker2, canRocketJump); + ZoneMarker(from_marker, goal_marker2, path_normal, canRocketJump, canHook); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, goal_marker2, canRocketJump, canHook); traveltime2 = max(self->fb.best_goal_time + traveltime, goal_entity->fb.saved_respawn_time); if (self->fb.bot_evade && self->fb.goal_enemy_repel) @@ -251,9 +308,11 @@ static void EvalGoal2(gedict_t *goal_entity, gedict_t *best_goal_marker, qbool c } } - if (traveltime2 < self->fb.skill.lookahead_time) + ignoreDistance = ShouldGoalIgnoreDistance(goal_entity); + + if (traveltime2 < self->fb.skill.lookahead_time || ignoreDistance) { - float goal_score2 = self->fb.best_goal_score + float goal_score2 = ignoreDistance ? goal_desire : self->fb.best_goal_score + (goal_desire * (self->fb.skill.lookahead_time - traveltime2) / (traveltime2 + 5)); @@ -286,9 +345,9 @@ static void EnemyGoalLogic(gedict_t *self) float traveltime2 = 0.0f; from_marker = goal_marker2; - ZoneMarker(from_marker, best_goal_marker, path_normal, self->fb.canRocketJump); + ZoneMarker(from_marker, best_goal_marker, path_normal, self->fb.canRocketJump, self->fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, best_goal_marker, - self->fb.canRocketJump); + self->fb.canRocketJump, self->fb.canHook); traveltime2 = max(goal_time2 + traveltime, best_respawn_time); if (traveltime2 < self->fb.skill.lookahead_time) @@ -307,9 +366,9 @@ static void EnemyGoalLogic(gedict_t *self) // Work out time to best goal marker from_marker = best_goal_marker; - ZoneMarker(from_marker, goal_marker2, path_normal, self->fb.canRocketJump); + ZoneMarker(from_marker, goal_marker2, path_normal, self->fb.canRocketJump, self->fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, goal_marker2, - self->fb.canRocketJump); + self->fb.canRocketJump, self->fb.canHook); traveltime2 = max(best_goal_time + traveltime, g_edicts[self->s.v.enemy].fb.saved_respawn_time); @@ -356,7 +415,7 @@ void UpdateGoal(gedict_t *self) BotEvadeLogic(self); - if (enemy_->fb.touch_marker) + if (enemy_->fb.touch_marker && !(self->ctf_flag & CTF_FLAG)) // Don't chase if carrying flag { self->fb.virtual_enemy = enemy_; self->fb.goal_enemy_desire = @@ -366,9 +425,9 @@ void UpdateGoal(gedict_t *self) gedict_t *enemy = &g_edicts[self->s.v.enemy]; // Time from here to the enemy's last marker from_marker = self->fb.touch_marker; - ZoneMarker(from_marker, enemy->fb.touch_marker, path_normal, self->fb.canRocketJump); + ZoneMarker(from_marker, enemy->fb.touch_marker, path_normal, self->fb.canRocketJump, self->fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, enemy->fb.touch_marker, - self->fb.canRocketJump); + self->fb.canRocketJump, self->fb.canHook); enemy_->fb.saved_respawn_time = 0; enemy_->fb.saved_goal_time = traveltime; @@ -422,6 +481,37 @@ void UpdateGoal(gedict_t *self) } } + // Dropped flags + for (goal_entity = world; (goal_entity = ez_find(goal_entity, "item_flag_team1"));) + { + EvalGoal(self, goal_entity); + } + for (goal_entity = world; (goal_entity = ez_find(goal_entity, "item_flag_team2"));) + { + EvalGoal(self, goal_entity); + } + + // Dropped runes + for (goal_entity = world; (goal_entity = ez_find(goal_entity, "rune"));) + { + EvalGoal(self, goal_entity); + } + + // Defense Markers and Discharge + // TODO hiipe - might be more efficient to make these goals instead and then they would be checked above? + // but this would be mixing generic behaviour the ctf and map specific goals. + for (goal_entity = world; (goal_entity = ez_find(goal_entity, "marker"));) + { + if (goal_entity->fb.T & MARKER_FLAG1_DEFEND || goal_entity->fb.T & MARKER_FLAG2_DEFEND) + { + EvalGoal(self, goal_entity); + } + if (goal_entity->fb.T & MARKER_E2M2_DISCHARGE) + { + EvalGoal(self, goal_entity); + } + } + if (teamplay && !isRA()) { gedict_t *search_entity = HelpTeammate(); @@ -448,7 +538,7 @@ void UpdateGoal(gedict_t *self) if (next && (next != world) && (next != dropper)) { EvalGoal2(self->fb.touch_marker->fb.goals[i].next_marker->fb.virtual_goal, - self->fb.best_goal->fb.touch_marker, self->fb.canRocketJump); + self->fb.best_goal->fb.touch_marker, self->fb.canRocketJump, self->fb.canHook); } } @@ -456,7 +546,7 @@ void UpdateGoal(gedict_t *self) { if (goal_entity->fb.touch_marker) { - EvalGoal2(goal_entity, self->fb.best_goal->fb.touch_marker, self->fb.canRocketJump); + EvalGoal2(goal_entity, self->fb.best_goal->fb.touch_marker, self->fb.canRocketJump, self->fb.canHook); } } diff --git a/src/bot_botimp.c b/src/bot_botimp.c index caea92ea..25fdd636 100644 --- a/src/bot_botimp.c +++ b/src/bot_botimp.c @@ -52,6 +52,8 @@ #define FB_CVAR_COMBATJUMP_CHANCE "k_fbskill_combatjump" #define FB_CVAR_MISSILEDODGE_TIME "k_fbskill_missiledodge" +#define FB_CVAR_CTF_ROLE "k_fbskill_ctf_role" + static float RangeOverSkill(int skill_level, float minimum, float maximum) { float skill = skill_level * 1.0f / (MAX_FROGBOT_SKILL - MIN_FROGBOT_SKILL); @@ -145,6 +147,8 @@ void RegisterSkillVariables(void) RegisterCvar(FB_CVAR_COMBATJUMP_CHANCE); RegisterCvar(FB_CVAR_MISSILEDODGE_TIME); + RegisterCvar(FB_CVAR_CTF_ROLE); + RegisterCvar(FB_CVAR_DISTANCEERROR); RegisterCvar(FB_CVAR_PAIN_VOLATILITY_INCREASE); RegisterCvar(FB_CVAR_SELF_MIDAIR_VOLATILITY_INCREASE); @@ -209,6 +213,14 @@ qbool SetAttributesBasedOnSkill(int skill) cvar_fset(FB_CVAR_COMBATJUMP_CHANCE, RangeOverSkill(skill, 0.03f, 0.1f)); cvar_fset(FB_CVAR_MISSILEDODGE_TIME, RangeOverSkill(skill, 1.0f, 0.5f)); + // CTF skill + if (isCTF()) + { + // Must have much lookahead time in CTF to be able to reach flags etc. Is variation even needed? + cvar_fset(FB_CVAR_LOOKAHEADTIME, RangeOverSkill(skill, 40.0f, 50.0f)); + cvar_fset(FB_CVAR_CTF_ROLE, (int)CountBots() % 3); + } + // Customise { char buf[1024 * 4]; @@ -292,14 +304,21 @@ void SetAttribs(gedict_t *self, qbool customised) char* BotNameEnemy(int botNumber) { - char *names[] = + char* names[] = { ": Timber", ": Sujoy", ": Nightwing", ": Cenobite", ": Thresh", ": Frick", ": Unholy", - ": Reptile", ": Nikodemus", ": Paralyzer", ": Xenon", ": Spice" - ": Kornelia", ": Rix", ": Batch", ": Gollum" }; + ": Reptile", ": Nikodemus", ": Paralyzer", ": Xenon", ": Spice" + ": Kornelia", ": Rix", ": Batch", ": Gollum" }; + + char *ctf_names[] = { ": Hippo", ": Velokitty", ": Shiny", ": Zagg" }; + char *custom_name = cvar_string(va("k_fb_name_enemy_%d", botNumber)); if (strnull(custom_name)) { + if (isCTF()) + { + return ctf_names[(int)bound(0, botNumber, sizeof(ctf_names) / sizeof(ctf_names[0]) - 1)]; + } return names[(int)bound(0, botNumber, sizeof(names) / sizeof(names[0]) - 1)]; } @@ -308,14 +327,21 @@ char* BotNameEnemy(int botNumber) char* BotNameFriendly(int botNumber) { - char *names[] = + char* names[] = { "> MrJustice", "> DanJ", "> Gunner", "> Tele", "> Jakey", "> Parrais", "> Thurg", - "> Kool", "> Zaphod", "> Dreamer", "> Mandrixx", "> Skill5", "> Vid", "> Soul99", - "> Jon", "> Gaz" }; + "> Kool", "> Zaphod", "> Dreamer", "> Mandrixx", "> Skill5", "> Vid", "> Soul99", + "> Jon", "> Gaz" }; + + char *ctf_names[] = { "> Micro", "> Elfeo", "> Malice", "> Killton" }; + char *custom_name = cvar_string(va("k_fb_name_team_%d", botNumber)); if (strnull(custom_name)) { + if (isCTF()) + { + return ctf_names[(int)bound(0, botNumber, sizeof(ctf_names) / sizeof(ctf_names[0]) - 1)]; + } return names[(int)bound(0, botNumber, sizeof(names) / sizeof(names[0]) - 1)]; } @@ -324,14 +350,22 @@ char* BotNameFriendly(int botNumber) char* BotNameGeneric(int botNumber) { - char *names[] = + char* names[] = { "/ bro", "/ goldenboy", "/ tincan", "/ grue", "/ dizzy", "/ daisy", "/ denzil", "/ dora", - "/ shortie", "/ machina", "/ gudgie", "/ scoosh", "/ frazzle", "/ pop", "/ junk", - "/ overflow" }; + "/ shortie", "/ machina", "/ gudgie", "/ scoosh", "/ frazzle", "/ pop", "/ junk", + "/ overflow" }; + + char *ctf_names[] = + { "/ Hippo", "/ Velokitty", "/ Shiny", "/ Zagg", "/ Micro", "/ Elfeo", "/ Malice", "/ Killton" }; + char *custom_name = cvar_string(va("k_fb_name_%d", botNumber)); if (strnull(custom_name)) { + if (isCTF()) + { + return ctf_names[(int)bound(0, botNumber, sizeof(ctf_names) / sizeof(ctf_names[0]) - 1)]; + } return names[(int)bound(0, botNumber, sizeof(names) / sizeof(names[0]) - 1)]; } diff --git a/src/bot_botjump.c b/src/bot_botjump.c index 4adc8217..353d060c 100644 --- a/src/bot_botjump.c +++ b/src/bot_botjump.c @@ -133,6 +133,13 @@ void BotCanRocketJump(gedict_t *self) } } +void BotCanHook(gedict_t* self) +{ + // TODO hiipe - is this slow? + if (cvar("k_ctf_hook") > 0 && isCTF()) self->fb.canHook = true; + else self->fb.canHook = false; +} + /* // FIXME: If teammate is strong (RA etc), fire anyway? Also if boomsticker and we are strong // FIXME: Use find_radius? Currently ignores teammates behind them. @@ -205,6 +212,21 @@ static void SetRocketJumpAngles(gedict_t *self) } } +static void SetHookAngles(gedict_t* self) +{ + vec3_t toTarget; + VectorSubtract(self->fb.hookTarget->s.v.origin, self->s.v.origin, toTarget); + vectoangles(toTarget, self->fb.desired_angle); + if (self->fb.desired_angle[0] > 180) + { + self->fb.desired_angle[0] = 360 - self->fb.desired_angle[0]; + } + else + { + self->fb.desired_angle[0] = 0 - self->fb.desired_angle[0]; + } +} + static void BotPerformLavaJump(gedict_t *self) { vec3_t point; @@ -388,6 +410,80 @@ void BotPerformRocketJump(gedict_t *self) } } +// Performs hook +// TODO hiipe - maybe put this in a new file? +void BotPerformHook(gedict_t* self) +{ + if (!(self->fb.touch_marker && self->fb.linked_marker)) + { + return; + } + + BotPerformLavaJump(self); + + if (self->fb.hooking) + { + // Stop if close to the target + vec3_t toTarget; + vec3_t differenceFromPreviousPosition; + qbool stop_early = false; + float distanceToTarget = VectorLength(toTarget); + + VectorSubtract(self->fb.hookTarget->s.v.origin, self->s.v.origin, toTarget); + + // Stop if obscured by a wall etc + traceline(PASSVEC3(self->fb.hookTarget->s.v.origin), PASSVEC3(self->s.v.origin), true, world); + + // Stop if not moved much since last frame (stuck near marker) + VectorSubtract(self->s.v.origin, self->fb.hookOldPosition, differenceFromPreviousPosition); + + if (self->fb.touch_marker->fb.paths[0].flags & HOOK && self->fb.touch_marker->fb.paths[0].time < 0.2) + stop_early = false; + else if (distanceToTarget < 20) stop_early = true; + + if (VectorLength(differenceFromPreviousPosition) < 0.01f || stop_early || g_globalvars.trace_fraction != 1) + { + self->fb.hooking = false; + self->fb.firing = false; + self->fb.state &= ~NOTARGET_ENEMY; + self->fb.state &= ~WAIT; + } + else + { + SetHookAngles(self); + VectorCopy(self->s.v.origin, self->fb.hookOldPosition); + } + } + else if (self->on_hook) + { + // Does this break demos?? TODO + GrappleReset(self->hook); + } + else if (self->fb.canHook) + { + qbool path_is_hook = self->fb.path_state & HOOK; + + float touch_dist = VectorDistance(self->s.v.origin, self->fb.touch_marker->s.v.origin); + + // Close enough to marker + qbool ok_distance = touch_dist >= 10 && touch_dist <= 100; + + // Don't hook to the same marker we are on + qbool ok_target = self->fb.touch_marker != self->fb.linked_marker; + + if (path_is_hook && /*ok_to_fire &&*/ ok_distance && ok_target) + { + self->fb.desired_weapon_impulse = 22; + self->fb.firing = true; + self->fb.hooking = true; + //self->fb.path_state |= DELIBERATE_AIR_WAIT_GROUND; + self->fb.state |= NOTARGET_ENEMY; + self->fb.hookTarget = self->fb.linked_marker; + SetHookAngles(self); + } + } +} + static qbool PlayerFiringLG(gedict_t *player) { return (player && player->s.v.button0 && ((int)player->s.v.weapon & IT_LIGHTNING) diff --git a/src/bot_botpath.c b/src/bot_botpath.c index 6933ad5b..c028de70 100644 --- a/src/bot_botpath.c +++ b/src/bot_botpath.c @@ -78,12 +78,12 @@ static void EvalCloseRunAway(float runaway_time, gedict_t *enemy_touch_marker, float traveltime2; from_marker = enemy_touch_marker; - ZoneMarker(from_marker, to_marker, path_normal, false); - traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, false); + ZoneMarker(from_marker, to_marker, path_normal, false, false); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, false, false); traveltime2 = traveltime; from_marker = touch_marker; - ZoneMarker(from_marker, to_marker, path_normal, false); - traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, false); + ZoneMarker(from_marker, to_marker, path_normal, false, false); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, false, false); if (look_traveltime) { test_away_score = g_random() * runaway_time @@ -178,6 +178,84 @@ static qbool LookingAtPlayer(gedict_t *self) return (self->fb.look_object && (self->fb.look_object->ct == ctPlayer)); } +void LookAtButton(gedict_t* button, qbool buttonIsDoor) +{ + gedict_t *target = buttonIsDoor ? button->fb.door_entity : button; + gedict_t *trigger = PROG_TO_EDICT(target->s.v.enemy); + + vec3_t target_origin; + VectorScale(target->s.v.absmin, 0.5, target_origin); + VectorMA(target_origin, 0.5, target->s.v.absmax, target_origin); + + traceline(self->s.v.origin[0], self->s.v.origin[1], self->s.v.origin[2] + 16, + target_origin[0], target_origin[1], target_origin[2], + true, self); + + if (g_globalvars.trace_fraction == 1 || + PROG_TO_EDICT(g_globalvars.trace_ent) == target || + PROG_TO_EDICT(g_globalvars.trace_ent) == button) + { + if ((target->s.v.takedamage)) + { + if ((buttonIsDoor && (target->state == STATE_TOP || target->state == STATE_UP)) || + (target->s.v.enemy && (trigger->state == STATE_TOP || trigger->state == STATE_UP)) || + (!target->s.v.enemy && !buttonIsDoor)) + { + self->fb.path_state |= FIRE_BUTTON; + self->fb.state |= NOTARGET_ENEMY; + self->fb.look_object = target; + } + } + } +} + +qbool CheckLookAtButton(gedict_t* self) +{ + // First check if the bot is in front of a shootable door + gedict_t* marker = self->fb.linked_marker; + if (marker && marker->fb.door_entity && marker->fb.door_entity->s.v.takedamage) + { + LookAtButton(marker, true); + return true; + } + + marker = self->fb.touch_marker; + if (marker && marker->fb.door_entity && marker->fb.door_entity->s.v.takedamage) + { + LookAtButton(marker, true); + return true; + } + + // Now check if there is a button linked to the current or next marker + if (self->fb.linked_marker && self->fb.linked_marker->fb.T & MARKER_LOOK_BUTTON) + { + marker = self->fb.linked_marker; + } + else if (self->fb.touch_marker && self->fb.touch_marker->fb.T & MARKER_LOOK_BUTTON) + { + marker = self->fb.touch_marker; + } + + if (marker) + { + int i; + for (i = 0; i < NUMBER_PATHS; i++) + { + fb_path_t* path = &marker->fb.paths[i]; + + if (path->next_marker && (path->flags & LOOK_BUTTON)) { + LookAtButton(path->next_marker, false); + return true; + } + } + } + + self->fb.path_state &= ~FIRE_BUTTON; + self->fb.state &= ~NOTARGET_ENEMY; + + return false; +} + qbool WaitingToHitGround(gedict_t *self) { return (self->fb.path_state & WAIT_GROUND) && !((int)self->s.v.flags & FL_ONGROUND); @@ -195,6 +273,22 @@ static qbool WalkTowardsDroppedItem(gedict_t *self) return true; } + else if (streq(goalentity_->classname, "rune")) + { + SetLinkedMarker(self, goalentity_, "ProcNewLinked(rune)"); + self->fb.linked_marker_time = g_globalvars.time + 5; + self->fb.old_linked_marker = self->fb.touch_marker; + + return true; + } + else if (streq(goalentity_->classname, "item_flag_team1") || streq(goalentity_->classname, "item_flag_team2")) + { + SetLinkedMarker(self, goalentity_, "ProcNewLinked(flag)"); + self->fb.linked_marker_time = g_globalvars.time + 5; + self->fb.old_linked_marker = self->fb.touch_marker; + + return true; + } else if (goalentity_->cnt) { SetLinkedMarker(self, goalentity_, "ProcNewLinked(dropped-powerup)"); @@ -223,9 +317,9 @@ static qbool PredictionShotLogic(gedict_t *self, gedict_t *goalentity_marker) if (look_marker) { path_normal = true; - ZoneMarker(from_marker, look_marker, path_normal, self->fb.canRocketJump); + ZoneMarker(from_marker, look_marker, path_normal, self->fb.canRocketJump, self->fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, look_marker, - self->fb.canRocketJump); + self->fb.canRocketJump, self->fb.canHook); look_traveltime = traveltime; } else @@ -238,9 +332,9 @@ static qbool PredictionShotLogic(gedict_t *self, gedict_t *goalentity_marker) to_marker = from_marker; from_marker = self->fb.linked_marker; path_normal = true; - ZoneMarker(from_marker, to_marker, path_normal, self->fb.canRocketJump); + ZoneMarker(from_marker, to_marker, path_normal, self->fb.canRocketJump, self->fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, to_marker, - self->fb.canRocketJump); + self->fb.canRocketJump, self->fb.canHook); if (look_traveltime < traveltime) { self->fb.look_object = look_marker; @@ -263,6 +357,7 @@ void ProcessNewLinkedMarker(gedict_t *self) NULL : goalentity_->fb.touch_marker); qbool trace_bprint = self->fb.debug; qbool rocket_jump_routes_allowed = self->fb.canRocketJump; + qbool hook_routes_allowed = self->fb.canHook; qbool rocket_alert = false; float best_score = PATH_SCORE_NULL; vec3_t player_direction; @@ -418,7 +513,7 @@ void ProcessNewLinkedMarker(gedict_t *self) PathScoringLogic(self->fb.goal_respawn_time, self->fb.be_quiet, self->fb.skill.lookahead_time, self->fb.path_normal_, self->s.v.origin, player_direction, self->fb.touch_marker, goalentity_marker, rocket_alert, - rocket_jump_routes_allowed, trace_bprint, self, &best_score, + rocket_jump_routes_allowed, hook_routes_allowed, trace_bprint, self, &best_score, &new_linked_marker, &new_path_state, &new_angle_hint, &new_rj_delay, new_rj_angles); SetLinkedMarker(self, new_linked_marker, "ProcNewLinked(std)"); @@ -484,7 +579,7 @@ void ProcessNewLinkedMarker(gedict_t *self) } // FIXME: Map-specific - if (DM6LookAtDoor(self) || LookingAtPlayer(self)) + if (DM6LookAtDoor(self) || LookingAtPlayer(self) || CheckLookAtButton(self)) { return; } diff --git a/src/bot_botthink.c b/src/bot_botthink.c index 8f9adefb..85d5149d 100644 --- a/src/bot_botthink.c +++ b/src/bot_botthink.c @@ -127,6 +127,56 @@ static void TargetEnemyLogic(gedict_t *self) } } +static void SurrenderCTFItemsLogic(gedict_t* self) +{ + gedict_t* teammate = IdentifyMostVisibleTeammate(self); + if (teammate != world && !teammate->isBot) { + vec3_t toTeam; + float distance; + + VectorSubtract(teammate->s.v.origin, self->s.v.origin, toTeam); + distance = VectorLength(toTeam); + + // If close to a human player, and if no enemies are near + if (distance < 150 && self->fb.enemy_dist >= 500) + { + // yikes! maybe a better way to do this? Not sure if changing the angles like this breaks demos. TODO! + vec3_t oldAngles; + vec3_t predictedOffset; + + VectorCopy(self->s.v.v_angle, oldAngles); + + VectorScale(teammate->s.v.velocity, 0.1, predictedOffset); + VectorAdd(toTeam, predictedOffset, toTeam); + + vectoangles(toTeam, self->s.v.v_angle); + if (self->s.v.v_angle[0] > 180) + { + self->s.v.v_angle[0] = 360 - self->s.v.v_angle[0]; + } + else + { + self->s.v.v_angle[0] = 0 - self->s.v.v_angle[0]; + } + + if (self->ctf_flag & CTF_RUNE_MASK && !(teammate->ctf_flag & CTF_RUNE_MASK)) + { + // Could take into account rune priorities? + TeamplayMessageByName(self, "tossrune"); + TossRune(); + } + + if (self->ctf_flag & CTF_FLAG) + { + TeamplayMessageByName(self, "tossflag"); + TossFlag(); + } + + VectorCopy(oldAngles, self->s.v.v_angle); + } + } +} + static void BotDodgeMovement(gedict_t *self, vec3_t dir_move, float dodge_factor) { if (dodge_factor) @@ -214,6 +264,16 @@ static void BotMoveTowardsLinkedMarker(gedict_t *self, vec3_t dir_move) qbool onGround = ((int)self->s.v.flags & FL_ONGROUND); qbool curlJump = ((int)self->fb.path_state & BOTPATH_CURLJUMP_HINT); + if (self->fb.hooking) + { + // Stop before starting to hook + if (VectorDistance(self->s.v.origin, self->fb.linked_marker->s.v.origin) < VectorDistance(self->fb.touch_marker->s.v.origin, self->fb.linked_marker->s.v.origin)) + { + VectorClear(dir_move); + return; + } + } + VectorAdd(linked->s.v.absmin, linked->s.v.view_ofs, temp); VectorSubtract(temp, self->s.v.origin, temp); normalize(temp, dir_move); @@ -276,6 +336,11 @@ static void BotTouchMarkerLogic(void) { TargetEnemyLogic(self); + if (isCTF()) + { + SurrenderCTFItemsLogic(self); + } + if (PAST(goal_refresh_time)) { UpdateGoal(self); diff --git a/src/bot_botweap.c b/src/bot_botweap.c index bd9cee4b..1d8ca378 100644 --- a/src/bot_botweap.c +++ b/src/bot_botweap.c @@ -15,6 +15,7 @@ // FIXME: globals, this is just setting void DM6SelectWeaponToOpenDoor(gedict_t *self); +qbool E2M2DischargeLogic(gedict_t* self); // FIXME: This is just stopping quad damage rocket shot, always replacing with shotgun // Can do far better than this @@ -348,9 +349,11 @@ static void RocketLauncherShot(gedict_t *self) from_marker = g_edicts[self->s.v.enemy].fb.touch_marker; path_normal = true; ZoneMarker(from_marker, self->fb.look_object, path_normal, - g_edicts[self->s.v.enemy].fb.canRocketJump); + g_edicts[self->s.v.enemy].fb.canRocketJump, + g_edicts[self->s.v.enemy].fb.canHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, self->fb.look_object, - g_edicts[self->s.v.enemy].fb.canRocketJump); + g_edicts[self->s.v.enemy].fb.canRocketJump, + g_edicts[self->s.v.enemy].fb.canHook); predict_dist = (traveltime * sv_maxspeed) + VectorDistance(testplace, self->fb.rocket_endpos); } @@ -444,9 +447,17 @@ static qbool PreWarBlockFiring(gedict_t *self) qbool looking_at_enemy = enemy == self->fb.look_object; qbool enemy_attacked = self->s.v.enemy && g_globalvars.time < enemy->attack_finished + 0.5; qbool debugging_door = (self->fb.debug_path && enemy_is_world); + qbool firing_at_button = (self->fb.path_state & FIRE_BUTTON); + + if (!k_practice && firing_at_button) + { + self->fb.firing = false; + + return true; + } // Don't fire at other bots - if ((self->s.v.enemy == 0) || enemy->isBot || !self->fb.look_object) + if ((self->s.v.enemy == 0 && !firing_at_button) || enemy->isBot || !self->fb.look_object) { self->fb.firing = false; @@ -496,6 +507,35 @@ static qbool KeepFiringAtEnemy(gedict_t *self) && BotUsingCorrectWeapon(self)); } +static void SelectWeaponToShootButton(gedict_t* self) +{ + if (self->fb.path_state & FIRE_BUTTON) + { + int items_ = (int)self->s.v.items; + int desired_weapon = 0; + + if (self->s.v.ammo_shells) + { + desired_weapon = IT_SHOTGUN; + } + else if ((items_ & IT_NAILGUN) && (self->s.v.ammo_nails)) + { + desired_weapon = IT_NAILGUN; + } + else if ((items_ & IT_SUPER_NAILGUN) && (self->s.v.ammo_nails)) + { + desired_weapon = IT_SUPER_NAILGUN; + } + else if ((items_ & IT_LIGHTNING) && (self->s.v.ammo_cells)) + { + desired_weapon = IT_LIGHTNING; + } + + self->fb.firing |= CheckNewWeapon(desired_weapon); + } + +} + static qbool MidairAimLogic(gedict_t *self, float rel_dist) { // In midair mode, delay firing until the rocket will hit opponent near peak of their jump @@ -589,6 +629,8 @@ void SetFireButton(gedict_t *self, vec3_t rel_pos, float rel_dist) DM6SelectWeaponToOpenDoor(self); + SelectWeaponToShootButton(self); + if (HurtSelfLogic(self)) { return; @@ -663,6 +705,11 @@ static qbool BotShouldDischarge(void) { gedict_t *enemy = &g_edicts[self->s.v.enemy]; + if (E2M2DischargeLogic(self)) + { + return true; + } + if (self->s.v.waterlevel != 3) { return false; @@ -726,6 +773,13 @@ static int DesiredWeapon(void) qbool firing_lg = self->fb.firing && self->s.v.weapon == IT_LIGHTNING && self->s.v.ammo_cells && g_globalvars.time < self->attack_finished; + + // If hooking, continue firing hook + if (self->fb.hooking) + { + return IT_HOOK; + } + if (TP_CouldDamageTeammate(self)) { return IT_SHOTGUN; @@ -913,6 +967,11 @@ void SelectWeapon(void) return; } + if (self->fb.path_state & FIRE_BUTTON) + { + return; + } + if (self->fb.state & HURT_SELF) { qbool has_rl = self->s.v.ammo_rockets && ((int)self->s.v.items & IT_ROCKET_LAUNCHER); diff --git a/src/bot_client.c b/src/bot_client.c index 3ed1efa1..82799b19 100644 --- a/src/bot_client.c +++ b/src/bot_client.c @@ -139,6 +139,10 @@ void BotClientEntersEvent(gedict_t *self, gedict_t *spawn_pos) self->fb.wiggle_run_dir = 0; SetMarker(self, spawn_pos); + if (isCTF()) { + // TODO hiipe - team1 and team2 spawns aren't markers! (yet) + SetMarker(self, LocateMarker(self->s.v.origin)); + } self->fb.arrow = 0; ClearLookObject(self); @@ -156,8 +160,9 @@ void BotClientEntersEvent(gedict_t *self, gedict_t *spawn_pos) qbool BotUsingCorrectWeapon(gedict_t *self) { - return ((self->fb.desired_weapon_impulse >= 1) && (self->fb.desired_weapon_impulse <= 8) - && (self->s.v.weapon == weapon_impulse_codes[self->fb.desired_weapon_impulse])); + return (((self->fb.desired_weapon_impulse >= 1) && (self->fb.desired_weapon_impulse <= 8) + && (self->s.v.weapon == weapon_impulse_codes[self->fb.desired_weapon_impulse])) + || (self->fb.desired_weapon_impulse == 22 && self->s.v.weapon == IT_HOOK)); } static float goal_client(gedict_t *self, gedict_t *other) @@ -210,6 +215,12 @@ static float goal_client(gedict_t *self, gedict_t *other) - self->fb.virtual_enemy->fb.total_damage * self->fb.virtual_enemy->fb.firepower) * 0.01); } + else if (EnemyHasFlag(self)) + { + return (((self->fb.total_damage + 20) * self->fb.firepower + - self->fb.virtual_enemy->fb.total_damage * self->fb.virtual_enemy->fb.firepower) + * 0.01); + } else if (EnemyDefenceless(self)) { return (((self->fb.total_damage + 120) * self->fb.firepower @@ -321,13 +332,40 @@ static void BotPeriodicMessages(gedict_t *self) qbool has_lg = ((int)self->s.v.items & IT_LIGHTNING) && self->s.v.ammo_cells >= 6; qbool is_strong = (has_rl || has_lg) && self->fb.total_damage >= 120; - if (is_strong && (self->tp.enemy_count == 0)) + if (isCTF() + && self->fb.best_goal + && (streq(self->fb.best_goal->classname, "item_flag_team1") || streq(self->fb.best_goal->classname, "item_flag_team2")) + && !(self->ctf_flag & CTF_FLAG) + && (self->fb.best_goal->cnt == FLAG_AT_BASE) + && self->fb.best_goal->fb.saved_goal_time < 5) { - TeamplayMessageByName(self, "secure"); + TeamplayMessageByName(self, "goingflag"); } - else if (is_strong && (self->tp.enemy_count > self->tp.teammate_count)) + else if (is_strong && (self->tp.enemy_count == 0)) { - TeamplayMessageByName(self, "help"); + if (isCTF() && self->fb.best_goal + && (self->fb.best_goal->fb.T & MARKER_FLAG1_DEFEND || self->fb.best_goal->fb.T & MARKER_FLAG2_DEFEND) + && VectorDistance(self->s.v.origin, self->fb.best_goal->s.v.origin) <= 500) + { + TeamplayMessageByName(self, "basesafe"); + } + else + { + TeamplayMessageByName(self, "secure"); + } + } + else if (self->tp.enemy_count > self->tp.teammate_count) + { + if (isCTF() && self->fb.best_goal + && (self->fb.best_goal->fb.T & MARKER_FLAG1_DEFEND || self->fb.best_goal->fb.T & MARKER_FLAG2_DEFEND) + && VectorDistance(self->s.v.origin, self->fb.best_goal->s.v.origin) <= 500) + { + TeamplayMessageByName(self, "basenotsafe"); + } + else if (is_strong) + { + TeamplayMessageByName(self, "help"); + } } else if (self->tp.enemy_count == 0) { diff --git a/src/bot_commands.c b/src/bot_commands.c index 9cb43dbf..a7744fda 100644 --- a/src/bot_commands.c +++ b/src/bot_commands.c @@ -44,8 +44,8 @@ static qbool customised_skill = false; #define EDITOR_SELECTED_NODE EF_GREEN // If the marker/path flag isn't set here, won't be included in .bot file -#define EXTERNAL_MARKER_PATH_FLAGS (WATERJUMP_ | DM6_DOOR | ROCKET_JUMP | JUMP_LEDGE | VERTICAL_PLATFORM) -#define EXTERNAL_MARKER_FLAGS (UNREACHABLE | MARKER_IS_DM6_DOOR | MARKER_FIRE_ON_MATCH_START | MARKER_DOOR_TOUCHABLE | MARKER_ESCAPE_ROUTE | MARKER_NOTOUCH) +#define EXTERNAL_MARKER_PATH_FLAGS (WATERJUMP_ | DM6_DOOR | ROCKET_JUMP | JUMP_LEDGE | VERTICAL_PLATFORM | HOOK | LOOK_BUTTON) +#define EXTERNAL_MARKER_FLAGS (UNREACHABLE | MARKER_IS_DM6_DOOR | MARKER_FIRE_ON_MATCH_START | MARKER_DOOR_TOUCHABLE | MARKER_ESCAPE_ROUTE | MARKER_NOTOUCH | MARKER_FLAG1_DEFEND | MARKER_FLAG2_DEFEND | MARKER_E2M2_DISCHARGE ) #define MIN_DISTANCE_BETWEEN_MARKERS 30 @@ -58,6 +58,7 @@ static vec3_t saved_marker_pos = { -999999, -999999, -999999 }; static gedict_t *saved_marker = NULL; static gedict_t *last_touched_marker = NULL; +static gedict_t* debug_marker; // FIXME: Globals extern gedict_t *markers[]; @@ -573,9 +574,9 @@ static void FrogbotsDebug(void) if (next != NULL) { - G_sprint(self, 2, " %d: %d (%s), time %3.1f, rj time %3.1f\n", i + 1, + G_sprint(self, 2, " %d: %d (%s), time %3.1f, rj time %3.1f, hook time %3.1f\n", i + 1, next->fb.index + 1, next->classname, marker->fb.paths[i].time, - marker->fb.paths[i].rj_time); + marker->fb.paths[i].rj_time, marker->fb.paths[i].hook_time); } } @@ -596,6 +597,13 @@ static void FrogbotsDebug(void) zone->next_rj->fb.index + 1, zone->next_rj->classname, zone->rj_time); } + + if (zone->next_hook) + { + G_sprint(self, 2, " HK%2d: %d (%s), time %3.1f\n", i + 1, + zone->next_hook->fb.index + 1, zone->next_hook->classname, + zone->hook_time); + } } G_sprint(self, 2, "Goals:\n"); @@ -615,6 +623,12 @@ static void FrogbotsDebug(void) goal->next_marker_rj->fb.index + 1, goal->next_marker_rj->classname, goal->rj_time); } + if (goal->next_marker_hook) + { + G_sprint(self, 2, " HK%2d: %d (%s), time %3.1f\n", i + 1, + goal->next_marker_hook->fb.index + 1, + goal->next_marker_hook->classname, goal->hook_time); + } } } } @@ -624,6 +638,9 @@ static void FrogbotsDebug(void) int start, end; qbool allow_rj = streq(sub_command, "path/rj"); + // TODO - debugging does not check hooking at all! Maybe in the future? + qbool allow_hook = false; + trap_CmdArgv(3, sub_command, sizeof(sub_command)); start = atoi(sub_command); trap_CmdArgv(4, sub_command, sizeof(sub_command)); @@ -642,8 +659,8 @@ static void FrogbotsDebug(void) G_sprint(self, 2, "From zone %d, subzone %d to zone %d subzone %d\n", from->fb.Z_, from->fb.S_, to->fb.Z_, to->fb.S_); from_marker = from; - ZoneMarker(from_marker, to, path_normal, allow_rj); - traveltime = SubZoneArrivalTime(zone_time, middle_marker, to, allow_rj); + ZoneMarker(from_marker, to, path_normal, allow_rj, allow_hook); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, to, allow_rj, allow_hook); G_sprint(self, 2, "Travel time %f, zone_time %f\n", traveltime, zone_time); G_sprint(self, 2, "Middle marker %d \20%s\21 (zone %d subzone %d), time %f\n", middle_marker->fb.index + 1, @@ -664,7 +681,7 @@ static void FrogbotsDebug(void) PathScoringLogic(to->fb.goal_respawn_time, false, 30, true, from->s.v.origin, player_direction, from, to, false, - allow_rj, true, NULL, &best_score, &linked_marker_, + allow_rj, allow_hook, true, NULL, &best_score, &linked_marker_, &new_path_state, &new_angle_hint, &new_rj_frame_delay, new_rj_angles); @@ -890,8 +907,8 @@ static void BotFileGenerate(void) snprintf(date, sizeof(date), "%d", i_rnd(0, 9999)); } - snprintf(fileName, sizeof(fileName), "bots/maps/%s[%s].bot", - strnull(entityFile) ? mapname : entityFile, date); + snprintf(fileName, sizeof(fileName), "bots/maps/%s%s[%s].bot", + strnull(entityFile) ? mapname : entityFile, isCTF() ? "_ctf" : "", date); file = std_fwopen("%s", fileName); if (file == -1) { @@ -1282,6 +1299,13 @@ static void FrogbotAddPath(void) return; } + if (nearest->fb.T & MARKER_DYNAMICALLY_ADDED) + { + G_sprint(self, PRINT_HIGH, "Cannot link a marker to a dynamically created marker!\n"); + + return; + } + if (source_to_target_path >= 0 && target_to_source_path >= 0) { RemovePath(saved_marker, source_to_target_path); @@ -1862,7 +1886,8 @@ static void FrogbotsFillServer(void) int plr_count = CountPlayers(); int i; - for (i = 0; i < min(max_clients - plr_count, 8); ++i) + // spawn a maximum of 8 + for (i = 0; i < min(max_clients - plr_count, 8 - plr_count); ++i) { FrogbotsAddbot(FrogbotSkillLevel(), "", true); } @@ -2051,9 +2076,9 @@ static void FrogbotSummary(void) { int marker_count = 0; int goal_count[NUMBER_GOALS] = - { 0 }; + { 0 }; int zone_count[NUMBER_ZONES] = - { 0 }; + { 0 }; int i, j; G_sprint(self, PRINT_HIGH, "Marker summary:\n"); @@ -2075,12 +2100,12 @@ static void FrogbotSummary(void) if (path_count == 0) { G_sprint(self, PRINT_HIGH, " %3d: %s: no paths%s\n", markers[i]->fb.index + 1, - markers[i]->classname, markers[i]->fb.Z_ ? "" : " and no zone"); + markers[i]->classname, markers[i]->fb.Z_ ? "" : " and no zone"); } else if (!markers[i]->fb.Z_) { G_sprint(self, PRINT_HIGH, " %3d: %s: no zone\n", markers[i]->fb.index + 1, - markers[i]->classname); + markers[i]->classname); } if (markers[i]->fb.G_) @@ -2108,6 +2133,59 @@ static void FrogbotsDisable(void) } } +static void FrogbotsGoMarker(void) +{ + vec3_t direction; + vec3_t src, dst, tmp; + vec3_t endpoint; + gedict_t* nearest; + gedict_t* player; + + if (!is_adm(self)) + { + G_sprint(self, PRINT_HIGH, "You must be an admin to use this command\n"); + return; + } + + trap_makevectors(self->s.v.v_angle); + aim(direction); + + VectorScale(g_globalvars.v_forward, 10, tmp); + VectorAdd(self->s.v.origin, tmp, src); + src[2] = self->s.v.absmin[2] + self->s.v.size[2] * 0.7; + + VectorCopy(src, dst); + VectorScale(direction, 8192, tmp); + VectorAdd(dst, tmp, dst); + + traceline(PASSVEC3(src), PASSVEC3(dst), false, self); + + if (!g_globalvars.trace_fraction) return; + + VectorSubtract(g_globalvars.trace_endpos, src, tmp); + VectorNormalize(tmp); + VectorScale(tmp, 10, tmp); + VectorSubtract(g_globalvars.trace_endpos, tmp, endpoint); + + nearest = LocateMarker(endpoint); + if (nearest == debug_marker) debug_marker = NULL; + else debug_marker = nearest; + + if (debug_marker) + { + G_sprint(self, PRINT_HIGH, "Sending all bots to %s\n", debug_marker->classname); + } + + for (player = world; (player = find_plr(player));) + { + if (player->isBot) + { + player->fb.fixed_goal = debug_marker; + TeamplayMessageByName(player, "coming"); + } + } +} + typedef struct frogbot_cmd_s { char *name; @@ -2123,7 +2201,8 @@ static frogbot_cmd_t std_commands[] = { "removebot", FrogbotsRemovebot_f, "Removes a single bot" }, { "removeall", FrogbotsRemoveAll, "Removes all bots from server" }, { "debug", FrogbotsDebug, "Debugging commands" }, - { "disable", FrogbotsDisable, "Disable frogbots" } }; + { "disable", FrogbotsDisable, "Disable frogbots" }, + { "gomarker", FrogbotsGoMarker, "Send all bots to the location pointed at" } }; static frogbot_cmd_t editor_commands[] = { @@ -2220,13 +2299,6 @@ void FrogbotsCommand(void) return; } - if (isCTF()) - { - G_sprint(self, PRINT_HIGH, "Cannot enable bots while in CTF mode\n"); - - return; - } - cvar_fset(FB_CVAR_ENABLED, 1); GotoNextMap(); UserMode(-cvar("_k_last_xonx")); @@ -2509,6 +2581,8 @@ void BotStartFrame(void) { BotCanRocketJump(self); + BotCanHook(self); + SelectWeapon(); BotsFireLogic(); diff --git a/src/bot_items.c b/src/bot_items.c index 7f34ea1c..0d9010fb 100644 --- a/src/bot_items.c +++ b/src/bot_items.c @@ -359,6 +359,169 @@ static float goal_artifact_super_damage(gedict_t *self, gedict_t *other) return 0; } +static float goal_flag_team(gedict_t* player, gedict_t* flag) +{ + if (flag->cnt == FLAG_DROPPED) + { + return 9999; + } + else if (player->ctf_flag & CTF_FLAG && flag->cnt == FLAG_AT_BASE) + { + return 9999; + } + else + { + return 0; + } +} + +static float goal_flag_enemy(gedict_t* player, gedict_t* flag) +{ + if (flag->cnt == FLAG_DROPPED) + { + return 9999; + } + else if (flag->cnt == FLAG_AT_BASE) + { + gedict_t* teammate = IdentifyMostVisibleTeammate(self); + if (self->fb.skill.ctf_role == FB_CTF_ROLE_DEFEND) return 0; + if (teammate != world && !teammate->isBot) + { + vec3_t toTeam; + float distance; + VectorSubtract(flag->s.v.origin, teammate->s.v.origin, toTeam); + distance = VectorLength(toTeam); + // If a human is near and no enemies are, let them take the flag + if (distance < 500 && (self->fb.enemy_dist >= 1500 || self->fb.enemy_dist == 600)) return 0; + } + return 200 + 700 * (self->fb.firepower / 100.0f); + } + else + { + return 0; + } +} + +static float goal_flag(gedict_t* player, gedict_t* flag) +{ + if (!isCTF()) return 0; + + flag->fb.touch_marker = LocateNextMarker(flag->s.v.origin, NULL); + + if (flag->fb.touch_marker) + { + flag->fb.Z_ = flag->fb.touch_marker->fb.Z_; + } + else + { + return 0; + } + + if (((flag->k_teamnumber == 1) && streq(getteam(player), "red")) + || ((flag->k_teamnumber == 2) && streq(getteam(player), "blue"))) + { + return goal_flag_team(player, flag); + } + else + { + return goal_flag_enemy(player, flag); + } +} + +static float goal_rune(gedict_t* self, gedict_t* rune) +{ + int newRune = rune->ctf_flag; + int currentRune = self->ctf_flag & CTF_RUNE_MASK; + gedict_t* teammate; + + if (!isCTF()) return 0; + + // Already have a rune + if (self->ctf_flag & CTF_RUNE_MASK) + { + if (newRune >= currentRune) + return 0; + } + + if (!Visible_360(self, rune)) return 0; + + rune->fb.touch_marker = LocateNextMarker(rune->s.v.origin, NULL); + + // Not reachable + if (rune->s.v.origin[2] - rune->fb.touch_marker->s.v.origin[2] > 40) + { + return 0; + } + + if (rune->fb.touch_marker) + { + rune->fb.Z_ = rune->fb.touch_marker->fb.Z_; + } + else + { + return 0; + } + + teammate = IdentifyMostVisibleTeammate(self); + if (teammate != world && !teammate->isBot && !(teammate->ctf_flag & CTF_RUNE_MASK)) + { + // If a human is near and no enemies are, let them take the rune + vec3_t toTeam; + float distance; + VectorSubtract(rune->s.v.origin, teammate->s.v.origin, toTeam); + distance = VectorLength(toTeam); + if (distance < 400 && !(teammate->ctf_flag & CTF_RUNE_MASK) && (self->fb.enemy_dist >= 1500 || self->fb.enemy_dist == 600)) + return 0; + } + + return 1200 + 100 / newRune; +} + +static float goal_defend_marker(gedict_t* self, gedict_t* marker) +{ + gedict_t *flag1, *flag2, *teamFlag, *enemyFlag; + + if (!isCTF()) return 0; + + // Don't try and defend if you are carrying the flag! + if (self->ctf_flag & CTF_FLAG) return 0; + + // Ignore other teams markers. + if (marker->fb.T & MARKER_FLAG1_DEFEND && streq(getteam(self), "blue")) return 0; + if (marker->fb.T & MARKER_FLAG2_DEFEND && streq(getteam(self), "red")) return 0; + + if (VectorDistance(self->s.v.origin, marker->s.v.origin) < 300) return 0; // BAD! + + + flag1 = find(world, FOFCLSN, "item_flag_team1"); + flag2 = find(world, FOFCLSN, "item_flag_team2"); + + if (streq(getteam(self), "red")) + { + teamFlag = flag1; + enemyFlag = flag2; + } + else + { + teamFlag = flag2; + enemyFlag = flag1; + } + + if (teamFlag->cnt == FLAG_AT_BASE) + { + if (enemyFlag->cnt == FLAG_AT_BASE) + { + if (self->fb.skill.ctf_role == FB_CTF_ROLE_DEFEND) return 1200 * (self->fb.firepower / 100.0f); + } + else + { + return 400 + 800 * (self->fb.firepower / 100.0f); + } + } + + return 0; +} + // Pickup functions (TODO) qbool pickup_health0(void) @@ -935,6 +1098,77 @@ static void fb_spawn_quad(gedict_t *ent) } } +static void fb_flag_taken(gedict_t* item, gedict_t* player) +{ + UpdateGoalEntity(item, player); + AssignVirtualGoal(item); +} + +static void fb_rune_taken(gedict_t* item, gedict_t* player) +{ + BotTookMessage(item, player); + UpdateGoalEntity(item, player); + AssignVirtualGoal(item); +} + +static qbool fb_rune_touch(gedict_t* rune, gedict_t* player) +{ + if (player->ctf_flag & CTF_RUNE_MASK) + { + int newRune = rune->ctf_flag; + int currentRune = player->ctf_flag & 15; + if (newRune < currentRune) + { + self = player; + TossRune(); + self = rune; + } + } + + return false; +} + +static void fb_spawn_flag1(gedict_t* ent) +{ + ent->netname = "Flag1"; + + ent->fb.desire = goal_flag; + ent->fb.pickup = pickup_true; + ent->fb.item_taken = fb_flag_taken; + ent->fb.item_respawned = AssignVirtualGoal; +} + +static void fb_spawn_flag2(gedict_t* ent) +{ + ent->netname = "Flag2"; + + ent->fb.desire = goal_flag; + ent->fb.pickup = pickup_true; + ent->fb.item_taken = fb_flag_taken; + ent->fb.item_respawned = AssignVirtualGoal; +} + +static void fb_spawn_rune(gedict_t* ent) +{ + ent->fb.desire = goal_rune; + ent->fb.pickup = pickup_true; + ent->fb.item_touch = fb_rune_touch; + ent->fb.item_taken = fb_rune_taken; + ent->fb.item_respawned = AssignVirtualGoal; +} + +static qbool fb_defense_marker_touch(gedict_t* ent, gedict_t* player) +{ + AssignVirtualGoal(ent); + return false; +} + +static void fb_spawn_defense_marker(gedict_t* ent) +{ + ent->fb.desire = goal_defend_marker; + ent->fb.item_touch = fb_defense_marker_touch; +} + static fb_spawn_t itemSpawnFunctions[] = { { "item_armor1", fb_spawn_armor }, @@ -955,7 +1189,11 @@ static fb_spawn_t itemSpawnFunctions[] = { "weapon_supernailgun", fb_spawn_sng }, { "weapon_grenadelauncher", fb_spawn_gl }, { "weapon_rocketlauncher", fb_spawn_rl }, - { "weapon_lightning", fb_spawn_lg }, }; + { "weapon_lightning", fb_spawn_lg }, + + // CTF + { "item_flag_team1", fb_spawn_flag1}, + { "item_flag_team2", fb_spawn_flag2} }; fb_spawn_t* ItemSpawnFunction(int i) { @@ -1124,9 +1362,33 @@ void BotsPowerupDropped(gedict_t *player, gedict_t *powerup) AssignVirtualGoal(powerup); } +void BotsFlag1Dropped(gedict_t* flag) +{ + VectorSet(flag->s.v.absmin, flag->s.v.origin[0], flag->s.v.origin[1], flag->s.v.origin[2]); + + fb_spawn_flag1(flag); +} + +void BotsFlag2Dropped(gedict_t* flag) +{ + VectorSet(flag->s.v.absmin, flag->s.v.origin[0], flag->s.v.origin[1], flag->s.v.origin[2]); + + fb_spawn_flag2(flag); +} + +void BotsRuneDropped(gedict_t* rune) +{ + fb_spawn_rune(rune); +} + void BotsPowerupTouchedNonPlayer(gedict_t *powerup, gedict_t *touch_ent) { LocateDynamicItem(powerup); } +void BotsSetUpDefenseMarker(gedict_t* marker) +{ + fb_spawn_defense_marker(marker); +} + #endif // BOT_SUPPORT diff --git a/src/bot_loadmap.c b/src/bot_loadmap.c index 477dbe1a..82a4abaf 100644 --- a/src/bot_loadmap.c +++ b/src/bot_loadmap.c @@ -17,6 +17,8 @@ qbool LoadBotRoutingFromFile(void); qbool pickup_true(void); int AddPath(gedict_t *marker, gedict_t *next_marker); void AssignGoalNumbers(void); +void BotsSetUpDefenseMarker(gedict_t* marker); +void BotsSetUpE2M2DischargeMarker(gedict_t* marker); // fixme: also in doors.c #define SECRET_OPEN_ONCE 1 // stays open @@ -38,15 +40,8 @@ static void fb_spawn_button(gedict_t *ent) { AddToQue(ent); - if (ent->s.v.health) - { - //Add_takedamage(ent); - } - else - { - BecomeMarker(ent); - adjust_view_ofs_z(ent); - } + BecomeMarker(ent); + adjust_view_ofs_z(ent); } static void fb_spawn_spawnpoint(gedict_t *ent) @@ -172,6 +167,8 @@ static fb_spawn_t stdSpawnFunctions[] = { "door", fb_spawn_door }, // covers func_door and func_door_secret { "func_button", fb_spawn_button }, { "info_player_deathmatch", fb_spawn_spawnpoint }, + //{ "info_player_team1", fb_spawn_spawnpoint }, // TODO hiipe - these should be in here, but adding them will mean having to + //{ "info_player_team2", fb_spawn_spawnpoint }, // put all ctf map files through a script to correct the numbering. Later... { "info_teleport_destination", fb_spawn_teleport_destination }, { "plat", fb_spawn_plat }, { "train", fb_spawn_simple }, @@ -237,12 +234,28 @@ static void CreateItemMarkers(void) int i = 0; qbool found = false; + // Door markers don't spawn an indicator when doors spawn + if (item->fb.door_entity) SpawnMarkerIndicator(item); + // Don't bother with search if it's already processed if (ProcessedItem(item)) { continue; } + + // Flags are dynamic, so don't create a marker for them! + if (streq(item->classname, "item_flag_team1") || streq(item->classname, "item_flag_team2")) + { + continue; + } + + // TODO hiipe - these dont work properly at the moment, so skip them + if (streq(item->classname, "info_player_team1") || streq(item->classname, "info_player_team2")) + { + continue; + } + // check for item spawn for (i = 0; i < ItemSpawnFunctionCount(); ++i) { @@ -444,6 +457,22 @@ static void CustomiseFrogbotMap(void) } } } + + // Assign goals for defense markers and E2M2 discharge in CTF + if (isCTF()) + { + for (ent = world; (ent = ez_find(ent, "marker"));) + { + if (ent->fb.T & MARKER_FLAG1_DEFEND || ent->fb.T & MARKER_FLAG2_DEFEND) + { + BotsSetUpDefenseMarker(ent); + } + if (/*mapname == "e2m2" &&*/ ent->fb.T & MARKER_E2M2_DISCHARGE) + { + BotsSetUpE2M2DischargeMarker(ent); + } + } + } } void LoadMap(void) @@ -451,7 +480,7 @@ void LoadMap(void) // Need to do this anyway, otherwise teleporters will be broken CreateItemMarkers(); - if (!(isRACE() || isCTF()) && deathmatch) + if ((!isRACE() && deathmatch) || isCTF()) { // If we have a .bot file, use that if (LoadBotRoutingFromFile()) diff --git a/src/bot_movement.c b/src/bot_movement.c index c0654d1b..5421f00e 100644 --- a/src/bot_movement.c +++ b/src/bot_movement.c @@ -434,6 +434,7 @@ void BotSetCommand(gedict_t *self) vec3_t direction; BotPerformRocketJump(self); + BotPerformHook(self); if (cmd_msec) { diff --git a/src/bot_routing.c b/src/bot_routing.c index 41f4d385..86de71ed 100644 --- a/src/bot_routing.c +++ b/src/bot_routing.c @@ -43,15 +43,17 @@ static qbool AvoidTeleport(fb_path_eval_t *eval) return SameTeam(eval->test_marker->fb.arrow_time_setby, eval->player); } -static float EvalPath(fb_path_eval_t *eval, qbool allowRocketJumps, qbool trace_bprint, +static float EvalPath(fb_path_eval_t *eval, qbool allowRocketJumps, qbool allowHook, qbool trace_bprint, float current_goal_time, float current_goal_time_125) { float path_score; float same_dir; vec3_t marker_position; - // don't try and pass through closed doors - if (BotDoorIsClosed(eval->test_marker)) + // don't try and pass through closed doors, unless a button is linked + // TODO hiipe - shouldn't go this way at all - at the moment, bots will get to the door and just stop. + if (BotDoorIsClosed(eval->test_marker) && + ((match_in_progress <= 1 && !k_practice) || !(eval->test_marker->fb.T & MARKER_LOOK_BUTTON))) { return PATH_SCORE_NULL; } @@ -76,6 +78,16 @@ static float EvalPath(fb_path_eval_t *eval, qbool allowRocketJumps, qbool trace_ return PATH_SCORE_NULL; } + if ((eval->description & HOOK) && allowHook) + { + vec3_t m_pos; + + VectorAdd(eval->touch_marker->s.v.absmin, eval->touch_marker->s.v.view_ofs, m_pos); + + // TODO hiipe - work out actual speed + eval->path_time = (VectorDistance(marker_position, m_pos) / (sv_maxspeed * 1.5)); + } + // FIXME: Map specific logic if (DM6DoorClosed(eval)) { @@ -87,6 +99,17 @@ static float EvalPath(fb_path_eval_t *eval, qbool allowRocketJumps, qbool trace_ return PATH_SCORE_NULL; } + if ((eval->description & HOOK) && !allowHook) + { + return PATH_SCORE_NULL; + } + + // These just point to a button, don't need to actually go there! + if (eval->description & LOOK_BUTTON) + { + return PATH_SCORE_NULL; + } + // FIXME: This code gives a bonus to routes carrying the player in the same direction { vec3_t direction_to_marker; @@ -123,9 +146,9 @@ static float EvalPath(fb_path_eval_t *eval, qbool allowRocketJumps, qbool trace_ // Calculate time from marker > goal entity from_marker = eval->test_marker; path_normal = eval->path_normal; - ZoneMarker(from_marker, eval->goalentity_marker, path_normal, allowRocketJumps); + ZoneMarker(from_marker, eval->goalentity_marker, path_normal, allowRocketJumps, allowHook); traveltime = SubZoneArrivalTime(zone_time, middle_marker, eval->goalentity_marker, - allowRocketJumps); + allowRocketJumps, allowHook); total_goal_time = eval->path_time + traveltime; if (total_goal_time > eval->goal_late_time) @@ -147,7 +170,7 @@ static float EvalPath(fb_path_eval_t *eval, qbool allowRocketJumps, qbool trace_ // FIXME: Breaking it up like this was useful for initial testing static void BotRouteEvaluation(qbool be_quiet, float lookahead_time, gedict_t *from_marker, gedict_t *to_marker, qbool rocket_alert, - qbool rocket_jump_routes_allowed, vec3_t player_origin, + qbool rocket_jump_routes_allowed, qbool hook_routes_allowed, vec3_t player_origin, vec3_t player_direction, qbool path_normal, qbool trace_bprint, float current_goal_time, float current_goal_time_125, float goal_late_time, gedict_t *player, float *best_score, @@ -179,7 +202,7 @@ static void BotRouteEvaluation(qbool be_quiet, float lookahead_time, gedict_t *f { float path_score = 0; - path_score = EvalPath(&eval, rocket_jump_routes_allowed, trace_bprint, + path_score = EvalPath(&eval, rocket_jump_routes_allowed, hook_routes_allowed, trace_bprint, current_goal_time, current_goal_time_125); if (path_score > *best_score) { @@ -198,7 +221,7 @@ static void BotRouteEvaluation(qbool be_quiet, float lookahead_time, gedict_t *f void PathScoringLogic(float goal_respawn_time, qbool be_quiet, float lookahead_time, qbool path_normal, vec3_t player_origin, vec3_t player_direction, gedict_t *touch_marker_, gedict_t *goalentity_marker, qbool rocket_alert, - qbool rocket_jump_routes_allowed, qbool trace_bprint, gedict_t *player, + qbool rocket_jump_routes_allowed, qbool hook_routes_allowed, qbool trace_bprint, gedict_t *player, float *best_score, gedict_t **linked_marker_, int *new_path_state, int *new_angle_hint, int *new_rj_frame_delay, float new_rj_angles[2]) { @@ -213,9 +236,9 @@ void PathScoringLogic(float goal_respawn_time, qbool be_quiet, float lookahead_t if (goalentity_marker) { from_marker = touch_marker_; - ZoneMarker(from_marker, goalentity_marker, path_normal, rocket_jump_routes_allowed); + ZoneMarker(from_marker, goalentity_marker, path_normal, rocket_jump_routes_allowed, hook_routes_allowed); traveltime = SubZoneArrivalTime(zone_time, middle_marker, goalentity_marker, - rocket_jump_routes_allowed); + rocket_jump_routes_allowed, hook_routes_allowed); current_goal_time = traveltime; current_goal_time_125 = traveltime + 1.25; @@ -250,7 +273,7 @@ void PathScoringLogic(float goal_respawn_time, qbool be_quiet, float lookahead_t eval.test_marker = touch_marker_; - path_score = EvalPath(&eval, rocket_jump_routes_allowed, trace_bprint, current_goal_time, + path_score = EvalPath(&eval, rocket_jump_routes_allowed, hook_routes_allowed, trace_bprint, current_goal_time, current_goal_time_125); if (path_score > *best_score) { @@ -264,7 +287,7 @@ void PathScoringLogic(float goal_respawn_time, qbool be_quiet, float lookahead_t // Evaluate all paths from touched marker to the goal entity BotRouteEvaluation(be_quiet, lookahead_time, touch_marker_, goalentity_marker, rocket_alert, - rocket_jump_routes_allowed, player_origin, player_direction, path_normal, + rocket_jump_routes_allowed, hook_routes_allowed, player_origin, player_direction, path_normal, trace_bprint, current_goal_time, current_goal_time_125, goal_late_time, player, best_score, linked_marker_, new_path_state, new_angle_hint, new_rj_frame_delay, new_rj_angles); diff --git a/src/bot_world.c b/src/bot_world.c index c3e78a8e..fba68042 100644 --- a/src/bot_world.c +++ b/src/bot_world.c @@ -71,7 +71,7 @@ static qbool VisibilityTest(gedict_t *self, gedict_t *visible_object, float min_ { vec3_t temp; - if (visible_object->s.v.takedamage) + if (visible_object->s.v.takedamage || visible_object->ctf_flag & CTF_RUNE_MASK) { // Can only see invisible objects when they're attacking if ((g_globalvars.time < visible_object->invisible_finished) diff --git a/src/client.c b/src/client.c index df4bdb09..bfb0a971 100644 --- a/src/client.c +++ b/src/client.c @@ -1577,7 +1577,8 @@ void ClientConnect(void) } // qqshka: force damn colors in CTF. - if (isCTF()) + // doing this seems to stop the bots from doing it properly + if (isCTF() && !self->isBot) { int red = 0; // Keeps track of which team and colors to set for new player diff --git a/src/commands.c b/src/commands.c index e4cacf05..3b6bcb5b 100644 --- a/src/commands.c +++ b/src/commands.c @@ -4575,16 +4575,6 @@ void UserMode(float umode) um = "matchless"; // use configs/usermodes/matchless instead of configs/usermodes/ffa in matchless mode } - if (streq(um, "ctf") && bots_enabled() && !sv_invoked) - { - if (bots_enabled()) - { - G_sprint(self, PRINT_HIGH, "Disable bots first with %s\n", redtext("/botcmd disable")); - - return; - } - } - //for 1on1 / 2on2 / 4on4 and ffa commands manipulation //0 - no one, 1 - admins, 2 elected admins too //3 - only real real judges, 4 - elected judges too diff --git a/src/ctf.c b/src/ctf.c index 78f433da..ae292509 100644 --- a/src/ctf.c +++ b/src/ctf.c @@ -32,12 +32,16 @@ #define CARRIER_DEFEND_TIME 4 void DropFlag(gedict_t *flag, qbool tossed); -void PlaceFlag(void); void FlagThink(void); void FlagTouch(void); void SP_item_flag_team1(void); void SP_item_flag_team2(void); +#ifdef BOT_SUPPORT +void BotsFlag1Dropped(gedict_t* ent); +void BotsFlag2Dropped(gedict_t* ent); +#endif + // Allows us to add flags (or other items) to dm maps when ctfing without actually changing bsp void G_CallSpawn(gedict_t *ent); void SpawnCTFItem(char *classname, float x, float y, float z, float angle) @@ -105,6 +109,10 @@ void SP_item_flag_team1(void) } spawn_item_flag(); + +#ifdef BOT_SUPPORT + BotsFlag1Dropped(self); +#endif } void SP_item_flag_team2(void) @@ -120,6 +128,13 @@ void SP_item_flag_team2(void) } spawn_item_flag(); + +#ifdef BOT_SUPPORT + if (bots_enabled()) + { + BotsFlag2Dropped(self); + } +#endif } // would love to know what a ctf wall is :O! @@ -282,6 +297,12 @@ void FlagThink(void) self->cnt2 += 0.1; if (g_globalvars.time > self->super_time) { +#ifdef BOT_SUPPORT + if (bots_enabled()) + { + self->fb.item_taken(self, other); + } +#endif RegenFlag(self); G_bprint(2, "The %s flag has been returned\n", redtext(((int)self->s.v.items & IT_KEY1) ? "BLUE" : "RED")); @@ -439,6 +460,13 @@ void FlagTouch(void) k_nochange = 0; // Set it so it should update scores at next attempt. refresh_plus_scores(); +#ifdef BOT_SUPPORT + if (bots_enabled()) + { + self->fb.item_taken(self, other); + } +#endif + return; } } @@ -475,6 +503,13 @@ void FlagTouch(void) owner->s.v.effects = (int)owner->s.v.effects | EF_FLAG1; } setmodel(self, ""); + +#ifdef BOT_SUPPORT + if (bots_enabled()) + { + self->fb.item_taken(self, other); + } +#endif } void FlagResetOwner(void) diff --git a/src/fb_globals.c b/src/fb_globals.c index bd09d815..9e092e29 100644 --- a/src/fb_globals.c +++ b/src/fb_globals.c @@ -290,6 +290,16 @@ qbool EnemyDefenceless(gedict_t *self) return false; } +qbool EnemyHasFlag(gedict_t* self) +{ + gedict_t* enemy = &g_edicts[self->s.v.enemy]; + if (self->s.v.enemy == 0) + { + return false; + } + return enemy->ctf_flag & CTF_FLAG; +} + gedict_t* FirstZoneMarker(int zone) { return zone_head[zone - 1]; diff --git a/src/maps_map_e2m2.c b/src/maps_map_e2m2.c new file mode 100644 index 00000000..06bb6526 --- /dev/null +++ b/src/maps_map_e2m2.c @@ -0,0 +1,44 @@ +#ifdef BOT_SUPPORT + +#include "g_local.h" + +// FIXME: Globals +extern gedict_t *markers[]; + +static float goal_discharge_marker(gedict_t* self, gedict_t* marker) +{ + // No need to check if we have the lg, as the bot will always pick it up when going to + // discharge. Otherwise the bot will start going for quad / flag then go back after changing goal. + if (self->deaths == 0 && self->team_no == 0) + { + return 99999; + } + + return 0; +} + +static qbool fb_discharge_marker_touch(gedict_t* ent, gedict_t* player) +{ + self->fb.look_object = ent; + AssignVirtualGoal(ent); + return false; +} + +void BotsSetUpE2M2DischargeMarker(gedict_t* marker) +{ + marker->fb.desire = goal_discharge_marker; + marker->fb.item_touch = fb_discharge_marker_touch; +} + + +qbool E2M2DischargeLogic(gedict_t *self) +{ + if (self->fb.touch_marker && self->fb.touch_marker->fb.T & MARKER_E2M2_DISCHARGE && self->deaths == 0 && self->team_no == 0) + { + return true; + } + + return false; +} + +#endif diff --git a/src/marker_load.c b/src/marker_load.c index 3be7d0b7..06f6ecbc 100644 --- a/src/marker_load.c +++ b/src/marker_load.c @@ -49,6 +49,11 @@ const char* EncodeMarkerFlags(int marker_flags) *s++ = '6'; } + if (marker_flags & MARKER_E2M2_DISCHARGE) + { + *s++ = 'd'; + } + if (marker_flags & MARKER_BLOCKED_ON_STATE_TOP) { *s++ = 'b'; @@ -74,6 +79,16 @@ const char* EncodeMarkerFlags(int marker_flags) *s++ = 'n'; } + if (marker_flags & MARKER_FLAG1_DEFEND) + { + *s++ = '1'; + } + + if (marker_flags & MARKER_FLAG2_DEFEND) + { + *s++ = '2'; + } + if (s == buffer) { return "(none)"; @@ -101,6 +116,10 @@ int DecodeMarkerFlagString(const char *s) marker_flags |= MARKER_IS_DM6_DOOR; break; + case 'd': + marker_flags |= MARKER_E2M2_DISCHARGE; + break; + case 'f': marker_flags |= MARKER_FIRE_ON_MATCH_START; break; @@ -120,6 +139,14 @@ int DecodeMarkerFlagString(const char *s) case 'n': marker_flags |= MARKER_NOTOUCH; break; + + case '1': + marker_flags |= MARKER_FLAG1_DEFEND; + break; + + case '2': + marker_flags |= MARKER_FLAG2_DEFEND; + break; } } @@ -146,6 +173,16 @@ const char* EncodeMarkerPathFlags(int path_flags) *s++ = 'r'; } + if (path_flags & HOOK) + { + *s++ = 'h'; + } + + if (path_flags & LOOK_BUTTON) + { + *s++ = 'l'; + } + if (path_flags & JUMP_LEDGE) { *s++ = 'j'; @@ -192,6 +229,14 @@ int DecodeMarkerPathFlagString(const char *s) path_flags |= ROCKET_JUMP; break; + case 'h': + path_flags |= HOOK; + break; + + case 'l': + path_flags |= LOOK_BUTTON; + break; + case 'j': path_flags |= JUMP_LEDGE; break; @@ -318,6 +363,11 @@ void SetMarkerPathFlags(int marker_number, int path_index, int flags) markers[marker_number]->fb.paths[path_index].rj_pitch = 78.25; markers[marker_number]->fb.paths[path_index].rj_yaw = -1; } + + if (flags & LOOK_BUTTON) + { + markers[marker_number]->fb.T |= MARKER_LOOK_BUTTON; + } } void SetMarkerPath(int source_marker, int path_index, int next_marker) @@ -368,10 +418,12 @@ qbool LoadBotRoutingFromFile(void) fileHandle_t file = -1; char lineData[128]; char argument[128]; + char botFileName[128]; // Load bot definition file: frogbots rely on objects spawning // markers, so be aware of alternative .ent files char *entityFile = cvar_string("k_entityfile"); + if (isCTF()) strlcat(entityFile, "_ctf", sizeof(entityFile)); if (!strnull(entityFile)) { file = std_fropen("maps/%s.bot", entityFile); @@ -381,12 +433,14 @@ qbool LoadBotRoutingFromFile(void) } } + strlcpy(botFileName, mapname, sizeof(botFileName)); + if (isCTF()) strlcat(botFileName, "_ctf", sizeof(botFileName)); if (file == -1) { - file = std_fropen("maps/%s.bot", mapname); + file = std_fropen("maps/%s.bot", botFileName); if (file == -1) { - file = std_fropen("bots/maps/%s.bot", mapname); + file = std_fropen("bots/maps/%s.bot", botFileName); } } diff --git a/src/match.c b/src/match.c index 34350b34..a8e4d036 100644 --- a/src/match.c +++ b/src/match.c @@ -2703,7 +2703,8 @@ void PlayerReady(qbool startIdlebot) self->k_teamnum = 0; // force red or blue color if ctf - if (isCTF()) + // doing this seems to stop the bots from doing it properly + if (isCTF() && !self->isBot) { if (streq(getteam(self), "blue")) { diff --git a/src/route_calc.c b/src/route_calc.c index 8f3d85e5..80320a5d 100644 --- a/src/route_calc.c +++ b/src/route_calc.c @@ -52,12 +52,41 @@ static void TravelTimeForPath(gedict_t *m, int i) player_speed = sv_maxspeed * (1 + max(0, DotProduct(distance, hor_distance))); // FIXME: RJ time is guideline, but we can do better than this? - m->fb.paths[i].time = 100000; + m->fb.paths[i].time = 1000000; m->fb.paths[i].rj_time = (total_distance / player_speed); return; } + if (m->fb.paths[i].flags & HOOK) + { + vec3_t m_P_pos; + vec3_t distance; + vec3_t hor_distance; + float player_speed; + float total_distance; + + VectorAdd(m_P->s.v.absmin, m_P->s.v.view_ofs, m_P_pos); + VectorSubtract(m_P_pos, m_pos, distance); + VectorCopy(distance, hor_distance); + total_distance = VectorNormalize(distance); + VectorNormalize(hor_distance); + player_speed = sv_maxspeed * (1 + max(0, DotProduct(distance, hor_distance))); + + // TODO hiipe - work out time correctly! + m->fb.paths[i].time = /*1000000*/(total_distance / player_speed); + //m->fb.paths[i].hook_time = (total_distance / player_speed); + + return; + } + + // Just points to a button, can't go this way! + if (m->fb.paths[i].flags & LOOK_BUTTON) + { + m->fb.paths[i].hook_time = m->fb.paths[i].rj_time = m->fb.paths[i].time = 1000000; + return; + } + if (m->fb.paths[i].flags & JUMP_LEDGE) { m->fb.paths[i].flags |= NO_DODGE; @@ -76,12 +105,12 @@ static void TravelTimeForPath(gedict_t *m, int i) if ((m->fb.T & T_WATER) || (m_P->fb.T & T_WATER)) { m->fb.paths[i].flags |= WATER_PATH; - m->fb.paths[i].rj_time = m->fb.paths[i].time = (VectorDistance(m_P_pos, m_pos) + m->fb.paths[i].hook_time = m->fb.paths[i].rj_time = m->fb.paths[i].time = (VectorDistance(m_P_pos, m_pos) / sv_maxwaterspeed); } else { - m->fb.paths[i].rj_time = m->fb.paths[i].time = (VectorDistance(m_P_pos, m_pos) + m->fb.paths[i].hook_time = m->fb.paths[i].rj_time = m->fb.paths[i].time = (VectorDistance(m_P_pos, m_pos) / sv_maxspeed); } } @@ -131,6 +160,17 @@ static qbool IdentifyFastestSubzoneRoute(gedict_t *m, fb_path_t *path) // ... not for now... } + if (m_D & HOOK) + { + // Hook this link, then standard path. Prefer hook to RJ + if (sub->hook_time > (path->hook_time + next_sub->hook_time)) + { + no_change = false; + sub->hook_time = path->hook_time + next_sub->hook_time; + sub->next_marker_hook = path->next_marker; + } + } + // If it's faster to walk than RJ, do that instead if (sub->rj_time > sub->time) { @@ -138,6 +178,12 @@ static qbool IdentifyFastestSubzoneRoute(gedict_t *m, fb_path_t *path) sub->rj_time = sub->time; sub->next_marker_rj = sub->next_marker; } + if (sub->hook_time > sub->time) + { + no_change = false; + sub->hook_time = sub->time; + sub->next_marker_hook = sub->next_marker; + } } } @@ -175,6 +221,13 @@ static qbool IdentifyFastestGoalRoute(gedict_t *m, fb_path_t *path) goal->rj_time = path->rj_time + next_goal->rj_time; no_change = false; } + + if (goal->hook_time > (path->hook_time + next_goal->hook_time)) + { + goal->next_marker_hook = next_goal->next_marker_hook; + goal->hook_time = path->hook_time + next_goal->hook_time; + no_change = false; + } } return no_change; @@ -219,6 +272,14 @@ static qbool IdentifyFastestZoneRoute(gedict_t *m, fb_path_t *path) no_change = false; } + if (zone->hook_time > (path->hook_time + next_zone->hook_time)) + { + zone->hook_time = path->hook_time + next_zone->hook_time; + zone->marker_hook = next_zone->marker_hook; + zone->next_hook = path->next_marker; + + no_change = false; + } if (zone->rj_time > zone->time) { @@ -226,6 +287,14 @@ static qbool IdentifyFastestZoneRoute(gedict_t *m, fb_path_t *path) zone->marker_rj = zone->marker; zone->next_rj = zone->next; + no_change = false; + } + if (zone->hook_time > zone->time) + { + zone->hook_time = zone->time; + zone->marker_hook = zone->marker; + zone->next_hook = zone->next; + no_change = false; } } @@ -526,7 +595,7 @@ static void Calc_G_time_11(void) } from_marker = m; - ZoneMarker(m, m_zone, path_normal, false); + ZoneMarker(m, m_zone, path_normal, false, false); if ((middle_marker != dropper) && (middle_marker != m)) { gedict_t *runaway_dest = middle_marker; @@ -538,10 +607,10 @@ static void Calc_G_time_11(void) do { from_marker = prev_marker = next_marker; - next_marker = ZonePathMarker(from_marker, runaway_dest, path_normal, false); + next_marker = ZonePathMarker(from_marker, runaway_dest, path_normal, false, false); from_marker = m; - ZoneMarker(m, next_marker, path_normal, false); - traveltime = SubZoneArrivalTime(zone_time, middle_marker, next_marker, false); + ZoneMarker(m, next_marker, path_normal, false, false); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, next_marker, false, false); if (traveltime >= min_traveltime) { if (strneq(next_marker->classname, "trigger_teleport")) @@ -592,7 +661,7 @@ static void Calc_G_time_12(void) if (runaway_dest != m) { from_marker = m; - traveltime = SubZoneArrivalTime(zone_time, middle_marker, runaway_dest, false); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, runaway_dest, false, false); if (traveltime < 1000000) { runaway_score = runaway_time = traveltime; @@ -611,7 +680,7 @@ static void Calc_G_time_12(void) { from_marker = m; traveltime = SubZoneArrivalTime(zone_time, middle_marker, next_marker, - false); + false, false); if (traveltime >= min_traveltime) { if (strneq(next_marker->classname, "trigger_teleport")) @@ -711,8 +780,8 @@ void InitialiseMarkerRoutes(void) { if (!m->fb.goals[j].next_marker) { - m->fb.goals[j].rj_time = m->fb.goals[j].time = 1000000; - m->fb.goals[j].next_marker_rj = m->fb.goals[j].next_marker = dropper; + m->fb.goals[j].hook_time = m->fb.goals[j].rj_time = m->fb.goals[j].time = 1000000; + m->fb.goals[j].next_marker_hook = m->fb.goals[j].next_marker_rj = m->fb.goals[j].next_marker = dropper; } } @@ -720,9 +789,9 @@ void InitialiseMarkerRoutes(void) { if (!m->fb.zones[j].marker) { - m->fb.zones[j].rj_time = m->fb.zones[j].time = m->fb.zones[j].reverse_time = + m->fb.zones[j].hook_time = m->fb.zones[j].rj_time = m->fb.zones[j].time = m->fb.zones[j].reverse_time = m->fb.zones[j].from_time = 1000000; - m->fb.zones[j].marker_rj = m->fb.zones[j].marker = m->fb.zones[j].reverse_marker = + m->fb.zones[j].marker_hook = m->fb.zones[j].marker_rj = m->fb.zones[j].marker = m->fb.zones[j].reverse_marker = dropper; } @@ -733,7 +802,7 @@ void InitialiseMarkerRoutes(void) { if (m->fb.S_ != j) { - m->fb.subzones[j].rj_time = m->fb.subzones[j].time = 1000000; + m->fb.subzones[j].hook_time = m->fb.subzones[j].rj_time = m->fb.subzones[j].time = 1000000; } } } diff --git a/src/route_fields.c b/src/route_fields.c index 9324705e..827de3bb 100644 --- a/src/route_fields.c +++ b/src/route_fields.c @@ -27,7 +27,7 @@ void SetGoalForMarker(int goal, gedict_t *marker) return; } - marker->fb.goals[goal - 1].next_marker_rj = marker->fb.goals[goal - 1].next_marker = marker; + marker->fb.goals[goal - 1].next_marker_hook = marker->fb.goals[goal - 1].next_marker_rj = marker->fb.goals[goal - 1].next_marker = marker; marker->fb.G_ = goal; } @@ -74,7 +74,7 @@ void SetZone(int zone, int marker_number) z = &marker->fb.zones[zone]; marker->fb.S_ = subzone_indexes[zone]++; - z->marker_rj = z->next_rj = z->marker = z->reverse_marker = z->next = z->reverse_next = marker; + z->marker_hook = z->next_hook = z->marker_rj = z->next_rj = z->marker = z->reverse_marker = z->next = z->reverse_next = marker; marker->fb.Z_ = zone + 1; AddZoneMarker(marker); @@ -212,6 +212,7 @@ void RemovePath(gedict_t *source, int path_number) source->fb.paths[path_number].next_marker = NULL; source->fb.paths[path_number].time = 0; source->fb.paths[path_number].rj_time = 0; + source->fb.paths[path_number].hook_time = 0; } int AddPath(gedict_t *source, gedict_t *next) @@ -243,6 +244,7 @@ int AddPath(gedict_t *source, gedict_t *next) source->fb.paths[place].flags = 0; source->fb.paths[place].time = 0; source->fb.paths[place].rj_time = 0; + source->fb.paths[place].hook_time = 0; } return place; diff --git a/src/route_lookup.c b/src/route_lookup.c index d7fe89d4..aa1ac249 100644 --- a/src/route_lookup.c +++ b/src/route_lookup.c @@ -11,9 +11,13 @@ #include "g_local.h" float SubZoneArrivalTime(float zone_time, gedict_t *middle_marker, gedict_t *from_marker, - qbool rl_routes) + qbool rl_routes, qbool hook_routes) { - if (rl_routes) + if (hook_routes) + { + return (zone_time + middle_marker->fb.subzones[from_marker->fb.S_].hook_time); + } + else if (rl_routes) { return (zone_time + middle_marker->fb.subzones[from_marker->fb.S_].rj_time); } @@ -46,7 +50,7 @@ float SightFromTime(gedict_t *from_marker, gedict_t *to_marker) to_marker->fb.zones[from_marker->fb.Z_ - 1].sight_from_time : 0.0f); } -void ZoneMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, qbool rl_jump_routes) +void ZoneMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, qbool rl_jump_routes, qbool hook_routes) { fb_zone_t *zone; @@ -68,6 +72,9 @@ void ZoneMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, q { middle_marker = rl_jump_routes ? zone->marker_rj : zone->marker; zone_time = rl_jump_routes ? zone->rj_time : zone->time; + + middle_marker = hook_routes ? zone->marker_hook : zone->marker; + zone_time = hook_routes ? zone->hook_time : zone->time; } else { @@ -77,7 +84,7 @@ void ZoneMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, q } gedict_t* ZonePathMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_normal, - qbool rl_jump_routes) + qbool rl_jump_routes, qbool hook_routes) { if ((from_marker == NULL) || (to_marker == NULL) || (to_marker->fb.Z_ == 0)) { @@ -91,6 +98,11 @@ gedict_t* ZonePathMarker(gedict_t *from_marker, gedict_t *to_marker, qbool path_ return from_marker->fb.zones[to_marker->fb.Z_ - 1].next_rj; } + if (hook_routes) + { + return from_marker->fb.zones[to_marker->fb.Z_ - 1].next_hook; + } + return from_marker->fb.zones[to_marker->fb.Z_ - 1].next; } @@ -133,7 +145,7 @@ gedict_t* SightMarker(gedict_t *from_marker, gedict_t *to_marker, float max_dist if (g_globalvars.trace_fraction == 1) { // - traveltime = SubZoneArrivalTime(zone_time, middle_marker, marker_, false); + traveltime = SubZoneArrivalTime(zone_time, middle_marker, marker_, false, false); if (look_traveltime > traveltime) { // Teleports don't count diff --git a/src/runes.c b/src/runes.c index d50c88b5..7126386d 100644 --- a/src/runes.c +++ b/src/runes.c @@ -9,6 +9,7 @@ void RuneRespawn(void); void RuneTouch(void); void RuneResetOwner(void); char* GetRuneSpawnName(void); +void BotsRuneDropped(gedict_t* rune); void DoDropRune(int rune, qbool on_respawn) { @@ -67,18 +68,22 @@ void DoDropRune(int rune, qbool on_respawn) if (rune & CTF_RUNE_RES) { setmodel(item, "progs/end1.mdl"); + item->tp_flags = it_rune1; } else if (rune & CTF_RUNE_STR) { setmodel(item, "progs/end2.mdl"); + item->tp_flags = it_rune2; } else if (rune & CTF_RUNE_HST) { setmodel(item, "progs/end3.mdl"); + item->tp_flags = it_rune3; } else if (rune & CTF_RUNE_RGN) { setmodel(item, "progs/end4.mdl"); + item->tp_flags = it_rune4; } setsize(item, -16, -16, 0, 16, 16, 56); @@ -91,6 +96,13 @@ void DoDropRune(int rune, qbool on_respawn) { sound(item, CHAN_VOICE, "items/itembk2.wav", 1, ATTN_NORM); // play respawn sound } + +#ifdef BOT_SUPPORT + if (bots_enabled()) + { + BotsRuneDropped(item); + } +#endif } void DoTossRune(int rune) @@ -124,18 +136,22 @@ void DoTossRune(int rune) if (rune & CTF_RUNE_RES) { setmodel(item, "progs/end1.mdl"); + item->tp_flags = it_rune1; } else if (rune & CTF_RUNE_STR) { setmodel(item, "progs/end2.mdl"); + item->tp_flags = it_rune2; } else if (rune & CTF_RUNE_HST) { setmodel(item, "progs/end3.mdl"); + item->tp_flags = it_rune3; } else if (rune & CTF_RUNE_RGN) { setmodel(item, "progs/end4.mdl"); + item->tp_flags = it_rune4; } setorigin(item, self->s.v.origin[0], self->s.v.origin[1], self->s.v.origin[2] - 24); @@ -144,6 +160,13 @@ void DoTossRune(int rune) item->touch = (func_t) RuneTouch; item->s.v.nextthink = g_globalvars.time + 0.75; item->think = (func_t) RuneResetOwner; + +#ifdef BOT_SUPPORT + if (bots_enabled()) + { + BotsRuneDropped(item); + } +#endif } void DropRune(void) @@ -272,6 +295,13 @@ void RuneTouch(void) self->s.v.nextthink = g_globalvars.time + 90; } +#ifdef BOT_SUPPORT + if (bots_enabled() && other->isBot) + { + self->fb.item_touch(self, other); + } +#endif + if (other->ctf_flag & CTF_RUNE_MASK) { if (g_globalvars.time > other->rune_notify_time) @@ -317,6 +347,15 @@ void RuneTouch(void) sound(other, CHAN_ITEM, "weapons/lock4.wav", 1, ATTN_NORM); stuffcmd(other, "bf\n"); ent_remove(self); + + TeamplayEventItemTaken(other, self); + +#ifdef BOT_SUPPORT + if (bots_enabled() && other->isBot) + { + self->fb.item_taken(self, other); + } +#endif } char* GetRuneSpawnName(void) diff --git a/src/teamplay.c b/src/teamplay.c index 041fafa0..00348a2b 100644 --- a/src/teamplay.c +++ b/src/teamplay.c @@ -46,10 +46,10 @@ #define TP_NAME_RING "{&cff0ring&cfff}" #define TP_NAME_SUIT "suit" #define TP_NAME_FLAG "flag" -#define TP_NAME_RUNE1 "{&c0f0resistance&cfff}" -#define TP_NAME_RUNE2 "{&cf00strength&cfff}" -#define TP_NAME_RUNE3 "{&cff0haste&cfff}" -#define TP_NAME_RUNE4 "{&c0ffregeneration&cfff}" +#define TP_NAME_RUNE1 "{&c0f0res&cfff}" +#define TP_NAME_RUNE2 "{&cf00str&cfff}" +#define TP_NAME_RUNE3 "{&cff0hst&cfff}" +#define TP_NAME_RUNE4 "{&c0ffrgn&cfff}" #define TP_NAME_ARMOR "armor" #define TP_SEPARATOR "/" #define TP_NAME_HEALTH "health" @@ -199,6 +199,25 @@ void TeamplayEventItemTaken(gedict_t *client, gedict_t *item) { client->tp.took.item = (item->healamount >= 100 ? it_mh : it_health); } + else if (streq(item->classname, "rune")) + { + if (item->ctf_flag & CTF_RUNE_RES) + { + client->tp.took.item = it_rune1; + } + else if (item->ctf_flag & CTF_RUNE_STR) + { + client->tp.took.item = it_rune2; + } + else if (item->ctf_flag & CTF_RUNE_HST) + { + client->tp.took.item = it_rune3; + } + else if (item->ctf_flag & CTF_RUNE_RGN) + { + client->tp.took.item = it_rune4; + } + } else { return; @@ -233,6 +252,16 @@ static qbool HAVE_POWERUP(gedict_t *client) return (client && ((int)client->s.v.items & (IT_QUAD | IT_INVULNERABILITY | IT_INVISIBILITY))); } +static qbool HAVE_FLAG(gedict_t* client) +{ + return client->ctf_flag & CTF_FLAG; +} + +static qbool HAVE_RUNE(gedict_t* client) +{ + return client->ctf_flag & CTF_RUNE_MASK; +} + static qbool HAVE_RING(gedict_t *client) { return (client && ((int)client->s.v.items & IT_INVISIBILITY)); @@ -514,6 +543,28 @@ static char* PowerupText(gedict_t *client) return buffer; } +static char* CTFItemText(gedict_t* client) +{ + static char buffer[128]; + + buffer[0] = '\0'; + if (HAVE_FLAG(client)) + { + if (streq(getteam(client), "red")) strlcat(buffer, "{&c57fFLAG&cfff}", sizeof(buffer)); + if (streq(getteam(client), "blue")) strlcat(buffer, "{&cf00FLAG&cfff}", sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + if (HAVE_RUNE(client)) + { + if (client->ctf_flag & CTF_RUNE_RES) strlcat(buffer, TP_NAME_RUNE1, sizeof(buffer)); + if (client->ctf_flag & CTF_RUNE_STR) strlcat(buffer, TP_NAME_RUNE2, sizeof(buffer)); + if (client->ctf_flag & CTF_RUNE_HST) strlcat(buffer, TP_NAME_RUNE3, sizeof(buffer)); + if (client->ctf_flag & CTF_RUNE_RGN) strlcat(buffer, TP_NAME_RUNE4, sizeof(buffer)); + } + + return buffer; +} + static void TeamplayMM2(gedict_t *client, char *text) { extern qbool ClientSay(qbool isTeamSay); @@ -713,20 +764,38 @@ static void TeamplayReportTaken(gedict_t *client) } else if ((NEED(needFlags, it_health) || NEED(needFlags, it_armor) || NEED(needFlags, it_rl) || NEED(needFlags, it_lg) || NEED(needFlags, it_rockets) - || NEED(needFlags, it_cells)) && HAVE_POWERUP(client)) + || NEED(needFlags, it_cells))) { // Note that we check if you are holding powerup. This is because TOOK remembers for 15 seconds. // So a case could arise where you took quad then died less than 15 seconds later, and you'd be reporting "team need %u" (because $colored_powerups would be empty) strlcpy(message, "{&c0b0team&cfff} ", sizeof(message)); - strlcat(message, PowerupText(client), sizeof(message)); - strlcat(message, " need ", sizeof(message)); + if (HAVE_POWERUP(client)) + { + strlcat(message, PowerupText(client), sizeof(message)); + strlcat(message, " ", sizeof(message)); + } + if (isCTF() && (HAVE_FLAG(client) || HAVE_RUNE(client))) + { + strlcat(message, CTFItemText(client), sizeof(message)); + strlcat(message, " ", sizeof(message)); + } + strlcat(message, "need ", sizeof(message)); strlcat(message, TeamplayNeedText(needFlags), sizeof(message)); } else if (HAVE_QUAD(client) || HAVE_RING(client)) { // notice we can't send this check to tp_msgenemypwr, because if enemy with powerup is in your view, tp_enemypwr reports enemypwr first, but in this function you want to report TEAM powerup. strlcpy(message, "{&c0b0team&cfff} ", sizeof(message)); - strlcat(message, PowerupText(client), sizeof(message)); + if (HAVE_POWERUP(client)) + { + strlcat(message, PowerupText(client), sizeof(message)); + strlcat(message, " ", sizeof(message)); + } + if (isCTF() && (HAVE_FLAG(client) || HAVE_RUNE(client))) + { + strlcat(message, CTFItemText(client), sizeof(message)); + strlcat(message, " ", sizeof(message)); + } } else { // In this case, you took quad or ring and died before 15 secs later. So just report what you need, nothing about powerups. @@ -744,6 +813,12 @@ static void TeamplayReportTaken(gedict_t *client) strlcat(message, " ", sizeof(message)); } + if (isCTF() && (HAVE_FLAG(client) || HAVE_RUNE(client))) + { + strlcpy(message, CTFItemText(client), sizeof(message)); + strlcat(message, " ", sizeof(message)); + } + if (TookSpecific(client, it_pack, IT_ROCKET_LAUNCHER)) { strlcat(message, "took " TP_NAME_RL_PACK, sizeof(message)); @@ -817,6 +892,12 @@ static void TeamplayReportPersonalStatus(gedict_t *client) strlcpy(buffer, PowerupText(client), sizeof(buffer)); } + if (isCTF() && (HAVE_FLAG(client) || HAVE_RUNE(client))) + { + strlcpy(buffer, CTFItemText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + strlcat(buffer, va("%s/%d ", ColoredArmor(client), (int)max(0, client->s.v.health)), sizeof(buffer)); if (HAVE_RL(client) && HAVE_LG(client)) @@ -875,9 +956,20 @@ static void TeamplayAreaSecure(gedict_t *client) return; } + if (HAVE_POWERUP(client)) + { + strlcpy(buffer, PowerupText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + if (isCTF() && (HAVE_RUNE(client) || HAVE_RUNE(client))) + { + strlcpy(buffer, CTFItemText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + // TODO: if pointing at enemy, degrade to TP_EnemyPowerup or no-op - strlcpy(buffer, "{&c0b0safe&cfff} {&c0b0[&cfff}{", sizeof(buffer)); + strlcat(buffer, "{&c0b0safe&cfff} {&c0b0[&cfff}{", sizeof(buffer)); strlcat(buffer, LocationName(PASSVEC3(client->s.v.origin)), sizeof(buffer)); strlcat(buffer, "}{&c0b0]&cfff} ", sizeof(buffer)); if (have_armor) @@ -912,7 +1004,16 @@ static void TeamplayAreaHelp(gedict_t *client) char buffer[128]; buffer[0] = '\0'; - strlcpy(buffer, PowerupText(client), sizeof(buffer)); + if (HAVE_POWERUP(client)) + { + strlcpy(buffer, PowerupText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + if (isCTF() && (HAVE_RUNE(client) || HAVE_RUNE(client))) + { + strlcpy(buffer, CTFItemText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } strlcat(buffer, "{&cff0help&cfff} {&cff0[&cfff}{", sizeof(buffer)); strlcat(buffer, LocationName(PASSVEC3(client->s.v.origin)), sizeof(buffer)); strlcat(buffer, va("}{&cff0]&cfff} {%d}", client->tp.enemy_count), sizeof(buffer)); @@ -1063,6 +1164,12 @@ static void TeamplayReportNeeds(gedict_t *client) strlcat(buffer, " ", sizeof(buffer)); } + if (isCTF() && (HAVE_FLAG(client) || HAVE_RUNE(client))) + { + strlcat(buffer, CTFItemText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + strlcat(buffer, "need ", sizeof(buffer)); strlcat(buffer, TeamplayNeedText(needFlags), sizeof(buffer)); strlcat(buffer, " \20{", sizeof(buffer)); @@ -1195,8 +1302,17 @@ static void TeamplayEnemyPowerup(gedict_t *client) } strlcpy(buffer, "{&c0b0team&cfff} ", sizeof(buffer)); - strlcat(buffer, PowerupText(client), sizeof(buffer)); - strlcat(buffer, " ", sizeof(buffer)); + + if (HAVE_POWERUP(client)) + { + strlcpy(buffer, PowerupText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + if (isCTF() && (HAVE_RUNE(client) || HAVE_RUNE(client))) + { + strlcpy(buffer, CTFItemText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } } else if (point && point->ct == ctPlayer && SameTeam(point, client)) { @@ -1440,6 +1556,12 @@ static void TeamplayBasicCommand(gedict_t *client, char *text) strlcat(buffer, " ", sizeof(buffer)); } + if (isCTF() && (HAVE_FLAG(client) || HAVE_RUNE(client))) + { + strlcpy(buffer, CTFItemText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + strlcat(buffer, text, sizeof(buffer)); strlcat(buffer, " \20{", sizeof(buffer)); strlcat(buffer, LocationName(PASSVEC3(client->s.v.origin)), sizeof(buffer)); @@ -1447,6 +1569,40 @@ static void TeamplayBasicCommand(gedict_t *client, char *text) TeamplayMM2(client, buffer); } +static void TeamplayTossingRune(gedict_t* client) +{ + char buffer[128]; + const char* rune = ""; + + if (client->ctf_flag & CTF_RUNE_RES) rune = TP_NAME_RUNE1; + if (client->ctf_flag & CTF_RUNE_STR) rune = TP_NAME_RUNE2; + if (client->ctf_flag & CTF_RUNE_HST) rune = TP_NAME_RUNE3; + if (client->ctf_flag & CTF_RUNE_RGN) rune = TP_NAME_RUNE4; + + + buffer[0] = '\0'; + + if (HAVE_POWERUP(client)) + { + strlcpy(buffer, PowerupText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + + if (isCTF() && (HAVE_FLAG(client) || HAVE_RUNE(client))) + { + strlcpy(buffer, CTFItemText(client), sizeof(buffer)); + strlcat(buffer, " ", sizeof(buffer)); + } + + strlcat(buffer, "tossed ", sizeof(buffer)); + strlcat(buffer, rune, sizeof(buffer)); + strlcat(buffer, " \20{", sizeof(buffer)); + strlcat(buffer, LocationName(PASSVEC3(client->s.v.origin)), sizeof(buffer)); + strlcat(buffer, "}\21", sizeof(buffer)); + + TeamplayMM2(client, buffer); +} + #define TEAMPLAY_BASIC(FunctionName, Text) static void FunctionName(gedict_t* client) { TeamplayBasicCommand(client, Text); } // Cmd_AddCommand ("tp_msgyesok", TP_Msg_YesOk_f); @@ -1466,6 +1622,11 @@ TEAMPLAY_BASIC(TeamplayTrick, "trick") // Cmd_AddCommand ("tp_msgcoming", TP_Msg_Coming_f); TEAMPLAY_BASIC(TeamplayComing, "coming") +TEAMPLAY_BASIC(TeamplayTossingFlag, "tossed " TP_NAME_FLAG) +TEAMPLAY_BASIC(TeamplayBaseSafe, "{&c0f0base safe&cfff}") +TEAMPLAY_BASIC(TeamplayBaseNotSafe, "{&cf00base not safe&cfff}") +TEAMPLAY_BASIC(TeamplayGoingForFlag, "{&c5afgoing for flag&cfff}") + void TeamplayDeathEvent(gedict_t *client) { VectorCopy(client->s.v.origin, client->tp.death_location); @@ -1664,7 +1825,12 @@ static teamplay_message_t messages[] = { "need", "report needs", TeamplayReportNeeds }, { "report", "report status", TeamplayReportPersonalStatus }, { "took", "item taken", TeamplayReportTaken }, - { "point", "player/item point", TeamplayPoint } + { "point", "player/item point", TeamplayPoint }, + { "tossflag", "tossing flag", TeamplayTossingFlag }, + { "tossrune", "tossing rune", TeamplayTossingRune}, + { "basesafe", "base safe", TeamplayBaseSafe}, + { "basenotsafe", "base not safe", TeamplayBaseNotSafe}, + { "goingflag", "going for flag", TeamplayGoingForFlag} }; qbool TeamplayMessageByName(gedict_t *client, const char *message)